跳到正文
Willh的个人博客 记录 · 思考 · 分享
《Flutter实战·第二版》

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类即可,下面我们演示一下具体如何实现。

我们需要分两步实现:

  1. 继承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的类没有什么不同。

  2. 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的终止状态就是用户传给它的值,而起始状态是不确定的,有以下两种情况:

    1. AnimatedDecoratedBox首次build,此时直接将其decoration值置为起始状态,即_decoration值为DecorationTween(begin: decoration) 。
    2. 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 当字体样式发生变化时,子组件中继承了该样式的文本组件会动态过渡到新样式。
表9-1:Flutter预置的动画过渡组件
下面我们通过一个示例来感受一下这些预置的动画过渡组件效果:
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所示:

图9-10

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