Implementing a TurnBox Component in Flutter
We have previously introduced the RotatedBox
, which can rotate its child components, but it has two drawbacks: first, it can only rotate its child nodes in multiples of 90 degrees; second, when the rotation angle changes, the update process does not animate.
In this section, we will implement a TurnBox
component that can rotate its child nodes at any angle and execute an animation to transition to the new state when the angle changes. Additionally, we can manually specify the animation speed.
The complete code for TurnBox
is as follows:
import 'package:flutter/widgets.dart'; class TurnBox extends StatefulWidget { const TurnBox({ Key? key, this.turns = .0, // Number of turns, one full turn is 360 degrees; for example, 0.25 turns is 90 degrees this.speed = 200, // Total duration for the transition animation this.child, }) : super(key: key); final double turns; final int speed; final Widget? child; @override _TurnBoxState createState() => _TurnBoxState(); } class _TurnBoxState extends State<TurnBox> with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, lowerBound: -double.infinity, upperBound: double.infinity, ); _controller.value = widget.turns; } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return RotationTransition( turns: _controller, child: widget.child, ); } @override void didUpdateWidget(TurnBox oldWidget) { super.didUpdateWidget(oldWidget); // Execute transition animation when the rotation angle changes if (oldWidget.turns != widget.turns) { _controller.animateTo( widget.turns, duration: Duration(milliseconds: widget.speed), curve: Curves.easeOut, ); } } }
In the code above:
We achieve the rotation effect by combining
RotationTransition
andchild
.In
didUpdateWidget
, we check whether the rotation angle has changed; if it has, we execute a transition animation.
Now let’s test the functionality of TurnBox
with the following test code:
import 'package:flutter/material.dart'; import '../widgets/index.dart'; class TurnBoxRoute extends StatefulWidget { const TurnBoxRoute({Key? key}) : super(key: key); @override _TurnBoxRouteState createState() => _TurnBoxRouteState(); } class _TurnBoxRouteState extends State<TurnBoxRoute> { double _turns = .0; @override Widget build(BuildContext context) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: <Widget>[ TurnBox( turns: _turns, speed: 500, child: const Icon( Icons.refresh, size: 50, ), ), TurnBox( turns: _turns, speed: 1000, child: const Icon( Icons.refresh, size: 150.0, ), ), ElevatedButton( child: const Text("Rotate Clockwise 1/5 Turn"), onPressed: () { setState(() { _turns += .2; }); }, ), ElevatedButton( child: const Text("Rotate Counterclockwise 1/5 Turn"), onPressed: () { setState(() { _turns -= .2; }); }, ) ], ), ); } }
When we click the rotate buttons, both icons will rotate 1/5 turn, but at different speeds. Readers can run the example themselves to see the effect.
In fact, this example only combines one component, RotationTransition
, which is a very simple composite component example. Additionally, if we encapsulate a StatefulWidget
, we must pay attention to whether state needs to be synchronized during component updates. For example, if we want to encapsulate a rich text display component MyRichText
that can automatically handle URL links, it can be defined as follows:
class MyRichText extends StatefulWidget { MyRichText({ Key? key, this.text, // Text string this.linkStyle, // URL link style }) : super(key: key); final String? text; final TextStyle? linkStyle; @override _MyRichTextState createState() => _MyRichTextState(); }
Next, in _MyRichTextState
, we need to implement two functionalities:
Parse the text string “text” and generate a cached
TextSpan
.Return the final rich text style in
build
.
The implementation of _MyRichTextState
might look like this:
class _MyRichTextState extends State<MyRichText> { late TextSpan _textSpan; @override Widget build(BuildContext context) { return RichText( text: _textSpan, ); } TextSpan parseText(String text) { // Time-consuming operation: parse the text string and construct a TextSpan. // Implementation omitted. } @override void initState() { super.initState(); _textSpan = parseText(widget.text); } }
Since parsing the text string and constructing a TextSpan
is a time-consuming operation, we cache the parsed result in initState
to avoid parsing it every time in build
, and directly use the parsed result _textSpan
. While this approach seems good, there is a serious problem: when the text
passed in by the parent component changes (while the component tree structure remains the same), the content displayed by MyRichText
will not update. The reason is that initState
is only called when the State
is created, so when the text
changes, parseText
is not executed again, resulting in _textSpan
retaining the old parsed value.
To solve this problem, we can simply add a didUpdateWidget
callback and call parseText
again inside it:
@override void didUpdateWidget(MyRichText oldWidget) { if (widget.text != oldWidget.text) { _textSpan = parseText(widget.text); } super.didUpdateWidget(oldWidget); }
Some readers may think this point is simple, and indeed it is, but it is crucial. The reason for emphasizing this is that this detail can easily be overlooked during actual development. Although it is simple, it is very important. In summary, when we cache certain data that depends on widget parameters in a State
, we must pay attention to whether we need to synchronize the state when the component updates.