Flutter的动画体系是怎么运作的,各组件之间的关联关系及原理什么,隐式动画、显式动画怎么区分,本文将会进行详细解答。
将会按照以下顺序进行介绍:
- 1.Flutter动画基本概念
- 2.隐式动画&显式动画
- 3.选择适合自己的动画
- 4.动画类型
- 5.常见动画模式
Flutter动画基本概念
什么是动画
讲解动画之前,需要先介绍一下帧的概念,上面我们看到的小视频,其实是由一张张连续的静止图片所构成,每一幅图片就是一帧。
传统电影每秒播放24帧,现在的手机每秒刷新60到120帧,我们在手机上看到的其实也是每秒刷新的图像。
这个小视频演示了从0变到光速的动画,利用的也是视觉差,假如手机帧率是每秒60帧,那么你看到的动画,其实是由渲染出来的60次图像所构成。那么,在Flutter系统中动画是怎么形成的呐?
Ticker
首先介绍Ticker,它是Flutter中动画运行的基础。Ticker是一个每帧都会执行某个函数
的对象,借助于此,我们可以在每帧的回调中连续改变UI视图的形态,这样视觉上看到的就是连续运行的动画了。
// 创建ticker
var ticker = Ticker((elapsed) => print(‘hello'));
创建完之后,需要手动开启ticker.start()
,使用完之后还需要手动释放资源ticker.dispose()
,此外还有muted(bool value)、stop()
等操作,管理起来比较麻烦,容易疏忽。好消息是99%d的场景你并不会直接使用Ticker,但是在动画中Ticker又是必不可少的,为了方便使用,系统提供了SingleTickerProviderStateMixin
来方便开发者,它继承自TickerProvider
,实现了createTicker
方法。
mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> implements TickerProvider {
Ticker _ticker;
@override
Ticker createTicker(TickerCallback onTick) {/*...*/}
/*...*/
}
这样当我们混入SingleTickerProviderStateMixin之后,自身的类就具备了创建createTicker的能力。
AnimationController
接着上图讲解,我们看到创建AnimationController
时vsync
参数传入this
参数,这是因为,我们混入了mixin之后具备了相应的能力,这块的详细解释可以看之前文章深入理解Flutter动画,传入this之后做了什么事情呐?
我们可以看到,this具备了createTicker
能力后,通过传入一个回调函数,就可以创建ticker
了,再看回调函数_tick
做了哪些事情呐?
主要有三件事:
- 根据创建动画传入的最大最小值(默认是0到1)每帧生成改变所对应的值。
- 通知值得监听者
- 改变动画状态
这样创建好的AnimationController就具备了每帧刷新double值
的能力,这里强调的是double值,你或许会好奇,我们的动画可能会改变color、rect、position、aligment
,这和double值有什么关系呐?这之间的关系稍后会详细讲述,这里先把controller讲述完。
AnimationController和它的名字一致是一个控制器,主要作用是控制动画、驱动动画
,其核心是通过控制value变量的值来驱动动画,这点可以很容易通过其Api看出来,几乎所有参数都是double类型。
动画正常执行forward,或者翻转reverse通过_animateToInternal(upperBound)
、_animateToInternal(lowerBound)
,注意其参数,即是动画改变范围的最小值,最大值。其他操作repeat
重复动画则是反复的从最小值到最大值之间变换,动画重置Reset
是讲value值重置为初始值。
回到之前1秒从0变光速的动画的例子,我们就可以通过AnimationController来实现
关于AnimationController基本就讲完了,其核心就是每帧改变一个double类型的值,还有改变动画状态,通知值得监听者,后面两个动作就涉及到了AnimationController的父类Animation
。通过下图可以看到其继承自Animation
类,Animation
泛型类型传入的也是double类型的值。
Animation
最简单的动画或许只需要改变值就可以改变UI效果了,但是如果你想要监听动画是否运行完成,当前运行到哪种程度了,那么只有一个值是不够的,这也是Animation<T>
设计的目的,
abstract class Animation<T> extends Listenable implements ValueListenable<T>
Animation就是结合了值、状态、并且能够提供回调的抽象类。因此看到需要传Animation<T>
类型的参数,需要使用其子类对象作为参数,它的子类常用的主要有两个AnimationController
和CurvedAnimation
。
Animation抽象类中另一个比较重要的方法就是drive
了,通过绑定一个具有Animatable
类型的对象来进行动画值的类型转换,这就是前面的疑问,怎么将AnimationController中的double值转换为想要的类型,比如color、rect、position、aligment
等。
Tween(Animatable)
Animatable也是一个抽象类,大部分子类都是Tween
类型的,也有一些不是比如CurveTween
,涉及到Curve的类有些特殊,稍后会讲到。
常见的Tween子类有ColorTween、SizeTween、RectTween、CurveTween、StepTween、IntTween
等,这里以ColorTween为例讲解一下转换过程。
- animation可以用来调用drive,返回值是Animation类型的,一般是由AnimationController来调用,传入一个Animatable类型的参数。
- 假设这个Animatable类型的参数,类型是ColorTween,它复写了父类Tween的
lerp
函数,当父类lerp被调用时,它将会被调用。 - ColorTween的真正实现,交给了具体值所在的类,这里是Color,传入三个参数,动画初始值begin和终点值end,此外还有一个
double t
, - t来自AnimationController的double值,根据转化公式,即可将动画控制double值转化为目标color值。
将tween和controller关联起来有两种方式,一种是刚才提到的通过调用Animation的drive
方法,另一种是Animatable类中的animate
方法,返回值都是Animation类型的,无论哪种方式都离不开AnimationController(提供的原始double值)。如下图
- controller.drive(tween)
- tween.anmation(controller)
多个tween还可以组合在一起,这样可以控制的动画属性就更多了,也有两种方式,通过chain
或者drive
组合。
Curve
除了通过tween对值类型进行改变,我们还可以通过Curve来控制动画执行的速度,常见的curveCurves.easeIn、Curves.easeOut
等都是基于Cubic
的不同参数对应的实例。系统内置了几十种不同的Cubic
实例,这也是curve最常用的控制,如果这些都不能满足用户,你也可以自己基于cubic创建。Cubic支持4个参数,Cubic(a, b, c, d)
,这个几个参数最终通过一系列公式,转化controller初始double t
值得改变顺序,从而实现控制动画执行的速度。
Cubic的本质其实是一个三阶贝塞尔曲线
这里有一个在线调试cubic的网站,支持实时观看、对比效果。
需要注意的一点是cubic计算出来的值有可能为负,小心动画值为负导致约束越界。
Curve的使用方式有三种:作为属性
,通过Curvetween
,通过CurveAnimation
,无论哪种都离不开 Controller提供的初始值。如下图:
基本概念小结
Ticker逐帧给我们提供了回调,AnimationController给我们默认提供了从0到1的值改变,可以用来进行动画控制,基于此初始值,我们可以使用Tween对初始值进行改变,使用Curve对动画速度进行控制。动画的值被包装成Animation类型,为我们提供了值和状态的监听。
隐式动画&显式动画
还记得上面的从0到光速的动画吗?那个只是简单的示范,其实有更简单的写法,甚至都不用自己创建AnimationController,讲完这一节关于隐式、显式动画你就明白啦。隐式动画和显式动画的区分是:是否需要自己创建管理AnimationController,还可以进一步细分为内置隐式动画,自定义隐式动画,内置显式动画,自定义显式动画。隐式动画可以做的,显式动画都可以实现,隐式动画只能控制duration
和curve
不需要创建controller
,简单易用,显式动画则有更多的控制权,在下面分别介绍后,会详细列出它们之间的对比。
隐式动画
常见的隐式动画都是以AnimatedFoo
命名的,Foo是没有动画时Widget的名字,系统提供了很多隐式动画,如下图。当下面的组件不满足时,也可以使用TweenAnimationBuilder
进行自定义隐式动画
,相应的系统提供的隐式动画被称为内置隐式动画
。
AnimatedFoo
第一次加载到Widget树中是没有动画的,思考下为什么?因为隐式动画是一次性的,只有每次当动画值改变时才有动画
。
以AnimatedAlign为例,其余隐式动画也一样,我们只需要关注属性的值变化、curve、duration
。
隐式动画大部分集成自ImplicitlyAnimatedWidget
类,但是也有一些特殊自接继承自SingleChildRenderObjectWidget
,这里也是个设计相关的问题,没有官方答案,自行研究完源码后欢迎一起探讨。隐式动画并不是不需要创建AnimationController,之所以被称之为隐式动画,是因为,创建controller这一步,隐式动画在内部帮助我们创建了,如下图,因此此类动画使用起来更加简便。
如果系统提供的内置隐式动画不满足需求,可以基于TweenAnimationBuilder
进行自定义,看下面示例
此示例涉及3个知识点
- Tween的值可以动态改变,谨记
TweenAnimationBuilder永远是从当前值运动到新的终值
- builder即是用户自定义想要实现的内容
- child参数放置不需要变的元素,比如自定义Widget中的icon,它会在builder构造中被当成参数传递进去,这块是系统为了
优化性能
所做的设计。
显式动画
显示动画需要自己创建并管理,系统也提供了一些实现好的显示动画,以FooTransition
Foo是没有动画时Widget的名字
大部分显示动画继承自AnimatedWidget
,和隐式动画类似,也有一部分直接继承自SingleChildRenderObjectWidget
,比如FadeTransition
,我的理解是此类Widget动画的改变,只需要直接改变渲染层即可,不涉及到Widget树的改变,你的理解是什么呐?
以AnimatedAlign
为例,可以和上面介绍的AnimatedAlign
对比起来观看,如下图,可以发现,隐式动画需要的参数是真正的值AlignmentGeometry alignment
,显示动画的参数变成了Animation<T>
类型Animation<AlignmentGeometry> alignment
,结合前面介绍的基本概念,这里可以将我们自定义的AnimationController
当做参数传进去,因为它也继承自Animation
。
如果系统内置的显式动画不满足需求,我们可以使用AnimationBuilder自定义显式动画
,AnimationBuilder继承自AnimatedWidget,因此我们也可以直接继承自AnimatedWidget
,自定义显示动画关注点和自定义隐式动画类似,同样只需要关注animation、builder、child
,其中animation即为自己创建的controller,builder为自定义Widget,child作用和上面一样用来优化性能。
自定义显式动画两种方式效果一样,建议是直接继承自AnimatedWidget定义单独的Widget,这样更独立,也方便以后复用。当然如果父节点比较简单时,首选AnimatedBuilder。下面有两个小示例
隐式动画&显式动画小结
对比 | 命名 | 控制器 | 值类型 | 父类 | 自定义 | 难易程度 |
---|---|---|---|---|---|---|
显式动画 | FooTransition | 显式创建AnimationController,完整的控制权 | Animation<T> value | 大多数继承自AnimatedWidget | 使用AnimatedBuilder或继承自AnimatedWidget | 中等 |
隐式动画 | AnimatedFoo | 隐式创建Controller,只能控制duration、curve | T value | 大多数继承自ImplicitlyAnimatedWidget | 使用TweenAnimationBuilder | 简单 |
选择适合自己的动画
上面介绍了内置隐式动画、自定义隐式动画,内置显式动画、自定义显式动画,在Flutter动画体系中还有其他类型,那么我们该如何选择使用哪种呐?
- 隐式动画可以做的,显式动画都可以实现,只是实现难易程度不一样
- 隐式动画只能控制duration和curve,不需要创建controller,简单易用,显式动画有更多的控制权
此外这里有一张翻译的图供你参考,更详细的介绍之前有翻译过一篇文章如何选择适合您的的Flutter Animation Widget,这里介绍的更详细。
动画类型
动画类型可以分为两大类
- 补间动画(Tween animation) 以上介绍的都可以算是补间动画
- 基于物理动画(Physics-based animation animation)
基于物理的动画可以参考系统Api AnimationController.animateWith和SpringSimulation,这里还有一个官方示例Widget 的物理模拟动画效果
这块实际项目中使用不多,这里不再详细介绍,如有特殊要求欢迎一起交流。
常见动画模式
- 列表和网格动画,常见的ListView,GridView展示,加载、删除的动画
- 共享元素动画(Hero),标准Hero动画和径向Hero动画
- 交织动画
标准Hero动画使用起来比较简单,系统提供有hero
Widget,只需要在转场前后页面保持同样的tag
即可。
其原理是,系统在动画运行的时候,在原视图的基础上覆盖一层overlay
,当然还有其中过渡动画。
径向Hero动画稍微复杂一些,先看下效果展示,共享元素代码示例
这部分的详细介绍看这里Hero动画,文章已经太长了,这里就不展开讲了。
交织动画主要考虑的是:
- 一个交织动画由一组序列动画或重叠动画所组成。
- 创建一个交织动画,要用到多个动画对象
- 一个 AnimationController 控制所有动画。无论动画在真实时间中播放多长时间,控制器的值必须在 0.0 和 1.0 之间,包括 0.0 和 1.0。
- 每个动画对象在一个间隔时间内指定一个动画。
- 为每一个要执行动画的属性创建一个 Tween
这里也不展开细讲,详细介绍可以看这里交织动画
下面是运行效果及设计图,下面动画源码交织动画示例
全文小结
文章第一部分先介绍了一些基本概念Ticker
、AnimationController
、Animation
、Tween
、Curve
这些是Flutter动画的核心,通过对其源码分析,了解到彼此间的关联关系。然后介绍了隐式动画和显示动画,以及如何进行自定义,接着又介绍了如何选用适合自己的动画,这部分之前文章有介绍,这里一笔带过了,建议详细阅读下之前的文章,最后介绍了Hero动画和交织动画。
看到这里,想必你对Flutter动画体系有了一定了解,文章中链接的文章,之前已经单开文章介绍过也推荐看一看。当然了解了之后还需要写代码练习,相信再看到Flutter动画代码的时候,就不会那么陌生了。