CardStackPageTransition.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Threading;
  4. using System.Threading.Tasks;
  5. using Avalonia;
  6. using Avalonia.Animation;
  7. using Avalonia.Animation.Easings;
  8. using Avalonia.Media;
  9. using Avalonia.Styling;
  10. namespace ControlCatalog.Pages.Transitions;
  11. /// <summary>
  12. /// Transitions between two pages with a card-stack effect:
  13. /// the top page moves/rotates away while the next page scales up underneath.
  14. /// </summary>
  15. public class CardStackPageTransition : PageSlide
  16. {
  17. private const double ViewportLiftScale = 0.03;
  18. private const double ViewportPromotionScale = 0.02;
  19. private const double ViewportDepthOpacityFalloff = 0.08;
  20. private const double SidePeekAngle = 4.0;
  21. private const double FarPeekAngle = 7.0;
  22. /// <summary>
  23. /// Initializes a new instance of the <see cref="CardStackPageTransition"/> class.
  24. /// </summary>
  25. public CardStackPageTransition()
  26. {
  27. }
  28. /// <summary>
  29. /// Initializes a new instance of the <see cref="CardStackPageTransition"/> class.
  30. /// </summary>
  31. /// <param name="duration">The duration of the animation.</param>
  32. /// <param name="orientation">The axis on which the animation should occur.</param>
  33. public CardStackPageTransition(TimeSpan duration, PageSlide.SlideAxis orientation = PageSlide.SlideAxis.Horizontal)
  34. : base(duration, orientation)
  35. {
  36. }
  37. /// <summary>
  38. /// Gets or sets the maximum rotation angle (degrees) applied to the top card.
  39. /// </summary>
  40. public double MaxSwipeAngle { get; set; } = 15.0;
  41. /// <summary>
  42. /// Gets or sets the scale reduction applied to the back card (0.05 = 5%).
  43. /// </summary>
  44. public double BackCardScale { get; set; } = 0.05;
  45. /// <summary>
  46. /// Gets or sets the vertical offset (pixels) applied to the back card.
  47. /// </summary>
  48. public double BackCardOffset { get; set; } = 0.0;
  49. /// <inheritdoc />
  50. public override async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
  51. {
  52. if (cancellationToken.IsCancellationRequested)
  53. {
  54. return;
  55. }
  56. var tasks = new List<Task>();
  57. var parent = GetVisualParent(from, to);
  58. var distance = Orientation == PageSlide.SlideAxis.Horizontal ? parent.Bounds.Width : parent.Bounds.Height;
  59. var translateProperty = Orientation == PageSlide.SlideAxis.Horizontal ? TranslateTransform.XProperty : TranslateTransform.YProperty;
  60. var rotationTarget = Orientation == PageSlide.SlideAxis.Horizontal ? (forward ? -MaxSwipeAngle : MaxSwipeAngle) : 0.0;
  61. var startScale = 1.0 - BackCardScale;
  62. if (from != null)
  63. {
  64. var (rotate, translate) = EnsureTopTransforms(from);
  65. rotate.Angle = 0;
  66. translate.X = 0;
  67. translate.Y = 0;
  68. from.Opacity = 1;
  69. from.ZIndex = 1;
  70. var animation = new Animation
  71. {
  72. Easing = SlideOutEasing,
  73. Duration = Duration,
  74. FillMode = FillMode,
  75. Children =
  76. {
  77. new KeyFrame
  78. {
  79. Setters =
  80. {
  81. new Setter(translateProperty, 0d),
  82. new Setter(RotateTransform.AngleProperty, 0d)
  83. },
  84. Cue = new Cue(0d)
  85. },
  86. new KeyFrame
  87. {
  88. Setters =
  89. {
  90. new Setter(translateProperty, forward ? -distance : distance),
  91. new Setter(RotateTransform.AngleProperty, rotationTarget)
  92. },
  93. Cue = new Cue(1d)
  94. }
  95. }
  96. };
  97. tasks.Add(animation.RunAsync(from, cancellationToken));
  98. }
  99. if (to != null)
  100. {
  101. var (scale, translate) = EnsureBackTransforms(to);
  102. scale.ScaleX = startScale;
  103. scale.ScaleY = startScale;
  104. translate.X = 0;
  105. translate.Y = BackCardOffset;
  106. to.IsVisible = true;
  107. to.Opacity = 1;
  108. to.ZIndex = 0;
  109. var animation = new Animation
  110. {
  111. Easing = SlideInEasing,
  112. Duration = Duration,
  113. FillMode = FillMode,
  114. Children =
  115. {
  116. new KeyFrame
  117. {
  118. Setters =
  119. {
  120. new Setter(ScaleTransform.ScaleXProperty, startScale),
  121. new Setter(ScaleTransform.ScaleYProperty, startScale),
  122. new Setter(TranslateTransform.YProperty, BackCardOffset)
  123. },
  124. Cue = new Cue(0d)
  125. },
  126. new KeyFrame
  127. {
  128. Setters =
  129. {
  130. new Setter(ScaleTransform.ScaleXProperty, 1d),
  131. new Setter(ScaleTransform.ScaleYProperty, 1d),
  132. new Setter(TranslateTransform.YProperty, 0d)
  133. },
  134. Cue = new Cue(1d)
  135. }
  136. }
  137. };
  138. tasks.Add(animation.RunAsync(to, cancellationToken));
  139. }
  140. await Task.WhenAll(tasks);
  141. if (from != null && !cancellationToken.IsCancellationRequested)
  142. {
  143. from.IsVisible = false;
  144. }
  145. if (!cancellationToken.IsCancellationRequested && to != null)
  146. {
  147. var (scale, translate) = EnsureBackTransforms(to);
  148. scale.ScaleX = 1;
  149. scale.ScaleY = 1;
  150. translate.X = 0;
  151. translate.Y = 0;
  152. }
  153. }
  154. /// <inheritdoc />
  155. public override void Update(
  156. double progress,
  157. Visual? from,
  158. Visual? to,
  159. bool forward,
  160. double pageLength,
  161. IReadOnlyList<PageTransitionItem> visibleItems)
  162. {
  163. if (visibleItems.Count > 0)
  164. {
  165. UpdateVisibleItems(progress, from, to, forward, pageLength, visibleItems);
  166. return;
  167. }
  168. if (from is null && to is null)
  169. return;
  170. var parent = GetVisualParent(from, to);
  171. var size = parent.Bounds.Size;
  172. var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal;
  173. var distance = pageLength > 0
  174. ? pageLength
  175. : (isHorizontal ? size.Width : size.Height);
  176. var rotationTarget = isHorizontal ? (forward ? -MaxSwipeAngle : MaxSwipeAngle) : 0.0;
  177. var startScale = 1.0 - BackCardScale;
  178. if (from != null)
  179. {
  180. var (rotate, translate) = EnsureTopTransforms(from);
  181. if (isHorizontal)
  182. {
  183. translate.X = forward ? -distance * progress : distance * progress;
  184. translate.Y = 0;
  185. }
  186. else
  187. {
  188. translate.X = 0;
  189. translate.Y = forward ? -distance * progress : distance * progress;
  190. }
  191. rotate.Angle = rotationTarget * progress;
  192. from.IsVisible = true;
  193. from.Opacity = 1;
  194. from.ZIndex = 1;
  195. }
  196. if (to != null)
  197. {
  198. var (scale, translate) = EnsureBackTransforms(to);
  199. var currentScale = startScale + (1.0 - startScale) * progress;
  200. var currentOffset = BackCardOffset * (1.0 - progress);
  201. scale.ScaleX = currentScale;
  202. scale.ScaleY = currentScale;
  203. if (isHorizontal)
  204. {
  205. translate.X = 0;
  206. translate.Y = currentOffset;
  207. }
  208. else
  209. {
  210. translate.X = currentOffset;
  211. translate.Y = 0;
  212. }
  213. to.IsVisible = true;
  214. to.Opacity = 1;
  215. to.ZIndex = 0;
  216. }
  217. }
  218. /// <inheritdoc />
  219. public override void Reset(Visual visual)
  220. {
  221. visual.RenderTransform = null;
  222. visual.RenderTransformOrigin = default;
  223. visual.Opacity = 1;
  224. visual.ZIndex = 0;
  225. }
  226. private void UpdateVisibleItems(
  227. double progress,
  228. Visual? from,
  229. Visual? to,
  230. bool forward,
  231. double pageLength,
  232. IReadOnlyList<PageTransitionItem> visibleItems)
  233. {
  234. var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal;
  235. var rotationTarget = isHorizontal
  236. ? (forward ? -MaxSwipeAngle : MaxSwipeAngle)
  237. : 0.0;
  238. var stackOffset = GetViewportStackOffset(pageLength);
  239. var lift = Math.Sin(Math.Clamp(progress, 0.0, 1.0) * Math.PI);
  240. foreach (var item in visibleItems)
  241. {
  242. var visual = item.Visual;
  243. var (rotate, scale, translate) = EnsureViewportTransforms(visual);
  244. var depth = GetViewportDepth(item.ViewportCenterOffset);
  245. var scaleValue = Math.Max(0.84, 1.0 - (BackCardScale * depth));
  246. var stackValue = stackOffset * depth;
  247. var baseOpacity = Math.Max(0.8, 1.0 - (ViewportDepthOpacityFalloff * depth));
  248. var restingAngle = isHorizontal ? GetViewportRestingAngle(item.ViewportCenterOffset) : 0.0;
  249. rotate.Angle = restingAngle;
  250. scale.ScaleX = scaleValue;
  251. scale.ScaleY = scaleValue;
  252. translate.X = 0;
  253. translate.Y = 0;
  254. if (ReferenceEquals(visual, from))
  255. {
  256. rotate.Angle = restingAngle + (rotationTarget * progress);
  257. stackValue -= stackOffset * 0.2 * lift;
  258. baseOpacity = Math.Min(1.0, baseOpacity + 0.08);
  259. }
  260. if (ReferenceEquals(visual, to))
  261. {
  262. var promotedScale = Math.Min(1.0, scaleValue + (ViewportLiftScale * lift) + (ViewportPromotionScale * progress));
  263. scale.ScaleX = promotedScale;
  264. scale.ScaleY = promotedScale;
  265. rotate.Angle = restingAngle * (1.0 - progress);
  266. stackValue = Math.Max(0.0, stackValue - (stackOffset * (0.45 + (0.2 * lift)) * progress));
  267. baseOpacity = Math.Min(1.0, baseOpacity + (0.12 * lift));
  268. }
  269. if (isHorizontal)
  270. translate.Y = stackValue;
  271. else
  272. translate.X = stackValue;
  273. visual.IsVisible = true;
  274. visual.Opacity = baseOpacity;
  275. visual.ZIndex = GetViewportZIndex(item.ViewportCenterOffset, visual, from, to);
  276. }
  277. }
  278. private static (RotateTransform rotate, TranslateTransform translate) EnsureTopTransforms(Visual visual)
  279. {
  280. if (visual.RenderTransform is TransformGroup group &&
  281. group.Children.Count == 2 &&
  282. group.Children[0] is RotateTransform rotateTransform &&
  283. group.Children[1] is TranslateTransform translateTransform)
  284. {
  285. visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
  286. return (rotateTransform, translateTransform);
  287. }
  288. var rotate = new RotateTransform();
  289. var translate = new TranslateTransform();
  290. visual.RenderTransform = new TransformGroup
  291. {
  292. Children =
  293. {
  294. rotate,
  295. translate
  296. }
  297. };
  298. visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
  299. return (rotate, translate);
  300. }
  301. private static (ScaleTransform scale, TranslateTransform translate) EnsureBackTransforms(Visual visual)
  302. {
  303. if (visual.RenderTransform is TransformGroup group &&
  304. group.Children.Count == 2 &&
  305. group.Children[0] is ScaleTransform scaleTransform &&
  306. group.Children[1] is TranslateTransform translateTransform)
  307. {
  308. visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
  309. return (scaleTransform, translateTransform);
  310. }
  311. var scale = new ScaleTransform();
  312. var translate = new TranslateTransform();
  313. visual.RenderTransform = new TransformGroup
  314. {
  315. Children =
  316. {
  317. scale,
  318. translate
  319. }
  320. };
  321. visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
  322. return (scale, translate);
  323. }
  324. private static (RotateTransform rotate, ScaleTransform scale, TranslateTransform translate) EnsureViewportTransforms(Visual visual)
  325. {
  326. if (visual.RenderTransform is TransformGroup group &&
  327. group.Children.Count == 3 &&
  328. group.Children[0] is RotateTransform rotateTransform &&
  329. group.Children[1] is ScaleTransform scaleTransform &&
  330. group.Children[2] is TranslateTransform translateTransform)
  331. {
  332. visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
  333. return (rotateTransform, scaleTransform, translateTransform);
  334. }
  335. var rotate = new RotateTransform();
  336. var scale = new ScaleTransform(1, 1);
  337. var translate = new TranslateTransform();
  338. visual.RenderTransform = new TransformGroup
  339. {
  340. Children =
  341. {
  342. rotate,
  343. scale,
  344. translate
  345. }
  346. };
  347. visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
  348. return (rotate, scale, translate);
  349. }
  350. private double GetViewportStackOffset(double pageLength)
  351. {
  352. if (BackCardOffset > 0)
  353. return BackCardOffset;
  354. return Math.Clamp(pageLength * 0.045, 10.0, 18.0);
  355. }
  356. private static double GetViewportDepth(double offsetFromCenter)
  357. {
  358. var distance = Math.Abs(offsetFromCenter);
  359. if (distance <= 1.0)
  360. return distance;
  361. if (distance <= 2.0)
  362. return 1.0 + ((distance - 1.0) * 0.8);
  363. return 1.8;
  364. }
  365. private static double GetViewportRestingAngle(double offsetFromCenter)
  366. {
  367. var sign = Math.Sign(offsetFromCenter);
  368. if (sign == 0)
  369. return 0;
  370. var distance = Math.Abs(offsetFromCenter);
  371. if (distance <= 1.0)
  372. return sign * Lerp(0.0, SidePeekAngle, distance);
  373. if (distance <= 2.0)
  374. return sign * Lerp(SidePeekAngle, FarPeekAngle, distance - 1.0);
  375. return sign * FarPeekAngle;
  376. }
  377. private static double Lerp(double from, double to, double t)
  378. {
  379. return from + ((to - from) * Math.Clamp(t, 0.0, 1.0));
  380. }
  381. private static int GetViewportZIndex(double offsetFromCenter, Visual visual, Visual? from, Visual? to)
  382. {
  383. if (ReferenceEquals(visual, from))
  384. return 5;
  385. if (ReferenceEquals(visual, to))
  386. return 4;
  387. var distance = Math.Abs(offsetFromCenter);
  388. if (distance < 0.5)
  389. return 4;
  390. if (distance < 1.5)
  391. return 3;
  392. return 2;
  393. }
  394. }