如果是前端开发的同学,对于Vue框架中的v-if和v-for肯定再熟悉不过了,鸿蒙也提供了相应的渲染控制的能力。文章分为三块:
- 通过if控制UI渲染;
- 使用ForEach遍历数据源数组,生成列表组件;
- 使用LazyForEach进行列表懒加载;
1. 渲染控制概述
ArkUI通过自定义组件的build()函数和@Builder装饰器中的声明式UI描述语句构建相应的UI,支持条件渲染语句和循环渲染语句以及对大数据量场景的数据懒加载语句。类似于Vue的v-if、v-for。
2. 条件渲染
使用规则:
- 支持if、else和else if语句;
- if、else if后跟随的条件语句可以使用状态变量;
- 可以在容器组件内使用,通过条件渲染语句构建不同的子组件;
- 某些容器组件限制子组件的类型或数量,这同样应用于条件渲染语句;
2.1 使用if进行条件渲染
@Entry
@Component
struct ViewA {
@State count: number = 0;
build() {
Column() {
Text(`count=${this.count}`)
// 状态变化时,条件语句会更新并重新渲染
if (this.count > 0) {
Text(`count is positive`)
.fontColor(Color.Green)
}
Button('increase count')
.onClick(() => {
this.count++;
})
Button('decrease count')
.onClick(() => {
this.count--;
})
}
}
}
2.2 if...else...语句和子组件状态
@Component
struct CounterView {
@State counter: number = 0;
label: string = 'unknown';
build() {
Row() {
Text(`${this.label}`)
Button(`counter ${this.counter} +1`)
.onClick(() => {
this.counter += 1;
})
}
}
}
@Entry
@Component
struct MainView {
@State toggle: boolean = true;
build() {
Column() {
// toggle的改变会重新创建子组件,子组件中的状态不会被保留counter
if (this.toggle) {
CounterView({ label: 'CounterView #positive' })
} else {
CounterView({ label: 'CounterView #negative' })
}
Button(`toggle ${this.toggle}`)
.onClick(() => {
this.toggle = !this.toggle;
})
}
}
}
// 如果想保留Counter,则需要进行更改:
@Component
struct CounterView {
@Link counter: number;
label: string = 'unknown';
build() {
Row() {
Text(`${this.label}`)
Button(`counter ${this.counter} +1`)
.onClick(() => {
this.counter += 1;
})
}
}
}
@Entry
@Component
struct MainView {
@State toggle: boolean = true;
@State counter: number = 0;
build() {
Column() {
if (this.toggle) {
CounterView({ counter: $counter, label: 'CounterView #positive' })
} else {
CounterView({ counter: $counter, label: 'CounterView #negative' })
}
Button(`toggle ${this.toggle}`)
.onClick(() => {
this.toggle = !this.toggle;
})
}
}
}
此处,@State counter变量归父组件所有。因此,当CounterView组件实例被删除时,该变量不会被销毁。CounterView组件通过@Link装饰器引用状态。状态必须从子级移动到其父级(或父级的父级),以避免在条件内容或重复内容被销毁时丢失状态。
3. 循环渲染
3.1 介绍
ForEach接口基于数组类型的数据来进行循环渲染,要和容器组件配合使用。
接口描述:
ForEach(
// Array类型的数组
arr: Array,
// 组件生成函数,为每个元素创建对应的组件
itemGenerator: (item: any, index?: number) => void,
// 键值生成函数,默认是 (item: T, index: number) => { return index + '__' + JSON.stringify(item); } 需要保证key的唯一性,跟Vue一样,提升渲染性能
keyGenerator?: (item: any, index?: number) => string
)
当确定键值生成规则之后,会为数据源的每个数据项创建组件,有两种情况:首次渲染和非首次渲染。
首次渲染:
在ForEach首次渲染时,会根据键值生成规则为每个数据项生成唯一的键值,并创建相应的组件:
@Entry
@Component
struct Parent {
@State simpleList: Array<string> = ['one', 'two', 'three'];
build() {
Row() {
Column() {
ForEach(this.simpleList, (item: string) => {
ChildItem({ 'item': item } as Record<string, string>)
}, (item: string) => item)
}
.width('100%')
.height('100%')
}
.height('100%')
.backgroundColor(0xF1F3F5)
}
}
@Component
struct ChildItem {
@Prop item: string;
build() {
Text(this.item)
.fontSize(50)
}
}
非首次渲染:
在ForEach组件进行非首次渲染时,它会检查新生成的键值是否已经存在,如果不存在则会创建一个新的组件;如果已经存在,则不会创建新的组件,而是直接渲染该键值所对应的组件:
@Entry
@Component
struct Parent {
@State simpleList: Array<string> = ['one', 'two', 'three'];
build() {
Row() {
Column() {
Text('点击修改第3个数组项的值')
.fontSize(24)
.fontColor(Color.Red)
.onClick(() => {
this.simpleList[2] = 'new three';
})
ForEach(this.simpleList, (item: string) => {
ChildItem({ item: item })
.margin({ top: 20 })
}, (item: string) => item)
}
.justifyContent(FlexAlign.Center)
.width('100%')
.height('100%')
}
.height('100%')
.backgroundColor(0xF1F3F5)
}
}
@Component
struct ChildItem {
@Prop item: string;
build() {
Text(this.item)
.fontSize(30)
}
}
如上面的例子中,simpleList数组项发生变化,会触发ForEach进行重新渲染。其中键值 one two已经存在,所以会进行复用。第三个 new three,由于键值不存在,所以会创建一个新的组件。
3.2 基础使用
class Article {
id: string;
title: string;
brief: string;
constructor(id: string, title: string, brief: string) {
this.id = id;
this.title = title;
this.brief = brief;
}
}
@Entry
@Component
struct ArticleListView {
@State isListReachEnd: boolean = false;
@State articleList: Array<Article> = [
new Article('001', '第1篇文章', '文章简介内容'),
new Article('002', '第2篇文章', '文章简介内容'),
new Article('003', '第3篇文章', '文章简介内容'),
new Article('004', '第4篇文章', '文章简介内容'),
new Article('005', '第5篇文章', '文章简介内容'),
new Article('006', '第6篇文章', '文章简介内容')
]
loadMoreArticles() {
this.articleList.push(new Article('007', '加载的新文章', '文章简介内容'));
}
build() {
Column({ space: 5 }) {
List() {
ForEach(this.articleList, (item: Article) => {
ListItem() {
ArticleCard({ article: item })
.margin({ top: 20 })
}
}, (item: Article) => item.id)
}
.onReachEnd(() => {
this.isListReachEnd = true;
})
.parallelGesture(
PanGesture({ direction: PanDirection.Up, distance: 80 })
.onActionStart(() => {
if (this.isListReachEnd) {
this.loadMoreArticles();
this.isListReachEnd = false;
}
})
)
.padding(20)
.scrollBar(BarState.Off)
}
.width('100%')
.height('100%')
.backgroundColor(0xF1F3F5)
}
}
@Component
struct ArticleCard {
@Prop article: Article;
build() {
Row() {
Image($r('app.media.icon'))
.width(80)
.height(80)
.margin({ right: 20 })
Column() {
Text(this.article.title)
.fontSize(20)
.margin({ bottom: 8 })
Text(this.article.brief)
.fontSize(16)
.fontColor(Color.Gray)
.margin({ bottom: 8 })
}
.alignItems(HorizontalAlign.Start)
.width('80%')
.height('100%')
}
.padding(20)
.borderRadius(12)
.backgroundColor('#FFECECEC')
.height(120)
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
}
3.3 数据源数组中子项的属性发生变化
如果我们对于数据源数组进行直接操作是可以触发ForEach重新渲染,但是如果对数组中的某一项的属性值修改,则需要结合@Observed和@ObjectLink装饰器使用,比如在文章列表卡片上点击“点赞”按钮,从而修改文章的点赞数量:
@Observed
class Article {
id: string;
title: string;
brief: string;
isLiked: boolean;
likesCount: number;
constructor(id: string, title: string, brief: string, isLiked: boolean, likesCount: number) {
this.id = id;
this.title = title;
this.brief = brief;
this.isLiked = isLiked;
this.likesCount = likesCount;
}
}
@Entry
@Component
struct ArticleListView {
@State articleList: Array<Article> = [
new Article('001', '第0篇文章', '文章简介内容', false, 100),
new Article('002', '第1篇文章', '文章简介内容', false, 100),
new Article('003', '第2篇文章', '文章简介内容', false, 100),
new Article('004', '第4篇文章', '文章简介内容', false, 100),
new Article('005', '第5篇文章', '文章简介内容', false, 100),
new Article('006', '第6篇文章', '文章简介内容', false, 100),
];
build() {
List() {
ForEach(this.articleList, (item: Article) => {
ListItem() {
ArticleCard({
article: item
})
.margin({ top: 20 })
}
}, (item: Article) => item.id)
}
.padding(20)
.scrollBar(BarState.Off)
.backgroundColor(0xF1F3F5)
}
}
@Component
struct ArticleCard {
@ObjectLink article: Article;
handleLiked() {
this.article.isLiked = !this.article.isLiked;
this.article.likesCount = this.article.isLiked ? this.article.likesCount + 1 : this.article.likesCount - 1;
}
build() {
Row() {
Image($r('app.media.icon'))
.width(80)
.height(80)
.margin({ right: 20 })
Column() {
Text(this.article.title)
.fontSize(20)
.margin({ bottom: 8 })
Text(this.article.brief)
.fontSize(16)
.fontColor(Color.Gray)
.margin({ bottom: 8 })
Row() {
Image(this.article.isLiked ? $r('app.media.iconLiked') : $r('app.media.iconUnLiked'))
.width(24)
.height(24)
.margin({ right: 8 })
Text(this.article.likesCount.toString())
.fontSize(16)
}
.onClick(() => this.handleLiked())
.justifyContent(FlexAlign.Center)
}
.alignItems(HorizontalAlign.Start)
.width('80%')
.height('100%')
}
.padding(20)
.borderRadius(12)
.backgroundColor('#FFECECEC')
.height(120)
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
}
Article类被@Observed装饰器修饰,父组件ArticleListView传入Article对象实例给子组件ArticleCard,子组件使用@ObjectLink装饰器接收该实例。
- 当点击第1个文章卡片上的点赞图标时,会触发ArticleCard组件的handleLiked函数。该函数修改第1个卡片对应组件里article实例的isLiked和likesCount属性值;
- 由于子组件ArticleCard中的article使用了@ObjectLink装饰器,父子组件共享同一份article数据。因此,父组件中articleList的第1个数组项的isLiked和likedCounts数值也会同步修改;
- 当父组件监听到数据源数组项属性值变化时,会触发ForEach重新渲染;
- 在此处,ForEach键值生成规则为数组项的id属性值。当ForEach遍历新数据源时,数组项的id均没有变化,不会新建组件;
- 渲染第1个数组项对应的ArticleCard组件时,读取到的isLiked和likesCount为修改后的新值;
4. LazyForEach:数据懒加载
LazyForEach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。
4.1 介绍
4.1.1 LazyForEach 接口描述
LazyForEach就是数据懒加载,可以理解为每次只加载部分。
LazyForEach(
dataSource: IDataSource, // 需要进行数据迭代的数据源
itemGenerator: (item: any, index: number) => void, // 子组件生成函数
keyGenerator?: (item: any, index: number) => string // 键值生成函数
): void
从上面的接口描述中可以看出,需要三个参数,后两个参数和ForEach一样,这里不再多讲,我们主要看下第一个参数dataSource。
可以和ForEach的接口描述进行以下对比,在ForEach中,因为数据源是死的、固定的,所以第一个参数直接传数据数组即可。但是对于LazyForEach来说,数据的数量是不固定的,那就需要让开发者实现相关接口,提供相应的数据了,这个和在iOS中给UITableView设置dataSource的思想是一样的。
4.1.2 IDataSource类型说明
我们可以构建一个BaseDataSource类实现IDataSource接口,接口如下:
interface IDataSource {
totalCount(): number; // 获得数据总数
getData(index: number): Object; // 获取索引值对应的数据
registerDataChangeListener(listener: DataChangeListener): void; // 注册数据改变的监听器
unregisterDataChangeListener(listener: DataChangeListener): void; // 注销数据改变的监听器
}
BaseDataSource这个类中可以:
- 维护一个名字为dataArray的数组,作为数据源;
- 维护一个名字为listeners的数组,用来存储listener;
下面来逐个看下每个方法该如何实现:
- totalCount(): 就是总的数据源的数量,直接返回该originDataArray数组的数量即可;
- getData(index: number): Object: 就是取originDataArray对应下标的数据返回即可;
- registerDataChangeListener(listener: DataChangeListener): void:listeners数组需要添加这个listener;
- unregisterDataChangeListener(listener: DataChangeListener): void:listeners数组中需要移除这个listener
关于listener相关的两个方法都是系统框架侧调用的,我们只需要通过listeners维护好该数组即可,然后在originDataArray数据源发生变化的时候,通知到listeners。
4.1.3 DataChangeListener类型说明
当元素改变(删除、移动、添加等)的时候,我们需要调用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; // 改变数据完成后调用
}
4.1.4 使用限制
- LazyForEach必须在容器组件中使用,仅在List Grid Swiper WaterFlow父子间支持数据懒加载,其余容器组件会一次性加载所有的;
- LazyForEach必须使用DataChangeListener对象来进行更新,第一个参数dataSource使用状态变量时,状态变量改变不会触发LazyForEach的UI刷新;
- 为了高性能渲染,通过DataChangeListener对象的onDataChange方法来更新UI时,需要生成不同于原来的键值来触发组件刷新;
- 生成的子组件必须是允许包含在LazyForEach父容器组件中的子组件,比如List中子组件必须是ListItem;
4.2 简单的例子
构建BasicDataSource类实现IDataSource接口,其中维护了两个数组:
- listeners:存放监听对象
- originDataArray:存放数据源
// Basic implementation of IDataSource to handle data listener
class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = [];
private originDataArray: string[] = [];
// 需要子类重写
public totalCount(): number {
return 0;
}
// 需要子类重写
public getData(index: number): string {
return this.originDataArray[index];
}
// 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听
// 在这个方法中将listener添加到listeners数组中
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
}
// 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听
// 在这个方法中将listener从listeners数组中移除
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.splice(pos, 1);
}
}
/*
下面是自定义的方法,在originDataArray数组中元素发生变化时需要调用下面对应的方法通知监听对象。
*/
// 通知LazyForEach组件需要重载所有子组件
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded();
})
}
// 通知LazyForEach组件需要在index对应索引处添加子组件
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index);
})
}
// 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index);
})
}
// 通知LazyForEach组件需要在index对应索引处删除该子组件
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index);
})
}
// 通知LazyForEach组件将from索引和to索引处的子组件进行交换
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to);
})
}
}
构建自定义类MyDataSource继承BasicDataSource类:
class MyDataSource extends BasicDataSource {
// 子类实际的数据源数组
private dataArray: string[] = [];
public totalCount(): number {
return this.dataArray.length;
}
public getData(index: number): string {
return this.dataArray[index];
}
/*
封装改变数组元素的方法,其中会调用父类的方法。
*/
public addData(index: number, data: string): void {
this.dataArray.splice(index, 0, data);
this.notifyDataAdd(index);
}
public pushData(data: string): void {
this.dataArray.push(data);
// this.notifyDataAdd(this.dataArray.length - 1);
}
}
在UI描述中使用:
@Entry
@Component
struct MyComponent {
private data: MyDataSource = new MyDataSource();
build() {
List({ space: 3 }) {
ListItem() {
Button("Add")
.onClick(()=>{
for (let i = 0; i <= 20; i++) {
this.data.pushData(`Hello ${i}`)
}
})
}
LazyForEach(this.data, (item: string) => {
ListItem() {
Row() {
Text(item).fontSize(50)
.onAppear(() => {
console.info("appear:" + item)
})
}.margin({ left: 10, right: 10 })
}
}, (item: string) => item)
// cachedCount 用于提前缓存数据,一般设置为屏幕中组件数量的一半。
}.cachedCount(5)
}
}
上面只是一个简单的用例,当想要添加一个元素的时候,会调用数据源data的pushData方法,该方法会在数据源末尾添加数据并调用notifyDataAdd方法。在notifyDataAdd方法内会又调用listener.onDataAdd方法,该方法会通知LazyForEach在该处有数据添加,LazyForEach便会在该索引处新建子组件。
其他操作数据源的方法也类似,需要调用listener对应的方法,才会触发LazyForEach的刷新。
这里只是LazyForEach的基本使用,如果在项目开发中遇到问题,还是需要看下官方文档: developer.huawei.com/consumer/cn…