在 Avalonia 框架中,我们通常使用 XAML 编写动画。然而,现实上 Avalonia 中还存在另一套鲜为人知(?)的动画体系——Composition Animation(合成动画)
本文将先容如安在 Avalonia 中编写合成动画,以及它相比平凡 XAML 动画的长处
XAML 动画
在先容合成动画之前,我们先复习一下现在告急使用的 XAML 动画的写法:- <Border
- Name="border1"
- Width="200"
- Height="200"
- Background="Red">
- <Border.Styles>
-
- </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 上的属性调解渲问鼎令参数,再将这些指令发送到各个后端举行终极渲染
flowchart LR subgraph 1[UI 线程] A[XAML 控件] -->B[CompositionVisual] end B --> |序列化|C[ServerCompositionVisual] subgraph 2[渲染线程] C --> D([渲染后端]) D --> E[Skia] D --> F[Vulkan] D --> G[...] endXAML 动画通过对 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 控件属性 —— 仍为1
- var visualOpacity = compositionVisul.Opacity; // UI 线程上的 CompositionVisual —— 仍为1
复制代码 一个办理方案是维护一个定时器,通过运行时间估算出当前动画进度和值,但仍不能确保与现实渲染的值的完全雷同
怎样选择?
只管我个人发起尽大概使用合成动画。然而,合成动画的一个巨大缺陷就是他不得当在 Style 中使用,且编写合成动画所需的代码量远弘大于 XAML 动画。因此,现在保举将合成动画用于以了局景
- 性能敏感
- 动画数目较多
- 须要连续播放
- 多个动画目的相互联动(使用 ExpressionAnimation)
现在已经有 PR 实现了在 XAML 中编写隐式合成动画,但尚未归并
官方文档中也纪录了一种方法(和 PR 中的类似),使用 AttachedProperty 来间接实如今 XAML 中编写合成动画
一旦有官方 API 可以大概在 XAML 中编写合成动画,你应该始终选择它而非传统动画
免责声明:如果侵犯了您的权益,请联系站长及时删除侵权内容,谢谢合作!qidao123.com:ToB企服之家,中国第一个企服评测及软件市场,开放入驻,技术点评得现金. |