Avalonia的composition Api 动画
composition api 可以为我们在所绑定的属性的值发生变化的时候自动产生隐式动画,这个可以为我们节省很多的时间,并且Composition API 是在渲染线程上执行的,而不是UI线程上执行的。
Compositor类也就是我们说的合成器, 管理着UI线程和渲染线程之前的通信,动画计算和渲染在独立的渲染线程上执行而不会阻塞UI线程。在Win平台上,composition api 可以利用DirectCompositon或者WINUI composition 进行硬件加速渲染,它使用批量提交机制,将多个属性变化打包成一个批次发送到渲染线程上,减少了线程之间的通信次数,提高了整体性能。
光说不看代码没用用处,我们直接上源码:
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Avalonia.Rendering.Composition.Animations;
using Avalonia.Rendering.Composition.Expressions;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition
{
/// <summary>
/// Base class of the composition API representing a node in the visual tree structure.
/// Composition objects are the visual tree structure on which all other features of the composition API use and build on.
/// The API allows developers to define and create one or many <see cref="CompositionVisual" /> objects each representing a single node in a Visual tree.
/// </summary>
public abstract class CompositionObject : ICompositorSerializable
{
/// <summary>
/// The collection of implicit animations attached to this object.
/// </summary>
public ImplicitAnimationCollection? ImplicitAnimations { get; set; }
private protected InlineDictionary<CompositionProperty, IAnimationInstance> PendingAnimations;
internal CompositionObject(Compositor compositor, SimpleServerObject? server)
{
Compositor = compositor;
Server = server;
}
/// <summary>
/// The associated Compositor
/// </summary>
public Compositor Compositor { get; }
SimpleServerObject ICompositorSerializable.TryGetServer(Compositor c)
{
Debug.Assert(c == Compositor);
return Server ?? ThrowInvalidOperation();
}
internal SimpleServerObject? Server { get; }
public bool IsDisposed { get; private set; }
private bool _registeredForSerialization;
[MethodImpl(MethodImplOptions.NoInlining)]
private static SimpleServerObject ThrowInvalidOperation() =>
throw new InvalidOperationException("There is no server-side counterpart for this object");
protected internal void Dispose()
{
if (!IsDisposed && Server != null)
Compositor.DisposeOnNextBatch(Server);
IsDisposed = true;
}
/// <summary>
/// Connects an animation with the specified property of the object and starts the animation.
/// </summary>
public void StartAnimation(string propertyName, CompositionAnimation animation)
=> StartAnimation(propertyName, animation, null);
internal virtual void StartAnimation(string propertyName, CompositionAnimation animation, ExpressionVariant? finalValue)
{
throw new ArgumentException("Unknown property " + propertyName);
}
/// <summary>
/// Starts an animation group.
/// The StartAnimationGroup method on CompositionObject lets you start CompositionAnimationGroup.
/// All the animations in the group will be started at the same time on the object.
/// </summary>
public void StartAnimationGroup(ICompositionAnimationBase grp)
{
if (grp is CompositionAnimation animation)
{
if(animation.Target == null)
throw new ArgumentException("Animation Target can't be null");
StartAnimation(animation.Target, animation);
}
else if (grp is CompositionAnimationGroup group)
{
foreach (var a in group.Animations)
{
if (a.Target == null)
throw new ArgumentException("Animation Target can't be null");
StartAnimation(a.Target, a);
}
}
}
bool StartAnimationGroupPart(CompositionAnimation animation, string target, ExpressionVariant finalValue)
{
if(animation.Target == null)
throw new ArgumentException("Animation Target can't be null");
if (animation.Target == target)
{
StartAnimation(animation.Target, animation, finalValue);
return true;
}
else
{
StartAnimation(animation.Target, animation);
return false;
}
}
internal bool StartAnimationGroup(ICompositionAnimationBase grp, string target, ExpressionVariant finalValue)
{
if (grp is CompositionAnimation animation)
return StartAnimationGroupPart(animation, target, finalValue);
if (grp is CompositionAnimationGroup group)
{
var matched = false;
foreach (var a in group.Animations)
{
if (a.Target == null)
throw new ArgumentException("Animation Target can't be null");
if (StartAnimationGroupPart(a, target, finalValue))
matched = true;
}
return matched;
}
throw new ArgumentException();
}
protected void RegisterForSerialization()
{
if (Server == null)
throw new InvalidOperationException("The object doesn't have an associated server counterpart");
if(_registeredForSerialization)
return;
_registeredForSerialization = true;
Compositor.RegisterForSerialization(this);
}
void ICompositorSerializable.SerializeChanges(Compositor c, BatchStreamWriter writer)
{
Debug.Assert(c == Compositor);
_registeredForSerialization = false;
SerializeChangesCore(writer);
}
private protected virtual void SerializeChangesCore(BatchStreamWriter writer)
{
}
}
}
ImplicitAnimations 是 CompositionObject 的一个公开属性,可以直接设置。这正是我们之前使用的方式,每个 CompositionObject 都有一个关联的 Server 对象,这是渲染线程上的对应实体。这就是为什么 Composition API 能在渲染线程上执行动画的原因。
RegisterForSerialization() 方法确保对象的更改会被序列化并发送到渲染线程。这是 UI 线程和渲染线程之间通信的关键机制。
StartAnimation 和 StartAnimationGroup 方法提供了显式启动动画的方式,而 ImplicitAnimations 则是在属性变化时自动触发。
所以我们应该推荐使用这种动画机制,下面我们使用一个简单的例子来看看,我们试着在loaded之后获取一下组件对象,然后获取组件的合成器来达到我们想要的效果。
private void MyBorder_Loaded(object? sender, RoutedEventArgs e)
{
var visual = ElementComposition.GetElementVisual(MyBorder);
if (visual == null) return;
var compositor = visual.Compositor;
if (compositor == null) return;
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
offsetAnimation.Target = "Offset";
offsetAnimation.InsertExpressionKeyFrame(1, "this.FinalValue");
offsetAnimation.Duration = TimeSpan.FromMilliseconds(2000);
var implicaitAnimation = compositor.CreateImplicitAnimationCollection();
implicaitAnimation["Offset"] = offsetAnimation;
visual.ImplicitAnimations = implicaitAnimation;
MyBorder.Loaded -= MyBorder_Loaded;
}
我们把这个事件给组件的Loaded去订阅,我们得到组件的合成器,然后利用它创建一个三维动画,绑定属性到Offset上面,为什么这里是3keyframe,因为它创建的是Vector3DKeyFrameAnimation,专门用于动画化 Vector3D 类型的属性。
在 Composition API 的关键帧动画中,InsertExpressionKeyFrame 方法的第一个参数是 normalizedProgressKey,它是一个 0.0 到 1.0 之间的浮点数,表示动画时间轴上的位置:
0.0(或0) = 动画开始 (0%)0.5(或0.5f) = 动画中间 (50%)1.0(或1) = 动画结束 (100%) Generator.KeyFrameAnimation.cs:39-42 后面的不用想就是设置持续时间,之后我们通过合成器去创建隐式动画,将隐式动画里加入我们之前创建的动画,然后将它传递给visual的隐式动画里去执行。
代码生成器里,每种类型都有对应的KeyFrameAnimation类,而这个方法就是创建这个类实例的工厂方法。
类似地,其他属性也有对应的动画类型:
Opacity(float) →CreateScalarKeyFrameAnimation()Color→CreateColorKeyFrameAnimation()Scale(Vector3D) →CreateVector3KeyFrameAnimation()RotationAngle(float) →CreateScalarKeyFrameAnimation()
如果想要添加颜色动画,我们需要创建一个CompositionSolidColorVisual 作为子视觉元素,Border 的 CompositionVisual 只支持 Offset、Opacity、Scale 等通用属性,不支持 Color
如果需要颜色动画,必须创建 CompositionSolidColorVisual 作为子视觉元素,我们可以使用ElementComposition.SetElementChildVisual() 将子视觉元素附加到控件上。
我们可以知道的是,每个Visual都有一个ChildCompositionVisual的属性,这就是通过SetElementChildVisual()设置的子窗口元素。
ChildComositionVisual 会被添加到CompositionVisual.Children集合的最后,这就意味着他会渲染在其他的子元素之上。
一下是CompositionSolidColorVisual类可以设置的属性:
<Object Name="CompositionVisual" Abstract="true">
<Property Name="Root" Type="CompositionTarget?" Internal="true" />
<Property Name="Parent" Type="CompositionVisual?" Internal="true" />
<Property Name="Visible" Type="bool" Animated="true" DefaultValue="true"/>
<Property Name="Opacity" Type="float" Animated="true" DefaultValue="1"/>
<Property Name="Clip" Type="Avalonia.Platform.IGeometryImpl?" Internal="true" />
<Property Name="ClipToBounds" Type="bool" Animated="true" DefaultValue="true"/>
<Property Name="Offset" Type="Vector3D" Animated="true"/>
<Property Name="Size" Type="Vector" Animated="true"/>
<Property Name="AnchorPoint" Type="Vector" Animated="true"/>
<Property Name="CenterPoint" Type="Vector3D" Animated="true"/>
<Property Name="RotationAngle" Type="float" Animated="true"/>
<Property Name="Orientation" Type="Quaternion" DefaultValue="Quaternion.Identity" Animated="true"/>
<Property Name="Scale" Type="Vector3D" DefaultValue="new Avalonia.Vector3D(1, 1, 1)" Animated="true"/>
<Property Name="TransformMatrix" Type="Avalonia.Matrix" DefaultValue="Avalonia.Matrix.Identity" Animated="true" Internal="true"/>
<Property Name="AdornedVisual" Type="CompositionVisual?" Internal="true" />
<Property Name="AdornerIsClipped" Type="bool" Internal="true" />
<Property Name="OpacityMaskBrush" ClientName="OpacityMaskBrushTransportField" Type="Avalonia.Media.IBrush?" Private="true" />
<Property Name="Effect" Type="Avalonia.Media.IImmutableEffect?" Internal="true" />
<Property Name="RenderOptions" Type="Avalonia.Media.RenderOptions" />
<Property Name="ShouldExtendDirtyRect" Type="bool" Internal="true" />
</Object>
<Object Name="CompositionContainerVisual" Inherits="CompositionVisual"/>
<Object Name="CompositionSolidColorVisual" Inherits="CompositionContainerVisual">
<Property Name="Color" Type="Avalonia.Media.Color" Animated="true" />
</Object>
<Object Name="CompositionSurfaceVisual" Inherits="CompositionContainerVisual">
<Property Name="Surface" Type="CompositionSurface?" />
</Object>
CompositionSolidColorVisual 不是其他组件的基类,而是一个具体的实现类。继承关系如下:
CompositionObject (抽象基类)
└── CompositionVisual (抽象基类)
└── CompositionContainerVisual
├── CompositionSolidColorVisual (具体类)
└── CompositionSurfaceVisual (具体类)
和他同级的CompositionSurfaceVisual 是用于显示自定义渲染内容的视觉元素,它通过 Surface 属性关联一个 CompositionSurface 对象来显示复杂的图形内容。它只有一个特有属性 Surface,类型为 CompositionSurface?。
具体实现如下:
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Rendering.Composition;
using Avalonia.VisualTree;
using System;
namespace AvaloniaTestDemo.Views;
public partial class MainView : UserControl
{
private CompositionSolidColorVisual _colorVisual;
public MainView()
{
InitializeComponent();
MyBorder.Loaded += MyBorder_Loaded;
}
private void MyBorder_Loaded(object? sender, RoutedEventArgs e)
{
var visual = ElementComposition.GetElementVisual(MyBorder);
if (visual == null) return;
var compositor = visual.Compositor;
if (compositor == null) return;
_colorVisual = compositor.CreateSolidColorVisual();
_colorVisual.Size = new Vector(MyBorder.Bounds.Width, MyBorder.Bounds.Height);
_colorVisual.Color = Colors.Transparent;
_colorVisual.ClipToBounds = true;
ElementComposition.SetElementChildVisual(MyBorder, _colorVisual);
// 创建 Offset 动画
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
offsetAnimation.Target = "Offset";
offsetAnimation.InsertExpressionKeyFrame(1, "this.FinalValue");
offsetAnimation.Duration = TimeSpan.FromMilliseconds(2000);
// 创建 Color 动画
var colorAnimation = compositor.CreateColorKeyFrameAnimation();
colorAnimation.Target = "Color";
colorAnimation.InsertExpressionKeyFrame(1, "this.FinalValue");
colorAnimation.Duration = TimeSpan.FromMilliseconds(2000);
// 将两个动画添加到同一个 ImplicitAnimationCollection
var implicitAnimations = compositor.CreateImplicitAnimationCollection();
implicitAnimations["Offset"] = offsetAnimation;
// 为 visual 设置 Offset 动画
visual.ImplicitAnimations = implicitAnimations;
// 为 _colorVisual 设置 Color 动画
var colorImplicitAnimations = compositor.CreateImplicitAnimationCollection();
colorImplicitAnimations["Color"] = colorAnimation;
_colorVisual.ImplicitAnimations = colorImplicitAnimations;
MyBorder.Loaded -= MyBorder_Loaded;
}
private void Border_Click(object? sender, Avalonia.Input.PointerPressedEventArgs e)
{
if (sender is Border border)
{
var visual = ElementComposition.GetElementVisual(border);
if (visual == null) return; // Check if visual is null before accessing its properties
visual.Offset += new Vector3D(100, 100, 0); // x=100, y=100
_colorVisual.Color = Colors.Red;
}
}
}
为什么Border的ElementVisual没有Color,根据根据 composition schema 的定义,Border 对应的 CompositionVisual 类型是 CompositionBorderVisual,它继承自 CompositionDrawListVisual: BorderVisual.cs:10-18
而 CompositionVisual 基类只定义了通用的可动画属性,不包括 Color: composition-schema.xml:16-37
因此,要为 Border 添加颜色动画,必须:
- 创建一个
CompositionSolidColorVisual作为子视觉元素 - 使用
Border的Compositor来创建这个子视觉元素 - 为子视觉元素设置
Color的隐式动画 - 通过
ElementComposition.SetElementChildVisual()将其附加到Border上 所以我也推荐给父组件visual设置裁切子元素: visual.ClipToBounds = ture;
这个颜色变化就是邪教用法了,实际上我们很少这么操作,因为组件本身已经有了属性动画了。