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

Jie*_*eng 27 wpf animation listbox

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

Big*_*uge 31

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

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

这里的主要问题源于Storyboard在将它们放入Control/Data Templates/Styles时会变得奇怪.将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, 
    }

}
Run Code Online (Sandbox Code Playgroud)

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>
Run Code Online (Sandbox Code Playgroud)

好哇!〜


Eig*_*ite 19

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

这是一个正确的痛苦.你可能更好地创建DataTemplate和处理FrameworkElement.LoadedFrameworkElement.Unloaded事件EventTrigger.

我在下面为你准备了一个快速样本.你必须自己解决删除代码,但我相信你已经做到了.

    <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>
Run Code Online (Sandbox Code Playgroud)

HTH,Stimul8d

  • 除非我遗漏了某些内容,否则在"FrameworkElement.Unloaded"事件中触发的动画将永远不可见,因为该项目会立即从视图中删除. (35认同)
  • 同样的注意事项,在项目不再可见之后触发事件. (2认同)