HarmonyOS应用开发之瀑布流、上拉加载、无限滚动一文搞定

瀑布流(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)的宽高并使用itemWidthArrayitemHeightArray保存。

具体代码如下

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种状态LadingEnd表示加载中和到底了。

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);
  }
})

对鸿蒙感兴趣的同学,免费考取鸿蒙开发者认证

相关推荐
用户5951433221772 小时前
鸿蒙应用开发之@Builder自定义构建函数:值传递与引用传递与UI更新
harmonyos
不爱吃糖的程序媛3 小时前
Flutter 开发的鸿蒙AtomGit OAuth 授权应用
华为·harmonyos
xq95278 小时前
编程之路 2025年终总结 ,勇往直前 再战江湖
harmonyos
不爱吃糖的程序媛10 小时前
鸿蒙PC命令行开发 macOS 上解决 pkg-config 命令未安装的问题
macos·华为·harmonyos
二流小码农11 小时前
鸿蒙开发:自定义一个圆形动画菜单
android·ios·harmonyos
不爱吃糖的程序媛11 小时前
解决鸿蒙PC命令行编译 macOS 上 cp 命令参数冲突问题
macos·harmonyos·策略模式
不爱吃糖的程序媛12 小时前
OpenHarmony PC 第三方 C/C++ 库适配完整指南
c语言·c++·harmonyos
不爱吃糖的程序媛12 小时前
OpenHarmony Linux 环境 SDK 使用说明(进阶--依赖库的解决方法)
linux·运维·harmonyos
狮子也疯狂12 小时前
【生态互联】| 鸿蒙三方库的选择与适配策略
华为·harmonyos