likes
comments
collection
share

Flutter动画篇

作者站长头像
站长
· 阅读数 3

开篇

动画在APP的设计中的有着重要的地位,具有目的性和功能性的动画,不仅仅能够增添美感的装饰,更能使用户获得良好的体验。

下面就由浅入深的来介绍一下在Flutter中动画是如何使用的,以及如何来选择合适自己需求的动画,也会带着源码来剖析在Flutter中动画是如何实现的。

隐式动画(Implicit Animaition)

介绍

ImplicitlyAnimatedWidget顾名思义是Flutter中用来做隐式动画的Widget,它是一个抽象类,其有很多子类可以使用,一般都可以用很少的代码来做出动画效果。

由于使用起来很简单,就不做过多的篇幅去介绍,下面就就说一下它有哪些子类、优缺点以及拿两个子类来做些范例。

隐式动画是如何实现的,会在文章后面去剖析。

隐式动画大家族

Flutter动画篇

从上图可以看出ImplicitlyAnimatedWidget有11个公有子类和2个私有子类,其中的11个公有子类都可以拿来直接使用来做出动画,且很容易使用。

优缺点

  • 优点

    • 子类多,也就是动画多
    • 易使用,易上手
    • 代码量少
  • 缺点

    • 过程无法控制
    • 只是简单的从一个状态切换到另一个状态
    • 灵活性差
    • 无法监听整个动画过程

例子

下面通过几个简单的例子来看一下如何来使用隐式动画

AnimatedContainer

AnimatedContainer有着跟Container类似的API,只需要在修改完AnimatedContainer的属性后重新刷新,Widget就会以动画的形式从旧值过渡到新值。

相对于普通的Container,AnimatedContainer多出了两个动画相关的属性,durationcurve

duration为必传参数,即执行动画的时长。

curve为动画曲线,后面会专门讲解。

Flutter动画篇 Flutter动画篇


AnimatedOpacity

AnimatedOpacity也有着跟Opacity类似的API,也只是在Opacity的原有基础上增加的一些动画相关的属性,duration、curve和onEnd, 当修改opacity值后,刷新Widget就会在规定时间内,以动画的形式过渡到新值。

duration为必传参数,即执行动画的时长。

curve为动画曲线,后面会专门讲解

onEnd为动画结束的回调

Flutter动画篇Flutter动画篇

隐式动画的Widget很多都是在普通的Widget的基础上增加了动画属性。

Container --> AnimatedContainer

Opacity --> AnimatedOpacity

Padding --> AnimatedPadding

Align --> AnimatedAlign

... ...

但是此时如果我们想要做一个隐式动画,但是我们又不知道该使用哪个隐式动画Widget去做,或者是没有能满足场景的隐式动画Widget,我们应该如何去做呢,这个时候就可以用上隐式动画中的全能选手TweenAnimationBuilder

隐式动画中的全能选手:TweenAnimationBuilder

TweenAnimationBuilder可以做到隐式动画大家族中其他Wigdet所能做到的所有动画。

构建一个TweenAnimationBuilder需要两个必要参数:

1、tween: Tween

构建一个Tween需要两个参数

Tween({ this.begin, this.end })

动画的过渡值就是在Tween的begin和end之间计算出来的。

假设我们现在需要Widget的宽从50放大到100,就可以构建一个50~100的tween

Tween(begin: 50, end: 100)

构建Tween时begin和end接受的都是泛型参数,因此Tween可以是任何类型的区间,所以Tween可能做到任意两个值之间的过渡。Flutter也帮我们提供好了很多实现好的Tween,如:ColorTween,SizeTween,BoxConstraintsTween等等,方便我们去使用。

2、builder: Widget Function(BuildContext context, T value, Widget child)

动画过程中会不断调用的回调函数,返回需要展示的Widget。

value则是当前动画时根据tween计算出来的需要展示的值。

颜色动画

3秒钟由红色变为蓝色

Flutter动画篇Flutter动画篇

尺寸动画

3秒钟有100变到300

Flutter动画篇Flutter动画篇

尺寸,颜色,圆角同时做动画

初始化一个0~1的tween,用Color和BorderRadius的lerp函数能够计算出当前动画时间应该得到的值,宽高则是从200涨到300。

Flutter动画篇Flutter动画篇

总结

隐式动画使用起来相对简单,更易上手,Flutter已经帮我们封装了很多场景下可以直接使用的隐式动画Widget,如果都不能满足的话可以使用TweenAnimationBuilder

但是如果碰到复杂的场景和想要更好的去控制动画,隐式动画就没法去胜任了,这个时候就需要用到显示动画或者更底层的动画去做了。

显式动画(Explicit Animaition)

介绍

AnimatedWidget是用来做显示动画的,它是一个抽象类,它也有很多子类可以帮助开发者来快速完成一些特定场景下的动画。

AnimatedWidget可以接收一个Listenable属性,可以监听动画的过程。AnimationController继承自Listenable,如果将一个AnimationController对象传给AnimatedWidget,便可以控制动画和监听动画的过程了。

显示动画大家族

Flutter动画篇

显示动画也是有11个公有类和2个私有类,11个公有类中除了AnimatedBuilder,其它的都可以帮助开发者来快速完成一些特定场景下的动画效果。

SizeTransition可以用来做大小的动画

ScaleTransition可以用来做缩放的动画

PositionedTransition可以用来做位置的动画

... ...

AnimationController

Flutter动画篇

AnimationController是一个动画控制器,可以起到控制和监听动画的作用。

初始化一个AnimationController必传一个参数是vsync,vsycn是一个TickerProvider类型的参数,它能够注册并触发每一帧的回调,我们在后面会专门介绍TickerProvider。

如果初始化了一个AnimationController,就必须要手动dispose掉,否则会造成内存泄露。

AnimationController控制动画:

可以控制动画从什么值开始播放forward(from:)

反转动画reverse(from:)

停止动画stop()

重置动画reset()

让动画播放到什么值animateTo(0.7)

等等一系列控制行为。

监听动画:

值监听:

 // 监听动画值的变化过程

 // value的取值范围是AnimationController的lowerBound ~ upperBound

 // 默认取值范围是0.0 ~ 1.0

animationController.addListener(() {

    print(animationController.value);

});

状态监听

// 用来监听动画的状态。

// status是AnimationStatus类型的一个值,取值范围有4个

animationController.addStatusListener((status) {

    print(status);

});

AnimationStatus

AnimationStatus描述
dismissed当controller.value==controller.lowerBound时的状态,也可以理解为当controller调用reverse()来执行动画,且动画完成时的状态
forward当controller调用forward()来执行动画时,在动画完成前整个动画过程的状态
reverse当controller调用reverse()来执行动画时,在动画完成前整个动画过程的状态
completed当controller.value==controller.upperBound时的状态,也可以理解为当controller调用forward()来执行动画,且动画完成时的状态

Curve

Curve我们在说隐式动画的时候也有用到,它其实就是动画曲线,就是在执行动画的过程中,可以让动画有线性、加速减速、弹性等动画曲线效果。

下面两个示意图就是展示如果在动画中使用了curve会是大概是什么样的效果。

Flutter动画篇Flutter动画篇

更多Curve效果:api.flutter.dev/flutter/ani…

例子

上面介绍了一些概念性的东西啊,下面也是用几个例子来带大家快速感受一个显式动画是如何使用的。

由于初始化一个AnimationController必传一个TickerProvider类型的vsync参数,Flutter已经帮助我们实现了两个TickerProvider,分别是SingleTickerProviderStateMixinTickerProviderStateMixin,我们只需要将当前需要做动画的State去混入就可以直接使用。

SingleTickerProviderStateMixin是在当前State只需要一个AnimationController时使用,如果当前类有多个AnimationController时就需要混入TickerProviderStateMixin了。

RotationTransition

Flutter动画篇Flutter动画篇

显式动画的核心步骤:

  1. 定义一个AnimationController变量
  2. 在initState()初始化animationController,设置动画时长和vsync。
  3. 将animationController赋值给显式动画
  4. 在需要执行动画的时候执行animationController.forward()即可。
  5. 一定要在dispose()方法里调用animationController.dispose(),否则会造成内存泄露。

在上面这个例子中我们用的是RotationTransition,这个显式动画Widget是做旋转的,它有个Animation类型的turns属性,而AnimationController就是继承Animation的。

所有的显式动画Widget也都有一个Animation类型的属性,不用的Widget属性名会不同,但是都可以接收AnimationController。

显式动画中的全能选手:AnimatedBuilder

跟隐式动画一样,有时候我们不知道有哪些显式动画可用,或者已有的显式动画不能满足使用场景,该怎么去处理呢?在显式动画家族里面也有一个全能选手,那就是AnimatedBuilder

AnimatedBuilder接收两个必传参数

Listenable animation;

Widget Function(BuildContext context, Widget child) builder;

animation可以直接将AnimationController赋值给它

builder方法中需要返回动画过程中需要展示的Widget

基础用法

Flutter动画篇Flutter动画篇

代码解释:

在上个例子中,我们在initState()中初始化了controller,并且设置了2秒的动画时长,还设置了lowerBound为100,upperBound为200。

当我们调用controller.forward()后,就会在屏幕刷新的每一帧中调用AnimatedBuilder的builder函数,且controller的value会在2秒钟的时间内以线性的速度从100增加到200。

所以在builder中将controller.value赋值给width和height就会有放大的动画效果。

同时做多种动画

我们希望同时做多种动画,改变大小,改变颜色,增加圆角,旋转

Flutter动画篇Flutter动画篇

代码解释:

在上面的例子中,我们做的是在2秒钟内,Container尺寸从100涨到200,红色变成蓝色,圆角从0增加到20,并且做了一圈的旋转。

从代码中我们可以看到,我们在State里面不止定义了一个controller,还定义了另外三个Animation类型的属性,分别是:

sizeAnimation:用来做尺寸动画

decorationAnimation:用来做颜色和圆角动画

rotationAnimation:用来做旋转动画

并且在AnimatedBuilder的builder函数里没有用到controller的属性,而是分别用了另外三个animation的value值。这三个animation究竟是什么?

initState()里面,我们先初始化了controller,设置了2秒的时长和vsync。

然后在下面分别初始化了另外三个Animation。

sizeAnimation就是一个100~200的Tween,然后用tween调用animate方法,并将controller传入,则就会返回一个Animation对象。

rotationAnimation跟sizeAnimation类似。

decorationAnimation我们可以看到,它是一个DecorationTween,DecorationTween其实就是继承Tween的一个类,它的begin和end接收Decoration类型。因为我们要做的是颜色和圆角的变化,而BoxDecoration就是继承Decoration,BoxDecoration里面又有颜色属性和圆角属性。所以这里就取巧用了DecorationTween,他的begin为color:red,borderRadius:0 end为color:blue,borderRadius:20,就是由红到蓝,圆角由0到20。

那为什么在builder里面用animation.value就会有动画效果呢?

真实的animation其实是_AnimatedEvaluation类型,在_AnimatedEvaluation里,他会同时持有Tween和AnimationController两个变量,所以当我们当用animation.value时其实是进行了一些运算的。

调用路径如下图:

Flutter动画篇

注意📢: 上图的第五步仅针对Tween,如果是Tween的子类,则有些子类会重写lerp函数,如DecorationTween就是重写了lerp函数。

所以在随着动画过程中controller的value的不断变化,在AnimatedBuilder的builder方法中调用animation的value也都是变化的,所以就产生的动画效果。

交错动画

我们有时希望有多个动画,但是并不是所有动画都是一起执行动的,而是有先后顺序。尺寸、颜色、圆角、旋转几个动画要依次去执行。

Flutter动画篇Flutter动画篇

代码解释:

上面的示例我们可以看到,动画是分了4段。

  1. 大小的变化
  2. 颜色的变化
  3. 圆角的变化
  4. 旋转

我们从代码中可以看到,这段代码和上面那个同时做动画的代码有百分之八九十都是一样的,只是在初始化各个Animation的时候多调用了一个chain方法,并且传入了一个CurveTween。

我们这里就主要看一下chain和CurveTween是做什么的,其他的整个动画步骤跟上面同时做多种动画是一模一样的,就不再讲解一遍了。

chain方法

chain方法是Animatable的一个方法,顾名思义是链接的意思,就是将多个Animatable链接到一起。Tween就是继承Animatable。

我们还记得上面在计算animation.value的时候,中间有一个步骤是调用tween的transform方法,如果我们将多个tween链接到一块后生成了animation,然后再去调用animation.value时,就会依次调用被链接的tween的transform去计算出最终的值。

CurveTween

就是一个可以生成动画曲线的Tween,可以更好的跟其他Tween结合。

CurveTween({ @required this.curve })

其他的Curve不再多说,上面也有介绍,这里介绍一个有特殊用法的Curve,它就是Interval。

Interval(this.begin, this.end, { this.curve = Curves.`` linear ``})

Interval的begin到end的取值范围是0 ~ 1。

它可以用于动画的延迟。

Flutter动画篇

假如我们有个8秒的动画,它使用了Interval,并且Interval的值是0.5~1,则这个动画的前4秒是没有效果的,后4秒才会开始执行动画,且最后4秒会完成所有动画效果。

如果Interval的值是0.25~0.5,则这个动画是从第2秒开始执行,到第4秒的时候动画就会执行结束,且第2秒到第4秒这两秒钟之间会完成所有动画效果。

这时我们再去看上面的代码就不难理解那4段动画是如何执行的。

我们总共有个8秒的动画,将尺寸变化的动画放在前2秒,将颜色变化的动画放在2秒到4秒之间,将圆角的动画放在4秒到6秒之间,将旋转的动画放在最后2秒。

总结

显式动画的使用比起隐式动画要稍微复杂一些,但也灵活了许多,AnimatedBuilder应该可以满足我们所有显式动画的场景。

在这一节我们讲解了AnimationController的使用,虽然没有逐个去说它的各个方法如何使用,但是讲了一些它的核心方法,有些方法看到方法名也就能清楚是做什么用的。

我们也介绍了Curve和Interval。

还有组合动画和交错动画的用法和原理。

TickerProvider

TickerProvider是动画中不可或缺的一个类,它能构建出一个Ticker对象,一个Ticker对象能够注册到SchedulerBinding里,并触发每一帧的回调。

而AnimationController的value就是在每一帧的回调里去计算出来的。

TickerProvider是一个抽象类,Flutter已经帮我实现了两个TickerProvider,分别是TickerProviderStateMixin和SingleTickerProviderStateMixin。SingleTickerProviderStateMixin的性能要好于TickerProviderStateMixin。当我们的State里只用到一个AnimationController的时候,推荐使用SingleTickerProviderStateMixin,而且大部分的动画场景一个AnimationController就已经能够胜任了。

接下来我们就剖析一下AnimationController、TickerProvider以及Ticker是如何合作的。

TickerProvider是一个抽象类,它里面只有一个实例方法createTicker(TickerCallback onTick)

我们来看一下SingleTickerProviderStateMixin是如何实现的。

省略掉一些注释和debug代码,可以看到里面就是简单的创建了一个Ticker对象,并将onTick这个回调传到ticker对象里去。

Flutter动画篇

那我们将SingleTickerProviderStateMixin混入到State里去后,这个createTicker又是哪里去调用的呢?

我们通过前面的学习可以知道,在初始化一个AnimationController的时候需要传入一个TickerProvider对象,在AnimationController的初始化方法里可以看到有调用createTicker方法,将创建的ticker实例保存了下来,并且传入了一个_tick回调方法。

Flutter动画篇

那么究竟_ticker这个实例是有什么用的呢,我们来看一下Ticker这个类里面究竟是做了什么。

先看一下Flutter是怎么介绍Ticker的,简单的说就是它用于动画帧的回调,刚初始化出来默认是不启动的,调用start方法后开始启动,被SchedulerBinding所驱动。

Flutter动画篇

我们来看看start里面做了什么操作,首先判断自身状态是否可用,如果可用就会调用scheduleTick方法,在scheduleTick方法里面就会将_tick这个回调方法传入到了SchedulerBinding里面。

Flutter动画篇

而在SchedulerBinding的scheduleFrameCallback方法里,首先会调用一下scheduleFrame,然后将callback封装到_FrameCallbackEntry实例对象里面,并将对象存到_transientCallbacks这个回调集合里面。

Flutter动画篇

下面图里圈出来的就是_transientCallbacks的解释,通俗点说就是在帧刷新前,会回调这个集合里面的所有回调函数,用于将应用程序的行为同步到系统的显示,我的理解就是在页面刷新前,将数据准备好,刷新的时候就会用准备的数据去渲染界面。

Flutter动画篇

调用scheduleFrame后会先初始化屏幕帧刷新的回调,然后标记屏幕帧刷新需要调用handleBeginFrame

Flutter动画篇

在handleBeginFrame这个方法里,就会调用我们之前Ticker的回调函数,调用完以后就会清掉保存回调的集合。

Flutter动画篇

上面调用回调函数相当于就是去调用AnimationController的_tick函数

Flutter动画篇

到这里我们总结一下几个主要的步骤:

  1. TickerProvider提供了一个创建Ticker对象的方法createTicker
  2. 将TickerProvider传入到AnimationController的初始化方法里
  3. 在AnimationController初始化的时候就会调用TickerProvider的createTicker来初始化一个Ticker对象,并将一个函数_tick传入这个Ticker对象里
  4. Ticker对象在启动后会将传进来的函数再次传入到SchedulerBinding里的_transientCallbacks这个回调集合里。
  5. SchedulerBinding调用标记函数,标记屏幕帧刷新的时候,要调用handleBeginFrame函数
  6. 在handleBeginFrame函数里就会调用_transientCallbacks里面的所有回调函数
  7. 最终AnimationController的_tick函数会被调用,在_tick里面会计算动画当前的值和动画状态,并通知给监听者

源码解析

我们在这一节将去看一下隐式动画和显式动画的源码,来看看他们究竟是怎么动起来的。

我们就选隐式动画和显式动画里最全能的那两个Widget来看,分别是TweenAnimationBuilder和AnimatedBuilder。

TweenAnimationBuilder

再来看下它是怎么用的,传入一个Tween,传入一个时间,传入一个build回调。

就会在1秒的时间内不停的调用build回调,而builder里的value就是从0到1,在1秒内线性增长的值。

Flutter动画篇

TweenAnimationBuilder的继承结构。

Flutter动画篇

通过上面的学习我们知道,想要做一个动画离不开屏幕帧刷新的回调,Ticker有监听帧刷新的能力,TickerPorvider有创建Ticker的能力,AnimationController接受一个TickerProvider。

我们在使用隐式动画的时候,全程是没有创建AnimationController的,那它是怎么工作的?

上面的继承图可以看到ImplicitlyAnimatedWidgetState是混入了TickerPorvider的。我们来到ImplicitlyAnimatedWidgetState里面看一下。

ImplicitlyAnimatedWidgetState创建了AnimationController

Flutter动画篇

AnimatedWidgetBaseState监听了AnimationController的值的变化,然后值变化后就是不停的调用setState来刷新自身

Flutter动画篇

因为父类调用setState,所以_TweenAnimationBuilderState的build会被不停的调用,而在build里面就是计算好tween对应的值,抛给了TweenAnimationBuilder的builder回调方法。

Flutter动画篇

TweenAnimationBuilder总结:

  1. ImplicitlyAnimatedWidgetState创建AnimationController
  2. AnimatedWidgetBaseState监听AnimationController值的变化,并调用setState。
  3. _TweenAnimationBuilderState重写了build方法,所有父类调用setState,子类的build会被调用。
  4. 在build里面根据AnimationController的值计算出Tween的值,并回调给widget传入的builder方法。

AnimatedBuilder

AnimatedBuilder的用法,自己创建和管理AnimationController,将AnimationController传给AnimatedBuilder,再传入一个回调builder。

Flutter动画篇

AnimatedBuilder的代码看起来更加简单

Flutter动画篇

AnimatedBuilder总结:

  1. 手动创建AnimationController传给AnimatedBuilder
  2. AnimatedBuilder将AnimationController传给了父类AnimatedWidget,并重写了父类的build方法
  3. 在AnimatedWidget的State里面监听了AnimationController的value的变化,并调用setState
  4. 调用setState就会调用state的build方法,在build方法里面调用了widget的build
  5. 由于AnimatedBuilder重写了build方法,所以在_AnimatedState里面调用的widget.build()就相当于调用的AnimatedBuilder的build方法
  6. AnimatedBuilder的build就是调用构建AnimatedBuilder时传入的builder回调函数。

总结

TweenAnimationBuilder和AnimatedBuilder的源码看起来都不困难,在理解动画原理后都可以很轻松的能知道源码是如何工作的,无非就是AnimationController的监听、value的计算、父类和子类之间的能力配合等。

结尾

在整个动画篇我们介绍了隐式动画、显式动画、AnimationController、Tween、Curve、组合动画、TickerProvider、源码解析等内容。

整个篇章可以让我们快速上手Flutter动画,理解Flutter动画的原理,轻松去应付各种动画场景。

参考

Flutter动画相关官方文档:api.flutter-io.cn/flutter/ani…

Curve: api.flutter.dev/flutter/ani…

如何选择自己想要的动画:medium.com/flutter/how…