HarmonyOS 高性能瀑布流:WaterFlow + LazyForEach 实现不等高多列布局

前言

电商、图文、笔记类应用里最常见的布局就是"瀑布流"——多列、每列高度不一、向下无限滚动。HarmonyOS 早期的 List 组件是单行/单列等高的,做不了瀑布流。系统提供了专门的 WaterFlow 容器配合 FlowItem,再叠加 LazyForEach 懒加载,就能写出既好看又不卡的长列表。本文从一次"瀑布流滑动掉帧 + 内存暴涨"的排查入手,给出可运行的完整实现。

问题描述

实际开发常踩的坑:

  1. ForEach 一次性渲染 500 张图片,WaterFlow 首屏直接卡死,内存瞬间上百 MB。
  2. 列数写死 2,平板上是窄瀑布、手机上又太挤,没有按屏幕宽度自适应。
  3. 图片高度在 FlowItem 里用 aspectRatio 固定,但数据源是"宽度固定、高度随机",导致图片被拉伸变形。
  4. 滚动到底不触发加载更多,或触发太频繁(每滚动一次就请求一次)。

本质问题:瀑布流既要懒加载(只渲染可视区),又要按"每列独立高度"布局,还要在合适的时机预加载下一页。

细节解析

1. WaterFlow 的基本结构

WaterFlow 直接放 FlowItem 子组件,通过 columnsTemplate'1fr 1fr' 等)控制列数与列宽比例,itemConstraintSize 控制单卡片约束。布局引擎会自动把每个 FlowItem 放进"当前最矮的那一列"。

2. 必须配 LazyForEach

WaterFlow 本身不负责懒加载,懒加载靠 LazyForEach + IDataSource。只用 ForEach 会全量创建节点。注意:LazyForEachkeyGenerator 必须稳定唯一,否则滚动复用会错乱。

3. 自适应列数

display.isFoldable() / 媒体查询或 window 宽度算列数:手机 2 列,平板 3–4 列。columnsTemplateArray(columns).fill('1fr').join(' ') 动态拼。

4. 预加载与加载更多

WaterFlowonReachEnd 在滑到最后触发;配合 LazyForEachgetData 拉下一页并设置 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
})

总结

  1. WaterFlow + LazyForEach 是标配:只用 ForEach 会全量建节点,长列表必卡;LazyForEach 只渲染可视区。
  2. keyGenerator 必须稳定唯一:否则滚动复用节点会串数据,这是瀑布流最常见的"图错位"根因。
  3. 高度交给数据源:卡片用真实 height + objectFit(Cover),不要靠 aspectRatio 硬拉,避免拉伸变形。
  4. 列数按屏宽自适应:手机 2 列、平板 3 列,columnsTemplate 动态拼 '1fr'
  5. 加载更多要节流onReachEnd + isLoading 锁,用 notifyDataAdd 局部追加,不要 reload 全量刷新,避免滚动跳动。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐