Xamarin.Forms 中的自定义动画

Animation 类是所有 Xamarin.Forms 动画的构建基块,通过 ViewExtensions 类中的扩展方法创建一个或多个 Animation 对象。 本文演示如何使用 Animation 类创建和取消动画、同步多个动画,以及创建自定义动画来对现有动画方法未进行动画处理的属性进行动画处理。

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

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

此外,Animation 类还有一个 IsEnabled 属性,可以检查该属性以确定操作系统是否禁用了动画,例如在激活节能模式的情况下。

创建动画

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

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

此代码定义了一个将 Image 实例的 Scale 属性从值 1 更改为值 2 的动画。 动画值(派生自 Xamarin.Forms)将传递给指定为第一个参数的回调,用于更改 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 毫秒)内将 ImageScale 属性从 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 类还包括 WithConcurrent 方法,可使用这些方法将子动画添加到父 Animation 对象。 但是,它们的 begin 和 finish 参数值不限于 0 到 1,但是只有对应于 0 到 1 范围的子动画部分才会处于活动状态。 例如,如果 WithConcurrent 方法调用定义了一个 Scale 属性为 1 到 6 的子动画,但 begin 和 finish 分别为 -2 和 3,则 begin 值 -2 对应于 Scale 值 1,并且 finish 值 3 对应于 Scale 值 6。 由于介于 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 = Color.Default);

生成的动画提供通过七彩色提升页面背景的外观。

有关创建复杂动画(包括贝塞尔曲线动画)的更多示例,请参阅使用 Xamarin.Forms 创建移动应用的第 22 章。

创建自定义动画扩展方法

ViewExtensions 类中的扩展方法从当前值到指定值对属性进行动画处理。 例如,这会增加创建用于生成颜色值更改动画的 ColorTo 动画方法的难度,原因如下:

此问题的解决方案是不让 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.R + t * (toColor.R - fromColor.R),
                     fromColor.G + t * (toColor.G - fromColor.G),
                     fromColor.B + t * (toColor.B - fromColor.B),
                     fromColor.A + t * (toColor.A - fromColor.A));
    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 参数内插 RGBA 值计算 Color 值。 然后,Color 值被传递到回调方法以应用于特定属性。

此方法允许 ColorTo 方法对任何 Color 属性进行动画处理,如以下代码示例所示:

await Task.WhenAll(
  label.ColorTo(Color.Red, Color.Blue, c => label.TextColor = c, 5000),
  label.ColorTo(Color.Blue, Color.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(Color.Blue, Color.Red, c => boxView.Color = c, 4000);

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