介绍
本示例介绍使用ArkUI WaterFlow 组件和 LazyForEach 实现瀑布流场景。该场景多用于购物、资讯类应用。
效果图预览
使用说明
- 加载完成后显示整个列表,超过一屏时可以上下滑动。
实现思路
- 创建WaterFlowDataSource类,实现IDataSource接口的对象,用于WaterFlow和LazyForEach加载数据。
- 通过@Builder自定义瀑布流列表项组件,作为FlowItem的子组件。
- 结合父组件传递的数据以及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