SelectingItemsControlBehavior.cs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. // Based on code: https://github.com/adirh3/Avalonia.ListBoxAnimation.Samples
  2. using System;
  3. using System.Linq;
  4. using System.Numerics;
  5. using Avalonia.Animation.Easings;
  6. using Avalonia.Controls;
  7. using Avalonia.Controls.Primitives;
  8. using Avalonia.Rendering.Composition;
  9. using Avalonia.VisualTree;
  10. namespace Avalonia.Xaml.Interactions.Custom;
  11. /// <summary>
  12. ///
  13. /// </summary>
  14. public class SelectingItemsControlBehavior
  15. {
  16. /// <summary>
  17. ///
  18. /// </summary>
  19. public static readonly AttachedProperty<bool> EnableSelectionAnimationProperty =
  20. AvaloniaProperty.RegisterAttached<SelectingItemsControl, bool>("EnableSelectionAnimation",
  21. typeof(SelectingItemsControlBehavior));
  22. /// <summary>
  23. ///
  24. /// </summary>
  25. static SelectingItemsControlBehavior()
  26. {
  27. EnableSelectionAnimationProperty.Changed.AddClassHandler<Control>(OnEnableSelectionAnimation);
  28. }
  29. private static void OnEnableSelectionAnimation(Control control, AvaloniaPropertyChangedEventArgs args)
  30. {
  31. if (control is SelectingItemsControl selectingItemsControl)
  32. {
  33. if (args.NewValue is true)
  34. {
  35. selectingItemsControl.PropertyChanged += SelectingItemsControlPropertyChanged;
  36. }
  37. else
  38. {
  39. selectingItemsControl.PropertyChanged -= SelectingItemsControlPropertyChanged;
  40. }
  41. }
  42. }
  43. private static void SelectingItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs args)
  44. {
  45. if (sender is not SelectingItemsControl selectingItemsControl ||
  46. args.Property != SelectingItemsControl.SelectedIndexProperty ||
  47. args.OldValue is not int oldIndex || args.NewValue is not int newIndex)
  48. {
  49. return;
  50. }
  51. if (selectingItemsControl.ContainerFromIndex(newIndex) is not TemplatedControl newSelection
  52. || selectingItemsControl.ContainerFromIndex(oldIndex) is not TemplatedControl oldSelection)
  53. {
  54. return;
  55. }
  56. StartOffsetAnimation(newSelection, oldSelection);
  57. }
  58. private static void StartOffsetAnimation(TemplatedControl newSelection, TemplatedControl oldSelection)
  59. {
  60. // Find the indicator border
  61. // NOTE:
  62. // The original required putting PART_SelectedPipe in template (e.g. ListBox > ListBoxItem)
  63. // and used GetTemplateChildren() instead of GetVisualDescendants()
  64. if (newSelection.GetVisualDescendants().FirstOrDefault(s => s.Name == "PART_SelectedPipe") is not { } borderPipe
  65. || oldSelection.GetVisualDescendants().FirstOrDefault(s => s.Name == "PART_SelectedPipe") is not
  66. { } oldPipe)
  67. {
  68. return;
  69. }
  70. // Clear old implicit animations if any
  71. ElementComposition.GetElementVisual(oldPipe)?.ImplicitAnimations?.Clear();
  72. // Get the composition visuals for all controls
  73. var pipeVisual = ElementComposition.GetElementVisual(borderPipe);
  74. var newSelectionVisual = ElementComposition.GetElementVisual(newSelection);
  75. var oldSelectionVisual = ElementComposition.GetElementVisual(oldSelection);
  76. if (pipeVisual == null || newSelectionVisual == null || oldSelectionVisual == null)
  77. {
  78. return;
  79. }
  80. // Calculate the offset between old and new selections
  81. var selectionOffset = oldSelectionVisual.Offset - newSelectionVisual.Offset;
  82. // Check whether the offset is vertical (e.g. ListBox) or horizontal (e.g. TabControl)
  83. // Note this code assumes the items are aligned in the SelectingItemsControl
  84. var isVerticalOffset = selectionOffset.Y != 0;
  85. var offset = isVerticalOffset ? selectionOffset.Y : selectionOffset.X;
  86. var compositor = pipeVisual.Compositor;
  87. // This is required
  88. var quadraticEaseIn = new SpringEasing();
  89. // Create new offset animation between old selection position to the current position
  90. var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
  91. offsetAnimation.Target = "Offset";
  92. var expression = (offset > 0 ? "+" : "-") + Math.Abs(offset);
  93. offsetAnimation.InsertExpressionKeyFrame(
  94. 0f,
  95. isVerticalOffset
  96. ? $"Vector3(this.FinalValue.X, this.FinalValue.Y{expression}, 0)"
  97. : $"Vector3(this.FinalValue.X{expression}, this.FinalValue.Y, 0)");
  98. offsetAnimation.InsertExpressionKeyFrame(1f, "this.FinalValue");
  99. offsetAnimation.Duration = TimeSpan.FromMilliseconds(250);
  100. // Create small scale animation so the pipe will "stretch" while it's moving
  101. var scaleAnimation = compositor.CreateVector3KeyFrameAnimation();
  102. scaleAnimation.Target = "Scale";
  103. scaleAnimation.InsertKeyFrame(0f, Vector3.One, quadraticEaseIn);
  104. scaleAnimation.InsertKeyFrame(0.5f,
  105. new Vector3(1f + (!isVerticalOffset ? 0.75f : 0f), 1f + (isVerticalOffset ? 0.75f : 0f), 1f),
  106. quadraticEaseIn);
  107. scaleAnimation.InsertKeyFrame(1f, Vector3.One, quadraticEaseIn);
  108. scaleAnimation.Duration = TimeSpan.FromMilliseconds(250);
  109. var compositionAnimationGroup = compositor.CreateAnimationGroup();
  110. compositionAnimationGroup.Add(offsetAnimation);
  111. compositionAnimationGroup.Add(scaleAnimation);
  112. var pipeVisualImplicitAnimations = compositor.CreateImplicitAnimationCollection();
  113. var currentOffset = isVerticalOffset ? pipeVisual.Offset.Y : pipeVisual.Offset.X;
  114. if (currentOffset == 0)
  115. {
  116. // Visual first shown, offset not calculated, lets trigger using Offset
  117. pipeVisualImplicitAnimations["Offset"] = compositionAnimationGroup;
  118. }
  119. else
  120. {
  121. // Visual already shown, we can't trigger on Offset as it won't change
  122. pipeVisualImplicitAnimations["Visible"] = compositionAnimationGroup;
  123. }
  124. pipeVisual.ImplicitAnimations = pipeVisualImplicitAnimations;
  125. }
  126. /// <summary>
  127. ///
  128. /// </summary>
  129. /// <param name="element"></param>
  130. /// <returns></returns>
  131. public static bool GetEnableSelectionAnimation(SelectingItemsControl element)
  132. {
  133. return element.GetValue(EnableSelectionAnimationProperty);
  134. }
  135. /// <summary>
  136. ///
  137. /// </summary>
  138. /// <param name="element"></param>
  139. /// <param name="value"></param>
  140. public static void SetEnableSelectionAnimation(SelectingItemsControl element, bool value)
  141. {
  142. element.SetValue(EnableSelectionAnimationProperty, value);
  143. }
  144. }