# Flutter 动画
TIP
动画系统是任何一个 UI 框架的核心功能,也是开发者学习一个 UI 框架的重中之重,同时也是比较难掌握的一部分
视觉暂留
任何程序的动画原理都是一样的,即:视觉暂留,视觉暂留又叫视觉暂停,人眼在观察景物时,光信号传入大脑神经,需经过一段短暂的时间,光的作用结束后,视觉形象并不立即消失,这种残留的视觉称“后像”,视觉的这一现象则被称为“视觉暂留”。
人眼能保留 0.1-0.4 秒左右的图像,所以在 1 秒内看到连续的 25 张图像,人就会感到画面流畅,而 1 秒内看到连续的多少张图像称为 帧率,即 FPS,理论上 达到 24 FPS 画面比较流畅,而 Flutter,理论上可以达到 60 FPS。
# 动画核心-AnimationController
它是动画控制器,控制动画的启动、停止,还可以获取动画的运行状态,AnimationController 通常在 initState 方法中初始化
vsync:当创建 AnimationController 时,需要传递一个 vsync 参数,存在 vsync 时会防止屏幕外动画消耗不必要的资源,单个 AnimationController 的时候使用SingleTickerProviderStateMixin,多个 AnimationController 使用TickerProviderStateMixin。duration:表示动画执行的时间。
在 State dispose 生命周期中释放 AnimationController
late AnimationController _animationController;
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 600))
..addListener(() {
setState(() {
// AnimationController 的值默认是 0 到 1
_width = 120 + 120 * _animationController.value;
});
});
}
void dispose() {
super.dispose();
_animationController.dispose();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
其中最重要的就是 AnimationController,_animationController.value 是当前动画的值,默认从 0 到 1
也可以通过参数形式设置最大值和最小值
_controller = AnimationController(vsync: this,duration: Duration(milliseconds: 500),lowerBound: 100,upperBound: 200)
..addListener(() {
setState(() {
_size = _controller.value;
});
})
2
3
4
5
6
此时 _controller.value 的值就是从 100 变化到 200。
除了使用 addListener 监听每一帧,还可以监听动画状态的变化:
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 500),
lowerBound: 100,
upperBound: 200)
..addStatusListener((status) {
print('status:$status');
})
2
3
4
5
6
7
8
# 动画的状态分为四种
- dismissed:动画停止在开始处。
- forward:动画正在从开始处运行到结束处(正向运行)。
- reverse:动画正在从结束处运行到开始处(反向运行)。
- completed:动画停止在结束处。
# 再来看下动画的控制方法
- forward:正向执行动画。
- reverse:反向执行动画。
- repeat:反复执行动画。
- reset:重置动画。
forward 和 reverse 方法中都有 from 参数,这个参数的意义是一样的,表示动画从此值开始执行,而不再是从 lowerBound 到 upperBound。比如上面的例子中 from 参数设置 150,那么执行动画时,蓝色盒子瞬间变为 150,然后再慢慢变大到 200。
让蓝色盒子大小从 100 到 200,然后再变到 100,再到 200,如此反复
# Tween
AnimationController 设置的最小/大值类型是 double,如果动画的变化是颜色要如何处理?
AnimationController 在执行动画期间返回的值是 0 到 1,颜色从蓝色变为红色方法如下
Flutter 中把这种从 0 -> 1 转换为 蓝色 -> 红色 行为称之为 Tween(映射)。
class _TweenDemoState extends State<TweenDemo>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<Color> _animation;
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {});
});
_animation =
ColorTween(begin: Colors.blue, end: Colors.red).animate(_controller);
}
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
_controller.forward();
},
child: Container(
height: 100,
width: 100,
color: _animation.value,
alignment: Alignment.center,
child: Text(
'点我变色',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
);
}
void dispose() {
super.dispose();
_controller.dispose();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Tween 仅仅是映射,动画的控制依然由 AnimationController 控制,因此需要 Tween.animate(_controller) 将控制器传递给 Tween。
# Curve
TIP
动画中还有一个重要的概念就是 Curve,即动画执行曲线。Curve 的作用和 Android 中的 Interpolator(差值器)是一样的,负责控制动画变化的速率,通俗地讲就是使动画的效果能够以匀速、加速、减速、抛物线等各种速率变化。
动画加上 Curve 后,AnimationController 的最小/大值必须是 [0,1]之间,
# 自定义动画曲线需要继承 Curve 重写 transformInternal 方法即可
class _StairsCurve extends Curve {
double transformInternal(double t) {
return t;
}
}
2
3
4
5
6
7
直接返回 t 其实就是线性动画,即 Curves.linear,实现楼梯效果动画代码如下
class _StairsCurve extends Curve {
//阶梯的数量
final int num;
double _perStairY;
double _perStairX;
_StairsCurve(this.num) {
_perStairY = 1.0 / (num - 1);
_perStairX = 1.0 / num;
}
double transformInternal(double t) {
return _perStairY * (t / _perStairX).floor();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
_animation = Tween(begin: 100.0, end: 200.0)
.chain(CurveTween(curve: _StairsCurve(5)))
.animate(_controller);
2
3
# 总结
动画系统的核心是 AnimationController,而且是不可或缺的,动画中必须有 AnimationController,
而 Tween 和 Curve 则是对 AnimationController 的补充, Tween 实现了将 AnimationController [0,1]的值映射为其他类型的值,比如颜色、样式等,Curve 是 AnimationController 动画执行曲线,默认是线性运行。
# AnimationController 、 Tween 、Curve 进行关联的方式
AnimationController _controller;
Animation _animation;
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 1000))
..addListener(() {
setState(() {});
});
_animation = Tween(begin: 100.0, end: 200.0)
.animate(_controller);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
_animation = _controller.drive(Tween(begin: 100.0, end: 200.0));
# 加入 Curve
_animation = Tween(begin: 100.0, end: 200.0)
.chain(CurveTween(curve: Curves.linear))
.animate(_controller);
2
3
_animation = _controller
.drive(CurveTween(curve: Curves.linear))
.drive(Tween(begin: 100.0, end: 200.0));
2
3
# Flutter 中组合动画使用 Interval
Animation _sizeAnimation = Tween(begin: 100.0, end: 300.0).animate(CurvedAnimation(
parent: _animationController, curve: Interval(0.5, 1.0)));
2
表示_sizeAnimation 动画从 0.5(一半)开始到结束,如果动画时长为 6 秒,_sizeAnimation 则从第 3 秒开始。
Interval 中 begin 和 end 参数值的范围是0.0到1.0。
# 特殊场景
TIP
一个红色的盒子,动画时长为 6 秒,前 40%的时间大小从 100->200,然后保持 200 不变 20%的时间,最后 40%的时间大小从 200->300,这种效果通过 TweenSequence 实现
_animation = TweenSequence([
TweenSequenceItem(
tween: Tween(begin: 100.0, end: 200.0)
.chain(CurveTween(curve: Curves.easeIn)),
weight: 40),
TweenSequenceItem(tween: ConstantTween<double>(200.0), weight: 20),
TweenSequenceItem(tween: Tween(begin: 200.0, end: 300.0), weight: 40),
]).animate(_animationController);
2
3
4
5
6
7
8
weight 表示每一个 Tween 的权重。
# 其他封装动画组件
# 系统封装的类似上面的组件是 AnimatedWidget,此类是抽象类
class AnimationDemo extends StatefulWidget {
State<StatefulWidget> createState() => _AnimationDemo();
}
class _AnimationDemo extends State<AnimationDemo>
with SingleTickerProviderStateMixin {
AnimationController _animationController;
Animation _animation;
void initState() {
_animationController =
AnimationController(duration: Duration(seconds: 2), vsync: this);
_animation = Tween(begin: .5, end: .1).animate(_animationController);
//开始动画
_animationController.forward();
super.initState();
}
Widget build(BuildContext context) {
return ScaleTransition(
scale: _animation,
child: Container(
height: 200,
width: 200,
color: Colors.red,
),
);
}
void dispose() {
_animationController.dispose();
super.dispose();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# ImplicitlyAnimatedWidget
AnimatedWidget 只是封装了 setState,系统是否有封装 AnimationController、Tween、Curve 且自动管理 AnimationController 的组件呢?有的,此组件就是 ImplicitlyAnimatedWidget,ImplicitlyAnimatedWidget 也是一个抽象类
class AnimatedWidgetDemo extends StatefulWidget {
_AnimatedWidgetDemoState createState() => _AnimatedWidgetDemoState();
}
class _AnimatedWidgetDemoState extends State<AnimatedWidgetDemo> {
double _opacity = 1.0;
Widget build(BuildContext context) {
return Center(
child: AnimatedOpacity(
opacity: _opacity,
duration: Duration(seconds: 2),
child: GestureDetector(
onTap: () {
setState(() {
_opacity = 0;
});
},
child: Container(
height: 60,
width: 150,
color: Colors.blue,
),
),
),
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 隐式动画组件
只需提供给组件动画开始、结束值,组件创建 AnimationController、Curve、Tween,执行动画,释放 AnimationController,我们称之为隐式动画组件,隐式动画组件有: AnimatedAlign、AnimatedContainer、AnimatedDefaultTextStyle、AnimatedOpacity、AnimatedPadding、AnimatedPhysicalModel、AnimatedPositioned、AnimatedPositionedDirectional、AnimatedTheme、SliverAnimatedOpacity、TweenAnimationBuilder、AnimatedContainer 等
# 显示动画组件
需要设置 AnimationController,控制动画的执行,使用显式动画可以完成任何隐式动画的效果,甚至功能更丰富一些,不过你需要管理该动画的 AnimationController 生命周期,AnimationController 并不是一个控件,所以需要将其放在 stateful 控件中。显示动画组件有:AlignTransition、AnimatedBuilder、AnimatedModalBarrier、DecoratedBoxTransition、DefaultTextStyleTransition、PositionedTransition、RelativePositionedTransition、RotationTransition、ScaleTransition、SizeTransition、SlideTransition 、FadeTransition 等
# 万能动画组件
显示动画组件和隐式动画组件中各有一个万能的组件,它们是 AnimatedBuilder 和 TweenAnimationBuilder,当系统中不存在我们想要的动画组件时,可以使用这两个组件
class AnimatedBuilderDemo extends StatefulWidget {
_AnimatedBuilderDemoState createState() => _AnimatedBuilderDemoState();
}
class _AnimatedBuilderDemoState extends State<AnimatedBuilderDemo>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<Color> _colorAnimation;
Animation<Size> _sizeAnimation;
void initState() {
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 2));
_colorAnimation =
ColorTween(begin: Colors.blue, end: Colors.red).animate(_controller);
_sizeAnimation =
SizeTween(begin: Size(100.0, 50.0), end: Size(200.0, 100.0))
.animate(_controller);
_controller.forward();
super.initState();
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, widget) {
return Container(
width: _sizeAnimation.value.width,
height: _sizeAnimation.value.height,
color: _colorAnimation.value,
);
},
),
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 是否内置动画组件使用
- 判断你的动画组件是否一直重复,比如一直转圈的 loading 动画,如果是选择显式动画。
- 判断你的动画组件是否需要多个组件联动,如果是选择显式动画。
- 判断你的动画组件是否需要组合动画,如果是选择显式动画。
- 如果上面三个条件都是否,就选择隐式动画组件,判断是否已经内置动画组件,如果没有,使用 TweenAnimationBuilder,有就直接使用内置动画组件。
- 选择显式动画组件,判断是否已经内置动画组件,如果没有,使用 AnimatedBuilder,有就直接使用内置动画组件。
# AnimatedList
AnimatedList 提供了一种简单的方式使列表数据发生变化时加入过渡动画
- itemBuilder 一个函数,列表的每一个索引会调用,这个函数有一个 animation 参数,可以设置成任何一个动画
- initialItemCount item 的个数
- scrollDirection 滚动方向,默认垂直
- controller scroll 控制器
class AnimatedListDemo extends StatefulWidget {
State<StatefulWidget> createState() => _AnimatedListDemo();
}
class _AnimatedListDemo extends State<AnimatedListDemo>
with SingleTickerProviderStateMixin {
List<int> _list = [];
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
void _addItem() {
final int _index = _list.length;
_list.insert(_index, _index);
_listKey.currentState.insertItem(_index);
}
void _removeItem() {
final int _index = _list.length - 1;
var item = _list[_index].toString();
_listKey.currentState.removeItem(
_index, (context, animation) => _buildItem(item, animation));
_list.removeAt(_index);
}
Widget _buildItem(String _item, Animation _animation) {
return SlideTransition(
position: _animation.drive(CurveTween(curve: Curves.easeIn)).drive(Tween<Offset>(begin: Offset(1,1),end: Offset(0,1))),
child: Card(
child: ListTile(
title: Text(
_item,
),
),
),
);
}
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedList(
key: _listKey,
initialItemCount: _list.length,
itemBuilder: (BuildContext context, int index, Animation animation) {
return _buildItem(_list[index].toString(), animation);
},
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
FloatingActionButton(
onPressed: () => _addItem(),
child: Icon(Icons.add),
),
SizedBox(
width: 60,
),
FloatingActionButton(
onPressed: () => _removeItem(),
child: Icon(Icons.remove),
),
],
),
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# 路由动画
转场 就是从当前页面跳转到另一个页面,跳转页面在 Flutter 中通过 Navigator,跳转到新页面如下:
Navigator.push(context, MaterialPageRoute(builder: (context) {
return _TwoPage();
}));
2
3
回退到前一个页面:
Navigator.pop(context);
Flutter 提供了两个转场动画,分别为 MaterialPageRoute 和 CupertinoPageRoute,
- MaterialPageRoute 根据不同的平台显示不同的效果,Android 效果为从下到上,iOS 效果为从左到右。
- CupertinoPageRoute 不分平台,都是从左到右。
通过源代码发现,默认情况下没有动画效果。
自定义转场动画只需修改 transitionsBuilder 即可:
Navigator.push(
context,
PageRouteBuilder(pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return _TwoPage();
}, transitionsBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
return SlideTransition(
position: Tween(begin: Offset(-1, 0), end: Offset(0, 0))
.animate(animation),
child: child,
);
}));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
将其封装,方便使用:
class LeftToRightPageRoute extends PageRouteBuilder {
final Widget newPage;
LeftToRightPageRoute(this.newPage)
: super(
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) =>
newPage,
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) =>
SlideTransition(
position: Tween(begin: Offset(-1, 0), end: Offset(0, 0))
.animate(animation),
child: child,
),
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Navigator.push(context, LeftToRightPageRoute(_TwoPage()));
不仅是这些平移动画,前面所学的旋转、缩放等动画直接替换 SlideTransition 即可。
上面的动画只对新的页面进行了动画,如果想实现当前页面被新页面从顶部顶出的效果,实现方式如下:
class CustomPageRoute extends PageRouteBuilder {
final Widget currentPage;
final Widget newPage;
CustomPageRoute(this.currentPage, this.newPage)
: super(
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) =>
currentPage,
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) =>
Stack(
children: <Widget>[
SlideTransition(
position: new Tween<Offset>(
begin: const Offset(0, 0),
end: const Offset(0, -1),
).animate(animation),
child: currentPage,
),
SlideTransition(
position: new Tween<Offset>(
begin: const Offset(0, 1),
end: Offset(0, 0),
).animate(animation),
child: newPage,
)
],
),
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Navigator.push(context, CustomPageRoute(this, _TwoPage()));