Анимация WPF иногда внезапно останавливается на полпути

У меня есть элемент управления, который отображает кривую с контрольными точками, которыми может управлять пользователь. Если пользователь перетаскивает контрольную точку за пределы элемента управления, она становится недоступной, поэтому я создаю небольшую анимацию, которая масштабирует кривую вниз до тех пор, пока все контрольные точки снова не станут видимыми. Я использую RectAnimation на DependencyProperty в суррогатном типе объекта, чтобы захватить значения, создаваемые анимацией. В основном это работает, за исключением того, что случайным образом, возможно, в одном случае из десяти, анимация внезапно останавливается на полпути, без исключения, без события Completed, и кривая не была полностью переведена в целевые границы.

Немного упрощенный код:

  Rect elementBounds = GetBoundsForControlPoints();

  if ((elementBounds.Left < 0) || (elementBounds.Right > ActualWidth)
   || (elementBounds.Top < 0) || (elementBounds.Bottom > ActualHeight))
  {
    var fitBounds = FitRectangleToVisibleArea(elementBounds); // same aspect but entirely visible

    var anim = new RectAnimation();

    anim.From = elementBounds;
    anim.To = fitBounds;
    anim.Duration = TimeSpan.FromSeconds(0.4);

    var animTarget = new AnimationTarget();

    animTarget.StartRect = elementBounds;
    animTarget.EndRect = fitBounds;
    
    animTarget.RectChanged +=
      (_, _) =>
      {
        TranslateControlPoints(animTarget.StartRect, animTarget.Rect);
      };

    animTarget.BeginAnimation(AnimationTarget.RectProperty, anim);
  }

Это прокси-класс:

  class AnimationTarget : UIElement
  {
    public static DependencyProperty RectProperty = DependencyProperty.Register(nameof(Rect), typeof(Rect), typeof(AnimationTarget), new UIPropertyMetadata(RectChangedCallback));

    public Rect StartRect;
    public Rect EndRect;

    public Rect Rect
    {
      get => (Rect)GetValue(RectProperty);
      set => SetValue(RectProperty, value);
    }

    public event EventHandler? RectChanged;

    static void RectChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      ((AnimationTarget)d).RectChanged?.Invoke(d, EventArgs.Empty);
    }
  }

Большую часть времени анимация выполняется идеально, но время от времени она просто... останавливается. Кривая находится на полпути своего перемещения и просто остается там. Я могу запустить новую анимацию, и она начнется с того места, где остановилась предыдущая.

Что может быть причиной того, что анимация иногда просто останавливается на полпути?

У меня есть оболочка вокруг моих раскадровок с «ожидаемыми результатами», которые проверяются на «завершено» или «остановлено», что дает мне то, с чем мне нужно работать при отладке. Хотя ваша продолжительность кажется постоянной, невозможно узнать, верны ли ваши граничные предположения.

Gerry Schmitz 19.06.2024 20:04

Хм, я не вижу «остановленного» события в объектах анимации...

Jonathan Gilbert 19.06.2024 21:26

Я не сказал «событие». Альтернативой доведению анимации до конца является ее «остановка»; который требует собственной «последовательности обработки», чтобы «синхронизировать» визуальный элемент с реальными данными. Пользователи могут «ускорять» или замедлять мою анимацию; что требует мышления за пределами «коробки раскадровки». Я также «связываю» их.

Gerry Schmitz 21.06.2024 18:48

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

Jonathan Gilbert 22.06.2024 02:23

Если вы имеете в виду «сбой», я могу сломать свои истории, если не «остановлю» их перед изменением. Я не могу позволить себе остановку анимации без причины, потому что это «является» основной частью приложения (поэтому шаблон тестируется без конца со всеми «проверками» на месте: ограничения; есть/не анимируется; есть вращение масштабируется и т. д.) Серьезное дело, эти анимации :0)

Gerry Schmitz 22.06.2024 08:20

Нет, не рушится. Это просто... останавливается. Я могу внести изменение, которое вызовет новую анимацию, и она обычно выполняется до завершения. Иногда тоже останавливается.

Jonathan Gilbert 23.06.2024 04:03

Когда дети лягут спать, я посмотрю, смогу ли я снять это на видео.

Jonathan Gilbert 23.06.2024 04:04

Это видео демонстрирует поведение: youtube.com/watch?v=NT1Y1WGoygU Подробности смотрите в описании видео.

Jonathan Gilbert 23.06.2024 11:15

Анимации работают независимо от кода. Вы можете нажать точку останова в своем коде, которая остановит «код», в то время как анимация продолжит работать на «визуальном слое». Чтобы «видеть», что делает и собирается делать анимация, у меня есть «монитор», который работает параллельно; таймер, который запускается каждую секунду и вычисляет текущую позицию каждой анимации с помощью преобразований рендеринга и сравнивает ее с ожидаемыми результатами. Цель состоит в том, чтобы обнаружить «столкновения» и другие вещи.

Gerry Schmitz 23.06.2024 15:32

Итак, я избегаю проблем в первую очередь за счет «прозрачности». Это была моя первоначальная мысль: нужно больше прозрачности при работе с анимацией, которая работает более или менее независимо от кода, который ее запускает.

Gerry Schmitz 23.06.2024 15:33

А при использовании для анимации свойств отличных от «преобразований» (вращение, масштабирование, перемещение, наклон) «могут» возникнуть проблемы. Например. "Предпочтительно" (?) использовать преобразование масштабирования вместо анимации "ширины и/или высоты"... и синхронизировать с фактическими данными (только), если и когда это необходимо. (Независимые и зависимые анимации; ваша выглядит как «зависимая»)

Gerry Schmitz 23.06.2024 16:02

«Зависимые» анимации вызывают изменения макета; независимые - нет.

Gerry Schmitz 23.06.2024 16:08

И последнее замечание: я прибегнул к «Раскадровкам» только после того, как исчерпал свои собственные методы «анимации». Если бы мне нужно было, я бы перемещал вещи «в коде», используя таймер. За несколько мс можно сделать многое.

Gerry Schmitz 23.06.2024 16:28

Моя анимация не ориентирована на перемещение объекта в визуальном дереве. Как вы можете видеть на видео, я анимирую свойство суррогатного объекта, и когда значение изменяется, я устанавливаю новые местоположения для точек кривой, которые затем отдельным образом передаются в пользовательский интерфейс. Анимируемое свойство имеет тип Rect и является единственным свойством объекта, поэтому масштаб и перевод не имеют отношения к моей ситуации. Поскольку этот суррогатный/прокси-объект представляет собой просто отдельное свойство Rect, его нет в визуальном дереве, и поэтому любые манипуляции с макетами не имеют отношения к анимации.

Jonathan Gilbert 23.06.2024 16:48

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

Jonathan Gilbert 23.06.2024 16:48

Что такое «Рект»? Это слева, сверху, ширина и высота. Это «зависимые» свойства «перевода» и «масштабирования». Я «анимирую» свои пользовательские элементы управления, используя эти свойства, но посредством «преобразований». Как я уже сказал, зависимые анимации вызывают обновления макета. «Визуальный слой» предназначен для работы со скоростью 60 кадров в секунду. Если ваша анимация «не может», она, вероятно, попадет в поток пользовательского интерфейса; который сейчас завален обновлениями. (Просто предположение).

Gerry Schmitz 24.06.2024 18:10

Это могло бы объяснить плохую производительность (чего на практике я не наблюдаю), но я не понимаю, как это объясняет отказ WPF от анимации на полпути. :-П

Jonathan Gilbert 24.06.2024 19:12

Ваши манипуляции с «контрольными точками» «случайны». Если в результате что-то станет «отрицательным» (например, ширина), это объяснит это. Это одно из объяснений. Я уверен, что есть и другие.

Gerry Schmitz 24.06.2024 19:28

В ходе тестирования я перетаскиваю контрольные точки во всех направлениях. Негативность вообще не должна иметь никакого влияния, а также, как анимация узнает об этом? Мое назначение обновленных значений находится в try/catch. Даже если бы он выдавал исключения, он должен продолжать запускать тики анимации.

Jonathan Gilbert 24.06.2024 21:48

Исходя из вашего подхода, вы не можете подтвердить значения раскадровки в момент остановки анимации, поэтому вы просто размышляете. И, как я уже говорил, анимация не «запускает ваш код» (кроме завершения), поэтому «ловить» нечего. Что касается «негативного осознания», если вы прикажете анимации изменять ширину, скажем, на 100, на -110 (используя шаблон «По» «с течением времени»), что, по вашему мнению, может произойти?

Gerry Schmitz 25.06.2024 04:45

Это невозможно. Анимация передается от одного Rect к другому Rect. Пока начальный и конечный Rect правильно сформированы, каждый промежуточный Rect также будет правильно сформирован. В любом случае, я готов оставить этот вопрос позади. Насколько я понимаю, у анимации WPF есть проблемы с надежностью. Я не знаю, что это такое, но код анимации, который я опубликовал в своем ответе на этот вопрос, не имеет подобной проблемы.

Jonathan Gilbert 25.06.2024 19:19
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
21
59
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Ну, у меня нет ответа в смысле объяснения основного поведения. Но у меня есть ответ в смысле устранения проблемы: анимацию я реализовал сам.

Я создал свой собственный класс RectAnimation, в котором есть члены From, To, Duration, Completed и который объединяется с классом AnimationTarget из моей предыдущей реализации, предоставляя события RectDependencyProperty и RectChanged.

Когда вызывается BeginAnimation, он запускает ThreadIsBackground, установленным на true), который фиксирует время начала, время окончания (время начала + продолжительность), начальный и конечный прямоугольник (чтобы гарантировать, что не может быть внешних изменений), а затем входит в цикл . Каждый раз при прохождении цикла он получает текущее значение DateTime и вычисляет значение progress (двойное, от 0,0 до 1,0), выходя из цикла, если progress выходит за пределы диапазона (например, если достигнуто время окончания), а затем передает это значение. progress к методу AnimationTick. Короткая задержка ограничивает теоретическую максимальную частоту кадров до 100 кадров в секунду.

AnimationTick вычисляет промежуточное Rect значение для предоставленного progress, а затем начинает процесс присвоения его свойству зависимости. Это процесс, поскольку, будучи свойством зависимости, назначение может быть выполнено только в правильном потоке. Dispatcher.BeginInvoke используется для постановки изменений в очередь, и код гарантирует, что в любой момент времени существует только одно невыполненное назначение, на случай, если очередь пользовательского интерфейса будет резервной копией. По этой причине, даже если он вычисляет 100 обновлений в секунду, если он обновляет только пользовательский интерфейс, например. 30 раз в секунду, то свойство Rect будет обновляться 30 раз в секунду.

Использование класса очень похоже на использование системы RectAnimation, с основным отличием в том, что он анимирует свойство непосредственно в моем классе RectAnimation, а не выполняется для указанного свойства какого-либо другого объекта.

        class RectAnimation : UIElement
        {
            public static DependencyProperty RectProperty = DependencyProperty.Register(nameof(Rect), typeof(Rect), typeof(RectAnimation), new UIPropertyMetadata(RectChangedCallback));

            public Rect StartRect;
            public Rect EndRect;
            public TimeSpan Duration;

            public event EventHandler? Completed;

            public Rect Rect
            {
                get => (Rect)GetValue(RectProperty);
                set => SetValue(RectProperty, value);
            }

            public void BeginAnimation()
            {
                var thread = new Thread(AnimationThreadProc);

                thread.IsBackground = true;
                thread.Start();
            }

            void AnimationThreadProc()
            {
                try
                {
                    DateTime startTime = DateTime.UtcNow;
                    DateTime endTime = startTime + Duration;

                    var startRect = StartRect;
                    var endRect = EndRect;

                    while (true)
                    {
                        DateTime now = DateTime.UtcNow;

                        double progress = (now - startTime) / Duration;

                        if ((progress < 0.0) || (progress > 1.0))
                            break;

                        AnimationTick(startRect, endRect, progress);

                        Thread.Sleep(10);
                    }

                    Rect = endRect;

                    Completed?.Invoke(this, EventArgs.Empty);
                }
                catch { }
            }

            bool _tickOutstanding = false;
            RectReference? _tickRect;

            // Allow for atomic updates.
            record RectReference(Rect Value);

            void AnimationTick(Rect startRect, Rect endRect, double progress)
            {
                _tickRect = new RectReference(new Rect(
                    startRect.X + (endRect.X - startRect.X) * progress,
                    startRect.Y + (endRect.Y - startRect.Y) * progress,
                    startRect.Width + (endRect.Width - startRect.Width) * progress,
                    startRect.Height + (endRect.Height - startRect.Height) * progress));

                if (!_tickOutstanding)
                {
                    _tickOutstanding = true;

                    Dispatcher.BeginInvoke(
                        DispatcherPriority.Send,
                        () =>
                        {
                            if (_tickRect != null)
                                Rect = _tickRect.Value;
                            _tickOutstanding = false;
                        });
                }
            }

            public Point TransformPoint(Point pt)
            {
                var currentRect = Rect;

                var relativePosition = pt - StartRect.TopLeft;

                relativePosition.X *= currentRect.Width / StartRect.Width;
                relativePosition.Y *= currentRect.Height / StartRect.Height;

                return (Point)(relativePosition + currentRect.TopLeft);
            }

            public event EventHandler? RectChanged;

            static void RectChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                ((RectAnimation)d).RectChanged?.Invoke(d, EventArgs.Empty);
            }
        }

Хотя мои собственные методы анимации были более мощными, в конечном итоге раскадровки были более плавными. Итак, вы придумали гибрид.

Gerry Schmitz 24.06.2024 18:31

Проблем с плавностью я не наблюдал, но в данном случае надежность в любом случае на первом месте, поэтому даже если бы она была чуть менее плавной, я бы все равно выбрал тот, которому можно доверять. :-)

Jonathan Gilbert 24.06.2024 19:13

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