HarmonyOS ArkUI 自定义 3×3 行列不等高布局实战 —— 从 Flutter 到 ArkUI 的范式迁移
HarmonyOS ArkUI 自定义 3×3 行列不等高布局实战 —— 从 Flutter 到 ArkUI 的范式迁移


目录
- 问题背景
- Flutter 方案回顾
- ArkUI 等价方案全景
- 方案一:Grid + 模板字符串
- 方案二:自定义 Layout 算法 + Stack 定位
- API 24 新特性与注意事项
- 完整代码逐段解析
- 性能分析与选型建议
- 从 Flutter 迁移到 ArkUI 的思维模型
- 总结
1. 问题背景
在移动端和跨平台 UI 开发中,「网格布局」是最常见也最核心的布局模式之一。标准的网格通常等宽等高,例如九宫格、照片墙等。但在实际业务场景中,我们往往需要不等高、不等宽的异形网格来承载不同权重的内容。
典型场景包括:
- 资讯首页:头条新闻占据 2 倍高度,次要新闻占 1 倍
- 商品展示:爆款商品占据更大面积
- 仪表盘:核心指标卡片尺寸突出
- 瀑布流导航:不同功能入口的重要性通过面积区分
本文将以此为切入点,深入剖析在 HarmonyOS ArkUI(API 24) 框架下,如何用两种不同的技术路线实现「3×3 行列不等高布局」,并与 Flutter 的 CustomMultiChildLayout + LayoutDelegate 方案进行对比,帮助跨平台开发者快速建立 ArkUI 布局思维模型。
2. Flutter 方案回顾
2.1 CustomMultiChildLayout 的核心机制
在 Flutter 中,CustomMultiChildLayout 是一个强大的自定义多子布局组件,它配合 LayoutDelegate 抽象类工作:
核心流程是三层:
- 父容器 将自己的尺寸传递给
LayoutDelegate performLayout中开发者手动测量每个子节点,计算其偏移- 每个子节点 被放置在计算出的位置上
2.2 3×3 不等高布局在 Flutter 中的实现
Flutter 中实现不等高网格有多种方式:
| StaggeredGridView (三方库) | 瀑布流 | 中 |
2.3 Flutter 方案的局限
性能开销高、复杂度高、需手动触发更新。
3. ArkUI 等价方案全景
3.1 核心对应关系
在 ArkUI(API 24)中,不存在与 CustomMultiChildLayout 完全对应的组件,但我们可以通过以下机制组合出等价能力:
| LayoutDelegate | 自定义 recalc() 方法 | 纯逻辑,无限制 |
| Size getSize(BoxConstraints) | onAreaChange 回调 | API 12+ |
| positionChild(id, offset) | .position({ x, y }) | 原生支持 |
| layoutChild(id, constraints) | .width(w).height(h) | 原生支持 |
| 响应式更新驱动 | @State 装饰器 + ForEach | 原生支持 |
3.2 两种实现路线
| 路线 | 核心组件 | 适用场景 | 代码量 |
|---|---|---|---|
路线 A:Grid + 模板字符串 |
Grid, rowsTemplate, columnsTemplate |
行列比例固定且孩子自动填充 | 约 30 行 |
路线 B:Stack + 自定义位置算法 |
Stack, @State + recalc() |
任意自定义排列或需要精确控制 | 约 80 行 |
3.3 API 24 带来的新能力
API 24(对应 HarmonyOS 6.1)在布局领域引入或强化了以下能力:
Grid组件rowsTemplate/columnsTemplate增强@Builder支持带参数@State数组重新赋值触发刷新ForEach键值生成器onAreaChange回调监听尺寸变化
这些能力共同构成了自定义布局的基础设施。
4. 方案一:Grid + 模板字符串
4.1 核心原理
Grid 组件的 rowsTemplate 和 columnsTemplate 属性接受以空格分隔的模板字符串,每个段定义一个轨道的尺寸。支持多种单位:
对于 3×3 不等高布局,我们的行高比为 1:2:1.5,列宽比为 1:2:1,模板字符串表示为:
.rowsTemplate('1fr 2fr 1.5fr')
.columnsTemplate('1fr 2fr 1fr')
4.2 示例代码
Grid() {
ForEach([0, 1, 2], (row: number) => {
ForEach([0, 1, 2], (col: number) => {
GridItem() {
CellItem({
cellColor: row === 0 ? '#FF7043' :
row === 1 ? '#42A5F5' :
'#66BB6A',
cellText: `(${row},${col})`
})
}
}, (col: number) => `col-${col}`)
}, (row: number) => `row-${row}`)
}
.columnsTemplate('1fr 2fr 1fr')
.rowsTemplate('1fr 2fr 1.5fr')
.columnsGap(6)
.rowsGap(6)
.width('100%')
.height(400)
4.3 布局效果示意
┌──────┬──────────┬──────┐ 行高比 1
│(0,0) │ (0,1) │(0,2) │ ───
├──────┼──────────┼──────┤ 行高比 2
│(1,0) │ (1,1) │(1,2) │
│ │ 最大最宽 │ │
├──────┼──────────┼──────┤ 行高比 1.5
│(2,0) │ (2,1) │(2,2) │ ───
└──────┴──────────┴──────┘
1fr 2fr 1fr
4.4 使用要点
fr(fraction)是比例单位。当容器可用尺寸为 S,总 fr 值为 T 时,每个 nfr 的轨道实际尺寸为 (n / T) × S。间隙会先从可用尺寸中扣除再分配。
GridItem 按声明顺序从左到右、从上到下自动填充。columnsGap / rowsGap 分别控制列/行间距。
4.5 优缺点
优点:
- 代码量极少,声明式表达
Grid内部自动处理滚动、复用(配合cachedCount)- 无需手动管理位置状态
- 语义清晰,易维护
缺点:
- 所有
GridItem必须有相同的父级容器(不能跨行跨列嵌套) - 行列比例在模板字符串中固定,无法运行时动态调整
- 无法实现非矩形的异形排列
5. 方案二:自定义 Layout 算法 + Stack 定位
5.1 核心思想
方案二的核心是手动模拟 Flutter 中 LayoutDelegate.performLayout() 的流程:
- 定义因子数组(
rowFactors/colFactors)描述每行每列的比例 - 通过
onAreaChange监听容器尺寸变化 - 在
recalc()方法中计算每个 cell 的{ x, y, width, height } - 将结果存入
@State数组,驱动Stack中的子组件重新定位
这套流程与 Flutter 的对应关系如下:
| Flutter | ArkUI 实现 |
|---|---|
LayoutDelegate.getSize() |
容器尺寸来自 onAreaChange 回调的 Area 参数 |
LayoutDelegate.performLayout() |
recalc(w, h) 方法 |
layoutChild(id, constraints) |
.width(cell.w).height(cell.h) |
positionChild(id, offset) |
.position({ x: cell.x, y: cell.y }) |
孩子由 LayoutId 标记 |
孩子通过 ForEach 遍历 @State cells 数组生成 |
setState() 触发重绘 |
@State cells 重新赋值自动触发 |
5.2 核心数据结构
interface CellData {
x: number; // 相对于容器的 X 偏移
y: number; // 相对于容器的 Y 偏移
w: number; // cell 宽度
h: number; // cell 高度
row: number; // 所在行索引
col: number; // 所在列索引
}
5.3 recalc() 算法实现
recalc(w: number, h: number): void {
if (w <= 0 || h <= 0) return;
const totalRowF = this.rowFactors.reduce((a, b) => a + b, 0);
const totalColF = this.colFactors.reduce((a, b) => a + b, 0);
const rowHs = this.rowFactors.map(
f => (f / totalRowF) * (h - 2 * this.gap)
);
const colWs = this.colFactors.map(
f => (f / totalColF) * (w - 2 * this.gap)
);
const newCells: CellData[] = [];
let yOff = 0;
for (let r = 0; r < 3; r++) {
let xOff = 0;
for (let c = 0; c < 3; c++) {
newCells.push({
x: xOff, y: yOff,
w: colWs[c], h: rowHs[r],
row: r, col: c
});
xOff += colWs[c] + this.gap;
}
yOff += rowHs[r] + this.gap;
}
this.cells = newCells; // @State 赋值 → UI 刷新
}
5.5 @Builder 构建可定位的子组件
@Builder
buildCell(cell: CellData) {
Text(`(${cell.row},${cell.col})`)
.textAlign(TextAlign.Center)
.fontSize(16)
.fontColor(Color.White)
.backgroundColor(
cell.row === 0 ? '#FF7043' :
cell.row === 1 ? '#42A5F5' :
'#66BB6A')
.border({ width: 2, color: Color.White })
.borderRadius(8)
.position({ x: cell.x, y: cell.y }) // ← 关键:定位
.width(cell.w)
.height(cell.h)
}
@Builder 带参数自 API 12+ 支持,API 24 完全支持。
5.6 优缺点
优点:
- 完全灵活:可以任意控制每个子项的位置和尺寸
- 动态比例:
rowFactors/colFactors可以用@State装饰,运行时修改 - 支持非网格排列:算法可以改为任意布局规则(环绕、放射、自由排列)
- 精确的动画控制:配合
animateTo()或过渡动画,可以实现位置形变动画
缺点:
- 代码量较大:需要手写布局算法
- 性能敏感:每次
recalc()需要重建数组,配合ForEach的 key 机制优化 - 需要理解坐标系统:Stack 子组件的 position 坐标相对于 Stack 容器自身
6. API 24 新特性与注意事项
6.1 fr 单位的增强
API 24 的 Grid 对 fr 单位的计算进行了优化,支持更复杂的表达式:
6.2 @Builder 参数支持
API 12+ 开始,@Builder 可以接受最多 1 个参数(API 24 延续此限制)。参数可以是基本类型或对象类型,但不能是函数类型。
// ✅ 合法
@Builder buildCell(cell: CellData) { ... }
// ❌ 非法(API 24 不支持多个参数)
@Builder buildCell(x: number, y: number) { ... }
// 改为:
@Builder buildCell(pos: { x: number, y: number }) { ... }
6.3 @State 数组的变更检测
@State 装饰的数组在以下操作时会触发 UI 刷新:
| 操作 | 是否触发刷新 | 说明 |
|---|---|---|
this.cells = [...] (新数组) |
✅ | 替换引用,触发 |
this.cells.push(item) |
❌ | 不替换引用,不触发 |
this.cells.splice(...) |
❌ | 同样不触发 |
this.cells = [...this.cells, item] |
✅ | 创建新数组,触发 |
因此 recalc() 中必须使用 this.cells = newCells 来赋值,而不是 this.cells.push(...)。
6.4 ResourceColor 的类型收窄
API 24 中 ResourceColor 类型定义如下:
直接传入 hex 字符串 '#FF7043' 是完全合法的,无需转换为 Color 对象。需要注意 argb 格式的字符串 '#80FF7043'(半透明)也受支持。
6.5 onAreaChange 回调
onAreaChange 是 API 12+ 引入的回调,用于监听组件尺寸和位置的变化。回调签名:
.onAreaChange((oldArea: Area, newArea: Area) => {
// oldArea: 变更前的区域信息
// newArea: 变更后的区域信息(width, height, x, y)
const w = newArea.width as number;
const h = newArea.height as number;
})
Area 的 width 和 height 类型为 number | string | undefined,需要使用类型断言 as number。
注意:onAreaChange 会在布局稳定后回调,如果在回调中修改状态导致重新布局,可能会引起循环触发。在 recalc() 中可以加入 if (w <= 0 || h <= 0) return; 作为保护。
6.6 与 API 11/12 的兼容性
如果项目需要向下兼容低版本 API(如 API 11),以下限制需要注意:
7. 完整代码逐段解析
7.1 关键组件
CellItem:ResourceColor 类型接收 hex 字符串,.width('100%').height('100%') 撑满父容器。
StyleA_GridLayout:双层 ForEach 构建 3×3 矩阵,rowsTemplate('1fr 2fr 1.5fr') 决定行高比。
StyleB_CustomLayout:@State cells 存储布局结果,onAreaChange 触发 recalc(),@Builder buildCell 用 .position() 定位。
8. 性能分析与选型建议
8.1 布局性能对比
| 指标 | Grid 模板方案 | Stack 自定义方案 |
|---|---|---|
| 首次布局时间 | O(1) — Grid 内置算法 | O(n) — n=9 需手动计算 |
| 重布局触发条件 | Grid 内部自动 | ΘnAreaChange → recalc |
| 子节点复用 | Grid 自带复用机制 | ForEach 键值控制复用 |
| 动画过渡 | Grid 内部动画 | 需自行实现过渡 |
| 内存占用 | 低 (原生组件) | 略高 (需维护数组) |
| 适用网格数 | 任意尺寸 | 建议 ≤ 100 |
8.2 何时选择方案一(Grid)
- ✅ 行列比例固定,运行时不变
- ✅ 网格数量较大(超过 20 个)
- ✅ 需要内置滚动复用
- ✅ 团队成员熟悉声明式布局
8.3 何时选择方案二(Stack + 自定义算法)
- ✅ 行列比例需要运行时动态调整
- ✅ 需要精确控制每个 cell 的位置(非严格网格)
- ✅ 需要自定义动画(如拖拽重排)
- ✅ 需要跨行跨列的 cell 合并
8.4 混合使用
两种方案并非互斥。实际项目中可以混合使用:
build() {
Column() {
// 固定区域使用 Grid
StyleA_GridLayout()
// 动态区域使用 Stack + 自定义布局
StyleB_CustomLayout()
}
}
9. 从 Flutter 迁移到 ArkUI 的思维模型
9.1 布局声明式的差异
Flutter 使用嵌套 Widget 树 + 构建函数来描述布局,每个 Widget 在其 build() 中返回子 Widget 树。ArkUI 则使用 @Component struct + build() 方法,结构上更接近 SwiftUI 和 Compose。
// Flutter — 每个 Widget 都是对象
return Scaffold(
body: Stack(
children: [
Positioned(
left: 10, top: 20,
child: Text("Hello"),
),
],
),
);
// ArkUI — 声明式 DSL
build() {
Stack() {
Text("Hello")
.position({ x: 10, y: 20 })
}
}
9.2 响应式状态的差异
| 特性 | Flutter | ArkUI |
|---|---|---|
| 状态声明 | StatefulWidget + setState() |
@State 装饰器 |
| 自动更新 | 调用 setState 触发 rebuild |
@State 变量赋值自动触发 |
| 数组监听 | 需 List.generate + setState |
@State 数组重新赋值触发 |
| 计算属性 | getter 方法 |
getter 方法(同) |
9.3 自定义布局的差异
| 步骤 | Flutter | ArkUI |
|---|---|---|
| 定义容器尺寸 | LayoutDelegate.getSize() |
onAreaChange 回调 |
| 计算子节点位置 | performLayout() |
recalc() 方法 |
| 设置子节点位置 | positionChild(id, offset) |
.position({ x, y }) |
| 设置子节点尺寸 | layoutChild(id, constraints) |
.width(w).height(h) |
| 触发重布局 | 父节点 requestLayout() |
@State 重新赋值 |
9.4 常见迁移误区
误区一:在 ArkUI 中找完全对应的组件
ArkUI 不是 Flutter 的「直接映射」,两种框架的设计哲学不同。Flutter 的「Everything is a Widget」与 ArkUI 的「声明式 DSL」有本质差异。更有效的方式是理解目标框架的原生能力,而不是在目标框架中找源框架的影子。
误区二:在 build() 中写计算逻辑
build() 应专注于 UI 描述。
build() 应该专注于 UI 描述。布局计算应放在回调或生命周期方法中。
误区三:用 Stack.position 做所有布局
Stack + .position() 是强大的工具,但不是万能工具。对于大量子组件,Grid 或 Flex 的声明式方案性能更好。
10. 总结
10.1 核心结论
本文通过一个「3×3 行列不等高布局」的具体案例,展示了在 HarmonyOS ArkUI(API 24) 中自定义布局的两种实现路线:
- 方案一:
Grid+ 模板字符串 — 最简洁,适合比例固定场景 - 方案二:
Stack+ 自定义算法 — 最灵活,适合精确控制场景
10.2 关键技术要点
fr比例单位:rowsTemplate('1fr 2fr 1.5fr')按比例分配空间@State响应式数组:重新赋值触发 UI 刷新onAreaChange回调:监听容器尺寸变化@Builder带参:提取可复用 UI 片段ResourceColor类型:直接接受 hex 字符串
10.3 延伸思考
本文的布局算法可扩展为不等跨布局、响应式因子、拖拽重排、动画过渡等场景。详细 API 见 HarmonyOS Grid 组件参考。
更多推荐

所有评论(0)