Flutter动画原理

  • 概述

    作为前端开发技术,动画是一门前端语言所必须的,在Flutter中的动画是如何使用的呢?它的设计原理又是什么呢?本文就从源码的角度来分析一下Flutter动画。

  • 从使用开始

    当然,我们还是从使用API的入口开始,看一下动画机制是怎么驱动的。下面是一个demo:

    //需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
    class _ScaleAnimationRouteState extends State
        with SingleTickerProviderStateMixin {
      late Animation animation;
      late AnimationController controller;
    
      initState() {
        super.initState();
        //step1.
        controller = AnimationController(
          duration: const Duration(seconds: 2),
          vsync: this,
        );
    
        //匀速
        //图片宽高从0变到300
        //step2.
        final Animation curve = CurvedAnimation(parent: controller, curve: Curves.easeOut);
        //animation = Tween(begin: 0.0, end: 300.0).animate(controller)
        //Tween可以持有AnimationController也可以持有CurvedAnimation,他们都是Animation类型
        //animation = Tween(begin: 0.0, end: 300.0).animate(controller)
        animation = Tween(begin: 0.0, end: 300.0).animate(curve)
          ..addListener(() {
            setState(() => {});
          });
    
        //启动动画(正向执行)
        //step3.
        controller.forward();
      }
    
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Image.asset(
            "imgs/avatar.png",
            //Image的宽高被动画的更新值驱动
            width: animation.value,
            height: animation.value,
          ),
        );
      }
    
      dispose() {
        //路由销毁时需要释放动画资源
        controller.dispose();
        super.dispose();
      }
    }
    

    首先我们需要一个AnimationController,不管是什么类型的动画一定得有这个,它的作用就是控制动画的开始停止等(原理是被vsync屏幕刷新信号驱动来控制动画值的更新,后面会分析到)。

    这里给出了两个必须的参数,一个是duration,表示动画时长,这个值如果不传会在执行的时候报错,另一个参数vsync是TickerProvider,它被required修饰,所以必传,这里传入的是this,这个this指向的并不是State而是SingleTickerProviderStateMixin,SingleTickerProviderStateMixin继承自TickerProvider。

    step2部分的Tween其实你可以选择去掉,但是需要在AnimationController构造时指定lowerBound和upperBound来确定范围,同时,你要用AnimationController调用addListener方法,并在回调中调用setState方法,否则动画不会引起图片大小的变化。

    Curve是用来描述速度变化快慢的,他最终想要驱动动画值变化还是要调用AnimationController,它只不过是AnimationController的进一步封装,相当于包装模式;同理,在这个demo 中,Tween也是完成了对于Curve的封装调用。

    最后执行forward方法来开启动画。

    总之,在上面的这个demo中,AnimationController和其必须的构造参数对象是必须要有的,中间的Tween就是指定图片大小的上下限取值,可以用AnimationController全权受理。

    下面我们针对上面用到的这些类来逐个研究。

  • AnimationController

    上面我们说到,AnimationController是用来管理动画的启动停止等动作的,并且是通过vsync信号驱动的,我们从源码角度来验证它。

    它的构造方法中有这样一段代码:

    _ticker = vsync.createTicker(_tick);
    _internalSetValue(value ?? lowerBound);
    

    _internalSetValue方法逻辑比较简单,我们先看 _internalSetValue方法:

    void _internalSetValue(double newValue) {
      _value = newValue.clamp(lowerBound, upperBound);
      if (_value == lowerBound) {
        _status = AnimationStatus.dismissed;
      } else if (_value == upperBound) {
        _status = AnimationStatus.completed;
      } else {
        _status = (_direction == _AnimationDirection.forward) ?
          AnimationStatus.forward :
          AnimationStatus.reverse;
      }
    }
    

    _value 是动画即时值,未指定时是null,如果它小于lowerBound,clamp方法会把它置为lowerBound,同样,大于upperBound会把它置为upperBound,这里会根据是它是等于lowerBound还是等于upperBound而给 _status赋值为AnimationStatus.dismissed或AnimationStatus.completed,前者表示在起点,后者表示在终点;如果是在规定区间内的值则会根据 _direction是否为 _AnimationDirection.forward来决定 _status是AnimationStatus.forward还是AnimationStatus.reverse。

    总之,_internalSetValue的作用就是初始化 _value和 _status。

    回过头来,我们看到_ticker在这里是调用的vsync的createTicker方法创建的,假如我们State依赖的mixin是SingleTickerProviderStateMixin,我们去看看它的这个方法:

    @override
    Ticker createTicker(TickerCallback onTick) {
      _ticker = Ticker(onTick, debugLabel: kDebugMode ? 'created by $this' : null);
      return _ticker!;
    }
    

    我只接截取了关键代码,assert等容错逻辑没截取。

    可以看到,在这里创建了一个Ticker对象,他持有了一个函数对象onTick,这个onTick就是AnimationController构造方法里传入的 _tick函数:

    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();
    }
    

    可以看到这里会通过 _simulation产生新的 _value值,所以猜测,这里可能是屏幕刷新后会回调的地方,但是怎么验证呢?我们去找找它是怎么和屏幕刷新机制关联上的。

    _simulation是什么呢?我们发现他没有初始化值,又在什么时候赋值的呢?因为屏幕刷新一直在进行,动画的回调想要被它影响一定会在某个时间点建立关联,那在什么时候呢?开启动画的时候就是最恰当的时候,所以我们直接从动画开启方法之一的forward方法找起:

    TickerFuture forward({ double? from }) {
      _direction = _AnimationDirection.forward;
      if (from != null)
        value = from;
      return _animateToInternal(upperBound);
    }
    

    可见它内部调用了_animateToInternal方法,这个方法就是开启动画的方法,很明显它是一个受保护的内部方法,调用它的有四处,也是供外部调用的四个开启方法:forward、reverse、animateTo、animateBack。

    _animateToInternal方法内部主要是做一些对于当前动画的一些停止和对即将开始的新动画的初始化工作,关键是它最后调用了 _startSimulation方法:

    return _startSimulation(_InterpolationSimulation(_value, target, simulationDuration, curve, scale));
    
    TickerFuture _startSimulation(Simulation simulation) {
      _simulation = simulation;
      _lastElapsedDuration = Duration.zero;
      _value = simulation.x(0.0).clamp(lowerBound, upperBound);
      final TickerFuture result = _ticker!.start();
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.forward :
        AnimationStatus.reverse;
      _checkStatusChanged();
      return result;
    }
    

    可以发现,_startSimulation的代码和 _tick中的代码好相似啊,仔细看会发现, _startSimulation方法中simulation的x方法的传参固定是0.0,因为这是动画开启;又发现这里没有调用stop方法,因为动画刚开启不需要结束,结束的判断就是要交给上面的 _tick方法。

    重要的是我们在这里找到了 _ticker的开启方法:

    TickerFuture start() {
      _future = TickerFuture._();
      if (shouldScheduleTick) {
        scheduleTick();
      }
      if (SchedulerBinding.instance!.schedulerPhase.index > SchedulerPhase.idle.index &&
          SchedulerBinding.instance!.schedulerPhase.index < SchedulerPhase.postFrameCallbacks.index)
        _startTime = SchedulerBinding.instance!.currentFrameTimeStamp;
      return _future!;
    }
    

    在这里会创建TickerFuture,isActive方法中会根据 _future来判断动画是否正在运行。然后调用scheduleTick方法:

    @protected
    void scheduleTick({ bool rescheduling = false }) {
      assert(!scheduled);
      assert(shouldScheduleTick);
      _animationId = SchedulerBinding.instance!.scheduleFrameCallback(_tick, rescheduling: rescheduling);
    }
    

    看到这里我们需要先明白一个原理,就是Flutter 应用在启动时都会绑定一个SchedulerBinding,通过SchedulerBinding可以给每一次屏幕刷新添加回调,而Ticker就是通过SchedulerBinding来添加屏幕刷新回调的,这样一来,每次屏幕刷新都会调用TickerCallback。使用Ticker(而不是Timer)来驱动动画会防止屏幕外动画(动画的UI不在当前屏幕时,如锁屏时)消耗不必要的资源,因为Flutter中屏幕刷新时会通知到绑定的SchedulerBinding,而Ticker是受SchedulerBinding驱动的,由于锁屏后屏幕会停止刷新,所以Ticker就不会再触发。

    所以这里调用了SchedulerBinding.instance的scheduleFrameCallback方法来和 _tick回调函数建立关联:

    int scheduleFrameCallback(FrameCallback callback, { bool rescheduling = false }) {
      scheduleFrame();
      _nextFrameCallbackId += 1;
      _transientCallbacks[_nextFrameCallbackId] = _FrameCallbackEntry(callback, rescheduling: rescheduling);
      return _nextFrameCallbackId;
    }
    

    我们在handleBeginFrame方法中找到了使用 _transientCallbacks的地方:

    void handleBeginFrame(Duration? rawTimeStamp) {
          ...
        callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
          if (!_removedIds.contains(id))
            _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp!, callbackEntry.debugStack);
        });
          ...
    }
    

    这个方法会确保注册在window上:

    @protected
    void ensureFrameCallbacksRegistered() {
      window.onBeginFrame ??= _handleBeginFrame;
      window.onDrawFrame ??= _handleDrawFrame;
    }
    

    到这里,我们就找出了动画回调函数和屏幕刷新回调之间的关联逻辑。

    而在Ticker的stop方法中我们会找到回调移除的逻辑:

    void stop({ bool canceled = false }) {
      if (!isActive)
        return;
    
      // We take the _future into a local variable so that isTicking is false
      // when we actually complete the future (isTicking uses _future to
      // determine its state).
      final TickerFuture localFuture = _future!;
      _future = null;
      _startTime = null;
      assert(!isActive);
      //移除动画回调
      unscheduleTick();
      if (canceled) {
        localFuture._cancel(this);
      } else {
        localFuture._complete();
      }
    }
    
  • Simulation

    在上面的分析中有一个Simulation类,它有三个方法:

    /// The position of the object in the simulation at the given time.
    double x(double time);
    
    /// The velocity of the object in the simulation at the given time.
    double dx(double time);
    
    /// Whether the simulation is "done" at the given time.
    bool isDone(double time);
    

    根据注释可知,可以通过x方法、dx方法、isDone方法分别可以得出当前动画的进度、速度和是否已完成的标志。我们上面传入的是它的实现类 _InterpolationSimulation,它内部持有的属性有开始值、终点值和执行时长,执行时长保存在 _durationInSeconds属性:

    _durationInSeconds = (duration.inMicroseconds * scale) / Duration.microsecondsPerSecond;
    

    可以看到,这里会将设置的市场按照scale进行压缩,并最终取微秒单位,因为动画要求肉眼看不出卡顿,所以这里使用最细致的微秒单位。

    我们先看看它的x方法:

    @override
    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);
    }
    

    可见,这里会在初始值的基础上加上需要前进的值,正常的t是自上次屏幕刷新后消逝的时间,这里调用 _curve的transform方法也正是Curve的原理所在,它对进度作了进一步的处理。

    @override
    double dx(double timeInSeconds) {
      final double epsilon = tolerance.time;
      return (x(timeInSeconds + epsilon) - x(timeInSeconds - epsilon)) / (2 * epsilon);
    }
    

    同样,dx方法中是关于速度的算法。

    isDone最简单,只是通过是否超出_durationInSeconds来判断动画是否结束:

    @override
    bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds;
    

    记住这个Simulation所做的事情,在下面对于Curve和Tween的原理分析中,你会看到似曾相识的逻辑

  • Curve

    在上面对于Simulation的分析中我们提到,在x方法获取最新进度值的时候会通过Curve的transform方法处理一下,而这一点也正是在动画机制中Curve发挥作用的关键。

    transform方法最终在其父类ParametricCurve中会调用transformInternal方法,这个方法理应由子类实现,Curve的子类有很多,代表着很多中变化曲线算法,这里就不展开讲了,Curve的原理在上文中其实已经讲完了。

  • Tween

    Tween是用来设置属性范围的,可能有人会讲,属性范围我完全可以通过AnimationController的lowerBound和upperBound来指定,为什么还需要这个多余的类呢?

    把属性的类型不设限于double,你就能体会到他应该有的作用了。

    class Tween extends Animatable {
      Tween({
        this.begin,
        this.end,
      });
      ...
    }
    

    可见,Tween持有了一个泛型,表示可以设置任何属性,通过Tween,我们动画最终产生的值就不再只局限于double了,我可以转成任何直接使用的属性值。

    上文我们知道,AnimationController会在vsync的驱动下执行动画回调获取动画最新值,那么在使用那部分我们看到,我们引用的直接是Tween的animate方法返回的Animation对象的value值,那么这个value值和AnimationController即时获取的最新值是怎么关联的呢?

    我们先看一下Tween的animate方法返回的是什么:

    Animation animate(Animation parent) {
      return _AnimatedEvaluation(parent, this);
    }
    

    可以看到,是一个_AnimatedEvaluation实例,它的parent指定为AnimationController,我们来看一下它的value:

    @override
    T get value => _evaluatable.evaluate(parent);
    

    value通过 _evaluatable的evaluate方法获取, _evaluatable就是前面传入的this,也就是Tween本身:

    T evaluate(Animation animation) => transform(animation.value);
    

    可见evaluate又调用了transform方法:

    @override
    T transform(double t) {
      if (t == 0.0)
        return begin as T;
      if (t == 1.0)
        return end as T;
      return lerp(t);
    }
    

    transform方法中如果是中间值的话又会调用lerp方法:

    @protected
    T lerp(double t) {
      ...
      return (begin as dynamic) + ((end as dynamic) - (begin as dynamic)) * t as T;
    }
    

    怎么样,熟悉吧,是不是和_InterpolationSimulation中的x方法如出一辙,这就是默认的进度处理逻辑,不同的是,这里的begin、end和返回值都是泛型指定的,而不是固定的double,t为double是因为t是刷新的微秒进度值,一定是double。

    你可以定义自己的Tween类,继承并重写lerp方法即可完成自定义的属性值转换,比如ColorTween:

    @override
    Color? lerp(double t) => Color.lerp(begin, end, t);
    

    Color的lerp方法如下:

    static Color? lerp(Color? a, Color? b, double t) {
      assert(t != null);
      if (b == null) {
        if (a == null) {
          return null;
        } else {
          return _scaleAlpha(a, 1.0 - t);
        }
      } else {
        if (a == null) {
          return _scaleAlpha(b, t);
        } else {
          return Color.fromARGB(
            _clampInt(_lerpInt(a.alpha, b.alpha, t).toInt(), 0, 255),
            _clampInt(_lerpInt(a.red, b.red, t).toInt(), 0, 255),
            _clampInt(_lerpInt(a.green, b.green, t).toInt(), 0, 255),
            _clampInt(_lerpInt(a.blue, b.blue, t).toInt(), 0, 255),
          );
        }
      }
    }
    

    这就是Tween的原理,可以知道,Tween只不过是内部持有了AnimationController,然后通过AnimationController拿到进度值然后做自己的处理之后返回给value使用。

    Tween的parent换做持有CurvedAnimation也是一样的原理:

    @override
    double get value {
      final Curve? activeCurve = _useForwardCurve ? curve : reverseCurve;
      final double t = parent.value;
      if (activeCurve == null)
        return t;
      if (t == 0.0 || t == 1.0) {
        return t;
      }
      return activeCurve.transform(t);
    }
    

    可见,只不过CurvedAnimation持有的是Curve,然后通过Curve去调用AnimationController而已。

  • TickerProviderStateMixin

    前面讲了SingleTickerProviderStateMixin,其实TickerProvider还有一个子类就是TickerProviderStateMixin:

    @override
    Ticker createTicker(TickerCallback onTick) {
      _tickers ??= <_WidgetTicker>{};
      final _WidgetTicker result = _WidgetTicker(onTick, this, debugLabel: 'created by $this');
      _tickers!.add(result);
      return result;
    }
    
    void _removeTicker(_WidgetTicker ticker) {
      assert(_tickers != null);
      assert(_tickers!.contains(ticker));
      _tickers!.remove(ticker);
    }
    

    可见,这个mixin是用来出来State中有多个AnimationController的情况,主要是用于Ticker的释放清理工作。

  • 总结

    Flutter的动画原理的核心主要在于AnimationController和Scheduler关联,通过给Scheduler添加回调函数的方式和系统的vsync信号建立关联,从而由屏幕的刷新信号驱动到动画回调函数,在回调函数中调用setState方法更新界面,而界面控件中又引用了动画类的value,value是通过直接或者间接的方式从AnimationController中获取的最新当前进度值,这就完成了整个动画的流程。

    可以通过CurvedAnimation、Tween等包装类(当然也可以自定义)使用包装的设计模式去包装AnimationController,使得在使用获取的进度值之前可以对其有更灵活的处理。

你可能感兴趣的:(Flutter动画原理)