如何实现淡入/淡出添加/删除的ListItems

时间:2010-11-13 09:24:04

标签: wpf animation listbox

假设我ListBox绑定了ObservableCollection,我想要添加/删除ListBoxItems动画,例如。 FadeIn / Out,SlideDown / Up等我该怎么做?

7 个答案:

答案 0 :(得分:30)

在花了几个小时疯狂追捕Google之后,我想我应该分享我是如何解决这个问题的,因为它似乎是一个非常简单的事情需要而且WPF让它非常令人沮丧,直到你非常了解动画如何实施。一旦你这样做,你就会意识到FrameworkElement.Unloaded是一个无用的动画事件。我已经在StackOverflow(以及其他)中看到了这个问题的许多版本,有各种各样的hackish方法来解决这个问题。希望我能提供一个最简单的例子,然后你可以为了很多目的而想象。

我不会显示Fade In示例,因为已经使用Loaded路由事件的大量示例涵盖了这一点。项目删除正逐渐消失,这是* @ $中的皇家痛苦。

这里的主要问题源于Storyboard在将它们放入控件/数据模板/样式时会变得如此奇怪。将DataContext(以及对象的ID)绑定到Storyboard是不可能的。已完成的事件在没有完成任务的情况下触发。潜水视觉树是没用的,因为所有数据模板化项目的容器名称都相同!当然,您可以编写一个函数来搜索整个集合以查找具有其删除标志属性设置的对象,但这是丑陋和诚实的,而不是您有意承认有意写的内容。如果你在彼此动画的长度内删除了几个对象(这是我的情况),它将无法工作。你也可以写一个类似的东西的清理线程,并在时间地狱迷路。没有什么好玩的。我离题了。解决方案。

假设:

  1. 您正在使用填充了一些自定义对象的ObservableCollection
  2. 您使用DataTemplate为这些提供自定义外观,因此您希望为其移除设置动画
  3. 将ObservableCollection绑定到ListBox(或像它一样简单的东西)
  4. 您在OC中的对象类上实现了INotifyPropertyChanged。
  5. 然后解决方案非常简单,非常痛苦,所以如果你花了很长时间试图解决这个问题。

    1. 创建一个故事板,在窗口的Window.Resources部分(DataTemplate上方)为您的淡出设置设置动画。

    2. (可选)将Duration定义为资源,以便您可以避免硬编码。或者只是硬编码持续时间。

    3. 在对象类中创建名为“Removing”,“isRemoving”,whatev的公共布尔属性。确保为此字段引发Property Changed事件。

    4. 创建一个绑定到“删除”属性的DataTrigger,并在True上播放淡出故事板。

    5. 在对象类中创建一个私有DispatcherTimer对象,并实现一个与淡出动画具有相同持续时间的简单计时器,并从其tick处理程序中的列表中删除您的对象。

    6. 下面的代码示例,希望能够轻松掌握。我尽可能简化了示例,因此您需要根据自己的需要调整环境。

      Code Behinds

      public partial class MainWindow : Window
      {
          public static ObservableCollection<Missiles> MissileRack = new ObservableCollection<Missiles>(); // because who doesn't love missiles? 
          public static Duration FadeDuration; 
      
          // main window constructor
          public MainWindow()
          {
              InitializeComponent();
      
              // somewhere here you'll want to tie the XAML Duration to your code-behind, or if you like ugly messes you can just skip this step and hard code away 
              FadeDuration = (Duration)this.Resources["cnvFadeDuration"];
              // 
              // blah blah
              // 
          }
      
          public void somethread_ShootsMissiles()
          {
              // imagine this is running on your background worker threads (or something like it)
              // however you want to flip the Removing flag on specific objects, once you do, it will fade out nicely
              var missilesToShoot = MissileRack.Where(p => (complicated LINQ search routine).ToList();
              foreach (var missile in missilesToShoot)
              {
                  // fire!
                  missile.Removing = true;
              }
          }
      }
      
      public class Missiles
      {
          public Missiles()
          {}
      
          public bool Removing
          {
              get { return _removing; }
              set
              {
                  _removing = value;
                  OnPropertyChanged("Removing"); // assume you know how to implement this
      
                  // start timer to remove missile from the rack
                  start_removal_timer();
              }
          }
          private bool _removing = false;
      
          private DispatcherTimer remove_timer;
          private void start_removal_timer()
          {
              remove_timer = new DispatcherTimer();
              // because we set the Interval of the timer to the same length as the animation, we know the animation will finish running before remove is called. Perfect. 
              remove_timer.Interval = MainWindow.TrackFadeDuration.TimeSpan; // I'm sure you can find a better way to share if you don't like global statics, but I am lazy
              remove_timer.Tick += new EventHandler(remove_timer_Elapsed);
              remove_timer.Start();
          }
      
          // use of DispatcherTimer ensures this handler runs on the GUI thread for us
          // this handler is now effectively the "Storyboard Completed" event
          private void remove_timer_Elapsed(object sender, EventArgs e)
          {
              // this is the only operation that matters for this example, feel free to fancy this line up on your own
              MainWindow.MissileRack.Remove(this); // normally this would cause your object to just *poof* before animation has played, but thanks to timer, 
          }
      
      }
      

      XAMLs

      <Window 
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              Title="Test" Height="300" Width="300">
          <Window.Resources>
              <Duration x:Key="cnvFadeDuration">0:0:0.3</Duration> <!-- or hard code this if you really must -->
              <Storyboard x:Key="cnvFadeOut" >
                  <DoubleAnimation Storyboard.TargetName="cnvMissile"
                                            Storyboard.TargetProperty="Opacity" 
                                            From="1" To="0" Duration="{StaticResource cnvFadeDuration}"
                                            />
              </Storyboard>
      
              <DataTemplate x:Key="MissileTemplate">
                  <Canvas x:Name="cnvMissile">
                      <!-- bunch of pretty missile graphics go here -->
                  </Canvas>
      
                  <DataTemplate.Triggers>
                      <DataTrigger Binding="{Binding Path=Removing}" Value="true" >
                          <DataTrigger.EnterActions>
                              <!-- you could actually just plop the storyboard right here instead of calling it as a resource, whatever suits your needs really -->
                              <BeginStoryboard Storyboard="{StaticResource cnvFadeOut}"  /> 
                          </DataTrigger.EnterActions>
                      </DataTrigger>
                  </DataTemplate.Triggers>
              </DataTemplate>
          </Window.Resources>
          <Grid>
              <ListBox /> <!-- do your typical data binding and junk -->
          </Grid>
      </Window>
      

      好哇!〜

答案 1 :(得分:19)

TJ博士的回答是对的。沿着那条路走下去,你必须包裹ObservableCollection<T>并实现一个BeforeDelete事件,然后你可以使用EventTrigger来控制故事板。

这是一个正确的痛苦。您可能更好地创建DataTemplate并处理FrameworkElement.Loaded中的FrameworkElement.UnloadedEventTrigger事件。

我在下面为你准备了一个快速样本。你必须自己整理删除代码,但我确信你已经完成了它。

    <ListBox>
        <ListBox.ItemsSource>
            <x:Array Type="sys:String">
                <sys:String>One</sys:String>
                <sys:String>Two</sys:String>
                <sys:String>Three</sys:String>
                <sys:String>Four</sys:String>
                <sys:String>Five</sys:String>
            </x:Array>
        </ListBox.ItemsSource>
        <ListBox.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding}"
                           Opacity="0">
                    <TextBlock.Triggers>
                        <EventTrigger RoutedEvent="FrameworkElement.Loaded">
                            <BeginStoryboard>
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetProperty="Opacity"
                                                     Duration="00:00:02"
                                                     From="0"
                                                     To="1" />
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger>
                        <EventTrigger RoutedEvent="FrameworkElement.Unloaded">
                            <BeginStoryboard>
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetProperty="Opacity"
                                                     Duration="00:00:02"
                                                     From="1"
                                                     To="0" />
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger>
                    </TextBlock.Triggers>
                </TextBlock>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

HTH,Stimul8d

答案 2 :(得分:2)

如果不重写ItemsControl基础实现,很可能无法淡出。问题是,当ItemsControl从集合中收到INotifyCollectionChanged事件时,它立即(并在深层私有代码中)将项容器标记为不可见(IsVisible是一个只读属性它来自隐藏缓存的值,因此无法访问)。

您可以通过以下方式轻松实现淡入:

public class FadingListBox : ListBox
{
    protected override void PrepareContainerForItemOverride(
        DependencyObject element, object item)
    {
        var lb = (ListBoxItem)element;
        DoubleAnimation anm = new DoubleAnimation(0, 1, 
            TimeSpan.FromMilliseconds(500));
        lb.BeginAnimation(OpacityProperty, anm);
        base.PrepareContainerForItemOverride(element, item);
    }
}

但是'淡出'等效物永远不会起作用,因为容器已经不可见并且无法重置。

public class FadingListBox : ListBox
{
    protected override void ClearContainerForItemOverride(
        DependencyObject element, object item)
    {
        var lb = (ListBoxItem) element;
        lb.BringIntoView();
        DoubleAnimation anm = new DoubleAnimation(
            1, 0, TimeSpan.FromMilliseconds(500));
        lb.BeginAnimation(OpacityProperty, anm);
        base.ClearContainerForItemOverride(element, item);
    }
}

即使您拥有自己的自定义容器生成器,也无法解决此问题

protected override DependencyObject GetContainerForItemOverride()
    {
        return new FadingListBoxItem();
    }

这种情况很有意义,因为如果容器在其代表的数据消失后仍然可见,那么理论上你可以点击容器(开启触发器,事件等)并体验一些微妙的错误。

答案 3 :(得分:1)

接受的答案适用于添加新项目的动画,但不适用于删除现有项目。这是因为在Unloaded事件触发时,该项目已被删除。删除工作的关键是添加一个&#34;标记为删除&#34;概念。被标记为删除应触发动画,动画的完成应触发实际删除。可能有很多方法可以实现这个想法,但我通过创建附加行为并稍微调整我的viewmodels来实现它。该行为公开了三个附加属性,所有这些属性都必须在每个ListViewItem上设置:

  1. &#34;故事板&#34;类型为Storyboard。这是您要删除项目时要运行的实际动画。
  2. &#34; PerformRemoval&#34;类型为ICommand。这是一个在动画完成运行时执行的命令。它应该执行代码以实际从数据绑定集合中删除元素。
  3. &#34; IsMarkedForRemoval&#34;类型为bool。当您决定从列表中删除项目时(例如,在按钮单击处理程序中),将此项设置为true。只要附加的行为看到此属性更改为true,它就会开始动画。当动画的Completed事件触发时,它将Execute PerformRemoval命令。
  4. Here是指向行为和示例用法的完整来源的链接(如果它是指向您自己博客的不良形式,我将删除该链接。我&#39; d将代码粘贴到此处,但它相当冗长。如果这样做有所不同,我就不会收到任何钱。)

答案 4 :(得分:0)

为淡入和淡出创建两个故事板,并将其值绑定到您为OpacityMask的{​​{1}}创建的画笔

答案 5 :(得分:0)

对于我FrameworkElement.Unloaded事件不起作用 - 该项目立即消失。我几乎不相信多年的WPF经验没有产生任何更漂亮的东西,但看起来这是唯一可行的方法是这里描述的黑客攻击:Animating removed item in Listbox?..

答案 6 :(得分:0)

嘿。由于接受的解决方案不起作用,让我们尝试另一轮;)

我们无法使用Unloaded事件,因为ListBox(或其他控件)在从原始列表中删除时会从可视树中删除项目。所以主要的想法是创建提供的ObservableCollection的阴影副本并将列表绑定到它。

首先 - XAML:

<ListBox ItemsSource="{Binding ShadowView}" IsSynchronizedWithCurrentItem="True">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Border Loaded="OnItemViewLoaded">
                <TextBlock Text="{Binding}"/>
            </Border>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

创建ListBox,将其绑定到我们的卷影副本,设置IsSynchronizedWithCurrentItem以获得正确的支持ICollectionView.CurrentItem(非常有用的接口),并在项目视图上设置Loaded事件。此事件处理程序需要关联视图(将进行动画处理)和项目(将被删除)。

private void OnItemViewLoaded (object sender, RoutedEventArgs e)
{
    var fe = (FrameworkElement) sender ;
    var dc = (DependencyObject) fe.DataContext ;

    dc.SetValue (ShadowViewSource.ViewProperty, fe) ;
}

初始化所有内容:

private readonly ShadowViewSource m_shadow ;

public ICollectionView ShadowView => m_shadow.View ;

public MainWindow ()
{
    m_collection = new ObservableCollection<...> () ;

    m_view = CollectionViewSource.GetDefaultView (m_collection) ;
    m_shadow = new ShadowViewSource (m_view) ;

    InitializeComponent ();
}

最后,但并非最不重要的,ShadowViewSource类(是的,它不完美,但作为概念验证它的工作原理):

using System ;
using System.Collections.Generic ;
using System.Collections.ObjectModel ;
using System.Collections.Specialized ;
using System.ComponentModel ;
using System.Linq ;
using System.Windows ;
using System.Windows.Data ;
using System.Windows.Media.Animation ;

namespace ShadowView
{
    public class ShadowViewSource
    {
        public static readonly DependencyProperty ViewProperty = DependencyProperty.RegisterAttached ("View", typeof (FrameworkElement), typeof (ShadowViewSource)) ;

        private readonly ICollectionView m_sourceView ;
        private readonly IEnumerable<object> m_source ;

        private readonly ICollectionView m_view ;
        private readonly ObservableCollection<object> m_collection ;

        public ShadowViewSource (ICollectionView view)
        {
            var sourceChanged = view.SourceCollection as INotifyCollectionChanged ;
            if (sourceChanged == null)
                throw new ArgumentNullException (nameof (sourceChanged)) ;

            var sortChanged = view.SortDescriptions as INotifyCollectionChanged ;
            if (sortChanged == null)
                throw new ArgumentNullException (nameof (sortChanged)) ;

            m_source = view.SourceCollection as IEnumerable<object> ;
            if (m_source == null)
                throw new ArgumentNullException (nameof (m_source)) ;

            m_sourceView = view ;

            m_collection = new ObservableCollection<object> (m_source) ;
            m_view = CollectionViewSource.GetDefaultView (m_collection) ;
            m_view.MoveCurrentTo (m_sourceView.CurrentItem) ;

            m_sourceView.CurrentChanged += OnSourceCurrentChanged ;
            m_view.CurrentChanged += OnViewCurrentChanged ;

            sourceChanged.CollectionChanged += OnSourceCollectionChanged ;
            sortChanged.CollectionChanged += OnSortChanged ;
        }

        private void OnSortChanged (object sender, NotifyCollectionChangedEventArgs e)
        {
            using (m_view.DeferRefresh ())
            {
                var sd = m_view.SortDescriptions ;
                sd.Clear () ;
                foreach (var desc in m_sourceView.SortDescriptions)
                    sd.Add (desc) ;
            }
        }

        private void OnSourceCollectionChanged (object sender, NotifyCollectionChangedEventArgs e)
        {
            var toAdd    = m_source.Except (m_collection) ;
            var toRemove = m_collection.Except (m_source) ;

            foreach (var obj in toAdd)
                m_collection.Add (obj) ;

            foreach (DependencyObject obj in toRemove)
            {
                var view = (FrameworkElement) obj.GetValue (ViewProperty) ;

                var begintime = 1 ;
                var sb = new Storyboard { BeginTime = TimeSpan.FromSeconds (begintime) } ;
                sb.Completed += (s, ea) => m_collection.Remove (obj) ;

                var fade = new DoubleAnimation (1, 0, new Duration (TimeSpan.FromMilliseconds (500))) ;
                Storyboard.SetTarget (fade, view) ;
                Storyboard.SetTargetProperty (fade, new PropertyPath (UIElement.OpacityProperty)) ;
                sb.Children.Add (fade) ;

                var size = new DoubleAnimation (view.ActualHeight, 0, new Duration (TimeSpan.FromMilliseconds (250))) ;
                Storyboard.SetTarget (size, view) ;
                Storyboard.SetTargetProperty (size, new PropertyPath (FrameworkElement.HeightProperty)) ;
                sb.Children.Add (size) ;
                size.BeginTime = fade.Duration.TimeSpan ;

                sb.Begin () ;
            }
        }

        private void OnViewCurrentChanged (object sender, EventArgs e)
        {
            m_sourceView.MoveCurrentTo (m_view.CurrentItem) ;
        }

        private void OnSourceCurrentChanged (object sender, EventArgs e)
        {
            m_view.MoveCurrentTo (m_sourceView.CurrentItem) ;
        }

        public ICollectionView View => m_view ;
    }
}

最后的话。首先它起作用。接下来 - 这种方法不需要对现有代码进行任何更改,通过删除属性等处理方法等等。特别是当实现为单个自定义控件时。你有ObservableCollection,添加项目,删除,做任何你想做的事情,UI总是试图正确反映这种变化。