
可能看到 CupertinoActivityIndicator 组件,有人会嗤之以鼻:不就是个 iOS 风格的菊花转 吗,用起来这么简单的对象,有什么好说的啊,看来你也要水文章了。 在我心目中 CupertinoActivityIndicator 是一个 教科书 级别的组件,它融汇了非常多组件相关的知识要点,比如动画、绘制、State 生命周期回调的使用,是非常值得去学习、分析、品味的。

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,
),
]
);
}
}除了普通构造外,还有一个 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 继承自 StatefulWidget ,表示它有内部状态更新的需求。其中定义了三个成员,用于组件信息配置,这三个属性在上面的使用中也介绍了作用。作为一个 StatefulWidget ,其组件构建的逻辑将交由对应的状态类进行,这里是 _CupertinoActivityIndicatorState 。

从 _CupertinoActivityIndicatorState 的类结构中可以看出,组件的构建依赖于 SizedBox 和 CustomPaint 。并覆写了三个 State 生命周期的回调方法。

CupertinoActivityIndicator 既然可以进行 loading 旋转,那必然需要进行动画处理。如下, _CupertinoActivityIndicatorState 混入 SingleTickerProviderStateMixin,在 initState 中实例化 AnimationController ,这里可以看出当 widget.animating 为 true ,动画器控制器会立刻 repeat 重复执行,周期为 1s 。

在 dispose 回调中将 _controller 释放。
@override
void dispose() {
_controller.dispose();
super.dispose();
}可能很多人不是很清楚这个回调的作用。当组件重建时,状态类是不会重新初始化的,而是会回调 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 配置信息变化的,也就无法完成是否动画的切换。
在 build 方法中,使用 SizedBox 组件进行尺寸的限定,通过 _CupertinoActivityIndicatorPainter 进行绘制。

在很久以前,对于那时还只会 setState 触发画板重绘,我一直对这种方式有疑问,因为 setState 更新画板会让画板对象重新创建,这对于绘制动画来说是很不友好的,因为触发的频率非常高。直到我看懂 CupertinoActivityIndicator 的源码,才对画板重绘有了全新的认知。这也为 《Flutter 绘制指南 - 妙笔生花》扫清了最后障碍。
都是看到 CupertinoActivityIndicator 并没有使用 setState ,却可以执行动画来更新内部状态,这是让人很兴奋的。经过一点点测试发现秘密在于 super(repaint: position) 。画板可以通过一个 Listenable 对象触发重绘,而不会触发任何组件的构建。至于其更深层的实现原理,在 《Flutter 绘制探索》专栏中有详细的源码分析。

具体的绘制逻辑也很简单,就是遍历旋转绘制圆角矩形而已。

从源码中可以看出 CupertinoActivityIndicator 的颜色是固定的,用户无法直接设置。但在 暗/亮 模式下,颜色会有差异,如下:

对于 activeColor 会根据 暗/亮 模式进行处理。如下,在暗色模式下,会略显白色。如果我们想要自己定义的组件支持 暗/亮 模式,也可以效仿一下,进行处理。


有一个注意点。比如,我通过 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 组件的使用方式到这里就介绍完毕,虽然是个简单的小组件,但麻雀虽小五脏俱全。是很值得去研究和学习的。那本文到这里就结束了,谢谢观看,明天见~