【共创季稿事节】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数组深度观测的前提。
@ObservedclassGridCategory{name:string;icon:string;color:number;constructor(name:string,icon:string,color:number){this.name=name;this.icon=icon;this.color=color;}}@ObservedclassWaterfallItem{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 子组件)
@BuilderGridCell(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 子组件)
@BuilderWaterfallCell(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 推荐使用新的装饰器体系:
@ObservedV2classWaterfallItem{@Tracetitle:string='';@Tracedescription:string='';@Traceheight:number=0;@Tracecolor:number=0;}@ObservedV2+@Trace的粒度更细,仅标记需要观测的属性,性能开销更低。
8.3 WaterFlow onReachEnd 的分页实践
在实际项目中,onReachEnd可以配合分页加载:
@Statepage:number=1;@StateisLoading: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 混合布局,就掌握了鸿蒙原生开发中最常用的首页架构。
十一、参考资料
- HarmonyOS NEXT 开发文档 - ArkUI 布局
- WaterFlow 组件 API
- Grid 组件 API
- @Observed 和 @ObservedV2 装饰器
