我们之前已经介绍过RotatedBox,它可以旋转子组件,但是它有两个缺点:一是只能将其子节点以90度的倍数旋转;二是当旋转的角度发生变化时,旋转角度更新过程没有动画。
    本节我们将实现一个TurnBox组件,它不仅可以以任意角度来旋转其子节点,而且可以在角度发生变化时执行一个动画以过渡到新状态,同时,我们可以手动指定动画速度。
    TurnBox的完整代码如下:

    1. import 'package:flutter/material.dart';
    2. import 'dart:async';
    3. void main() => runApp(new MyApp());
    4. class MyApp extends StatelessWidget {
    5. @override
    6. Widget build(BuildContext context) {
    7. return MaterialApp(
    8. debugShowCheckedModeBanner: true,
    9. title: '组合实例:TurnBox',
    10. theme: ThemeData(
    11. primarySwatch: Colors.blue,
    12. ),
    13. home: Scaffold(
    14. appBar: AppBar(title: Text("组合实例:TurnBox")),
    15. body: TurnBoxRoute(),
    16. ));
    17. }
    18. }
    19. class TurnBoxRoute extends StatefulWidget {
    20. @override
    21. _TurnBoxRouteState createState() => new _TurnBoxRouteState();
    22. }
    23. class _TurnBoxRouteState extends State<TurnBoxRoute> {
    24. double _turns = .0;
    25. double _turns2 = .0;
    26. @override
    27. void initState() {
    28. Timer.periodic(Duration(milliseconds: 1000), (timer) {
    29. setState(() {
    30. _turns2 += 1/60;
    31. });
    32. });
    33. super.initState();
    34. }
    35. @override
    36. Widget build(BuildContext context) {
    37. return Center(
    38. child: Column(
    39. children: <Widget>[
    40. TurnBox(
    41. turns: _turns,
    42. speed: 500,
    43. child: Icon(
    44. Icons.refresh,
    45. size: 50,
    46. ),
    47. ),
    48. TurnBox(
    49. turns: _turns,
    50. speed: 1000,
    51. child: Icon(
    52. Icons.refresh,
    53. size: 150.0,
    54. ),
    55. ),
    56. TurnBox(
    57. turns: _turns2,
    58. speed: 1000,
    59. child: Icon(
    60. Icons.refresh,
    61. size: 200.0,
    62. ),
    63. ),
    64. RaisedButton(
    65. child: Text("顺时针旋转1/5圈"),
    66. onPressed: () {
    67. setState(() {
    68. _turns += .2;
    69. });
    70. },
    71. ),
    72. RaisedButton(
    73. child: Text("逆时针旋转1/5圈"),
    74. onPressed: () {
    75. setState(() {
    76. _turns -= .2;
    77. });
    78. },
    79. )
    80. ],
    81. ),
    82. );
    83. }
    84. }
    85. class TurnBox extends StatefulWidget {
    86. const TurnBox(
    87. {Key key,
    88. this.turns = .0, //旋转的“圈”数,一圈为360度,如0.25圈即90度
    89. this.speed = 200, //过渡动画执行的总时长
    90. this.child})
    91. : super(key: key);
    92. final double turns;
    93. final int speed;
    94. final Widget child;
    95. @override
    96. _TurnBoxState createState() => new _TurnBoxState();
    97. }
    98. class _TurnBoxState extends State<TurnBox> with SingleTickerProviderStateMixin {
    99. AnimationController _controller;
    100. @override
    101. void initState() {
    102. super.initState();
    103. _controller = new AnimationController(
    104. vsync: this, lowerBound: -double.infinity, upperBound: double.infinity);
    105. _controller.value = widget.turns;
    106. }
    107. @override
    108. void dispose() {
    109. _controller.dispose();
    110. super.dispose();
    111. }
    112. @override
    113. Widget build(BuildContext context) {
    114. return RotationTransition(
    115. turns: _controller,
    116. child: widget.child,
    117. );
    118. }
    119. @override
    120. void didUpdateWidget(TurnBox oldWidget) {
    121. super.didUpdateWidget(oldWidget);
    122. //旋转角度发生变化时执行过渡动画
    123. if (oldWidget.turns != widget.turns) {
    124. _controller.animateTo(
    125. widget.turns,
    126. duration: Duration(milliseconds: widget.speed ?? 200),
    127. curve: Curves.easeOut,
    128. );
    129. }
    130. }
    131. }

    上面代码中:

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

    测试代码运行后效果如图所示:
    组合实例:TurnBox - 图1
    当我们点击旋转按钮时,两个图标的旋转都会旋转1/5圈,但旋转的速度是不同的,读者可以自己运行一下示例看看效果
    实际上本示例只组合了RotationTransition一个组件,它是一个最简的组合类组件示例。另外,如果我们封装的是StatefulWidget,那么一定要注意在组件更新时是否需要同步状态。

    我们要封装一个富文本展示组件MyRichText ,它可以自动处理url链接,定义如下:
    接下来我们在_MyRichTextState中要实现的功能有两个:

    • 解析文本字符串“text”,生成TextSpan缓存起来;
    • 在build中返回最终的富文本样式; ```dart import ‘package:flutter/material.dart’;

    void main() => runApp(new MyApp());

    class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: true, title: ‘MyRichText’, theme: ThemeData( primarySwatch: Colors.blue, ), home: Scaffold( appBar: AppBar(title: Text(“MyRichText”)), body: TestMyRichText(), )); } }

    class TestMyRichText extends StatelessWidget { const TestMyRichText({Key key}) : super(key: key);

    @override Widget build(BuildContext context) { return MyRichText( key: ValueKey(1), text: “我在学习Flutter”, // linkStyle: // TextStyle(color: Colors.red, decoration: TextDecoration.underline), ); } }

    class MyRichText extends StatefulWidget { MyRichText({ Key key, @required this.text, // 文本字符串 this.linkStyle, // url链接样式 }) : super(key: key); final String text; final TextStyle linkStyle; @override _MyRichTextState createState() => _MyRichTextState(); }

    class _MyRichTextState extends State { TextSpan _textSpan; @override @override void didUpdateWidget(MyRichText oldWidget) { if (widget.text != oldWidget.text) { _textSpan = parseText(widget.text); } super.didUpdateWidget(oldWidget); }

    @override void initState() { _textSpan = parseText(widget.text); super.initState(); }

    TextSpan parseText(String text) { // 耗时操作:解析文本字符串,构建出TextSpan。 // 省略具体实现。 if (widget.linkStyle != null) return TextSpan( text: text, style: widget.linkStyle, ); else return TextSpan( text: text, style: TextStyle(color: Colors.blue, decoration: TextDecoration.underline), ); }

    Widget build(BuildContext context) { return RichText( text: _textSpan, ); } }

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

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