一、前言
🔊:自定义组件必须使用struct
定义,并且被Component
装饰器修饰。
在arkTs
中,自定义组件分为两种:
- 根组件 :就是被装饰器
@Entry
装饰的入口组件,这也是自定义组件(父组件)。
typescript
// 根组件(父组件)
@Entry
@Component
struct FatherComponent {}
- 子组件 :没有被
@Entry
装饰的自定义组件,只有@Component
装饰器(子组件)。
typescript
// 子组件
@Component
struct SonComponent {}
❗️注意:
- 子组件必须被父组件调用,才能在页面上展示出来,它自己无法展示。页面由一个父组件和无数个子组件、系统组件构成。
- 如果在另外的文件中引用该自定义组件,需要使用
export
关键字导出,并在使用的页面import
该自定义组件。
二、自定义组件的基本结构
struct
:自定义组件基于struct
实现,struct + 自定义组件名 + {...}
的组合构成自定义组件,不能有继承关系。对于struct
的实例化,可以省略new
。
🍀说明:自定义组件名、类名、函数名不能和系统组件名相同。
@Component
:@Component
装饰器仅能装饰struct
关键字声明的数据结构。struct
被@Component
装饰后具备组件化的能力,需要实现build
方法描述UI,一个struct
只能被一个@Component
装饰。@Component
可以接受一个可选的bool
类型参数。
typescript
@Component
struct MyComponent {
build() {
//描述UI
}
}
🍀说明:从API version 11开始,@Component
可以接受一个可选的bool类型参数。
自定义组件处于非激活状态时,状态变量将不响应更新,即@Watch
不会调用,状态变量关联的节点不会刷新。通过freezeWhenInactive
属性来决定是否使用冻结功能,不传参数时默认不使用。支持的场景有:页面路由 ,TabContent ,LazyForEach ,Navigation。
typescript
@Component({freezeWhenInactive: true})
struct MyComponent {
build() {
//描述UI
}
}
build()
函数:build()
函数用于定义自定义组件的声明式UI描述,自定义组件必须定义build()
函数。
typescript
@Component
struct MyComponent {
build() {
//描述UI
}
}
@Entry
:@Entry
装饰的自定义组件将作为UI页面的入口。在单个UI页面中,最多可以使用一个@Entry
装饰一个自定义组件。@Entry
可以接受一个可选的LocalStorage的参数。
typescript
@Entry
@Component
struct MyComponent {
build() {
//描述UI
}
}
🍀说明:从API version 10开始,@Entry
可以接受一个可选的LocalStorage的参数或者一个可选的EntryOptions参数。
typescript
@Entry({ routeName : 'myPage' })
@Component
struct MyComponent {
build() {
//描述UI
}
}
@Reusable
:@Reusable
装饰的自定义组件具备可复用能力。
🍀说明:从API version 10开始,该装饰器支持在ArkTS
卡片中使用。
三、页面和自定义组件生命周期
在开始之前,先明确自定义组件和页面的关系:
-
自定义组件 :
@Component
装饰的UI单元,可以组合多个系统组件实现UI的复用,可以调用组件的生命周期。 -
页面 :即应用的UI页面。可以由一个或者多个自定义组件组成,
@Entry
装饰的自定义组件为页面的入口组件,即页面的根节点,一个页面有且仅能有一个@Entry
。只有被@Entry
装饰的组件才可以调用页面的生命周期。
页面生命周期,即被@Entry
装饰的组件生命周期,提供以下生命周期接口:
-
onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景。
-
onPageHide:页面每次隐藏时触发一次,包括路由过程、应用进入后台等场景。
-
onBackPress:当用户点击返回按钮时触发。
组件生命周期,即一般用@Component
装饰的自定义组件的生命周期,提供以下生命周期接口:
-
aboutToAppear :组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其
build()
函数之前执行。 -
aboutToDisappear :
aboutToDisappear
函数在自定义组件析构销毁之前执行。不允许在aboutToDisappear
函数中改变状态变量,特别是@Link
变量的修改可能会导致应用程序行为不稳定。
生命周期流程图,下图展示的是被@Entry
装饰的组件(页面)生命周期。
示例:
index.ets
typescript
// Index.ets
import router from '@ohos.router';
@Entry
@Component
struct MyComponent {
@State showChild: boolean = true;
@State btnColor:string = "#FF007DFF"
// 只有被@Entry装饰的组件才可以调用页面的生命周期
onPageShow() {
console.info('Index onPageShow');
}
// 只有被@Entry装饰的组件才可以调用页面的生命周期
onPageHide() {
console.info('Index onPageHide');
}
// 只有被@Entry装饰的组件才可以调用页面的生命周期
onBackPress() {
console.info('Index onBackPress');
this.btnColor ="#FFEE0606"
return true // 返回true表示页面自己处理返回逻辑,不进行页面路由;返回false表示使用默认的路由返回逻辑,不设置返回值按照false处理
}
// 组件生命周期
aboutToAppear() {
console.info('MyComponent aboutToAppear');
}
// 组件生命周期
aboutToDisappear() {
console.info('MyComponent aboutToDisappear');
}
build() {
Column() {
// this.showChild为true,创建Child子组件,执行Child aboutToAppear
if (this.showChild) {
Child()
}
// this.showChild为false,删除Child子组件,执行Child aboutToDisappear
Button('delete Child')
.margin(20)
.backgroundColor(this.btnColor)
.onClick(() => {
this.showChild = false;
})
// push到page页面,执行onPageHide
Button('push to next page')
.onClick(() => {
router.pushUrl({ url: 'pages/page' });
})
}
}
}
@Component
struct Child {
@State title: string = 'Hello World';
// 组件生命周期
aboutToDisappear() {
console.info('[lifeCycle] Child aboutToDisappear')
}
// 组件生命周期
aboutToAppear() {
console.info('[lifeCycle] Child aboutToAppear')
}
build() {
Text(this.title).fontSize(50).margin(20).onClick(() => {
this.title = 'Hello ArkUI';
})
}
}
page.ets
typescript
// page.ets
@Entry
@Component
struct page {
@State textColor: Color = Color.Black;
@State num: number = 0
onPageShow() {
this.num = 5
}
onPageHide() {
console.log("page onPageHide");
}
onBackPress() { // 不设置返回值按照false处理
this.textColor = Color.Grey
this.num = 0
}
aboutToAppear() {
this.textColor = Color.Blue
}
build() {
Column() {
Text(`num 的值为:${this.num}`)
.fontSize(30)
.fontWeight(FontWeight.Bold)
.fontColor(this.textColor)
.margin(20)
.onClick(() => {
this.num += 5
})
}
.width('100%')
}
}
以上示例中,Index页面包含两个自定义组件,一个是被@Entry
装饰的MyComponent,也是页面的入口组件,即页面的根节点;一个是Child,是MyComponent的子组件。只有@Entry
装饰的节点才可以使页面级别的生命周期方法生效,因此在MyComponent中声明当前Index页面的页面生命周期函数(onPageShow
/ onPageHide
/ onBackPress
)。MyComponent和其子组件Child分别声明了各自的组件级别生命周期函数(aboutToAppear
/ aboutToDisappear
)。
应用冷启动的初始化流程为:MyComponent aboutToAppear --> MyComponent build --> Child aboutToAppear --> Child build --> Child build执行完毕 --> MyComponent build执行完毕 --> Index onPageShow
。
点击"delete Child",if绑定的this.showChild
变成false,删除Child组件,会执行Child aboutToDisappear方法。
点击"push to next page",调用router.pushUrl
接口,跳转到另外一个页面,当前Index页面隐藏,执行页面生命周期Index onPageHide。此处调用的是router.pushUrl
接口,Index页面被隐藏,并没有销毁,所以只调用onPageHide。跳转到新页面后,执行初始化新页面的生命周期的流程。
如果调用的是router.replaceUrl
,则当前Index页面被销毁,执行的生命周期流程将变为:Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear
。上文已经提到,组件的销毁是从组件树上直接摘下子树,所以先调用父组件的aboutToDisappear,再调用子组件的aboutToDisappear,然后执行初始化新页面的生命周期流程。
点击返回按钮,触发页面生命周期Index onBackPress,且触发返回一个页面后会导致当前Index页面被销毁。
最小化应用或者应用进入后台,触发Index onPageHide。当前Index页面没有被销毁,所以并不会执行组件的aboutToDisappear。应用回到前台,执行Index onPageShow。
退出应用,执行Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear
。
四、自定义组件的自定义布局
如果需要通过测算的方式布局自定义组件内子组件的位置,建议使用以下接口:
-
onMeasureSize
:组件每次布局时触发,计算子组件的尺寸,其执行时间先于onPlaceChildren
。 -
onPlaceChildren
:组件每次布局时触发,设置子组件的起始位置。
应用示例:
typescript
// xxx.ets
@Entry
@Component
struct Index {
build() {
Column() {
CustomLayout({ builder: ColumnChildren })
}
}
}
// 通过builder的方式传递多个组件,作为自定义组件的一级子组件(即不包含容器组件,如Column)
@Builder
function ColumnChildren() {
ForEach([1, 2, 3], (index: number) => { // 暂不支持lazyForEach的写法
Text('S' + index)
.fontSize(30)
.width(100)
.height(100)
.borderWidth(2)
.offset({ x: 10, y: 20 })
})
}
@Component
struct CustomLayout {
/** 属性 */
@State startSize: number = 100;
@BuilderParam builder: () => void = this.doNothingBuilder;
private result: SizeResult = {
width: 0,
height: 0
};
/** 自定义构建函数 */
@Builder
doNothingBuilder() {
};
/** 自定义布局 */
// 第一步:计算各子组件的大小
onMeasureSize(selfLayoutInfo: GeometryInfo, children: Array<Measurable>, constraint: ConstraintSizeOptions) {
let size = 100;
children.forEach((child) => {
let result: MeasureResult = child.measure({ minHeight: size, minWidth: size, maxWidth: size, maxHeight: size })
size += result.width / 2;
})
this.result.width = 100;
this.result.height = 400;
return this.result;
}
// 第二步:放置各子组件的位置
onPlaceChildren(selfLayoutInfo: GeometryInfo, children: Array<Layoutable>, constraint: ConstraintSizeOptions) {
let startPos = 300;
children.forEach((child) => {
let pos = startPos - child.measureResult.height;
child.layout({ x: pos, y: pos })
})
}
build() {
this.builder()
}
}
以上示例中,Index页面包含一个实现了自定义布局的自定义组件,且对应自定义组件的子组件通过index页面内的builder方式传入。
而在自定义组件中,调用了onMeasureSize
和onPlaceChildren
设置子组件大小和放置位置。例如,在本示例中,在onMeasureSize
中初始化组件大小size=100,后续的每一个子组件size会加上上一个子组件大小的一半,实现组件大小递增的效果。而在onPlaceChildren
中,定义startPos=300,设置每一个子组件的位置为startPos减去子组件自身的高度,所有子组件右下角一致在顶点位置(300,300),实现一个从右下角开始展示组件的类Stack组件。
五、自定义组件冻结功能场景
5.1 页面跳转
当页面A调用router.pushUrl
接口跳转到页面B时,页面A为隐藏不可见状态,此时如果更新页面A中的状态变量,不会触发页面A刷新。
当应用退到后台运行时无法被冻结。
页面A:
typescript
import router from '@ohos.router';
@Entry
@Component({ freezeWhenInactive: true })
struct FirstTest {
/** 状态变量 */
@StorageLink('PropA') @Watch("first") storageLink: number = 47;
/** 自定义方法 */
private first() {
console.info("first page " + `${this.storageLink}`)
}
/** 系统构建函数 */
build() {
Column({space:10}) {
Text(`From fist Page ${this.storageLink}`)
.fontSize(30)
Button('first page storageLink + 1')
.fontSize(18)
.onClick(() => {
this.storageLink += 1
})
Button('go to next page')
.fontSize(18)
.onClick(() => {
router.pushUrl({ url: 'pages/second' })
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
页面B:
typescript
import router from '@ohos.router';
@Entry
@Component({ freezeWhenInactive: true })
struct SecondTest {
/** 状态变量 */
@StorageLink('PropA') @Watch("second") storageLink2: number = 1;
/** 自定义函数 */
private second() {
console.info("second page: " + `${this.storageLink2}`)
}
build() {
Column({ space: 10 }) {
Text(`second Page ${this.storageLink2}`).fontSize(30)
Button('Change Divider.strokeWidth')
.fontSize(18)
.onClick(() => {
router.back()
})
Button('second page storageLink2 + 2')
.fontSize(18)
.onClick(() => {
this.storageLink2 += 2
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
结果:
当页面A 设置 @Component({ freezeWhenInactive: false })
,点击页面B点击 'second page storeageLink2' 按钮时,结果如下:
当页面A 设置 @Component({ freezeWhenInactive: true })
,点击页面B 点击 'second page storeageLink2' 按钮时,结果如下:
在上面的示例中:
-
点击页面A中的Button "first page storageLink + 1",storLink状态变量改变,
@Watch
中注册的方法first会被调用。 -
通过router.pushUrl({url: 'pages/second'}),跳转到页面B,页面A隐藏,状态由
active
变为inactive
。 -
点击页面B中的Button "this.storageLink2 += 2",只回调页面B
@Watch
中注册的方法second,因为页面A的状态变量此时已被冻结。 -
点击"back",页面B被销毁,页面A的状态由
inactive
变为active
,重新刷新在inactive
时被冻结的状态变量,页面A@Watch
中注册的方法first被再次调用。
5.2 TabContent
对Tabs中当前不可见的TabContent
进行冻结,不会触发组件的更新。
需要注意的是:在首次渲染的时候,Tab只会创建当前正在显示的TabContent
,当切换全部的TabContent
后,TabContent
才会被全部创建。
⏰ 示例
typescript
@Entry
@Component
struct TabContentTest {
/** 状态变量 */
@State @Watch("onMessageUpdated") message: number = 0;
private data: number[] = [0, 1]
/** 自定义函数 */
onMessageUpdated() {
console.info(`TabContent message callback func ${this.message}`)
}
/** 系统构建函数 */
build() {
Row() {
Column() {
Button('change message')
.margin({ top: 10 })
.onClick(() => {
this.message++
})
Tabs() {
ForEach(this.data, (item: number) => {
TabContent() {
FreezeChild({ message: this.message, index: item })
}.tabBar(`tab${item}`)
}, (item: number) => item.toString())
}
}
.width('100%')
}
.height('100%')
}
}
@Component({ freezeWhenInactive: true })
struct FreezeChild {
/** 状态变量 */
@Link @Watch("onMessageUpdated") message: number
private index: number = 0
/** 自定义函数 */
onMessageUpdated() {
console.info(`FreezeChild message callback func ${this.message}, index: ${this.index}`)
}
/** 系统构建函数 */
build() {
Text("message" + `${this.message}, index: ${this.index}`)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
}
结果:
当设置自定义组件 FreezeChild 设置 @Component({ freezeWhenInactive: false })
时
当设置自定义组件 FreezeChild 设置 @Component({ freezeWhenInactive: true })
时