Flutter 学习之旅08 动画


1 动画基础

不管是什么视图框架,动画的实现原理都是相同的,即在一段时限的时间内,多次、快速地改变视图外观来实现连续播放的效果。

视图的一次改变称为一个动画帧,对应一次屏幕刷新,决定动画流畅度的一个重要指标就是帧率(Frame Per Second,FPS),即每秒的动画帧数。

对于人眼,动画帧率超过16FPS就认为是流畅的,超过32FPS基本就感受不到任何卡顿,为了保证良好的视觉体验需要帧率尽可能达到60FPS。

Flutter框架是可以实现60FPS的,这和原生应用的帧率标准是基本持平的。

1.1 Animation

Animation是一个Flutter动画中的核心抽象类,主要用于保存动画的插值和状态,它本身与视图渲染没有任何关系。

Animation对象的状态有4种:

1)dismissed:动画处于开始状态;

2)forward:动画正在正向执行;

3)reverse:动画正在反向执行;

4)completed:动画处于结束状态。

Animation对象有Listeners和StatusListeners两个监听器两个监听器,可以用来监听动画的变化。

如果需要监听动画每一帧以及执行状态的变化,可以使用addListener()和addStatusListener()。

addListener()用于给Animation对象添加帧监听器,每一帧都会被调用,当帧监听器监听到状态发生改变后就会调用setState()来触发视图的重建。

addStatusListener()用于给Animation对象添加动画状态改变监听器,动画开始、结束、正向或反向时就会调用状态改变的监听器。

1.2 AnimationController

AnimationController表示动画控制器,是一个特殊的Animation对象,主要用于控制动画的开始、结束、正向、反向等操作。

1
2
3
4
5
6
AnimationController controller = AnimationController (
duration: const Duration(milliseconds: 2000),
lowerBound: 10.0,
upperBound: 20.0,
vsync: this,//接收一个TickerProvider类型的对象,防止屏幕锁屏后继续执行动画造成资源浪费
)

使用AnimationController 创建的Animation动画对象,默认情况下不会启动,要让动画运行,需要调用AnimationController 的forward()方法。

TickerProvider是一个抽象类:

1
2
3
abstract class TickerProvider {
Ticker createTicker(TickerCallback onTick);
}

Flutter应用在启动时会绑定一个SchedulerBinding,通过SchedulerBinding可以给每一次屏幕刷新添加回调,而Ticker对象就是通过SchedulerBinding来实现屏幕刷新回调的,每次屏幕刷新都会调用TickerCallback 。

在Flutter动画中,使用Ticker而不是Timer来驱动动画,可以有效防止屏幕外动画(如锁屏)带来的资源消耗。Flutter在屏幕刷新时会通知绑定的SchedulerBinding,而Ticker是受SchedulerBinding驱动的,锁屏后屏幕停止刷新,Ticker也就不会再被触发。

AnimationController 常用函数:

1)forward():开始播放动画;

2)stop():停止动画播放;

3)reset():重置动画为初始化状态;

4)reverse():反向动画播放,必须正向动画播放完成后才有效;

5)repeat():循环播放动画;

6)dispose():销毁动画,释放动画占用资源。

1.3 Curve

Curve主要用来控制动画随时间的变化率,默认为均匀的线性变化。

在Flutter应用开发中,可以通过CurvedAnimation来指定动画的曲线:

1
2
3
4
CurvedAnimation curve = CurvedAnimation (
parent: controller,
curve: Curves.easeIn
)

Curves类定义的动画曲线:

1)linear:匀速动画;

2)decelerate:匀减速动画;

3)ease:先加速后减速动画;

4)easeIn:先快后慢动画;

5)easeOut:先慢后快动画;

6)easeInOut:先慢,然后加速,最后减速动画。

示例代码:

1
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
import 'package:flutter/material.dart';

void main() => runApp(CurvePage());

class CurvePage extends StatefulWidget {
State<StatefulWidget> createState() {
return CurveState();
}
}

class CurveState extends State<CurvePage> with SingleTickerProviderStateMixin{
CurvedAnimation curve;
AnimationController controller;

@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 5000),
vsync: this,
);
curve = CurvedAnimation(
parent: controller,
curve: Curves.easeIn
)..addListener(() {
setState(() {});
});
controller.forward();
}

@override
Widget build(BuildContext context) {
return MaterialApp(
title: '动画',
home: Scaffold(
appBar: AppBar(title: Text('动画')),
body: Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: 300 * curve.value,
width: 300 * curve.value,
child: FlutterLogo()
)
)
)
);
}

@override
void dispose() {
controller.dispose();
super.dispose();
}
}

运行上面的代码,动画会在5秒内执行先慢后快的非线性动画,图标慢慢变大。

1.4 Tween

默认情况下,AnimationController创建的动画对象的取值范围是[0.0, 1.0],可以使用Tween来自定义范围。

1
Tween doubleTween = Tween<double>(begin: 0.0, end: 100.0);

Tween是一个无状态对象,它继承自Animatable,Animatable是一个控制动画类型的类,定义了动画值的映射规则,需要传入begin和end两个参数。

Animatable支持多取值类型,如数字、颜色等。

1
2
3
4
Tween colorTween = ColorTween(
begin: Colors.white,
end: Colors.black
);

如果需要使用Tween对象,可以调用其animate(),然后传入一个控制器对象。

1
2
3
4
5
6
7
8
9
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this
);

Animation<int> alpha = IntTween(
begin: 0,
end: 255
).animate(controller);

2 动画组件

2.1 基础用法

使用Animation和AnimationController实现心跳动画示例。

示例代码:

1
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
import 'package:flutter/material.dart';

void main() => runApp(HeartAnimPage());

class HeartAnimPage extends StatefulWidget {
HeartAnimPage({Key key}): super(key: key);
_HeartAnimState createState() => _HeartAnimState();
}

class _HeartAnimState extends State<HeartAnimPage> with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;

@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this
);
animation = CurvedAnimation(
parent: controller,
curve: Curves.easeInOut,
reverseCurve: Curves.easeOut
);
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
animation = Tween(begin: 50.0, end: 120.0).animate(controller);
controller.forward();
}

@override
Widget build(BuildContext context) {
return MaterialApp(
title: '动画--基础用法',
home: Scaffold(
appBar: AppBar(title: Text('动画--基础用法')),
body: Center(
child: AnimatedBuilder(
animation: animation,
builder: (ctx, child) {
return Icon(Icons.favorite, color: Colors.red, size: animation.value);
},
)
)
)
);
}

@override
void dispose() {
controller.dispose();
super.dispose();
}
}

示例效果:

img

2.2 AnimatedWidget

Flutter官方提供了AnimatedWidget组件,用于简化动画开发中addListener()和setState()的调用流程。

使用AnimatedWidget组件重构心跳动画的代码:

1
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
import 'package:flutter/material.dart';

void main() => runApp(HeartAnimPage());

class HeartAnimPage extends StatefulWidget {
HeartAnimPage({Key key}): super(key: key);
_HeartAnimState createState() => _HeartAnimState();
}

class _HeartAnimState extends State<HeartAnimPage> with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;

@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this
);
animation = CurvedAnimation(
parent: controller,
curve: Curves.easeInOut,
reverseCurve: Curves.easeOut
);
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
animation = Tween(begin: 50.0, end: 120.0).animate(controller);
controller.forward();
}

@override
Widget build(BuildContext context) {
return MaterialApp(
title: '动画--AnimatedWidget',
home: Scaffold(
appBar: AppBar(title: Text('动画--AnimatedWidget')),
body: Center(
child: HeatAnimatedWidget(animation)
)
)
);
}

@override
void dispose() {
controller.dispose();
super.dispose();
}
}

class HeatAnimatedWidget extends AnimatedWidget {
HeatAnimatedWidget(Animation animation): super(listenable: animation);

@override
Widget build(BuildContext context) {
Animation animation = listenable;
return Icon(Icons.favorite, color: Colors.red, size: animation.value);
}
}

AnimatedWidget组件有效地将业务逻辑和功能组件分离,简化了动画操作的逻辑。

2.3 AnimatedBuilder

使用AnimatedBuilder组件, 系统只会重新构建动画组件自身,对父子组件不做任何处理,从而避免不必要的性能开销。

使用AnimatedBuilder组件还有以下优点:

1)不需要显示添加帧监听器以及调用setState();

2)缩小动画构建的范围,避免不必要的视图构建,从而提高视图渲染性能;

3)可以封装一些常见的动画效果,从而提高代码的复用性。

3 转场动画

在原生Android开发中,可以使用共享元素动画(Shared Element Transition,又称Hero Transition)来实现多个页面的切换动画。

Hero指的是可以在路由(即Flutter页面)之间飞行的组件。

在Flutter中,实现Hero动画效果至少需要两个路由,即源路由和目标路由,然后使用Hero组件包裹在需要动画控制的组件外面,同时为它们设置相同的tag属性。

Hero动画组件的构造函数:

1
2
3
4
5
6
7
8
9
const Hero({
Key key,
@required this.tag,//Hero组件的标识,两个Hero组件就是通过tag标识关联起来的
this.createRectTween,
this.flightShuttleBuilder,
this.placeholderBuilder,
this.transitionOnUserGestures = false,
@required this.child,
});

示例代码:

1
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
69
70
import 'package:flutter/material.dart';

void main() => runApp(HeroPage());

class HeroPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '动画--转场动画',
home: HeroAPage(),
);
}
}

class HeroAPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('第一个页面')),
body: Container(
child: Center(
child: GestureDetector(
child: Hero(
tag: 'avatar',
child: Image.asset(
'images/wx.png',
width: 100,
height: 100,
fit: BoxFit.fill
),
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (BuildContext context) => HeroBPage())
);
}
)
)
)
);
}
}

class HeroBPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('第二个页面')),
body: Container(
child: Center(
child: GestureDetector(
child: Hero(
tag: 'avatar',
child: Image.asset(
'images/wx.png',
width: 300,
height: 300,
fit: BoxFit.fill
),
),
onTap: () {
Navigator.pop(context);
}
)
)
)
);
}
}

示例效果:

img

4 交错动画

在Flutter中,渐变、平移、缩放和旋转动画都属于基础动画,如果要实现一些复杂的动画效果,可以把这些基础动画组合起来形成一个动画序列或重叠动画,Flutter将这些动画序列或重叠动画称为交错动画。

在Flutter开发中,使用交错动画需要满足以下几点:

1)创建交错动画时需要创建多个动画对象;

2)一个AnimationController动画控制器控制所有的动画对象;

3)给每一个动画对象指定时间间隔,且动画的时间间隔为[0.0, 1.0]。

使用交错动画实现Flutter图标缩放和渐变的动画示例。

示例代码:

1
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
69
70
71
72
73
74
75
76
77
78
79
import 'package:flutter/material.dart';

void main() => runApp(ParallelWidget());

class ParallelWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '动画--交错动画',
home: ParallelPage(),
);
}
}

class ParallelPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return ParallelState();
}
}

class ParallelState extends State<ParallelPage> with TickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;

@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 3000)
);
animation = CurvedAnimation(
parent: controller,
curve: Curves.bounceIn
);
animation.addStatusListener((status) {
if(status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
controller.forward();
}

@override
Widget build(BuildContext context) {
return ParallelAnimatedWidget(animation: animation);
}

@override
void dispose() {
super.dispose();
controller.dispose();
}
}

class ParallelAnimatedWidget extends AnimatedWidget {
final _opacityTween = Tween<double>(begin: 0.1, end: 1.0);
final _sizeTween = Tween<double>(begin: 0.0, end: 300.0);

ParallelAnimatedWidget({Key key, Animation<double> animation}): super(key: key, listenable: animation);

Widget build(BuildContext context) {
Animation<double> animation = listenable;
return Center(
child: Opacity(
opacity: _opacityTween.evaluate(animation),
child: Container(
margin: EdgeInsets.symmetric(vertical: 10.0),
height: _sizeTween.evaluate(animation),
width: _sizeTween.evaluate(animation),
child: FlutterLogo()
),
),
);
}
}

示例效果:

img