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}上面代码中:
- 我们是通过组合
RotationTransition和child来实现的旋转效果。 - 在
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所示:

当我们点击旋转按钮时,两个图标的旋转都会旋转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中要实现的功能有两个:
- 解析文本字符串“text”,生成
TextSpan缓存起来; - 在
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参数的数据时,一定要注意在组件更新时是否需要同步状态。