WPF动画有时会突然中途停止

问题描述 投票:0回答:1

我有一个控件,它显示一条带有用户可以控制的控制点的曲线。如果用户将控制点拖动到控件边界之外,则无法再访问它,因此我创建了一个小动画来缩小曲线,直到所有控制点再次可见。我在代理对象类型中的

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

大多数时候,动画完美地完成,但时不时地,它只是......停止。曲线处于平移的中间位置并保持在那里。我可以触发一个新的动画,它会从上一个动画停止的地方继续。

什么可能导致动画有时会中途停止?

wpf animation
1个回答
0
投票

嗯,我没有解释潜在行为的答案。但我确实有一个解决问题的答案:我自己实现了动画。

我创建了自己的

RectAnimation
类,其中包含成员
From
To
Duration
Completed
,并合并到我之前实现的
AnimationTarget
类中,提供了
Rect
DependencyProperty
RectChanged
活动。

当调用

BeginAnimation
时,它会启动一个
Thread
IsBackground
设置为
true
),捕获开始时间、结束时间(开始时间+持续时间)、开始矩形和结束矩形(以确保不会有是外部变化),然后进入循环。每次循环时,它都会获取当前的
DateTime
,如果到达结束时间则退出循环,然后调用具有
AnimationTick
值(双精度,0.0 到 1.0)的方法
progress
。短暂的延迟将理论最大帧速率限制为 100fps。

AnimationTick
计算所提供的
Rect
的中间
progress
值,然后开始将其分配给依赖属性的过程。这是一个过程,因为作为依赖属性,分配只能在正确的线程上完成。
Dispatcher.BeginInvoke
用于对更改进行排队,代码确保在任何时间点都只有一个未完成的分配,以防 UI 队列备份。因此,即使它每秒计算 100 次更新,如果它只是更新 UI,例如每秒 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);
            }
        }
© www.soinside.com 2019 - 2024. All rights reserved.