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

10.3 组合实例:TurnBox

我们之前已经介绍过RotatedBox,它可以旋转子组件,但是它有两个缺点:一是只能将其子节点以90度的倍数旋转;二是当旋转的角度发生变化时,旋转角度更新过程没有动画。

本节我们将实现一个TurnBox组件,它不仅可以以任意角度来旋转其子节点,而且可以在角度发生变化时执行一个动画以过渡到新状态,同时,我们可以手动指定动画速度。

TurnBox的完整代码如下:

1import 'package:flutter/widgets.dart';
2
3class TurnBox extends StatefulWidget {
4  const TurnBox({
5    Key key,
6    this.turns = .0, //旋转的“圈”数,一圈为360度,如0.25圈即90度
7    this.speed = 200, //过渡动画执行的总时长
8    this.child
9  }) :super(key: key);
10
11  final double turns;
12  final int speed;
13  final Widget child;
14
15  
16  _TurnBoxState createState() => _TurnBoxState();
17}
18
19class _TurnBoxState extends State<TurnBox>
20    with SingleTickerProviderStateMixin {
21  AnimationController _controller;
22
23  
24  void initState() {
25    super.initState();
26    _controller = AnimationController(
27        vsync: this,
28        lowerBound: -double.infinity,
29        upperBound: double.infinity
30    );
31    _controller.value = widget.turns;
32  }
33
34  
35  void dispose() {
36    _controller.dispose();
37    super.dispose();
38  }
39
40  
41  Widget build(BuildContext context) {
42    return RotationTransition(
43      turns: _controller,
44      child: widget.child,
45    );
46  }
47
48  
49  void didUpdateWidget(TurnBox oldWidget) {
50    super.didUpdateWidget(oldWidget);
51    //旋转角度发生变化时执行过渡动画
52    if (oldWidget.turns != widget.turns) {
53      _controller.animateTo(
54        widget.turns,
55        duration: Duration(milliseconds: widget.speed??200),
56        curve: Curves.easeOut,
57      );
58    }
59  }
60}

上面代码中:

  1. 我们是通过组合RotationTransition和child来实现的旋转效果。
  2. 在didUpdateWidget中,我们判断要旋转的角度是否发生了变化,如果变了,则执行一个过渡动画。

下面我们测试一下TurnBox的功能,测试代码如下:

1import 'package:flutter/material.dart';
2import '../widgets/index.dart';
3
4class TurnBoxRoute extends StatefulWidget {
5  
6  _TurnBoxRouteState createState() => _TurnBoxRouteState();
7}
8
9class _TurnBoxRouteState extends State<TurnBoxRoute> {
10  double _turns = .0;
11
12  
13  Widget build(BuildContext context) {
14
15    return Center(
16      child: Column(
17        children: <Widget>[
18          TurnBox(
19            turns: _turns,
20            speed: 500,
21            child: Icon(Icons.refresh, size: 50,),
22          ),
23          TurnBox(
24            turns: _turns,
25            speed: 1000,
26            child: Icon(Icons.refresh, size: 150.0,),
27          ),
28          RaisedButton(
29            child: Text("顺时针旋转1/5圈"),
30            onPressed: () {
31              setState(() {
32                _turns += .2;
33              });
34            },
35          ),
36          RaisedButton(
37            child: Text("逆时针旋转1/5圈"),
38            onPressed: () {
39              setState(() {
40                _turns -= .2;
41              });
42            },
43          )
44        ],
45      ),
46    );
47  }
48}

测试代码运行后效果如图10-2所示:

图10-2

当我们点击旋转按钮时,两个图标的旋转都会旋转1/5圈,但旋转的速度是不同的,读者可以自己运行一下示例看看效果。

实际上本示例只组合了RotationTransition一个组件,它是一个最简的组合类组件示例。另外,如果我们封装的是StatefulWidget,那么一定要注意在组件更新时是否需要同步状态。比如我们要封装一个富文本展示组件MyRichText ,它可以自动处理url链接,定义如下:

1class MyRichText extends StatefulWidget {
2  MyRichText({
3    Key key,
4    this.text, // 文本字符串
5    this.linkStyle, // url链接样式
6  }) : super(key: key);
7
8  final String text;
9  final TextStyle linkStyle;
10
11  
12  _MyRichTextState createState() => _MyRichTextState();
13}

接下来我们在_MyRichTextState中要实现的功能有两个:

  1. 解析文本字符串“text”,生成TextSpan缓存起来;
  2. 在build中返回最终的富文本样式;

_MyRichTextState 实现的代码大致如下:

1class _MyRichTextState extends State<MyRichText> {
2
3  TextSpan _textSpan;
4
5  
6  Widget build(BuildContext context) {
7    return RichText(
8      text: _textSpan,
9    );
10  }
11
12  TextSpan parseText(String text) {
13    // 耗时操作:解析文本字符串,构建出TextSpan。
14    // 省略具体实现。
15  }
16
17  
18  void initState() {
19    _textSpan = parseText(widget.text)
20    super.initState();
21  }
22}

由于解析文本字符串,构建出TextSpan是一个耗时操作,为了不在每次build的时候都解析一次,所以我们在initState中对解析的结果进行了缓存,然后再build中直接使用解析的结果_textSpan。这看起来很不错,但是上面的代码有一个严重的问题,就是父组件传入的text发生变化时(组件树结构不变),那么MyRichText显示的内容不会更新,原因就是initState只会在State创建时被调用,所以在text发生变化时,parseText没有重新执行,导致_textSpan任然是旧的解析值。要解决这个问题也很简单,我们只需添加一个didUpdateWidget回调,然后再里面重新调用parseText即可:

1
2void didUpdateWidget(MyRichText oldWidget) {
3  if (widget.text != oldWidget.text) {
4    _textSpan = parseText(widget.text);
5  }
6  super.didUpdateWidget(oldWidget);
7}

有些读者可能会觉得这个点也很简单,是的,的确很简单,之所以要在这里反复强调是因为这个点在实际开发中很容易被忽略,它虽然简单,但却很重要。总之,当我们在State中会缓存某些依赖Widget参数的数据时,一定要注意在组件更新时是否需要同步状态。