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

在这里插入图片描述
在这里插入图片描述


目录

  1. 问题背景
  2. Flutter 方案回顾
  3. ArkUI 等价方案全景
  4. 方案一:Grid + 模板字符串
  5. 方案二:自定义 Layout 算法 + Stack 定位
  6. API 24 新特性与注意事项
  7. 完整代码逐段解析
  8. 性能分析与选型建议
  9. 从 Flutter 迁移到 ArkUI 的思维模型
  10. 总结

1. 问题背景

在移动端和跨平台 UI 开发中,「网格布局」是最常见也最核心的布局模式之一。标准的网格通常等宽等高,例如九宫格、照片墙等。但在实际业务场景中,我们往往需要不等高、不等宽的异形网格来承载不同权重的内容。

典型场景包括:

  • 资讯首页:头条新闻占据 2 倍高度,次要新闻占 1 倍
  • 商品展示:爆款商品占据更大面积
  • 仪表盘:核心指标卡片尺寸突出
  • 瀑布流导航:不同功能入口的重要性通过面积区分

本文将以此为切入点,深入剖析在 HarmonyOS ArkUI(API 24) 框架下,如何用两种不同的技术路线实现「3×3 行列不等高布局」,并与 Flutter 的 CustomMultiChildLayout + LayoutDelegate 方案进行对比,帮助跨平台开发者快速建立 ArkUI 布局思维模型。


2. Flutter 方案回顾

2.1 CustomMultiChildLayout 的核心机制

在 Flutter 中,CustomMultiChildLayout 是一个强大的自定义多子布局组件,它配合 LayoutDelegate 抽象类工作:

核心流程是三层:

  1. 父容器 将自己的尺寸传递给 LayoutDelegate
  2. performLayout 中开发者手动测量每个子节点,计算其偏移
  3. 每个子节点 被放置在计算出的位置上

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 两种实现路线

路线 核心组件 适用场景 代码量
路线 AGrid + 模板字符串 Grid, rowsTemplate, columnsTemplate 行列比例固定孩子自动填充 约 30 行
路线 BStack + 自定义位置算法 Stack, @State + recalc() 任意自定义排列需要精确控制 约 80 行

3.3 API 24 带来的新能力

API 24(对应 HarmonyOS 6.1)在布局领域引入或强化了以下能力:

  1. Grid 组件 rowsTemplate / columnsTemplate 增强
  2. @Builder 支持带参数
  3. @State 数组重新赋值触发刷新
  4. ForEach 键值生成器
  5. onAreaChange 回调监听尺寸变化

这些能力共同构成了自定义布局的基础设施。


4. 方案一:Grid + 模板字符串

4.1 核心原理

Grid 组件的 rowsTemplatecolumnsTemplate 属性接受以空格分隔的模板字符串,每个段定义一个轨道的尺寸。支持多种单位:

对于 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() 的流程

  1. 定义因子数组rowFactors / colFactors)描述每行每列的比例
  2. 通过 onAreaChange 监听容器尺寸变化
  3. recalc() 方法中计算每个 cell 的 { x, y, width, height }
  4. 将结果存入 @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 的 Gridfr 单位的计算进行了优化,支持更复杂的表达式:

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;
})

Areawidthheight 类型为 number | string | undefined,需要使用类型断言 as number

注意onAreaChange 会在布局稳定后回调,如果在回调中修改状态导致重新布局,可能会引起循环触发。在 recalc() 中可以加入 if (w <= 0 || h <= 0) return; 作为保护。

6.6 与 API 11/12 的兼容性

如果项目需要向下兼容低版本 API(如 API 11),以下限制需要注意:


7. 完整代码逐段解析

7.1 关键组件

CellItemResourceColor 类型接收 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() 是强大的工具,但不是万能工具。对于大量子组件,GridFlex 的声明式方案性能更好。


10. 总结

10.1 核心结论

本文通过一个「3×3 行列不等高布局」的具体案例,展示了在 HarmonyOS ArkUI(API 24) 中自定义布局的两种实现路线:

  1. 方案一:Grid + 模板字符串 — 最简洁,适合比例固定场景
  2. 方案二:Stack + 自定义算法 — 最灵活,适合精确控制场景

10.2 关键技术要点

  • fr 比例单位rowsTemplate('1fr 2fr 1.5fr') 按比例分配空间
  • @State 响应式数组:重新赋值触发 UI 刷新
  • onAreaChange 回调:监听容器尺寸变化
  • @Builder 带参:提取可复用 UI 片段
  • ResourceColor 类型:直接接受 hex 字符串

10.3 延伸思考

本文的布局算法可扩展为不等跨布局、响应式因子、拖拽重排、动画过渡等场景。详细 API 见 HarmonyOS Grid 组件参考

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐