HarmonyOS Developer 学习ArkTS语言
ArkTS语法(声明式UI)- 动态构建UI元素
基本UI描述介绍的是如何创建一个内部UI结构固定的自定义组件,为了满足开发者自定义组件内部UI结构的需求,ArkTS同时提供了动态构建UI元素的能力。
@Builder
可通过@Builder装饰器进行描述,该装饰器可以修饰一个函数,此函数可以在build函数之外声明,并在build函数中或其他@Builder修饰的函数中使用,从而实现在一个自定义组件内快速生成多个布局内容。使用方式如下面示例所示。
// xxx.ets
@Component
struct CompB {
@State CompValue: string = ''
aboutToAppear() {
console.info('CompB aboutToAppear.')
}
aboutToDisappear() {
console.info('CompB aboutToDisappear.')
}
build() {
Column() {
Button(this.CompValue)
.margin(5)
}
}
}
@Entry
@Component
struct CompA {
size1: number = 100
@State CompValue1: string = "Hello,CompValue1"
@State CompValue2: string = "Hello,CompValue2"
@State CompValue3: string = "Hello,CompValue3"
// @Builder装饰的函数CompC内使用自定义组件CompB
@Builder CompC(value: string) {
CompB({ CompValue: value })
}
@Builder SquareText(label: string) {
Text(label)
.fontSize(18)
.width(1 * this.size1)
.height(1 * this.size1)
}
// @Builder装饰的函数RowOfSquareTexts内使用@Builder装饰的函数SquareText
@Builder RowOfSquareTexts(label1: string, label2: string) {
Row() {
this.SquareText(label1)
this.SquareText(label2)
}
.width(2 * this.size1)
.height(1 * this.size1)
}
build() {
Column() {
Row() {
this.SquareText("A")
this.SquareText("B")
}
.width(2 * this.size1)
.height(1 * this.size1)
this.RowOfSquareTexts("C", "D")
Column() {
// 使用三次@Builder装饰的自定义组件
this.CompC(this.CompValue1)
this.CompC(this.CompValue2)
this.CompC(this.CompValue3)
}
.width(2 * this.size1)
.height(2 * this.size1)
}
.width(2 * this.size1)
.height(2 * this.size1)
}
}
@BuilderParam8+
@BuilderParam装饰器用于修饰自定义组件内函数类型的属性(例如:@BuilderParam noParam: () => void),并且在初始化自定义组件时被@BuilderParam修饰的属性必须赋值。
引入动机
当开发者创建自定义组件,并想对该组件添加特定功能时(例如在自定义组件中添加一个点击跳转操作)。若直接在组件内嵌入事件方法,将会导致所有引入该自定义组件的地方均增加了该功能。为解决此问题,引入了@BuilderParam装饰器,此装饰器修饰的属性值可为@Builder装饰的函数,开发者可在初始化自定义组件时对此属性进行赋值,为自定义组件增加特定的功能。
参数初始化组件
通过参数初始化组件时,将@Builder装饰的函数赋值给@BuilderParam修饰的属性,并在自定义组件内调用该属性值。若@BuilderParam修饰的属性在进行赋值时不带参数(如:noParam: this.specificNoParam),则此属性的类型需定义为无返回值的函数(如:@BuilderParam noParam: () => void);若带参数(如:withParam: this.SpecificWithParam('WithParamA')),则此属性的类型需定义成any(如:@BuilderParam withParam: any)。
// xxx.ets
@Component
struct CustomContainer {
header: string = ''
@BuilderParam noParam: () => void
@BuilderParam withParam: any
footer: string = ''
build() {
Column() {
Text(this.header)
.fontSize(30)
this.noParam()
this.withParam()
Text(this.footer)
.fontSize(30)
}
}
}
@Entry
@Component
struct CustomContainerUser {
@Builder specificNoParam() {
Column() {
Text('noParam').fontSize(30)
}
}
@Builder SpecificWithParam(label: string) {
Column() {
Text(label).fontSize(30)
}
}
build() {
Column() {
CustomContainer({
header: 'HeaderA',
noParam: this.specificNoParam,
withParam: this.SpecificWithParam('WithParamA'),
footer: 'FooterA'
})
Divider().color(Color.Black)
.strokeWidth(3)
.margin(10)
CustomContainer({
header: 'HeaderB',
noParam: this.specificNoParam,
withParam: this.SpecificWithParam('WithParamB'),
footer: 'FooterB'
})
}
}
}
@Styles
ArkTS为了避免开发者对重复样式的设置,通过@Styles装饰器可以将多个样式设置提炼成一个方法,直接在组件声明时调用,通过@Styles装饰器可以快速定义并复用自定义样式。当前@Styles仅支持通用属性。
@Styles可以定义在组件内或组件外,在组件外定义时需在方法名前面添加function关键字,组件内定义时则不需要添加function关键字。
// xxx.ets
@Styles function globalFancy () {
.width(150)
.height(100)
.backgroundColor(Color.Pink)
}
@Entry
@Component
struct FancyUse {
@Styles componentFancy() {
.width(100)
.height(200)
.backgroundColor(Color.Yellow)
}
build() {
Column({ space: 10 }) {
Text('FancyA')
.globalFancy()
.fontSize(30)
Text('FancyB')
.globalFancy()
.fontSize(20)
Text('FancyC')
.componentFancy()
.fontSize(30)
Text('FancyD')
.componentFancy()
.fontSize(20)
}
}
}
@Styles还可以在StateStyles属性内部使用,在组件处于不同的状态时赋予相应的属性。
在StateStyles内可以直接调用组件外定义的@Styles方法,但需要通过this关键字调用组件内定义的@Styles方法。
// xxx.ets
@Styles function globalFancy () {
.width(120)
.height(120)
.backgroundColor(Color.Green)
}
@Entry
@Component
struct FancyUse {
@Styles componentFancy() {
.width(80)
.height(80)
.backgroundColor(Color.Red)
}
build() {
Row({ space: 10 }) {
Button('Fancy')
.stateStyles({
normal: {
.width(100)
.height(100)
.backgroundColor(Color.Blue)
},
disabled: this.componentFancy,
pressed: globalFancy
})
}
}
}
@Extend
@Extend装饰器将新的属性方法添加到Text、Column、Button等内置组件上,通过@Extend装饰器可以快速地扩展原生组件。@Extend不能定义在自定义组件struct内。
// xxx.ets
@Extend(Text) function fancy (fontSize: number) {
.fontColor(Color.Red)
.fontSize(fontSize)
.fontStyle(FontStyle.Italic)
.fontWeight(600)
}
@Entry
@Component
struct FancyUse {
build() {
Row({ space: 10 }) {
Text("Fancy")
.fancy(16)
Text("Fancy")
.fancy(24)
Text("Fancy")
.fancy(32)
}
}
}
说明
- @Extend装饰器不能定义在自定义组件struct内。
- @Extend装饰器内仅支持属性方法设置。
@CustomDialog
@CustomDialog装饰器用于装饰自定义弹窗组件,使得弹窗可以动态设置内容及样式。
// xxx.ets
@CustomDialog
struct DialogExample {
controller: CustomDialogController
action: () => void
build() {
Row() {
Button('Close CustomDialog')
.onClick(() => {
this.controller.close()
this.action()
})
}.padding(20)
}
}
@Entry
@Component
struct CustomDialogUser {
dialogController: CustomDialogController = new CustomDialogController({
builder: DialogExample({ action: this.onAccept }),
cancel: this.existApp,
autoCancel: true
});
onAccept() {
console.info('onAccept');
}
existApp() {
console.info('Cancel dialog!');
}
build() {
Column() {
Button('Click to open Dialog')
.onClick(() => {
this.dialogController.open()
})
}
}
}
使用限制与扩展
在生成器函数中的使用限制
ArkTS语言的使用在生成器函数中存在一定的限制:
- 表达式仅允许在字符串(${expression})、if/else条件语句、ForEach的参数以及组件的参数中使用。
- 任何表达式都不能导致应用程序中状态变量(@State、@Link、@Prop)的改变,否则会造成未定义和潜在不稳定的框架行为。
- 生成器函数内部不能有局部变量。
上述限制都不适用于事件方法(如onClick)的匿名函数实现。
变量的双向绑定
ArkTS支持通过$$双向绑定变量,通常应用于状态值频繁改变的变量。
- 当前$$支持基础类型变量,以及@State、@Link和@Prop装饰的变量。
- 当前$$仅支持bindPopup属性方法的show参数,Radio组件的checked属性,Refresh组件的refreshing参数。
- $$绑定的变量变化时,仅渲染刷新当前组件,提高渲染速度。
// xxx.ets
@Entry
@Component
struct bindPopupPage {
@State customPopup: boolean = false
build() {
Column() {
Button('Popup')
.margin(20)
.onClick(() => {
this.customPopup = !this.customPopup
})
.bindPopup($$this.customPopup, {
message: "showPopup"
})
}
}
}
状态变量数据类型声明使用限制
- 所有的状态装饰器变量需要显式声明变量类型,不允许声明any,不支持Date数据类型。示例:
// xxx.ets
@Entry
@Component
struct DatePickerExample {
//错误写法: @State isLunar: any = false
@State isLunar: boolean = false
//错误写法: @State selectedDate: Date = new Date('2021-08-08')
private selectedDate: Date = new Date('2021-08-08')
build() {
Column() {
Button('切换公历农历')
.margin({ top: 30 })
.onClick(() => {
this.isLunar = !this.isLunar
})
DatePicker({
start: new Date('1970-1-1'),
end: new Date('2100-1-1'),
selected: this.selectedDate
})
.lunar(this.isLunar)
.onChange((value: DatePickerResult) => {
this.selectedDate.setFullYear(value.year, value.month, value.day)
console.info('select current date is: ' + JSON.stringify(value))
})
}.width('100%')
}
}
- @State、@Provide、 @Link和@Consume四种状态变量的数据类型声明只能由简单数据类型或引用数据类型的其中一种构成。
类型定义中的Length、Resource、ResourceColor三个类型是简单数据类型或引用数据类型的组合,所以不能被以上四种状态装饰器变量使用。 Length、Resource、ResourceColor的定义请参考类型定义。
示例:
// xxx.ets
@Entry
@Component
struct IndexPage {
//错误写法: @State message: string | Resource = 'Hello World'
@State message: string = 'Hello World'
//错误写法: @State message: ResourceStr = $r('app.string.hello')
@State resourceStr: Resource = $r('app.string.hello')
build() {
Row() {
Column() {
Text(`${this.message}`)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}
自定义组件成员变量初始化的方式与约束
组件的成员变量可以通过两种方式初始化:
- 本地初始化:
@State counter: Counter = new Counter()
- 在构造组件时通过构造参数初始化:
MyComponent({counter: $myCounter})
具体允许哪种方式取决于状态变量的装饰器:
装饰器类型 | 本地初始化 | 通过构造函数参数初始化 |
@State | 必须 | 可选 |
@Prop | 禁止 | 必须 |
@Link | 禁止 | 必须 |
@StorageLink | 必须 | 禁止 |
@StorageProp | 必须 | 禁止 |
@Provide | 必须 | 可选 |
@Consume | 禁止 | 禁止 |
@ObjectLink | 禁止 | 必须 |
常规成员变量 | 推荐 | 可选 |
从上表中可以看出:
- @State变量需要本地初始化,初始化的值可以被构造参数覆盖。
- @Prop和@Link变量必须且仅通过构造函数参数进行初始化。
通过构造函数方法初始化成员变量,需要遵循如下规则:
从父组件中的变量(右)到子组件中的变量(下) | regular | @State | @Link | @Prop | @Provide | @Consume | @ObjectLink |
regular | 支持 | 支持 | 支持 | 支持 | 不支持 | 不支持 | 支持 |
@State | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
@Link | 不支持 | 支持(1) | 支持(1) | 支持(1) | 支持(1) | 支持(1) | 支持(1) |
@Prop | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
@Provide | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
@Consume | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 |
@ObjectLink | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 |
从父组件中的变量(右)到子组件中的变量(下) | @StorageLink | @StorageProp | @LocalStorageLink | @LocalStorageProp |
regular | 支持 | 不支持 | 不支持 | 不支持 |
@State | 支持 | 支持 | 支持 | 支持 |
@Link | 支持(1) | 支持(1) | 支持(1) | 支持(1) |
@Prop @Provide @Consume @ObjectLink | 支持 支持 不支持 不支持 | 支持 支持 不支持 不支持 | 支持 支持 不支持 不支持 | 支持 支持 不支持 不支持 |
说明
支持(1):必须使用$, 例如 this.$varA。
regular:未加修饰的常规变量。
从上表中可以看出:
- 子组件的@ObjectLink变量不支持父组件装饰器变量的直接赋值,其父组件的源必须是数组的项或对象的属性,该数组或对象必现用@State、@Link、@Provide、@Consume或@ObjectLink装饰器修饰。
- 父组件的常规变量可以用于初始化子组件的@State变量,但不能用于初始化@Link、@Consume和@ObjectLink变量。
- 父组件的@State变量可以初始化子组件的@Prop、@Link(通过$)或常规变量,但不能初始化子组件的@Consume变量。
- 父组件的@Link变量不可以初始化子组件的@Consume和@ObjectLink变量。
- 父组件的@Prop变量不可以初始化子组件的@Consume和@ObjectLink变量。
- 不允许从父组件初始化@StorageLink, @StorageProp, @LocalStorageLink, @LocalStorageProp修饰的变量。
- 除了上述规则外,还需要遵循TS的强类型规则。
示例:
@Entry
@Component
struct Parent {
message: string = "Hello World"
build() {
Column() {
Child({
stateMessage: this.message,
/* ArkTS:ERROR The regular property 'message' cannot be assigned
to the @Link property 'linkMessage'.*/
linkMessage: this.$message
})
}
.width('100%')
}
}
@Component
struct Child {
@State stateMessage: string = "Hello World"
@Link linkMessage: string
build() {
Column() {
Text(this.stateMessage)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
}