9.7 动画过渡组件
为了表述方便,本书约定,将在Widget属性发生变化时会执行过渡动画的组件统称为”动画过渡组件“,而动画过渡组件最明显的一个特征就是它会在内部自管理AnimationController。我们知道,为了方便使用者可以自定义动画的曲线、执行时长、方向等,在前面介绍过的动画封装方法中,通常都需要使用者自己提供一个AnimationController对象来自定义这些属性值。但是,如此一来,使用者就必须得手动管理AnimationController,这又会增加使用的复杂性。因此,如果也能将AnimationController进行封装,则会大大提高动画组件的易用性。
9.7.1 自定义动画过渡组件
我们要实现一个AnimatedDecoratedBox,它可以在decoration属性发生变化时,从旧状态变成新状态的过程可以执行一个过渡动画。根据前面所学的知识,我们实现了一个AnimatedDecoratedBox1组件:
1class AnimatedDecoratedBox1 extends StatefulWidget {
2 AnimatedDecoratedBox1({
3 Key? key,
4 required this.decoration,
5 required this.child,
6 this.curve = Curves.linear,
7 required this.duration,
8 this.reverseDuration,
9 });
10
11 final BoxDecoration decoration;
12 final Widget child;
13 final Duration duration;
14 final Curve curve;
15 final Duration? reverseDuration;
16
17
18 _AnimatedDecoratedBox1State createState() => _AnimatedDecoratedBox1State();
19}
20
21class _AnimatedDecoratedBox1State extends State<AnimatedDecoratedBox1>
22 with SingleTickerProviderStateMixin {
23
24 AnimationController get controller => _controller;
25 late AnimationController _controller;
26
27 /// The animation driving this widget's implicit animations.
28 Animation<double> get animation => _animation;
29 late Animation<double> _animation;
30
31 late DecorationTween _tween;
32
33
34 Widget build(BuildContext context) {
35 return AnimatedBuilder(
36 animation: _animation,
37 builder: (context, child) {
38 return DecoratedBox(
39 decoration: _tween.animate(_animation).value,
40 child: child,
41 );
42 },
43 child: widget.child,
44 );
45 }
46
47
48 void initState() {
49 super.initState();
50 _controller = AnimationController(
51 duration: widget.duration,
52 reverseDuration: widget.reverseDuration,
53 vsync: this,
54 );
55 _tween = DecorationTween(begin: widget.decoration);
56 _updateCurve();
57 }
58
59 void _updateCurve() {
60 _animation = CurvedAnimation(parent: _controller, curve: widget.curve);
61 }
62
63
64 void didUpdateWidget(AnimatedDecoratedBox1 oldWidget) {
65 super.didUpdateWidget(oldWidget);
66 if (widget.curve != oldWidget.curve) _updateCurve();
67 _controller.duration = widget.duration;
68 _controller.reverseDuration = widget.reverseDuration;
69 //正在执行过渡动画
70 if (widget.decoration != (_tween.end ?? _tween.begin)) {
71 _tween
72 ..begin = _tween.evaluate(_animation)
73 ..end = widget.decoration;
74
75 _controller
76 ..value = 0.0
77 ..forward();
78 }
79 }
80
81
82 void dispose() {
83 _controller.dispose();
84 super.dispose();
85 }
86}下面我们来使用AnimatedDecoratedBox1来实现按钮点击后背景色从蓝色过渡到红色的效果:
1Color _decorationColor = Colors.blue;
2var duration = Duration(seconds: 1);
3...//省略无关代码
4AnimatedDecoratedBox1(
5 duration: duration,
6 decoration: BoxDecoration(color: _decorationColor),
7 child: TextButton(
8 onPressed: () {
9 setState(() {
10 _decorationColor = Colors.red;
11 });
12 },
13 child: Text(
14 "AnimatedDecoratedBox",
15 style: TextStyle(color: Colors.white),
16 ),
17 ),
18)点击前效果如图9-8所示,点击后截取了过渡过程的一帧如图9-9所示:
点击后,按钮背景色会从蓝色向红色过渡,图9-9是过渡过程中的一帧,有点偏紫色,整个过渡动画结束后背景会变为红色。
上面的代码虽然实现了我们期望的功能,但是代码却比较复杂。稍加思考后,我们就可以发现,AnimationController的管理以及Tween更新部分的代码都是可以抽象出来的,如果我们这些通用逻辑封装成基类,那么要实现动画过渡组件只需要继承这些基类,然后定制自身不同的代码(比如动画每一帧的构建方法)即可,这样将会简化代码。
为了方便开发者来实现动画过渡组件的封装,Flutter提供了一个ImplicitlyAnimatedWidget抽象类,它继承自StatefulWidget,同时提供了一个对应的ImplicitlyAnimatedWidgetState类,AnimationController的管理就在ImplicitlyAnimatedWidgetState类中。开发者如果要封装动画,只需要分别继承ImplicitlyAnimatedWidget和ImplicitlyAnimatedWidgetState类即可,下面我们演示一下具体如何实现。
我们需要分两步实现:
继承
ImplicitlyAnimatedWidget类。1class AnimatedDecoratedBox extends ImplicitlyAnimatedWidget { 2 AnimatedDecoratedBox({ 3 Key? key, 4 required this.decoration, 5 required this.child, 6 Curve curve = Curves.linear, 7 required Duration duration, 8 }) : super( 9 key: key, 10 curve: curve, 11 duration: duration, 12 ); 13 final BoxDecoration decoration; 14 final Widget child; 15 16 17 _AnimatedDecoratedBoxState createState() { 18 return _AnimatedDecoratedBoxState(); 19 } 20}其中
curve、duration、reverseDuration三个属性在ImplicitlyAnimatedWidget中已定义。 可以看到AnimatedDecoratedBox类和普通继承自StatefulWidget的类没有什么不同。State类继承自
AnimatedWidgetBaseState(该类继承自ImplicitlyAnimatedWidgetState类)。1class _AnimatedDecoratedBoxState 2 extends AnimatedWidgetBaseState<AnimatedDecoratedBox> { 3 var _decoration; //定义一个Tween 4 5 6 Widget build(BuildContext context) { 7 return DecoratedBox( 8 decoration: _decoration.evaluate(animation), 9 child: widget.child, 10 ); 11 } 12 13 14 void forEachTween(visitor) { 15 // 在需要更新Tween时,基类会调用此方法 16 _decoration = visitor( 17 _decoration, 18 widget.decoration, 19 (value) => DecorationTween(begin: value), 20 ); 21 } 22}可以看到我们实现了
build和forEachTween两个方法。在动画执行过程中,每一帧都会调用build方法(调用逻辑在ImplicitlyAnimatedWidgetState中),所以在build方法中我们需要构建每一帧的DecoratedBox状态,因此得算出每一帧的decoration状态,这个我们可以通过_decoration.evaluate(animation)来算出,其中animation是ImplicitlyAnimatedWidgetState基类中定义的对象,_decoration是我们自定义的一个DecorationTween类型的对象,那么现在的问题就是它是在什么时候被赋值的呢?要回答这个问题,我们就得搞清楚什么时候需要对_decoration赋值。我们知道_decoration是一个Tween,而Tween的主要职责就是定义动画的起始状态(begin)和终止状态(end)。对于AnimatedDecoratedBox来说,decoration的终止状态就是用户传给它的值,而起始状态是不确定的,有以下两种情况:AnimatedDecoratedBox首次build,此时直接将其decoration值置为起始状态,即_decoration值为DecorationTween(begin: decoration)。AnimatedDecoratedBox的decoration更新时,则起始状态为_decoration.animate(animation),即_decoration值为DecorationTween(begin: _decoration.animate(animation),end:decoration)。
现在forEachTween的作用就很明显了,它正是用于来更新Tween的初始值的,在上述两种情况下会被调用,而开发者只需重写此方法,并在此方法中更新Tween的起始状态值即可。而一些更新的逻辑被屏蔽在了visitor回调,我们只需要调用它并给它传递正确的参数即可,visitor方法签名如下:
1 Tween<T> visitor(
2 Tween<T> tween, //当前的tween,第一次调用为null
3 T targetValue, // 终止状态
4 TweenConstructor<T> constructor,//Tween构造器,在上述三种情况下会被调用以更新tween
5 );可以看到,通过继承ImplicitlyAnimatedWidget和ImplicitlyAnimatedWidgetState类可以快速的实现动画过渡组件的封装,这和我们纯手工实现相比,代码简化了很多。
如果读者还有疑惑,建议查看
ImplicitlyAnimatedWidgetState的源码并结合本示例代码对比理解。
9.7.2 Flutter预置的动画过渡组件
Flutter SDK中也预置了很多动画过渡组件,实现方式和大都和AnimatedDecoratedBox差不多,如表9-1所示:
| 组件名 | 功能 |
|---|---|
| AnimatedPadding | 在padding发生变化时会执行过渡动画到新状态 |
| AnimatedPositioned | 配合Stack一起使用,当定位状态发生变化时会执行过渡动画到新的状态。 |
| AnimatedOpacity | 在透明度opacity发生变化时执行过渡动画到新状态 |
| AnimatedAlign | 当alignment发生变化时会执行过渡动画到新的状态。 |
| AnimatedContainer | 当Container属性发生变化时会执行过渡动画到新的状态。 |
| AnimatedDefaultTextStyle | 当字体样式发生变化时,子组件中继承了该样式的文本组件会动态过渡到新样式。 |
1import 'package:flutter/material.dart';
2
3class AnimatedWidgetsTest extends StatefulWidget {
4
5 _AnimatedWidgetsTestState createState() => _AnimatedWidgetsTestState();
6}
7
8class _AnimatedWidgetsTestState extends State<AnimatedWidgetsTest> {
9 double _padding = 10;
10 var _align = Alignment.topRight;
11 double _height = 100;
12 double _left = 0;
13 Color _color = Colors.red;
14 TextStyle _style = TextStyle(color: Colors.black);
15 Color _decorationColor = Colors.blue;
16
17
18 Widget build(BuildContext context) {
19 var duration = Duration(seconds: 5);
20 return SingleChildScrollView(
21 child: Column(
22 children: <Widget>[
23 RaisedButton(
24 onPressed: () {
25 setState(() {
26 _padding = 20;
27 });
28 },
29 child: AnimatedPadding(
30 duration: duration,
31 padding: EdgeInsets.all(_padding),
32 child: Text("AnimatedPadding"),
33 ),
34 ),
35 SizedBox(
36 height: 50,
37 child: Stack(
38 children: <Widget>[
39 AnimatedPositioned(
40 duration: duration,
41 left: _left,
42 child: RaisedButton(
43 onPressed: () {
44 setState(() {
45 _left = 100;
46 });
47 },
48 child: Text("AnimatedPositioned"),
49 ),
50 )
51 ],
52 ),
53 ),
54 Container(
55 height: 100,
56 color: Colors.grey,
57 child: AnimatedAlign(
58 duration: duration,
59 alignment: _align,
60 child: RaisedButton(
61 onPressed: () {
62 setState(() {
63 _align = Alignment.center;
64 });
65 },
66 child: Text("AnimatedAlign"),
67 ),
68 ),
69 ),
70 AnimatedContainer(
71 duration: duration,
72 height: _height,
73 color: _color,
74 child: TextButton(
75 onPressed: () {
76 setState(() {
77 _height = 150;
78 _color = Colors.blue;
79 });
80 },
81 child: Text(
82 "AnimatedContainer",
83 style: TextStyle(color: Colors.white),
84 ),
85 ),
86 ),
87 AnimatedDefaultTextStyle(
88 child: GestureDetector(
89 child: Text("hello world"),
90 onTap: () {
91 setState(() {
92 _style = TextStyle(
93 color: Colors.blue,
94 decorationStyle: TextDecorationStyle.solid,
95 decorationColor: Colors.blue,
96 );
97 });
98 },
99 ),
100 style: _style,
101 duration: duration,
102 ),
103 AnimatedDecoratedBox(
104 duration: duration,
105 decoration: BoxDecoration(color: _decorationColor),
106 child: TextButton(
107 onPressed: () {
108 setState(() {
109 _decorationColor = Colors.red;
110 });
111 },
112 child: Text(
113 "AnimatedDecoratedBox",
114 style: TextStyle(color: Colors.white),
115 ),
116 ),
117 )
118 ].map((e) {
119 return Padding(
120 padding: EdgeInsets.symmetric(vertical: 16),
121 child: e,
122 );
123 }).toList(),
124 ),
125 );
126 }
127}运行后效果如图9-10所示:

读者可以点击一下相应组件来查看一下实际的运行效果。