HarmonyOS WaterFlow 瀑布流图片墙:从数据、懒加载到分页与长列表优化【鸿蒙心迹】

大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~
本文目录:
前言
图片社区、商品推荐、内容卡片这类页面有一个共同特点:每个条目的高度并不固定。图片比例不同、标题行数不同、附加信息不同,如果仍然把所有内容塞进规则网格,就很容易出现大量留白,或者不得不提前把每个条目的高度“修”成一致。
ArkUI 提供的 WaterFlow 正是针对这类场景的瀑布流容器。本文不做复杂业务封装,而是围绕一个可以复现的图片墙,把数据构造、FlowItem、LazyForEach、触底分页、图片尺寸稳定和长列表性能几个问题串起来。
本文以 HarmonyOS 7 / API 26.0.0 为开发背景。华为当前升级适配文档明确说明,HarmonyOS 7.0 对应 API 26.0.0,并建议应用升级开发套件后检查 API 行为变化和兼容性。
一、为什么这里不直接用 Grid
Grid 很适合规则网格:例如九宫格入口、相册缩略图、固定规格商品卡片。它通过 rowsTemplate、columnsTemplate 等能力定义网格行列,也支持不规则 GridItem 等更复杂的网格布局。
但“支持不规则网格”不等于“所有不等高内容都应该用 Grid”。
图片流更常见的目标是:列宽基本一致,每个条目的主轴尺寸由内容决定,各列最终自然形成参差排列。 WaterFlow 的布局模型与这种需求更直接。华为当前的瀑布流最佳实践也把图片资讯、购物商品、直播视频等作为典型 WaterFlow 场景。
所以本文把场景缩小为:
两列图片内容流;图片拥有不同宽高比;数据按页增加;只按需创建当前需要显示的条目;滚动到底部继续追加下一页。
二、先把几个官方规则弄清楚
这次实践主要涉及 ArkUI 的 WaterFlow、FlowItem、LazyForEach 和 Image。
FlowItem 是 WaterFlow 的直接子组件。官方 API 参考明确说明,FlowItem 从 API version 9 开始支持,只能作为 WaterFlow 的子组件使用,而且一个 FlowItem 只支持一个直接子组件。
因此下面这种结构是本文的基础:
WaterFlow
├─ FlowItem
│ └─ Column
│ ├─ Image
│ └─ Text
├─ FlowItem
│ └─ Column
└─ ...
注意“一个直接子组件”的限制并不意味着一张卡片只能放一张图片。正确方式是在 FlowItem 里面放一个 Column、Stack 等容器,再由这个容器组织图片、标题和其他信息。
HarmonyOS 7 对应 API 26.0.0,而本文使用的基础 WaterFlow/FlowItem 能力并不是 HarmonyOS 7 才新增的能力。换句话说,不能把 WaterFlow 写成“HarmonyOS 7 新特性”;这里只是在 HarmonyOS 7 的开发环境下使用已经存在的 ArkUI 布局能力。
本文示例使用应用本地 media 图片,因此不需要为了 WaterFlow 本身增加权限,也没有额外的 module.json5 权限配置。如果实际项目改成网络图片,请再根据实际网络访问方案核对对应配置,不要把网络权限和 WaterFlow 布局能力混在一起。
三、先构造真正“不等高”的数据
瀑布流 Demo 最容易写偏的地方,是组件用了 WaterFlow,数据却全部一样高。那样只能证明“它能排列”,看不出瀑布流真正解决的问题。
这里给每条数据保存一个图片宽高比:
interface PhotoItem {
id: string;
title: string;
image: Resource;
ratio: number;
}
ratio 表示宽高比。比如 1.0 是正方形,0.75 会形成偏高的图片,1.4 则更偏横向。
准备至少三张放在 resources/base/media 下的本地图片,例如:
photo_1.jpg
photo_2.jpg
photo_3.jpg
页面中按页构造数据:
private buildPage(page: number, count: number): PhotoItem[] {
const ratios: number[] = [0.72, 1.0, 1.28, 0.82, 1.45, 0.92];
const images: Resource[] = [
$r('app.media.photo_1'),
$r('app.media.photo_2'),
$r('app.media.photo_3')
];
let result: PhotoItem[] = [];
for (let i = 0; i < count; i++) {
const index = page * count + i;
result.push({
id: `photo-${index}`,
title: `图片内容 ${index + 1}`,
image: images[index % images.length],
ratio: ratios[index % ratios.length]
});
}
return result;
}
这里没有用随机高度,而是使用固定比例序列。这样每次打开页面得到的布局一致,更适合观察分页、重排和性能问题。
四、给 LazyForEach 准备数据源
如果直接用 ForEach 展示几百甚至几千条数据,所有条目的组件创建策略并不是我们想要的长列表方案。对于大量可滚动内容,应把数据按需渲染纳入设计。华为当前长列表及网格文档也持续强调懒加载、缓存、组件复用等优化方向。
下面实现一个最小 IDataSource:
class PhotoDataSource implements IDataSource {
private data: PhotoItem[] = [];
private listeners: DataChangeListener[] = [];
totalCount(): number {
return this.data.length;
}
getData(index: number): PhotoItem {
return this.data[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const index = this.listeners.indexOf(listener);
if (index >= 0) {
this.listeners.splice(index, 1);
}
}
append(items: PhotoItem[]): void {
this.data.push(...items);
// 最小示例采用整体刷新通知,重点放在 WaterFlow 与分页流程。
this.listeners.forEach((listener: DataChangeListener) => {
listener.onDataReloaded();
});
}
}
LazyForEach 数据源的关键不只是保存数组,还要通过 DataChangeListener 告诉框架数据发生了变化。这里为了让示例集中在瀑布流本身,分页追加后使用 onDataReloaded() 通知刷新。
如果业务需要频繁插入、删除、交换条目,可以进一步针对具体变化发送更细粒度的数据变更通知,而不是所有变化都整体 reload。
五、实现基础 WaterFlow 图片墙
有了数据之后,页面主体其实很短:
@Entry
@Component
struct WaterFlowPage {
private dataSource: PhotoDataSource = new PhotoDataSource();
private page: number = 0;
private readonly pageSize: number = 20;
private isLoading: boolean = false;
aboutToAppear(): void {
this.dataSource.append(this.buildPage(this.page, this.pageSize));
}
private buildPage(page: number, count: number): PhotoItem[] {
const ratios: number[] = [0.72, 1.0, 1.28, 0.82, 1.45, 0.92];
const images: Resource[] = [
$r('app.media.photo_1'),
$r('app.media.photo_2'),
$r('app.media.photo_3')
];
let result: PhotoItem[] = [];
for (let i = 0; i < count; i++) {
const index = page * count + i;
result.push({
id: `photo-${index}`,
title: `图片内容 ${index + 1}`,
image: images[index % images.length],
ratio: ratios[index % ratios.length]
});
}
return result;
}
build() {
Column() {
Text('WaterFlow 图片墙')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.width('100%')
.padding(16)
WaterFlow() {
LazyForEach(
this.dataSource,
(item: PhotoItem) => {
FlowItem() {
Column() {
Image(item.image)
.width('100%')
.aspectRatio(item.ratio)
.objectFit(ImageFit.Cover)
Text(item.title)
.fontSize(14)
.width('100%')
.padding(10)
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.clip(true)
}
},
(item: PhotoItem) => item.id
)
}
.columnsTemplate('1fr 1fr')
.columnsGap(10)
.rowsGap(10)
.padding({ left: 12, right: 12, bottom: 12 })
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
真正需要关注的是三个地方。
第一,columnsTemplate('1fr 1fr') 把交叉轴分成两列;官方当前瀑布流实践和 WaterFlow FAQ 都展示了 columnsTemplate、rowsTemplate、间距等布局配置。
第二,每个 FlowItem 的高度没有统一写死,而是由内部图片和文本共同决定。
第三,LazyForEach 的 key 使用业务唯一的 id,不要直接拿不断变化的数组位置冒充稳定业务标识。
六、滚动到底部加载下一页
WaterFlow 可以通过 onReachEnd 处理到达内容末尾后的逻辑。华为 2026 年更新的 WaterFlow FAQ 直接给出了通过 onReachEnd 在触底时增加数据的示例;当前瀑布流最佳实践的“上拉加载”场景同样采用这一事件。
在前面的 WaterFlow 后继续添加:
.onReachEnd(() => {
if (this.isLoading) {
return;
}
this.isLoading = true;
this.page++;
const nextPage = this.buildPage(this.page, this.pageSize);
this.dataSource.append(nextPage);
this.isLoading = false;
})
这里的 isLoading 很重要。触底事件只是“加载下一页”的触发点,不应该等价于“无条件发起一次请求”。
真实项目通常是:
onReachEnd
↓
检查 loading / hasMore
↓
请求下一页
↓
成功:追加数据
失败:保留当前数据并恢复状态
↓
更新 loading / hasMore
本文没有伪造网络接口,所以示例直接构造下一页本地数据。迁移到真实业务时,只需要把 buildPage() 换成自己的分页数据请求,WaterFlow 布局部分不需要跟着改。
七、图片加载完成后再改变高度,为什么要谨慎
图片瀑布流还有一个经常被忽略的问题:布局第一次测量时不知道图片最终高度怎么办?
一种直觉写法是先给图片一个临时高度,等 Image.onComplete 后再根据图片结果修改高度。ArkUI 的 Image 确实提供图片加载完成事件,华为当前 FAQ 中也有 Image(...).onComplete(...) 的实际用法。
但对于 WaterFlow,这意味着图片完成加载后,FlowItem 的主轴尺寸发生变化,布局需要重新计算。大量网络图片在不同时间完成加载时,就可能形成连续的尺寸变化。
所以对于图片墙、商品流这类服务端通常已经掌握素材尺寸的场景,更稳妥的方案是:数据接口同时返回原图宽高,在图片真正下载之前就确定比例。
例如服务端返回:
interface NetworkPhoto {
id: string;
url: string;
imageWidth: number;
imageHeight: number;
}
页面计算:
const ratio: number = item.imageWidth / item.imageHeight;
再直接:
Image(item.url)
.width('100%')
.aspectRatio(ratio)
.objectFit(ImageFit.Cover)
这样图片从占位状态变成真实内容时,外层卡片的几何尺寸可以保持稳定。
如果确实只能在加载完成后才能获得业务所需信息,onComplete 可以处理加载状态,但建议把“图片加载完成”和“必须修改整个 FlowItem 高度”分开考虑。例如仅切换占位视觉状态:
Image(this.item.image)
.width('100%')
.aspectRatio(this.item.ratio)
.objectFit(ImageFit.Cover)
.onComplete(() => {
this.loaded = true;
})
核心思路不是禁止动态高度,而是尽可能避免大量图片完成加载时才第一次确定布局高度。
八、LazyForEach 只是第一层优化
把 ForEach 换成 LazyForEach 后,并不意味着长列表性能问题已经结束。
图片瀑布流的成本至少还有几类:图片解码和资源加载、FlowItem 测量、复杂卡片组件创建、快速滚动时的新节点准备,以及数据分页本身的耗时。
因此性能验证建议分成下面几个维度,而不是只看“页面能不能滑”:
| 检查项 | 重点观察 |
|---|---|
| 首屏 | 首次进入时是否集中创建过多组件 |
| 连续滚动 | 快速滑动时是否出现明显白块或卡顿 |
| 图片加载 | 图片完成加载是否频繁改变卡片几何尺寸 |
| 分页 | onReachEnd 是否重复触发业务请求 |
| 长时间滚动 | 数据持续增长后内存和组件创建是否异常 |
| 卡片复杂度 | FlowItem 内是否存在大量不必要的嵌套和计算 |
对于真正的大规模图片流,华为目前还提供了一条更进一步的官方最佳实践路线:ScrollComponents。
这里必须区分清楚:WaterFlow、FlowItem 是 ArkUI 系统组件;最佳实践文档中的 @hadss/scroll_components 则被官方文档明确称为三方库,它在系统 NodeAdapter、BuilderNode、FrameNode、Prefetcher 等能力之上封装了组件复用、预创建、预加载等方案。不能把 WaterFlowManager 写成 WaterFlow 自带的系统 API。
当前官方最佳实践还专门覆盖了瀑布流首屏、无限滑动、上拉加载、组件复用和资源预取等场景。对于图片很多、卡片复杂、快速滚动白块明显的页面,可以在完成基础 WaterFlow 实现后,再评估这套方案,而不是一开始就把最小 Demo 堆成复杂框架。
另外,HarmonyOS 当前的组件复用迁移文档已经给出了 WaterFlow 配合 Repeat(...).virtualScroll() 与 @ReusableV2 的示例。这说明面向新工程继续做性能深化时,也值得关注 V2 状态管理和新的虚拟滚动/复用路线,而不是把历史写法当成唯一方案。
九、几个容易理解错的地方
WaterFlow 不等于“随机高度”。 高度应该来自真实内容约束。Demo 可以构造不同高度,但业务里最好来自图片比例、文本内容或明确的数据模型。
FlowItem 不能随便塞多个直接子组件。 官方规定它只支持一个子组件,因此图片、标题、价格等内容应该先放进 Column、Stack 等容器。
LazyForEach 和分页是两件事。 LazyForEach 解决的是 UI 子组件按需生成;onReachEnd 分页解决的是业务数据什么时候继续增加。用了懒加载,并不会自动帮应用请求下一页。
触底回调里必须考虑防重入。 官方瀑布流上拉加载实践同样使用 isLoadMore 一类状态避免重复加载。
图片加载和布局高度最好解耦。 如果服务端能够提供原图宽高,优先在进入布局前算出 aspectRatio,而不是等图片下载结束再决定 FlowItem 到底有多高。
不要把 ScrollComponents 当成系统 WaterFlow API。 它是华为官方最佳实践文档介绍的三方库方案,底层利用系统节点和懒加载能力进一步处理复杂长列表性能问题。
十、实际项目可以按这个顺序排查
遇到“瀑布流错位、加载重复、越滑越卡”时,可以按一条比较固定的链路检查:先确认目标 HarmonyOS/API 版本以及项目 SDK 配置;再确认 WaterFlow → FlowItem → 单一直接子容器 的结构;检查每个 item 的高度是否有稳定来源;确认 LazyForEach 的 key 是否真正唯一;检查数据变化后 DataSource 是否正确通知;再看 onReachEnd 有没有 loading/hasMore 防护;随后观察图片加载是否导致大面积重新测量;如果基础实现已经正确但长列表仍有明显性能压力,再评估组件复用、预创建、资源预取或 ScrollComponents。
HarmonyOS 7 升级时还应通过 DevEco Studio 的 API Change Assistant 等工具检查所用 API 在 SDK 版本变化之间是否存在行为调整,并在目标新旧设备环境进行兼容性验证。华为的 26.0.0 升级指南对此给出了明确流程。
开发经验总结
WaterFlow 真正有价值的地方,并不是“能做出两列高低不一样的卡片”,而是它把不等高内容的布局模型直接表达出来了。
一个可维护的图片瀑布流,可以把问题拆成四层:数据层保存稳定 ID 和图片比例;WaterFlow 负责瀑布布局;LazyForEach 负责按需生成 UI;onReachEnd 只负责决定什么时候继续取数据。图片加载、分页请求和布局测量尽量不要互相绑死。
对于普通图片墙,这套基础结构已经足够清楚;当数据量、图片资源和卡片复杂度继续增加,再把关注点转到复用、预加载、预创建和资源预取。这样性能优化才是沿着实际瓶颈逐层增加,而不是一开始就把页面做得很重。
如果正在做商品流或内容流,可以重点检查一个问题:接口是否已经提供图片原始宽高? 如果答案是否定的,瀑布流页面很可能会把本可以在数据层解决的尺寸问题,拖到图片加载和 UI 重排阶段处理。
如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~
更多推荐




所有评论(0)