| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447 |
- using System;
- using System.Collections.Generic;
- using System.Threading;
- using System.Threading.Tasks;
- using Avalonia;
- using Avalonia.Animation;
- using Avalonia.Animation.Easings;
- using Avalonia.Media;
- using Avalonia.Styling;
- namespace ControlCatalog.Pages.Transitions;
- /// <summary>
- /// Transitions between two pages with a card-stack effect:
- /// the top page moves/rotates away while the next page scales up underneath.
- /// </summary>
- public class CardStackPageTransition : PageSlide
- {
- private const double ViewportLiftScale = 0.03;
- private const double ViewportPromotionScale = 0.02;
- private const double ViewportDepthOpacityFalloff = 0.08;
- private const double SidePeekAngle = 4.0;
- private const double FarPeekAngle = 7.0;
- /// <summary>
- /// Initializes a new instance of the <see cref="CardStackPageTransition"/> class.
- /// </summary>
- public CardStackPageTransition()
- {
- }
- /// <summary>
- /// Initializes a new instance of the <see cref="CardStackPageTransition"/> class.
- /// </summary>
- /// <param name="duration">The duration of the animation.</param>
- /// <param name="orientation">The axis on which the animation should occur.</param>
- public CardStackPageTransition(TimeSpan duration, PageSlide.SlideAxis orientation = PageSlide.SlideAxis.Horizontal)
- : base(duration, orientation)
- {
- }
- /// <summary>
- /// Gets or sets the maximum rotation angle (degrees) applied to the top card.
- /// </summary>
- public double MaxSwipeAngle { get; set; } = 15.0;
- /// <summary>
- /// Gets or sets the scale reduction applied to the back card (0.05 = 5%).
- /// </summary>
- public double BackCardScale { get; set; } = 0.05;
- /// <summary>
- /// Gets or sets the vertical offset (pixels) applied to the back card.
- /// </summary>
- public double BackCardOffset { get; set; } = 0.0;
- /// <inheritdoc />
- public override async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
- {
- if (cancellationToken.IsCancellationRequested)
- {
- return;
- }
- var tasks = new List<Task>();
- var parent = GetVisualParent(from, to);
- var distance = Orientation == PageSlide.SlideAxis.Horizontal ? parent.Bounds.Width : parent.Bounds.Height;
- var translateProperty = Orientation == PageSlide.SlideAxis.Horizontal ? TranslateTransform.XProperty : TranslateTransform.YProperty;
- var rotationTarget = Orientation == PageSlide.SlideAxis.Horizontal ? (forward ? -MaxSwipeAngle : MaxSwipeAngle) : 0.0;
- var startScale = 1.0 - BackCardScale;
- if (from != null)
- {
- var (rotate, translate) = EnsureTopTransforms(from);
- rotate.Angle = 0;
- translate.X = 0;
- translate.Y = 0;
- from.Opacity = 1;
- from.ZIndex = 1;
- var animation = new Animation
- {
- Easing = SlideOutEasing,
- Duration = Duration,
- FillMode = FillMode,
- Children =
- {
- new KeyFrame
- {
- Setters =
- {
- new Setter(translateProperty, 0d),
- new Setter(RotateTransform.AngleProperty, 0d)
- },
- Cue = new Cue(0d)
- },
- new KeyFrame
- {
- Setters =
- {
- new Setter(translateProperty, forward ? -distance : distance),
- new Setter(RotateTransform.AngleProperty, rotationTarget)
- },
- Cue = new Cue(1d)
- }
- }
- };
- tasks.Add(animation.RunAsync(from, cancellationToken));
- }
- if (to != null)
- {
- var (scale, translate) = EnsureBackTransforms(to);
- scale.ScaleX = startScale;
- scale.ScaleY = startScale;
- translate.X = 0;
- translate.Y = BackCardOffset;
- to.IsVisible = true;
- to.Opacity = 1;
- to.ZIndex = 0;
- var animation = new Animation
- {
- Easing = SlideInEasing,
- Duration = Duration,
- FillMode = FillMode,
- Children =
- {
- new KeyFrame
- {
- Setters =
- {
- new Setter(ScaleTransform.ScaleXProperty, startScale),
- new Setter(ScaleTransform.ScaleYProperty, startScale),
- new Setter(TranslateTransform.YProperty, BackCardOffset)
- },
- Cue = new Cue(0d)
- },
- new KeyFrame
- {
- Setters =
- {
- new Setter(ScaleTransform.ScaleXProperty, 1d),
- new Setter(ScaleTransform.ScaleYProperty, 1d),
- new Setter(TranslateTransform.YProperty, 0d)
- },
- Cue = new Cue(1d)
- }
- }
- };
- tasks.Add(animation.RunAsync(to, cancellationToken));
- }
- await Task.WhenAll(tasks);
- if (from != null && !cancellationToken.IsCancellationRequested)
- {
- from.IsVisible = false;
- }
- if (!cancellationToken.IsCancellationRequested && to != null)
- {
- var (scale, translate) = EnsureBackTransforms(to);
- scale.ScaleX = 1;
- scale.ScaleY = 1;
- translate.X = 0;
- translate.Y = 0;
- }
- }
- /// <inheritdoc />
- public override void Update(
- double progress,
- Visual? from,
- Visual? to,
- bool forward,
- double pageLength,
- IReadOnlyList<PageTransitionItem> visibleItems)
- {
- if (visibleItems.Count > 0)
- {
- UpdateVisibleItems(progress, from, to, forward, pageLength, visibleItems);
- return;
- }
- if (from is null && to is null)
- return;
- var parent = GetVisualParent(from, to);
- var size = parent.Bounds.Size;
- var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal;
- var distance = pageLength > 0
- ? pageLength
- : (isHorizontal ? size.Width : size.Height);
- var rotationTarget = isHorizontal ? (forward ? -MaxSwipeAngle : MaxSwipeAngle) : 0.0;
- var startScale = 1.0 - BackCardScale;
- if (from != null)
- {
- var (rotate, translate) = EnsureTopTransforms(from);
- if (isHorizontal)
- {
- translate.X = forward ? -distance * progress : distance * progress;
- translate.Y = 0;
- }
- else
- {
- translate.X = 0;
- translate.Y = forward ? -distance * progress : distance * progress;
- }
- rotate.Angle = rotationTarget * progress;
- from.IsVisible = true;
- from.Opacity = 1;
- from.ZIndex = 1;
- }
- if (to != null)
- {
- var (scale, translate) = EnsureBackTransforms(to);
- var currentScale = startScale + (1.0 - startScale) * progress;
- var currentOffset = BackCardOffset * (1.0 - progress);
- scale.ScaleX = currentScale;
- scale.ScaleY = currentScale;
- if (isHorizontal)
- {
- translate.X = 0;
- translate.Y = currentOffset;
- }
- else
- {
- translate.X = currentOffset;
- translate.Y = 0;
- }
- to.IsVisible = true;
- to.Opacity = 1;
- to.ZIndex = 0;
- }
- }
- /// <inheritdoc />
- public override void Reset(Visual visual)
- {
- visual.RenderTransform = null;
- visual.RenderTransformOrigin = default;
- visual.Opacity = 1;
- visual.ZIndex = 0;
- }
- private void UpdateVisibleItems(
- double progress,
- Visual? from,
- Visual? to,
- bool forward,
- double pageLength,
- IReadOnlyList<PageTransitionItem> visibleItems)
- {
- var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal;
- var rotationTarget = isHorizontal
- ? (forward ? -MaxSwipeAngle : MaxSwipeAngle)
- : 0.0;
- var stackOffset = GetViewportStackOffset(pageLength);
- var lift = Math.Sin(Math.Clamp(progress, 0.0, 1.0) * Math.PI);
- foreach (var item in visibleItems)
- {
- var visual = item.Visual;
- var (rotate, scale, translate) = EnsureViewportTransforms(visual);
- var depth = GetViewportDepth(item.ViewportCenterOffset);
- var scaleValue = Math.Max(0.84, 1.0 - (BackCardScale * depth));
- var stackValue = stackOffset * depth;
- var baseOpacity = Math.Max(0.8, 1.0 - (ViewportDepthOpacityFalloff * depth));
- var restingAngle = isHorizontal ? GetViewportRestingAngle(item.ViewportCenterOffset) : 0.0;
- rotate.Angle = restingAngle;
- scale.ScaleX = scaleValue;
- scale.ScaleY = scaleValue;
- translate.X = 0;
- translate.Y = 0;
- if (ReferenceEquals(visual, from))
- {
- rotate.Angle = restingAngle + (rotationTarget * progress);
- stackValue -= stackOffset * 0.2 * lift;
- baseOpacity = Math.Min(1.0, baseOpacity + 0.08);
- }
- if (ReferenceEquals(visual, to))
- {
- var promotedScale = Math.Min(1.0, scaleValue + (ViewportLiftScale * lift) + (ViewportPromotionScale * progress));
- scale.ScaleX = promotedScale;
- scale.ScaleY = promotedScale;
- rotate.Angle = restingAngle * (1.0 - progress);
- stackValue = Math.Max(0.0, stackValue - (stackOffset * (0.45 + (0.2 * lift)) * progress));
- baseOpacity = Math.Min(1.0, baseOpacity + (0.12 * lift));
- }
- if (isHorizontal)
- translate.Y = stackValue;
- else
- translate.X = stackValue;
- visual.IsVisible = true;
- visual.Opacity = baseOpacity;
- visual.ZIndex = GetViewportZIndex(item.ViewportCenterOffset, visual, from, to);
- }
- }
- private static (RotateTransform rotate, TranslateTransform translate) EnsureTopTransforms(Visual visual)
- {
- if (visual.RenderTransform is TransformGroup group &&
- group.Children.Count == 2 &&
- group.Children[0] is RotateTransform rotateTransform &&
- group.Children[1] is TranslateTransform translateTransform)
- {
- visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
- return (rotateTransform, translateTransform);
- }
- var rotate = new RotateTransform();
- var translate = new TranslateTransform();
- visual.RenderTransform = new TransformGroup
- {
- Children =
- {
- rotate,
- translate
- }
- };
- visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
- return (rotate, translate);
- }
- private static (ScaleTransform scale, TranslateTransform translate) EnsureBackTransforms(Visual visual)
- {
- if (visual.RenderTransform is TransformGroup group &&
- group.Children.Count == 2 &&
- group.Children[0] is ScaleTransform scaleTransform &&
- group.Children[1] is TranslateTransform translateTransform)
- {
- visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
- return (scaleTransform, translateTransform);
- }
- var scale = new ScaleTransform();
- var translate = new TranslateTransform();
- visual.RenderTransform = new TransformGroup
- {
- Children =
- {
- scale,
- translate
- }
- };
- visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
- return (scale, translate);
- }
- private static (RotateTransform rotate, ScaleTransform scale, TranslateTransform translate) EnsureViewportTransforms(Visual visual)
- {
- if (visual.RenderTransform is TransformGroup group &&
- group.Children.Count == 3 &&
- group.Children[0] is RotateTransform rotateTransform &&
- group.Children[1] is ScaleTransform scaleTransform &&
- group.Children[2] is TranslateTransform translateTransform)
- {
- visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
- return (rotateTransform, scaleTransform, translateTransform);
- }
- var rotate = new RotateTransform();
- var scale = new ScaleTransform(1, 1);
- var translate = new TranslateTransform();
- visual.RenderTransform = new TransformGroup
- {
- Children =
- {
- rotate,
- scale,
- translate
- }
- };
- visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
- return (rotate, scale, translate);
- }
- private double GetViewportStackOffset(double pageLength)
- {
- if (BackCardOffset > 0)
- return BackCardOffset;
- return Math.Clamp(pageLength * 0.045, 10.0, 18.0);
- }
- private static double GetViewportDepth(double offsetFromCenter)
- {
- var distance = Math.Abs(offsetFromCenter);
- if (distance <= 1.0)
- return distance;
- if (distance <= 2.0)
- return 1.0 + ((distance - 1.0) * 0.8);
- return 1.8;
- }
- private static double GetViewportRestingAngle(double offsetFromCenter)
- {
- var sign = Math.Sign(offsetFromCenter);
- if (sign == 0)
- return 0;
- var distance = Math.Abs(offsetFromCenter);
- if (distance <= 1.0)
- return sign * Lerp(0.0, SidePeekAngle, distance);
- if (distance <= 2.0)
- return sign * Lerp(SidePeekAngle, FarPeekAngle, distance - 1.0);
- return sign * FarPeekAngle;
- }
- private static double Lerp(double from, double to, double t)
- {
- return from + ((to - from) * Math.Clamp(t, 0.0, 1.0));
- }
- private static int GetViewportZIndex(double offsetFromCenter, Visual visual, Visual? from, Visual? to)
- {
- if (ReferenceEquals(visual, from))
- return 5;
- if (ReferenceEquals(visual, to))
- return 4;
- var distance = Math.Abs(offsetFromCenter);
- if (distance < 0.5)
- return 4;
- if (distance < 1.5)
- return 3;
- return 2;
- }
- }
|