阅读 3791

Flutter框架分析(五)-- 动画

Flutter框架分析分析系列文章:

《Flutter框架分析(一)-- 总览和Window》

《Flutter框架分析(二)-- 初始化》

《Flutter框架分析(三)-- Widget,Element和RenderObject》

《Flutter框架分析(四)-- Flutter框架的运行》

《Flutter框架分析(五)-- 动画》

《Flutter框架分析(六)-- 布局》

《Flutter框架分析(七)-- 绘制》

前言

前四篇文章介绍了Flutter框架的全貌,相信大家对Flutter框架有了个整体的了解。这一系列文章始终是围绕着渲染流水线的的运行的各个阶段加以说明。我们知道在Vsync信号到来以后首先运行的是动画(Animate)阶段。而这个阶段是在从engine回调windowonBeginFrame函数开始运行的。那么这篇文章我们就来介绍一下Flutter框架的动画基本原理。

例子

所谓动画其实就是一系列连续变化的图片在极短的时间逐帧显示,在人眼看来就是动画了。这里我们举一个简单的例子先说明一下在Flutter中怎么运行一个动画:

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(home: LogoAnim()));
}

class LogoAnim extends StatefulWidget {
  _LogoAnimState createState() => _LogoAnimState();
}

class _LogoAnimState extends State<LogoAnim> with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      ..addListener(() {
        setState(() {
        });
      });
    controller.forward(from: 0);
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: FlutterLogo(),
      ),
    );
  }

  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

复制代码

这个动画是在手机屏幕上由小到大渐变的显示一个Flutter标志。从上述代码中我们可以看到在Flutter中实现一个动画要做这么几件事。

  1. 首先施加动画的Widget是个StatefulWidget。其State要混入(mixin) SingleTickerProviderStateMixin
  2. initState()里要加入和动画相关的初始化,这里我们实例化了两个类AnimationControllerAnimation。实例化AnimationController的时候我们传入了两个参数,一个是动画的时长,另一个是State自己,这里其实是利用到了混入的SingleTickerProviderStateMixin。实例化另一个Animation的时候,我们首先实例化的是一个Tween。这个类其实代表了从最小值到最大值的一个线性变化。所以实例化的时候要传入开始和结束值。然后调用animate()并传入之前的controller。这个调用会返回我们需要的Animation实例。显然我们需要知道动画的属性变化的时候的消息,所以这里会通过..addListener()Animation实例注册回调。这个回调只做一件事,那就是调用setState()来更新UI。最后就是调用controller.forward()来启动动画。
  3. 注意在build()函数里我们构建widget的时候用到了animation.value。所以这里的链条就是动画在收到回调后会调用setState(),而从我们上篇文章知道setState之后在渲染流水线的构建阶段会走到build()来重建Widget。重建的时候就用到了发生变化以后的animation.value。这个一帧一帧的循环,我们的动画就动起来了。
  4. 最后在dispose()的时候要记得调用controller.dispose()释放资源。

接下来我们就深入Flutter源码来看一下动画是如何运行的。

分析

首先我们来看一下混入到State中的SingleTickerProviderStateMixin

mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> implements TickerProvider {
  Ticker _ticker;

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

  @override
  void didChangeDependencies() {
    if (_ticker != null)
      _ticker.muted = !TickerMode.of(context);
    super.didChangeDependencies();
  }
}
复制代码

这个混入其实就做了一件事,实现createTicker()来实例化一个Ticker类。在另一个函数didChangeDependencies()里,有这样一行代码_ticker.muted = !TickerMode.of(context);。这行代码的意思是在这个带有动画的State的在element tree中的依赖发生变化的时候是否mute自己的_ticker。一个场景就是当前页的动画还在播放的时候,用户导航到另外一个页面,当前页的动画就没有必要再播放了,反之在页面切换回来的时候动画有可能还要继续播放,控制的地方就在这里,注意TickerMode.of(context)这种方式,我们在Flutter框架中很多地方都会见到,基本上就是从element tree的祖先里找到对应那个InheritedWidget的方式。

Ticker顾名思义,就是给动画提供vsync信号的吧。我们来看下源码一探究竟。

class Ticker {

  TickerFuture _future;
  
  bool get muted => _muted;
  bool _muted = false;
  set muted(bool value) {
    if (value == muted)
      return;
    _muted = value;
    if (value) {
      unscheduleTick();
    } else if (shouldScheduleTick) {
      scheduleTick();
    }
  }

  bool get isTicking {
    if (_future == null)
      return false;
    if (muted)
      return false;
    if (SchedulerBinding.instance.framesEnabled)
      return true;
    if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.idle)
      return true; 
    return false;
  }

  bool get isActive => _future != null;

  Duration _startTime;

  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;
  }
  
  void stop({ bool canceled = false }) {
    if (!isActive)
      return;

    final TickerFuture localFuture = _future;
    _future = null;
    _startTime = null;

    unscheduleTick();
    if (canceled) {
      localFuture._cancel(this);
    } else {
      localFuture._complete();
    }
  }


  final TickerCallback _onTick;

  int _animationId;

  @protected
  bool get scheduled => _animationId != null;

  @protected
  bool get shouldScheduleTick => !muted && isActive && !scheduled;

  void _tick(Duration timeStamp) {
    _animationId = null;

    _startTime ??= timeStamp;
    _onTick(timeStamp - _startTime);
    
    if (shouldScheduleTick)
      scheduleTick(rescheduling: true);
  }

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

  @protected
  void unscheduleTick() {
    if (scheduled) {
      SchedulerBinding.instance.cancelFrameCallbackWithId(_animationId);
      _animationId = null;
    }
    
  }
}
复制代码

可以看到Ticker主要在做的有点像控制一个计时器,有start()stop()mute。还记录当前自己的状态isTicking。我们需要关注的的是scheduleTick()这个函数:

@protected
  void scheduleTick({ bool rescheduling = false }) {
    _animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
  }
复制代码

你看,这里就跑到了我们之前文章说的SchedulerBinding里面去了。这里调度的时候会传入Ticker的回调函数_tick

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

在调度一帧的时候Ticker的回调函数_tick被加入了transientCallbacks。从之前对渲染流水线的分析,我们知道transientCallbacks会在vsync信号到来以后windowonBeginFrame回调里被执行一次。也就是说此时就进入到渲染流水线的动画Animate阶段了。

接着我们就看下Ticker的回调函数_tick做了什么:

  void _tick(Duration timeStamp) {
    _animationId = null;

    _startTime ??= timeStamp;
    _onTick(timeStamp - _startTime);

    if (shouldScheduleTick)
      scheduleTick(rescheduling: true);
  }
复制代码

这里的_onTick是在实例化Ticker时候传入的。_onTick被调用之后,Ticker如果发现自己的任务还没有完成,还要接着跳动,那就再来调度新一帧。所以你看动画的动力其实还是来自vsync信号的。

那么这个_onTick又是啥样的呢?这个函数是在实例化Ticker的时候传入的。而从上述分析我们又知道,Ticker的实例化是在调用TickerProvider.createTicker()的时候完成的。谁来调用这个函数呢?是AnimationController

  AnimationController({
    double value,
    this.duration,
    this.debugLabel,
    this.lowerBound = 0.0,
    this.upperBound = 1.0,
    this.animationBehavior = AnimationBehavior.normal,
    @required TickerProvider vsync,
  }) : _direction = _AnimationDirection.forward {
    _ticker = vsync.createTicker(_tick);
    _internalSetValue(value ?? lowerBound);
  }
复制代码

可见在其构造函数里就调用createTicker()了,传入的参数是_ticker。 接着看_ticker

  void _tick(Duration elapsed) {
    _lastElapsedDuration = elapsed;
    final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
    _value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound);
    if (_simulation.isDone(elapsedInSeconds)) {
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.completed :
        AnimationStatus.dismissed;
      stop(canceled: false);
    }
    notifyListeners();
    _checkStatusChanged();
  }
复制代码

这个回调里做这几件事,根据vsync到来以后的时间戳来计算更新一下新的值,这里计算用的是个_simulation。为啥叫这名?因为这是用来模拟一个物体在外力作用下在不同的时间点的运动状态的变化,这也算是动画的本质吧。

算出来新的值以后就调用notifyListeners()来通知各位观察者。还记的在开始的例子里我们实例化animation以后会通过..addListener()添加的回调吗?在这里这个回调就会被调用,也就是setState()会被调用了。接下来就是渲染流水线的构建(build)阶段了。

看到这里你可能会有疑问,事情都让AnimationController做了,那那个例子里的Tween是用来干啥的?

AnimationController的构造函数里我们可以看出来,它只管[0.0, 1.0]之间的模拟,也就是说不管动画怎么动,它任何时候只输出0.0到1.0之间的值,但是我们的动画有旋转角度,颜色渐变,图形变化以及更复杂的组合,显然我们得想办法把0.0到1.0之间的值转换为我们需要的角度,位置,颜色,透明度等等,这个转化就是由各种Animation来完成的,像例子里说的Tween,它的任务在动画期间把值从0渐变到300。怎么做呢?在实例化Tween以后我们会调用animate(),传入AnimationController实例。

  Animation<T> animate(Animation<double> parent) {
    return _AnimatedEvaluation<T>(parent, this);
  }

复制代码

你看,入参是个Animation<double>,这里也就是AnimationController。出参则是个Animation<T>。这样就完成了从[0.0, 1.0]到任意类型的变化。

具体怎么变呢?这个变化其实是在用到这个值得时候发生的,上面的例子里在State.build()函数里构造widget的时候会调用到animation.value这个getter。这其实调用的是_AnimatedEvaluation.value

 @override
  T get value => _evaluatable.evaluate(parent);
复制代码

_evaluatable就是Tween了,parent就是AnimationController了。所以呢,这个转换是Tween自己完成的,也是,只有它自己知道需要什么样的输出。

T evaluate(Animation<double> animation) => transform(animation.value);
复制代码

又到了transform()里了

@override
  T transform(double t) {
    if (t == 0.0)
      return begin;
    if (t == 1.0)
      return end;
    return lerp(t);
  }
复制代码

看到范围限制了吗?真正的转换又是在lerp()里完成的。

@protected
  T lerp(double t) {
    return begin + (end - begin) * t;
  }
复制代码

很简单的线性插值。

所里你要理解Flutter中的Tween动画是干什么的只要把握住它在自己的transform()函数中做了什么事情就知道了,从上可知Tween其实就是在做线性插值的动画而已。Tween是线性插值的,那如果我想搞非线性插值的动画呢?那就用CurvedAnimation。Flutter里有一大票各种各样的线性插值动画和非线性插值的动画,你甚至可以自己定义自己的非线性动画,只要重写变换函数就行了:

import 'dart:math';
class ShakeCurve extends Curve {
  @override
  double transform(double t) => sin(t * pi * 2);
}
复制代码

好了,关于Flutter框架里的动画就先分析到这里。

总结

本篇文章是Flutter框架分析系列文章的第五篇,本系列文章主要是以Flutter的渲染流水线为线索来分析其运行的。本篇主要针对的是渲染流水线的动画阶段,从底层的角度对Flutter的动画机制做了一个简要的分析。期望大家对Flutter的动画有一个基础的认识。在此之上的各种眼花缭乱的动画相关widgets都是在此基础上衍生出来的,所谓道生一,一生二,二生三,三生万物。掌握了道,就不会被万物所迷惑。