瀑布流(WaterFlow)
瀑布流常用于展示图片信息,尤其在购物和资讯类应用中。ArkUI提供了WaterFlow容器组件,用于构建瀑布流布局。WaterFlow组件支持条件渲染、循环渲染和懒加载等方式生成子组件。
瀑布流支持横向和纵向布局。
- 在纵向布局中,可以通过
columnsTemplate设置列数。 - 在横向布局中,可以通过
rowsTemplate设置行数。
在瀑布流的纵向布局中,第一行的子节点按从左到右顺序排列,从第二行开始,每个子节点将放置在当前总高度最小的列。如果多个列的总高度相同,则按照从左到右的顺序填充。如下图:

在瀑布流的横向布局中,每个子节点都会放置在当前总宽度最小的行。若多行总宽度相同,则按照从上到下的顺序进行填充。

基本使用
瀑布流常用于无限滚动的信息流。可以在瀑布流组件到达末尾位置时触发的onReachEnd事件回调,配合LazyForEach增加新数据,并将footer做成正在加载新数据的样式。
如下图所示

接下来,按照以下步骤实现上图的效果。
准备数据源
需要使用LazyForEach渲染子组件时,数据源必须是IDataSource的实现类。创建WaterFlowDataSource.ets,用于给WaterFlow瀑布流组件加载数据。
在构造函数中初始化100条数据,并提供获取数据、修改数据、添加数据、删除数据、获取数据总数据等函数。
ts
// WaterFlowDataSource.ets
// 实现IDataSource接口的对象,用于瀑布流组件加载数据
export class WaterFlowDataSource implements IDataSource {
private dataArray: number[] = [];
private listeners: DataChangeListener[] = [];
constructor() {
for (let i = 0; i < 100; i++) {
this.dataArray.push(i);
}
}
// 获取索引对应的数据
public getData(index: number): number {
return this.dataArray[index];
}
// 通知控制器数据重新加载
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded();
})
}
// 通知控制器数据增加
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index);
})
}
// 通知控制器数据变化
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index);
})
}
// 通知控制器数据删除
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index);
})
}
// 通知控制器数据位置变化
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to);
})
}
//通知控制器数据批量修改
notifyDatasetChange(operations: DataOperation[]): void {
this.listeners.forEach(listener => {
listener.onDatasetChange(operations);
})
}
// 获取数据总数
public 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 add1stItem(): void {
this.dataArray.splice(0, 0, this.dataArray.length);
this.notifyDataAdd(0);
}
// 在数据尾部增加一个元素
public addLastItem(): void {
this.dataArray.splice(this.dataArray.length, 0, this.dataArray.length);
this.notifyDataAdd(this.dataArray.length - 1);
}
// 在指定索引位置增加一个元素
public addItem(index: number): void {
this.dataArray.splice(index, 0, this.dataArray.length);
this.notifyDataAdd(index);
}
// 删除第一个元素
public delete1stItem(): void {
this.dataArray.splice(0, 1);
this.notifyDataDelete(0);
}
// 删除第二个元素
public delete2ndItem(): void {
this.dataArray.splice(1, 1);
this.notifyDataDelete(1);
}
// 删除最后一个元素
public deleteLastItem(): void {
this.dataArray.splice(-1, 1);
this.notifyDataDelete(this.dataArray.length);
}
// 在指定索引位置删除一个元素
public deleteItem(index: number): void {
this.dataArray.splice(index, 1);
this.notifyDataDelete(index);
}
// 重新加载数据
public reload(): void {
this.dataArray.splice(1, 1);
this.dataArray.splice(3, 2);
this.notifyDataReload();
}
// 在数据尾部增加count个元素
public addNewItems(count: number): void {
let len = this.dataArray.length;
for (let i = 0; i < count; i++) {
this.dataArray.push(this.dataArray[len - 1] + i + 1);
this.notifyDataAdd(this.dataArray.length - 1);
}
}
// 刷新所有元素
public refreshItems(): void {
let newDataArray: number[] = [];
for (let i = 0; i < 100; i++) {
newDataArray.push(this.dataArray[0] + i + 1000);
}
this.dataArray = newDataArray;
this.notifyDataReload();
}
}
使用LazyForEach循环渲染
为了使WaterFlow的每一个FlowItem尺寸不同,以达到交错的效果,采用随机数生成[80,180)的宽高并使用itemWidthArray和itemHeightArray保存。
具体代码如下
ts
import { WaterFlowDataSource } from '../datasource/WaterFlowDataScource';
@Entry
@Component
struct Index {
//瀑布流的数据源
private dataSource: WaterFlowDataSource = new WaterFlowDataSource();
//每一个FlowItem的高度
private itemWidthArray: number[] = [];
private itemHeightArray: number[] = [];
// 计算FlowItem宽/高
getSize() {
let ret = Math.floor(Math.random() * 180);
return (ret > 80 ? ret : 80);
}
// 设置FlowItem的宽/高数组
setItemSizeArray() {
for (let i = 0; i < 100; i++) {
this.itemWidthArray.push(this.getSize());
this.itemHeightArray.push(this.getSize());
}
}
aboutToAppear() {
this.setItemSizeArray();
}
build() {
Column() {
WaterFlow() {
LazyForEach(this.dataSource, (item: number, index: number) => {
FlowItem() {
Column() {
Text(`${item}`).fontSize(20).fontWeight(FontWeight.Bold)
}.width("100%")
.height("100%")
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.White)
}
.width('100%')
.height(this.itemHeightArray[index])
})
}.columnsTemplate("1fr 1fr")
.columnsGap(10)
.rowsGap(10)
.padding(10)
}.width("100%")
.height("100%")
.backgroundColor("#9ACEED")
}
}
上拉加载
添加尾部组件
在创建WaterFlow时,通过footer参数设置尾部组件,当上拉到底部时显示。
由于加载数据需要时间,在加载时需要给用户视觉上的反馈。我们给加载尾部组件定义2种状态Lading和End表示加载中和到底了。

ts
// Index.ets
import { WaterFlowDataSource } from './WaterFlowDataSource';
//尾部组件状态
enum FooterState {
Loading = 0,
End = 1
}
//尾部组件
@Builder
itemFooter() {
// 不要直接用IfElse节点作为footer的根节点。
Column() {
if (this.footerState == FooterState.Loading) {
Text(`加载中...`)
.fontSize(10)
.backgroundColor(Color.Red)
.width(50)
.height(50)
.align(Alignment.Center)
.margin({ top: 2 })
} else if (this.footerState == FooterState.End) {
Text(`到底啦...`)
.fontSize(10)
.backgroundColor(Color.Red)
.width(50)
.height(50)
.align(Alignment.Center)
.margin({ top: 2 })
} else {
Text(`Footer`)
.fontSize(10)
.backgroundColor(Color.Red)
.width(50)
.height(50)
.align(Alignment.Center)
.margin({ top: 2 })
}
}
}
将itemFooter绑定个WaterFlow
ts
WaterFlow({
footer: this.itemFooter()
}){
//...
}
添加尾部监听
给WaterFlow设置尾部监听的回调,onReachEnd(event: () => void) 瀑布流内容到达末尾位置时触发。
ts
WaterFlow({
footer: this.itemFooter()
}){
//...
}
// 触底加载数据
.onReachEnd(() => {
//每次到到底部,检查是否还有数据可加载(这里模拟到达200条数据时,无数据可加载)
if (this.dataSource.totalCount() >= 200) {
this.footerState = FooterState.End
return
}
//2s后,添加100条数据
setTimeout(() => {
for (let i = 0; i < 100; i++) {
this.dataSource.addLastItem()
}
}, 2000)
})
此时测试滑到底部时,加载100条数据,2秒后更新列表数据。
提前加载数据
虽然在onReachEnd()触发时加载数据可以实现无限加载,但在滑动到底部会出现明显的停顿。
为了实现更加流畅的无限滑动,需要调整增加新数据的时机。比如可以在LazyForEach还剩余若干个数据未遍历的情况下提前加载新数据。
如下图所示,在触底前20条时开始加载数据

代码如下
ts
WaterFlow({
footer: this.itemFooter()
}){
//...
}
//提前20条加载数据
.onScrollIndex((first: number, last: number) => {
if (last + 20 >= this.dataSource.totalCount()) {
setTimeout(() => {
this.dataSource.addNewItems(100);
}, 1000);
}
})
对鸿蒙感兴趣的同学,免费考取鸿蒙开发者认证