Avalonia的动画:
我们最常使用到的是avalonia的属性动画,比如Transitions标签,我们经常在style标签里去定义Property去指定Transitions,然后在Transitions标签中去影响所有的过渡类型。
根据Avalonia的实现,Transitions集合支持以下的过渡类型:
DoubleTransition
- Property 支持: 所有
double类型的属性 XamlIlTests.cs:31-33 - 常见属性:
Opacity、Width、Height、FontSize等 ScrollBar.xaml:74 - 影响的变换: 数值的平滑过渡
TransformOperationsTransition
- Property 支持:
RenderTransform属性 TransitionsPage.xaml:49 - 支持的变换函数:
translateX()/translateY()/translate()- 平移scaleX()/scaleY()/scale()- 缩放rotate()- 旋转skewX()/skewY()/skew()- 倾斜
CornerRadiusTransition
- Property 支持:
CornerRadius属性 ScrollBar.xaml:33 - 影响的变换: 圆角半径的平滑过渡
ThicknessTransition
- Property 支持:
Thickness类型的属性 - 常见属性:
Margin、Padding、BorderThickness - 影响的变换: 边距/内边距的平滑过渡
BrushTransition
- Property 支持:
IBrush类型的属性 - 常见属性:
Background、Foreground、BorderBrush - 影响的变换: 颜色/画刷的平滑过渡
FloatTransition
- Property 支持:
float类型的属性 - 影响的变换: 浮点数值的平滑过渡
VectorTransition
- Property 支持:
Vector类型的属性 - 影响的变换: 向量值的平滑过渡
PointTransition
- Property 支持:
Point类型的属性 - 影响的变换: 点坐标的平滑过渡
SizeTransition
- Property 支持:
Size类型的属性 - 影响的变换: 尺寸的平滑过渡
所有的过渡类型都是继承自TransitionBase,因此都是支持以下的属性的:
- Duration: 过渡持续时间 TransitionBase.cs:57-61
- Delay: 延迟开始时间 TransitionBase.cs:66-70
- Easing: 缓动函数 TransitionBase.cs:75-79
- Property: 要过渡的属性 TransitionBase.cs:83-87
这些过渡类型都是可以组合使用的,比如说我们常见的SchrollBar组件:
<Setter Property="Transitions">
<Transitions>
<CornerRadiusTransition Property="CornerRadius" Duration="0:0:0.1" />
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.1" />
</Transitions>
</Setter>
过渡类型都是实现了ITransition接口的,过渡只有在被附加到了可视树的时候才会生效。其中我们的TransformOperationsTransition是最为灵活的,可以处理所有的2D变换组合。
namespace Avalonia.Animation
{
/// <summary>
/// Defines how a property should be animated using a transition. /// </summary> public abstract class TransitionBase : AvaloniaObject, ITransition
{
/// <summary>
/// Defines the <see cref="Duration"/> property. /// </summary> public static readonly DirectProperty<TransitionBase, TimeSpan> DurationProperty =
AvaloniaProperty.RegisterDirect<TransitionBase, TimeSpan>(
nameof(Duration),
o => o._duration,
(o, v) => o._duration = v);
/// <summary>
/// Defines the <see cref="Delay"/> property. /// </summary> public static readonly DirectProperty<TransitionBase, TimeSpan> DelayProperty =
AvaloniaProperty.RegisterDirect<TransitionBase, TimeSpan>(
nameof(Delay),
o => o._delay,
(o, v) => o._delay = v);
/// <summary>
/// Defines the <see cref="Easing"/> property. /// </summary> public static readonly DirectProperty<TransitionBase, Easing> EasingProperty =
AvaloniaProperty.RegisterDirect<TransitionBase, Easing>(
nameof(Easing),
o => o._easing,
(o, v) => o._easing = v);
/// <summary>
/// Defines the <see cref="Property"/> property. /// </summary> public static readonly DirectProperty<TransitionBase, AvaloniaProperty?> PropertyProperty =
AvaloniaProperty.RegisterDirect<TransitionBase, AvaloniaProperty?>(
nameof(Property),
o => o._prop,
(o, v) => o._prop = v);
private TimeSpan _duration;
private TimeSpan _delay = TimeSpan.Zero;
private Easing _easing = new LinearEasing();
private AvaloniaProperty? _prop;
/// <summary>
/// Gets or sets the duration of the transition. /// </summary>public TimeSpan Duration
{
get { return _duration; }
set { SetAndRaise(DurationProperty, ref _duration, value); }
}
/// <summary>
/// Gets or sets delay before starting the transition. /// </summary>public TimeSpan Delay
{
get { return _delay; }
set { SetAndRaise(DelayProperty, ref _delay, value); }
}
/// <summary>
/// Gets the easing class to be used. /// </summary> public Easing Easing
{
get { return _easing; }
set { SetAndRaise(EasingProperty, ref _easing, value); }
}
/// <inheritdoc/>
[DisallowNull]
public AvaloniaProperty? Property
{
get { return _prop; }
set { SetAndRaise(PropertyProperty, ref _prop, value); }
}
AvaloniaProperty ITransition.Property
{
get => Property ?? throw new InvalidOperationException("Transition has no property specified.");
set => Property = value;
}
/// <inheritdoc/>
IDisposable ITransition.Apply(Animatable control, IClock clock, object? oldValue, object? newValue)
=> Apply(control, clock, oldValue, newValue);
internal abstract IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue);
internal override void BuildDebugDisplay(StringBuilder builder, bool includeContent)
{ base.BuildDebugDisplay(builder, includeContent);
DebugDisplayHelper.AppendOptionalValue(builder, nameof(Property), Property, includeContent);
DebugDisplayHelper.AppendOptionalValue(builder, nameof(Duration), Duration, includeContent);
} }
现在我们来讲一下常用的Easing效果,比较常用的有平滑过渡,线性的,二阶的、三阶的,还有可以创造出复杂动画效果的弹簧振子。
二次方缓动 (Quadratic)
- QuadraticEaseIn - 二次方加速
- QuadraticEaseOut - 二次方减速
- QuadraticEaseInOut - 二次方先加速后减速
三次方缓动 (Cubic)
- CubicEaseIn - 三次方加速
- CubicEaseOut - 三次方减速
- CubicEaseInOut - 三次方先加速后减速
四次方缓动 (Quartic)
- QuarticEaseIn - 四次方加速
- QuarticEaseOut - 四次方减速
- QuarticEaseInOut - 四次方先加速后减速
五次方缓动 (Quintic)
- QuinticEaseIn - 五次方加速
- QuinticEaseOut - 五次方减速
- QuinticEaseInOut - 五次方先加速后减速
指数缓动 (Exponential)
- ExponentialEaseIn - 指数加速
- ExponentialEaseOut - 指数减速
- ExponentialEaseInOut - 指数先加速后减速
圆形缓动 (Circular)
- CircularEaseIn - 圆形加速
- CircularEaseOut - 圆形减速
- CircularEaseInOut - 圆形先加速后减速
回弹缓动 (Back)
- BackEaseIn - 回弹加速(先后退再前进)
- BackEaseOut - 回弹减速(超过目标后回弹) KeySplineTests.cs:219-220
- BackEaseInOut - 回弹先加速后减速
弹性缓动 (Elastic)
- ElasticEaseIn - 弹性加速(震荡加速)
- ElasticEaseOut - 弹性减速(震荡减速) KeySplineTests.cs:223
- ElasticEaseInOut - 弹性先加速后减速
弹跳缓动 (Bounce)
- BounceEaseIn - 弹跳加速
- BounceEaseOut - 弹跳减速 BounceEaseUtils.cs:14
- BounceEaseInOut - 弹跳先加速后减速
自定义贝塞尔曲线
- SplineEasing - 使用自定义三次贝塞尔曲线控制点 (X1, Y1, X2, Y2) SplineEasing.cs:9
Animation支持的数值类型
我们在avalonia里经常使用Animations,比如我们常常创建一个延迟透明度和Translate平移的组合动画,通过设定visual对象和自定义动画关键字和延迟的方式创建:
private void TranslateAnimation() {
ScrollViewer navigationScrollViewer = NaviScrollViewer;
if (navigationScrollViewer.Content is StackPanel stackPanel)
{
var children = stackPanel.Children;
foreach (var child in children)
{
child.RenderTransform = new TranslateTransform(-50, 0);
child.Opacity = 0;
}for (int i = 0; i < children.Count; i++)
{
var child = children[i];
var delay = TimeSpan.FromMilliseconds(i * 200);
var animation = new Animation
{
Duration = TimeSpan.FromMilliseconds(500),
Delay = delay,
FillMode = FillMode.Forward,
Easing = new BackEaseOut(),
Children =
{
new KeyFrame
{
Cue = new Cue(0),
Setters =
{
new Setter(TranslateTransform.XProperty, -50.0), // 关键:作为附加属性使用
}
},
new KeyFrame
{
Cue = new Cue(1),
Setters =
{
new Setter(TranslateTransform.XProperty, 0.0),
}
}
}
};
var animation_opacity = new Animation
{
Duration = TimeSpan.FromMilliseconds(500),
Delay = delay,
FillMode = FillMode.Forward,
Children =
{ new KeyFrame
{
Cue = new Cue(0),
Setters =
{ new Setter(OpacityProperty, 0.0),
}
}, new KeyFrame
{
Cue = new Cue(1),
Setters =
{ new Setter(OpacityProperty, 1.0),
}
} } }; animation_opacity.RunAsync(child);
animation.RunAsync(child);
}
} }
除了Opacity,还有很多属性也是可以这样使用的:
数值类型
- Double:
Opacity、Width、Height、FontSize等<link to Repo AvaloniaUI/Avalonia: src/Avalonia.Base/Visual.cs:64-65><link to Repo AvaloniaUI/Avalonia: tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs:461-469> - Float: 浮点数属性
- Integer: 整数属性
变换属性
- TranslateTransform.X / Y: 平移变换<link to Repo AvaloniaUI/Avalonia: src/Avalonia.Base/Media/TranslateTransform.cs:15-22><link to Repo AvaloniaUI/Avalonia: tests/Avalonia.Base.UnitTests/Animation/KeySplineTests.cs:236-242>
- RotateTransform.Angle: 旋转角度<link to Repo AvaloniaUI/Avalonia: samples/RenderDemo/Pages/AnimationsPage.xaml:54-62>
- ScaleTransform.ScaleX / ScaleY: 缩放比例<link to Repo AvaloniaUI/Avalonia: samples/RenderDemo/Pages/AnimationsPage.xaml:58-71>
- SkewTransform.AngleX / AngleY: 倾斜角度<link to Repo AvaloniaUI/Avalonia: samples/RenderDemo/Pages/AnimationsPage.xaml:102-107>
颜色和画刷
- Color: 颜色值<link to Repo AvaloniaUI/Avalonia: src/Avalonia.Base/composition-schema.xml:63-63>
- Background / Foreground: 背景和前景画刷<link to Repo AvaloniaUI/Avalonia: samples/RenderDemo/Pages/AnimationsPage.xaml:116-137>
布局属性
- Width / Height: 宽度和高度<link to Repo AvaloniaUI/Avalonia: tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs:105-111>
- Margin: 外边距
- Padding: 内边距
视觉效果
- BoxShadow: 阴影效果<link to Repo AvaloniaUI/Avalonia: samples/RenderDemo/Pages/AnimationsPage.xaml:147-149>
- CornerRadius: 圆角半径
- BorderThickness: 边框粗细
Composition API 属性
- Offset: 位置偏移 (Vector3D)<link to Repo AvaloniaUI/Avalonia: src/Avalonia.Base/composition-schema.xml:24-24>
- Scale: 缩放 (Vector3D)<link to Repo AvaloniaUI/Avalonia: src/Avalonia.Base/composition-schema.xml:30-30>
- RotationAngle: 旋转角度<link to Repo AvaloniaUI/Avalonia: src/Avalonia.Base/composition-schema.xml:28-28>
- Visible: 可见性<link to Repo AvaloniaUI/Avalonia: src/Avalonia.Base/composition-schema.xml:19-19>
其他类型
- Boolean: 布尔值属性<link to Repo AvaloniaUI/Avalonia: src/Avalonia.Base/composition-schema.xml:62-62>
- Vector / Vector2 / Vector3: 向量类型<link to Repo AvaloniaUI/Avalonia: src/Avalonia.Base/composition-schema.xml:64-67>
- Quaternion: 四元数(用于 3D 旋转)<link to Repo AvaloniaUI/Avalonia: src/Avalonia.Base/composition-schema.xml:69-69>
需要题型一点的是,Children属性不是StackPanel的特权,一般来说,只要是Panel类型,那么都是有Children这个属性的,除了垂直、水平堆叠布局StackPanel以外,我们还有其他常用的Panel类型:
- Grid - 网格布局
- Canvas - 绝对定位布局
- DockPanel - 停靠布局
- WrapPanel - 自动换行布局
- VirtualizingStackPanel - 虚拟化堆叠布局
对于使用ItemsControl的场景,我们可以通过ItemsPresenter来获取实际的面板和子元素。
internal Control? ContainerFromIndex(int index)
{
if (Panel is VirtualizingPanel v)
return v.ContainerFromIndex(index);
return index >= 0 && index < Panel?.Children.Count ? Panel.Children[index] : null;
}
internal IEnumerable<Control>? GetRealizedContainers()
{
if (Panel is VirtualizingPanel v)
return v.GetRealizedContainers();
return Panel?.Children;
}
在 Composition API 的隐式动画示例中,也使用了 ItemsControl 配合 WrapPanel CompositionPage.axaml:9-14
<ItemsControl x:Name="Items">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.DataTemplates>
<DataTemplate DataType="pages:CompositionPageColorItem">
<Border
pages:CompositionPage.EnableAnimations="True"
Padding="10" BorderBrush="Gray" BorderThickness="2"
Background="{Binding ColorBrush}" Width="100" Height="100" Margin="10">
<TextBlock Text="{Binding ColorHexValue}"/>
</Border>
</DataTemplate>
</ItemsControl.DataTemplates>
视口判断
Avalonia可以通过EffectiveViewportChanged 事件来获取组件是否在视口中,利用它我们可以完成一些加载动画。
我们需要将要判断的控件放在scrollViewer中,如果没有在里面的话,那么被判定的组件的EffectiveViewport 通常会覆盖整个控件区域,导致 isInViewport 始终为 true
EffectiveViewport 的计算会递归考虑所有父控件的 ClipToBounds 和 RenderTransform 设置<link to Repo AvaloniaUI/Avalonia: src/Avalonia.Base/Layout/LayoutManager.cs:404-437>
private void OnAttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
MyBorder.EffectiveViewportChanged += OnEffectiveViewportChanged;
}
private void OnEffectiveViewportChanged(object? sender, EffectiveViewportChangedEventArgs e)
{
var viewport = e.EffectiveViewport;
var controlBounds = new Rect(MyBorder.Bounds.Size);
// 判断是否部分可见
bool isInViewport = viewport.Intersects(controlBounds);
// 判断是否完全可见
bool isFullyInViewport = viewport.Contains(controlBounds);
if (DataContext is MainViewModel vm)
{
vm.IsViewport = isInViewport.ToString();
vm.IsFullyviewport = isFullyInViewport.ToString();
}
}
于是我们这样设置,就可以在窗口上观察到我们想要的内容了,可以正确判断是否在视口上,并且是否完整显示了,并且,我们即便是将opacity调整为了0,也是只影响控件的视觉渲染,而不会影响布局和视口检测,我们完全可以用它来创建一些淡入的加载动画。
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Rendering.Composition;
using Avalonia.VisualTree;
using AvaloniaTestDemo.ViewModels;
using System;
namespace AvaloniaTestDemo.Views;
public partial class MainView : UserControl
{
public MainView()
{
InitializeComponent();
MyBorder.Loaded += MyBorder_Loaded;
MyBorder.AttachedToVisualTree += OnAttachedToVisualTree;
MyBorder.AttachedToLogicalTree += WrapPanelLoadAnimation;
}
private void OnAttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
MyBorder.EffectiveViewportChanged += OnEffectiveViewportChanged;
}
private void WrapPanelLoadAnimation(object? sender, LogicalTreeAttachmentEventArgs e)
{
if (ContentContainer is WrapPanel wrapPanel && ContentContainer is not null)
{
var childrens = wrapPanel.Children;
foreach (var child in childrens)
{
child.Opacity = 0;
child.EffectiveViewportChanged += EffectiveViewportChangedAnimation;
}
}
}
private void EffectiveViewportChangedAnimation(object? sender, EffectiveViewportChangedEventArgs e)
{
var viewport = e.EffectiveViewport;
if (sender is Border border)
{
var controlBounds = new Rect(border.Bounds.Size);
bool isInViewport = viewport.Intersects(controlBounds);
if (isInViewport)
{
border.Opacity = 1;
border.EffectiveViewportChanged -= EffectiveViewportChangedAnimation;
}
}
}
private void OnEffectiveViewportChanged(object? sender, EffectiveViewportChangedEventArgs e)
{
var viewport = e.EffectiveViewport;
var controlBounds = new Rect(MyBorder.Bounds.Size);
// 判断是否部分可见
bool isInViewport = viewport.Intersects(controlBounds);
// 判断是否完全可见
bool isFullyInViewport = viewport.Contains(controlBounds);
if (DataContext is MainViewModel vm)
{
vm.IsViewport = isInViewport.ToString();
vm.IsFullyviewport = isFullyInViewport.ToString();
}
}
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;
// 创建 Offset 动画
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
offsetAnimation.Target = "Offset";
offsetAnimation.InsertExpressionKeyFrame(1, "this.FinalValue");
offsetAnimation.Duration = TimeSpan.FromMilliseconds(2000);
var implicitAnimations = compositor.CreateImplicitAnimationCollection();
implicitAnimations["Offset"] = offsetAnimation;
// 为 visual 设置 Offset 动画
visual.ImplicitAnimations = implicitAnimations;
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.ClipToBounds = true;
visual.Offset += new Vector3D(100, 100, 0); // x=100, y=100
}
}
}
就这样,我们就可以实现当组件出现在视口的时候进行透明度变化加载(前提是要把属性动画设置好)。