Как я могу реализовать постепенное исчезновение и добавление / удаление ListItems

Предположим, у меня есть ListBox привязан к ObservableCollection и я хочу анимировать добавление / удаление ListBoxItems например. FadeIn/Out, SlideDown/Up и т. Д. Как я могу это сделать?

7 ответов

Решение

Ответ доктора ТиДжей достаточно правильный. Спускаясь по этому маршруту, вы должны будете завернуть ObservableCollection<T> и реализовать событие BeforeDelete,.. тогда вы могли бы использовать EventTrigger контролировать раскадровки.

Это правильная боль, хотя. Вы, вероятно, лучше создавать DataTemplate и обработка FrameworkElement.Loaded а также FrameworkElement.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>

HTH, Stimul8d

Проведя безумные часы, охотясь на дебри Google, я думаю, что должен поделиться тем, как я решил эту проблему, поскольку она кажется довольно простой вещью, которая нужна, и все же WPF делает это до смешного разочарования, пока вы не поймете, как реализована анимация. Как только вы это сделаете, вы поймете, что FrameworkElement.Unloaded - бесполезное событие для анимации. Я видел много версий этого вопроса по всему Stackru (среди прочих), со всевозможными хакерскими способами решить эту проблему. Надеюсь, я смогу привести самый простой пример, который вы затем сможете придумать для своих многочисленных целей.

Я не буду показывать пример Fade In, поскольку он покрыт множеством примеров, уже использующих перенаправленное событие Loaded. Это затухание при удалении предмета - это королевская боль в *@$.

Основная проблема здесь связана с тем, что раскадровки становятся странными, когда вы помещаете их в элементы управления / шаблоны данных / стили. Невозможно привязать DataContext (и, следовательно, идентификатор вашего объекта) к раскадровке. Событие Completed начинается с нулевого представления о том, кем оно только что закончилось. Погружение в визуальное дерево бесполезно, поскольку все элементы шаблона данных имеют одинаковые имена для своих контейнеров! Конечно, вы могли бы написать функцию, которая будет выполнять поиск во всей коллекции объектов, для которых установлено свойство флажка удаления, но это уродливо и честно, просто не то, что вы когда-либо хотели бы написать специально. И это не сработает, если у вас есть несколько объектов, удаляемых в пределах длины вашей анимации друг друга (это мой случай). Вы также можете просто написать поток очистки, который делает подобные вещи и теряется во времени. Не весело. Я отвлекся. На решение.

Предположения:

  1. Вы используете ObservableCollection, заполненную некоторыми пользовательскими объектами
  2. Вы используете DataTemplate, чтобы придать им индивидуальный вид, поэтому вы хотите анимировать их удаление.
  3. Вы связываете ObservableCollection с ListBox (или с чем-то простым)
  4. У вас есть INotifyPropertyChanged, реализованный на классе объектов в вашем OC.

Тогда решение очень простое, на самом деле, мучительно, так что если вы потратили много времени, пытаясь решить эту проблему.

  1. Создайте раскадровку, которая оживляет ваше затухание в разделе Window.Resources вашего окна (над шаблоном DataTemplate).

  2. (Необязательно) Определите Duration отдельно как ресурс, чтобы вы могли избежать жесткого кодирования. Или просто жесткий код длительности.

  3. Создайте в вашем объектном классе открытое логическое свойство с именами "Removing", "isRemoving", whatev. Убедитесь, что вы подняли событие Property Changed для этого поля.

  4. Создайте DataTrigger, который привязывается к вашему свойству "Удаление" и в True воспроизводит раскадровку с исчезающим эффектом.

  5. Создайте частный объект DispatcherTimer в своем классе объектов и реализуйте простой таймер, который имеет ту же продолжительность, что и анимация исчезновения, и удаляет ваш объект из списка в его обработчике тиков.

Ниже приведен пример кода, который, надеюсь, облегчает понимание. Я максимально упростил пример, поэтому вам нужно будет адаптировать его к своей среде так, как вам удобно.

Код позади

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>

Ура!~

Принятый ответ работает для анимации добавления новых элементов, но не для удаления существующих. Это потому, что к тому времени Unloaded событие происходит, элемент уже был удален. Ключом к удалению на работу является добавление понятия "помечено для удаления". Отметка для удаления должна вызвать анимацию, а завершение анимации должно вызвать фактическое удаление. Вероятно, существует множество способов реализации этой идеи, но я заставил ее работать, создав привязанное поведение и немного подправив свои модели представления. Поведение предоставляет три прикрепленных свойства, каждое из которых должно быть установлено на каждом ListViewItem:

  1. "Раскадровка" типа Storyboard, Это фактическая анимация, которую вы хотите запустить при удалении элемента.
  2. "PerformRemoval" типа ICommand, Это команда, которая будет выполнена после завершения анимации. Он должен выполнить код, чтобы фактически удалить элемент из коллекции данных.
  3. "IsMarkedForRemoval" типа bool, Установите для этого параметра значение true, если вы решили удалить элемент из списка (например, в обработчике нажатия кнопки). Как только присоединенное поведение увидит, что это свойство изменилось на true, оно начнет анимацию. И когда анимация Completed событие происходит, оно будет Execute PerformRemoval команда.

Вот ссылка на полный исходный код для поведения и примера использования (если неправильно направлять на свой блог, я удалю ссылку. Я вставлю сюда код, но он довольно длинный. Я не получаю любые деньги от вещи, если это имеет значение).

Исчезновение, вероятно, будет невозможно без переписывания 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();
    }

И этот вид имеет смысл, потому что если контейнер все еще был виден после исчезновения данных, которые он представляет, то вы могли бы теоретически щелкнуть по контейнеру (сбросив триггеры, события и т. Д.) И, возможно, столкнуться с некоторыми незначительными ошибками.

Для меня FrameworkElement.Unloaded событие не работает - элемент просто исчезает мгновенно. Я с трудом могу поверить, что многолетний опыт работы с WPF не принес ничего более красивого, но похоже, что единственный способ, которым это может сработать, - это хак, описанный здесь: Анимация удаленного элемента в Listbox?..

Хех. Поскольку принятое решение не работает, давайте попробуем еще один раунд;)

Мы не можем использовать событие 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, добавляйте элементы, удаляйте, делайте что хотите, пользовательский интерфейс всегда будет стараться корректно отразить эти изменения.

Создайте две доски историй для постепенного появления и исчезновения и свяжите их значение с кистью, которую вы создали для OpacityMask вашей ListBox

Другие вопросы по тегам