Flutter 理解 Ticker、Animate 原理

3,963 阅读6分钟

1. 先说说 Ticker

为什么要先讲 Ticker 呢?我们在做动画,定义 AnimationController 时,必须传递 vsync 参数,通常这个参数为 this,由当前 State 类,混入(mixin)SingleTickerProviderStateMixin 提供。根据这个混入类的名字,我们可以看出它主要作用是提供了一个 Ticker。那么 Ticker 是什么?

class Ticker {
  Ticker(this._onTick, { this.debugLabel }) {
    ...
  }
  ...
  // 源码不是这样的
  bool muted;
  bool isTicking;
  bool isActive;
  bool isicking;
  ...
  TickerFuture start() {
    ...
    if (shouldScheduleTick) {  
      scheduleTick();
    }
    ...
  }
  void stop({ bool canceled = false }) {
    ...
  }
  void _tick(Duration timeStamp) {
    ...
  }

  @protected
  bool get shouldScheduleTick => !muted && isActive && !scheduled;
  @protectedvoid 
  scheduleTick({ bool rescheduling = false }) {  
    _animationId = SchedulerBinding.instance!.scheduleFrameCallback(_tick, rescheduling: rescheduling);
  }

  void _tick(Duration timeStamp) {  
    assert(isTicking);  
    assert(scheduled);  
    _animationId = null;  
    _startTime ??= timeStamp;  
    _onTick(timeStamp - _startTime!);  
    // The onTick callback may have scheduled another tick already, for  
    // example by calling stop then start again.  
    if (shouldScheduleTick)    
      scheduleTick(rescheduling: true);
  }
  ...
}

Ticker 的源码比较简单,这里插入一些主要代码。可以看出 Ticker 很像一个“滴答-滴答”响的时钟。可以开始、停止,有自己的状态。

看到它,总让我想到小时候家里的一块钟表,每晚睡觉时都可以听到它“滴答-滴答”。。。

看一下 Ticker 是如何工作的:start 方法调用 shceduleTick,scheduleTick 利用 SchedulerBinding 对象向系统调度一帧,并添加回调 _tick。注意 _tick 里又根据条件调用了 scheduleTick 从而造成循环调用。当条件不满足时,停止调度。

2. AnimationController

AnimationController 就是我们直接控制动画的对象了。分析一下主要字段和方法。

class AnimationController extends Animation<double>  
  with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
  AnimationController({  
    double? value,  
    this.duration,  
    this.reverseDuration,  
    this.debugLabel,  
    this.lowerBound = 0.0,     
    this.upperBound = 1.0,  
    this.animationBehavior = AnimationBehavior.normal,  
    required TickerProvider vsync,
  }) : assert(lowerBound != null),     
       assert(upperBound != null),     
       assert(upperBound >= lowerBound),     
       assert(vsync != null),     
       _direction = _AnimationDirection.forward {  
    _ticker = vsync.createTicker(_tick);  
    _internalSetValue(value ?? lowerBound);
  }  ...
  @overrideAnimationStatus get status => _status;
  late AnimationStatus _status;  
  TickerFuture forward({ double? from }) {  
    _direction = _AnimationDirection.forward;  
    if (from != null)    
      value = from;  
    return _animateToInternal(upperBound);
  }
  void reset() {  
    value = lowerBound;
  }
  void stop({ bool canceled = true }) {
    _simulation = null; 
    _lastElapsedDuration = null;  
    _ticker!.stop(canceled: canceled);
  }  ...
}

可以看到 AnimationController 构造方法,在初始化列表利用 vsync 对象,创建了 _ticker。动画启动 forward 方法,最终调用到 _startSimulation 方法,启动 _ticker。stop 方法通过 _ticker.stop 方法,停止动画。注意 Animation 的值 _value 在动画启动过程 _animateToInternal 和动画执行过程 _tick 中的变化。

可以看出 AnimationController 通过 Ticker 对系统请求帧调度,来执行动画。在动画的执行过程中计算当前动画值 _value,利用当前值 _value 更新动画。

3. Explicit Animations

Flutter 框架介绍动画时,根据用户不同需求介绍了两种基本动画,explicit animations 和 implicit animations。一个简单分辨,我们在某种场景下该使用这两种动画中的哪一个的判断条件是,“你是否需要干预动画”。如,动画完成时你需要重复执行、翻转动画、在动画执行过程中停止动画等。如果你需要干预动画请选择 explicit animations 否则,选择 implicit animations。

Flutter 框架为我们预制了一些 explicit animations,一般命名为 XxxTransition,继承自 AnimatedWidget。

AnimatedWidget

前边讲了 Ticker、AnimationController。我们有时钟,有值,那么这个值是怎么反映到动画上的呢?

AnimatedWidget

abstract class AnimatedWidget extends StatefulWidget {
  const AnimatedWidget({  
    Key key,  @required this.listenable,
  }) : assert(listenable != null),     
    super(key: key);  final Listenable listenable;
  ...
  @overrid
  _AnimatedState createState() => _AnimatedState();
  ...
}

_AnimatedState

class _AnimatedState extends State<AnimatedWidget> {  
  @override  void initState() {    
    super.initState();    
    widget.listenable.addListener(_handleChange);  
  }
  ...
  void _handleChange() {  
    setState(() {    
    // The listenable's state is our build state, and it changed already.  
    });
  }  ...
}

我们在构造 XxxTransition 时,需要传递一个 Animation 类型对象。这是一个 Listenable(通常就是我们的 AnimationController ),

交个父类(AnimatedWidget)。从 _AnimatedState 的代码中可以看到,在这里系统添加了监听回调更新状态。

那么我们从哪里发出通知告诉 Widget 需要更新呢?很容易想到动画值 _value 发生了变化,需要刷新页面执行动画。那么再看 AnimationController:

AnimationController

class AnimationController ... {
  ...
  void _tick(Duration elapsed) {  
    _lastElapsedDuration = elapsed;  
    final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;  
    assert(elapsedInSeconds >= 0.0);  
    _value = _simulation!.x(elapsedInSeconds).clamp(lowerBound, upperBound);  
    if (_simulation!.isDone(elapsedInSeconds)) {    
      _status = (_direction == _AnimationDirection.forward) ?      
        AnimationStatus.completed :      
        AnimationStatus.dismissed;    
    stop(canceled: false);  
    }  
    // 发出通知
    notifyListeners();  
    _checkStatusChanged();
  }
  ...
}

可以看到在 _tick 中每次计算完 _value 后,都会发出通知,并且检查动画状态。

当然,不止有这一个地方需要发出通知更新动画。其余出现地方,读者可以自行查看。

AnimatedBuilder

顺便提一下 AnimatedBuilder。AnimatedBuilder 也继承自 AnimatedWidget 。在系统提供给我们的 XxxTransition 无法满足我们的需求时,我们可以使用 AnimatedBuilder 来自定义我们的动画。这里就需要我们自己根据动画值 _value 来处理我们想修改的属性。

注意 builder 的第二个参数 child,在动画不涉及修改内部子 Widget 时,可以用这个字段做优化处理。

4. Implicit Animations

当我们只需要“一次性”动画,不需要 AnimationController 控制动画时,我们可以使用 implicit animation。

类似 explicit animation,Flutter 为我们提供了 AnimatedXxx ,作为 implicit animations 使用。

有了前边的知识,implicit animations 就简单了。我们这里主要看下 AnimationController、State相关内容。

AnimatedXxx 是 StatefulWidget,所以我们主要看看其对应的 State。

abstract class AnimatedWidgetBaseState<T extends ImplicitlyAnimatedWidget> extends ImplicitlyAnimatedWidgetState<T> {  
  @override  
  void initState() {    
    super.initState();    
    controller.addListener(_handleAnimationChanged);  
  }  
  void _handleAnimationChanged() {    
    setState(() { 
      /* The animation ticked. Rebuild with new animation value */ 
    });  
  }
}

可以看到在 AnimatedWidgetBaseState 里,做了监听,更新状态。

abstract class ImplicitlyAnimatedWidgetState<T extends ImplicitlyAnimatedWidget> extends State<T> with SingleTickerProviderStateMixin<T> {
  @protected
  AnimationController get controller => _controller;
  AnimationController _controller;
  ...
  @overridevoid 
  initState() {
    super.initState();
    _controller = AnimationController(  
      duration: widget.duration,  
      debugLabel: kDebugMode ? widget.toStringShort() : null,  
      vsync: this,
    );
    ...
  }
}

在 ImplicitlyAnimatedWidgetState 里,初始化了 AnimationController。

5. 必须得讲 Tween 了

上边提到了动画过程每次先计算值 _value 然后发出通知进行更新视图。但是并没有讲,值 _value 是如何计算的。显然这里要讲的 Tween 和计算值有关了。

class Tween<T extends dynamic> extends Animatable<T> {
  Tween({  
    this.begin,  
    this.end,
  });

  T? begin;
  T? end;

  @protected
  T lerp(double t) {  
    assert(begin != null);  
    assert(end != null);  
    return begin + (end - begin) * t as T;
  }
  @override
  T transform(double t) {  
    if (t == 0.0)    
      return begin as T;  
    if (t == 1.0)    
      return end as T;  
    return lerp(t);
  }
}

源码很简单,一个起始值,一个结束值,两个获取值的方法。

你可能会比较疑惑,这么简单为什么还要讲一下?

因为它没有看上去那么简单。

我们通常理解计算就是数值加减乘除,那我想动态改变颜色怎么办?形状呢?Flutter framework 已经为我们提供了关于这些属性变化的计算类型。

随便标记了两部分可能常用的 Tween。

下面讲一下 implicit animations 中 Tween 的应用。关于 explicit animations 的 Tween 的使用,读者可以自己查阅源码。

Implicit Animation Tween 的应用

我们在使用 Implicit Animation AnimatedXxx 时,好像并没有告诉它要怎么变化,那么它是如何进行变化的?

这里看下 _AnimatedPaddingState

class _AnimatedPaddingState extends AnimatedWidgetBaseState<AnimatedPadding> {
  EdgeInsetsGeometryTween _padding;
  @overridevoid 
  forEachTween(TweenVisitor<dynamic> visitor) {  
    _padding = visitor(_padding, widget.padding, (dynamic value) => EdgeInsetsGeometryTween(begin: value as EdgeInsetsGeometry)) as EdgeInsetsGeometryTween;
  }
  @override
  Widget build(BuildContext context) {  
    return Padding(    
      padding: _padding      
        .evaluate(animation)      
        .clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity),    
      child: widget.child,  
    );
  }
}

首先说下 forEachTween 方法,forEachTween 方法有一个参数 visitor 接收 3 个参数,Tween tween、T value、Tween Function(T value)。这个方法的作用主要是,根据 value 构造 tween 或者更新 tween,也就是 State 里的属性。

可以看到在 build 方法里,使用 padding 时,系统根据当前动画做了计算获取具体的“ padding值”。

Curve 

说了 Tween 那就得提一句 Curve 了。Curve 是描述动画变化速率的一个类,读者可以参考这个链接查看一些 Curve 效果。

Curves

总结

Ticker 就是驱动动画的“时钟”。AnimationController 封装隐藏了对 Ticker 的操作,我们通过 controller 控制 ticker 完成对动画执行的控制。Tween 的工作是根据当前时间计算特定动画类型 Animation 的值 value,进而更新 Widget 属性,做动画。