HarmonyOS 高性能瀑布流:WaterFlow + LazyForEach 实现不等高多列布局
HarmonyOS 高性能瀑布流:WaterFlow + LazyForEach 实现不等高多列布局
前言
电商、图文、笔记类应用里最常见的布局就是"瀑布流"——多列、每列高度不一、向下无限滚动。HarmonyOS 早期的 List 组件是单行/单列等高的,做不了瀑布流。系统提供了专门的 WaterFlow 容器配合 FlowItem,再叠加 LazyForEach 懒加载,就能写出既好看又不卡的长列表。本文从一次"瀑布流滑动掉帧 + 内存暴涨"的排查入手,给出可运行的完整实现。
问题描述
实际开发常踩的坑:
- 用
ForEach一次性渲染 500 张图片,WaterFlow首屏直接卡死,内存瞬间上百 MB。 - 列数写死 2,平板上是窄瀑布、手机上又太挤,没有按屏幕宽度自适应。
- 图片高度在
FlowItem里用aspectRatio固定,但数据源是"宽度固定、高度随机",导致图片被拉伸变形。 - 滚动到底不触发加载更多,或触发太频繁(每滚动一次就请求一次)。
本质问题:瀑布流既要懒加载(只渲染可视区),又要按"每列独立高度"布局,还要在合适的时机预加载下一页。
细节解析
1. WaterFlow 的基本结构
WaterFlow 直接放 FlowItem 子组件,通过 columnsTemplate('1fr 1fr' 等)控制列数与列宽比例,itemConstraintSize 控制单卡片约束。布局引擎会自动把每个 FlowItem 放进"当前最矮的那一列"。
2. 必须配 LazyForEach
WaterFlow 本身不负责懒加载,懒加载靠 LazyForEach + IDataSource。只用 ForEach 会全量创建节点。注意:LazyForEach 的 keyGenerator 必须稳定唯一,否则滚动复用会错乱。
3. 自适应列数
用 display.isFoldable() / 媒体查询或 window 宽度算列数:手机 2 列,平板 3–4 列。columnsTemplate 用 Array(columns).fill('1fr').join(' ') 动态拼。
4. 预加载与加载更多
WaterFlow 的 onReachEnd 在滑到最后触发;配合 LazyForEach 的 getData 拉下一页并设置 notifyDataReload/addData。为避免抖动,建议在 onReachEnd 里节流(isLoading 锁)。
示例代码
数据源(实现 IDataSource)
// src/main/ets/model/Photo.ets
export class Photo {
id: string
url: string
width: number
height: number
constructor(id: string, url: string, w: number, h: number) {
this.id = id; this.url = url; this.width = w; this.height = h
}
}
// 模拟分页数据源
export class PhotoDataSource implements IDataSource {
private list: Photo[] = []
private listeners: DataChangeListener[] = []
totalCount(): number { return this.list.length }
getData(index: number): Photo { return this.list[index] }
registerDataListener(l: DataChangeListener): void { this.listeners.push(l) }
unregisterDataListener(l: DataChangeListener): void {
const i = this.listeners.indexOf(l); if (i >= 0) this.listeners.splice(i, 1)
}
// 追加一页(通知监听器局部刷新,而非整体 reload)
appendPage(items: Photo[]) {
const start = this.list.length
this.list.push(...items)
this.listeners.forEach(l => l.onDatasetChange([
DataOperation.notifyDataAdd(start, items.length)
]))
}
}
页面(WaterFlow + LazyForEach + 自适应列 + 加载更多)
// src/main/ets/pages/WaterFlowPage.ets
import { Photo, PhotoDataSource } from '../model/Photo'
@Entry
@Component
struct WaterFlowPage {
@State data: PhotoDataSource = new PhotoDataSource()
@State columns: number = 2
private isLoading: boolean = false
aboutToAppear(): void {
// 按屏幕宽度决定列数:宽度>600vp 用 3 列,否则 2 列
const w = vp2px ? 0 : 0 // 占位,实际用 window
this.loadPage()
}
private buildColumnsTemplate(): string {
return Array(this.columns).fill('1fr').join(' ')
}
// 模拟拉取一页
private loadPage() {
if (this.isLoading) return
this.isLoading = true
// 真实项目里这里调 http 请求
const batch: Photo[] = []
const base = this.data.totalCount()
for (let i = 0; i < 20; i++) {
const h = 120 + Math.floor(Math.random() * 200) // 随机高度模拟不等高
batch.push(new Photo(`p${base + i}`, `https://picsum.photos/300/${h}`, 300, h))
}
setTimeout(() => {
this.data.appendPage(batch)
this.isLoading = false
}, 300)
}
build() {
Column() {
WaterFlow({ columnsTemplate: this.buildColumnsTemplate(), itemConstraintSize: { minHeight: 80 } }) {
LazyForEach(this.data, (item: Photo) => {
FlowItem() {
Column() {
// 用固定宽度 + 真实高度,避免拉伸
Image(item.url)
.width('100%')
.height(item.height)
.objectFit(ImageFit.Cover)
.borderRadius(8)
Text(item.id)
.fontSize(12)
.margin({ top: 4 })
}
.backgroundColor('#F5F5F5')
.borderRadius(8)
.padding(6)
}
}, (item: Photo) => item.id) // keyGenerator:稳定唯一
}
.columnsGap(8)
.rowsGap(8)
.padding(8)
.onReachEnd(() => {
// 滑到底:拉下一页(isLoading 已节流)
console.info('[WaterFlow] 触底,加载更多')
this.loadPage()
})
.layoutWeight(1)
if (this.isLoading) {
Text('加载中...').fontSize(12).margin(8)
}
}
.width('100%').height('100%')
}
}
自适应列数(在 aboutToAppear 里算)
import { window } from '@kit.ArkUI'
// 在 aboutToAppear 中:
const ctx = getContext(this) as common.UIAbilityContext
window.getLastWindow(ctx).then(win => {
const wpx = win.getWindowProperties().windowRect.width
const wvp = px2vp(wpx)
this.columns = wvp > 600 ? 3 : 2
})
总结
WaterFlow+LazyForEach是标配:只用ForEach会全量建节点,长列表必卡;LazyForEach只渲染可视区。keyGenerator必须稳定唯一:否则滚动复用节点会串数据,这是瀑布流最常见的"图错位"根因。- 高度交给数据源:卡片用真实
height+objectFit(Cover),不要靠aspectRatio硬拉,避免拉伸变形。 - 列数按屏宽自适应:手机 2 列、平板 3 列,
columnsTemplate动态拼'1fr'。 - 加载更多要节流:
onReachEnd+isLoading锁,用notifyDataAdd局部追加,不要reload全量刷新,避免滚动跳动。
更多推荐


所有评论(0)