Flutter - 动画基础知识点

1,706 阅读9分钟

Flutter 动画部分 API 有些凌乱,我看有的人说挺简单的,但是在我来看系统 API 不是很友好,逻辑思路是扭着的,不知道有没有相同感受的朋友,这篇文章中我详细说说,把点都写上,给大家一个全面的参考

官方文档


动画原理

这部分解释还得是官方文档解释的清晰啊,我直接复制过来了:

在任何系统的UI框架中,动画实现的原理都是相同的,即:在一段时间内,快速地多次改变UI外观;由于人眼会产生视觉暂留,所以最终看到的就是一个“连续”的动画,这和电影的原理是一样的。我们将UI的一次改变称为一个动画帧,对应一次屏幕刷新,而决定动画流畅度的一个重要指标就是帧率FPS(Frame Per Second),即每秒的动画帧数。很明显,帧率越高则动画就会越流畅!一般情况下,对于人眼来说,动画帧率超过16FPS,就比较流畅了,超过32FPS就会非常的细腻平滑,而超过32FPS,人眼基本上就感受不到差别了。由于动画的每一帧都是要改变UI输出,所以在一个时间段内连续的改变UI输出是比较耗资源的,对设备的软硬件系统要求都较高,所以在UI系统中,动画的平均帧率是重要的性能指标,而在Flutter中,理想情况下是可以实现60FPS的,这和原生应用能达到的帧率是基本是持平的。

Fluuter 理论刷新率是 120FPS,大家想啊,Flutter 自有线程负责 UI 计算,自然可以用系统 60帧更高的帧数去做,所以这也是 Flutter 在有复杂动画时依然能达到接近 60帧的原理所在,因为实际上是用比 60更高的帧数去做的,自然丢帧就少了,总有新的帧可以显示


动画 API 代码分层:

  • Animation - 动画 API 的基础,所有的动画最终都是用 Animation 类型来承载。Animation 主要职能是保存动画每一帧的数值
  • Curve - 动画的插值器,用于动画每帧数值的计算的,这个大家都是熟悉
  • AnimationController - 动画的控制器,动画操控,监听部分都写在这里
  • Tween - 数值区间,主要用来处理不同数据类型的数据,比如 widget 动画中最常用的 都 double,color 等
  • Ticker - 负责分发 async,触发页面 rebuild,详细的去看源码研究,代码一般用不上这个

看上图,Flutter 动画 API 中根接口就是2个:AnimationAnimatableAnimation 作为承载数据的基本类型自有他自己是 Animation 类型的,其他 API 都是 Animatable 的,Animatable 可以理解位动画中数据的变化,Animatable 通过 animate 这个方法返回一个 Animation 对象来参与后面的计算,基本就是这个套路

官方解释:

主角当然是我们的Animation类了,它可以借助Animatable进行强化 Animatable通过animate函数接收一个Animation对象,再返回Animation对象,这不就是包装吗? 通过Animation对象回调即可获取规律变画的值,进行渲染。这是动画的基本


吐槽下 API 设计:

有人说这个包装的思路,但是我认为这个套路真是糟糕至极

  • 第一,上手就不容易理解,容易懵,非常不友好
  • 其次从代码的功能分层来看也不合理,AnimationController 作为控制器天然就是最外层 API,其他 API 作为一个个功能,应该 .setAnimationController 或是 builder 里面,而不是每个功能 API .animate 再返回 Animation 类型的对象,而且就连 AnimationController 也要 .animate 办顶到别人身上,而不是别人绑定到 AnimationController 身上,整个 API 全 NM 是反着来的,恶心死了

要是我的话,我会通过

AnimationController
  ..durntion
  ..Curve
  ..Tween

这种方式来设计代码,可以选择放在 AnimationController 构造函数里面,也可以专门搞一个 builder 出来,总之这样逻辑思路是顺应大家平时思路的,写着舒服,看着简单,容易理解,好扩展。Animation依然还是承载具体数据的类,只不过变成功能类了,Animatable 作为增加数据变化的抽象接口使用,屏蔽在内部,不暴露出来。


动画监听方法,就是2个:

  • addListener() - 每一帧都会被调用一次,一般就是用 setState() 来触发UI重建
  • addStatusListener() - 监听动画状态的变化

动画状态:

  • dismissed - 动画在起始点停止
  • forward - 动画正在正向执行
  • reverse - 动画正在反向执行
  • completed - 动画在终点停止

Animation

Animation 是抽象基类,是 Flutter 动画 API 这块的基础。Animation 中就是储存动画每一帧的数据,其他 API 使用代理方式(其实说装饰着模式也行,虽然代码看着不像,但是是这个意思)Animation 添加功能,然后还返回 Animation 这个根类型对象

Animation 使用泛型来承载不同的数据类型,常用的就是:Animation<Color>Animation<Size>Animation<double> 这3个了


Curve

final CurvedAnimation curve = new CurvedAnimation(
  parent: controller, 
  curve: Curves.easeIn);
Curve 是动画插值器,Flutter 里面包含下面的类型:
  • linear
  • decelerate
  • ease
  • easeIn
  • easeOut
  • easeInOut
  • fastOutSlowIn
  • bounceIn
  • bounceOut
  • bounceInOut
  • elasticIn
  • elasticOut
  • elasticInOut
详细的不解释了,大家看下面这篇效果图吧:
自定义 Curve 也是可以的,不过我一直不明白这里的数学知识...
例如我们定义一个正弦曲线:
class ShakeCurve extends Curve {
  @override
  double transform(double t) {
    return math.sin(t * math.PI * 2);
  }
}

AnimationController

AnimationController 用于控制动画,主要就是3个方法:

  • forward() - 启动
  • stop() - 停止
  • reverse() - 反向播放

AnimationController 派生自 Animation<double>,每一帧动画的数值可以通过 Animation.value 获取,默认的数值区间是:0.0 到 1.0,想更改数据范围,看下面的设置

final AnimationController controller = new AnimationController(
    duration: const Duration(milliseconds: 2000), vsync: this);
// 可以通过lowerBound和upperBound 来指定数值区间:
final AnimationController controller = new AnimationController( 
 duration: const Duration(milliseconds: 2000), 
 lowerBound: 10.0,
 upperBound: 20.0,
 vsync: this
);

Tween

AnimationController 默认的数值范围就是[0.0,1.0],即便可以设置也只是可以设置最大值,最小值,若是我们想从数值大到数值小的变化呢,这时候我们就需要可以更自由设置数据的类:Tween

Tween 可以自由设置起始值和结束值:

final Tween doubleTween = new Tween<double>(begin: -200.0, end: 0.0);
final Tween colorTween = new ColorTween(
  begin: Colors.transparent,
  end: Colors.black54);

Tween 可以计算很多类型的数据,每种类型都有专门对应的子类:

  • ColorTween
  • SizeTween
  • IntTween
  • RectTween
  • ReverseTween
  • StepTween
  • ConstantTween

Ticker

Ticker 这部分是官方的解释,我看着还行,一般我们不会用到这个,用也就是个 this 罢了,大家记住这个负责分发 async 信号就行了

当创建一个AnimationController时,需要传递一个vsync参数,它接收一个TickerProvider类型的对象,它的主要职责是创建Ticker,定义如下:

abstract class TickerProvider {
  //通过一个回调创建一个Ticker
  Ticker createTicker(TickerCallback onTick);
}

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

通常我们会将SingleTickerProviderStateMixin添加到State的定义中,然后将State对象作为vsync的值,这在后面的例子中可以见到。

页面 widget 需要继承 Ticker 类以实现动画的页面刷新:SingleTickerProviderStateMixin


动画循环执行

目前只找到了添加监听这一种方法

  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(seconds: 1), vsync: this);
    //图片宽高从0变到300
    animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
    animation.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        //动画执行结束时反向执行动画
        controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        //动画恢复到初始状态时执行动画(正向)
        controller.forward();
      }
    });

    //启动动画(正向)
    controller.forward();
  }

Flutter 动画的基础套路

1. 构建 AnimationController 对象 2. 构建 Tween 对象,使用 animate 方法绑定 AnimationController 3. widget 的某些属性关联动画数值:animation.value 4. animationController?.forward(); 开始执行动画 5. 页面 widget 继承 SingleTickerProviderStateMixin

这个大家看代码就行了,万年不变的套路就是这样:

class TestWidgetState extends State<TestWidget> with SingleTickerProviderStateMixin{

  Animation<double> animation;
  AnimationController animationController;

  @override
  void initState() {
    super.initState();

    animationController = AnimationController(
      duration: Duration(milliseconds: 300),
      vsync: this,
    );

    animation = CurvedAnimation(parent: animationController, curve: Curves.bounceInOut);

    animation = Tween(begin: 100.0, end: 300.0).animate(animationController)
      ..addListener(() {
        setState(() {});
      });
  }

  dispose() {
    animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Container(
            margin: EdgeInsets.only(bottom: 20),
            width: animation.value,
            height: animation.value,
            color: Colors.blueAccent,
          ),
          RaisedButton(
            child: Text("放大"),
            onPressed: () {
              animationController?.forward();
            },
          ),
        ],
      ),
    );
  }
}

AnimatedWidget

AnimatedWidget 这块大家看看就行了,一般不直接用他。AnimatedWidget 是一个基类,帮我们处理了 addListener()和setState() 的工作。思路是我们自己写动画 widget 时继承 AnimatedWidget 这个类,在构造方法中传入 animation 动画对象,在布局中绑定动画属性参与的属性值

看看官方文档的代码,熟悉就行,一般我们不这么写

class AnimatedImage extends AnimatedWidget {
  AnimatedImage({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);

  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return new Center(
      child: Image.asset("imgs/avatar.png",
          width: animation.value,
          height: animation.value
      ),
    );
  }
}
class ScaleAnimationRoute1 extends StatefulWidget {
  @override
  _ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
}

class _ScaleAnimationRouteState extends State<ScaleAnimationRoute1>
    with SingleTickerProviderStateMixin {

  Animation<double> animation;
  AnimationController controller;

  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(seconds: 3), vsync: this);
    //图片宽高从0变到300
    animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
    //启动动画
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedImage(animation: animation,);
  }

  dispose() {
    //路由销毁时需要释放动画资源
    controller.dispose();
    super.dispose();
  }
}

AnimatedBuilder

AnimatedBuilder 是对上面 AnimatedWidget 的进一步简化,AnimatedWidget 还要求我们把布局写到继承 AnimatedWidget 的类里面,可是实际上我们都是在页面中书写所有的页面 widget 的,AnimatedBuilder 实现了关于 widget 和 animation 的抽象,widget 和 animation 都变成了传递的数据了,这样我们在构建动画时才能真的实现灵活,随心所欲,书写方便了

class GrowTransition extends StatelessWidget {
  GrowTransition({this.child, this.animation});

  final Widget child;
  final Animation<double> animation;

  Widget build(BuildContext context) {
    return new Center(
      child: new AnimatedBuilder(
          animation: animation,
          builder: (BuildContext context, Widget child) {
            return new Container(
                height: animation.value, 
                width: animation.value, 
                child: child
            );
          },
          child: child
      ),
    );
  }
}