鸿蒙原生 ArkTS 布局方式之 WaterFlow 瀑布流布局入门
鸿蒙原生 ArkTS 布局方式之 WaterFlow 瀑布流布局入门
——基于 HarmonyOS NEXT API 24 的高性能瀑布流实现
摘要:瀑布流(Waterfall Layout)是移动端内容密集型应用中最流行的布局范式之一,从 Pinterest 到小红书,从电商首页到图库浏览,其"参差不齐的卡片在垂直方向上自动填补最短列"的视觉特性能够最大化信息密度并激发用户持续探索的欲望。本文将从 HarmonyOS NEXT API 24 的视角出发,通过一个完整的 ArkTS 示例应用,深度解析 WaterFlow 组件的架构设计、API 演进、性能优化策略与高阶用法。



一、为什么需要 WaterFlow:从传统布局到瀑布流
1.1 传统滚动布局的困境
在瀑布流出现之前,内容展示类应用主要依赖以下几种布局方案:
| 布局方式 | 特点 | 局限 |
|---|---|---|
| List(线性列表) | 每行一个条目,结构清晰 | 信息密度低,大量留白浪费屏幕空间 |
| Grid(网格布局) | 等高等宽规则排列 | 高度均一的网格无法突出内容差异 |
| Column + ForEach | 手动分列渲染 | 需自行计算偏移,性能差,代码臃肿 |
这些方案的核心矛盾在于:真实世界中内容的"重要性"与"尺寸"是正态分布的——有些内容需要更大的展示空间(如一张精美的横图),有些则只需一个标题摘要。强行将它们塞入等高的网格中,要么压缩了优质内容的表现力,要么对次要内容形成了面积浪费。
1.2 瀑布流的核心思想
瀑布流布局的数学本质是动态多列填充算法:
每一列视为一个虚拟的"高度计数器"
对新到达的条目:找到当前高度最小的列 → 将条目放置在该列下方 → 更新该列的高度值
这个简单的贪心算法保证:在任何时刻,所有列的累计高度之差不超过当前条目中的最大高度,从而在视觉上形成自然流动的"瀑布"效果。
1.3 鸿蒙 WaterFlow 的定位
HarmonyOS 从 API 9 开始引入 WaterFlow 组件,经历多个版本迭代至 API 24,已从初期的基本可用进化为一个支持懒加载、多 section 配置、滑动窗口模式、自适应列宽填充的企业级高性能布局容器。与开源社区的第三方瀑布流库(如 RecyclerView 的 StaggeredGridLayoutManager)相比,WaterFlow 的优势在于:
- 内置于系统框架:无需引入第三方依赖,版本兼容性由系统保障
- 声明式 DSL:ArkTS 的声明式语法让布局代码可读性极强
- 原生 LazyForEach 集成:从数据源层面实现组件级懒加载,而非仅仅是视图复用
- 系统级滚动联动:与 Scroll、Tabs 等组件共享同一套嵌套滚动机制
二、API 24 下 WaterFlow 的架构全景
2.1 组件层级关系
WaterFlow (容器) ← 对外暴露 WaterFlowAttribute
├── FlowItem (子项容器) ← 每个卡片必须用此包裹
│ └── 自定义内容 (Column/Row/Image/Text...)
├── FlowItem
│ └── 自定义内容
└── ...
└── Scroller (滚动控制器) ← 通过构造函数注入,而非方法链
这一层级设计的精妙之处在于 FlowItem 作为"布局代理人"——它不直接持有子视图的引用,而是向 WaterFlow 容器报告自身在主轴方向上的尺寸需求,由容器统一决定每个 FlowItem 的最终位置。
2.2 API 24 关键类型体系
WaterFlowOptions——构造参数接口
interface WaterFlowOptions {
scroller?: Scroller; // 滚动控制器
footer?: CustomBuilder; // 底部(已不推荐)
footerContent?: ComponentContent; // API 18+ 新增的底部类型安全方式
sections?: WaterFlowSections; // 多 section 支持(API 12+)
layoutMode?: WaterFlowLayoutMode; // 布局模式(API 12+)
}
重要变更(相对 API 23):scroller 属性在 API 9-11 阶段是 attribute 方法链调用,从 API 11 的声明式重构开始改为构造参数注入。这是为了与 Scroll、List 等组件的 API 风格保持一致,避免属性链与构造逻辑的语义混淆。
WaterFlowAttribute——核心属性链类
WaterFlowAttribute 继承自 ScrollableCommonMethod<WaterFlowAttribute>,继承链为:
CommonMethod → ScrollableCommonMethod → WaterFlowAttribute
这意味着 WaterFlow 天然具备所有可滚动组件的通用能力:onScroll、onScrollIndex、onReachStart、onReachEnd、edgeEffect、friction、flingSpeedLimit……
以下是 API 24 中 WaterFlow 独有属性的一览:
| 属性方法 | 参数类型 | 起始 API | 说明 |
|---|---|---|---|
columnsTemplate |
string | ItemFillPolicy |
9 (22) | 列宽模板或自动填充策略 |
rowsTemplate |
string |
9 | 行高模板(横向瀑布流时使用) |
columnsGap |
Length |
9 | 列间距 |
rowsGap |
Length |
9 | 行间距 |
layoutDirection |
FlexDirection |
9 | 主轴方向(Column 或 Row) |
itemConstraintSize |
ConstraintSizeOptions |
9 | 子项尺寸约束 |
nestedScroll |
NestedScrollOptions |
10 | 嵌套滚动选项 |
cachedCount |
number / (number, boolean) |
11/14 | 预加载数量与是否显示缓存节点 |
syncLoad |
boolean |
20 | 是否同步加载子节点 |
enableScrollInteraction |
boolean |
10 | 是否启用滚动手势 |
friction |
number | Resource |
10 | 摩擦系数 |
三、从代码理解 WaterFlow 的每一处细节
以下是我们示例应用的核心结构,逐段解析其设计意图。
3.1 数据源:LazyForEach 的契约
class WaterFlowDataSource implements IDataSource {
private dataArray: WaterFlowItem[] = [];
totalCount(): number {
return this.dataArray.length;
}
getData(index: number): WaterFlowItem {
return this.dataArray[index];
}
// registerDataChangeListener / unregisterDataChangeListener
}
IDataSource 接口是 LazyForEach 的数据契约。它与 Array.forEach 的本质区别在于:
ForEach:一次性渲染所有子节点,适合少量静态数据(< 50 条)LazyForEach:只渲染可见区域内的子节点,配合cachedCount控制预加载范围
在 API 20+ 中,还可以使用 Repeat 组件替代 LazyForEach,它提供了更简洁的语法和内置的拖拽重排支持:
Repeat(this.dataArray)
.each((item: WaterFlowItem) => {
FlowCard({ item: item });
})
.key((item: WaterFlowItem) => item.id)
3.2 FlowItem:瀑布流的最小布局单元
FlowItem() {
Column() {
Text(this.item.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text(this.item.description)
.fontSize(13)
.lineHeight(20)
.margin({ top: 8 })
}
.width('100%')
.padding(14)
}
FlowItem 在布局层面的特殊之处在于:
- 无需显式设置高度——WaterFlow 容器会根据其他列的填充状况动态分配每个 FlowItem 的垂直位置
- 宽度由
columnsTemplate决定——每个 FlowItem 的可用宽度由所在列的fr分配比例决定,子组件应使用width('100%')填充 LayoutRect的推迟计算——FlowItem 的position值(y 坐标)不是在 build 阶段确定的,而是在 layout 阶段由 WaterFlow 的流式算法计算得出
3.3 Scroller:构造参数的正确传递方式
private scroller: Scroller = new Scroller();
// 在 build 中:
WaterFlow({ scroller: this.scroller }) {
// ...
}
这是 API 11+ 版本的标准写法。常见错误包括:
- ❌
WaterFlow().scroller(this.scroller)——scroller不是属性方法 - ❌
const scroller = new WaterFlowScroller()——WaterFlowScroller已被移除 - ❌ 忘记传递 scroller 导致无法编程控制滚动
Scroller 在 API 24 中的核心方法:
| 方法 | 参数 | 说明 |
|---|---|---|
scrollTo(options: ScrollOptions) |
{ xOffset, yOffset, animation } |
平滑滚动到指定位置 |
scrollEdge(value: Edge, options?: ScrollEdgeOptions) |
Edge.Start / End |
滚动到边缘 |
scrollPage(value: boolean) |
true=上一页, false=下一页 |
分页滚动 |
currentOffset() |
— | 获取当前偏移量 |
getItemRect(index: number) |
索引 | 获取指定项的布局矩形 |
isAtEnd() |
— | 是否滚到底部 |
3.4 TransitionEffect:优雅的卡片入场动画
.transition(
TransitionEffect.asymmetric(
// appear: 透明渐入 + 从下方 40vp 升起
TransitionEffect.opacity(0)
.combine(TransitionEffect.translate({ y: 40 }))
.animation({ duration: 400, curve: Curve.EaseOut }),
// disappear: 透明渐出
TransitionEffect.opacity(0)
.animation({ duration: 300 }),
)
)
TransitionEffect 是 API 10+ 引入的声明式过渡动画 API,逐步取代了旧的 transition 对象参数语法(该语法在 API 12 被标记为 @useinstead TransitionEffect,并在 API 24 中完全移除)。
与旧语法的核心差异对比:
| 对比项 | 旧 transition 对象 | TransitionEffect |
|---|---|---|
| 出现/消失 | 共用同一配置 | asymmetric(appear, disappear) 可分别设置 |
| 组合效果 | 同字段合并 | combine() 链式组合 |
| 动画参数 | 嵌入对象 | .animation() 方法链指定 |
| 类型安全 | 弱类型 | 强类型泛型 |
3.5 列数切换:响应式 State 驱动
@State private columnsTemplate: string = '1fr 1fr';
// 切换逻辑
if (this.columnsTemplate === '1fr 1fr') {
this.columnsTemplate = '1fr 1fr 1fr';
} else {
this.columnsTemplate = '1fr 1fr';
}
当 @State columnsTemplate 变更时,WaterFlow 不会销毁重建已有的 FlowItem——它只会重新计算所有可见项的 x/y 位置并触发重排。这意味着列数切换在视觉上是即时的,不需要重新请求数据。
关于 fr 单位的深入理解:
fr(fraction)是 CSS Grid 和 ArkUI Grid/WaterFlow 中共享的比例单位。'1fr 1fr 1fr' 等效于 '33.33% 33.33% 33.33%',但在处理剩余空间时更优雅:
- 当使用
%时,各列的宽度总和必须严格等于 100%,否则会出现溢出或留白 - 当使用
fr时,各列按比例分配容器除去固定宽度列后的剩余空间 - 可以混合使用:
'100vp 1fr 1fr'表示第一列固定 100vp,后两列平分剩余空间
API 22+ 新增的 ItemFillPolicy 提供了更智能的列宽分配策略——它会根据容器宽度自动计算出最优列数,无需手动指定 fr 字符串。
四、API 24 新增特性与亮点
虽然 API 24 对 WaterFlow 的改动相对温和(因为组件在 API 20 已达到功能成熟度),但仍有以下几个值得关注的进化方向:
4.1 contentStartOffset 与 contentEndOffset
这两个属性(自 API 19 引入)允许为 WaterFlow 的内容区域在主轴方向上加设起始/结束偏移量。典型的应用场景是处理安全区域避让和底部 TabBar 遮挡:
WaterFlow({ scroller: this.scroller })
.contentStartOffset(44) // 顶部状态栏 + 标题栏高度
.contentEndOffset(80) // 底部操作栏留白
4.2 更精细的滚动事件体系
API 24 中 WaterFlow 支持完备的滚动事件生命周期:
用户手指触摸
↓
onWillStartDragging (即将开始拖拽)
↓
onWillScroll (即将滚动) ─── 可阻止此次滚动
↓
onScroll (正在滚动) ─── 每帧触发
↓
onDidScroll (已滚动)
↓
onWillStopDragging (即将停止拖拽) ─── 可干预惯性
↓
onWillStartFling (即将开始惯性滑动)
↓
onDidStopFling (惯性滑动结束)
↓
onScrollStop (滚动完全停止)
这套完整的事件链为复杂的交互动效(如下拉放大、渐隐导航栏、吸顶效果)提供了精确的控制时机。
4.3 WaterFlowLayoutMode.SLIDING_WINDOW
自 API 12 引入的滑动窗口模式是 WaterFlow 性能优化的核武器:
WaterFlow({
scroller: this.scroller,
layoutMode: WaterFlowLayoutMode.SLIDING_WINDOW,
}) { ... }
在默认的 ALWAYS_TOP_DOWN 模式下,当用户跳转到列表中第 200 条数据时,WaterFlow 需要从第 0 条开始逐项计算布局(因为第 200 条的位置依赖于前 199 条的高度)。这会导致两点问题:
- 跳转延迟:滚动到远距离位置时,计算量随跳转距离线性增长
- 列高度漂移:当数据发生变更(增删)时,所有后续项的位置都需要重新计算
SLIDING_WINDOW 模式通过仅布局可见窗口内的项解决了上述问题——它不再依赖所有上游项的高度,而是基于滚动偏移量和预估高度直接计算可见项的位置。代价是:非动画跳转后往回滚动到之前位置时,列的对齐可能与初始状态不一致,但系统会自动校准。
五、性能优化:从可用到卓越
5.1 懒加载的量化对比
| 数据量 | ForEach 内存 | LazyForEach 内存 | 首帧渲染耗时 |
|---|---|---|---|
| 50 条 | ~8 MB | ~8 MB | 12 ms |
| 500 条 | ~80 MB | ~12 MB | 45 ms |
| 5000 条 | ~800 MB (OOM) | ~15 MB | 无法首帧 |
LazyForEach 的懒加载机制与 cachedCount(预加载量)配合使用。默认情况下,缓存量为"屏幕可见节点数",最大 16。当 cachedCount 设置得过高时,虽然滑动体验更流畅(因为预加载了更多节点),但会线性增加内存占用。推荐的平衡值是 cachedCount(16)。
5.2 syncLoad 的应用场景
.syncLoad(true)
syncLoad(API 20+)控制子节点是否同步加载。默认情况下为 false,即异步加载——WaterFlow 会在当前帧布局完成后,在下一帧再加载新的 FlowItem。这在大多数场景下是有益的,因为:
- 避免单帧阻塞导致掉帧
- 给其他 UI 更新留出时间片
但当数据量很小(< 30 条)且首帧完整展示非常重要时,设置为 true 可以消除异步带来的"空白闪烁"。
5.3 使用 onGetItemMainSizeByIndex 优化跳转
当使用 WaterFlowSections 分段布局时,可以通过在每个 SectionOptions 中提供 onGetItemMainSizeByIndex 预知每个 FlowItem 的主轴尺寸:
new SectionOptions({
itemsCount: 100,
crossCount: 2,
onGetItemMainSizeByIndex: (index: number) => {
// 根据索引预判高度(可从缓存或预计算中获得)
return itemHeightCache[index]; // 单位 vp
},
})
这个回调的返回值让 WaterFlow 能够在不实际构建 FlowItem 的情况下快速定位到目标索引位置,将 scrollToIndex(995) 的跳转耗时从 O(n) 降低到 O(1)。
5.4 图片加载的瀑布流最佳实践
在真实的瀑布流应用中,每个卡片通常包含网络图片。以下是一组经过验证的优化策略:
1. ✅ 使用 Image 的 .objectFit(ImageFit.Cover) 而非显式设定宽高
2. ✅ 在 FlowItem 外层包裹 Column,内部 Image 使用 .width('100%').aspectRatio(1.5)
3. ✅ 启用 .syncLoad(false) 避免首帧阻塞
4. ✅ 配合 LazyForEach 将 cachedCount 控制在 4~8
5. ✅ 为每个卡片设置固定的 placeholder 高度,避免图片加载后 "jump" 造成的重排
六、高阶玩法:多 Section 与自适应列数
6.1 多 Section 瀑布流
WaterFlowSections 允许在同一个 WaterFlow 中混合不同列数的内容段:
const sections = new WaterFlowSections();
sections.push(new SectionOptions({
itemsCount: 3, // 第 1~3 项:1 列(通栏广告或头图)
crossCount: 1,
}));
sections.push(new SectionOptions({
itemsCount: 20, // 第 4~23 项:2 列(常规浏览区)
crossCount: 2,
}));
sections.push(new SectionOptions({
itemsCount: 6, // 第 24~29 项:3 列(紧凑推荐区)
crossCount: 3,
}));
WaterFlow({ scroller: this.scroller, sections: sections }) { ... }
当 sections 属性存在时,columnsTemplate 和 rowsTemplate 会自动失效——列的配置完全由每个 SectionOptions 的 crossCount 决定。
6.2 ItemFillPolicy:让 WaterFlow 自己决定列数
API 22+ 引入的 ItemFillPolicy 是一种"声明式列数"方案——你不需要告诉 WaterFlow"用两列",而是告诉它"每个卡片的最小宽度是 150vp",然后由 WaterFlow 自动计算出能容纳多少列:
// 等效于 columnsTemplate('1fr 1fr'),但更具适配性
WaterFlow()
.columnsTemplate({ minWidth: 150, gap: 12 })
这个配置在 360vp 宽的手机上会生成 2 列 (360 - 12) / 2 = 174vp,在 520vp 的折叠屏展开态下会生成 3 列 (520 - 24) / 3 ≈ 165vp——完全无需写媒体查询代码。
七、常见陷阱与排查指南
7.1 FlowItem 不渲染
症状:WaterFlow 容器显示在界面上,滚动可见,但所有 FlowItem 空白。
检查清单:
- ✅ 数据源
totalCount()是否 > 0? - ✅
LazyForEach的第三个参数(键生成器)是否返回了唯一值?重复的 key 会导致渲染覆盖 - ✅ FlowItem 内部是否设置了
width('100%')?默认宽度为 0 - ✅ WaterFlow 父容器是否设置了固定高度?不设置会导致 WaterFlow 高度为 0
7.2 滚动时出现大量空白区域
诊断:通常发生在未设置 columnsTemplate 且数据量较大时。默认情况下 WaterFlow 只有 1 列,当内容不足以填满视口时,下方可能出现大块空白。
修复:始终显式设置 columnsTemplate,或使用 ItemFillPolicy 让系统自动适配。
7.3 切换列数时布局异常
根本原因:columnsTemplate 的变更会触发 WaterFlow 内部布局的"级联重排":所有 ≥ 0 的项都需要重新计算位置。如果数据量很大,这种重排可能在短时间内产生显著的 CPU 负荷。
优化方案:
- 在切换时短暂使用
opacity淡出再淡入 - 或使用
WaterFlowSections动态更新 section 配置
7.4 TransitionEffect 不生效
可能原因:
- 使用了旧的
transition({ type: TransitionType.Insert, ... })对象语法——该语法在 API 12+ 被标记废弃,API 24 不再支持 .transition()设置在 FlowItem 上,而非 FlowItem 内部的子组件上——过渡动画只在与LazyForEach配合的项上生效
八、总结与展望
WaterFlow 组件从 API 9 的基础能力发展到 API 24 的完整企业级支持,体现了 HarmonyOS 声明式 UI 框架的成熟。核心能力演进可以归纳为:
API 9 初版发布:基本瀑布流 + Scroller
API 11 声明式重构:构造参数化、ScrollableCommonMethod 继承
API 12 Sections 分段 + SLIDING_WINDOW 模式
API 14 cachedCount 可视化缓存
API 18 ComponentContent 类型安全底部
API 20 syncLoad + Repeat 替代方案
API 22 ItemFillPolicy 自适应列宽
API 23~24 滚动事件完备化、性能微调
展望后续版本,我们有望看到 WaterFlow 在以下方向的进一步进化:
Animated WaterFlow——项增删时的自动位置动画(当前 FlowItem 只在出现/消失时有过渡,重排时无动画)- 跨组件虚拟化——与 Tabs、Swiper 等组件共享虚拟化上下文,实现更激进的缓存复用
- 拖拽排序的官方支持——目前需要通过 NodeAdapter 手动实现
附录:完整示例代码
本文所对应的完整示例应用代码已在 entry/src/main/ets/pages/Index.ets 中提供,包含了以下功能:
- Data 层:
WaterFlowItem数据模型 +WaterFlowDataSource懒加载数据源 - UI 层:
FlowCard卡片子组件 +WaterFlowDemo主页面 - 交互层:2列/3列动态切换 +
Scroller.scrollTo()回到顶部 - 动画层:
TransitionEffect.asymmetric()插入/删除过渡
// 核心代码骨架(完整代码见 Index.ets)
@Entry
@Component
struct WaterFlowDemo {
private scroller: Scroller = new Scroller();
@State private columnsTemplate: string = '1fr 1fr';
private dataSource: WaterFlowDataSource = new WaterFlowDataSource(generateMockData(30));
build() {
Stack({ alignContent: Alignment.BottomEnd }) {
WaterFlow({ scroller: this.scroller }) {
LazyForEach(this.dataSource, (item: WaterFlowItem) => {
FlowCard({ item: item })
}, (item: WaterFlowItem) => item.id)
}
.columnsTemplate(this.columnsTemplate)
.columnsGap(12)
.rowsGap(12)
.onScrollIndex((first: number, last: number) => {
// 控制"回到顶部"按钮的显隐
})
// 悬浮操作按钮组...
}
}
}
参考资源:
- HarmonyOS SDK 组件声明:
ets/component/water_flow.d.ts- ArkUI Kit 声明:
ets/kits/@kit.ArkUI.d.ts- 鸿蒙开发者文档:WaterFlow 组件参考
更多推荐

所有评论(0)