赞
踩
前面章节介绍了如何ArkTS语言UI范式-基础语法(一)。了解了自定义组件的创建与使用,本章节我们来学习下UI相关的装饰器
。
@Builder装饰器
是自定义构建函数
,ArkUI
中提供了一种更轻量的UI元素复用
机制@Builder
,@Builder
所装饰的函数遵循build()
函数语法规则,开发者可以将重复使用
的UI元素抽象成一个方法,在build
方法里调用。
为了简化语言,我们将@Builder装饰
的函数也称为“自定义构建函数
”。
// 定义
@Builder MyBuilderFunction() { ... }
// 使用
this.MyBuilderFunction()
自定义组件内可以定义多个@Builder方法
,对该组件私有
、不允许组件外调用
,通过this
访问自定义组件的状态变量
而不是参数传递
。
// 定义
@Builder function MyGlobalBuilderFunction() { ... }
// 使用
MyGlobalBuilderFunction()
全局的自定义构建函数可以被整个应用获取
,不允许使用this和bind方法
。如果不涉及组件状态变化,建议使用全局的自定义构建方法。
自定义构建函数的参数传递有按值传递和按引用传递两种,均需遵守以下规则:
类型一致
,不允许undefined、null和返回undefined、null的表达式。@Builder
修饰的函数内部,不允许改变参数值。@Builder
内UI语法遵循UI语法规则。按引用传递参数时,传递的参数可为状态变量,且状态变量的改变会引起@Builder
方法内的UI刷新。ArkUI提供$$
作为按引用传递参数的范式
。
class ABuilderParam { paramA1: string = '' paramB1: string = '' } @Builder function ABuilder($$ : ABuilderParam) {...} class ABuilderParam { paramA1: string = '' } @Builder function ABuilder($$: ABuilderParam) { Row() { Text(`UseStateVarByReference: ${$$.paramA1} `) } } @Entry @Component struct Parent { @State label: string = 'Hello'; build() { Column() { // 在Parent组件中调用ABuilder的时候,将this.label引用传递给ABuilder ABuilder({ paramA1: this.label }) Button('Click me').onClick(() => { // 点击“Click me”后,UI从“Hello”刷新为“ArkUI” this.label = 'ArkUI'; }) } } }
调用@Builder
装饰的函数默认按值传递
。当传递的参数为状态变量时,状态变量的改变不会引起@Builder
方法内的UI刷新。所以当使用状态变量的时候,推荐使用按引用传递
。
@Builder function ABuilder(paramA1: string) { Row() { Text(`UseStateVarByValue: ${paramA1} `) } } @Entry @Component struct Parent { @State label: string = 'Hello'; build() { Column() { ABuilder(this.label) } } }
@BuilderParam
装饰器是引用@Builder
函数。当开发者创建了自定义组件,并想对该组件添加特定功能时,例如在自定义组件中添加一个点击跳转操作。若直接在组件内嵌入事件方法,将会导致所有引入该自定义组件的地方均增加了该功能。为解决此问题,ArkUI
引入了@BuilderParam装饰器
,@BuilderParam
用来装饰指向@Builder
方法的变量,开发者可在初始化自定义组件时对此属性进行赋值,为自定义组件增加特定的功能。该装饰器用于声明任意UI描述的一个元素
,类似slot占位符
。
@BuilderParam
装饰的方法只能被自定义构建函数(@Builder装饰的方法)初始化
。
使用所属自定义组件的自定义构建函数或者全局的自定义构建函数,在本地初始化@BuilderParam
。
@Builder function GlobalBuilder0() {}
@Component
struct Child {
@Builder doNothingBuilder() {};
@BuilderParam aBuilder0: () => void = this.doNothingBuilder;
@BuilderParam aBuilder1: () => void = GlobalBuilder0;
build(){}
}
用父组件自定义构建函数初始化子组件@BuilderParam
装饰的方法。
@Component struct Child { @Builder FunABuilder0() {} @BuilderParam aBuilder0: () => void = this.FunABuilder0; build() { Column() { this.aBuilder0() } } } @Entry @Component struct Parent { @Builder componentBuilder() { Text(`Parent builder `) } build() { Column() { Child({ aBuilder0: this.componentBuilder }) } } }
需注意this
指向正确。
以下示例中,Parent
组件在调用this.componentBuilder()
时,this
指向其所属组件,即“Parent”。@Builder componentBuilder()
传给子组件@BuilderParam aBuilder0
,在Child组件中调用this.aBuilder0()时,this指向在Child的label,即“Child”。对于@BuilderParam aBuilder1
,在将this.componentBuilder传给aBuilder1时,调用bind绑定了this,因此其this.label指向Parent的label
。使用bind
改变函数调用的上下文,可能会使this
指向混乱。
@Component struct Child { label: string = `Child` @Builder FunABuilder0() {} @Builder FunABuilder1() {} @BuilderParam aBuilder0: () => void = this.FunABuilder0; @BuilderParam aBuilder1: () => void = this.FunABuilder1; build() { Column() { this.aBuilder0() this.aBuilder1() } } } @Component struct Parent { label: string = `Parent` @Builder componentBuilder() { Text(`${this.label}`) } build() { Column() { this.componentBuilder() Child({ // 这里是当Child的build调用时才调用this.componentBuilder // 所以this.label指向Child内的 aBuilder0: this.componentBuilder, // 调用bind绑定了this,因此其this.label指向Parent的label。 // 可以理解为这里是先调用this.componentBuilder()那到值后再返回函数赋值给aBuilder1 aBuilder1: (): void => { this.componentBuilder() } }) } } }
@BuilderParam
装饰的方法可以是有参数
和无参数
的两种形式,需与指向的@Builder
方法类型一致
。
class Tmp{ label:string = '' } @Builder function GlobalBuilder1($$ : Tmp) { Text($$.label) .width(400) .height(50) .backgroundColor(Color.Green) } @Component struct Child { label: string = 'Child' @Builder FunABuilder0() {} // 无参数类,指向的componentBuilder也是无参数类型 @BuilderParam aBuilder0: () => void = this.FunABuilder0; // 有参数类型,指向的GlobalBuilder1也是有参数类型的方法 @BuilderParam aBuilder1: ($$ : Tmp) => void = GlobalBuilder1; build() { Column() { this.aBuilder0() this.aBuilder1({label: 'global Builder label' } ) } } } @Entry @Component struct Parent { label: string = 'Parent' @Builder componentBuilder() { Text(`${this.label}`) } build() { Column() { this.componentBuilder() Child({ aBuilder0: this.componentBuilder, aBuilder1: GlobalBuilder1 }) } } }
在自定义组件中使用@BuilderParam
装饰的属性时也可通过尾随闭包
进行初始化。在初始化自定义组件时,组件后紧跟一个大括号“{}”
形成尾随闭包场景
。
@BuilderParam
装饰的属性。不支持使用通用属性
。开发者可以将尾随闭包内的内容看做@Builder
装饰的函数传给@BuilderParam
。示例如下:
// xxx.ets @Component struct CustomContainer { @Prop header: string = ''; @Builder CloserFun(){} @BuilderParam closer: () => void = this.CloserFun build() { Column() { Text(this.header) .fontSize(30) this.closer() } } } @Builder function specificParam(label1: string, label2: string) { Column() { Text(label1) .fontSize(30) Text(label2) .fontSize(30) } } @Entry @Component struct CustomContainerUser { @State text: string = 'header'; build() { Column() { // 创建CustomContainer,在创建CustomContainer时,通过其后紧跟一个大括号“{}”形成尾随闭包 // 作为传递给子组件CustomContainer @BuilderParam closer: () => void的参数 CustomContainer({ header: this.text }) { Column() { specificParam('testA', 'testB') }.backgroundColor(Color.Yellow) .onClick(() => { this.text = 'changeHeader'; }) } } } }
如果每个组件的样式都需要单独设置
,在开发过程中会出现大量代码在进行重复样式设置,虽然可以复制粘贴,但为了代码简洁性和后续方便维护,我们推出了可以提炼公共样式进行复用
的装饰器@Styles
。
@Styles
装饰器可以将多条样式设置提炼成一个方法,直接在组件声明的位置调用。通过@Styles
装饰器可以快速定义并复用自定义样式。用于快速定义并复用自定义样式
。
当前@Styles
仅支持通用属性
和通用事件
。
@Styles
方法不支持参数,反例如下。
// 反例: @Styles不支持参数
@Styles function globalFancy (value: number) {
.width(value)
}
@Styles
可以定义在组件内
或全局
,在全局定义时需在方法名前面添加function
关键字,组件内定义时则不需要添加function
关键字。
// 全局
@Styles function functionName() { ... }
// 在组件内
@Component
struct FancyUse {
@Styles fancy() {
.height(100)
}
}
定义在组件内的@Styles
可以通过this
访问组件的常量和状态变量,并可以在@Styles
里通过事件来改变状态变量的值,示例如下:
@Component
struct FancyUse {
@State heightValue: number = 100
@Styles fancy() {
.height(this.heightValue)
.backgroundColor(Color.Yellow)
.onClick(() => {
this.heightValue = 200
})
}
}
组件内@Styles的优先级高于全局@Styles。 框架优先找当前组件内的@Styles
,如果找不到,则会全局查找。
以下示例中演示了组件内@Styles
和全局@Styles
的用法。
// 定义在全局的@Styles封装的样式 @Styles function globalFancy() { .width(150) .height(100) .backgroundColor(Color.Pink) } @Entry @Component struct FancyUse { @State heightValue: number = 100 // 定义在组件内的@Styles封装的样式 @Styles fancy() { .width(200) .height(this.heightValue) .backgroundColor(Color.Yellow) .onClick(() => { this.heightValue = 200 }) } build() { Column({ space: 10 }) { // 使用全局的@Styles封装的样式 Text('FancyA') .globalFancy() .fontSize(30) // 使用组件内的@Styles封装的样式 Text('FancyB') .fancy() .fontSize(30) } } }
在前文的示例中,可以使用@Styles
用于样式的扩展,在@Styles
的基础上,我们提供了@Extend
,用于扩展原生组件样式
。
语法
@Extend(UIComponentName) function functionName { ... }
使用规则
@Styles
不同,@Extend
仅支持在全局定义,不支持在组件内部定义
。@Styles
不同,@Extend
支持封装指定的组件的私有属性
和私有事件
,以及预定义相同组件的@Extend
的方法。// @Extend(Text)可以支持Text的私有属性fontColor
@Extend(Text) function fancy () {
.fontColor(Color.Red)
}
// superFancyText可以调用预定义的fancy
@Extend(Text) function superFancyText(size:number) {
.fontSize(size)
.fancy()
}
和@Styles
不同,@Extend
装饰的方法支持参数
,开发者可以在调用时传递参数,调用遵循TS
方法传值调用。
// xxx.ets @Extend(Text) function fancy (fontSize: number) { .fontColor(Color.Red) .fontSize(fontSize) } @Entry @Component struct FancyUse { build() { Row({ space: 10 }) { Text('Fancy') .fancy(16) Text('Fancy') .fancy(24) } } }
@Extend
装饰的方法的参数可以为function
,作为Event
事件的句柄。
@Extend(Text) function makeMeClick(onClick: () => void) { .backgroundColor(Color.Blue) .onClick(onClick) } @Entry @Component struct FancyUse { @State label: string = 'Hello World'; onClickHandler() { this.label = 'Hello ArkUI'; } build() { Row({ space: 10 }) { Text(`${this.label}`) .makeMeClick(() => {this.onClickHandler()}) } } }
@Extend
的参数可以为状态变量
,当状态变量改变时,UI可以正常的被刷新渲染。
@Extend(Text) function fancy (fontSize: number) { .fontColor(Color.Red) .fontSize(fontSize) } @Entry @Component struct FancyUse { @State fontSizeValue: number = 20 build() { Row({ space: 10 }) { Text('Fancy') .fancy(this.fontSizeValue) .onClick(() => { this.fontSizeValue = 30 }) } } }
以下示例声明了3个Text组件,每个Text
组件均设置了fontStyle
、fontWeight
和backgroundColor
样式。
@Entry @Component struct FancyUse { @State label: string = 'Hello World' build() { Row({ space: 10 }) { Text(`${this.label}`) .fontStyle(FontStyle.Italic) .fontWeight(100) .backgroundColor(Color.Blue) Text(`${this.label}`) .fontStyle(FontStyle.Italic) .fontWeight(200) .backgroundColor(Color.Pink) Text(`${this.label}`) .fontStyle(FontStyle.Italic) .fontWeight(300) .backgroundColor(Color.Orange) }.margin('20%') } }
@Extend
将样式组合复用,示例如下。
@Extend(Text) function fancyText(weightValue: number, color: Color) {
.fontStyle(FontStyle.Italic)
.fontWeight(weightValue)
.backgroundColor(color)
}
通过@Extend
组合样式后,使得代码更加简洁,增强可读性。
@Entry @Component struct FancyUse { @State label: string = 'Hello World' build() { Row({ space: 10 }) { Text(`${this.label}`) .fancyText(100, Color.Blue) Text(`${this.label}`) .fancyText(200, Color.Pink) Text(`${this.label}`) .fancyText(300, Color.Orange) }.margin('20%') } }
@Styles
和@Extend
仅仅应用于静态页面的样式复用
,stateStyles
可以依据组件的内部状态的不同,快速设置不同样式。这就是我们要介绍的内容stateStyles
(又称为:多态样式)。
stateStyles
是属性方法
,可以根据UI内部状态来设置样式,类似于css
伪类,但语法不同。ArkUI
提供以下五种状态:
focused
:获焦态。normal
:正常态。pressed
:按压态。disabled
:不可用态。selected10+
:选中态。下面的示例展示了stateStyles
最基本的使用场景。Button
处于第一个组件,默认获焦,生效focused
指定的粉色样式。按压时显示为pressed
态指定的黑色。如果在Button
前再放一个组件,使其不处于获焦态,就会生效normal
态的黄色。
@Entry @Component struct StateStylesSample { build() { Column() { Button('Click me') .stateStyles({ focused: { .backgroundColor(Color.Pink) }, pressed: { .backgroundColor(Color.Black) }, normal: { .backgroundColor(Color.Yellow) } }) }.margin('30%') } }
@Styles
和stateStyles
联合使用
以下示例通过@Styles
指定stateStyles
的不同状态。
@Entry @Component struct MyComponent { @Styles normalStyle() { .backgroundColor(Color.Gray) } @Styles pressedStyle() { .backgroundColor(Color.Red) } build() { Column() { Text('Text1') .fontSize(50) .fontColor(Color.White) .stateStyles({ normal: this.normalStyle, pressed: this.pressedStyle, }) } } }
在stateStyles
里使用常规变量和状态变量
stateStyles
可以通过this
绑定组件内的常规变量和状态变量。
@Entry @Component struct CompWithInlineStateStyles { @State focusedColor: Color = Color.Red; normalColor: Color = Color.Green build() { Column() { Button('clickMe').height(100).width(100) .stateStyles({ normal: { .backgroundColor(this.normalColor) }, focused: { .backgroundColor(this.focusedColor) } }) .onClick(() => { this.focusedColor = Color.Pink }) .margin('30%') } } }
Button
默认获焦显示红色,点击事件触发后,获焦态变为粉色。
@AnimatableExtend
装饰器用于自定义可动画的属性方法,在这个属性方法中修改组件不可动画的属性。在动画执行过程时,通过逐帧回调函数修改不可动画属性值,让不可动画属性也能实现动画效果。
可动画属性
:如果一个属性方法在animation
属性前调用,改变这个属性的值可以生效animation属性的动画效果,这个属性称为可动画属性。比如height、width、backgroundColor、translate属性,Text组件的fontSize属性等。
不可动画属性
:如果一个属性方法在animation
属性前调用,改变这个属性的值不能生效animation属性的动画效果,这个属性称为不可动画属性。比如Ployline组件的points属性等。
该装饰器从API Version 10开始支持
。后续版本如有新增内容,则采用上角标单独标记该内容的起始版本。
语法
@AnimatableExtend(UIComponentName) function functionName(value: typeName) {
.propertyName(value)
}
1. @AnimatableExtend
仅支持定义在全局
,不支持在组件内部定义。
2. @AnimatableExtend
定义的函数参数类型必须为number
类型或者实现 AnimtableArithmetic<T>
接口的自定义类型。
3. @AnimatableExtend
定义的函数体内只能调用@AnimatableExtend
括号内组件的属性方法
。
AnimtableArithmetic<T>
接口说明
对复杂数据类型做动画,需要实现AnimtableArithmetic接口中加法、减法、乘法和判断相等函数。
以下示例实现字体大小的动画效果。
@AnimatableExtend(Text) function animatableFontSize(size: number) { .fontSize(size) } @Entry @Component struct AnimatablePropertyExample { @State fontSize: number = 20 build() { Column() { Text("AnimatableProperty") .animatableFontSize(this.fontSize) .animation({duration: 1000, curve: "ease"}) Button("Play") .onClick(() => { this.fontSize = this.fontSize == 20 ? 36 : 20 }) }.width("100%") .padding(10) } }
以下示例实现折线的动画效果。
class Point { x: number y: number constructor(x: number, y: number) { this.x = x this.y = y } plus(rhs: Point): Point { return new Point(this.x + rhs.x, this.y + rhs.y) } subtract(rhs: Point): Point { return new Point(this.x - rhs.x, this.y - rhs.y) } multiply(scale: number): Point { return new Point(this.x * scale, this.y * scale) } equals(rhs: Point): boolean { return this.x === rhs.x && this.y === rhs.y } } class PointVector extends Array<Point> implements AnimatableArithmetic<PointVector> { constructor(value: Array<Point>) { super(); value.forEach(p => this.push(p)) } plus(rhs: PointVector): PointVector { let result = new PointVector([]) const len = Math.min(this.length, rhs.length) for (let i = 0; i < len; i++) { result.push((this as Array<Point>)[i].plus((rhs as Array<Point>)[i])) } return result } subtract(rhs: PointVector): PointVector { let result = new PointVector([]) const len = Math.min(this.length, rhs.length) for (let i = 0; i < len; i++) { result.push((this as Array<Point>)[i].subtract((rhs as Array<Point>)[i])) } return result } multiply(scale: number): PointVector { let result = new PointVector([]) for (let i = 0; i < this.length; i++) { result.push((this as Array<Point>)[i].multiply(scale)) } return result } equals(rhs: PointVector): boolean { if (this.length != rhs.length) { return false } for (let i = 0; i < this.length; i++) { if (!(this as Array<Point>)[i].equals((rhs as Array<Point>)[i])) { return false } } return true } get(): Array<Object[]> { let result: Array<Object[]> = [] this.forEach(p => result.push([p.x, p.y])) return result } } @AnimatableExtend(Polyline) function animatablePoints(points: PointVector) { .points(points.get()) } @Entry @Component struct AnimatablePropertyExample { @State points: PointVector = new PointVector([ new Point(50, Math.random() * 200), new Point(100, Math.random() * 200), new Point(150, Math.random() * 200), new Point(200, Math.random() * 200), new Point(250, Math.random() * 200), ]) build() { Column() { Polyline() .animatablePoints(this.points) .animation({duration: 1000, curve: "ease"}) .size({height:220, width:300}) .fill(Color.Green) .stroke(Color.Red) .backgroundColor('#eeaacc') Button("Play") .onClick(() => { this.points = new PointVector([ new Point(50, Math.random() * 200), new Point(100, Math.random() * 200), new Point(150, Math.random() * 200), new Point(200, Math.random() * 200), new Point(250, Math.random() * 200), ]) }) }.width("100%") .padding(10) } }
参考文献:
[1]OpenHarmoney应用开发文档
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。