跳至主要內容

Flutter 动画分析之 AnimationController

JI,XIAOYONG...大约 9 分钟

本文讨论的 Flutter 动画主要限定在:随着每一帧的变化,修改 Flutter Widget 的大小、颜色、位置等属性,使之看起来从一种状态渐变为另外一种状态 这一范围。

Flutter 中关于动画的类有很多,为了便于分析,将其分为两大类:

  • Flutter 框架底层实现动画的各个类,比如 AnimationController、Ticker、Tween、Curve 等
  • 基于底层实现,提供进一步封装的 Flutter 动画相关的 Widget 类,比如 AnimatedWidget、ImplicitlyAnimatedWidget 和他们的子类。

他们的关系如下:

AnimationController 通过 Ticker 监听 Flutter 屏幕帧刷新:

Flutter 中 AnimationController 与 Ticker 关系
Flutter 中 AnimationController 与 Ticker 关系

每一帧刷新后,AnimationController 监听并根据 Duration 等计算出当前的 Animation.value;
此外也可以通过 Tween 将 double 类型转化为其他的类型比如 Offset 等;
上述两种方式中 value 都是随着时间线性变化,而 Curve 可以与 CurveTween、AnimationController 等结合使 value 实现非线性的变化。

Flutter 各种动画底层类关系
Flutter 各种动画底层类关系

当随着时间变化,计算出当前的 Animation.value 时,便可以根据此值修改 Flutter Widget 的各个属性,从而实现动画的视觉效果。

Flutter 中与动画有关的 Widget
Flutter 中与动画有关的 Widget

从上图可以看到,Flutter 提供的动画 Widget 主要分为两大类:

  • ImplicitlyAnimatedWidget 隐式动画,关于动画的开始、停止等都封装在 Widget 内部,只要 Widget 前后传入的值不同便可以自动从 old 渐变到 new,内置的这些类主要以 AnimatedFoo 命名。

  • AnimatedWidget,显式动画,需要使用者自己创建 Animation(一般是 AnimationController)并通过其管理动画,此类 Widget 主要是监听 AnimationController 的值并刷新 Widget 的内容。

    此类 Widget 主要有三种使用方式:

    • 继承 AnimatedWidget
    • 使用 AnimatedBuilder
    • 使用各种内置的 AnimatedWidget 子类,一般以 FooTransition 命名。

对于 Flutter 中这些与动画有关的类如何选择,Flutter 官方给了一张图:

如何实现 Flutter 中的动画
如何实现 Flutter 中的动画

简单来说,Flutter 有一些内置的动画,在要写动画的时候,可以依次考虑(实现程度由易到难):


本文主要分析 AnimationController 及其相关类。

源码分析

AnimationController 是 Flutter 中动画的基石,它继承自 Animation,根据不同的方法调用创建对应的 Simulation 并开始监听传入的 Ticker;

每当 Flutter 中帧刷新时,从_simulation 中获取当前 Animation._value 并对 listener 发出通知;

这样需要使用 Animation.value 的各个 Widget 便可以根据其值修改自身属性,实现动画视觉效果。

Animation

根据上述分析,我们首先来看一下 Animation 类:

An animation with a value of type T

Animation 主要的作用是持有 value 和 status,并允许其他对象监听二者的变化。

abstract class Animation<T> extends Listenable implements ValueListenable<T> {
    /// The current value of the animation.
    T get value;
    /// The current status of this animation.
    AnimationStatus get status;
}

Animation 继承自 Listenable,实现 ValueListenable 接口,其他类可以通过 addListener/removeListener 或者 addStatusListener/removeStatusListener 监听 Animation 的 value 或者 status 变化。

Animation 共有 4 种状态:dismissed、forward、reverse、completed。

除此之外,Animation.drive 方法可以创建一个新的将传入的 Animatable 应用到自身的 Animation。

  
  Animation<U> drive<U>(Animatable<U> child) {
    assert(this is Animation<double>);
    // 通过 Animatable.transform 将此 Animation.value 的值从 double 转化为 U
    return child.animate(this as Animation<double>);
  }

也就是说,提供了将 Animation<double> 转化为 Animation<U> 类型的方法。

其他子类

除了后面要详细分析的 AnimationController 之外,Animation 还有如下子类:

class说明
AlwaysStoppedAnimation永远停留在指定值的 animation
ProxyAnimation代理 Animation,适用于动画可能会变化的情况,先使用 ProxyAnimation 应用一个 Animation,然后再修改为其他 Animation(不用手动添加移除 listener)
ReverseAnimation返回和当前 animation 反方向的 Animation
CurvedAnimation可以为传入的 animation 使用 Curve 的 animation,适用于将原先线性变化的 Animation 改为非线性的
TrainHoppingAnimation监听传入的两个 Animation<double>,当第二个 Animation 的值超过第一个 Animation 的值时自动切换到第二个并回调 onSwitchedTrain。如果一开始两个 Animation 就在同一个值,则切换到第二个并不会调用 onSwitchedTrain。
CompoundAnimation可以组合多个 Animation<T>的接口,当 Animation<T> next 处于运动状态时返回 next 的状态,否则返回 Animation<T> first 的状态。

对于上述的 CompoundAnimation,子类只需重写 double get value 方法即可,其有三个子类:

  • AnimationMean 返回 first 和 next 值和的二分之一,值为 double
  • AnimationMax<T extends num> 返回 first 和 next 中最大值
  • AnimationMin<T extends num> 返回 first 和 next 中最小值

AnimationController

A controller for an animation.

尽管有各种子类,但 Animation 最常用的子类是 AnimationController,使用者可以用它来控制、监听动画、创建其他动画。

class AnimationController extends Animation<double> with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
}

构造方法

AnimationController 有两种构造方法,这两种构造方法主要会初始化以下变量:

  • double value 当前值
  • Duration? duration,reverseDuration 动画正向、反向运行的时长,初始化时可以为 null,但在实际开始动画之前,至少保证 duration 不为 null
  • double lowerBound,double upperBound 当 value 触达此值时,animation 分别被认为是 dismissed、completed
  • Ticker? _ticker 由构造方法中必传的 TickerProvider vsync 创建

他的两个构造方法分别是:

  • AnimationController()

    默认构造方法,double lowerBound,double upperBound 默认分别为 0.0,1.0

  • AnimationController.unbounded()

    不限制 value 值的构造方法,double lowerBound,double upperBound 默认分别为 double.negativeInfinity,double.infinity。适用于没有预设编辑的物理模拟动画。

在这两个构造方法内部,都会通过_ticker = vsync.createTicker(_tick)创建_ticker,并保证当_ticker 回调时执行AnimationController._tick()方法。

这里的 TickerProvider 主要有 2 种:

  • SingleTickerProviderStateMixin 适用于 State 中只有一个 AnimationController 的情况,性能更好
  • TickerProviderStateMixin 适用于 State 生命周期内有多个 AnimationController 的情况

除了从 Animation 继承的方法外,AnimationController 还提供了如下方法,用于操纵动画:

操纵从double? from正向/反向开始动画

  • TickerFuture forward({ double? from })
  • TickerFuture reverse({ double? from })

操纵正向/反向开始朝向double target开始动画,此类动画还可以改变DurationCurve

  • TickerFuture animateTo(double target, { Duration? duration, Curve curve = Curves.linear })
  • TickerFuture animateBack(double target, { Duration? duration, Curve curve = Curves.linear })

上述四种方法,内部都是通过 AnimationController._animateToInternal()方法实现,而此方法内部又是执行 AnimationController._startSimulation(),除此之外,还有以下几类方法内部也是基于_startSimulation() 方法实现,主要区别在于不同方法方法创建了不同的 Simulation:

  • TickerFuture repeat({ double? min, double? max, bool reverse = false, Duration? period })
  • TickerFuture fling({ double velocity = 1.0, SpringDescription? springDescription, AnimationBehavior? animationBehavior })
  • TickerFuture animateWith(Simulation simulation)

_startSimulation

AnimationController._startSimulation()方法是其实现动画的基石,其内部主要是开启了_ticker 并发出通知:

TickerFuture _startSimulation(Simulation simulation) {
    assert(simulation != null);
    assert(!isAnimating);
    _simulation = simulation;
    _lastElapsedDuration = Duration.zero;
    _value = simulation.x(0.0).clamp(lowerBound, upperBound);
    // 开始 ticker
    final TickerFuture result = _ticker!.start();
    _status = (_direction == _AnimationDirection.forward) ?
      AnimationStatus.forward :
      AnimationStatus.reverse;
    _checkStatusChanged();
    return result;
  }

_ticker.start()方法最终通过SchedulerBinding.instance.scheduleFrameCallback()方法监听 Flutter Framework 的帧刷新,并回调 AnimationController._tick 方法

_tick

在此方法内部根据当前时间和_simulation 获取_value 并发出通知。

  void _tick(Duration elapsed) {
    _lastElapsedDuration = elapsed;
    final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
    assert(elapsedInSeconds >= 0.0);
    // 通过_simulation 获取当前动画的_value
    _value = _simulation!.x(elapsedInSeconds).clamp(lowerBound, upperBound);
    // 如果动画已经结束了,就停止监听
    if (_simulation!.isDone(elapsedInSeconds)) {
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.completed :
        AnimationStatus.dismissed;
      stop(canceled: false);
    }
    notifyListeners();
    _checkStatusChanged();
  }

其他方法

  • void resync(TickerProvider vsync) 使用 vsync 重新创建新的_ticker
  • void stop({ bool canceled = true }) 停止动画,不会触发通知,默认标记动画为 canceled
  • void dispose() 释放资源,动画被标记为 canceled

Simulation

从上面的分析中,我们看到 Simulation 在 AnimationController 动画中也起到很重要的作用:Simulation 主要是在一维空间对物理进行位置、速度等建模。

abstract class Simulation {
  /// Initializes the [tolerance] field for subclasses.
  Simulation({ this.tolerance = Tolerance.defaultTolerance });

  // 指定时间的位置
  double x(double time);

  // 指定时间的速度
  double dx(double time);

  /// Whether the simulation is "done" at the given time.
  bool isDone(double time);

  // 公差,如果两个数值相差小于等于此值则认为二者相等,用于 isDone 中
  Tolerance tolerance;

  
  String toString() => objectRuntimeType(this, 'Simulation');
}

在 AnimationController 中常用的子类有以下两种:

_InterpolationSimulation

_InterpolationSimulation(this._begin, this._end, Duration duration, this._curve, double scale){...}

其 x() 方法中除了 t 为 0.0 或 1.0 的情况外,其余时候依靠 Curve(默认为 Curves.linear)计算值。

  double x(double timeInSeconds) {
    final double t = (timeInSeconds / _durationInSeconds).clamp(0.0, 1.0);
    if (t == 0.0)
      return _begin;
    else if (t == 1.0)
      return _end;
    else
      return _begin + (_end - _begin) * _curve.transform(t);
  }

_RepeatingSimulation

_RepeatingSimulation(double initialValue, this.min, this.max, this.reverse, Duration period, this.directionSetter){}

没有 Curve,其 double x(double timeInSeconds) 方法可以自动判断是否需要反向并修改方向(会触发 status 改变通知):

  double x(double timeInSeconds) {
    assert(timeInSeconds >= 0.0);

    final double totalTimeInSeconds = timeInSeconds + _initialT;
    final double t = (totalTimeInSeconds / _periodInSeconds) % 1.0;
    final bool isPlayingReverse = (totalTimeInSeconds ~/ _periodInSeconds).isOdd;

    if (reverse && isPlayingReverse) {
      directionSetter(_AnimationDirection.reverse);
      return ui.lerpDouble(max, min, t)!;
    } else {
      directionSetter(_AnimationDirection.forward);
      return ui.lerpDouble(min, max, t)!;
    }
  }

此外比较特殊的是他的 isDone 方法一致返回 false,表示不会主动结束动画。

SpringSimulation

用于 fling 方法,创建弹性的模拟

总结

经过上述分析,应该能了解 Flutter 动画中 AnimationController 的作用:

  • AnimationController 通过传入的 TickerProvider 创建并监听 Ticker,确保 Ticker 收到系统帧回调时触发 AnimationController._tick 方法;

  • 提供 forward,reverse,animateTo,animateBack,repeat,fling,animateWith 等方法创建不同的 Simulation 并开启 Ticker,从而可以通过 SchedulerBinding.instance.scheduleFrameCallback 监听 Flutter 每一帧刷新。

    并且在 animateTo,animateBack 方法中可以使用 Curve 实现非线性变化。

  • 当 Flutter 帧刷新时,_tick 方法中通过_simulation 结合时间,lowerBound 和 upperBound 等获取当前值_value 和状态_status 并发出通知。

  • 使用者可以通过 AnimationController 继承自父类 Animation 的 addListener/removeListener、addStatusListener/removeStatusListener 监听动画的值和状态

  • 使用者可以从父类 Animation<double>继承的Animation<U> drive<U>(Animatable<U> child)方法使用 Animatable<U>Animation<double>的 animation 创建一个新的Animation<U>,从而可以得到可以随时间变化过渡的 Offset、Size 等动画。

  • stop 方法可以停止动画

参考资料

动画效果介绍 flutter.cnopen in new window

Animation api.flutter.devopen in new window

AnimationController api.flutter.devopen in new window

TickerProvider api.flutter.devopen in new window

SingleTickerProviderStateMixin api.flutter.devopen in new window

文章标题:《Flutter 动画分析之 AnimationController》
本文作者: JI,XIAOYONG
发布时间: 2022/08/21 10:43:24 UTC+8
更新时间: 2023/12/30 16:17:02 UTC+8
written by human, not by AI
本文地址: https://jixiaoyong.github.io/blog/posts/6ec43bd8.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 许可协议。转载请注明出处!
你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.8