1. 前言
我们之前已经介绍过RotatedBox,它可以旋转子组件,但是它有两个缺点:一是只能将其子节点以90度的倍数旋转;二是当旋转的角度发生变化时,旋转角度更新过程没有动画。
本节我们将实现一个TurnBox组件和UrlRichText组件,
TurnBox组件它不仅可以以任意角度来旋转其子节点,而且可以在角度发生变化时执行一个动画以过渡到新状态,同时,我们可以手动指定动画速度。
UrlRichText组件 ,它可以自动处理url链接,点击链接时,会有回调。
2. TurnBox
TurnBox的完整代码如下:
class TurnBox extends StatefulWidget {
const TurnBox({
Key? key,
this.turns = 0.0,
this.speed = 200,
this.child,
}) : super(key: key);
final double turns; // 选装的圈数 一圈360度 如0.25圈即90度
final int speed; // 过渡动画执行的总时长 毫秒
final Widget? child; // 子组件
@override
State<TurnBox> createState() => _TurnBoxState();
}
class _TurnBoxState extends State<TurnBox> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
_controller = AnimationController(
vsync: this,
lowerBound: -double.infinity,
upperBound: double.infinity,
);
_controller.value = widget.turns;
super.initState();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return RotationTransition(
turns: _controller,
child: widget.child,
);
}
@override
void didUpdateWidget(covariant TurnBox oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.turns != widget.turns) {
// 执行动画 从当前值到目标值
_controller.animateTo(widget.turns,
duration: Duration(milliseconds: widget.speed),
curve: Curves.easeOut);
}
}
}
上面代码中:
- 我们是通过组合RotationTransition和child来实现的旋转效果。
- 在didUpdateWidget中,我们判断要旋转的角度是否发生了变化,如果变了,则执行一个过渡动画。
下面我们测试一下TurnBox的功能,测试代码如下:
class MSTurnBoxDemo extends StatefulWidget {
const MSTurnBoxDemo({Key? key}) : super(key: key);
@override
State<MSTurnBoxDemo> createState() => _MSTurnBoxDemoState();
}
class _MSTurnBoxDemoState extends State<MSTurnBoxDemo> {
double _turns = .0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("TurnBoxDemo")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TurnBox(
turns: _turns,
speed: 500,
child: Icon(Icons.refresh, size: 50),
),
TurnBox(
turns: _turns,
speed: 1000,
child: Icon(Icons.refresh, size: 100),
),
ElevatedButton(
onPressed: () {
_turns += 0.2;
setState(() {});
},
child: Text("顺时针旋转1/5圈"),
),
ElevatedButton(
onPressed: () {
_turns -= 0.2;
setState(() {});
},
child: Text("逆时针旋转1/5圈"),
),
],
),
),
);
}
}
当我们点击旋转按钮时,两个图标的旋转都会旋转1/5圈,但旋转的速度是不同的。
实际上本示例只组合了RotationTransition一个组件,它是一个最简的组合类组件示例。另外,如果我们封装的是StatefulWidget,那么一定要注意在组件更新时是否需要同步状态。

3. UrlRichText
我们要封装一个富文本展示组件UrlRichText ,它可以自动处理url链接,点击链接时,会有回调。
UrlRichText 定义如下:
typedef UrlClickedCallBack = void Function(String text);
class UrlRichText extends StatefulWidget {
const UrlRichText({
Key? key,
required this.text,
this.linkStyle = const TextStyle(color: Colors.blue),
this.textStyle = const TextStyle(color: Colors.black),
required this.tapCallback,
}) : super(key: key);
final String text; // 文本字符串
final TextStyle linkStyle; // url链接样式
final TextStyle textStyle; // 文本样式
final UrlClickedCallBack tapCallback; // 点击URL 回调
@override
State<UrlRichText> createState() => _UrlRichTextState();
}
接下来我们在_UrlRichTextState中要实现的功能有两个:
- 解析文本字符串“text”,生成TextSpan缓存起来;
- 在build中返回最终的富文本样式;
class _UrlRichTextState extends State<UrlRichText> {
late TextSpan _textSpan;
@override
void initState() {
_textSpan = parseText(widget.text);
super.initState();
}
@override
void didUpdateWidget(covariant UrlRichText oldWidget) {
if (oldWidget.text != widget.text) {
// 文本不一致时,重新解析文本
_textSpan = parseText(widget.text);
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return RichText(
text: _textSpan,
);
}
TextSpan parseText(String text) {
// 解析文本中的Url
List<InlineSpan> _contentList = [];
RegExp exp = new RegExp(
r'(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?');
// text = "如果www.baidu.com这是一段文本但是里面包含了连接";
Iterable<RegExpMatch> matches = exp.allMatches(text);
int index = 0;
matches.forEach((match) {
/// start 0 end 8
/// start 10 end 12
String c = text.substring(match.start, match.end);
// print('---地址-url:--$c');
if (match.start == index) {
index = match.end;
}
if (index < match.start) {
String a = text.substring(index, match.start);
// print('---地址-内容AAAA--$a');
index = match.end;
_contentList.add(
TextSpan(text: a, style: widget.textStyle),
);
}
if (RegexUtil.isURL(c)) {
_contentList.add(
TextSpan(
text: c,
style: widget.linkStyle,
recognizer: TapGestureRecognizer()
..onTap = () {
widget.tapCallback(c);
},
),
);
} else {
_contentList.add(
TextSpan(text: c, style: widget.textStyle),
);
}
});
if (index < text.length) {
String a = text.substring(index, text.length);
// print('---地址-内容BBBB--$a');
_contentList.add(
TextSpan(text: a, style: widget.textStyle),
);
}
return TextSpan(children: _contentList);
}
}
由于解析文本字符串,构建出TextSpan是一个耗时操作,为了不在每次build的时候都解析一次,所以我们在initState中对解析的结果进行了缓存,然后再build中直接使用解析的结果_textSpan。当父Widget触发重建(rebuild)时,需要在didUpdateWidget中重新解析一次。
下面测试下 UrlRichText
class MSUrlRichTextDemo extends StatelessWidget {
const MSUrlRichTextDemo({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("UrlRichTextDemo")),
body: Center(
child: UrlRichText(
text: "百度:http://www.baidu.com 百度一下",
tapCallback: (c) {
print("点击URL:$c");
},
),
),
);
}
}

点击网址,控制台会有打印
网友评论