[C#] ListView/ListBox ItemsSource Animation

    • XAML: WPF
    • .NET (FX) 4.5–4.8

    Es gibt 7 Antworten in diesem Thema. Der letzte Beitrag () ist von VincentTB.

      [C#] ListView/ListBox ItemsSource Animation

      Hi,
      habe mir gestern überlegt, dass so eine Item-Animation schon was feines wäre. Folgende Idee: Immer, wenn ich den ItemsSource einer ListView/ListBox verändere, werden die Items des neuen ItemsSources schön eingeblendet.

      Ich habe mir dafür ein Behavior geschrieben. Dieses funktioniert, wie schon gesagt, mit einer ListBox oder einem ListView. Sollte der Benutzer scrollen, wird die Animation gestoppt und alle Items werden sofort sichtbar, sodass es nicht komisch aussieht, weil eben nur die Items animiert werden, die der Benutzer zuerst sieht. Das ganze ist vollständig MVVM kompatibel und läuft auch super mit Data Visualisierung. So sieht dann das Resultat aus:


      Die Animation könnt ihr ganz einfach ändern, ihr müsst nur die TODO-Kommentare suchen.

      Hier ist der Code des Animation-Behaviors:
      Spoiler anzeigen

      C#-Quellcode

      1. class ItemsControlAnimationBehavior
      2. {
      3. public static readonly DependencyProperty IsAnimationEnabledProperty = DependencyProperty.RegisterAttached(
      4. "IsAnimationEnabled", typeof (bool), typeof (ItemsControlAnimationBehavior), new PropertyMetadata(false, PropertyChangedCallback));
      5. private static void PropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
      6. {
      7. var itemsControl = dependencyObject as ItemsControl;
      8. if (itemsControl == null)
      9. throw new ArgumentException("The dependencyObject isn't an ItemsControl");
      10. var dependencyPropertyDescriptor = DependencyPropertyDescriptor.FromProperty(ItemsControl.ItemsSourceProperty, typeof(ItemsControl));
      11. if ((bool)dependencyPropertyChangedEventArgs.NewValue)
      12. {
      13. dependencyPropertyDescriptor.AddValueChanged(dependencyObject, ItemsSourceChanged);
      14. }
      15. else
      16. {
      17. dependencyPropertyDescriptor.RemoveValueChanged(itemsControl, ItemsSourceChanged);
      18. }
      19. }
      20. private static readonly Dictionary<ItemsControl, DispatcherTimer> Timers = new Dictionary<ItemsControl, DispatcherTimer>();
      21. private async static void ItemsSourceChanged(object sender, EventArgs eventArgs)
      22. {
      23. var itemsControl = (ItemsControl) sender;
      24. //TODO: Hier die Animationen ändern
      25. var opacityAnimation = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(260), FillBehavior.Stop);
      26. var marginAnimation = new ThicknessAnimation(new Thickness(-20, 0, 20, 0), new Thickness(0),
      27. TimeSpan.FromMilliseconds(240), FillBehavior.Stop)
      28. {
      29. EasingFunction = new CircleEase {EasingMode = EasingMode.EaseOut}
      30. };
      31. if (Timers.ContainsKey(itemsControl)) //Wenn der Timer bereits gestartet ist, stoppen wir diesen
      32. {
      33. Timers[itemsControl].Stop();
      34. Timers.Remove(itemsControl);
      35. }
      36. List<ListBoxItem> visibleItems;
      37. while (true)
      38. {
      39. visibleItems = ListBoxExtensions.GetVisibleItemsFromItemsControl(itemsControl,
      40. Window.GetWindow(itemsControl));
      41. if (visibleItems.Count > 0 || itemsControl.Items.Count == 0)
      42. break;
      43. await Task.Delay(1); //Das Problem ist, dass das Event ausgelöst wird, bevor die Items gerendert wurden. Also warten wir, bis welche da sind
      44. }
      45. foreach (var item in visibleItems)
      46. {
      47. item.Opacity = 0; //Wir setzten bei allen die Transparenz auf 0, damit die noch nicht animierten nicht sichtbar sind
      48. }
      49. DispatcherTimer dispatcherTimer;
      50. var enumerator = visibleItems.GetEnumerator();
      51. if (enumerator.MoveNext())
      52. {
      53. ScrollChangedEventHandler scrollChangedEventHandler = null;
      54. scrollChangedEventHandler = (o, args) =>
      55. {
      56. var handler = scrollChangedEventHandler; //Wir holen uns den Handler hier rein
      57. if (handler != null) //Wir löschen diesen, damit sich hier nichts überschneidet
      58. itemsControl.RemoveHandler(ScrollViewer.ScrollChangedEvent, handler);
      59. if (!Timers.ContainsKey(itemsControl) || !Timers[itemsControl].IsEnabled) //Wenn der Timer für das für diesen Handler bestimmten ItemsControl nicht existiert oder deaktiviert ist, wurde die Animation wohl schon beendet. Da wollen wir nicht dazwischen funken.
      60. return;
      61. var timer = Timers[itemsControl]; //Wir holen uns erstmal den Timer...
      62. timer.Stop(); //... und stoppen diesen
      63. while (true) //Anschließend gehen wir alle noch nicht animierte Elemente durch und machen sie sichtbar
      64. {
      65. var item = enumerator.Current;
      66. if (item == null)
      67. break;
      68. item.Opacity = 1;
      69. if (!enumerator.MoveNext()) break;
      70. }
      71. };
      72. dispatcherTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(30) }; //TODO: Hier könnt ihr die Zeitspanne zwischen den Animationen für die Element einstellen
      73. dispatcherTimer.Tick += (s, timerE) =>
      74. {
      75. var item = enumerator.Current; //Bei jedem Tick animieren wir das nächte Item
      76. if (item == null) return;
      77. item.BeginAnimation(FrameworkElement.MarginProperty, marginAnimation);
      78. item.BeginAnimation(UIElement.OpacityProperty, opacityAnimation);
      79. item.Opacity = 1;
      80. if (!enumerator.MoveNext()) //Wenn keines mehr da ist, stoppen wir den Timer und löschen den Handler für das Scroll-Event
      81. {
      82. dispatcherTimer.Stop();
      83. itemsControl.RemoveHandler(ScrollViewer.ScrollChangedEvent, scrollChangedEventHandler);
      84. }
      85. };
      86. Timers.Add(itemsControl, dispatcherTimer);
      87. dispatcherTimer.Start();
      88. itemsControl.AddHandler(ScrollViewer.ScrollChangedEvent, scrollChangedEventHandler);
      89. }
      90. }
      91. public static void SetIsAnimationEnabled(DependencyObject element, bool value)
      92. {
      93. element.SetValue(IsAnimationEnabledProperty, value);
      94. }
      95. public static bool GetIsAnimationEnabled(DependencyObject element)
      96. {
      97. return (bool) element.GetValue(IsAnimationEnabledProperty);
      98. }
      99. }


      und hier noch die ListBox-Extensions: (Weil das ListViewItem von ListBoxItem erbt, funktionieren die mit der ListView auch)
      Spoiler anzeigen

      C#-Quellcode

      1. static class ListBoxExtensions
      2. {
      3. private static bool IsUserVisible(FrameworkElement element, FrameworkElement container)
      4. {
      5. if (!element.IsVisible)
      6. return false;
      7. Rect bounds =
      8. element.TransformToAncestor(container).TransformBounds(new Rect(0.0, 0.0, element.ActualWidth, element.ActualHeight));
      9. var rect = new Rect(0.0, 0.0, container.ActualWidth, container.ActualHeight);
      10. return rect.Contains(bounds.TopLeft) || rect.Contains(bounds.BottomRight);
      11. }
      12. public static List<ListBoxItem> GetVisibleItemsFromItemsControl(ItemsControl itemsControl, FrameworkElement parentToTestVisibility)
      13. {
      14. var items = new List<ListBoxItem>();
      15. foreach (var item in itemsControl.ItemsSource)
      16. {
      17. var lvItem = (ListBoxItem)itemsControl.ItemContainerGenerator.ContainerFromItem(item);
      18. if (lvItem == null)
      19. continue;
      20. if (IsUserVisible(lvItem, parentToTestVisibility))
      21. {
      22. items.Add(lvItem);
      23. }
      24. else if (items.Any())
      25. {
      26. break;
      27. }
      28. }
      29. return items;
      30. }
      31. }


      Um das dann zum laufen zu bekommen, müsst ihr in XAML einfach nur den Namespace importieren, in der die ItemsControlAnimationExtension-Klasse liegt und dann die Eigenschaft IsAnimationEnabled der Klasse auf true setzen:

      XML-Quellcode

      1. local:ItemsControlAnimationExtension.IsAnimationEnabled="true"


      Ein Beispielprojekt findet ihr im Anhang. Viel Spaß :)


      PS: Die Idee stammt von @GimpTutWorks :D
      Dateien
      Mfg
      Vincent

      Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „VincentTB“ ()

      @nafets
      Also? Die Items fliegen 20 Einheiten von links nach rechts in 200 ms ein und werden in 255 ms Sichtbar. Alle 25 ms startet die Animation für das nächste Item. Was würde man dann deiner Meinung nach verändern?
      Mfg
      Vincent

      So meinte ich es nicht. Ich hatte gedacht, dass das Einblenden (Opacity & Bewegung von links) mit "Easing" halt: Erst beschleunigt die Bewegung und dann wird sie am Ende wieder langsamer. Dadurch sieht die Bewegung viel natürlicher aus.
      @Artentus
      Ich habe mich bei WPF noch nie damit beschäftigt aber das scheint passend zu sein. Ich hatte mal die meisten Funktionen selbst implementiert - interessant dass ich übersehen hatte, dass es schon was dafür im Framework gibt :D
      @nafets
      Ach so, das meinst du :)
      Habe das mal (Dank @Artentus) hinzugefügt. Kannte das noch gar nicht (also, dass das einfach so geht), werde das jetzt häufiger benutzen, wieder was gelernt.

      @ErfinderDesRades
      Danke, man lernt immer dazu :) Habe das geändert.
      Mfg
      Vincent