参考资料:《Flutter实战·第二版》 10.3 组合实例:TurnBox
这里尝试实现一个更为复杂的例子,其能够旋转子组件。Flutter中的RotatedBox
可以旋转子组件,但是它有两个缺点:
- 一是只能将其子节点以90度的倍数旋转
- 二是当旋转的角度发生变化时,旋转角度更新过程没有动画
因此,这里将自定义一个TurnBox,不仅可以设置任意角度旋转的子Widget,还能再角度发生改变时执行一个过渡动画,同时,还可以手动设置动画的执行时长。
首先,组件一定是一个动画组件,需要实现SingleTickerProviderStateMixin
并设置Controler对象,这里没有定义AnimationController对象,直接在Controler内部设置起始值,默认值是[0, 1],类型为浮点数。输入参数有旋转的多少、旋转动画的时长和子Widget,其具有默认的初始值。
在组件初始化阶段,首先定义_controller
,其取值范围为
[
−
∞
,
+
∞
]
[-\infin,+\infin]
[−∞,+∞],并将其初始值设为传入参数,否则为默认值0。
组件通过RotationTransition
构建,需传入一个Animation<double>
对象并设置子Widget。
当外部传入的参数turns
或speed
变化时(turns
为主要控制变量),则执行动画到目标状态。
注意在组件销毁的dispose()
函数当中销毁_controller
防止内存泄漏的问题。
class TurnBox extends StatefulWidget {
const TurnBox({
Key? key,
this.turns = .0, //旋转的“圈”数,一圈为360度,如0.25圈即90度
this.speed = 200, //过渡动画执行的总时长
this.child
}) :super(key: key);
final double turns;
final int speed;
final Widget? child;
TurnBoxState createState() => TurnBoxState();
}
class TurnBoxState extends State<TurnBox>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
lowerBound: -double.infinity,
upperBound: double.infinity
);
_controller.value = widget.turns;
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return RotationTransition(
turns: _controller,
child: widget.child,
);
}
void didUpdateWidget(TurnBox oldWidget) {
super.didUpdateWidget(oldWidget);
//旋转角度发生变化时执行过渡动画
if (oldWidget.turns != widget.turns) {
_controller.animateTo(
widget.turns,
duration: Duration(milliseconds: widget.speed??200),
curve: Curves.easeOut,
);
}
}
}
下面可以测试一下定义好组件的功能,大小两个组件全部采用一个state控制,但是旋转速度不同,大的会慢一些:
class TurnBoxRoute extends StatefulWidget {
const TurnBoxRoute({Key? key}) : super(key: key);
TurnBoxRouteState createState() => TurnBoxRouteState();
}
class TurnBoxRouteState extends State<TurnBoxRoute> {
double _turns = .0;
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("顺时针旋转1/5圈"),
onPressed: () {
setState(() {
_turns += .2;
});
},
),
const SizedBox(height: 10,),
ElevatedButton(
child: const Text("逆时针旋转1/5圈"),
onPressed: () {
setState(() {
_turns -= .2;
});
},
)
],
),
);
}
}
这部分内容最常用到的函数就是didUpdateWidget()
,其在传入参数发生变化时调用。例如我们要实现一个解析url链接的富文本文件,那么在一开始要对传入的文本进行解析,而后才能生成对应的Widget。解析的过程与构建过程分开较为合适,可以保证UI发生变化时,所需的文本不会被反复解析,以减少不必要的耗时,因此放在initState()
中是一个不错的选择。但是,当传入的参数发生变化时(组件树结构改变),initState()
并不执行,文本内容不会更新。因此,可以将解析过程放在didUpdateWidget()
中,这样当参数变化时,能够及时对UI进行重构:
class _MyRichTextState extends State<MyRichText> {
TextSpan _textSpan;
Widget build(BuildContext context) {
return RichText(
text: _textSpan,
);
}
TextSpan parseText(String text) {
// 耗时操作:解析文本字符串,构建出TextSpan。
// 省略具体实现。
}
void initState() {
_textSpan = parseText(widget.text)
super.initState();
}
void didUpdateWidget(MyRichText oldWidget) {
if (widget.text != oldWidget.text) {
_textSpan = parseText(widget.text);
}
super.didUpdateWidget(oldWidget);
}
}
虽然这看起来是一个简单的方式,但是在实际开发过程中很容易被忽略,一定要注意传入参数是否会经常发生改变,及时更新输入状态。