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

问题描述 投票:27回答:7

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

wpf animation listbox
7个回答
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


30
投票

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

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

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

假设:

  1. 您正在使用填充了一些自定义对象的ObservableCollection
  2. 您使用DataTemplate为这些提供自定义外观,因此您希望为其移除动画
  3. 您将ObservableCollection绑定到ListBox(或类似的简单)
  4. 您在OC中的对象类上实现了INotifyPropertyChanged。

然后解决方案非常简单,非常痛苦,所以如果你花了很长时间试图解决这个问题。

  1. 创建一个故事板,在窗口的Window.Resources部分(DataTemplate上方)为您的淡出设置动画。
  2. (可选)将Duration定义为资源,这样可以避免硬编码。或者只是硬编码持续时间。
  3. 在对象类中创建一个名为“Removing”,“isRemoving”,whatev的公共布尔属性。确保为此字段引发Property Changed事件。
  4. 创建一个绑定到“正在删除”属性的DataTrigger,并在True上播放淡出故事板。
  5. 在对象类中创建一个私有DispatcherTimer对象,并实现一个与淡出动画具有相同持续时间的简单计时器,并从其tick处理程序中的列表中删除您的对象。

代码示例如下,希望这一切都很容易掌握。我尽可能简化了示例,因此您需要根据自己的需要调整环境。

代码背后

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>

好哇!〜


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();
    }

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


1
投票

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

  1. Storyboard类型的“故事板”。这是您要删除项目时要运行的实际动画。
  2. ICommand类型的“PerformRemoval”。这是一个在动画完成运行时执行的命令。它应该执行代码以实际从数据绑定集合中删除元素。
  3. bool类型的“IsMarkedForRemoval”。当您决定从列表中删除项目时(例如,在按钮单击处理程序中),将此项设置为true。只要附加的行为看到此属性更改为true,它就会开始动画。当动画的Completed事件发生时,它将Execute PerformRemoval命令。

Here是一个链接到行为和示例用法的完整来源(如果它是指向您自己的博客的错误形式,我将删除链接。我会在这里粘贴代码,但它相当冗长。我没有收到如果这有所作为,那么任何来自该东西的钱)。


0
投票

为淡入和淡出创建两个故事板并将其值绑定到您为OpacityMaskListBox创建的画笔


0
投票

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


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将始终尝试正确反映这一变化。

© www.soinside.com 2019 - 2024. All rights reserved.