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

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

1. 动画四要素

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

1.1 插值器(Tweens)

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

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

咱们也能够将值设置为相应须要改变的对象值,好比将起始值设置为红色,结束值设置为蓝色,那么 tweens 产生的动画即是由红渐渐的变成蓝色。以下:markdown

colorTween = ColorTween(begin: Colors.red, end: Colors.blue);
复制代码

1.2 动画曲线(Animation Curves)

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

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

几个表明的动画曲线模型图以下:ide

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 对象,以下:ui

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 函数返回一个 Tween<T> 对象,咱们将其赋值给组件中当前的插值器对象做为下次调用 forEachTween 方法时的当前值。

4. 其余

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

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

个人博客原文:meandni.com/2019/07/01/…

Github:github.com/MeandNi

微信:yangjk128

5. 参考

Flutter Doc

Flutter Animated Series : Animated Containers

相关文章
相关标签/搜索