在 Avalonia 中编写高性能动画 XAML 动画在介绍合成动画之前我们先复习一下目前主要使用的 XAML 动画的写法BorderNameborder1Width200Height200BackgroundRedBorder.StylesStyleStyle.AnimationsAnimation PlaybackDirectionAlternate IterationCount10 Duration0:0:3KeyFrame Cue0%Setter PropertyTranslateTransform.X Value0.0 //KeyFrameKeyFrame Cue100%Setter PropertyTranslateTransform.X Value1000.0 //KeyFrame/Animation/Style.Animations/Style/Border.Styles/Border这段代码为 Border 创建了一个动画让它沿着 X 方向往返移动 10 次简单易读然而这种动画有一个重大的缺陷它运行在 UI 线程上因此 UI 线程的高负载操作如 Measure / Arrange会严重影响动画的流畅程度。反过来这些动画也会影响 UI 线程上的输入事件等给用户体验造成很大影响。如果我们调用Thread.Sleep()模拟 UI 线程卡顿动画将会完全停止而且Transition 系统也基于相同的原理。因此在 Avalonia 中你用 XAML 编写的所有动画理论上都有上面的缺点合成动画为了解决这些问题Avalonia 11 引入了全新的合成Composition渲染器也带来了这篇文章的主角——合成动画Composition Animation系统合成动画不是什么新鲜事物它最早出现在 UWP / WinUI 中。可惜无论是 WinUI 还是 Avalonia关于合成动画的资料都少之又少只能通过硬啃 Avalonia 的源码来略知一二我们需要知道的是每个 Avalonia 控件在 UI 线程有一个 CompositionVisual 继承自 CompositionObject同时在渲染层都有一个对应的 ServerCompositionVisual继承自 ServerObject。CompositionObject 上的每个属性在 ServerObject 上都有对应的属性修改 XAML 控件上的属性时会先修改 UI 线程的 CompositionVisual 上的相应属性再序列化到渲染线程的 ServerCompositionVisual。在渲染过程中合成器会根据 ServerCompositionVisual 上的属性调整渲染指令参数再将这些指令发送到各个后端进行最终渲染UI 线程渲染线程序列化XAML 控件CompositionVisualServerCompositionVisual渲染后端SkiaVulkan...XAML 动画通过对 XAML 控件上的属性进行插值而合成动画在渲染层计算并直接修改 ServerObject 上的属性从而摆脱对 UI 线程的依赖在 Avalonia 中我们可以用以下方式定义并使用一个关键帧合成动画private void AnimateSquare(Compositor compositor, CompositionVisual redVisual){Vector3KeyFrameAnimation animation compositor.CreateVector3KeyFrameAnimation();animation.InsertKeyFrame(1f, new Vector3(1000f, 0f, 0f));animation.Duration TimeSpan.FromSeconds(2);animation.Direction PlaybackDirection.Alternate;animation.IterationCount 10; // 重复10次redVisual.StartAnimation(Translation, animation);}var visual ElementComposition.GetElementVisual(border1);AnimateSquare(visual!.Compositor, visual);现在再来调用Thread.Sleep()你可以发现合成动画依然能正常运行完全不受 UI 线程影响同样也可以编写隐式动画在某个属性变化时自动触发private ImplicitAnimationCollection? _implicitAnimations;private void EnsureImplicitAnimations(){var compositor ElementComposition.GetElementVisual(this)!.Compositor;var offsetAnimation compositor.CreateVector3KeyFrameAnimation();offsetAnimation.Target Offset;offsetAnimation.InsertExpressionKeyFrame(1.0f, this.FinalValue); // 将动画最终值设定为更改的目标值offsetAnimation.Duration TimeSpan.FromMilliseconds(400);var rotationAnimation compositor.CreateScalarKeyFrameAnimation();rotationAnimation.Target RotationAngle;rotationAnimation.InsertKeyFrame(.5f, 0.160f);rotationAnimation.InsertKeyFrame(1f, 0f);rotationAnimation.Duration TimeSpan.FromMilliseconds(400);var animationGroup compositor.CreateAnimationGroup();animationGroup.Add(offsetAnimation);animationGroup.Add(rotationAnimation);_implicitAnimations compositor.CreateImplicitAnimationCollection();_implicitAnimations[Offset] animationGroup; // 在 Offset 变化时自动触发此隐式动画组}// 元素加载完成后调用compositionVisual.ImplicitAnimations _implicitAnimations;然而合成动画的强大之处远不止于此。注意到刚刚隐式动画里的InsertExpressionKeyFrame了吗这里的字符串不是硬编码的关键字而是一种用特定语法编写的表达式有一个专门的 解释器 负责这些表达式的解析与执行。在 Avalonia 中表达式的语法和 WinUI 里的几乎完全相同可以直接参考 WinUI 文档 。表达式动画非常重要的一个功能就是它可以设置引用即“监听”其他 CompositionObject 上属性的变化并在属性变化时重新对表达式进行计算。例如我们就可以基于 ExpressionAnimation 来模拟一堆齿轮完整代码见 Avalonia.Labs/samples/Avalonia.Labs.Catalog/Views/Composition/Gears.axaml.cs private void ConfigureGearAnimation(CompositionVisual currentGear, CompositionVisual previousGear){var compositor currentGear.Compositor;var rotationExpression compositor.CreateExpressionAnimation(-prevGear.RotationAngle);rotationExpression.SetReferenceParameter(prevGear, previousGear); // prevGear相当于 previousGear 在表达式中的变量名currentGear.StartAnimation(RotationAngle, rotationExpression);}只需要对第一个齿轮播放动画后面所有齿轮都会根据前一个齿轮的运动状态旋转。即使有上千个齿轮动画也能保持较高帧率运行如果觉得字符串表达式写着很恶心Avalonia.Labs 中的 ExpressionBuilder 能允许你用 C# 代码辅助编写这些表达式例如上面的代码使用 ExpressionBuilder 后就可以变成这样var rotateExpression - previousGear.GetReference().RotationAngle;currentGear.StartAnimation(RotationAngle, rotateExpression);当然合成动画也有一定的局限性。由于在渲染线程运行我们在 UI 线程无法直接获取到合成动画的进度UI 线程的 Visual 的属性值和最终渲染所使用的属性值也是不同的var compositionVisual ElementComposition.GetElementVisual(border1)!;var animation compositor.CreateDoubleKeyFrameAnimation();animation.Duration TimeSpan.FromSeconds(4);animation.InsertKeyFrame(0f, 1f);animation.InsertKeyFrame(1f, 0f);compositionVisual.StartAnimation(Opacity, animation);await Task.Delay(2000);var xamlOpacity border1.Opacity; // XAML 控件属性 —— 仍为1var visualOpacity compositionVisul.Opacity; // UI 线程上的 CompositionVisual —— 仍为1一个解决方案是维护一个定时器通过运行时间估算出当前动画进度和值但仍不能确保与实际渲染的值的完全相同如何选择尽管我个人建议尽可能使用合成动画。然而合成动画的一个巨大缺陷就是他不适合在 Style 中使用且编写合成动画所需的代码量远远大于 XAML 动画。因此目前推荐将合成动画用于以下场景性能敏感