Flutter 动画全解析(动画四要素、动画组件、隐式动画组件原理等)

本文通过拆解 Flutter 中动画的实现方式以及原理来介绍动画实现的整个过程。

1. 动画四要素

动画在各个平台的实现原理都基本相同,是在一段时间内一系列连续变化画面的帧构成的。在 Flutter 中,动画的过程又被量化成一段值区间,我们可以利用这些值设置控件的各个属性来实现动画,其内部由四个关键的部分来实现这一过程。

1.1 插值器(Tweens)

tweens 可为动画提供起始值和结束值。默认情况下,Flutter 中的动画将任何给定时刻的值映射到介于 0.0 和 1.0 之间的 double 值。 我们可以使用以下 Tween 将其间值的范围定义为从 -200.0变为 0.0:

tween = Tween<double>(begin: -200, end: 0);

我们也可以将值设置为相应需要改变的对象值,比如将起始值设置为红色,结束值设置为蓝色,那么 tweens 产生的动画便是由红渐渐的变成蓝色。如下:

colorTween = ColorTween(begin: Colors.red, end: Colors.blue);

1.2 动画曲线(Animation Curves)

Curves 用来调整动画过程中随时间的变化率,默认情况下,动画以均匀的线性模型变化。读者可以通过自定义继承 Curves 的类来定义动画的变化率,比如设置为加速、减速或者先加速后减速等曲线模型。Flutter 内部也提供了一系列实现相应变化率的 Curves 对象:

  • linear
  • decelerate
  • ease
  • easeIn
  • easeOut
  • easeInOut
  • fastOutSlowIn
  • bounceIn
  • bounceOut
  • bounceInOut
  • elasticIn
  • elasticOut
  • elasticInOut

动画曲线模型图如下:

curve_linear.gif
curve_ease_in.gif
curve_bounce_in.gif

1.3 Ticker providers

Flutter 中的动画以屏幕频繁的重绘而实现,即每秒 60 帧。Ticker 可以被应用在 Flutter 每个对象中,当对象实现了 Ticker 的功能后,每次动画帧改变便会通知该对象。这里,开发者们不需要为对象手动实现 Ticker,flutter 提供了 TickerProvider 类可以帮助我们快速实现该功能。例如,在有状态控件下使用动画时,通常需要在 State 对象下混入 TickerProviderStateMixin。

class _MyAnimationState extends State<MyAnimation> 
    with TickerProviderStateMixin {
    
}

1.4 动画控制器(AnimationController)

Flutter 中动画的实现还有一个非常重要的类 AnimationController,即动画控制器。很明显,我们用它来控制动画,即动画的启动、暂停等。其接受两个参数,第一个是 vsync,为 Ticker 对象,其作用是当接受到来自 tweens 和 curves 的新值后通知对应对象,第二个 duration 参数为动画持续的时长。

// 混入 SingleTickerProviderStateMixin 使对象实现 Ticker 功能
class _AnimatedContainerState extends State<AnimatedContainer>
        with SingleTickerProviderStateMixin {
  AnimationController _controller;

  @override
  void initState() {
    super.initState();
    // 创建 AnimationController 动画
    _controller = AnimationController(
      // 传入 Ticker 对象
      vsync: this,
      // 传入 动画持续时间
      duration: new Duration(milliseconds: 1000),
    );
    startAnimation();
  }

  Future<void> startAnimation() async {
    // 调用 AnimationController 的 forward 方法启动动画
    await _controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: _controller.value;
      child: //...
    );
  }
}

AnimationController 继承自 Animation,具有一系列控制动画的方法,如可用 forward() 方法来启动动画,可用 repeat() 方法使动画重复执行,也可以通过其 value 属性得到当前值。

1.4.1 Animation

我们可以通过在 CurvedAnimation 传入 AnimationController 和 Curve 对象创建一个 Animation 对象,如下:

AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
final Animation<double> animation = CurvedAnimation(
  parent: controller,
  curve: Curves.ease,
);

也可以通过调用 tween 的 animate 方法传入 controller 对象创建 Animation 对象,如下:

AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);

Animation 是一个抽象类,其中保存了动画的过程值(value)和状态,下面是四种状态类型。

enum AnimationStatus {
  /// 动画处于停止状态
  dismissed,
  /// 动画从头到尾执行
  forward,
  /// 动画从尾到头执行
  reverse,
  /// 动画已执行完成
  completed,
}

AnimationController 是它的一个实现类。其内部通过范型机制可实现对各类型对象的动画,比如 Animation<double>Animation<Color>Animation<Size> 等。其另一个实现类 Curved­Animation,可以用来与 Curves 结合实现各类曲线模型函数的动画。

Animation 另一个实现方法是调用 tween 对象的 animate 方法传入 Animation 对象创建另一个 Animation 对象,该方法可通过将使动画值定义在 tween 区间内,如下:

AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
final Animation curve =
    CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(curve);

1.4.5 动画监听

Animation 对象可以有设置两种监听器,分别是帧监听器和状态监听器。使用 addListener() 添加帧监听器,使用addStatusListener() 添加状态监听器。

只要动画的值发生变化,就会触发帧监听器的回调。 通常,我们在其内部调用 setState() 来重建组件来实现动画效果,如下:

animation = new CurvedAnimation(
        parent: animationController, curve: Curves.elasticOut)
animation.addListener(() => this.setState(() {}))

动画开始,结束,前进或后退时会触发 StatusListener 的回调,如下:

animation = new CurvedAnimation(
        parent: animationController, curve: Curves.elasticOut)
animation.addStatusListener((AnimationStatus status) {});

2. 动画组件

我们已经知道了 Flutter 控制动画的四大要素,其中涉及的各个概念可以帮助我们设计出各种各样的动画效果,但不免也多了一些需要重复编写的模版代码,比如,在 Animation 的帧监听器设置的监听器回调里,几乎所有场景中我们都只是调用 setState(),再比如 State 对象每次都需要我们手动地混入 SingleTickerProviderStateMixin 等等这类情况。Flutter 为了提高开发者的开发效率,提供了 AnimatedWidget 抽象类来封装这部分模版代码,其源码非常简单,如下:

abstract class AnimatedWidget extends StatefulWidget {
  /// Creates a widget that rebuilds when the given listenable changes.
  ///
  /// The [listenable] argument is required.
  const AnimatedWidget({
    Key key,
    @required this.listenable
  }) : assert(listenable != null),
       super(key: key);

  /// The [Listenable] to which this widget is listening.
  ///
  /// Commonly an [Animation] or a [ChangeNotifier].
  final Listenable listenable;

  /// Override this method to build widgets that depend on the state of the
  /// listenable (e.g., the current value of the animation).
  @protected
  Widget build(BuildContext context);

  /// Subclasses typically do not override this method.
  @override
  _AnimatedState createState() => _AnimatedState();
}

class _AnimatedState extends State<AnimatedWidget> {
  @override
  void initState() {
    super.initState();
    widget.listenable.addListener(_handleChange);
  }

  @override
  void didUpdateWidget(AnimatedWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.listenable != oldWidget.listenable) {
      oldWidget.listenable.removeListener(_handleChange);
      widget.listenable.addListener(_handleChange);
    }
  }

  @override
  void dispose() {
    widget.listenable.removeListener(_handleChange);
    super.dispose();
  }

  void _handleChange() {
    setState(() {
      // The listenable's state is our build state, and it changed already.
    });
  }

  @override
  Widget build(BuildContext context) => widget.build(context);
}

AnimatedWidget 作为一个抽象类可供我们实现一个我们自己的具体类,其接受一个 Listenable 对象作为参数,并需要重写 build 方法。我们上一节中多次提到的 Animation 继承自 Listenable。下面的这个这个组件就是我自己实现的动画组件:??

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

  @override
  Widget build(BuildContext context) {
    final Animation<Color> animation = listenable;
    var maxWidth = MediaQuery.of(context).size.width;
    var margin = (maxWidth * .3) / 3;

    return new AspectRatio(
        aspectRatio: 1.0,
        child: new Container(
            margin: EdgeInsets.symmetric(horizontal: margin),
            constraints: BoxConstraints(
              maxWidth: maxWidth,
            ),
            decoration: new BoxDecoration(
              shape: BoxShape.circle,
              color: animation.value,
            )));
  }
}

我们可以通过传入已经定义好的 Animation 对象来使用该组件:??

class AnimateWidgetState extends State<AnimateWidget> {
  AnimationController _animationController;
  ColorTween _colorTween;
  ...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: 
          Column(
        children: <Widget>[
          Sun(animation: _colorTween.animate(_animationController)),
        ],
      ),
    );
  }
}

这样我们就封装了自己的一个动画组件,另外,Flutter 内部为我们提供了多个已经封装好的动画组件,利用好这些组件可以大大地提高我们的开发效率:

  • SlideTransition
  • ScaleTransition
  • RotationTransition
  • SizeTransition

3. 隐式动画组件

利用动画组件我们已经可以方便地封装出一系列控件动画了,但是这种实现方式均需要我们自己提供 Animation 对象,然后通过提供的接口方法来启动我们的动画,控件的属性由 Animation 对象提供并在动画过程中改变而达到动画的效果。为了使动画使用起来更加方便,Flutter 帮助了开发者从另一个角度以更简单的方式实现了动画效果——隐式动画组件(ImplicitlyAnimatedWidget)。

通过隐式动画组件,我们不需要手动实现插值器、曲线等对象,开发者甚至也不需要使用 AnimationController 来启动动画,它的实现方式更贴近对组件本身的操作,我们可以直接通过 setState() 的方法改变隐式动画组件的属性值,其内部自行为我们实现动画过程的过渡效果,即隐藏了所有动画实现的细节。Flutter 内部为我们提供了多个实用的隐式动画组件,我们本节分别介绍 AnimatedContainer 和 AnimatedOpacity 这两个最常用的隐式动画组件。

3.1 AnimatedContainer

AnimatedContainer 是我们最常使用到的隐式动画组件之一,从名字可以看出这个控件是以动画形式而成的 Contianer 控件,它们都是页面中渲染一个空的容器并且使用方法也非常相似。我们可以用下面的方式使用 Contianer 控件:

var height = 40.0  
...
    
Container(
    width: 60.0,
    height: height,
    color: Color(0xff14ff65),
  ),

上面的代码中,我们将 Container 的高度设置为 height 变量,即为 40.0,当我们使用一个 Button 按钮触发改变 height 值的事件并且重绘界面时,Container 的高度会随之改变:

onPressed: (){
  setState(() {
    height = 320.0;
  });
},

但这种变化很明显仅是属性的改变并不是一个平滑的过渡动画,然而同样的事件发生在 AnimatedContainer 控件上,便会有一个渐变的效果:

AnimatedContainer(
  duration: Duration(seconds: 5),
  width: 60.0,
  height: height,
  color: Color(0xff14ff65),
)

使用 AnimatedContainer 后,我们再次触发 height 变量改变后,页面中的 AnimatedContainer 便会平滑的过渡到相应的高度,其 duration 属性用于设置动画过渡的时间,这里,我们设置为 5 秒??。

我们可以用相同的方式为 Container 的 Color、width 等各种属性设置动画,同时也可以通过为其设置 alignment 属性来设置其内部子控件的位置。

3.2 AnimatedOpacity

在 Flutter 中,另一种常用的动画是控件透明度的过渡动画,其对应的隐式动画组件为 AnimatedOpacity。它的用法与 Opacity 相似,内部持有的 opacity 属性可以设置为 0.0~1.0 中的任意浮点数,分别对应完全透明与完全不透明,使用下面的方式,我们便可以设置了一个半透明的 Opacity 控件:

Opacity(
    opacity: 0.5,
    child: Text("hello"),
)

我们以相同的方法使用 AnimatedOpacity:

double opacity = 1.0;
...
AnimatedOpacity(
    opacity: opacity,
    duration: Duration(seconds: 1),
    child: Text("hello"),
)

它也接受 duration 属性来设置过渡时间,通过改变 opacity 变量的值可以实现透明度变化的动画效果:

setState(() {
    opacity = 0.0;
});

3.3 隐式动画原理简析

我们已经在本书前部分介绍了 Flutter 中的三棵重要的树及它们在组件渲染中的作用了。在元素树中,每个 Element 对象持有控件树中 Widget 组件的状态信息,这里我们将它称为 State 对象,Widget 刷新重建时,Element 会对比自己所对应 Widget 是否更新而做出相应屏幕渲染上的改变。

在各个隐式动画组件中,其动画信息便储存在 Element 所持有的 State 对象中,Widget 每次刷新都会引起 Element 对其重新引用,当对应的 Widget 类型改变则其 Element 会连带 State 对象自然而然的需要重新渲染,然而当 Widget 类型不变,则 Element 不需要重建,只需要改变 State 对象储存的动画信息即可。这样一种连续更新属性的过程便实现了更为我们所方便使用的隐式动画。

3.4 实现自定义隐式动画组件

实现自定义的隐式动画组件,我们需要使用到两个类:ImplicitlyAnimatedWidget 和 AnimatedWidgetBaseState。??

ImplicitlyAnimatedWidget 是所有隐式动画组件的父类,继承自 StatefulWidget,并且仅需要接受动画曲线 curve 与动画过渡时长 duration 两个参数:

const ImplicitlyAnimatedWidget({
    Key key,
    this.curve = Curves.linear,
    @required this.duration
  }) 

在我们自定义的隐式动画组件可以扩充他的参数类型满足我们的需求。

AnimatedWidgetBaseState 即 ImplicitlyAnimatedWidget 这个有状态组件所对应的 State 对象类,我们自定义的隐式动画组件所对应的 State 也必须继承该类,其内部需要重写 forEachTween 方法。

下面就是我自己定义的隐式动画组件:

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));
  }
  
  @override
  Widget build(BuildContext context) {
    //return a widget built on a parameter
  }
}

上面代码中,我们在父类的基础之上拓展了 param 参数,其是我们在动画过程中需要关注的动画属性值。我们还需要重点关注 _MyAnimatedWidgetState 类中 �forEachTween 方法,它是隐式动画实现的核心方法,其用于每次更新组件的动画属性,接受一个 TweenVisitor 对象 visitor 作为参数。visitor 同时接受是那个参数,第一个为一个插值器对象 Tween<T>,其是应用在属性中的插值器当前补间值,第二个参数为一个 T 类型的值,即新的目标属性值,第三个参数为一个回调函数,用于配置给定的 value 值作为新的插值器开始值。TweenVisitor<T> 函数返回一个 Tween<T> 对象,我们将其赋值给组件中当前的插值器对象作为下次调用 forEachTween 方法时的当前值。

4. 其他

笔者水平有限,如果文中有错误的地方,请留言指正。

欢迎一起交流学习,联系方式:

我的博客原文:https://meandni.com/2019/07/01/c0f2/

Github:https://github.com/MeandNi

微信:yangjk128

5. 参考

Flutter Doc

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容