Flutter 隐式动画 Implicit Animation 初解

首先容许我日常怒黑一波Flutter的文档

谈到Flutter动画的时候,我们一般想到的是什么?

如果你照着Flutter文档手册看完,脑子里肯定一堆Tween, AnimationController, forward()等等乱七八糟的东西。写肯定是能写,但是我觉得写这种动画的时候心里无数个草泥马在奔腾。遍地的animatedTo(), forward(), reset()还有设置Tween的起始结束还要监听动画状态,写个简单的还好,写一堆简直折磨人。说好的“声明式编程”呢?这一用到动画简直回归面向过程了。然而你不用动画的话,那简直辣眼睛。

但偏偏Flutter文档里的东西全是这玩意。唯一一个有“声明式”意味的差不多就是Hero组件了。坑人的是,明明Flutter文档其他地方提到了存在着”Implicit animation”这个东西,但是文档中就是不讲。间接导致其他教程也不怎么讲。

0. 简介

什么是Implicit Animation呢?

简单来说就是用setState (还有其他机制,比如StreamBuilder)就能呼唤出来的动画。对于实现了隐式动画的组件,只要Widget被更新,那么一个过渡动画就会自动产生并且播放。在了解这个之前,写过渡动画简直就是精神折磨,使用了之后,代码终于变成了简洁的“声明式”风格。

也就正如Flutter文档里提到的

Since Flutter’s framework encourages developers to describe the interface configuration matching the current application state, a mechanism exists to implicitly animate between these configurations.

1.初步

Flutter当然自带一批Implicit Animation控件。以Anminated名称起头的控件差不多有一半以上都是Implicit Animation,然而诡异的是,介绍动画的教程里就是不提,嗨呀,又逼着用户一个一个翻documentation。

最有代表性的AnimatedContainer,用法和Container一模一样。

var width = 200;
var height = 200;
var color = Colors.blue;

//.....

AnimatedContainer(
  width: width,
  height: height,
  color: color,
  duration: Duration(seconds: 1),
),

duration用于控制过渡动画的时间。

当使用setState改变参数的时候,AnimatedContainer自动生成过渡动画并且播放

setState(() {
  height=500;
  width=500;
  color=Colors.orange;
});

《Flutter 隐式动画 Implicit Animation 初解》
《Flutter 隐式动画 Implicit Animation 初解》

其他的自带组件同理。善于使用可以极大的简化代码,至少什么AnimatedController一堆东西在这种情况下就可以免了。我个人甚至都觉得不带Animated的基础控件最好就别用,两边用法都一摸一样,白加的动画效果不要白不要。

Padding都有Animated的版本了。用用用!!!不用还是人?#狗头

《Flutter 隐式动画 Implicit Animation 初解》
《Flutter 隐式动画 Implicit Animation 初解》 这么多组件,文档里一声不吭真的好么?

2. 进阶

其实上,使用自带的Implicitly Animated组件已经足够完成任务了(如果某个需求你用自带的隐式动画组件完成不了,那很有可能进阶的也完成不了,或者写起来一样恶心,直接显式的使用AnimationController吧)

但是如果想要更进一步,自定义一些声明式动画的组件,也是可以的

2.1 Flutter原理

首先需要了解Flutter内部的渲染流程

2.1.1 Widget树与Element树

Flutter为了渲染出整个界面,内部维护着三个结构:Widget树,Element树,RenderObject树。

平时我们编程时写的东西其实上都是在写各个Widget,然而用户实际上看到的是一堆的RenderObject,而RenderObject树直接生成自Element树,Element树实际上记录了整个应用内部所有元素的结构信息。接下来我们只讨论Element树和Widget树的关系。

《Flutter 隐式动画 Implicit Animation 初解》
《Flutter 隐式动画 Implicit Animation 初解》

每一个Widget只是一组设计图纸,告诉Flutter应当怎么构建Element树、应当怎么渲染。但是设计图纸内部并不一定会被真正执行到,也不包含任何运行时的数据。是否需要渲染、用哪个设计图纸渲染,是由Element来决定的,这样能避免很多不必要的开销。

这也能回答很多初学者的疑问,build(context)函数里每一句话都是在初始化新的Widget,那刷新界面的时候运算量岂不是要爆炸?事实上,Widget只是一组设计图纸,并不一定会改变最终界面,所以构造和销毁的开销非常小。

2.1.2 刷新界面的匹配过程

那刷新界面的时候Flutter到底进行了什么魔法操作?

正常来说,Element树和Widget树是保持对应的,每个Element要知道自己对应的Widget是哪一个。

刷新界面时,被setState糟蹋过的Element(被称为“脏”的Element)会去找对应的Widget,调用Widget的build函数内部的图纸进行重建。build函数会按照内部语句的字面意思(字面意思就是,一堆new new new … 只不过Dart2.0开始new关键字可以省略了),新建一大堆崭新的子Widget,老Widget直接扔掉(与此相对地,一堆Element失去匹配)。一大堆崭新的子Widget出现之后,失去匹配的Element会尝试去匹配新的Widget。匹配上了,直接复用Element,匹配不上,Element会被扔掉重建。

匹配的标准是什么?

首先,匹配的范围仅限于父节点下的所有直接子节点(不会匹配到稀奇古怪的地方)

在没有Key的情况下,只看Widget的类型是不是正确的(没错,其他的不看,只看Type)

有Key的情况下,再比较Key

2.1.3 State与Widget

有人可能要问,Widget内部不能保存运行时状态的话,那StatefulWidget是什么?

StatefulWidget仍然是Widget,但是那个伴随的State<T>可不是Widget!所以Flutter才有这种看起来很奇特的设计——一个StatefulWidget居然要写两个类。

State与对应的Element生命周期非常相似,你可以认为State绑定在对应的Element身上,嵌在Element树上。只要Element不被销毁,那么State也不会被销毁。所以Widget死了一批又一批,但是State却可以活着保存所有的状态信息。

这就是我们接下来自定义隐式动画组件的基础。

2.1.4 隐式动画原理

在调用setState()之后,对应的Element被标记为“脏”,下次刷新时会去调用对应的Widget来重建。

由于setState改变了参数,因此重建的Widget实际上和老Widget不一样(废话)。但是因为毕竟还是同一个类型的,Element能匹配的上。Element无压力存活。

《Flutter 隐式动画 Implicit Animation 初解》
《Flutter 隐式动画 Implicit Animation 初解》

这个时候Element绑定的State就要发挥作用了。对于隐式动画组件,State里会额外保存当前动画进程下整个组件的信息。State同时也会跑去检查Widget里保存的信息。这时候,因为匹配的Widget换了一个新的,State会发现自己保存的信息和Widget里查到的信息不一样,于是State就着手生成并播放动画了。

这也就是setState呼出隐式动画的原理。

Flutter宣称这样的设计有额外的好处。因为放出来的动画只与 1. State保存的当前信息 2. 新Widget的信息 有关,和被送进垃圾回收的老Widget一点关系都没有。所以可以完美应对“过渡动画放了一半没放完,新的setState()又来了”这种窘境——直接从当前State状态出发再准备新的动画就完事了。卧槽我都感动哭了好吗,这个功能要初学者自己用显式动画写,估计瞬移bug+崩溃满天飞,或者就是一堆队列代码逻辑长到爆。

(Flutter你这么牛逼,你文档里为什么不讲讲怎么用呢)

2.2 具体编写

涉及到两个类的用法,ImplicitlyAnimatedWidget 和 AnimatedWidgetBaseState

直接上sample code吧,我觉得上代码比我讲的要快

class MyAnimatedWidget extends ImplicitlyAnimatedWidget {
  MyAnimatedWidget({
    Key key,
    this.param, //导致动画的参数
    Curve curve = Curves.linear,
    @required Duration duration,
  }) :super(key: key, curve: curve, duration: duration);
  final double param;
  
  @override
  _MyAnimatedWidgetState createState() => _MyAnimatedWidgetState();
}

class _MyAnimatedWidgetState extends AnimatedWidgetBaseState<MyAnimatedWidget> {
  Tween<double> _param; //State内部保存的当前状态信息,类型为Tween
  
  @override
  void forEachTween(TweenVisitor<dynamic> visitor) {
    _param = visitor(_param, widget.param, (value) => Tween<double>(begin: value));
    //widget.param表示来自新Widget的参数,也就是动画的结尾
    //意义即为,根据当前状态与新Widget的参数,重新生成动画
    //可以放置多个参数
    //本文原始链接:zhuanlan.zhihu.com/p/52572411,遇到抄袭欢迎举报
    //默认的Tween都是线性变化的,如果想引入新的逻辑(比如参数互相影响),把默认的visitor无视掉自己实现一个
  }
  
  @override
  Widget build(BuildContext context) {
    //return a widget built on a parameter
  }
}

使用方法同AnimatedContainer

不理解的地方可以去翻Flutter的源码,这部分Flutter源码写的相当好懂

3. 与其他设计模式的结合

前面说了这么多,可是,setState()是一个很丑陋而且限制很多的方法啊?用setState,还不如让我死.jpg

但是如果你把2.2部分看完了,你就会发现,隐式动画其实并不一定和setState挂钩。

其实上,只要发生了Widget的替换,而且对应的Element能够匹配上这个新的Widget(即Widget在树中的位置不变、类型不变、不乱折腾Key),隐式动画就可以起作用

这样的话,很多Flutter设计模式都能呼唤出隐式动画,尤其是我觉得最爽的BLoC模式。

比方说隐式动画和StreamBuilder的结合。StreamBuilder每接收到一个新值都会重建Widget,而且前后的Widget都是挂在StreamBuilder的节点下的,在树里的位置不变。

StreamBuilder(
  stream: observable.stream,
  builder: (context, snapshot) =>
    AnimatedContainer(
      height: snapshot.data,
      //...
    ),
),

//...
observable.add(500.0);

完美工作,而且用了BLoC模式后实现了相当良好的逻辑与界面分离

总结

  • 隐式动画可以让你用声明式的方法,写出过渡动画的效果。在同样实现动画的情况下,代码的易读性飙升
  • Flutter有相当多的隐式动画组件,很好用
  • 自己写隐式动画也挺方便
  • 隐式动画和一些设计模式简直天生一对。没有隐式动画的话,BLoC模式实现个过渡动画能把人给写死。
  • Flutter你的文档是在搞笑吗?搞笑吗?这玩意文档提都不提的?
    原文作者:IctusPrimus
    原文地址: https://zhuanlan.zhihu.com/p/52572411
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞