自定义动画

.NET 多平台应用 UI (.NET MAUI) Animation 类是所有 .NET MAUI 动画的构建基块,类中的 ViewExtensions 扩展方法创建一个或多个 Animation 对象。

创建 Animation 对象时必须指定多个参数,包括要进行动画处理的属性的开始值和结束值,以及更改属性值的回调。 对象 Animation 还可以维护可以运行和同步的子动画的集合。 有关详细信息,请参阅 子动画

通过调用Commit方法,可以运行使用Animation类创建的动画(这其中可能包含子动画)。 此方法指定动画的持续时间,并包含其他参数,其中包括用于控制是否重复动画的回调函数。

注释

Animation 类具有一个 IsEnabled 属性,可以检查以确定操作系统是否已禁用动画,例如激活节能模式时。

在 Android 上,动画遵循系统动画设置:

  • 如果系统动画被禁用(通过辅助功能或开发人员功能),新动画将立即跳转到其完成状态。
  • 如果在动画正在进行时激活设备的节能模式,动画将立即跳转到其完成状态。
  • 如果在动画正在进行时,设备的动画持续时间设置为零(已禁用),并且 API 版本为 33 或更高版本,则动画将立即跳转到其完成状态。

创建动画

创建 Animation 对象时,通常至少需要三个参数,如以下代码示例所示:

var animation = new Animation(v => image.Scale = v, 1, 2);

在此示例中,实例属性的ScaleImage动画从值 1 到值 2 定义。 动画值将传递给指定为第一个参数的回调,该回调用于更改 Scale 属性值。

动画是通过调用Commit方法启动的。

animation.Commit(this, "SimpleAnimation", 16, 2000, Easing.Linear, (v, c) => image.Scale = 1, () => true);

注释

该方法 Commit 不返回对象 Task 。 而是通过回调方法来发送通知。

方法中 Commit 指定了以下参数:

  • 第一个参数 (owner) 标识动画的所有者。 这可以是应用动画的视觉元素,也可以是另一个视觉元素,例如页面。
  • 第二个参数 (name) 使用名称标识动画。 动画的名称与所有者相结合,以唯一标识动画。 然后,可以使用此唯一标识来确定动画是正在运行的(AnimationIsRunning),还是取消它(AbortAnimation)。
  • 第三个参数 (rate) 指示对构造函数中 Animation 定义的回调方法的每个调用之间的毫秒数。
  • 第四个参数 (length) 指示动画的持续时间(以毫秒为单位)。
  • 第五个参数 (Easing) 定义要在动画中使用的缓动函数。 可以将缓动函数指定为Animation构造函数的参数。 有关缓动函数的详细信息,请参阅 缓动函数
  • 第六个参数 (finished) 是一个回调,将在动画完成时执行。 此回调函数接受两个参数,第一个参数表示最终值,第二个参数是 bool,在动画被取消时,该参数会设置为 true。 或者,您可以在构造函数Animation中将finished回调指定为一个参数。 但是,如果在finished构造函数和Animation方法中都指定了回调,而只使用单个动画,则只有Commit方法中指定的回调会被执行。
  • 第七个参数 (repeat) 是一个回调,允许重复动画。 它在动画末尾调用,并返回 true 指示应重复动画。

在上面的示例中,整体效果是创建一个动画,该动画在 2 秒内(2000 毫秒)使用Linear缓动函数,将Image实例的Scale属性从 1 增加到 2。 每次动画完成时,其 Scale 属性将重置为 1,并且动画重复。

注释

可以为每个动画创建一个 Animation 对象,然后在每个动画上调用 Commit 该方法,从而构建彼此独立运行的并发动画。

子动画

Animation 类还支持子动画,这些动画是 Animation 将其他 Animation 对象添加为子级的对象。 这使一系列动画可以运行和同步。 以下代码示例演示如何创建和运行子动画:

var parentAnimation = new Animation();
var scaleUpAnimation = new Animation(v => image.Scale = v, 1, 2, Easing.SpringIn);
var rotateAnimation = new Animation(v => image.Rotation = v, 0, 360);
var scaleDownAnimation = new Animation(v => image.Scale = v, 2, 1, Easing.SpringOut);

parentAnimation.Add(0, 0.5, scaleUpAnimation);
parentAnimation.Add(0, 1, rotateAnimation);
parentAnimation.Add(0.5, 1, scaleDownAnimation);

parentAnimation.Commit(this, "ChildAnimations", 16, 4000, null, (v, c) => SetIsEnabledButtonState(true, false));

或者,可以更简洁地编写代码示例:

new Animation
{
    { 0, 0.5, new Animation (v => image.Scale = v, 1, 2) },
    { 0, 1, new Animation (v => image.Rotation = v, 0, 360) },
    { 0.5, 1, new Animation (v => image.Scale = v, 2, 1) }
}.Commit (this, "ChildAnimations", 16, 4000, null, (v, c) => SetIsEnabledButtonState (true, false));

在这两个示例中,将创建一个父 Animation 对象,然后向其添加其他 Animation 对象。 方法的前两个参数 Add 指定何时开始和完成子动画。 参数值必须介于 0 和 1 之间,并表示指定子动画将在父动画中处于活动状态的相对时间段。 因此,在此示例中,动画 scaleUpAnimation 的前半部分将处于活动状态, scaleDownAnimation 动画后半部分将处于活动状态,并且 rotateAnimation 整个持续时间将处于活动状态。

此示例的总体效果是动画在 4 秒(4000 毫秒)内发生。 scaleUpAnimation 动画将 Scale 属性从 1 动画到 2,持续 2 秒。 然后,scaleDownAnimation在2秒内将Scale属性从 2 动画化到 1。 当这两个缩放动画都发生时,rotateAnimation 会改变 Rotation 属性从 0 到 360,持续 4 秒。 这两个缩放动画也都使用缓动函数。 SpringIn缓动函数会导致Image实例在变大之前最初收缩,SpringOut缓动函数导致Image其实际大小在完整动画结束时变小。

使用子动画的对象与不使用子动画的对象之间存在 Animation 许多差异:

  • 使用子动画时, finished 子动画的回调指示子动画何时完成, finished 传递给该方法的 Commit 回调指示整个动画完成的时间。
  • 使用子动画时,从方法的truerepeat回调返回Commit不会导致动画重复,但动画将继续运行而不使用新值。
  • 在方法中包含 Commit 缓动函数并且缓动函数返回大于 1 的值时,动画将终止。 如果缓动函数返回的值小于 0,该值将固定到 0。 若要使用返回小于 0 或大于 1 的值的缓动函数,必须在其中一个子动画而不是方法中 Commit 指定它。

Animation 类还包括 WithConcurrent 可用于向父 Animation 对象添加子动画的方法。 但是,它们的beginfinish参数值不限于 0 到 1,但只有对应于 0 到 1 范围的子动画部分才会处于活动状态。 例如,如果通过 WithConcurrent 方法调用定义了一个从 1 到 6 针对 Scale 属性的子动画,但 beginfinish 的值分别为 -2 和 3,begin 值为 -2 时对应的 Scale 值为 1,finish 值为 3 时对应的 Scale 值为 6。 由于介于 0 和 1 之外的值在动画中没有任何作用,因此该 Scale 属性将仅从 3 到 6 进行动画处理。

取消动画

应用可以通过调用 AbortAnimation 扩展方法取消自定义动画:

this.AbortAnimation ("SimpleAnimation");

由于动画由动画所有者和动画名称的组合唯一标识,因此必须指定运行动画时指定的所有者和名称才能取消动画。 因此,此示例将立即取消页面拥有的、命名为SimpleAnimation的动画。

创建自定义动画

到目前为止,此处显示的示例演示了与类中 ViewExtensions 的方法同样可以实现的动画。 但是,类的优点 Animation 是它有权访问回调方法,该方法在动画值更改时执行。 这允许回调函数实现任何所需的动画。 例如,下面的代码示例通过使用Color.FromHsla方法创建Color值来设置BackgroundColor属性,以为页面的该属性添加动画效果,其色调值范围为 0 到 1。

new Animation (callback: v => BackgroundColor = Color.FromHsla (v, 1, 0.5),
  start: 0,
  end: 1).Commit (this, "Animation", 16, 4000, Easing.Linear, (v, c) => BackgroundColor = Colors.Black);

生成的动画通过彩虹的颜色展现页面背景逐渐变化的效果。

创建自定义动画扩展方法

类中的 ViewExtensions 扩展方法会将属性从其当前值动画至指定值。 这样就很难创建一种动画方法,例如用于将颜色从一个 ColorTo 值变换到另一个值的方法。 这是因为不同的控件具有不同的类型 Color属性。 VisualElement类定义了一个BackgroundColor属性,但这并不总是需要进行动画处理的Color属性。

此问题的解决方案是没有 ColorTo 方法以特定 Color 属性为目标。 相反,可以使用回调方法编写,该方法会将内 Color 插值传回调用方。 此外,该方法将采用 start 和 end Color 参数。

该方法 ColorTo 可以作为扩展方法实现,该方法使用 Animate 类中的 AnimationExtensions 方法提供其功能。 这是因为 Animate 该方法可用于定位不属于类型的 double属性,如以下代码示例所示:

public static class ViewExtensions
{
    public static Task<bool> ColorTo(this VisualElement self, Color fromColor, Color toColor, Action<Color> callback, uint length = 250, Easing easing = null)
    {
        Func<double, Color> transform = (t) =>
            Color.FromRgba(fromColor.Red + t * (toColor.Red - fromColor.Red),
                           fromColor.Green + t * (toColor.Green - fromColor.Green),
                           fromColor.Blue + t * (toColor.Blue - fromColor.Blue),
                           fromColor.Alpha + t * (toColor.Alpha - fromColor.Alpha));
        return ColorAnimation(self, "ColorTo", transform, callback, length, easing);
    }

    public static void CancelAnimation(this VisualElement self)
    {
        self.AbortAnimation("ColorTo");
    }

    static Task<bool> ColorAnimation(VisualElement element, string name, Func<double, Color> transform, Action<Color> callback, uint length, Easing easing)
    {
        easing = easing ?? Easing.Linear;
        var taskCompletionSource = new TaskCompletionSource<bool>();

        element.Animate<Color>(name, transform, callback, 16, length, easing, (v, c) => taskCompletionSource.SetResult(c));
        return taskCompletionSource.Task;
    }
}

该方法 Animate 需要一个 transform 参数,即回调方法。 此回调的输入始终是 double,其范围介于 0 到 1 之间。 因此,在此示例中,该方法 ColorTo 定义其自己的转换,该转换 Func 接受 double 范围从 0 到 1,并返回 Color 与该值对应的值。 通过对两个提供的Color参数的RedGreenBlueAlpha值进行插值计算,来得出Color的值。 Color然后将该值传递给要应用于属性的回调方法。 这种方法允许 ColorTo 方法对任何指定的 Color 属性进行动画处理:

await Task.WhenAll(
  label.ColorTo(Colors.Red, Colors.Blue, c => label.TextColor = c, 5000),
  label.ColorTo(Colors.Blue, Colors.Red, c => label.BackgroundColor = c, 5000));
await this.ColorTo(Color.FromRgb(0, 0, 0), Color.FromRgb(255, 255, 255), c => BackgroundColor = c, 5000);
await boxView.ColorTo(Colors.Blue, Colors.Red, c => boxView.Color = c, 4000);

在此代码示例中,该方法为LabelTextColorBackgroundColor属性、页面的BackgroundColor属性以及BoxViewColor属性设置动画。