前言:
这是我参与8月更文挑战的第 6 天,活动详情查看:8月更文挑战。为应掘金的八月更文挑战
,我准备在本月挑选 31
个以前没有介绍过的组件,进行全面分析和属性介绍。这些文章将来会作为 Flutter 组件集录
的重要素材。希望可以坚持下去,你的支持将是我最大的动力~
一、CupertinoActivityIndicator 的使用
可能看到 CupertinoActivityIndicator
组件,有人会嗤之以鼻:不就是个 iOS 风格的菊花转
吗,用起来这么简单的对象,有什么好说的啊,看来你也要水文章了。 在我心目中 CupertinoActivityIndicator
是一个 教科书
级别的组件,它融汇了非常多组件相关的知识要点
,比如动画
、绘制
、State 生命周期回调的使用
,是非常值得去学习、分析、品味的。
1. CupertinoActivityIndicator 的属性
CupertinoActivityIndicator
的使用确实非常简单,普通构造中只有两个参数:
属性名 | 类型 | 默认值 | 用途 |
---|---|---|---|
animating | bool | true | 表示是否进行动画 |
radius | double | 10 | 表示指示器半径 |
如下是 CupertinoActivityIndicator
两个属性使用的小案例,左侧半径 15
,且animating
置为 true
,所以在不停旋转,进行 loading
展示。右侧半径 20
,且animating
置为 false
,则表现为静态。
class CupertinoActivityIndicatorDemo extends StatelessWidget {
const CupertinoActivityIndicatorDemo({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 40,
children:[
CupertinoActivityIndicator(
animating: true,
radius: 15,
),
CupertinoActivityIndicator(
animating: false,
radius: 20,
),
]
);
}
}
2.CupertinoActivityIndicator 的 partiallyRevealed 构造
除了普通构造外,还有一个 partiallyRevealed 构造
,从下面的定义中可以看出,属性只有半径 radius
和进度 progress
。而且 animating
固定为 false
,表示这个构造是指定进度的 静态
效果。
下面是 progress
从 0 ~ 1
间隔 0.1
个效果:
class CupertinoActivityIndicatorDemo extends StatelessWidget {
const CupertinoActivityIndicatorDemo({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 20,
children: List.generate(
10,
(index) => CupertinoActivityIndicator.partiallyRevealed(
progress: 0.1 * index,
radius: 15,
),
).toList());
}
}
二、CupertinoActivityIndicator 源码知识点
1. CupertinoActivityIndicator 组件源码介绍
CupertinoActivityIndicator
继承自 StatefulWidget
,表示它有内部状态更新的需求。其中定义了三个成员,用于组件信息配置,这三个属性在上面的使用中也介绍了作用。作为一个 StatefulWidget
,其组件构建的逻辑将交由对应的状态类进行,这里是 _CupertinoActivityIndicatorState
。
从 _CupertinoActivityIndicatorState
的类结构中可以看出,组件的构建依赖于 SizedBox
和 CustomPaint
。并覆写了三个 State
生命周期的回调方法。
2. 动画的处理
CupertinoActivityIndicator
既然可以进行 loading
旋转,那必然需要进行动画处理。如下, _CupertinoActivityIndicatorState
混入 SingleTickerProviderStateMixin
,在 initState
中实例化 AnimationController
,这里可以看出当 widget.animating
为 true
,动画器控制器会立刻 repeat
重复执行,周期为 1s
。
在 dispose
回调中将 _controller
释放。
@override
void dispose() {
_controller.dispose();
super.dispose();
}
3. didUpdateWidget 回调
可能很多人不是很清楚这个回调的作用。当组件重建
时,状态类
是不会重新初始化
的,而是会回调 didUpdateWidget
来对比新旧两个 Widget
的配置信息进行响应逻辑处理。明面上使 组件重建
的方式非常多,比如 setState
、ValueListenableBuilder
、FutrueBuilder
等,本质上基本都是 setState
。
@override
void didUpdateWidget(CupertinoActivityIndicator oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.animating != oldWidget.animating) {
if (widget.animating)
_controller.repeat();
else
_controller.stop();
}
}
如果不处理 didUpdateWidget
会有什么后果?比如通过 Switch
来开关 CupertinoActivityIndicator
的 animating
属性, CupertinoActivityIndicator
重建时,如果没有 didUpdateWidget
处理,状态类是无法感知 widget 配置信息变化的,也就无法完成是否动画的切换。
4. 绘制的处理
在 build
方法中,使用 SizedBox
组件进行尺寸的限定,通过 _CupertinoActivityIndicatorPainter
进行绘制。
在很久以前,对于那时还只会 setState
触发画板重绘,我一直对这种方式有疑问,因为 setState
更新画板会让画板对象重新创建,这对于绘制动画来说是很不友好的,因为触发的频率非常高。直到我看懂 CupertinoActivityIndicator
的源码,才对画板重绘有了全新的认知。这也为 《Flutter 绘制指南 - 妙笔生花》扫清了最后障碍。
都是看到 CupertinoActivityIndicator
并没有使用 setState
,却可以执行动画来更新内部状态,这是让人很兴奋的。经过一点点测试发现秘密在于 super(repaint: position)
。画板可以通过一个 Listenable
对象触发重绘,而不会触发任何组件的构建。至于其更深层的实现原理,在 《Flutter 绘制探索》专栏中有详细的源码分析。
具体的绘制逻辑也很简单,就是遍历旋转绘制
圆角矩形而已。
4. CupertinoActivityIndicator 的颜色
从源码中可以看出 CupertinoActivityIndicator
的颜色是固定的,用户无法直接设置。但在 暗/亮
模式下,颜色会有差异,如下:
对于 activeColor
会根据 暗/亮
模式进行处理。如下,在暗色模式下,会略显白色。如果我们想要自己定义的组件支持 暗/亮
模式,也可以效仿一下,进行处理。
三、CupertinoActivityIndicator 的注意点
有一个注意点。比如,我通过 Wrap
包裹 CupertinoActivityIndicator
和另一个 CustomPaint
,通过 BoxPainter
画一个方块。
class CupertinoActivityIndicatorDemo extends StatelessWidget {
const CupertinoActivityIndicatorDemo({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 20,
children: [
CupertinoActivityIndicator(
animating: true,
radius: 15,
),
CustomPaint(
size: Size(50, 50),
painter: BoxPainter(),
)
],
);
}
}
class BoxPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
print('-------BoxPainter----------');
canvas.drawRect(Offset.zero & size, Paint());
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
通过日志可以发现 BoxPainter
会随着 CupertinoActivityIndicator
的动画进行重绘。也能有人会非常疑惑,明明 BoxPainter
不需要重绘,为什么会一直绘制, CupertinoActivityIndicator
太垃圾了。
这也算不上什么异常,本质就是 RepaintBoundary
机制,通过 debugDumpRenderTree()
方法查看渲染树,可以看出:这两者在同一渲染区域内,如下它们都在 up7
。在同一片渲染区域内的一个节点重绘,会连带这片区域的所有渲染节点重绘。像 Wrap
、Column
、Row
、SingleChildScrollView
、Stack
这样可以有多个子组件,对应的渲染对象会在同一层。
我们可以通过 RepaintBoundary
,将 CupertinoActivityIndicator
对应的渲染对象隔开,这样就不会影响其他节点。注意,这并不是 CupertinoActivityIndicator
自身的问题,是 RepaintBoundary
机制使然。
Wrap(
spacing: 20,
children: [
RepaintBoundary( //<----
child: CupertinoActivityIndicator(
animating: false,
radius: 15,
),
),
CustomPaint(
size: Size(50, 50),
painter: BoxPainter(),
),
],
),
CupertinoActivityIndicator 组件
的使用方式到这里就介绍完毕,虽然是个简单的小组件,但麻雀虽小五脏俱全。是很值得去研究和学习的。那本文到这里就结束了,谢谢观看,明天见~