【共创季稿事节】Grid+WaterFlow混合布局-鸿蒙ArkTS实战博客
鸿蒙原生 ArkTS 布局实战:Grid + WaterFlow 混合布局(顶部网格 + 底部瀑布流)
一、引言
在移动端应用开发中,混合布局是首页设计的常见模式。典型的「顶部网格分类 + 底部瀑布流内容推荐」布局,既能高效展示多入口(Grid),又能以错落有致的卡片流(WaterFlow)承载大量内容,兼顾信息密度与视觉层次。
HarmonyOS NEXT 的 ArkUI 声明式框架提供了 Grid 和 WaterFlow 两个高性能布局容器,配合 Column 纵向容器,可以非常优雅地实现这一经典布局。
本文从零开始构建一个完整的示例应用,深入剖析每个组件的配置要点及混合布局的实现细节。
二、项目环境
| 项目 | 版本 |
|---|---|
| 操作系统 | HarmonyOS NEXT |
| API 版本 | 24(对应 SDK 7.0.0) |
| 开发工具 | DevEco Studio NEXT |
| 构建系统 | hvigor 6.23+ |
| 语言 | ArkTS(声明式 UI + 装饰器语法) |
在 build-profile.json5 中确认 SDK 配置:
{
"app": {
"products": [
{
"name": "default",
"targetSdkVersion": "7.0.0(24)",
"compatibleSdkVersion": "7.0.0(24)",
"runtimeOS": "HarmonyOS"
}
]
}
}
三、整体布局架构
我们使用三层嵌套实现完整的混合布局:
Column(根容器,撑满全屏)
├── Column(顶部网格区域)
│ ├── Row ← "🔥 热门分类" 标题栏
│ └── Grid ← 2×2 固定网格(不滚动)
│ ├── GridItem → 新品推荐(圆角卡片)
│ ├── GridItem → 热门榜单
│ ├── GridItem → 限时特惠
│ └── GridItem → 每日签到
│
└── Column(底部瀑布流区域,layoutWeight 占满剩余空间)
├── Row ← "✨ 为你推荐" 标题栏
└── WaterFlow ← 2 列瀑布流(自管理滚动)
├── FlowItem → 卡片 1(200vp)
├── FlowItem → 卡片 2(280vp)
├── FlowItem → 卡片 3(160vp)
└── ... → 共 12 项,高度 160~310vp 不等
关键设计原则:
- Grid:固定高度,不参与页面滚动——作为「顶部导航」区域,始终可见或随页面整体滚动
- WaterFlow:
layoutWeight(1)填满剩余空间,完全接管其内容区域的纵向滚动 - Column:作为串联容器,将两个布局区域纵向排列
四、数据模型层(@Observed 装饰器)
ArkTS 中使用 @Observed 装饰类,使其属性变化可被 UI 观察。这是 @State 数组深度观测的前提。
@Observed
class GridCategory {
name: string;
icon: string;
color: number;
constructor(name: string, icon: string, color: number) {
this.name = name;
this.icon = icon;
this.color = color;
}
}
@Observed
class WaterfallItem {
title: string;
description: string;
height: number; // 卡片高度,单位 vp —— 控制瀑布流错落效果
color: number; // 卡片色调
constructor(title: string, description: string, height: number, color: number) {
this.title = title;
this.description = description;
this.height = height;
this.color = color;
}
}
为什么用 @Observed?
在 ArkTS 中,@State 装饰的数组只观测引用变化(如 push、splice、重新赋值)。当数组中对象的属性发生变化时,需要 @Observed 装饰该类,才能触发 UI 刷新。虽然本示例不涉及动态属性修改,但这是生产环境最佳实践。
五、顶部 Grid 网格布局详解
5.1 布局配置
Grid() {
ForEach(this.gridList, (item: GridCategory) => {
GridItem() {
this.GridCell(item);
}
}, (item: GridCategory, index?: number) => index!.toString());
}
.columnsTemplate('1fr 1fr') // 2 列等宽
.rowsTemplate('1fr 1fr') // 2 行等高
.columnsGap(12) // 列间距 12vp
.rowsGap(12) // 行间距 12vp
.height(180) // 固定高度,不滚动
.clip(true) // 超限裁剪
.padding({ left: 16, right: 16 });
5.2 关键属性解析
| 属性 | 值 | 说明 |
|---|---|---|
columnsTemplate |
'1fr 1fr' |
两列等宽,fr 是网格的弹性单位,1fr 表示等分剩余空间 |
rowsTemplate |
'1fr 1fr' |
两行等高 |
columnsGap |
12 |
列与列之间 12vp 间距 |
rowsGap |
12 |
行与行之间 12vp 间距 |
height |
180 |
固定高度——与 rowsTemplate 配合,确保 Grid 不自带滚动 |
clip |
true |
超出高度部分裁剪,避免与下方 WaterFlow 重叠 |
5.3 「固定高度不滚动」的意义
Grid 组件默认拥有自己的滚动行为——当内容超出设定高度时会内部滚动。但在混合布局中,我们不希望 Grid 独立滚动,而是让整体由 WaterFlow 统一管理滚动。因此设置 height(180) + clip(true),将 Grid 限制为 2×2 静态网格。
5.4 Grid 单元格(@Builder 子组件)
@Builder
GridCell(item: GridCategory) {
Column() {
// 圆形图标背景
Text(item.icon)
.fontSize(32)
.width(48).height(48)
.backgroundColor(Color.White)
.borderRadius(24);
// 分类名称
Text(item.name)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.margin({ top: 8 });
}
.width('100%').height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor(item.color)
.borderRadius(14)
.shadow({ radius: 6, offsetX: 0, offsetY: 3, color: 0x22000000 });
}
设计要点:
@Builder将 UI 抽取为独立子组件,避免build()方法膨胀- 白色圆形背景托起 emoji 图标,形成「图标在卡片中央」的视觉效果
- 半透明阴影(
0x22为 alpha 通道,对应约 13% 不透明度)增加层次感 .width('100%').height('100%')使单元格撑满GridItem的区域
六、底部 WaterFlow 瀑布流布局详解
6.1 布局配置
WaterFlow() {
ForEach(this.waterfallList, (item: WaterfallItem) => {
FlowItem() {
this.WaterfallCell(item);
}
}, (item: WaterfallItem, index?: number) => index!.toString());
}
.columnsTemplate('1fr 1fr') // 2 列等宽
.columnsGap(10) // 列间距 10vp
.rowsGap(10) // 行间距 10vp
.layoutWeight(1) // 填满 Column 剩余空间
.width('100%')
.padding({ left: 12, right: 12 })
.backgroundColor('#F5F5F5')
.onReachEnd(() => { // 触底加载更多
console.info('[WaterFlow] 触底,可在此加载更多数据');
})
.onReachStart(() => { // 到顶
console.info('[WaterFlow] 到达顶部');
})
.onScrollIndex((firstIndex: number, lastIndex: number) => {
console.info(`[WaterFlow] 可见区间: [${firstIndex}, ${lastIndex}]`);
});
6.2 核心属性解析
| 属性 | 值 | 说明 |
|---|---|---|
columnsTemplate |
'1fr 1fr' |
两列等宽,和 Grid 语法一致 |
columnsGap |
10 |
列间距 |
rowsGap |
10 |
行间距(WaterFlow 中表现为垂直间距) |
layoutWeight |
1 |
在 Column / Row / Flex 中占据剩余比例空间 |
onReachEnd |
回调 | 上拉加载更多的入口,适合数据分页 |
onReachStart |
回调 | 下拉刷新判断点 |
onScrollIndex |
回调 | 获取当前可见区间,可用于埋点或懒加载 |
6.3 WaterFlow 的工作原理
WaterFlow 是 ArkUI 提供的高性能瀑布流容器,其核心机制:
- 自动布局:按
columnsTemplate分列,每个FlowItem从上到下依次排入当前最短列 - 按需渲染:只渲染可见区域的
FlowItem,内存与性能优于手动计算位置 - 自管理滚动:无需外层
Scroll包裹,自身处理手势滚动 - 高度自适应:每个
FlowItem的高度独立计算,形成错落有致的「瀑布」效果
6.4 瀑布流卡片(@Builder 子组件)
@Builder
WaterfallCell(item: WaterfallItem) {
Column() {
// 模拟图片区域
Column()
.width('100%')
.height(item.height - 80) // 根据 item.height 动态计算图片区
.borderRadius({ topLeft: 12, topRight: 12 })
.backgroundColor(item.color);
// 文字内容区域
Column() {
Text(item.title)
.fontSize(15).fontWeight(FontWeight.Bold)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis });
Text(item.description)
.fontSize(12).lineHeight(18)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 4 });
}
.padding({ left: 10, right: 10, top: 8, bottom: 10 })
.width('100%')
.layoutWeight(1);
}
.width('100%')
.height(item.height) // 每个卡片独立高度
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 4, offsetX: 0, offsetY: 2, color: 0x15000000 });
}
错落感来源:12 张卡片的高度从 160vp 到 310vp 不等,WaterFlow 自动将每个卡片排入当前最短的列,产生自然的「瀑布」错落效果。
数据示例:
晨曦山峦 200vp │ 城市夜景 280vp
海洋日记 160vp │ 林间小径 240vp
星空物语 300vp │ 咖啡时光 180vp
花海漫游 260vp │ 古建筑韵 220vp
美食探索 190vp │ 极光之舞 310vp
沙漠驼铃 170vp │ 湖畔垂钓 250vp
七、混合布局的尺寸管理
7.1 layoutWeight 的妙用
本示例中最关键的尺寸管理手段是 layoutWeight:
Column() {
// ... 顶部 Grid 区域(自然高度)
}
// ...(没有 layoutWeight,取自身高度)
Column() {
// ... 标题
WaterFlow()
.layoutWeight(1) // WaterFlow 填满此 Column
}
.layoutWeight(1) // 此 Column 填满根 Column 的剩余空间
工作流程:
- 根
Column高度为100%(全屏) - 顶部 Grid 区域按内容自然高度渲染(约 240vp)
- 底部 WaterFlow 区域的
Column设置了.layoutWeight(1),自动占据剩余全部空间 - 该 Column 内部的
WaterFlow同样设置了.layoutWeight(1),填满 Column 内剩余高度 - WaterFlow 在自己的有效区域内管理滚动,超出的卡片内容通过内部滚动查看
7.2 为什么用两层 Column?
有人可能会问:为什么不在根 Column 直接放 WaterFlow?
// ❌ 不推荐的简化写法
Column() {
Grid() // 顶部网格
.height(180); // 固定高度
WaterFlow() // 瀑布流
.layoutWeight(1); // 填满剩余空间
}
这种写法理论上可行,但实际使用中存在两个问题:
- 标题缺失:Grid 和 WaterFlow 各自需要标题栏(“热门分类”、“为你推荐”),需要额外的布局容器
- 样式隔离:顶部 Grid 区域有白色背景 + 圆角,底部 WaterFlow 区域有灰色背景,用两层 Column 可以天然隔离样式作用域
因此两层 Column 的结构在可维护性和视觉还原度上更优。
八、API 24 新增特性与最佳实践
HarmonyOS NEXT API 24(SDK 7.0.0)在布局方面带来以下改进:
8.1 性能优化
- WaterFlow 的懒加载性能提升:配合
LazyForEach实现真正按需渲染,千级数据量也不卡顿 - Grid 的复用机制:
GridItem支持节点复用,减少频繁建销毁的开销
8.2 推荐使用 @ObservedV2 + @Trace
虽然本示例使用 @Observed(它在 API 24 中仍然兼容),但 API 24 推荐使用新的装饰器体系:
@ObservedV2
class WaterfallItem {
@Trace title: string = '';
@Trace description: string = '';
@Trace height: number = 0;
@Trace color: number = 0;
}
@ObservedV2 + @Trace 的粒度更细,仅标记需要观测的属性,性能开销更低。
8.3 WaterFlow onReachEnd 的分页实践
在实际项目中,onReachEnd 可以配合分页加载:
@State page: number = 1;
@State isLoading: boolean = false;
WaterFlow() {
// ...
}
.onReachEnd(() => {
if (this.isLoading) return;
this.isLoading = true;
this.page++;
loadMoreData(this.page).then((newItems) => {
this.waterfallList.push(...newItems);
this.isLoading = false;
});
});
九、运行效果
在 DevEco Studio 中连接模拟器或真机运行后,可以看到:
- 顶部:4 个彩色圆角卡片排成 2×2 网格,每个卡片中央显示图标与分类名称,底部有柔和阴影
- 底部:双列瀑布流卡片依次排布,卡片高度从 160vp 到 310vp 不等,左列和右列的卡片起始位置不同,形成自然的「瀑布」落差视觉效果
- 滚动交互:上滑时 WaterFlow 区域流畅滚动,下滑到顶触发
onReachStart回调,滑到底触发onReachEnd回调 - 视觉层次:网格区白色背景 + 大圆角,瀑布流区浅灰背景,两者之间通过
margin隔离,层次分明
十、总结
本文通过一个完整的示例,详细讲解了如何在 HarmonyOS NEXT 中使用 Grid + WaterFlow + Column 实现「顶部网格 + 底部瀑布流」的混合布局。
核心收获:
| 知识点 | 要点 |
|---|---|
| Grid 固定高度 | 配合 .clip(true) 关闭内部滚动,作为静态网格区域 |
| WaterFlow 自管理滚动 | layoutWeight(1) 填满剩余空间,内部滚动不冲突 |
| Column 串联 | 两个 Column 分别承载 Grid 和 WaterFlow,样式隔离 |
| @Builder 抽离子组件 | 提升代码可读性与复用性 |
| @Observed 数据模型 | 支持深度属性变化的 UI 观测 |
| onReachEnd 分页回调 | 真正的上拉加载更多入口 |
这套布局模式适用于绝大多数移动端首页——淘宝、京东、小红书等主流应用均采用类似结构。掌握了 Grid + WaterFlow 混合布局,就掌握了鸿蒙原生开发中最常用的首页架构。
十一、参考资料



更多推荐




所有评论(0)