WPF:使用效果显示和隐藏ItemsControl中的项

7
我一直在使用这篇优秀的文章来显示和隐藏带有过渡效果的元素。它非常完美,因为它允许您像平常绑定Visibility属性,然后定义可见性更改时的操作(例如,动画其不透明度或触发故事板)。当您隐藏一个元素时,它使用值强制使其保持可见,直到��渡完成。
我正在寻找类似的解决方案,用于ItemsControlObservableCollection。换句话说,我想像平常绑定ItemsSourceObservableCollection,但要控制添加和删除项时会发生什么,并触发动画。我不认为在这里使用值强制会起作用,但显然,物品仍然需要保留在列表中,直到它们的过渡完成。是否有任何现有的解决方案可以使此变得简单?
我希望任何解决方案都是相对通用且易于应用于任何类型的列表。理想情况下,样式和动画行为应分开,将其应用于特定列表应该是一个简单的任务,例如给它一个附加属性。

我有一个可行的解决方案来添加项目... 我还在寻找删除项目的方法... - Bathineni
3个回答

8
淡入很容易,但是对于淡出,项目需要在动画完成之前保留在源列表中(就像你所说的)。
如果我们仍然希望能够正常使用源ObservableCollection(添加/删除等),那么我们必须创建一个镜像集合,该集合与源集合始终保持同步,并延迟删除直到动画完成。这可以使用CollectionChanged事件完成。
以下是我使用附加行为实现的实现。它可用于ItemsControl、ListBox、DataGrid或任何其他从ItemsControl派生的内容。
不要绑定ItemsSource,而是绑定附加属性ItemsSourceBehavior.ItemsSource。它将使用Reflection创建一个镜像ObservableCollection,将其用作ItemsSource并处理FadeIn/FadeOut动画。 请注意,我没有对此进行广泛测试,可能存在错误和多个可以改进的地方,但在我的场景中效果非常好。
示例用法
<ListBox behaviors:ItemsSourceBehavior.ItemsSource="{Binding MyCollection}">
    <behaviors:ItemsSourceBehavior.FadeInAnimation>
        <Storyboard>
            <DoubleAnimation Storyboard.TargetProperty="Opacity"
                             From="0.0"
                             To="1.0"
                             Duration="0:0:3"/>
        </Storyboard>
    </behaviors:ItemsSourceBehavior.FadeInAnimation>
    <behaviors:ItemsSourceBehavior.FadeOutAnimation>
        <Storyboard>
            <DoubleAnimation Storyboard.TargetProperty="Opacity"
                             To="0.0"
                             Duration="0:0:1"/>
        </Storyboard>
    </behaviors:ItemsSourceBehavior.FadeOutAnimation>
    <!--...-->
</ListBox>

ItemsSourceBehavior

public class ItemsSourceBehavior
{
    public static readonly DependencyProperty ItemsSourceProperty =
        DependencyProperty.RegisterAttached("ItemsSource",
                                            typeof(IList),
                                            typeof(ItemsSourceBehavior),
                                            new UIPropertyMetadata(null, ItemsSourcePropertyChanged));
    public static void SetItemsSource(DependencyObject element, IList value)
    {
        element.SetValue(ItemsSourceProperty, value);
    }
    public static IList GetItemsSource(DependencyObject element)
    {
        return (IList)element.GetValue(ItemsSourceProperty);
    }

    private static void ItemsSourcePropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
        ItemsControl itemsControl = source as ItemsControl;
        IList itemsSource = e.NewValue as IList;
        if (itemsControl == null)
        {
            return;
        }
        if (itemsSource == null)
        {
            itemsControl.ItemsSource = null;
            return;
        }

        Type itemsSourceType = itemsSource.GetType();
        Type listType = typeof(ObservableCollection<>).MakeGenericType(itemsSourceType.GetGenericArguments()[0]);
        IList mirrorItemsSource = (IList)Activator.CreateInstance(listType);
        itemsControl.SetBinding(ItemsControl.ItemsSourceProperty, new Binding{ Source = mirrorItemsSource });

        foreach (object item in itemsSource)
        {
            mirrorItemsSource.Add(item);
        }
        FadeInContainers(itemsControl, itemsSource);

        (itemsSource as INotifyCollectionChanged).CollectionChanged += 
            (object sender, NotifyCollectionChangedEventArgs ne) =>
        {
            if (ne.Action == NotifyCollectionChangedAction.Add)
            {
                foreach (object newItem in ne.NewItems)
                {
                    mirrorItemsSource.Add(newItem);
                }
                FadeInContainers(itemsControl, ne.NewItems);
            }
            else if (ne.Action == NotifyCollectionChangedAction.Remove)
            {
                foreach (object oldItem in ne.OldItems)
                {
                    UIElement container = itemsControl.ItemContainerGenerator.ContainerFromItem(oldItem) as UIElement;
                    Storyboard fadeOutAnimation = GetFadeOutAnimation(itemsControl);
                    if (container != null && fadeOutAnimation != null)
                    {
                        Storyboard.SetTarget(fadeOutAnimation, container);

                        EventHandler onAnimationCompleted = null;
                        onAnimationCompleted = ((sender2, e2) =>
                        {
                            fadeOutAnimation.Completed -= onAnimationCompleted;
                            mirrorItemsSource.Remove(oldItem);
                        });

                        fadeOutAnimation.Completed += onAnimationCompleted;
                        fadeOutAnimation.Begin();
                    }
                    else
                    {
                        mirrorItemsSource.Remove(oldItem);
                    }
                }
            }
        };
    }

    private static void FadeInContainers(ItemsControl itemsControl, IList newItems)
    {
        EventHandler statusChanged = null;
        statusChanged = new EventHandler(delegate
        {
            if (itemsControl.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
            {
                itemsControl.ItemContainerGenerator.StatusChanged -= statusChanged;
                foreach (object newItem in newItems)
                {
                    UIElement container = itemsControl.ItemContainerGenerator.ContainerFromItem(newItem) as UIElement;
                    Storyboard fadeInAnimation = GetFadeInAnimation(itemsControl);
                    if (container != null && fadeInAnimation != null)
                    {
                        Storyboard.SetTarget(fadeInAnimation, container);
                        fadeInAnimation.Begin();
                    }
                }
            }
        });
        itemsControl.ItemContainerGenerator.StatusChanged += statusChanged;
    }

    public static readonly DependencyProperty FadeInAnimationProperty =
        DependencyProperty.RegisterAttached("FadeInAnimation",
                                            typeof(Storyboard),
                                            typeof(ItemsSourceBehavior),
                                            new UIPropertyMetadata(null));
    public static void SetFadeInAnimation(DependencyObject element, Storyboard value)
    {
        element.SetValue(FadeInAnimationProperty, value);
    }
    public static Storyboard GetFadeInAnimation(DependencyObject element)
    {
        return (Storyboard)element.GetValue(FadeInAnimationProperty);
    }

    public static readonly DependencyProperty FadeOutAnimationProperty =
        DependencyProperty.RegisterAttached("FadeOutAnimation",
                                            typeof(Storyboard),
                                            typeof(ItemsSourceBehavior),
                                            new UIPropertyMetadata(null));
    public static void SetFadeOutAnimation(DependencyObject element, Storyboard value)
    {
        element.SetValue(FadeOutAnimationProperty, value);
    }
    public static Storyboard GetFadeOutAnimation(DependencyObject element)
    {
        return (Storyboard)element.GetValue(FadeOutAnimationProperty);
    }
}

1
这是一个很棒的解决方案!在启动淡出动画时有一个小错误:事件处理程序没有从故事板中移除,因此会不断堆积,导致删除一个项目时多个项目被删除。我已编辑您的答案并修复了该问题。 - Tim Rogers
1
我之前已经做出了镜像列表,但是无法弄清如何将故事板附加到项目上。我的解决方案涉及强制使用ItemsSource属性来生成和返回镜像列表。这样,您就可以继续使用相同的旧ItemsSource属性并使用样式设置故事板。如果我成功了,我也会在这里发布该解决方案。谢谢Meleak。 - Tim Rogers
@Tim Rogers:强制ItemsSource属性听起来不错,很有意思看你能否让它正常工作!我也在尝试使用“NotifyCollectionChangedAction.Move”来创建一个移动动画,如果我成功组合出什么东西,我也会在这里更新的。 - Fredrik Hedblad
我更新了代码,使得对FadeInContainers的调用发生在将项目添加到数据绑定列表之前,因为我发现FadeInContainers设置的事件处理程序在数据绑定列表更新后立即触发,因此您会看到先前添加的项目已经淡入。这似乎只是ItemsControl而不是ListBox控件的问题。 - Martin Randall
克隆Storyboard中的淡出,否则当同时淡化2个或更多容器时,所有已完成的处理程序将在第一个完成时运行。 Storyboard fadeOutAnimation = GetFadeOutAnimation(itemsControl).Clone(); - FreddyFlares
显示剩余3条评论

1

@Fredrik Hedblad 干得好。我有几点意见。

  • 添加项目时,动画有时会从先前添加的项目开始。

  • 将项目插入列表中,所有项目都添加到底部(因此不支持排序列表)

  • (个人问题:需要为每个项目单独设置动画)

在下面的代码中,我有一个经过改进的版本,解决了上述问题。

public class ItemsSourceBehavior
{
    public static void SetItemsSource(DependencyObject element, IList value)
    {
        element.SetValue(ItemsSourceProperty, value);
    }

    public static IList GetItemsSource(DependencyObject element)
    {
        return (IList) element.GetValue(ItemsSourceProperty);
    }

    private static void ItemsSourcePropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
        //If animations need to be run together set this to 'false'.
        const bool separateAnimations = true;

        var itemsControl = source as ItemsControl;
        var itemsSource = e.NewValue as IList;
        if (itemsControl == null)
        {
            return;
        }
        if (itemsSource == null)
        {
            itemsControl.ItemsSource = null;
            return;
        }

        var itemsSourceType = itemsSource.GetType();
        var listType = typeof (ObservableCollection<>).MakeGenericType(itemsSourceType.GetGenericArguments()[0]);
        var mirrorItemsSource = (IList) Activator.CreateInstance(listType);
        itemsControl.SetBinding(ItemsControl.ItemsSourceProperty, new Binding {Source = mirrorItemsSource});

        foreach (var item in itemsSource)
        {
            mirrorItemsSource.Add(item);
            if (separateAnimations)
                StartFadeInAnimation(itemsControl, new List<object> {item});
        }

        if (!separateAnimations)
        {
            StartFadeInAnimation(itemsControl, itemsSource);
        }

        (itemsSource as INotifyCollectionChanged).CollectionChanged +=
            (object sender, NotifyCollectionChangedEventArgs ne) =>
            {
                if (ne.Action == NotifyCollectionChangedAction.Add)
                {
                    foreach (var newItem in ne.NewItems)
                    {
                        //insert the items instead of just adding them
                        //this brings support for sorted collections
                        mirrorItemsSource.Insert(ne.NewStartingIndex, newItem);

                        if (separateAnimations)
                        {
                            StartFadeInAnimation(itemsControl, new List<object> {newItem});
                        }
                    }

                    if (!separateAnimations)
                    {
                        StartFadeInAnimation(itemsControl, ne.NewItems);
                    }
                }
                else if (ne.Action == NotifyCollectionChangedAction.Remove)
                {
                    foreach (var oldItem in ne.OldItems)
                    {
                        var container = itemsControl.ItemContainerGenerator.ContainerFromItem(oldItem) as UIElement;
                        var fadeOutAnimation = GetFadeOutAnimation(itemsControl);
                        if (container != null && fadeOutAnimation != null)
                        {
                            Storyboard.SetTarget(fadeOutAnimation, container);

                            EventHandler onAnimationCompleted = null;
                            onAnimationCompleted = ((sender2, e2) =>
                            {
                                fadeOutAnimation.Completed -= onAnimationCompleted;
                                mirrorItemsSource.Remove(oldItem);
                            });

                            fadeOutAnimation.Completed += onAnimationCompleted;
                            fadeOutAnimation.Begin();
                        }
                        else
                        {
                            mirrorItemsSource.Remove(oldItem);
                        }
                    }
                }
            };
    }

    private static void StartFadeInAnimation(ItemsControl itemsControl, IList newItems)
    {
        foreach (var newItem in newItems)
        {
            var container = itemsControl.ItemContainerGenerator.ContainerFromItem(newItem) as UIElement;
            var fadeInAnimation = GetFadeInAnimation(itemsControl);
            if (container != null && fadeInAnimation != null)
            {
                Storyboard.SetTarget(fadeInAnimation, container);
                fadeInAnimation.Begin();
            }
        }
    }

    public static void SetFadeInAnimation(DependencyObject element, Storyboard value)
    {
        element.SetValue(FadeInAnimationProperty, value);
    }

    public static Storyboard GetFadeInAnimation(DependencyObject element)
    {
        return (Storyboard) element.GetValue(FadeInAnimationProperty);
    }

    public static void SetFadeOutAnimation(DependencyObject element, Storyboard value)
    {
        element.SetValue(FadeOutAnimationProperty, value);
    }

    public static Storyboard GetFadeOutAnimation(DependencyObject element)
    {
        return (Storyboard) element.GetValue(FadeOutAnimationProperty);
    }

    public static readonly DependencyProperty ItemsSourceProperty =
        DependencyProperty.RegisterAttached("ItemsSource",
            typeof (IList),
            typeof (ItemsSourceBehavior),
            new UIPropertyMetadata(null, ItemsSourcePropertyChanged));

    public static readonly DependencyProperty FadeInAnimationProperty =
        DependencyProperty.RegisterAttached("FadeInAnimation",
            typeof (Storyboard),
            typeof (ItemsSourceBehavior),
            new UIPropertyMetadata(null));

    public static readonly DependencyProperty FadeOutAnimationProperty =
        DependencyProperty.RegisterAttached("FadeOutAnimation",
            typeof (Storyboard),
            typeof (ItemsSourceBehavior),
            new UIPropertyMetadata(null));
}

0

类似,但据我所见它并不能帮助我绑定到ObservableCollection并挂钩其事件。 - Tim Rogers

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接