Flutter动画篇
开篇
动画在APP的设计中的有着重要的地位,具有目的性和功能性的动画,不仅仅能够增添美感的装饰,更能使用户获得良好的体验。
下面就由浅入深的来介绍一下在Flutter中动画是如何使用的,以及如何来选择合适自己需求的动画,也会带着源码来剖析在Flutter中动画是如何实现的。
隐式动画(Implicit Animaition)
介绍
ImplicitlyAnimatedWidget顾名思义是Flutter中用来做隐式动画的Widget,它是一个抽象类,其有很多子类可以使用,一般都可以用很少的代码来做出动画效果。
由于使用起来很简单,就不做过多的篇幅去介绍,下面就就说一下它有哪些子类、优缺点以及拿两个子类来做些范例。
隐式动画是如何实现的,会在文章后面去剖析。
隐式动画大家族
从上图可以看出ImplicitlyAnimatedWidget有11个公有子类和2个私有子类,其中的11个公有子类都可以拿来直接使用来做出动画,且很容易使用。
优缺点
-
优点
- 子类多,也就是动画多
- 易使用,易上手
- 代码量少
-
缺点
- 过程无法控制
- 只是简单的从一个状态切换到另一个状态
- 灵活性差
- 无法监听整个动画过程
例子
下面通过几个简单的例子来看一下如何来使用隐式动画
AnimatedContainer
AnimatedContainer有着跟Container类似的API,只需要在修改完AnimatedContainer的属性后重新刷新,Widget就会以动画的形式从旧值过渡到新值。
相对于普通的Container,AnimatedContainer多出了两个动画相关的属性,duration和curve。
duration为必传参数,即执行动画的时长。
curve为动画曲线,后面会专门讲解。
AnimatedOpacity
AnimatedOpacity也有着跟Opacity类似的API,也只是在Opacity的原有基础上增加的一些动画相关的属性,duration、curve和onEnd, 当修改opacity值后,刷新Widget就会在规定时间内,以动画的形式过渡到新值。
duration为必传参数,即执行动画的时长。
curve为动画曲线,后面会专门讲解
onEnd为动画结束的回调
隐式动画的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秒钟由红色变为蓝色
尺寸动画
3秒钟有100变到300
尺寸,颜色,圆角同时做动画
初始化一个0~1的tween,用Color和BorderRadius的lerp函数能够计算出当前动画时间应该得到的值,宽高则是从200涨到300。
总结
隐式动画使用起来相对简单,更易上手,Flutter已经帮我们封装了很多场景下可以直接使用的隐式动画Widget,如果都不能满足的话可以使用TweenAnimationBuilder
。
但是如果碰到复杂的场景和想要更好的去控制动画,隐式动画就没法去胜任了,这个时候就需要用到显示动画或者更底层的动画去做了。
显式动画(Explicit Animaition)
介绍
AnimatedWidget是用来做显示动画的,它是一个抽象类,它也有很多子类可以帮助开发者来快速完成一些特定场景下的动画。
AnimatedWidget可以接收一个Listenable属性,可以监听动画的过程。AnimationController继承自Listenable,如果将一个AnimationController对象传给AnimatedWidget,便可以控制动画和监听动画的过程了。
显示动画大家族
显示动画也是有11个公有类和2个私有类,11个公有类中除了AnimatedBuilder,其它的都可以帮助开发者来快速完成一些特定场景下的动画效果。
SizeTransition可以用来做大小的动画
ScaleTransition可以用来做缩放的动画
PositionedTransition可以用来做位置的动画
... ...
AnimationController
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会是大概是什么样的效果。
更多Curve效果:api.flutter.dev/flutter/ani…
例子
上面介绍了一些概念性的东西啊,下面也是用几个例子来带大家快速感受一个显式动画是如何使用的。
由于初始化一个AnimationController必传一个TickerProvider类型的vsync参数,Flutter已经帮助我们实现了两个TickerProvider,分别是SingleTickerProviderStateMixin
和TickerProviderStateMixin
,我们只需要将当前需要做动画的State去混入就可以直接使用。
SingleTickerProviderStateMixin
是在当前State只需要一个AnimationController时使用,如果当前类有多个AnimationController时就需要混入TickerProviderStateMixin
了。
RotationTransition
显式动画的核心步骤:
- 定义一个AnimationController变量
- 在initState()初始化animationController,设置动画时长和vsync。
- 将animationController赋值给显式动画
- 在需要执行动画的时候执行
animationController.forward()
即可。 - 一定要在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
基础用法
代码解释:
在上个例子中,我们在initState()
中初始化了controller,并且设置了2秒的动画时长,还设置了lowerBound为100,upperBound为200。
当我们调用controller.forward()
后,就会在屏幕刷新的每一帧中调用AnimatedBuilder的builder函数,且controller的value会在2秒钟的时间内以线性的速度从100增加到200。
所以在builder中将controller.value赋值给width和height就会有放大的动画效果。
同时做多种动画
我们希望同时做多种动画,改变大小,改变颜色,增加圆角,旋转
代码解释:
在上面的例子中,我们做的是在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时其实是进行了一些运算的。
调用路径如下图:
注意📢: 上图的第五步仅针对Tween,如果是Tween的子类,则有些子类会重写lerp函数,如DecorationTween就是重写了lerp函数。
所以在随着动画过程中controller的value的不断变化,在AnimatedBuilder的builder方法中调用animation的value也都是变化的,所以就产生的动画效果。
交错动画
我们有时希望有多个动画,但是并不是所有动画都是一起执行动的,而是有先后顺序。尺寸、颜色、圆角、旋转几个动画要依次去执行。
代码解释:
上面的示例我们可以看到,动画是分了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。
它可以用于动画的延迟。
假如我们有个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对象里去。
那我们将SingleTickerProviderStateMixin混入到State里去后,这个createTicker又是哪里去调用的呢?
我们通过前面的学习可以知道,在初始化一个AnimationController的时候需要传入一个TickerProvider对象,在AnimationController的初始化方法里可以看到有调用createTicker方法,将创建的ticker实例保存了下来,并且传入了一个_tick回调方法。
那么究竟_ticker这个实例是有什么用的呢,我们来看一下Ticker这个类里面究竟是做了什么。
先看一下Flutter是怎么介绍Ticker的,简单的说就是它用于动画帧的回调,刚初始化出来默认是不启动的,调用start方法后开始启动,被SchedulerBinding所驱动。
我们来看看start里面做了什么操作,首先判断自身状态是否可用,如果可用就会调用scheduleTick方法,在scheduleTick方法里面就会将_tick这个回调方法传入到了SchedulerBinding里面。
而在SchedulerBinding的scheduleFrameCallback方法里,首先会调用一下scheduleFrame,然后将callback封装到_FrameCallbackEntry实例对象里面,并将对象存到_transientCallbacks这个回调集合里面。
下面图里圈出来的就是_transientCallbacks的解释,通俗点说就是在帧刷新前,会回调这个集合里面的所有回调函数,用于将应用程序的行为同步到系统的显示,我的理解就是在页面刷新前,将数据准备好,刷新的时候就会用准备的数据去渲染界面。
调用scheduleFrame后会先初始化屏幕帧刷新的回调,然后标记屏幕帧刷新需要调用handleBeginFrame
在handleBeginFrame这个方法里,就会调用我们之前Ticker的回调函数,调用完以后就会清掉保存回调的集合。
上面调用回调函数相当于就是去调用AnimationController的_tick函数
到这里我们总结一下几个主要的步骤:
- TickerProvider提供了一个创建Ticker对象的方法createTicker
- 将TickerProvider传入到AnimationController的初始化方法里
- 在AnimationController初始化的时候就会调用TickerProvider的createTicker来初始化一个Ticker对象,并将一个函数_tick传入这个Ticker对象里
- Ticker对象在启动后会将传进来的函数再次传入到SchedulerBinding里的_transientCallbacks这个回调集合里。
- SchedulerBinding调用标记函数,标记屏幕帧刷新的时候,要调用handleBeginFrame函数
- 在handleBeginFrame函数里就会调用_transientCallbacks里面的所有回调函数
- 最终AnimationController的_tick函数会被调用,在_tick里面会计算动画当前的值和动画状态,并通知给监听者
源码解析
我们在这一节将去看一下隐式动画和显式动画的源码,来看看他们究竟是怎么动起来的。
我们就选隐式动画和显式动画里最全能的那两个Widget来看,分别是TweenAnimationBuilder和AnimatedBuilder。
TweenAnimationBuilder
再来看下它是怎么用的,传入一个Tween,传入一个时间,传入一个build回调。
就会在1秒的时间内不停的调用build回调,而builder里的value就是从0到1,在1秒内线性增长的值。
TweenAnimationBuilder的继承结构。
通过上面的学习我们知道,想要做一个动画离不开屏幕帧刷新的回调,Ticker有监听帧刷新的能力,TickerPorvider有创建Ticker的能力,AnimationController接受一个TickerProvider。
我们在使用隐式动画的时候,全程是没有创建AnimationController的,那它是怎么工作的?
上面的继承图可以看到ImplicitlyAnimatedWidgetState是混入了TickerPorvider的。我们来到ImplicitlyAnimatedWidgetState里面看一下。
ImplicitlyAnimatedWidgetState创建了AnimationController
AnimatedWidgetBaseState监听了AnimationController的值的变化,然后值变化后就是不停的调用setState来刷新自身
因为父类调用setState,所以_TweenAnimationBuilderState的build会被不停的调用,而在build里面就是计算好tween对应的值,抛给了TweenAnimationBuilder的builder回调方法。
TweenAnimationBuilder总结:
- ImplicitlyAnimatedWidgetState创建AnimationController
- AnimatedWidgetBaseState监听AnimationController值的变化,并调用setState。
- _TweenAnimationBuilderState重写了build方法,所有父类调用setState,子类的build会被调用。
- 在build里面根据AnimationController的值计算出Tween的值,并回调给widget传入的builder方法。
AnimatedBuilder
AnimatedBuilder的用法,自己创建和管理AnimationController,将AnimationController传给AnimatedBuilder,再传入一个回调builder。
AnimatedBuilder的代码看起来更加简单
AnimatedBuilder总结:
- 手动创建AnimationController传给AnimatedBuilder
- AnimatedBuilder将AnimationController传给了父类AnimatedWidget,并重写了父类的build方法
- 在AnimatedWidget的State里面监听了AnimationController的value的变化,并调用setState
- 调用setState就会调用state的build方法,在build方法里面调用了widget的build
- 由于AnimatedBuilder重写了build方法,所以在_AnimatedState里面调用的widget.build()就相当于调用的AnimatedBuilder的build方法
- 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…
转载自:https://juejin.cn/post/6998847893666791461