Подтвердить что ты не робот

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

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

4b9b3361

Ответ 1

Ответ Dr TJ достаточно прав. Спустившись по этому маршруту, вы должны обернуть 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

Ответ 2

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

Я не буду показывать пример Fade In, поскольку это охвачено множеством примеров, используя уже загруженное маршрутизируемое событие. Это Fading Out на удаление предмета, что является королевской болью в * @$.

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

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

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

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

  • Создайте Раскадку, которая оживляет ваше исчезновение в разделе Window.Resources вашего окна (над DataTemplate).

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

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

  • Создайте DataTrigger, который связывается с вашим свойством "Удалить", а на True воспроизводит раскадровку.

  • Создайте частный объект 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>

ура! ~

Ответ 3

Затухание, вероятно, будет невозможно без повторной записи базовой реализации ItemsControl. Проблема заключается в том, что когда ItemsControl получает событие INotifyCollectionChanged из коллекции, он немедленно (и внутри глубокого частного кода) отмечает контейнер элемента как не видимый (IsVisible - свойство readonly, которое получает его значение из скрытого кеша поэтому доступ к ним невозможен).

Вы можете легко реализовать затухание следующим образом:

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

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

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

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

Ответ 4

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

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

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

Ответ 5

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

Ответ 6

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

Ответ 7

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

Мы не можем использовать событие 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 event on item view. Этот обработчик событий должен ассоциировать представление (которое будет анимировано) и элемент (который будет удален).

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