纯血鸿蒙APP实战开发——主页瀑布流实现

介绍

本示例介绍使用ArkUI WaterFlow 组件和 LazyForEach 实现瀑布流场景。该场景多用于购物、资讯类应用。

效果图预览

使用说明

  1. 加载完成后显示整个列表,超过一屏时可以上下滑动。

实现思路

  1. 创建WaterFlowDataSource类,实现IDataSource接口的对象,用于WaterFlow和LazyForEach加载数据。
  2. 通过@Builder自定义瀑布流列表项组件,作为FlowItem的子组件。
  3. 结合父组件传递的数据以及WaterFlow和LazyForEach循环构造出整个列表。

WaterFlow

子组件

包含 FlowItem 子组件。

说明
WaterFlow子组件的visibility属性设置为None时不显示,但依然会占用子组件对应的网格。

接口

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

参数:

参数名 参数类型 必填 参数描述
footer CustomBuilder 设置WaterFlow尾部组件。
scroller Scroller 可滚动组件的控制器,与可滚动组件绑定。
目前瀑布流仅支持Scroller组件的scrollToIndex接口。

属性

除支持 通用属性 外,还支持以下属性:

名称 参数类型 描述
columnsTemplate string 设置当前瀑布流组件布局列的数量,不设置时默认1列。
例如, '1fr 1fr 2fr' 是将父组件分3列,将父组件允许的宽分为4等份,第一列占1份,第二列占1份,第三列占2份。并支持 auto-fill 。
默认值:'1fr'
rowsTemplate string 设置当前瀑布流组件布局行的数量,不设置时默认1行。
例如, '1fr 1fr 2fr'是将父组件分三行,将父组件允许的高分为4等份,第一行占1份,第二行占一份,第三行占2份。并支持 auto-fill 。
默认值:'1fr'
itemConstraintSize ConstraintSizeOptions 设置约束尺寸,子组件布局时,进行尺寸范围限制。
columnsGap Length 设置列与列的间距。
默认值:0
rowsGap Length 设置行与行的间距。 默认值:0
layoutDirection FlexDirection 设置布局的主轴方向。
默认值:FlexDirection.Column

layoutDirection优先级高于rowsTemplate和columnsTemplate。根据layoutDirection设置情况,分为以下三种设置模式:

  • layoutDirection设置纵向布局(FlexDirection.Column 或 FlexDirection.ColumnReverse)
    此时columnsTemplate有效(如果未设置,取默认值)。例如columnsTemplate设置为"1fr 1fr"、rowsTemplate设置为"1fr 1fr 1fr"时,瀑布流组件纵向布局,辅轴均分成横向2列。

  • layoutDirection设置横向布局(FlexDirection.Row 或 FlexDirection.RowReverse)
    此时rowsTemplate有效(如果未设置,取默认值)。例如columnsTemplate设置为"1fr 1fr"、rowsTemplate设置为"1fr 1fr 1fr"时,瀑布流组件横向布局,辅轴均分成纵向3列。

  • layoutDirection未设置布局方向
    布局方向为layoutDirection的默认值:FlexDirection.Column,此时columnsTemplate有效。例如columnsTemplate设置为"1fr 1fr"、rowsTemplate设置为"1fr 1fr 1fr"时,瀑布流组件纵向布局,辅轴均分成横向2列。

事件

除支持 通用事件 外,还支持以下事件:

名称 功能描述
onReachStart(event: () => void) 瀑布流组件到达起始位置时触发。
onReachEnd(event: () => void) 瀑布流组件到底末尾位置时触发。

auto-fill说明

WaterFlow的columnsTemplate、rowsTemplate属性的auto-fill仅支持以下格式:

repeat(auto-fill, track-size)

其中repeat、auto-fill为关键字。track-size为行高或者列宽,支持的单位包括px、vp、%或有效数字,track-size至少包括一个有效行高或者列宽。

示例

// 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): any {
      return this.dataArray[index]
  }

  // 通知控制器数据重新加载
  notifyDataReload(): void {
      this.listeners.forEach(listener => {
          listener.onDataReloaded()
      })
  }

  // 通知控制器数据增加
  notifyDataAdd(index: number): void {
      this.listeners.forEach(listener => {
          listener.onDataAdded(index)
      })
  }

  // 通知控制器数据变化
  notifyDataChange(index: number): void {
      this.listeners.forEach(listener => {
          listener.onDataChanged(index)
      })
  }

  // 通知控制器数据删除
  notifyDataDelete(index: number): void {
      this.listeners.forEach(listener => {
          listener.onDataDeleted(index)
      })
  }

  // 通知控制器数据位置变化
  notifyDataMove(from: number, to: number): void {
      this.listeners.forEach(listener => {
          listener.onDataMoved(from, to)
      })
  }

  // 获取数据总数
  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 Reload(): void {
      this.dataArray.splice(1, 1)
      this.dataArray.splice(3, 2)
      this.notifyDataReload()
  }
}

// WaterflowDemo.ets
import { WaterFlowDataSource } from './WaterFlowDataSource'

@Entry
@Component
struct WaterflowDemo {
  @State minSize: number = 50
  @State maxSize: number = 100
  @State fontSize: number = 24
  @State colors: number[] = [0xFFC0CB, 0xDA70D6, 0x6B8E23, 0x6A5ACD, 0x00FFFF, 0x00FF7F]
  scroller: Scroller = new Scroller()
  datasource: WaterFlowDataSource = new WaterFlowDataSource()
  private itemWidthArray: number[] = []
  private itemHeightArray: number[] = []

  // 计算flow item宽/高  
  getSize() {
    let ret = Math.floor(Math.random() * this.maxSize)
    return (ret > this.minSize ? ret : this.minSize)
  }

  // 保存flow item宽/高   
  getItemSizeArray() {
    for (let i = 0; i < 100; i++) {
      this.itemWidthArray.push(this.getSize())
      this.itemHeightArray.push(this.getSize())
    }
  }

  aboutToAppear() {
    this.getItemSizeArray()
  }

  @Builder itemFoot() {
    Column() {
      Text(`Footer`)
        .fontSize(10)
        .backgroundColor(Color.Red)
        .width(50)
        .height(50)
        .align(Alignment.Center)
        .margin({ top: 2 })
    }
  }

  build() {
    Column({ space: 2 }) {
      WaterFlow({ footer: this.itemFoot.bind(this), scroller: this.scroller }) {
        LazyForEach(this.datasource, (item: number) => {
          FlowItem() {
            Column() {
              Text("N" + item).fontSize(12).height('16')
              Image('res/waterFlowTest(' + item % 5 + ').jpg')
                .objectFit(ImageFit.Fill)
            }
          }
          .width(this.itemWidthArray[item])
          .height(this.itemHeightArray[item])
          .backgroundColor(this.colors[item % 5])
        }, item => item)
      }
      .columnsTemplate("1fr 1fr 1fr 1fr")
      .itemConstraintSize({
        minWidth: 0,
        maxWidth: '100%',
        minHeight: 0,
        maxHeight: '100%'
      })
      .columnsGap(10)
      .rowsGap(5)
      .onReachStart(() => {
        console.info("onReachStart")
      })
      .onReachEnd(() => {
        console.info("onReachEnd")
      })
      .backgroundColor(0xFAEEE0)
      .width('100%')
      .height('80%')
      .layoutDirection(FlexDirection.Column)
    }
  }
}

LazyForEach:数据懒加载

LazyForEach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。

接口描述

LazyForEach(
    dataSource: IDataSource,             // 需要进行数据迭代的数据源
    itemGenerator: (item: any, index: number) => void,  // 子组件生成函数
    keyGenerator?: (item: any, index: number) => string // 键值生成函数
): void

参数:

参数名 参数类型 必填 参数描述
dataSource IDataSource LazyForEach数据源,需要开发者实现相关接口。
itemGenerator (item: any, index:number) => void 子组件生成函数,为数组中的每一个数据项创建一个子组件。
说明:
item是当前数据项,index是数据项索引值。
itemGenerator的函数体必须使用大括号{...}。itemGenerator每次迭代只能并且必须生成一个子组件。itemGenerator中可以使用if语句,但是必须保证if语句每个分支都会创建一个相同类型的子组件。itemGenerator中不允许使用ForEach和LazyForEach语句。
keyGenerator (item: any, index:number) => string 键值生成函数,用于给数据源中的每一个数据项生成唯一且固定的键值。当数据项在数组中的位置更改时,其键值不得更改,当数组中的数据项被新项替换时,被替换项的键值和新项的键值必须不同。键值生成器的功能是可选的,但是,为了使开发框架能够更好地识别数组更改,提高性能,建议提供。如将数组反向时,如果没有提供键值生成器,则LazyForEach中的所有节点都将重建。
说明:
item是当前数据项,index是数据项索引值。
数据源中的每一个数据项生成的键值不能重复。

IDataSource类型说明

interface IDataSource {
    totalCount(): number; // 获得数据总数
    getData(index: number): Object; // 获取索引值对应的数据
    registerDataChangeListener(listener: DataChangeListener): void; // 注册数据改变的监听器
    unregisterDataChangeListener(listener: DataChangeListener): void; // 注销数据改变的监听器
}
接口声明 参数类型 说明
totalCount(): number - 获得数据总数。
getData(index: number): any number 获取索引值index对应的数据。
index:获取数据对应的索引值。
registerDataChangeListener(listener:DataChangeListener): void DataChangeListener 注册数据改变的监听器。
listener:数据变化监听器
unregisterDataChangeListener(listener:DataChangeListener): void DataChangeListener 注销数据改变的监听器。
listener:数据变化监听器

DataChangeListener类型说明

interface DataChangeListener {
    onDataReloaded(): void; // 重新加载数据完成后调用
    onDataAdded(index: number): void; // 添加数据完成后调用
    onDataMoved(from: number, to: number): void; // 数据移动起始位置与数据移动目标位置交换完成后调用
    onDataDeleted(index: number): void; // 删除数据完成后调用
    onDataChanged(index: number): void; // 改变数据完成后调用
    onDataAdd(index: number): void; // 添加数据完成后调用
    onDataMove(from: number, to: number): void; // 数据移动起始位置与数据移动目标位置交换完成后调用
    onDataDelete(index: number): void; // 删除数据完成后调用
    onDataChange(index: number): void; // 改变数据完成后调用
}
接口声明 参数类型 说明
onDataReloaded(): void - 通知组件重新加载所有数据。
键值没有变化的数据项会使用原先的子组件,键值发生变化的会重建子组件。
onDataAdd(index: number): void8+ number 通知组件index的位置有数据添加。
index:数据添加位置的索引值。
onDataMove(from: number, to: number): void8+ from: number,
to: number
通知组件数据有移动。
from: 数据移动起始位置,to: 数据移动目标位置。
说明:
数据移动前后键值要保持不变,如果键值有变化,应使用删除数据和新增数据接口。
onDataDelete(index: number):void8+ number 通知组件删除index位置的数据并刷新LazyForEach的展示内容。
index:数据删除位置的索引值。
说明:
需要保证dataSource中的对应数据已经在调用onDataDelete前删除,否则页面渲染将出现未定义的行为。
onDataChange(index: number): void8+ number 通知组件index的位置有数据有变化。
index:数据变化位置的索引值。
onDataAdded(index: number):void(deprecated) number 通知组件index的位置有数据添加。
从API 8开始,建议使用onDataAdd。
index:数据添加位置的索引值。
onDataMoved(from: number, to: number): void(deprecated) from: number,
to: number
通知组件数据有移动。
从API 8开始,建议使用onDataMove。
from: 数据移动起始位置,to: 数据移动目标位置。
将from和to位置的数据进行交换。
说明:
数据移动前后键值要保持不变,如果键值有变化,应使用删除数据和新增数据接口。
onDataDeleted(index: number):void(deprecated) number 通知组件删除index位置的数据并刷新LazyForEach的展示内容。
从API 8开始,建议使用onDataDelete。
index:数据删除位置的索引值。
onDataChanged(index: number): void(deprecated) number 通知组件index的位置有数据有变化。
从API 8开始,建议使用onDataChange。
index:数据变化监听器。

使用限制

  • LazyForEach必须在容器组件内使用,仅有List、Grid、Swiper以及WaterFlow组件支持数据懒加载(可配置cachedCount属性,即只加载可视部分以及其前后少量数据用于缓冲),其他组件仍然是一次性加载所有的数据。
  • LazyForEach在每次迭代中,必须创建且只允许创建一个子组件。
  • 生成的子组件必须是允许包含在LazyForEach父容器组件中的子组件。
  • 允许LazyForEach包含在if/else条件渲染语句中,也允许LazyForEach中出现if/else条件渲染语句。
  • 键值生成器必须针对每个数据生成唯一的值,如果键值相同,将导致键值相同的UI组件渲染出现问题。
  • LazyForEach必须使用DataChangeListener对象来进行更新,第一个参数dataSource使用状态变量时,状态变量改变不会触发LazyForEach的UI刷新。
  • 为了高性能渲染,通过DataChangeListener对象的onDataChange方法来更新UI时,需要生成不同于原来的键值来触发组件刷新。

键值生成规则

在LazyForEach循环渲染过程中,系统会为每个item生成一个唯一且持久的键值,用于标识对应的组件。当这个键值变化时,ArkUI框架将视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。

LazyForEach提供了一个名为keyGenerator的参数,这是一个函数,开发者可以通过它自定义键值的生成规则。如果开发者没有定义keyGenerator函数,则ArkUI框架会使用默认的键值生成函数,即(item: any, index: number) => { return viewId + '-' + index.toString(); }, viewId在编译器转换过程中生成,同一个LazyForEach组件内其viewId是一致的。

写在最后

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙

  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  • 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
  • 想要获取更多完整鸿蒙最新学习知识点,请移步前往小编:https://gitee.com/MNxiaona/733GH/blob/master/jianshu
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容