自定义动画

.NET Multi-platform App UI (.NET MAUI) Animation 类是所有.NET MAUI动画的构造块,ViewExtensions 类中的扩展方法可创建一个或多个 Animation 对象。

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

运行使用 Animation 类创建的动画(可能包括也可能不包括子动画),可通过调用 Commit 方法来实现。 此方法指定动画的持续时间以及用于控制是否重复动画的回调。

注意

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

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

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

创建动画

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

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

在此示例中,Image 实例的 Scale 属性定义从值 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。 或者,可以将 finished 回调指定为 Animation 构造函数的参数。 但是,对单个动画而言,如果在 Animation 构造函数和 Commit 方法中均指定 finished 回调,则只会执行 Commit 方法中指定的回调。
  • 第七个参数 (repeat) 是一个回调,允许重复动画。 它在动画末尾调用,并返回 true 指示动画应重复播放。

在上述示例中,整体效果是使用 Linear 缓动函数创建动画,在 2 秒(2000 毫秒)内将 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 在 2 秒内从 1 到 2 对 Scale 属性进行动画处理。 然后,scaleDownAnimation 在 2 秒内从 2 到 1 对 Scale 属性进行动画处理。 当这两个缩放动画都发生时,rotateAnimation 在 4 秒内对 Rotation 属性进行动画处理(从 0 到 360)。 这两个缩放动画也都使用缓动函数。 SpringIn 缓动函数会导致 Image 实例在变大之前开始收缩,而 SpringOut 缓动函数会导致 Image 在完整动画处理即将结束时的大小小于实际大小。

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

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

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

取消动画

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

this.AbortAnimation ("SimpleAnimation");

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

创建自定义动画

到目前为止,此处显示的示例展示了使用 ViewExtensions 类中的方法可以同样实现的动画。 但是,Animation 类的优点是它可以访问回调方法,此方法在动画值更改时执行。 这允许通过回调实现任何所需的动画。 例如,下面的代码示例通过将页面的 BackgroundColor 属性设置为 Color.FromHsla 方法创建的 Color 值对其进行动画处理,其色调值范围为 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 方法作为使用 AnimationExtensions 类中 Animate 方法的扩展方法实现,以提供其功能。 这是因为 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 参数,即回调方法。 此回调的输入始终为范围为 0 到 1 的 double。 因此,在此示例中,ColorTo 方法定义其自己的转换 Func,该转换接受介于 0 到 1 之间的 double,并返回与该值对应的 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);

在此代码示例中,ColorTo 方法对 LabelTextColorBackgroundColor 属性、页面的 BackgroundColor 属性以及 BoxViewColor 属性进行动画处理。