本文翻译自
原文地址:Animation deep dive
原作者:Filip Hracek
去年我录制了Flutter动画系列的一集视频视频链接 ,我想我也应该给那些喜欢看文字而不是视频的人发布相同的内容。
Animation videos from the Flutter YouTube channel 在该系列的其他剧集中,我的同事们讨论了在Flutter中制作动画的所有实用方法,但是在我的剧集中不是这样子的。在这里,您将学习如何以可想象的最不务实的方式实现动画(但是,此过程中您仍会学到一些东西)。
让我们以轻松而又简单的事情开始吧:
什么是运动
你看,运动是一种幻想,看这里(Filip挥舞着他的手的视频):
这是一个假象,你实际上看到的是很多快速连续显示的静止图像,电影就是这样工作的。单个图片在电影中称之为帧(frame)--因为数码显示屏的工作原理类似--因此在此处也称之为帧。在电影中通常一秒显示24帧,现代数码设备每秒显示60到120帧。
因此,如果运动只是假象的话,那么这些AnimationFoo
和 FooTransition
到底在做什么,当然由于每秒最多需要构建120帧,UI不可能每次都会重新构建。
或者,可能?
实际上,Flutter中的动画只是在每一帧上重建部件树的一部分的一种方法,没有特殊情况,Flutter能足够快的做到这一点。
让我们看一下Flutter动画的构建块之一:AnimatedBuilder
,它继承自AnimatedWidget
,在AnimatedWidget
中会创建_AnimatedState
,在这个State
的initState()
初始化方法中,我们监听了Animation
(或者在这称之为Listenable
),当它的值发生变化时候,我们调用了...setState()
。
这个源码调用截图,证明了我段落内阐述的事实,动画的构造函数真的每一帧都会调用setState()
方法。
所以,在Flutter中动画只是一连串的快速的改变Widget的状态,每秒60到120次。
我能证明上述结论。这是一个从0到光速的‘动画化’的动画,尽管他只是每一帧改变了Text文字,从Flutter的角度来看,他只是又一种动画!
让我们使用Flutter的动画框架从基本原理出发构建动画。
通常我们将会使用TweenAnimationBuilder
或者其他类似的的Widget,但是在这篇文章中,我们先忽略这些,转而使用ticker
、controller
、setState
。
Ticker
先介绍一下Ticker
是什么,99%的场景,你并不会直接使用到ticker,但是我认为介绍一下仍会是有帮助的--即使只是揭开它的神秘面纱。
// ticker 是一个每帧都会执行某个函数的对象
// A ticker is an object that calls a function for every frame.
var ticker = Ticker((elapsed) => print('hello'));
ticker.start();
在这个例子中,我们每帧都会打印“hello”,虽然并没有什么用。
并且,我们忘记了调用ticker.dispose()
,因此我们的ticker将会一直运行,直到我们杀掉APP。
这也是为什么Flutter给您提供SingleTickerProviderStateMixin
,也就是你在之前的一些视频中看到的名字很恰如其分的mixin。
这种mixin解决了管理ticker的麻烦,只需将它混入到你widget的state中,你的State就隐秘的具备了TickerProvider
能力。
class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin<MyWidget> {
@override
Widget build(BuildContext context) {
return Container();
}
}
这意味着Flutter框架可以向你的state寻求ticker能力,最重要的是AnimationController
可以向state寻求ticker。
class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin<MyWidget> {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
}
@override
Widget build(BuildContext context) {
return Container();
}
}
AnimationController
的初始化需要ticker
参数,如果你使用了SingleTickerProviderStateMixin
或者类似的TickerProviderStateMixin
,你就可以将this
传参给AnimationController
,那么就初始化完成了。
AnimationController
AnimationController
通常用来播放
、暂停
、翻转
、停止
动画,和纯粹的"tick"不同,AnimationController
可以随时告诉我们动画执行到了哪个点,例如我们的动画执行到一半吗,执行到了99%,还是已经执行完成了?
通常你可以使用AnimationController
,或者使用Curve
对它进行转换,或者将它放置到Tween
中,或者可以用在像FadeTransition
、TweenAnimationBuilder
这样便捷使用的widget中。但是出于教学目的,以上这些我们都不使用,相反,我们直接调用setState
。
setState
初始化AnimationController
之后,我们通过它添加一个监听器,在监听器内调用setState
class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin<MyWidget> {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
_controller.addListener(_update);
}
void _update() {
setState(() {
// TODO
});
}
@override
Widget build(BuildContext context) {
return Container();
}
}
现在,我们或许应该设置state了,保持简单的integer类型,不要忘记在build方法中实际使用state,并且在我们的监听中根据controller的当前值改变state。
class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin<MyWidget> {
AnimationController _controller;
int i = 0;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
_controller.addListener(_update);
}
void _update() {
setState(() {
i = (_controller.value * 299792458).round();
});
}
@override
Widget build(BuildContext context) {
return Text('$i m/s');
}
}
这段代码根据动画的进度,分配一个从0到光速的值。
运行动画
现在我们只需要告诉动画执行应该多长时间,然后开始动画
class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin<MyWidget> {
AnimationController _controller;
int i = 0;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
_controller.addListener(_update);
}
void _update() {
setState(() {
i = (_controller.value * 299792458).round();
});
}
@override
Widget build(BuildContext context) {
return Text('$i m/s');
}
}
这个widget动画一旦添加到屏幕上就立刻开始执行,”动画“在一秒内从0过渡到光速。
销毁controller
不要忘记销毁AnimationController
,否则你的APP将会有内存泄漏。
class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin<MyWidget> {
AnimationController _controller;
int i = 0;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);
_controller.addListener(_update);
_controller.forward();
}
void _update() {
setState(() {
i = (_controller.value * 299792458).round();
});
}
@override
Widget build(BuildContext context) {
return Text('$i m/s');
}
}
只使用内置widget,可以吗
如你所见,独自完成所有操作并不理想,达到同样的功能,使用TweenAnimationBuilder
用了少的代码,并且不用在AnimationController
和调用setState
之间费神。
class MyPragmaticWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return TweenAnimationBuilder(
tween: IntTween(begin: 0, end: 299792458),
duration: const Duration(seconds: 1),
builder: (BuildContext context, int i, Widget child) {
return Text('$i m/s');
},
);
}
}
总结
我们了解了Ticker
是什么,了解了怎么给AnimationController
添加监听,了解了从根本上讲,动画就是快速连续的对widget进行rebuild,你可以在做在每一帧做你想做的任何事情。
系列文章: