本文是结合HarmonyOS3.1/4.0的开发文档,结合一些代码示例而成的,如有不妥,请告知。
前言
ArkTs是HarmonyOS主推的应用开发语言。ArkTs是基于Typescript进一步扩展出来的产物,继承了Typescript的所有特性,是typescript的超集。
不过,ArkTs还扩展了如下的能力:
- 基本语法:ArkTs定义了声明式UI描述、自定义组件和动态扩展UI元素的能力;
- 状态管理:ArkTs提供了多维度的状态管理机制。在UI开发框架中,与UI相关联的数据可以在组件内使用,也可以在不同组件层级间传递,解决了相同以及不同层级组件之间的通讯。
- 渲染控制:ArkTs提供了渲染控制能力条件渲染可根据应用的不同状态,渲染对应状态下的UI内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件,数据懒加载从数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。
1 基本语法
因为ArkTs是基于Typescript扩展的,所以语法基本跟Typescript一致的,需要先熟悉一下Typescript,再来学习ArkTs,如果没有了解过Typescript的同学,可以去Typescript的官网看看。
1.1 基本语法概述
了解ArkTs的基本语法,我们将从下面一段代码去分析一下
@Entry // 装饰器
@Component // 装饰器
// struct Index:定义自定义组件
struct Index {
// @State 装饰器
@State message: string = 'Hello World'
// 整个build 方法里的内容属于UI描述,Row、Column等属于内置组件
build() {
Row() {
Column() {
Text(this.message)
// 事件方法
.onClick(() => {
console.log('---');
})
// 下面两个是属性方法
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
// DemoComponent()
}
.height('100%')
}
}
具体描述:
- 装饰器:用于装饰类、结构、方法以及变量,并赋予特殊含义。如上述的代码片段中@Entry/@Component/@State都属于装饰器,@表示自定义组件,@Entry表示该自定义组件为入口组件,@State表示组件中的状态变量,类似于赋予变量响应式,状态变量值发生变化会触发组件更新。
- UI描述:以声明式的方式来描述UI的结构,如build方法中的代码块(有点类似Flutter)。
- 自定义组件:可复用的UI单元,可组合其它组件。如上面代码被@Component装饰的struct Index。struct是固定范式,类似于定义类的class。
- 内置组件:ArkUI框架中默认内置的基础组价和容器组件,可直接使用,无需引入。
- 容器组件:Row、Column等。
- 基础组件:Text、Button等
- 属性方法:在ArkTs中组件是可以通过链式调用配置多项属性的,如上述的fontSize()、fontWeight()、height()等。
- 事件方法:组件可以通过链式调用设置多个事件的响应逻辑,如上述代码中Text后面的onClick()。
其它更多详细的系统组件、属性方法等介绍,可以参考 ArkTs的声明式开发范式
1.2 声明式UI简介
Harmony主推的ArkTs,为了帮助开发者实现应用交互逻辑,以声明方式组合和扩展组件来描述应用程序的UI,同时提供了可链式调用的基本属性、事件方法等,极大程度地帮助开发者提高开发效率。
1.2.1 创建组件
根据ArkTs提供的组件构造方法不同,有两种方式创建组件:有参和无参
注意:因为struct 关键字的原因,创建组件的时候不需要通过new运算符去创建
1.2.1.1 无参数组件
当组件的接口定义了无参构造函数,则组件后面的"()"不需要传递任何参数,内置组件Divider就是如此。
Row() {
Button("确定").onClick(() => {
this.btnText = "删除";
console.log("======", this.btnText);
}).width(120)
.height(40)
.backgroundColor(Color.White)
.fontColor("#ff2064")
// 分割线
Divider()
// 文本显示
Text(this.btnText)
}
1.2.1.2 有参数组件
当组件的接口定义了有参构造函数(参数分必填和可选),则在组件后面的"()"传递对应的参数。
- Button组件的非必填参数options
button组件源码中构造参数定义,如下图:
// 无参形式
Button()
.width(120)
.height(40)
// 有参形式:string类型参数以及非必填参数options
Button("确定", { type: ButtonType.Capsule })
.width(120)
.height(40)
.backgroundColor(Color.Orange)
.fontColor("#fff")
// $r形式引入应用资源,可用于多语言环境 title_value为自定义属性,其配置如下图
Button($r('app.string.title_value'))
.width(120)
.height(40)
- Image组件的必填参数src
// 网络图片在真机或虚拟机上需要配置网络请求权限才可以
Image("https://test/test.png")
1.2.2 配置属性
组件属性方法以"."链式调用方式进行配置内置组件的样式和其他属性
- 配置Button组件的字体颜色
Button("按钮")
.fontColor("#fff")
- 配置组件的多个属性
Button("确定", { type: ButtonType.Capsule })
.width(120)
.height(this.height ? this.height : 20)
.fontColor("#fff")
- 对于内置组件,ArkUI还为其属性定义了一些枚举类型给开发者调用,例如Button组件的type等
Button("确定", { type: ButtonType.Capsule })
Text("Hello ArkTs")
.fontColor(Color.Orange)
.fontWeight(FontWeight.Bold)
1.2.3 配置事件
事件方法跟其他属性一样,也是以"."链式调用的方式使用,下面介绍一下几种事件方法。
- 使用箭头函数配置组件的事件方法
Button($r('app.string.title_value'))
.onClick(() => {
console.log("这是一个箭头函数");
})
- 使用匿名函数表达式配置组件的事件方法,这里需要保证函数体的this指向当前组件。
Button($r('app.string.title_value'))
.onClick(function(){
this.btnText = "测试";
console.log("===this====", this.btnText); // 输出:测试
}.bind(this))
- 使用成员函数来配置组件的事件方法
// 定义成员函数跟定义变量同级
handleClick(): number {
return 100;
}
...
Button($r('app.string.title_value'))
.onClick(this.handleClick.bind(this))
- 使用声明的箭头函数配置组件的事件方法,不行bind this
struct DemoComponent {
@State btnText: string = 'Hello World'
handleFn = (): number => {
return 100;
}
build() {
Row() {
Button($r('app.string.title_value'))
.onClick(this.handleFn)
}
}
}
1.2.4 配置子组件
组件如果支持子组件配置,则需在闭包"{…}"中为组件添加子组件。
- 下面是简单的Row组件配置子组件的示例
Row() {
Text("这是一行文本!")
Divider()
Button("删除按钮")
}
- 在内置组件中容器组件都支持子组件配置,可以实现一些复杂的嵌套
Row() {
Column() {
Row() {
Text("这是一行文本!")
.fontSize(14)
.fontColor(Color.Red)
}
Row() {
Button("删除")
.onClick(() => {
console.log("删除操作");
})
}
.justifyContent(FlexAlign.End)
}
}
1.3 自定义组件
要实现一个复杂的功能,基本都不会在同一个组件中实现整个业务逻辑,这个时候需要对整个功能页面进行拆解成不同的小块,这时候自定义组件就派上用场了。
1.3.1 创建自定义组件
自定义组件具有以下特点:
- 可组合:允许开发者组合使用内置组件、以及其属性和方法
- 可重用:自定义组件可以被其它组件重用,并作为不同的实例在不同的父组件或容器中使用
- 数据驱动UI更新:通过状态变量的变更,来驱动UI的更新
自定义组件的基本用法
说明:如果该组件需要给到其它组件调用,则需要使用关键字export导出,使用需要使用关键字import导入
@Component
export struct Child {
@State count: number = 0;
build() {
Row() {
Text(`the count is ${this.count}`)
Button("点击+1")
.onClick(() => {
this.count += 1;
})
}
.width("100%")
.height("100%")
}
}
在其它组件中使用child
import { Child } from './child'
@Component
struct Father {
build() {
Column() {
Text("这是父组件!")
// 在子组件定义的count变量会编程可选参数
Child({ count: 20 })
Divider()
// 多次重复使用
Child()
}
}
}
1.3.2 自定义组件简介
上面简单讲了自定义组件的基本使用,这里我们将对自定义组件做进一步的了解。
-
自定义组件的基本结构
- struct:自定义组件是基于struct实现的,struct + 组件名称 + {…}的组合构成了自定义组件,而且不允许有继承关系。struct实例化的时候,可以省略new关键字。
注意:自定义组件组件名、类名、函数名都能和内置组件名相同。
- @Component:该装饰器仅能装饰struct关键字声明的数据结构,自定义组件必须有该装饰器声明。struct被@Component装饰后,具备组件化能力,而实现build方法是用来描述UI,同时一个struct只能被一个@Component装饰。
说明:从API 9 开始,@Component装饰器支持在ArkTs卡片中使用。
- build()函数:该函数用于自定义组件的声明式UI描述,自定义组件必须实现该方法,否则报错。
@Component struct Demo1 { build() { ... } }
- @Entry:该装饰器装饰的自定义组件将作为页面的入口。在单个UI页面中,有且只有一个@Entry装饰一个自定义组件。该装饰器接收一个可选localStorage参数。
说明:该装饰器也是从API 9 开始支持在ArkTs 卡片中使用
-
成员函数和变量
自定义组件除了必须实现的build函数,还有其他一些可选成员函数,比如onPageShow、aboutToDisappear等,具体可自查其源码。成员函数具有以下约束:
- 不支持静态函数
- 成员函数的访问是私有的
自定义组件可以有成员变量,但是成员变量具有以下约束:
- 不支持静态成员变量
- 所有成员变量都是私有的,变量的访问规则与成员函数访问规则相同。
- 自定义组件的成员变量初始化有些是可选的,有些是必选的,具体看是否需要本地初始化,是否需要从父组件传递参数作为子组件的初始化成员变量。
- build函数
所有声明在build函数的语言,在ArkTs中统称为UI描述,而UI描述有着相对严格的规则约束。
- @Entry装饰的自定义组件,build()函数下的根节点必须是唯一的,而且是容器组件,其中ForEach禁止作为根节点。
@Component装饰的自定义组件,其build()函数下的根节点也必须是唯一的,可以是非容器组件,同样禁止ForEach作为根节点。
import { DemoComponent } from './demo/Demo1'
@Entry
@Component
struct Index {
@State message: string = 'Hello World'
build() {
// 根节点是容器组件且唯一的
Row() {
DemoComponent()
Text("测试")
}
.height('100%')
}
}
@Component
export struct Child {
build() {
Text("这是子组件")
}
}
- 不允许声明本地变量,错误示例如下。
...
build() {
let a: string = '错误示例';
}
- 不允许在UI描述里直接使用console打印信息,但可以在方法或者函数里使用。
@Component
export struct Child {
build() {
console.log("打印信息"); // 报错提示: console.log' does not comply with the UI component syntax. <etsLint>
Text("这是子组件")
.onClick(() => {
console.log("正确使用");
})
}
}
- 不允许创建本地作用域,如下
...
build() {
{
...
}
}
- 不允许调用方法体或返回值是组件(自定义或内置)以及方法体为空的且没有@Builder装饰的方法,但允许内置组件的参数是TS方法的返回值。
@Component
export struct Child {
@State count: number = 0;
@Builder doSomethings() {
Text("111111")
}
getTextVal(): string {
return 'value';
}
hasNothing() {
}
build() {
// 错误示例
this.hasNothing()
// 正确使用
this.doSomethings()
Text(this.getTextVal())
}
}
- 不允许使用Switch、表达式,如需判断可以使用if
@Component
export struct Child {
@State count: number = 0;
build() {
// 错误示例
switch(count) {
case 1:
Text("1")
break;
case 2:
Button("2")
break;
}
// 错误示例
this.count > 10 ? Text("111") : Button("222")
// 正确示例
if (this.count == 0) {
Text("正确操作")
} else {
Image("https://这是一张图片")
}
}
}
1.3.3 组件的生命周期
ArkTs的生命周期,分为页面和自定义组件的生命周期,有相同之处也有区别的地方
二者的关系:
- 自定义组件:由@Component装饰的UI单元,可以组合多个内置组件实现UI复用,可以调用组件的生命周期。
- 页面:即应用的页面。由一个或者多个自定义组件组成,@Entry装饰的自定义组件为页面的入口组件,即页面的根节点,一个页面有且只能有一个@Entry装饰器。只有被@Entry装饰的组件才可以调用页面的生命周期。
下面我们简单了解一下页面生命周期:
组件的生命周期函数声明在源码的common.d.ts文件下,可以自行去查看
- onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景,仅@Entry装饰的自定义组件生效
// Index.ets
import { DemoComponent } from './demo/Demo1'
@Entry
@Component
struct Index {
// 该钩子函数触发了
onPageShow() {
console.log("=====Index pageShow=====");
}
build() {
Row() {
DemoComponent()
}
.height('100%')
}
}
// DemoComponent.ets
@Component
export struct DemoComponent {
@State btnText: string = 'Hello World'
// 该钩子函数没有触发
onPageShow() {
console.log("======demo1 pageShow======");
}
build() {
Row() {
Button("按钮")
.width(120)
.height(40)
}
}
}
- onPageHide:页面每次隐藏时触发一次,包括路由过程、应用进入后台等场景,仅@Entry装饰的自定义组件生效。
- onBackPress:当用户点击返回按钮时触发,仅@Entry装饰的自定义组件生效。
这里使用IDE的预览模拟了一下,生效
组件生命周期,即一般使用@Component装饰的自定义组件的生命周期,如下:
- aboutToAppear:组件即将出现时会调用该钩子,具体时机在为创建自定义组件的新实例后,在执行build()函数之前执行。而父组件的比子组件的先执行。
- aboutToDisappear:在自定义组件析构销毁之前执行。不允许在该钩子函数中变更状态变量,特别是@Link变量的修改可能会导致应用程序行为不稳定。该钩子函数在父组件中比子组件先执行。
上述生命周期流程如下所示,展示的是被@Entry装饰的组件或者页面生命周期。
根据上面的流程图,从自定义组件的初始创建、重新渲染和删除来简单解释一下。
自定义组件的创建和渲染流程
- 自定义组件的创建:其实例由ArkTsUI框架创建。
- 初始化自定义组件的成员变量:通过本地默认值或者构造方法参数来初始化组件的成员变量,初始化顺序为定义成员变量的顺序。
- 成员变量初始化结束,如果定义了aboutToAppear钩子函数,则执行该钩子函数
- 在首次渲染的时候,build方法渲染内置组件,如果子组件为自定义组件,则创建自定义组件实例。在执行build()函数过程中,框架会观察每个状态变量的读取状态,将保存两个map (具体可查看源码):
a.状态变量 -> UI组件(包括ForEach和if)
b.UI组件 -> 此组件的更新函数,即一个lambda方法,作为build()函数的子集,创建对应的UI组件并执行其属性方法,示例:
build() {
...
this.observeComponentCreation(() => {
Button.create();
})
this.observeComponentCreation(() => {
Text.create();
})
...
}
当应用在后台启动时,此时应用并没有被销毁,所以仅需要执行onPageShow钩子函数。
自定义组件重新渲染
当组件中的状态变量发生改变的时,或者localStorage/AppStorage中的属性发生变更而导致绑定的状态变量改变时:
- 框架观察到了变化,则会触发组件重新渲染
- 根据初次渲染时存储的两个map,框架可以知道该状态变量管理了哪些UI组件,以及这些UI组件对应的更新函数。执行这些需要变更的更新函数,从而实现最小化更新。
自定义组件的删除
如果if组件的分支发生改变或者ForEach循环渲染中数组的个数发生改变,组件将被删除
- 在删除组件之前,将先调用aboutToDisappear钩子函数,标记着该节点将要被销毁。ArkUI的节点删除机制是:后端节点直接从组件树上摘下,后端节点被销毁,对前端节点解开引用,前端节点已经没有引用时,将被JS虚拟机垃圾回收。
- 自定义组件和它的变量将被删除,如果有同步的变量,比如@Link、@Prop、@StorageLink,将从同步源上取消注册。
自定义组件的生命周期函数中还有几个不算太常用(个人意见,勿喷)的
- pageTransition:实现页面跳转间的动画效果
- onLayout:自定义组件中,该钩子函数可以用来覆盖其子组件的布局。该API将从API 10 开始被onPlaceChildren替换(疑似社区的官方回答)
- onMeasure:自定义组件中,该钩子函数可以用来测量(计算)其子组件大小。该API将从API 10 开始被onMeasureSize替换(疑似社区的官方回答)
笔者目前也还没搞清楚onLayout和onMeasure两个的具体使用,社区上有人说是跟安卓的layout类似,笔者不会安卓,安卓同学方便可以告知一下。
下面是简单的示例:
// 官方示例
// 定义一个CustomLayout组件
@Component
export struct CustomLayout {
@BuilderParam builder: () => void
//
onLayout(children: Array<LayoutChild>, constraint: ConstraintSizeOptions) {
let pos = 0;
children.forEach((child) => {
child.layout({ position: { x: pos, y: pos }, constraint: constraint })
child.borderInfo.borderWidth = { top: 2, left: 2, right: 2, bottom: 2 }
pos += 100;
})
}
onMeasure(children: Array<LayoutChild>, constraint: ConstraintSizeOptions) {
let size = 100;
children.forEach((child) => {
child.measure({ minHeight: size, minWidth: size, maxWidth: size, maxHeight: size })
size += 50;
})
}
build() {
this.builder()
}
}
// DemoIndex.ets 中使用
import { CustomLayout } from './CustomLayout'
@Entry
@Component
struct DemoIndex {
build() {
Row() {
Column() {
Button("按钮")
.width("100%")
.height(50)
.backgroundColor(Color.Blue)
CustomLayout() {
ForEach([1,2,3], index => {
Text("ArkTs " + index)
.fontSize(30)
.fontColor(Color.Red)
.backgroundColor(Color.Black)
})
}
}
}
.height("100%")
.width("100%")
}
}
预览效果图:看结构剖析,显示内容跟结构节点(虚拟框框)不在同一位置
1.3.4 应用生命周期
1.4 常用装饰器
1.4.1 @Builder装饰器:自定义构建函数
前面我们了解了自定义组件的创建及其基本结构的理解。除了一些固定的结构之外,ArkUI还提供了一种更轻量的UI元素复用机制@Builder,被@Builder修饰的函数遵循build()函数语法规则,我们可以把一些重复的UI结构抽象到一个方法,方便在build()函数中调用。
@Builder的简单使用,如下:
- 自定义组件内使用
语法:
@Builder myFnc(){...}
@Component
struct DemoBuilder {
// 自定义构建函数
@Builder showRepeatCode() {
Text("重复UI调用")
.fontColor(Color.Red)
.fontSize(12)
}
build() {
this.showRepeatCode()
}
}
- 自定义组件内允许有一个或者多个@Builder方法,这些方法被默认为是组件的私有、特殊类型的成员函数。
- 自定义构建函数不允许在所属组件外调用,可以在所属组件的build方法或者其他自定义构建函数中使用。
- 在自定义构建函数中,this指向当前所属组件。
- 全局自定义构建函数
语法:
@Builder
function myGlobalBuilder(){...}
下面用一个简单的例子说明一下:
// 全局定义自定义构建函数 builder.ets
@Builder
export function myGlobalBuilder() {
Column(){
Text("全局Builder")
.fontColor(Color.Blue)
.fontSize(30)
}
.height(100)
.width(300)
.border({ width: 2, color: Color.Black })
}
// 调用组件 DemoBuilder.ets
import { myGlobalBuilder } from '../../utils/builder'
@Component
export struct DemoBuilder {
// 自定义构建函数
@Builder showRepeatCode() {
Text("重复UI调用")
.fontColor(Color.Red)
.fontSize(30)
}
build() {
Column() {
this.showRepeatCode()
myGlobalBuilder() // 全局自定义构建函数
}
}
}
由上述例子我们可以得出以下两点:
- 全局的自定义构建函数可以被整个应用使用,不能使用this和bind方法
- 单纯的静态展示可以直接使用全局自定义构建函数,不建议传参,如果涉及状态变更,推荐使用自定义组件。
参数传递规则
自定义构建函数的参数传递有两种:按值传递和按引用传递,均需要遵守以下规则:
- 不允许传递undefined、null和返回undefined、null。
- 在自定义构建函数内部,不允许改变参数值。如果需要改变参数值,且同步回调用点,建议使用@Link。
- @Builder内UI语法遵循UI语法规则。
-只有传入一个参数,且参数需要直接传入对象字面量才会按引用传递该参数,其余传递方式均为按值传递。
1. 按引用传递参数
按引用类型传递参数时,传递参数可以是状态变量,变量值的变化也会引起@Builder方法内的UI更新。ArkUI提供$$作为按引用传递参数的范式。
语法方式:
BuilderDemo($$: {param1: string, param2: string});
示例1:官方范式
@Builder function ABuilder($$: { paramA1: string }) {
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';
})
}
}
}
示例2:自定义类型
interface ItemList {
id: number,
name: string,
value?: string
}
@Builder function ItemCard(item: ItemList) {
Row() {
Text(item.name)
.fontColor(Color.White)
.width(120)
if (item.value) {
Text(item.value)
.fontColor(Color.Blue)
.fontSize(30)
}
}
.justifyContent(FlexAlign.SpaceBetween)
}
@Component
export struct ListBuilder {
@State list: Array<ItemList> = [
{ id: 1, name: '测试1', value: 'hello' },
{ id: 2, name: '发送接口' },
{ id: 5, name: '数据库的费' }
]
build() {
Column() {
List({ space: 20 }) {
ForEach(
this.list,
item => {
ListItem() {
ItemCard(item)
}
.width("100%")
.height(60)
.padding(10)
.backgroundColor("#ff6602")
}
)
}
Button("添加一行")
.onClick(() => {
const item: ItemList = this.list[this.list.length - 1];
this.list.push({
id: item.id + 1,
name: "增加一行" + item.id + 1,
value: item.id % 3 === 0 ? "add row" + item.id : ""
})
})
.margin({ top: 30 })
.width("100%")
.height(45)
.fontSize(22)
.fontColor(Color.White)
.backgroundColor(Color.Blue)
}
.width("100%")
}
}
2. 按值传递参数
调用@Builder装饰的函数默认按值传递,当传递的参数为状态变量的时,变量值的改变并没有触发组件的更新。所以传递参数使用状态变量的时候,推荐使用按引用传递。
示例:
@Builder function updateByValue(value: number) {
Row() {
Text("the number is " + value)
.fontSize(20)
.fontColor(Color.White)
}.width("100%").height(30).backgroundColor("#ff6602")
}
@Component
export struct ValueBuilder {
@State num: number = 10;
build() {
Column() {
updateByValue(this.num)
Button("添加一行")
.onClick(() => {
this.num = 100;
})
.margin({ top: 30 })
.width("100%")
.height(45)
.fontSize(22)
.fontColor(Color.White)
.backgroundColor(Color.Blue)
}
.width("100%")
}
}
1.4.2 @BuilderParam装饰器
作为开发者的我们,在创建一个自定义组件的时候,而该组件承载的是公共的适应多页面使用的,当需要在该组件内设置某个页面需要的特定功能时,我们不能直接在组件内部去设置,这样不仅会导致组件更加臃肿,而且会导致其它使用该组件的地方增加了本来不需要的功能。
为了解决这种问题,ArkUI引入了@BuilderParam装饰器,用来装饰执行@Builder方法的变量,这样开发者就可以根据需要,在自定义组件初始化前传递自己需要的属性,设置特定的功能点。这个装饰器的作用就类似前端开发中的占位符slot。
装饰器使用说明
根据官方文档,@BuilderParam装饰的方法只能被自定义构建函数 (@Builder装饰的方法) 初始化。
下面通过代码展示一下:
- 使用所属自定义的自定义构建函数或全局自定义构建函数,在本地初始化@BuilderParam
- 父组件自定义构建函数初始化子组件自定义@BuilderParam装饰的方法
// 编写BuilderParamDemo.ets
@Builder function WholeApp1() {
Text("===WholeApp1===")
}
@Component
export struct BuilderParamDemo {
@Builder doNothing() {}
@BuilderParam builderParam1: () => void = WholeApp1;
@BuilderParam builderParam2: () => void = this.doNothing;
build() {
Column() {
this.builderParam1()
Row().margin({ top: 20, bottom: 20 })
this.builderParam2()
}
}
}
// 在Index.ets中使用
import { BuilderParamDemo } from './demo/BuilderParamDemo';
@Entry
@Component
struct Index {
@Builder updateBuilderParam() {
Button("测试")
.width("100%")
.height(60)
.backgroundColor(Color.Brown)
}
build() {
Row() {
// 无参
// BuilderParamDemo()
// 有参--父组件自定义构建函数初始化子组件自定义@BuilderParam装饰的方法
BuilderParamDemo({
builderParam2: this.updateBuilderParam
})
}
.height('100%')
}
}
下面来个综合的例子,演示一下@BuilderParam装饰的自定义构建函数在以下两个场景中的使用:
- 参数初始化组件
- 尾随闭包初始化组件
@Builder function WholeBuilder($$: { value: string }) {
Text($$.value)
.fontSize(30)
.fontColor(Color.Red)
}
@Entry
@Component
struct BuilderIndex {
childVal: string = "parent component"
@Builder ParentBuilder($$: { value: string }) {
Text($$.value)
.fontColor(Color.Black)
.fontSize(30)
}
build() {
Column(){
// 场景1:通过参数初始化
ChildIndex({ showItem: WholeBuilder })
Divider()
ChildIndex({ showItem: WholeBuilder, title: "BuilderIndex" })
// 场景2:尾随闭包初始化组件
// 该场景下自定义组件内有且只有一个使用@BuilderParam装饰的属性
Divider()
ChildIndex() {}
Divider()
ChildIndex({ title: "测试场景2-2" }) {}
Divider()
ChildIndex({ title: "测试场景2-3" }) {
this.ParentBuilder({ value: "the parent Builder" })
}
Divider()
ChildIndex({ title: "测试场景2-4" }) {
Button("Parent button")
.width(300)
.height(60)
.fontSize(26)
.backgroundColor("#ff6602")
}
}
.width("100%")
.height("100%")
}
}
@Component
struct ChildIndex {
@State title: string = "ChildIndex"
@BuilderParam showItem: ($$: { value: string | number }) => void
build() {
Row() {
Column() {
Text(this.title)
.fontColor(Color.Blue)
.fontSize(30)
.margin({ bottom: 20 })
this.showItem({ value: "default value" })
}
.width("100%")
}
.margin({ top: 10, bottom: 10 })
}
}
当我们把ChildIndex中@BuilderParam装饰的自定义构建函数注释掉,不在build()函数执行,在parent组件中使用的效果如下图,发现slot效果消失了,所以定义之后的@BuilderParam构建函数一定要在build中执行才有效果。
1.5 样式装饰器
样式是应用开发中重要的一环,几乎每个组件都必须要设置,也不可避免的出现大量重复的样式代码,这样不仅造成单个文件内代码量过多,而且不容易维护,不容易扩展等问题,所以ArkUI给开发者提供了相应的装饰器解决问题。
1.5.1 @Styles装饰器
@Styles装饰器可以把多条样式写在一个方法,可以在组件需要的地方调用即可。下面是该装饰器支持的与限制的用法
- 目前@Styles仅支持通用属性和通用事件。
- @Styles方法不支持参数
- @Styles可以定义在组件内或全局,在全局定义时需要在方法名前添加function关键字,组件内定义则不需要(跟@Builder类似)。
- 定义在组件内的@Styles可以访问组件的常量和状态变量,还可以通过事件来改变状态变量的值。
- 组件内的@Styles的优先级高于全局的。
通过下面的代码,我们熟悉一下@Styles
// 全局样式
@Styles function GlobalStyles() {
.width("100%")
.height("100%")
.backgroundColor("#f8f8f8")
}
@Entry
@Component
struct SytlesIndex {
@State tColor: string = "#FF164FFA"
// 组件内定义
@Styles txt_cls() {
.width("100%")
.height(60)
.backgroundColor("#ff6602")
.onClick(() => {
this.tColor = "#ff6602"
})
}
@Styles txt() {
.width("100%")
.height(50)
.backgroundColor(this.tColor)
}
build() {
Column() {
Text("这是一行文本")
.txt() // 使用组件内定义样式
.fontSize(30)
.fontColor(Color.White)
.margin({ bottom: 20 })
Button("change color")
.txt_cls() // 使用组件内定义样式
.fontSize(30)
}
.GlobalStyles() // 定义的全局样式
}
}
总结:@Styles可以提取大部分的样式,但是发现文本类型的样式没办法提取(希望我没有用错),总体上还是很方便的,有点可惜就是不能传参。
1.5.2 @Extend装饰器
@Extend装饰器是在@Styles的基础上,为开发者提供扩展原生(内置)组件样式的能力。
@Extend语法:
@Extend(UI组件名) function fncName() {...}
使用@Extend装饰器需要遵循的规则:
- @Extend只支持全局定义,不能在组件内部定义
- @Extend支持封装指定的组件的私有属性和私有事件和预定义相同组件的@Extend的方法
- 与@Styles不同的是,@Extend装饰的方法是可以传递参数的,同时,@Extend也可以支持文本类型的样式属性
- @Extend装饰的方法的参数可以是function,作为Event 事件的句柄
- @Extend的参数如果是状态变量,当变量的值发生改变,则会触发UI的刷新
// 只能扩展内置组件,不能扩展自定义组件
// @Extend(Demo) function demoFnc() {}
@Extend(Row) function rowCls() {
.width("100%")
.height(60)
.margin({ bottom: 10 })
.backgroundColor("#ff6602")
}
@Extend(Row) function oddCls() {
.rowCls() // 调用预定义的rowCls
.backgroundColor(Color.Blue)
}
// 支持文本css属性
@Extend(Text) function txtCls($$: { color: string | Color }) {
.fontSize(30)
.fontColor($$.color)
}
@Entry
@Component
struct ExtendIndex {
@State newColor: string | Color = "#000"
changeColor = () => {
this.newColor = Color.Red
}
build() {
Column() {
Row() {
Text("first row")
.txtCls({ color: "#fff" })
}
.rowCls()
Row() {
Text("odd row")
.txtCls({ color: this.newColor }) // 传递参数:状态变量
}
.oddCls()
Row() {
Button("change color")
.fontSize(30)
.onClick(this.changeColor)
}
}
}
}
@Component
struct Demo {
build() {
Column() {
Text("demo text")
.fontSize(30)
}
}
}
注意:目前ArkTs并不支持@Extend、@Styles的跨文件使用,不能使用export 导出,如果有其他跨文件使用,可以告知一下。
1.5.3 stateStyles 多态样式
@Styles和@Extend作用于静态页面的样式复用,而stateStyles可以依据组件的内部状态的不同,快速设置不同的样式。
stateStyles是属性方法,可以根据UI内部状态来变更样式,类似于css伪类,不过语法不一样。ArkUI提供以下四种状态:
- focused:聚焦状态
- normal:正常状态
- pressed:按压状态
- disabled:禁用状态
下面是示例:
@Entry
@Component
struct StateStylesIndex {
@State inputTxt: string = "";
@Styles normalCls() {
.width("100%")
.height(60)
.backgroundColor(Color.Orange)
}
build() {
Column() {
Button("按钮")
.onClick(() => {
this.inputTxt = "点击了按钮";
})
.margin({ bottom: 30, top:5 })
.fontSize(30)
.stateStyles({
normal: this.normalCls,
pressed: {
.backgroundColor(Color.Blue)
.fontColor(Color.White)
}
})
TextInput({
placeholder: "请输入内容",
text: this.inputTxt
})
.width("90%")
.height(50)
.fontSize(25)
.stateStyles({
normal: {
.fontColor("#999999")
.borderWidth(2)
.borderColor(Color.Gray)
},
focused: {
.borderWidth(2)
.borderColor(Color.Blue)
}
})
}
.width("100%")
}
}
总结:以上文章的介绍,让我们对ArkTs有了基本的了解,知道了如何去构建页面和自定义组件,后续,我们将去学习它的状态管理等知识点。