news 2026/9/26 4:14:26

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

作者头像

张小明

前端开发工程师

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

大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~

本文目录:

    • 前言
    • 一、为什么这里不直接用 Grid
    • 二、先把几个官方规则弄清楚
    • 三、先构造真正“不等高”的数据
    • 四、给 LazyForEach 准备数据源
    • 五、实现基础 WaterFlow 图片墙
    • 六、滚动到底部加载下一页
    • 七、图片加载完成后再改变高度,为什么要谨慎
    • 八、LazyForEach 只是第一层优化
    • 九、几个容易理解错的地方
    • 十、实际项目可以按这个顺序排查
    • 开发经验总结

前言

图片社区、商品推荐、内容卡片这类页面有一个共同特点:每个条目的高度并不固定。图片比例不同、标题行数不同、附加信息不同,如果仍然把所有内容塞进规则网格,就很容易出现大量留白,或者不得不提前把每个条目的高度“修”成一致。

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,数据却全部一样高。那样只能证明“它能排列”,看不出瀑布流真正解决的问题。

这里给每条数据保存一个图片宽高比:

interfacePhotoItem{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

页面中按页构造数据:

privatebuildPage(page:number,count:number):PhotoItem[]{constratios:number[]=[0.72,1.0,1.28,0.82,1.45,0.92];constimages:Resource[]=[$r('app.media.photo_1'),$r('app.media.photo_2'),$r('app.media.photo_3')];letresult:PhotoItem[]=[];for(leti=0;i<count;i++){constindex=page*count+i;result.push({id:`photo-${index}`,title:`图片内容${index+1}`,image:images[index%images.length],ratio:ratios[index%ratios.length]});}returnresult;}

这里没有用随机高度,而是使用固定比例序列。这样每次打开页面得到的布局一致,更适合观察分页、重排和性能问题。

四、给 LazyForEach 准备数据源

如果直接用ForEach展示几百甚至几千条数据,所有条目的组件创建策略并不是我们想要的长列表方案。对于大量可滚动内容,应把数据按需渲染纳入设计。华为当前长列表及网格文档也持续强调懒加载、缓存、组件复用等优化方向。

下面实现一个最小IDataSource:

classPhotoDataSourceimplementsIDataSource{privatedata:PhotoItem[]=[];privatelisteners:DataChangeListener[]=[];totalCount():number{returnthis.data.length;}getData(index:number):PhotoItem{returnthis.data[index];}registerDataChangeListener(listener:DataChangeListener):void{if(this.listeners.indexOf(listener)<0){this.listeners.push(listener);}}unregisterDataChangeListener(listener:DataChangeListener):void{constindex=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@Componentstruct WaterFlowPage{privatedataSource:PhotoDataSource=newPhotoDataSource();privatepage:number=0;privatereadonlypageSize:number=20;privateisLoading:boolean=false;aboutToAppear():void{this.dataSource.append(this.buildPage(this.page,this.pageSize));}privatebuildPage(page:number,count:number):PhotoItem[]{constratios:number[]=[0.72,1.0,1.28,0.82,1.45,0.92];constimages:Resource[]=[$r('app.media.photo_1'),$r('app.media.photo_2'),$r('app.media.photo_3')];letresult:PhotoItem[]=[];for(leti=0;i<count;i++){constindex=page*count+i;result.push({id:`photo-${index}`,title:`图片内容${index+1}`,image:images[index%images.length],ratio:ratios[index%ratios.length]});}returnresult;}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++;constnextPage=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的主轴尺寸发生变化,布局需要重新计算。大量网络图片在不同时间完成加载时,就可能形成连续的尺寸变化。

所以对于图片墙、商品流这类服务端通常已经掌握素材尺寸的场景,更稳妥的方案是:数据接口同时返回原图宽高,在图片真正下载之前就确定比例。

例如服务端返回:

interfaceNetworkPhoto{id:string;url:string;imageWidth:number;imageHeight:number;}

页面计算:

constratio: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 重排阶段处理。

如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 4:14:23

星阅书城接口自动化测试实战揭秘

1. 测试概述1.1 测试背景星阅书城平台的登录、用户管理、商品与订单等接口是业务主链路&#xff1a;用户必须先登录拿到Token凭证&#xff0c;才能新增、修改用户&#xff0c;下单链路则要按 "商品列表 → 商品详情 → 提交订单 → 订单支付 → 校验订单状态" 的顺序…

作者头像 李华
网站建设 2026/9/26 4:14:05

终端的庖丁解牛

根因 早期计算机是大型主机&#xff0c;主机本体放在机房&#xff0c;运算、存储全部由主机完成。操作人员不可能趴在主机上操作&#xff0c;于是就造出终端&#xff1a;本身没有算力&#xff0c;只负责接收人的输入、展示主机返回的输出&#xff0c;通过线缆连接远端主机。 到…

作者头像 李华
网站建设 2026/9/26 4:13:33

基于STM32单片机太阳能手机充电宝锂电池电量电压电流蓝牙无线APP/WiFi无线APP/摄像头视频监控/云平台设计S526

STM32-S526-太阳能充电宝USB输出输出电压电流功率锂电池电压电量欠压OLED屏声光提醒按键(无线方式选择)产品功能描述&#xff1a;本系统由STM32F103C8T6单片机核心板、OLED屏、&#xff08;无线蓝牙/无线WIFI/无线视频监控/联网云平台模块-可选&#xff09;、太阳能接口、锂电池…

作者头像 李华
网站建设 2026/9/26 4:13:30

SAP HANA SQLScript Watchpoints 深度解析,从变量监视到条件触发,掌握数据库调试器的精准定位能力

我们的 SAP S/4HANA 系统正在执行一批销售订单的价格计算。绝大多数订单的金额都正确,只有少数订单在经过折扣、税费和币种换算之后,最终金额与业务预期不一致。 负责价格计算的数据库存储过程并不复杂,里面包含订单明细查询、折扣规则匹配、金额汇总和税费计算等逻辑。不过…

作者头像 李华
网站建设 2026/9/26 4:12:02

WorkBuddy数据与隐私设置全指南:工作区授权、缓存迁移与记忆管理

1. 为什么数据与隐私设置值得单独拎出来讲很多人上手 WorkBuddy 的时候&#xff0c;注意力全在"怎么让它帮我干活"上——写代码、抓数据、生成网站、跑自动化工作流&#xff0c;恨不得第一天就把所有 Skill 都装一遍。结果用了两三周&#xff0c;突然发现工作台里堆了…

作者头像 李华