基本动画概念和类
Flutter 中的动画系统基于类型化的 Animation
对象。 Widgets 既可以通过读取当前值和监听状态变化直接合并动画到 build 函数,也可以作为传递给其他 widgets 的更精细动画的基础。
Animation<double>
在 Flutter 中,动画对象无法获取屏幕上显示的内容。 Animation
是一个已知当前值和状态(已完成或已解除)的抽象类。一个比较常见的动画类型是 Animation
。
一个 Animation
对象在一段时间内,持续生成介于两个值之间的插入值。这个 Animation
对象输出的可能是直线,曲线,阶梯函数,或者任何自定义的映射。根据 Animation
对象的不同控制方式,它可以反向运行,或者中途切换方向。
动画还可以插入除 double 以外的类型,比如 Animation
或者 Animation
。
Animation
对象具有状态。它的当前值在 .value
中始终可用。
Animation
对象与渲染或 build()
函数无关。
CurvedAnimation
CurvedAnimation
定义动画进程为非线性曲线。
content_copy
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);
提示
Curves
类定义了很多常用曲线,或者您也可以自定义。例如:
content_copy
import 'dart:math';
class ShakeCurve extends Curve {
@override
double transform(double t) => sin(t * pi * 2);
}
浏览 Curves
文档以获取完整的(带有预览)Flutter 附带的 Curves
常数列表。
CurvedAnimation
和 AnimationController
(下面将会详细说明)都是 Animation
类型,所以可以互换使用。 CurvedAnimation
封装正在修改的对象 — 不需要将 AnimationController
分解成子类来实现曲线。
AnimationController
AnimationController
是个特殊的 Animation
对象,每当硬件准备新帧时,他都会生成一个新值。默认情况下,AnimationController
在给定期间内会线性生成从 0.0 到 1.0 的数字。例如,这段代码创建了一个动画对象,但是没有启动运行。
content_copy
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
AnimationController
源自于 Animation
,所以可以用在任何需要 Animation
对象的地方。但是 AnimationController
还有其他方法控制动画。例如,使用 .forward()
方法启动动画。数字的生成与屏幕刷新关联,所以一般来说每秒钟会生成 60 个数字。数字生成之后,每个动画对象都调用附加 Listener 对象。为每个 child 创建自定义显示列表,请参考 RepaintBoundary
。
创建 AnimationController
的同时,也赋予了一个 vsync
参数。 vsync
的存在防止后台动画消耗不必要的资源。您可以通过添加 SingleTickerProviderStateMixin
到类定义,将有状态的对象用作 vsync。可参考 GitHub 网站 animate1 中的示例。
提示
在一些情况下,一个位置可能会超过 AnimationController
的 0.0-1.0 的范围。例如,fling()
函数允许提供速度,力和位置(通过 Force 对象)。这个位置可以是任意的,所以可能会超出 0.0-1.0 的范围。
即使 AnimationController
在范围内, CurvedAnimation
也可能会出现超出 0.0-1.0 范围的情况。根据所选曲线的不同,CurvedAnimation
的输出范围可能会超过输入。举个例子,弹性曲线(比如Curves.elasticIn)会明显超出或低于默认范围。
Tween
在默认情况下,AnimationController
对象的范围是 0.0-0.1。如果需要不同的范围或者不同的数据类型,可以使用 Tween
配置动画来插入不同的范围或数据类型。例如下面的示例中,Tween
的范围是 -200 到 0.0。
content_copy
tween = Tween<double>(begin: -200, end: 0);
Tween
是无状态的对象,只有 begin
和 end
。 Tween
的这种单一用途用来定义从输入范围到输出范围的映射。输入范围一般为 0.0-1.0,但这并不是必须的。
Tween
源自 Animatable
,而不是 Animation
。像动画这样的可动画元素不必重复输出。例如,ColorTween
指定了两种颜色之间的过程。
content_copy
colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);
Tween
对象不存储任何状态。而是提供 evaluate(Animation animation)
方法,将映射函数应用于动画当前值。 Animation
对象的当前值可以在 .value
方法中找到。 evaluate 函数还执行一些内部处理内容,比如确保当动画值在 0.0 和1.0 时分别返回起始点和终点。
Tween.animate
要使用 Tween
对象,请在 Tween
调用 animate()
,传入控制器对象。例如,下面的代码在 500 ms 的进程中生成 0-255 范围内的整数值。
content_copy
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);
提示
animate()
方法会返回一个 Animation
,而不是 Animatable
。
下面的示例展示了一个控制器,一个曲线,和一个 Tween
。
content_copy
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
final Animation<double> curve =
CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(curve);
动画通知
一个 Animation
对象可以有不止一个 Listener
和 StatusListener
,用 addListener()
和 addStatusListener()
来定义。当动画值改变时调用 Listener
。 Listener
最常用的操作是调用 setState()
进行重建。当一个动画开始,结束,前进或后退时,会调用 StatusListener
,用 AnimationStatus
来定义。下一部分有关于 addListener()
方法的示例,在 监控动画过程 中也有 addStatusListener()
的示例。
动画示例
这部分列举了五个动画示例,每个示例都提供了源代码的链接。
渲染动画
要点
- 如何使用
addListener()
和setState()
为 widget 添加基础动画。 - 每次动画生成一个新的数字,
addListener()
函数就会调用setState()
。 - 如何使用所需的
vsync
参数定义一个AnimationController
。 - 理解 “
..addListener
” 中的 “..
” 语法,也称为 Dart 的 cascade notation。 - 如需使类私有,在名字前面添加下划线(
_
)。
目前为止,我们学习了如何随着时间生成数字序列。但屏幕上并未显示任何内容。要显示一个 Animation
对象,需将 Animation
对象存储为您的 widget 成员,然后用它的值来决定如何绘制。
参考下面的应用程序,它没有使用动画绘制 Flutter logo。
content_copy
import 'package:flutter/material.dart';
void main() => runApp(const LogoApp());
class LogoApp extends StatefulWidget {
const LogoApp({super.key});
@override
State<LogoApp> createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
height: 300,
width: 300,
child: const FlutterLogo(),
),
);
}
}
源代码: animate0
下面的代码是加入动画效果的,logo 从无到全屏。当定义 AnimationController
时,必须要使用一个 vsync
对象。在 AnimationController
部分 会具体介绍 vsync
参数。
对比无动画示例,改动部分被突出显示:
{animate0 → animate1}/lib/main.dart Viewed
@@ -9,16 +9,39 @@ | |
---|---|
99 | State<LogoApp> createState() => _LogoAppState(); |
1010 | } |
11 | - class _LogoAppState extends State<LogoApp> { |
11 | + class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin { |
12 | + late Animation<double> animation; |
13 | + late AnimationController controller; |
14 | + |
15 | + @override |
16 | + void initState() { |
17 | + super.initState(); |
18 | + controller = |
19 | + AnimationController(duration: const Duration(seconds: 2), vsync: this); |
20 | + animation = Tween<double>(begin: 0, end: 300).animate(controller) |
21 | + ..addListener(() { |
22 | + setState(() { |
23 | + // The state that has changed here is the animation object’s value. |
24 | + }); |
25 | + }); |
26 | + controller.forward(); |
27 | + } |
28 | + |
1229 | @override |
1330 | Widget build(BuildContext context) { |
1431 | return Center( |
1532 | child: Container( |
1633 | margin: const EdgeInsets.symmetric(vertical: 10), |
17 | - height: |
18 | - width: |
34 | + height: animation.value, |
35 | + width: animation.value, |
1936 | child: const FlutterLogo(), |
2037 | ), |
2138 | ); |
2239 | } |
40 | + |
41 | + @override |
42 | + void dispose() { |
43 | + controller.dispose(); |
44 | + super.dispose(); |
45 | + } |
2346 | } |
源代码: animate1
因为addListener()
函数调用 setState()
,所以每次 Animation
生成一个新的数字,当前帧就被标记为 dirty,使得 build()
再次被调用。在 build()
函数中,container 会改变大小,因为它的高和宽都读取 animation.value
,而不是固定编码值。当 State
对象销毁时要清除控制器以防止内存溢出。
经过这些小改动,你成功创建了第一个 Flutter 动画。
Dart 语言技巧: 你可能对于 Dart 的级联操作符(..addListener()
中的两点)不太熟悉。这个语法意思是使用 animate()
的返回值调用 addListener()
方法。参考下面示例:
content_copy
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addListener(() {
// ···
});
这段代码相当于:
content_copy
animation = Tween<double>(begin: 0, end: 300).animate(controller);
animation.addListener(() {
// ···
});
更多关于级联操作符的内容,请参考 Dart Language Tour。
使用 AnimatedWidget 进行简化
要点
- 如何使用
AnimatedWidget
帮助类(代替addListener()
和setState()
)创建动画 widget。 - 利用
AnimatedWidget
创建一个可以运行重复使用动画的 widget。如需区分 widget 过渡,可以使用AnimatedBuilder
,你可以在 使用 AnimatedBuilder 进行重构 部分查阅更多信息。 - Flutter API 中的
AnimatedWidget
:AnimatedBuilder
,RotationTransition
,ScaleTransition
,SizeTransition
,SlideTransition
。
AnimatedWidget
基本类可以从动画代码中区分出核心 widget 代码。 AnimatedWidget
不需要保持 State
对象来 hold 动画。可以添加下面的 AnimatedLogo
类:
lib/main.dart (AnimatedLogo)
content_copy
class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({super.key, required Animation<double> animation})
: super(listenable: animation);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: const FlutterLogo(),
),
);
}
}
在绘制时,AnimatedLogo
会读取 animation
当前值。
LogoApp
持续控制 AnimationController
和 Tween
,并将 Animation
对象传给 AnimatedLogo
:
{animate1 → animate2}/lib/main.dart Viewed
@@ -1,10 +1,28 @@ | |
---|---|
11 | import 'package:flutter/material.dart'; |
22 | void main() => runApp(const LogoApp()); |
3 | + class AnimatedLogo extends AnimatedWidget { |
4 | + const AnimatedLogo({super.key, required Animation<double> animation}) |
5 | + : super(listenable: animation); |
6 | + |
7 | + @override |
8 | + Widget build(BuildContext context) { |
9 | + final animation = listenable as Animation<double>; |
10 | + return Center( |
11 | + child: Container( |
12 | + margin: const EdgeInsets.symmetric(vertical: 10), |
13 | + height: animation.value, |
14 | + width: animation.value, |
15 | + child: const FlutterLogo(), |
16 | + ), |
17 | + ); |
18 | + } |
19 | + } |
20 | + |
321 | class LogoApp extends StatefulWidget { |
422 | const LogoApp({super.key}); |
523 | @override |
624 | State<LogoApp> createState() => _LogoAppState(); |
725 | } |
@@ -15,32 +33,18 @@ | |
1533 | @override |
1634 | void initState() { |
1735 | super.initState(); |
1836 | controller = |
1937 | AnimationController(duration: const Duration(seconds: 2), vsync: this); |
20 | - animation = Tween<double>(begin: 0, end: 300).animate(controller) |
21 | - ..addListener(() { |
22 | - setState(() { |
23 | - // The state that has changed here is the animation object’s value. |
24 | - }); |
25 | - }); |
38 | + animation = Tween<double>(begin: 0, end: 300).animate(controller); |
2639 | controller.forward(); |
2740 | } |
2841 | @override |
29 | - Widget build(BuildContext context) |
30 | - return Center( |
31 | - child: Container( |
32 | - margin: const EdgeInsets.symmetric(vertical: 10), |
33 | - height: animation.value, |
34 | - width: animation.value, |
35 | - child: const FlutterLogo(), |
36 | - ), |
37 | - ); |
38 | - } |
42 | + Widget build(BuildContext context) => AnimatedLogo(animation: animation); |
3943 | @override |
4044 | void dispose() { |
4145 | controller.dispose(); |
4246 | super.dispose(); |
4347 | } |
源代码: animate2
监控动画过程
要点
- 使用
addStatusListener()
作为动画状态的变更提示,比如开始,结束,或改变方向。 - 通过在动画完成或返回起始状态时改变方向,使动画无限循环。
了解动画何时改变状态通常是很有用的,比如完成,前进或后退。可以通过 addStatusListener()
来获得提示。下面是之前示例修改后的代码,这样就可以监听状态的改变和更新。修改部分会突出显示:
content_copy
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addStatusListener((status) => print('$status'));
controller.forward();
}
// ...
}
运行这段代码,得到如下结果:
content_copy
AnimationStatus.forward
AnimationStatus.completed
下一步,在起始或结束时,使用 addStatusListener()
反转动画。制造“呼吸”效果:
{animate2 → animate3}/lib/main.dart Viewed
@@ -35,7 +35,15 @@ | |
---|---|
3535 | void initState() { |
3636 | super.initState(); |
3737 | controller = |
3838 | AnimationController(duration: const Duration(seconds: 2), vsync: this); |
39 | - animation = Tween<double>(begin: 0, end: 300).animate(controller) |
39 | + animation = Tween<double>(begin: 0, end: 300).animate(controller) |
40 | + ..addStatusListener((status) { |
41 | + if (status == AnimationStatus.completed) { |
42 | + controller.reverse(); |
43 | + } else if (status == AnimationStatus.dismissed) { |
44 | + controller.forward(); |
45 | + } |
46 | + }) |
47 | + ..addStatusListener((status) => print('$status')); |
4048 | controller.forward(); |
4149 | } |
源代码: animate3
使用 AnimatedBuilder 进行重构
要点
-
AnimatedBuilder
知道如何渲染过渡效果 - 但
AnimatedBuilder
不会渲染 widget,也不会控制动画对象。 - 使用
AnimatedBuilder
描述一个动画是其他 widget 构建方法的一部分。如果只是单纯需要用可重复使用的动画定义一个 widget,可参考文档:简单使用 AnimatedWidget。 - Flutter API 中
AnimatedBuilders
:BottomSheet
,ExpansionTile
,PopupMenu
,ProgressIndicator
,RefreshIndicator
,Scaffold
,SnackBar
,TabBar
,TextField
。
animate3 示例代码中有个问题,就是改变动画需要改变渲染 logo 的widget。较好的解决办法是,将任务区分到不同类里:
- 渲染 logo
- 定义动画对象
- 渲染过渡效果
您可以使用 AnimatedBuilder
类方法来完成分配。 AnimatedBuilder
作为渲染树的一个单独类。像 AnimatedWidget
,AnimatedBuilder
自动监听动画对象提示,并在必要时在 widget 树中标出,所以这时不需要调用 addListener()
。
应用于 animate4 示例的 widget 树长这样:
从 widget 树底部开始,渲染 logo 的代码很容易:
content_copy
class LogoWidget extends StatelessWidget {
const LogoWidget({super.key});
// Leave out the height and width so it fills the animating parent
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 10),
child: const FlutterLogo(),
);
}
}
图表中间的三部分都是用 GrowTransition
中的 build()
方法创建的,如下。 GrowTransition
widget 本身是无状态的,而且拥有定义过渡动画所需的一系列最终变量。 build() 函数创建并返回 AnimatedBuilder
, AnimatedBuilder
使用(Anonymous
builder)方法并将 LogoWidget 对象作为参数。渲染过渡效果实际上是在(Anonymous
builder)方法中完成的,该方法创建一个适当大小 Container
强制 LogoWidget
配合。
在下面这段代码中,一个比较棘手的问题是 child 看起来被指定了两次。其实是 child 的外部参照被传递给了 AnimatedBuilder
,再传递给匿名闭包,然后用作 child 的对象。最终结果就是 AnimatedBuilder
被插入渲染树的两个 widgets 中间。
content_copy
class GrowTransition extends StatelessWidget {
const GrowTransition(
{required this.child, required this.animation, super.key});
final Widget child;
final Animation<double> animation;
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return SizedBox(
height: animation.value,
width: animation.value,
child: child,
);
},
child: child,
),
);
}
}
最后,初始动画的代码看起来很像 animate2 的示例。 initState()
方法创建了 AnimationController
和 Tween
,然后用 animate()
绑定它们。神奇的是 build()
方法,它返回一个以LogoWidget
为 child 的 GrowTransition
对象,和一个驱动过渡的动画对象。上面列出了三个主要因素。
{animate2 → animate4}/lib/main.dart Viewed
@@ -1,27 +1,47 @@ | |
---|---|
11 | import 'package:flutter/material.dart'; |
22 | void main() => runApp(const LogoApp()); |
3 | - class |
4 | - const |
5 | - ~~ : super(listenable: animation);~~ |
3 | + class LogoWidget extends StatelessWidget { |
4 | + const LogoWidget({super.key}); |
5 | + |
6 | + // Leave out the height and width so it fills the animating parent |
7 | + @override |
8 | + Widget build(BuildContext context) { |
9 | + return Container( |
10 | + margin: const EdgeInsets.symmetric(vertical: 10), |
11 | + child: const FlutterLogo(), |
12 | + ); |
13 | + } |
14 | + } |
15 | + |
16 | + class GrowTransition extends StatelessWidget { |
17 | + const GrowTransition( |
18 | + {required this.child, required this.animation, super.key}); |
19 | + |
20 | + final Widget child; |
21 | + final Animation<double> animation; |
622 | @override |
723 | Widget build(BuildContext context) { |
8 | - final animation = listenable as Animation<double>; |
924 | return Center( |
10 | - child: |
11 | - |
12 | - |
13 | - ~~ width:~~ |
14 | - ~~ child~~: |
25 | + child: AnimatedBuilder( |
26 | + animation: animation, |
27 | + builder: (context, child) { |
28 | + return SizedBox( |
29 | + height: animation.value, |
30 | + width: animation.value, |
31 | + child: child, |
32 | + ); |
33 | + }, |
34 | + child: child, |
1535 | ), |
1636 | ); |
1737 | } |
1838 | } |
1939 | class LogoApp extends StatefulWidget { |
2040 | const LogoApp({super.key}); |
2141 | @override |
2242 | State<LogoApp> createState() => _LogoAppState(); |
@@ -34,18 +54,23 @@ | |
3454 | @override |
3555 | void initState() { |
3656 | super.initState(); |
3757 | controller = |
3858 | AnimationController(duration: const Duration(seconds: 2), vsync: this); |
3959 | animation = Tween<double>(begin: 0, end: 300).animate(controller); |
4060 | controller.forward(); |
4161 | } |
4262 | @override |
43 | - Widget build(BuildContext context) |
63 | + Widget build(BuildContext context) { |
64 | + return GrowTransition( |
65 | + animation: animation, |
66 | + child: const LogoWidget(), |
67 | + ); |
68 | + } |
4469 | @override |
4570 | void dispose() { |
4671 | controller.dispose(); |
4772 | super.dispose(); |
4873 | } |
4974 | } |
源代码: animate4
同步动画
重点提醒
-
Curves
类定义了一列常用的曲线,您可以配合CurvedAnimation
来使用。
在这部分内容中,您会根据 监控动画过程 (animate3) 创建示例,该示例将使用 AnimatedWidget
持续进行动画。可以用在需要对透明度进行从透明到不透明动画处理的情况。
提示
这个示例展示了如何在同一个动画控制器中使用复合补间动画,每个补间动画控制一个动画的不同效果。仅用于说明目的。如果您需要在代码中加入渐变不透明度和尺寸效果,可能需要用 FadeTransition
和 SizeTransition
来代替。
每个补间动画控制一个动画的不同方面,例如:
content_copy
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
sizeAnimation = Tween<double>(begin: 0, end: 300).animate(controller);
opacityAnimation = Tween<double>(begin: 0.1, end: 1).animate(controller);
通过 sizeAnimation.value
我们可以得到尺寸,通过 opacityAnimation.value
可以得到不透明度,但是 AnimatedWidget
的构造函数只读取单一的 Animation
对象。为了解决这个问题,该示例创建了一个 Tween
对象并计算确切值。
修改 AnimatedLogo
来封装其 Tween
对象,以及其 build()
方法在母动画对象上调用 Tween.evaluate()
来计算所需的尺寸和不透明度值。下面的代码中将这些改动突出显示:
content_copy
class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({super.key, required Animation<double> animation})
: super(listenable: animation);
// Make the Tweens static because they don't change.
static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
static final _sizeTween = Tween<double>(begin: 0, end: 300);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Opacity(
opacity: _opacityTween.evaluate(animation),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
height: _sizeTween.evaluate(animation),
width: _sizeTween.evaluate(animation),
child: const FlutterLogo(),
),
),
);
}
}
class LogoApp extends StatefulWidget {
const LogoApp({super.key});
@override
State<LogoApp> createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
controller.forward();
}
@override
Widget build(BuildContext context) => AnimatedLogo(animation: animation);
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
以上就是flutter的显式动画的构建学习了,想成为flutter工程师还要学习许多;比喻:从最基础的语法dart、UI、线程、启动流程、framework框架、性能监控等等一系列的进阶学习。这里我推荐大家参考学习《flutter手册》里面是从最基础的语法dart开始教学。我建议新手或者想进阶flutter技术的,可以通过这个手册辅助自己进阶。
文末
Flutter已经成为最流行的跨平台开发框架
先来看这张图,2021年,Flutter的占有率第一次超过RN,成为被开发者使用最多的跨平台开发框架。
那么Flutter的活跃度如何呢,我们再来看张图
138k Star足以说明受追捧的程度了。
10k+的open issues和56k的closed,也充分说明了社区的活跃以及官方的跟进力度。而Flutter采用Dart开发, 上手难度也不算大, 同时也支持了安卓, ios, windows等平台, 对比Uni-app性能提升了很多, 与原生开发等应用体验上几乎无异
网友评论