鸿蒙 ArkUI 自定义布局深度实践:用 onMeasureSize + onPlaceChildren 构建环形流程指示器




鸿蒙 ArkUI 自定义布局深度实践:用 onMeasureSize + onPlaceChildren 构建环形流程指示器
适用版本:HarmonyOS API 24+(兼容 SDK 6.1.1(24))
技术栈:ArkTS / ArkUI 声明式 UI 框架
源码位置:entry/src/main/ets/pages/Index.ets
构建验证:targetSdkVersion 26.0.0,兼容 API 24
一、引言
在鸿蒙应用开发中,布局是 UI 构建的基石。ArkUI 提供了丰富的内置布局容器——Column、Row、Stack、Flex、Grid、RelativeContainer 等——它们覆盖了绝大多数常见的 UI 排列需求。然而,当产品设计提出"将多个步骤节点均匀排布在一个圆周上,形成环形流程"这样的非标准布局时,内置容器就力不从心了。
这种环形布局广泛存在于项目管理看板、制造执行系统(MES)、CI/CD 流水线、OTA 升级进度、多阶段审批流等场景中。它的视觉优势显而易见:
- 紧凑性:相同步数下,环形比线性排列节省约 40% 的屏幕宽度;
- 对称美:圆周对称天然符合人眼的视觉重心感知;
- 可扩展性:添加或减少步骤不会破坏整体布局结构,只需重新计算角度。
本文将以一个完整的环形流程指示器应用为载体,深入讲解鸿蒙 ArkUI 自定义布局的底层 API——onMeasureSize 和 onPlaceChildren——从数学原理到代码实现,从浅层使用到深度优化,帮助读者真正掌握自定义布局的能力。
二、理解鸿蒙自定义布局 API
2.1 API 演进脉络
HarmonyOS ArkUI 的自定义布局能力经历了三个阶段,理解这个演进有助于我们正确选择技术方案。
| API 版本 | 接口 | 状态 | 特征 |
|---|---|---|---|
| API 9 | onLayout + onMeasure |
已废弃(deprecated) | 参数为 Array<LayoutChild>,需手动遍历每个子节点做 child.measure() 和 child.layout() |
| API 10 | onPlaceChildren + onMeasureSize |
稳定(stable) | 参数分离为 Array<Measurable>(测量)和 Array<Layoutable>(布局),类型更安全 |
| API 12+ | 新增 fill 等便利方法 |
拓展 | 对 Circle、Shape 等图形组件增加直接填充 API(需 apiAvailable 保护) |
本文采用 API 10 引入的稳定接口,同时兼容 API 24(对应 HarmonyOS 4.x / API 10),这正是 compatibleSdkVersion: "6.1.1(24)" 所保证的兼容范围。
2.2 核心接口详解
onMeasureSize
onMeasureSize(
selfLayoutInfo: GeometryInfo,
children: Array<Measurable>,
constraint: ConstraintSizeOptions
): SizeResult
selfLayoutInfo:父框架传递给组件的自身布局信息,包含已由父容器确定的尺寸范围。children:所有子组件的可测量接口数组。每一个Measurable对象暴露一个measure(constraint)方法,用于给子组件施加尺寸约束。constraint:父容器下发的尺寸约束(minWidth、maxWidth、minHeight、maxHeight)。- 返回值
SizeResult:当前组件自己想要的尺寸。这是一个重要设计点——自定义布局组件可以自主决定自身尺寸,而不完全受父容器约束。
生命周期语义:ArkUI 框架会在布局阶段自上而下遍历组件树。父组件测量自己时调用 onMeasureSize,在此方法中,父组件需要逐一调用 children[i].measure() 来通知子组件进行递归度量。所有子组件都测量完毕后,父组件返回自己的期望尺寸,该尺寸会向上传递给更上层的父容器。
onPlaceChildren
onPlaceChildren(
selfLayoutInfo: GeometryInfo,
children: Array<Layoutable>,
constraint: ConstraintSizeOptions
): void
selfLayoutInfo:此时已确定自身尺寸的布局信息。children:子组件的可布局接口数组。每个Layoutable对象暴露layout(position: Position)方法,用于指定子组件在父组件坐标系中的放置位置。constraint:父容器约束(与onMeasureSize接收的相同)。
生命周期语义:度量阶段完成后,ArkUI 框架自下而上执行布局。子组件先完成自己的内部布局,然后父组件在 onPlaceChildren 中决定每个子组件的位置。这里的关键是 selfLayoutInfo.width 和 selfLayoutInfo.height 已经是 onMeasureSize 返回的最终尺寸,因此可以作为定位参考。
2.3 与 @BuilderParam 的协同
自定义布局组件必须配合 @BuilderParam 使用。架构模式如下:
@Component
struct CustomContainer {
@Builder
doNothingBuilder() {} // 占位 builder
@BuilderParam content: () => void = this.doNothingBuilder;
onMeasureSize(...) { ... }
onPlaceChildren(...) { ... }
build() {
Stack() { this.content() } // 必须将 builder 包在容器中
}
}
使用时,通过尾部闭包(trailing closure)传入子组件:
CustomContainer({ ... }) {
ChildComponent()
ChildComponent()
}
ArkUI 框架会将尾部闭包内的组件收集为 children 数组,传递给 onMeasureSize 和 onPlaceChildren。这是一个声明式框架中非常优雅的设计——子组件的构造在声明式语法中完成,而布局则由开发者通过接口精确控制。
三、项目架构总览
3.1 组件层次结构
Index(@Entry 主页面)
├── buildTitle() —— 顶部标题区
├── Stack(环形区域,clip 裁剪)
│ ├── Canvas —— 底层:连接线 + 箭头 + 中心进度(Canvas 绘制)
│ └── CircularFlowLayout —— 上层:自定义环形布局
│ ├── StepNode(0) —— "需求分析"
│ ├── StepNode(1) —— "系统设计"
│ ├── StepNode(2) —— "编码开发"
│ ├── StepNode(3) —— "测试验证"
│ ├── StepNode(4) —— "部署发布"
│ └── StepNode(5) —— "运维监控"
├── buildStepDetail() —— 步骤详情卡片
├── buildNavigationButtons() —— 上一步 / 下一步 按钮
└── buildResetButton() —— 重置流程
3.2 数据流设计
steps[] (静态数据)
│
▼
@State currentStepIndex (响应式状态)
│
├── @Watch('onStepChanged') ──▶ Canvas 重绘
├── getStepStatus(i) ──▶ 动态计算各节点状态
│ ├── i < index → 'completed'
│ ├── i == index → 'active'
│ └── i > index → 'pending'
└── UI 绑定 ──▶ StepNode 属性 / 详情面板 / 按钮 enabled
这种"索引驱动"的设计避免了深层对象观测的复杂性。steps 数组作为纯数据容器不携带状态,currentStepIndex 作为单一响应式数据源,所有 UI 状态都是它的投射。这不仅代码更简洁,而且性能更好——改变 currentStepIndex 是一次单一的状态变更,ArkUI 框架只需一次重渲染。
四、CircularFlowLayout:核心自定义布局实现
4.1 组件声明与参数
@Component
struct CircularFlowLayout {
stepCount: number = 6; // 步骤总数
orbitRadius: number = 100; // 轨道半径
childSize: number = 72; // 每个子节点的期望宽度
@Builder
doNothingBuilder() {};
@BuilderParam content: () => void = this.doNothingBuilder;
// ...
}
三个公开属性——stepCount、orbitRadius、childSize——构成了布局的可配参数接口。使用者可以从外部传入,实现不同规模(4 步到 12 步)和不同视觉风格(紧凑或宽松)的环形布局。
4.2 测量阶段(onMeasureSize)
onMeasureSize(
selfLayoutInfo: GeometryInfo,
children: Array<Measurable>,
constraint: ConstraintSizeOptions
): SizeResult {
for (let i = 0; i < children.length; i++) {
children[i].measure({
minWidth: this.childSize,
maxWidth: this.childSize,
minHeight: 92,
maxHeight: 92
});
}
return {
width: this.orbitRadius * 2 + 80,
height: this.orbitRadius * 2 + 80
};
}
这里有两个关键设计决策:
(一)固定尺寸约束:对每个子组件使用固定的 minWidth = maxWidth = childSize、minHeight = maxHeight = 92。这意味着所有步骤节点将被强制统一尺寸。在环形布局中这是必要的——如果节点尺寸不一致,圆周上的间距就会不均匀,视觉上会显得歪斜。
(二)自描述尺寸:组件通过 orbitRadius * 2 + 80 计算自己的尺寸。+80 是留出的内边距余量,确保最外侧的节点标签不会跑到组件边界之外。这个尺寸与使用处的 .width('100%').height(380) 父容器尺寸无关——父容器会自适应裁剪。
4.3 布局阶段(onPlaceChildren)
这是环形布局的数学核心:
onPlaceChildren(
selfLayoutInfo: GeometryInfo,
children: Array<Layoutable>,
constraint: ConstraintSizeOptions
): void {
const count = children.length;
const cx = selfLayoutInfo.width / 2; // 圆心 X
const cy = selfLayoutInfo.height / 2; // 圆心 Y
const radius = this.orbitRadius;
const startAngle = -Math.PI / 2; // 从正上方(12点钟方向)开始
for (let i = 0; i < count; i++) {
const angle = startAngle + (i * 2 * Math.PI) / count;
const nodeX = cx + radius * Math.cos(angle) - this.childSize / 2;
const nodeY = cy + radius * Math.sin(angle) - 46;
children[i].layout({ x: Math.round(nodeX), y: Math.round(nodeY) });
}
}
为什么从 -π/2 开始? 标准数学中,角度 0 对应正右方(3 点钟方向)。但在 UI 场景中,用户通常期望第一个节点在正上方(12 点钟方向)。将起始角度偏移 -π/2(即 -90°),就把角度 0 映射到了正上方。
为什么减去半宽半高? 子组件的 layout() 方法使用的是左上角坐标,而非中心点坐标。因此计算出的圆周中心位置 (cx + r*cosθ, cy + r*sinθ) 需要减去子组件自身尺寸的一半。
为什么使用 Math.round()? 子像素定位可能导致渲染模糊。四舍五入到整像素可以确保锐利渲染。
4.4 组件构建
build() {
Stack() { this.content() }
.width(this.orbitRadius * 2 + 80)
.height(this.orbitRadius * 2 + 80)
}
这里有一个重要细节:build() 方法将 @BuilderParam 的尾部闭包内容包裹在 Stack 中。这是因为 ArkUI 要求 build() 方法必须有且仅有一个根容器组件。直接写 this.content() 是不允许的,因为 content() 的返回值是 void——它只是将尾部闭包内的组件挂载到子节点列表,并不是一个可渲染的顶层组件。
五、StepNode:步骤节点封装
5.1 三态视觉设计
┌──────────────────────────────────────────────┐
│ 状态 │ 圆形填充 │ 边框 │ 内部文字 │
├──────────┼───────────┼────────┼──────────┤
│ completed │ #52C41A绿 │ #389E0D │ ✓ │
│ active │ #1890FF蓝 │ #096DD9 │ 数字 │
│ pending │ #F5F5F5灰 │ #BFBFBF │ 数字 │
└──────────┴───────────┴────────┴──────────┘
每个状态对应三种视觉属性,通过三个辅助方法(getCircleColor、getStrokeColor、getLabelColor)映射:
getCircleColor(): ResourceColor {
switch (this.status) {
case 'completed': return '#52C41A'; // 绿色实心
case 'active': return '#1890FF'; // 蓝色实心
default: return '#F5F5F5'; // 浅灰空心
}
}
5.2 激活态呼吸光晕
if (this.status === 'active') {
Circle()
.width(60).height(60)
.fill('#FFFFFF')
.stroke('#1890FF')
.strokeWidth(3)
.opacity(0.3)
}
当前激活的步骤外围有一个 60px 的半透明圆环,视觉上类似"呼吸光晕"效果。fill('#FFFFFF') 与 stroke('#1890FF') 结合,配合 opacity(0.3),在白色背景下呈现柔和的蓝色光晕。这里注意:.fill() 和 .stroke() 是 API 12 新增的便利方法,用于替代旧版中 shape 对象的复杂构造。由于本文目标兼容 API 24,且 fill() 在 API 12 引入并不冲突(target 26 高于 12),所以可以安全使用。如果需严格兼容更低版本,可改用 Shape + Path 绘制。
5.3 点击交互
.onClick(() => { this.stepClick?.(); })
这里使用 stepClick?: () => void 而非 onClick 作为属性名。原因在于:ArkUI 的 @Component 基类已经预定义了 onClick 属性作为通用的点击事件处理器,如果我们再声明一个同名的属性会导致类型冲突——编译器报错 Property 'onClick' in type 'StepNode' is not assignable to the same property in base type。这是 ArkTS 严格类型系统的表现,也是和其他 TS 框架的一个区别点。解决方案就是使用非冲突名称,如 stepClick。
六、Canvas 背景层:连接线与中心进度
自定义布局只负责放置节点,连接节点之间的线条以及圆心处的进度信息仍然需要 Canvas 来绘制。这体现了 ArkUI 中两种不同的技术路线:
- 组件化:使用
@Component+build()声明式构建 UI,适合可交互、可复用的 UI 单元; - 命令式:使用
Canvas+CanvasRenderingContext2D过程式绘制,适合几何图形、自定义视觉元素。
在环形流程指示器中,两者各司其职,通过 Stack 层叠组合。
6.1 连线绘制策略
for (let i = 0; i < stepCount; i++) {
const p = positions[i];
const q = positions[(i + 1) % stepCount];
const s = this.getStepStatus(i);
const sn = this.getStepStatus((i + 1) % stepCount);
const isActive = s === 'completed' || (s === 'active' && sn === 'pending');
// 根据 isActive 决定颜色和线宽
}
连线的状态规则:
| 前置步骤状态 | 后置步骤状态 | 连线效果 |
|---|---|---|
| completed | 任意 | 绿色粗线 + 箭头 |
| active | pending | 绿色粗线 + 箭头 |
| active | completed | 灰色细线(理论上不会出现,因为 active 到 completed 时 active 已经被前移) |
| pending | 任意 | 灰色细线 |
这个规则确保了"已完成路径"是连续的——从第一个已完成步骤连接到当前进行中的步骤,再连接到下一个待开始步骤的箭头被高亮,形成一条可视的"进度路径"。
6.2 方向箭头绘制
drawArrow(ctx, fx, fy, tx, ty): void {
const mx = (fx + tx) / 2; // 中点
const angle = Math.atan2(ty - fy, tx - fx);
const sz = 9; // 箭头大小
const a = Math.PI / 7; // 箭张角(约 25.7°)
// 实心三角形箭头
ctx.beginPath();
ctx.moveTo(mx, my);
ctx.lineTo(mx - sz * cos(angle - a), my - sz * sin(angle - a));
ctx.lineTo(mx - sz * cos(angle + a), my - sz * sin(angle + a));
ctx.closePath();
ctx.fillStyle = '#52C41A';
ctx.fill();
// 中心白点装饰
ctx.arc(mx, my, 2.5, 0, π * 2);
ctx.fillStyle = '#FFFFFF';
ctx.fill();
}
箭头绘制在每段连线的中点,指向下一节点。箭张角 π/7 产生约 25.7° 的尖锐角度,视觉上干净利落。中心点的小白圆起到"针尖"装饰作用,让箭头看起来更精致。
6.3 中心进度圆
圆心处的白色圆环展示整体进度百分比:
const completedCount = /* 遍历统计 */;
const pct = Math.round((completedCount / stepCount) * 100);
// 白色填充圆
ctx.arc(cx, cy, 50, 0, π * 2);
ctx.fillStyle = '#FFFFFF';
ctx.fill();
// 文字
ctx.fillStyle = '#8C8C8C'; ctx.fillText('流程进度', cx, cy - 16);
ctx.fillStyle = '#1A1A2E'; ctx.fillText(pct + '%', cx, cy + 16);
百分比的计算完全依赖于 getStepStatus() > 'completed' 计数,无需额外的状态跟踪,和整个数据流设计保持了一致。
七、状态管理与自动重绘
7.1 @State + @Watch 模式
@State @Watch('onStepChanged') currentStepIndex: number = 0;
onStepChanged(): void {
this.drawConnectingLines();
}
这是 ArkUI 中一种简洁的副作用模式:
@State确保currentStepIndex变更时触发组件重渲染;@Watch('onStepChanged')在变更后立即调用回调函数;- 回调中调用 Canvas 重绘,确保连线颜色、进度百分比与状态同步。
值得注意的是:@Watch 回调的执行时机是在状态更新之后、UI 重渲染之前。因此当 onStepChanged 调用 drawConnectingLines 时,this.getStepStatus(i) 已经基于新索引返回正确结果。
7.2 Canvas .onReady() 初始化
Canvas(this.canvasContext)
.onReady(() => {
this.drawConnectingLines();
})
onReady 回调在 Canvas 组件首次布局完成后触发一次,用于初始绘制。这一步是必须的,因为 Canvas 在挂载时不会自动绘制内容——绘图完全是命令式的,必须显式调用绘制函数。
7.3 深拷贝与浅观测
项目早期的代码尝试将状态直接存储在 steps 数组中:
interface StepItem {
title: string;
status: 'completed' | 'active' | 'pending';
}
@State steps: StepItem[] = [ ... ];
但这引入了深层观测问题:修改 steps[i].status 时,@State 无法检测到对象内部属性的变更,因为 @State 只做浅层引用比较。必须手动创建新的数组或使用 @Observed + @ObjectLink。
最终的解决方案是将状态外提为 currentStepIndex,让 status 成为计算属性。这不仅解决了观测问题,还带来了两个额外好处:
- 可预测性:给定索引,所有节点的状态是确定性的,不会出现状态不一致;
- 可测试性:测试只需验证
getStepStatus(2) === 'completed'这样的纯函数。
八、交互与导航
8.1 节点点击(Canvas 命中检测)
虽然 CircularFlowLayout 中的每个 StepNode 都有自己的 .onClick 处理器,但 Canvas 背景层也需要响应用户点击——用户可能点击到 Canvas 区域的空白处或连线附近,也应精确命中最近的节点。
handleCanvasClick(clickX: number, clickY: number): void {
// 重新计算所有节点的圆周位置
for (let i = 0; i < stepCount; i++) {
const angle = -π/2 + (i * 2π / stepCount);
const nx = cx + radius * cos(angle);
const ny = cy + radius * sin(angle);
const dist = sqrt((clickX - nx)² + (clickY - ny)²);
if (dist <= 30) { // 命中半径阈值
this.navigateToStep(i);
break;
}
}
}
这里需要注意:Canvas 的点击坐标 (event.x, event.y) 是相对于 Canvas 组件自身的坐标系。因为 Stack 中的 Canvas 和 CircularFlowLayout 是相同尺寸(380x380),所以 Canvas 上计算的位置与 onPlaceChildren 中计算的位置是精确对齐的。
8.2 导航按钮
上一步 和 下一步 按钮各有边界禁用逻辑:
.enabled(this.currentStepIndex > 0) // 上一步:索引 > 0 才可点击
.enabled(this.currentStepIndex < this.steps.length - 1) // 下一步:未到最后才可点击
同时,禁用态的按钮背景色变为 #BFBFBF(灰色),与 #1890FF(蓝色)的可用态形成明确对比,符合无障碍设计规范(颜色对比度 > 4.5:1)。
8.3 重置流程
resetFlow(): void {
this.currentStepIndex = 0;
}
简化为一行——重置索引到 0。由于 @Watch 机制,Canvas 自动重绘,详情面板自动刷新,按钮状态自动更新。整个 UI 回到初始态。
九、API 24 兼容性保障
9.1 Fill 方法版本保护
编译过程中出现过的警告:
The 'fill' API is supported since SDK version 26.0.0.
However, the current compatible SDK version is 6.1.1(24).
Circle 组件的 .fill() 和 .stroke() 方法是 API 12(对应 SDK 26)新增的便利 API。在 API 24 上运行时,这些方法在运行时不会报错——ArkUI 的 API 前瞻兼容策略是:新 API 被调用时,在旧版本上会被静默忽略。但为了更严谨的兼容性,可以使用 canIUse 或 apiAvailable 接口做运行时防护:
if (canIUse('SystemCapability.ArkUI.ArkUI.Fill')) {
// 使用 .fill()
} else {
// 回落方案:使用 Shape + Path
}
在本文的用例中,fill('#FFFFFF') 只是为激活态的光晕附加白色填充,即使被忽略也不会影响核心功能(用户只是看不到白色光晕层,蓝色描边光晕仍然显示)。因此我们选择保持代码简洁,不对该警告做特殊处理。
9.2 onMeasureSize / onPlaceChildren 版本要求
这两个 API 从 API 10 开始稳定。API 24 完全包含 API 10 的全部功能,因此不存在兼容性问题。这与旧的 onLayout / onMeasure(API 9 引入,API 10 废弃)不同——如果使用了已废弃的 API,编译器会给出 deprecated 警告。
9.3 布局约束策略
较低的兼容 API 版本对布局约束的传递机制略有不同。在 API 24 上,constraint 参数可能传递更宽松的约束(最大尺寸更大),因此自定义布局组件应始终基于 selfLayoutInfo 而非 constraint 来计算自身尺寸。本文的实现正是如此——onMeasureSize 返回的尺寸完全基于 orbitRadius 计算,不依赖 constraint 的具体值。
十、性能优化分析
10.1 布局计算复杂度
CircularFlowLayout.onPlaceChildren:
- 时间复杂度:O(n),n = 步骤数
- 每次操作:2 次三角函数 + 4 次乘除法 + 1 次 layout() 调用
- 6 步骤时:6 × (2 × sin/cos + 4 × 乘除) ≈ 36 次基本运算
对于典型的 4~12 个步骤,这个计算量在微秒级别,对 60fps 的布局管道没有任何压力。
10.2 Canvas 重绘频率
- 初始绘制:1 次(
onReady) - 每次导航:1 次(
@Watch触发) - 重置:1 次
在典型使用场景中(用户完成 6 个步骤),Canvas 最多重绘 7 次。Canvas 绘制的内容复杂度也很低——6 条线段 + 6 个箭头 + 1 个圆形 + 3 行文字——总绘制时间远低于 16ms(60fps 预算)。
10.3 避免不必要的重渲染
因为 currentStepIndex 是唯一的 @State 变量,任何 UI 更新都只发生一次状态变更 → 一次重渲染管道。对比在数组中存储多个状态的做法(每个步骤一个状态),这种方法将重渲染次数从 O(n) 降低到 O(1)。
十一 完整代码导读
为了方便读者对照学习,这里将完整的 Index.ets 分解为逻辑块进行导读:
11.1 类型定义(1-15行)
interface StepInfo {
title: string;
desc: string;
}
StepInfo 仅包含静态展示数据,不包含状态字段。状态由索引派生。
11.2 StepNode 组件(17-106行)
包含一个 64x64 的 Stack(圆形指示器)和一个 Text(标签文字)。圆形指示器内使用条件渲染区分三态。激活态附加 60px 半透明圆环作为光晕。
11.3 CircularFlowLayout(108-174行)
核心自定义布局组件,实现了 onMeasureSize 和 onPlaceChildren。内部维护 stepCount、orbitRadius、childSize 三个配置参数。build() 中通过 Stack 包裹 @BuilderParam。
11.4 Index 主页面(176-553行)
包含:
- 状态声明(190行):
@State @Watch('onStepChanged') currentStepIndex - Canvas 初始化(193-194行):
RenderingContextSettings+CanvasRenderingContext2D - 布局编排(205-217行):
Column作为根容器,内嵌五个@Builder区域 - 状态计算(220-224行):
getStepStatus(i)三值逻辑 - Canvas 绘制(395-506行):连线、箭头、中心进度
- 点击检测(509-526行):圆周命中计算
- 导航控制(529-536行):
navigateToStep和resetFlow - 辅助方法(539-552行):颜色和标签映射
十二、扩展与变体
12.1 改变步骤数量
只需在 buildCircularArea 中调整 orbitRadius 和 stepCount:
4 步骤 → orbitRadius = 100
6 步骤 → orbitRadius = 120
8 步骤 → orbitRadius = 140
CircularFlowLayout 会自动重新计算角度分布。
12.2 增加动画过渡
ArkUI 的 animateTo 可以轻松为状态切换添加动画:
navigateToStep(targetIndex: number): void {
animateTo({ duration: 300, curve: Curve.EaseInOut }, () => {
this.currentStepIndex = targetIndex;
});
}
然后结合 @Animatable 装饰器让 Canvas 绘制参数(如连线颜色 alpha)渐进变化。这超出了本文范围,但值得作为后续优化方向。
12.3 支持拖拽重排
在 CircularFlowLayout.onPlaceChildren 中,子组件的位置由 children[i] 的索引决定。如果引入拖拽事件并交换子节点在 children 数组中的顺序,即可实现"拖拽重排步骤"的交互。
十三、总结
本文从零开始构建了一个基于鸿蒙 ArkUI 自定义布局的环形流程指示器应用。核心收获包括:
-
自定义布局的正确 API:在 API 24 及以上版本中,使用
onMeasureSize+onPlaceChildren而非已废弃的onLayout+onMeasure。类名也不是 Flutter 中惯用的LayoutDelegate或CustomMultiChildLayout——这些在 HarmonyOS ArkUI 中不存在。 -
数学与 UI 的映射:环形布局的本质是将一维线性索引映射到二维圆周坐标。
-π/2起始角、2π/n角间距、减去半宽半高居中,这四个要素构成了核心定位算法。 -
组件化与命令式绘制的协同:
@Component负责可交互的节点,Canvas负责纯视觉的连接线和装饰元素,两者在Stack中分层叠加。 -
状态管理的最小化原则:单一
@State变量的变更自动驱动整个 UI 树的重渲染和 Canvas 重绘,避免了深层对象观测的复杂性。 -
API 版本兼容意识:在
compatibleSdkVersion约束下,对新 API(如.fill())的使用需要权衡——在核心功能不受影响的前提下,可以容忍轻微的兼容警告。
参考文献
更多推荐

所有评论(0)