本文同步发表于我的微信公众号,微信搜索 程语新视界 即可关注,每个工作日都有文章更新

瀑布流布局是现代移动应用中常见的UI设计模式,尤其在电商、图库、社交媒体等场景中广泛应用。鸿蒙系统通过WaterFlow组件提供了强大的瀑布流实现能力。本文将全面讲解鸿蒙开发中瀑布流的使用方法,包括核心API、常用方法、性能优化技巧。

一、瀑布流概念

1.1 瀑布流布局原理

瀑布流(WaterFlow)是一种非对称的网格布局,其核心特点是:

动态高度:每个子元素(FlowItem)根据内容自动调整高度智能填充:新元素会自动填充到当前高度最小的列(纵向布局)或行(横向布局)视觉层次:形成错落有致的视觉效果,提升浏览体验

在纵向布局中,第一行的子节点按从左到右顺序排列,从第二行开始,每个子节点将放置在当前总高度最小的列。如果多个列的总高度相同,则按照从左到右的顺序填充。

1.2 WaterFlow组件简介

WaterFlow是鸿蒙ArkUI提供的瀑布流容器组件,主要特点包括:

支持纵向和横向两种布局方向支持条件渲染、循环渲染和懒加载提供丰富的布局控制API内置滚动和触底加载功能从API Version 9开始支持

1.3 基本组成元素

一个完整的瀑布流实现通常包含以下部分:

WaterFlow容器:承载所有子组件FlowItem子组件:瀑布流中的每一项内容数据源:通常实现IDataSource接口懒加载组件:LazyForEach优化性能布局参数:控制列数、间距等

二、WaterFlow核心API详解

2.1 组件构造方法

WaterFlow组件提供两种构造方式:

// 基础构造

WaterFlow()

// 带参数的构造

WaterFlow(options?: {footer?: CustomBuilder, scroller?: Scroller})

参数说明:

footer: 可选的底部构建器,常用于显示"加载更多"状态 scroller: 可滚动组件的控制器,目前仅支持scrollToIndex接口

示例:

WaterFlow({footer: this.itemFoot, scroller: this.scroller}) {

// 子组件

}

2.2 布局控制属性

2.2.1 列/行模板

columnsTemplate: 设置列数和列宽比例(纵向布局时有效)

.columnsTemplate('1fr 1fr 1fr') // 三等分

.columnsTemplate('1fr 2fr') // 第一列占1/3,第二列占2/3 rowsTemplate: 设置行数和行高比例(横向布局时有效)

.rowsTemplate('1fr 1fr 1fr') // 三行等高

支持auto-fill关键字自动填充可用空间:

.columnsTemplate('repeat(auto-fill, 100vp)') // 自动填充100vp宽的列

2.2.2 间距控制

columnsGap: 列间距,默认0

.columnsGap(10) // 10vp列间距 rowsGap: 行间距,默认0

.rowsGap(8) // 8vp行间距

完整示例:

WaterFlow()

.columnsTemplate('1fr 1fr')

.columnsGap(10)

.rowsGap(8)

.margin({left: 15, right: 15})

2.2.3 布局方向

layoutDirection控制主轴方向,影响columnsTemplate/rowsTemplate的生效情况:

.layoutDirection(FlexDirection.Column) // 纵向布局(默认)

.layoutDirection(FlexDirection.Row) // 横向布局

注意:

纵向布局时columnsTemplate有效横向布局时rowsTemplate有效未设置时默认为FlexDirection.Column

2.2.4 尺寸约束

itemConstraintSize可设置子组件的约束尺寸:

.itemConstraintSize({

minWidth: 0,

maxWidth: '100%',

minHeight: 0,

maxHeight: '100%'

})

2.3 事件API

2.3.1 触底事件

onReachEnd在滚动到底部时触发,常用于实现无限滚动:

.onReachEnd(() => {

console.info('onReached')

setTimeout(() => {

for(let i = 0; i < 40; i++){

this.dataSource.addLastItem()

}

}, 1000)

})

2.3.2 触顶事件

onReachStart在滚动到顶部时触发:

.onReachStart(() => {

console.info("onReachStart")

})

三、数据管理与性能优化

3.1 LazyForEach懒加载

LazyForEach是性能优化的关键,它按需创建组件而非一次性加载全部:

LazyForEach(

dataSource: IDataSource, // 数据源

itemGenerator: (item: any, index: number) => void, // 子组件生成函数

keyGenerator?: (item: any, index: number) => string // 键值生成函数

)

关键特性:

只在可视区域加载组件必须实现IDataSource接口每次迭代只能创建一个子组件需要提供唯一键值生成器

完整示例:

LazyForEach(this.dataSource, (item: Item, index) => {

FlowItem(){

Column(){

Text('第' + `${item.index.toString()}` + '个')

Image(item.image)

.objectFit(ImageFit.Fill)

}

.backgroundColor('#ffa4eaac')

.borderRadius(8)

}

}, (item: Item) => item.index.toString())

3.2 IDataSource接口实现

自定义数据源需要实现IDataSource接口:

export class WaterFlowDataSource implements IDataSource {

private dataArray: Item[] = []

private listeners: DataChangeListener[] = []

// 获取数据项

getData(index: number): Item {

return this.dataArray[index]

}

// 返回数据总数

totalCount(): number {

return this.dataArray.length

}

// 注册数据变化监听器

registerDataChangeListener(listener: DataChangeListener): void {

if(this.listeners.indexOf(listener) < 0){

this.listeners.push(listener)

}

}

// 注销数据变化监听器

unregisterDataChangeListener(listener: DataChangeListener): void {

const pos = this.listeners.indexOf(listener)

if(pos >= 0){

this.listeners.splice(pos, 1)

}

}

// 添加新项

public addLastItem(): void {

let newItem = this.createNewItem(this.dataArray.length)

this.dataArray.push(newItem)

this.notifyDataAdd(this.dataArray.length - 1)

}

// 创建新项

private createNewItem(index: number): Item {

let randomNumber = getRandomInt(1,6)

return new Item($r(`app.media.img_gril_${randomNumber}`), index+1)

}

// 通知监听器数据变化

private notifyDataAdd(index: number): void {

this.listeners.forEach(listener => {

listener.onDataAdd(index)

})

}

}

3.3 无限滚动实现

结合onReachEnd和动态数据加载实现无限滚动:

// 1. 定义footer组件

@Builder itemFoot() {

Row() {

LoadingProgress()

.color(Color.Blue).height(50).aspectRatio(1).width('20%')

Text('正在加载')

.fontSize(20)

.width('30%')

.height(50)

.align(Alignment.Center)

.margin({top: 2})

}

.width('100%')

.justifyContent(FlexAlign.Center)

}

// 2. 在build方法中使用

build() {

Column() {

WaterFlow({footer: this.itemFoot, scroller: this.scroller}) {

LazyForEach(this.dataSource, (item: number) => {

FlowItem() {

// 内容

}

}, (item: string) => item)

}

.onReachEnd(() => {

setTimeout(() => {

this.dataSource.addNewItems(10); // 加载10个新项

}, 1000);

})

}

}

// 3. 数据源中添加新项的方法

public addNewItems(count: number): void {

let len = this.dataArray.length;

for(let i = 0; i < count; i++) {

this.dataArray.push(this.dataArray.length);

}

this.listeners.forEach(listener => {

listener.onDatasetChange([{

type: DataOperationType.ADD,

index: len,

count: count

}]);

})

}

四、实战案例

4.1 瀑布流

完整实现一个电商商品列表,包含图片和标题:

// 商品接口定义

export interface GoodsItem {

title: string

imageUrl: string

}

// mock数据

export const mockGoodsList: GoodsItem[] = [

{

title: '宁雨昔美式复古字母三条杠圆领短袖T恤女纯棉宽松2024夏季新款学生上衣 白色 M',

imageUrl: 'https://img10.360buyimg.com/n2/s240x240_jfs/t1/145307/22/41197/71267/65b5d932Fb67b2c27/cd986ae610999146.jpg!q70.jpg.webp'

},

// 更多商品...

]

// 主页面实现

@Entry

@Component

struct WaterFlowGoodsPage {

@State goodsList: GoodsItem[] = mockGoodsList

@State isLoadMore: boolean = false

@Builder

getGoodsItemView(item: GoodsItem, index: number) {

Column({ space: 5 }) {

Image(item.imageUrl)

.height(index % 2 ? 120 : 180) // 交错高度

.borderRadius(8)

Text(item.title)

.fontSize(14)

.lineHeight(22)

.maxLines(3)

.textOverflow({ overflow: TextOverflow.Ellipsis })

}

}

@Builder

getFooter() {

Row() {

Text('加载更多...')

}

.justifyContent(FlexAlign.Center)

.backgroundColor(Color.Pink)

.height(60)

.width('100%')

}

build() {

WaterFlow({ footer: this.getFooter }) {

ForEach(this.goodsList, (item: GoodsItem, index: number) => {

FlowItem() {

this.getGoodsItemView(item, index)

}

})

}

.height('100%')

.columnsTemplate('1fr 1fr')

.columnsGap(10)

.rowsGap(10)

.padding(10)

.onReachEnd(async () => {

if (!this.isLoadMore) {

try {

this.isLoadMore = true

await this.loadMore()

this.isLoadMore = false

} catch (error) {

promptAction.showToast({ message: JSON.stringify(error) })

}

}

})

}

loadMore() {

return new Promise((resolve) => {

setTimeout(() => {

this.goodsList.push(...this.goodsList.slice(0, 5)) // 追加5个商品

resolve(true)

}, 2000)

})

}

}

4.2 图片瀑布流画廊

实现一个图片瀑布流,包含随机高度和点击预览:

@Entry

@Component

struct ImageWaterFlow {

private scroller: Scroller = new Scroller()

private dataSource: WaterFlowDataSource = new WaterFlowDataSource()

@Builder

itemFoot() {

Row() {

LoadingProgress()

Text('加载更多图片...')

}

.width('100%')

.justifyContent(FlexAlign.Center)

}

build() {

Column() {

WaterFlow({ footer: this.itemFoot, scroller: this.scroller }) {

LazyForEach(this.dataSource, (item: Item, index) => {

FlowItem() {

Column() {

Text('第' + `${index + 1}` + '张')

Image(item.image)

.objectFit(ImageFit.Fill)

.sharedTransition(`sharedImage${index}`, {

duration: 300,

curve: Curve.Linear,

delay: 50

})

}

.backgroundColor('#ffa4eaac')

.borderRadius(8)

}

.onClick(() => {

router.pushUrl({

url: 'pages/waterflow/Preview',

params: { item: item, index: index }

})

})

})

}

.columnsTemplate('1fr 1fr 1fr')

.columnsGap(10)

.rowsGap(8)

.margin({left: 15, right: 15})

.onReachEnd(() => {

setTimeout(() => {

for(let i = 0; i < 10; i++) {

this.dataSource.addLastItem()

}

}, 1000)

})

}

.margin({top: 20})

}

}

// 数据源实现

export class WaterFlowDataSource implements IDataSource {

private dataArray: Item[] = []

private listeners: DataChangeListener[] = []

constructor() {

for(let i = 1; i <= 40; i++) {

let r = getRandomInt(1, 6)

this.dataArray.push(new Item($r(`app.media.img_gril_${r}`), i))

}

}

// ...其他必要接口方法实现

public addLastItem(): void {

let newItem = this.createNewItem(this.dataArray.length)

this.dataArray.push(newItem)

this.notifyDataAdd(this.dataArray.length - 1)

}

private createNewItem(index: number): Item {

let randomNumber = getRandomInt(1,6)

return new Item($r(`app.media.img_gril_${randomNumber}`), index+1)

}

}

五、高级技巧

5.1 性能优化策略

合理设置FlowItem尺寸

避免频繁测量布局对于已知尺寸的内容,明确设置宽高 FlowItem()

.width(this.itemWidthArray[item % 100])

.aspectRatio(this.itemHeightArray[item % 100] / this.itemWidthArray[item%100]) 使用缓存策略

对于网络图片,实现内存和磁盘缓存考虑使用ReusableFlowItem复用组件 虚拟化长列表

确保使用LazyForEach而非普通ForEach设置合理的keyGenerator提高复用率

LazyForEach(this.dataSource, (item) => {

// ...

}, (item) => item.id.toString()) // 唯一键值

5.2 动态布局调整

根据屏幕方向或尺寸变化调整布局:

@State columnsTemplate: string = '1fr 1fr' // 默认两列

// 监听屏幕变化

.onAreaChange((oldValue, newValue) => {

if(newValue.width > 600) { // 宽屏设备

this.columnsTemplate = '1fr 1fr 1fr' // 三列

} else {

this.columnsTemplate = '1fr 1fr' // 两列

}

})

// 应用动态模板

WaterFlow()

.columnsTemplate(this.columnsTemplate)

六、常见问题与解决方案

6.1 布局错乱问题

现象:图片加载后布局跳动或重叠

解决方案:

预计算或固定宽高比 .aspectRatio(1) // 1:1比例 使用占位图保持布局稳定实现图片加载完成回调后再显示

6.2 滚动卡顿问题

优化建议:

减少FlowItem内部组件复杂度避免在FlowItem中使用深层次嵌套对于复杂内容,考虑使用自定义绘制代替多个组件

6.3 内存占用过高

控制策略:

实现数据分页加载,不一次性加载所有数据监听滚动位置,释放不可见区域的资源对于大图,使用合适尺寸的缩略图