我有一个控件,它显示一条带有用户可以控制的控制点的曲线。如果用户将控制点拖动到控件边界之外,则无法再访问它,因此我创建了一个小动画来缩小曲线,直到所有控制点再次可见。我在代理对象类型中的
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);
}
}
大多数时候,动画完美地完成,但时不时地,它只是......停止。曲线处于平移的中间位置并保持在那里。我可以触发一个新的动画,它会从上一个动画停止的地方继续。
什么可能导致动画有时会中途停止?
嗯,我没有解释潜在行为的答案。但我确实有一个解决问题的答案:我自己实现了动画。
我创建了自己的
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);
}
}