Harmony详细入门教程,只讲入门的

339 阅读26分钟

参考链接

tips

  • 创建页面时需要配置页面路由,配置路径为“entry > src > main > resources > base > profile > mainn_pages.json
  • ide工具Previewer中顶部栏有刷新按钮
  • 页面,组件等都是使用类的模式来编写到ets文件内,样式,结构也写在里面

鸿蒙页面的最基本的几个概念:

在这一部分会讲鸿蒙的一些基本概念和一些基本语法,比较适用于已经学过ts的同学进行食用。

build()函数

不管是page还是component,都是通过build函数作为入口,而build函数也是ui编写的地方。

所有声明在build()函数的语句,我们统称为UI描述,UI描述需要遵循以下规则:

  • @Entry装饰的自定义组件,其build()函数下的根节点唯一且必要,且必须为容器组件,其中ForEach禁止作为根节点。

    @Component装饰的自定义组件,其build()函数下的根节点唯一且必要,可以为非容器组件,其中ForEach禁止作为根节点。

@Entry
@Component
struct MyComponent {
  build() {
    // 根节点唯一且必要,必须为容器组件
    Row() {
      ChildComponent() 
    }
  }
}

@Component
struct ChildComponent {
  build() {
    // 根节点唯一且必要,可为非容器组件
    Image('test.jpg')
  }
}
  • build函数内不允许声明本地变量
  • 不允许在UI描述里直接使用console.info,但允许在方法或者函数里使用
  • 不允许创建本地的作用域
  • 不允许调用没有用@Builder装饰的方法,允许系统组件的参数是TS方法的返回值。
  • 不允许使用switch语法,如果需要使用条件判断,请使用if。
  • 不允许使用表达式
  • 不允许直接改变状态变量
  • 在ArkUI状态管理中,状态驱动UI更新。

总结:在build函数内只做ui相关的事情,不然可能会导致重复渲染

装饰器

装饰器是鸿蒙开发中的重要一环,页面定义,内容定义等都需要装饰器的使用。即怎么判断这是个页面,有entry就是。怎么判断是不是组件,组件上面需要有一个component注解。

@Entry

表示为页面入口struct,放置于类名上面

@Component

表示该struct为组件,放置于类名上面

目录用途

entry:主目录,框架代码

src:代码编写目录

main:应用文件夹

ets: 应用代码

pages:页面代码

resources:应用资源存放

mock:模拟数据

ohosTest:

test:测试用例

页面

页面:即应用的UI页面。可以由一个或者多个自定义组件组成,@Entry装饰的自定义组件为页面的入口组件,即页面的根节点,一个页面有且仅能有一个@Entry。只有被@Entry装饰的组件才可以调用页面的生命周期。

生命周期

  • onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景。
  • onPageHide:页面每次隐藏时触发一次,包括路由过程、应用进入后台等场景。
  • onBackPress:当用户点击返回按钮时触发。

自定义组件

自定义组件:@Component装饰的UI单元,可以组合多个系统组件实现UI的复用,可以调用组件的生命周期。

自定义组件基于struct实现,使用ets文件。类名格式为struct + 自定义组件名 + { …. }。不能有继承关系,对于struct的实例化可以省略new。

自定义组件不需要添加@entry 注解,一个页面只能有一个entry注解

生命周期

  • aboutToAppear:组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其build()函数之前执行。
  • onDidBuild:组件build()函数执行完成之后回调该接口,不建议在onDidBuild函数中更改状态变量、使用animateTo等功能,这可能会导致不稳定的UI表现。
  • aboutToDisappear:aboutToDisappear函数在自定义组件析构销毁之前执行。不允许在aboutToDisappear函数中改变状态变量,特别是@Link变量的修改可能会导致应用程序行为不稳定。

生命周期

Untitled

基本语法介绍

在鸿蒙官方的文档中有更全面的基本知识,因为这里面大部分都和ts是类似的,所以这里就不过多赘述,只讲一些特殊的或者开发比较不常用到但好用的,下面也提供了鸿蒙的基本知识链接

developer.huawei.com/consumer/cn…

函数

函数的作用域

函数中定义的变量和其他实例仅可以在函数内部访问,不能从外部访问。

如果函数中定义的变量与外部作用域中已有实例同名,则函数内的局部变量定义将覆盖外部定义。

函数重载

我们可以通过编写重载,指定函数的不同调用方式。具体方法为,为同一个函数写入多个同名但签名不同的函数头,函数实现紧随其后。

function foo(x: number): void;            /* 第一个函数定义 */
function foo(x: string): void;            /* 第二个函数定义 */
function foo(x: number | string): void {  /* 函数实现 */
}

foo(123);     //  OK,使用第一个定义
foo('aa'); // OK,使用第二个定义

字段初始化

为了减少运行时的错误和获得更好的执行性能,

ArkTS要求所有字段在声明时或者构造函数中显式初始化。这和标准TS中的strictPropertyInitialization模式一样。

继承

使用extends 来继承另一个类

使用implements来继承interface接口,接口只定义声明,不做具体实现

对象字面量

对象字面量是一个表达式,可用于创建类实例并提供一些初始值。它在某些情况下更方便,可以用来代替new表达式。

class C {
  n: number = 0
  s: string = ''
}

let c: C = {n: 42, s: 'foo'};

Record类型的对象字面量

泛型Record<K, V>用于将类型(键类型)的属性映射到另一个类型(值类型)。

类型K可以是字符串类型或数值类型,而V可以是任何类型。

let map: Record<string, number> = {
  'John': 25,
  'Mary': 21,
}

map['John']; // 25

接口

接口只做声明,是定义代码协定的常见方式。

任何一个类的实例只要实现了特定接口,就可以通过该接口实现多态。

接口属性

接口属性可以是字段、getter、setter或getter和setter组合的形式。

interface Style {
  get color(): string
  set color(x: string)
}

接口继承

interface Style {
  color: string
}

interface ExtendedStyle extends Style {
  width: number
}

泛型类型和函数

泛型约束

interface Hashable {
  hash(): number
}
class MyHashMap<Key extends Hashable, Value> {
  public set(k: Key, v: Value) {
    let h = k.hash();
    // ...其他代码...
  }
}

泛型函数

function last<T>(x: T[]): T {
  return x[x.length - 1];
}

泛型默认值

泛型类型的类型参数可以设置默认值。这样可以不指定实际的类型实参,而只使用泛型类型名称。下面的示例展示了类和函数的这一点。

class SomeType {}
interface Interface <T1 = SomeType> { }
class Base <T2 = SomeType> { }
class Derived1 extends Base implements Interface { }
// Derived1在语义上等价于Derived2
class Derived2 extends Base<SomeType> implements Interface<SomeType> { }

function foo<T = number>(): T {
  // ...
}
foo();
// 此函数在语义上等价于下面的调用
foo<number>();

空安全

默认情况下,ArkTS中的所有类型都是不可为空的,因此类型的值不能为空。这类似于TypeScript的严格空值检查模式(strictNullChecks),但规则更严格

空值的变量可以定义为联合类型T | null。

非空断言运算符

后缀运算符!可用于断言其操作数为非空。

应用于空值时,运算符将抛出错误。否则,值的类型将从T | null更改为T:

class C {
  value: number | null = 1;
}

let c = new C();
let y: number;
y = c.value + 1;  // 编译时错误:无法对可空值作做加法
y = c.value! + 1; // ok,值为2

空值合并运算符

空值合并二元运算符??用于检查左侧表达式的求值是否等于null或者undefined。如果是,则表达式的结果为右侧表达式;否则,结果为左侧表达式。

换句话说,a ?? b等价于三元运算符(a != null && a != undefined) ? a : b。

在以下示例中,getNick方法如果设置了昵称,则返回昵称;否则,返回空字符串:

**class Person {
  // ...
  nick: string | null = null
  getNick(): string {
    return this.nick ?? '';
  }
}**

模块

导出

可以使用关键字export导出顶层的声明。

未导出的声明名称被视为私有名称,只能在声明该名称的模块中使用。

注意:通过export方式导出,在导入时要加{}。

export class Point {
  x: number = 0
  y: number = 0
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}
export let Origin = new Point(0, 0);
export function Distance(p1: Point, p2: Point): number {
  return Math.sqrt((p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y));
}

导入

静态导入

导入声明用于导入从其他模块导出的实体,并在当前模块中提供其绑定。导入声明由两部分组成:

  • 导入路径,用于指定导入的模块;
  • 导入绑定,用于定义导入的模块中的可用实体集和使用形式(限定或不限定使用)。

导入绑定可以有几种形式。

假设模块具有路径“./utils”和导出实体“X”和“Y”。

导入绑定* as A表示绑定名称“A”,通过A.name可访问从导入路径指定的模块导出的所有实体:

import * as Utils from './utils'Utils.X // 表示来自Utils的XUtils.Y // 表示来自Utils的Y

导入绑定{ ident1, ..., identN }表示将导出的实体与指定名称绑定,该名称可以用作简单名称:

import { X, Y } from './utils'X // 表示来自utils的XY // 表示来自utils的Y

如果标识符列表定义了ident as alias,则实体ident将绑定在名称alias下:

import { X as Z, Y } from './utils'Z // 表示来自Utils的XY // 表示来自Utils的YX // 编译时错误:'X'不可见

动态导入

应用开发的有些场景中,如果希望根据条件导入模块或者按需导入模块,可以使用动态导入代替静态导入。

import()语法通常称为动态导入dynamic import,是一种类似函数的表达式,用来动态导入模块。以这种方式调用,将返回一个promise。

如下例所示,import(modulePath)可以加载模块并返回一个promise,该promise resolve为一个包含其所有导出的模块对象。该表达式可以在代码中的任意位置调用。

let modulePath = prompt("Which module to load?");import(modulePath).then(obj => <module object>).catch(err => <loading error, e.g. if no such module>)

如果在异步函数中,可以使用let module = await import(modulePath)。

// say.tsexport function hi() {  console.log('Hello');}export function bye() {  console.log('Bye');}

那么,可以像下面这样进行动态导入:

async function test() {  let ns = await import('./say');  let hi = ns.hi;  let bye = ns.bye;  hi();  bye();}

更多的使用动态import的业务场景和使用实例见动态import

关键字

this

关键字this只能在类的实例方法中使用。

使用限制:

  • 不支持this类型
  • 不支持在函数和类的静态方法中使用this

关键字this的指向:

  • 调用实例方法的对象
  • 正在构造的对象

从ts到ArkTS和ArkTS相对于ts的一些约束

developer.huawei.com/consumer/cn…

实战系列

编写ArkTS的UI基本语法

tips

  • 关于ui层绑定的状态变量改变会导致build函数重新渲染

常用标签

基本标签文档

  • Text:文本标签
  • Image:图片标签

一个最基本标签的写法:

Button('Click me')
	.fontSize(20) // 设置标签样式
  .onClick(() => { // 设置标签事件
    console.log('Click Button')
  })

配置子组件

如果组件支持子组件配置,则需在尾随闭包"{...}"中为组件添加子组件的UI描述。Column、Row、Stack、Grid、List等组件都是容器组件。

  • 以下是简单的Column组件配置子组件的示例。

    Column() {  
    	Text('Hello')    
    		.fontSize(100)  
    	Divider()  
    	Text(this.myText)    
    		.fontSize(100)    
    		.fontColor(Color.Red)
    }
    

自定义组件

基本用法为

/components/hello.ets
@Component
export struct HelloComponent {
  @State message: string = 'Hello, World!';

  build() {
    // HelloComponent自定义组件组合系统组件Row和Text
    Row() {
      Text(this.message)
        .onClick(() => {
          // 状态变量message的改变驱动UI刷新,UI从'Hello, World!'刷新为'Hello, ArkUI!'
          this.message = 'Hello, ArkUI!';
        })
    }
  }
}
/pages/index.ets
import { HelloComponent } from '../components/hello.ets'

@Entry
@Component
struct Index {
	build(){
		HelloComponent()
		HelloComponent({message: 'custom message'})
	}
}

自定义组件的基本结构

  • struct:自定义组件基于struct实现,struct + 自定义组件名 + {...}的组合构成自定义组件,不能有继承关系。对于struct的实例化,可以省略new。
  • @Component:@Component装饰器仅能装饰struct关键字声明的数据结构。struct被@Component装饰后具备组件化的能力,需要实现build方法描述UI,一个struct只能被一个@Component装饰。@Component可以接受一个可选的bool类型参数。

相关装饰器

@Reusable:@Reusable装饰的自定义组件具备可复用能力

@Entry:@Entry装饰的自定义组件将作为UI页面的入口。在单个UI页面中,最多可以使用@Entry装饰一个自定义组件。@Entry可以接受一个可选的LocalStorage的参数。

@Component:@Component装饰器仅能装饰struct关键字声明的数据结构。struct被@Component装饰后具备组件化的能力,需要实现build方法描述UI,一个struct只能被一个@Component装饰。@Component可以接受一个可选的bool类型参数。

@Builder装饰器:自定义构建函数

使用@builder装饰的函数只能在build函数内使用,可以从外部传入

  • 允许在自定义组件内定义一个或多个@Builder方法,该方法被认为是该组件的私有、特殊类型的成员函数。
  • 自定义构建函数可以在所属组件的build方法和其他自定义构建函数中调用,但不允许在组件外调用。
  • 在自定义函数体中,this指代当前所属组件,组件的状态变量可以在自定义构建函数内访问。建议通过this访问自定义组件的状态变量而不是参数传递。

@BuilderParam装饰器:引用@Builder函数

当开发者创建了自定义组件,并想对该组件添加特定功能时,例如在自定义组件中添加一个点击跳转操作。若直接在组件内嵌入事件方法,将会导致所有引入该自定义组件的地方均增加了该功能。为解决此问题,ArkUI引入了@BuilderParam装饰器,@BuilderParam用来装饰指向@Builder方法的变量(@BuilderParam是用来承接@Builder函数的),开发者可在初始化自定义组件时对此属性进行赋值,为自定义组件增加特定的功能。该装饰器用于声明任意UI描述的一个元素,类似slot占位符。

@Styles装饰器:定义组件重用样式

@Extend装饰器:定义扩展组件样式

@AnimatableExtend装饰器:定义可动画属性

@Require装饰器:校验构造传参

UI描述规范:

developer.huawei.com/consumer/cn…

设置自定义组件布局和位置

如果需要通过测算的方式布局自定义组件内子组件的位置,建议使用以下接口:

  • onMeasureSize:组件每次布局时触发,计算子组件的尺寸,其执行时间先于onPlaceChildren。
  • onPlaceChildren:组件每次布局时触发,设置子组件的起始位置。
@Entry
@Component
struct Layout {

  build() {
    Column() {
      CustomLayout({builder: ColumnChildren})
    }
  }
}

@Component
struct CustomLayout {
  @Builder
  doNothingBuilder() { // 设置一个默认的builder  可忽略
  };

  @BuilderParam builder: () => void = this.doNothingBuilder;
  @State startSize: number = 100;
  result: SizeResult = {
    width: 0,
    height: 0
  };

  build() {
    // 使用外部传入的builder作为渲染
    this.builder()
  }

  // 第一步 计算各子组件的大小
  onMeasureSize(selfLayoutInfo: GeometryInfo, children: Measurable[], constraint: ConstraintSizeOptions): SizeResult {
      let size = 100;
      // 或者组件生成的节点并且通过measure方法设置大小
      children.forEach((child) => {
        let result = 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;
    // layout方法设置位置
    children.forEach((child) => {
      let pos = startPos - child.measureResult.height;
      child.layout({ x: pos, y: pos })
    })
  }
}

// 通过builder的方式传递多个组件,作为自定义组件的一级子组件(即不包含容器组件,如Column)
@Builder
function ColumnChildren() {
  // 创建三个text标签,并设置样式
  ForEach([1, 2, 3], (index: number) => { // 暂不支持lazyForEach的写法
    Text('S' + index)
      .fontSize(30)
      .width(100)
      .height(100)
      .borderWidth(2)
      .offset({ x: 10, y: 20 })
  })
}

组件冻结功能

自定义组件处于非激活状态时,状态变量将不响应更新,即@Watch不会调用,状态变量关联的节点不会刷新。通过freezeWhenInactive属性来决定是否使用冻结功能,不传参数时默认不使用。支持的场景有:页面路由,TabContent,LazyForEach,Navigation。

developer.huawei.com/consumer/cn…

怎么使用slot

builderParam+builder 可以作用slot来使用,例子可以参考上方设置自定义组件布局和位置

wrapBuilder

创建一个模板builder,可以用作全局builder组件

使用场景

  1. 将wrapBuilder赋值给globalBuilder,且把MyBuilder作为wrapBuilder参数,用来替代MyBuilder不能直接赋值给globalBuilder。
  2. 自定义组件Index使用ForEach来进行不同@Builder函数的渲染,可以使用builderArr声明的wrapBuilder数组进行不同@Builder函数效果体现。整体代码会较整洁。
@Builder
function MyBuilder(value: string, size: number) {
  Text(value)
    .fontSize(size)
}

let globalBuilder: WrappedBuilder<[string, number]> = wrapBuilder(MyBuilder);

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    Row() {
      Column() {
        globalBuilder.builder(this.message, 50)
      }
      .width('100%')
    }
    .height('100%')
  }
}

@Styles装饰器:定义组件重用样式

如果每个组件的样式都需要单独设置,在开发过程中会出现大量代码在进行重复样式设置

@Styles装饰器可以将多条样式设置提炼成一个方法,直接在组件声明的位置调用。通过@Styles装饰器可以快速定义并复用自定义样式。用于快速定义并复用自定义样式。

  • 当前@Styles仅支持通用属性通用事件
  • @Styles方法不支持参数
  • @Styles可以定义在组件内或全局,在全局定义时需在方法名前面添加function关键字,组件内定义时则不需要添加function关键字。
// 全局
@Styles function functionName() { ... }

// 在组件内
@Component
struct FancyUse {
  @Styles fancy() {
    .height(100)
  }
}

使用案例

// 定义在全局的@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)
    }
  }
}

如果需要设置动态样式,比如夜间模式等,可以考虑使用AttributeModifier

可以参考使用动态属性设置

// index.ets
import { MyButtonModifier } from './setAttribute'

@Entry
@Component
struct attributeDemo {
  @State modifier: MyButtonModifier = new MyButtonModifier()

  build() {
    Row() {
      Column() {
        Button("Button")
          .attributeModifier(this.modifier)
          .onClick(() => {
            this.modifier.isDark = !this.modifier.isDark
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

// setAttribute.ets
export class MyButtonModifier implements AttributeModifier<ButtonAttribute> {
  isDark: boolean = false
  applyNormalAttribute(instance: ButtonAttribute): void {
    if (this.isDark) {
      instance.backgroundColor(Color.Black)
    } else {
      instance.backgroundColor(Color.Red)
    }
  }
}

@Extend装饰器:定义扩展组件样式

使用@Styles用于样式的扩展,在@Styles的基础上,我们提供了@Extend,用于扩展原生组件样式。

  • 和@Styles不同,@Extend仅支持在全局定义,不支持在组件内部定义。仅能在当前文件内使用,不支持export
  • 和@Styles不同,@Extend支持封装指定组件的私有属性、私有事件和自身定义的全局方法。
  • 和@Styles不同,@Extend装饰的方法支持参数,开发者可以在调用时传递参数,调用遵循TS方法传值调用。
  • @Extend装饰的方法的参数可以为function,作为Event事件的句柄。
  • @Extend的参数可以为状态变量,当状态变量改变时,UI可以正常的被刷新渲染。

使用实例

@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(Text) function fancyText(weightValue: number, color: Color) {
  .fontStyle(FontStyle.Italic)
  .fontWeight(weightValue)
  .backgroundColor(color)
}

@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%')
  }
}

@style主要用于静态样式的复用

@Extend则用于当前组件样式复用,可传入参数来设置不同样式

stateStyles:多态样式

@Styles仅仅应用于静态页面的样式复用,stateStyles可以依据组件的内部状态的不同,快速设置不同样式。这就是我们本章要介绍的内容stateStyles(又称为:多态样式)。

stateStyles是属性方法,可以根据UI内部状态来设置样式,类似于css伪类,但语法不同。ArkUI提供以下五种状态:

  • focused:获焦态。
  • normal:正常态。
  • pressed:按压态。
  • disabled:不可用态。
  • selected10+:选中态。

使用案例

@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%')
    }
  }
}

@AnimatableExtend装饰器:定义可动画属性

@AnimatableExtend装饰器用于自定义可动画的属性方法,在这个属性方法中修改组件不可动画的属性。在动画执行过程时,通过逐帧回调函数修改不可动画属性值,让不可动画属性也能实现动画效果。

  • 可动画属性:如果一个属性方法在animation属性前调用,改变这个属性的值可以生效animation属性的动画效果,这个属性称为可动画属性。比如height、width、backgroundColor、translate属性,Text组件的fontSize属性等。
  • 不可动画属性:如果一个属性方法在animation属性前调用,改变这个属性的值不能生效animation属性的动画效果,这个属性称为不可动画属性。比如Polyline组件的points属性等。

AnimtableArithmetic接口说明

对复杂数据类型做动画,需要实现AnimtableArithmetic接口中加法、减法、乘法和判断相等函数。

名称入参类型返回值类型说明
plusAnimtableArithmeticAnimtableArithmetic加法函数
subtractAnimtableArithmeticAnimtableArithmetic减法函数
multiplynumberAnimtableArithmetic乘法函数
equalsAnimtableArithmeticboolean相等判断函数

示例,实现字体大小的动画效果

@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)
  }
}

这种为简单动画,只需要对一个变量进行值的动态修改即可

而对于复杂动画,则需要实现AnimtableArithmetic接口进行动态修改

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)
  }
}

@Require装饰器:校验构造传参

@Require是校验@Prop、@State、@Provide、@BuilderParam和普通变量(无状态装饰器修饰的变量)是否需要构造传参的一个装饰器。

当@Require装饰器和@Prop、@State、@Provide、@BuilderParam、普通变量(无状态装饰器修饰的变量)结合使用时,在构造该自定义组件时,@Prop、@State、@Provide、@BuilderParam和普通变量(无状态装饰器修饰的变量)必须在构造时传参

使用案例

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  @Builder buildTest() {
    Row() {
      Text('Hello, world')
        .fontSize(30)
    }
  }

  build() {
    Row() {
      Child({ regular_value: this.message, state_value: this.message, provide_value: this.message, initMessage: this.message, message: this.message,
        buildTest: this.buildTest, initBuildTest: this.buildTest })
    }
  }
}

@Component
struct Child {
  @Builder buildFunction() {
    Column() {
      Text('initBuilderParam')
        .fontSize(30)
    }
  }
  @Require regular_value: string = 'Hello';
  @Require @State state_value: string = "Hello";
  @Require @Provide provide_value: string = "Hello";
  @Require @BuilderParam buildTest: () => void;
  @Require @BuilderParam initBuildTest: () => void = this.buildFunction;
  @Require @Prop initMessage: string = 'Hello';
  @Require @Prop message: string;

  build() {
    Column() {
      Text(this.initMessage)
        .fontSize(30)
      Text(this.message)
        .fontSize(30)
      this.initBuildTest();
      this.buildTest();
    }
    .width('100%')
    .height('100%')
  }
}

状态管理概述

如果说上面的内容让我们知道了怎么写一个静态的页面,那么这节的内容就可以让我们知道怎么让我们的变量状态在不同路由,不同组件之间流转,保存

状态管理主要有V1(稳定版)和V2(试用版),这里主要介绍V2

ArkUI状态管理V1提供了多种装饰器,通过使用这些装饰器,状态变量不仅可以观察在组件内的改变,还可以在不同组件层级间传递,比如父子组件、跨组件层级,也可以观察全局范围内的变化。根据状态变量的影响范围,将所有的装饰器可以大致分为:

  • 管理组件拥有状态的装饰器:组件级别的状态管理,可以观察组件内变化,和不同组件层级的变化,但需要唯一观察同一个组件树上,即同一个页面内。
  • 管理应用拥有状态的装饰器:应用级别的状态管理,可以观察不同页面,甚至不同UIAbility的状态变化,是应用内全局的状态管理。

从数据的传递形式和同步类型层面看,装饰器也可分为:

  • 只读的单向传递;
  • 可变更的双向传递。

V1状态管理文档

v1版本的注解比较类似于前端常用的vue,有watch,有双向绑定,props等。如果在正式开发中,建议使用稳定的版本

V1版本解决对象没有深层监听的方式

使用@ObjectLink装饰器与自定义组件的方式实现观测

@Observed
class Father {
  son: Son;

  constructor(name: string, age: number) {
    this.son = new Son(name, age);
  }
}
@Observed
class Son {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}
@Component
struct Child {
  @ObjectLink son: Son;

  build() {
    Row() {
      Column() {
        Text(`name: ${this.son.name} age: ${this.son.age}`)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            this.son.age++;
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}
@Entry
@Component
struct Index {
  @State father: Father = new Father("John", 8);

  build() {
    Column() {
      Child({son: this.father.son})
    }
  }
}

V2的监听变量

@ObservedV2装饰器和@Trace装饰器:类属性变化观测

V2状态管理文档

@ObservedV2
class Son {
  @Trace age: number = 100;
}
class Father {
  son: Son = new Son();
}
@Entry
@ComponentV2
struct Index {
  father: Father = new Father();

  build() {
    Column() {
      // 当点击改变age时,Text组件会刷新
      Text(`${this.father.son.age}`)
        .onClick(() => {
          this.father.son.age++;
      })
    }
  }
}

使用限制:

  • 非@Trace装饰的成员属性用在UI上无法触发UI刷新

• @Trace不能用在没有被@ObservedV2装饰的class上。

• @Trace是class中属性的装饰器,不能用在struct中。

• @ObservedV2、@Trace不能与@Observed@Track混合使用。

• 使用@ObservedV2与@Trace装饰的类不能和@State以及现有框架的装饰器混合使用。

• @ObservedV2的类实例目前不支持使用JSON.stringify进行序列化。

Trace装饰一些深层的数据结构

@Trace装饰基础类型的数组

使用支持的API能够观测变化观察变化

@Trace装饰对象数组

数组内的对象使用Trace修饰即可观察变化

@Trace装饰Map类型,@Trace装饰Set类型,@Trace装饰Date类型

可以直接观察

@ComponentV2装饰器:自定义组件

@Component装饰器一样,@ComponentV2装饰器用于装饰自定义组件:

  • 在@ComponentV2装饰的自定义组件中,开发者仅可以使用全新的状态变量装饰器,包括@Local、@Param、@Once、@Event、@Provider、@Consumer等。
  • @ComponentV2装饰的自定义组件暂不支持组件复用、组件冻结、LocalStorage等现有自定义组件的能力。
  • 无法同时使用@ComponentV2与@Component装饰同一个struct结构。

@Local装饰器:组件内部状态

类似于@state装饰器的使用,可以实现对状态的变化检测

@Local表示组件内部的状态,使得自定义组件内部的变量具有观测变化的能力:

  • 被@Local装饰的变量无法从外部初始化,因此必须在组件内部进行初始化。
  • 当被@Local装饰的变量变化时,会刷新使用该变量的组件。
  • @Local支持观测number、boolean、string、Object、class等基本类型以及Array、Set、Map、Date等内嵌类型。
  • @Local的观测能力仅限于被装饰的变量本身。当装饰简单类型时,能够观测到对变量的赋值;当装饰对象类型时,仅能观测到对对象整体的赋值;当装饰数组类型时,能观测到数组整体以及数组元素项的变化;当装饰Array、Set、Map、Date等内嵌类型时,可以观测到通过API调用带来的变化。详见观察变化
  • @Local支持null、undefined以及联合类型。

观察变化范围

  • 当装饰的变量类型为boolean、string、number时,可以观察到对变量赋值的变化
  • 当装饰的变量类型为类对象时,仅可以观察到对类对象整体赋值的变化,无法直接观察到对类成员属性赋值的变化,对类成员属性的观察依赖@ObservedV2和@Trace装饰器。注意,@Local无法和@Observed装饰的类实例对象混用,只能整体全部赋值
  • 当装饰的变量类型为简单类型的数组时,可以观察到数组整体或数组项的变化
  • 当装饰的变量是嵌套类或对象数组时,@Local无法观察深层对象属性的变化。对深层对象属性的观测依赖@ObservedV2与@Trace装饰器

@Param:组件外部输入

类似于V1的@Prop装饰器,用于接收从外部传入的状态

  • @Param装饰的变量支持本地初始化,但是不允许在组件内部直接修改变量本身。
  • 被@Param装饰的变量能够在初始化自定义组件时从外部传入,当数据源也是状态变量时,数据源的修改会同步给@Param。
  • @Param可以接受任意类型的数据源,包括普通变量、状态变量、常量、函数返回值等。
  • @Param装饰的变量变化时,会刷新该变量关联的组件。
  • @Param支持观测number、boolean、string、Object、class等基本类型以及Array、Set、Map、Date等内嵌类型。
  • 对于复杂类型如类对象,@Param会接受数据源的引用。在组件内可以修改类对象中的属性,该修改会同步到数据源。
  • @Param的观测能力仅限于被装饰的变量本身。当装饰简单类型时,对变量的整体改变能够观测到;当装饰对象类型时,仅能观测对象整体的改变;当装饰数组类型时,能观测到数组整体以及数组元素项的改变;当装饰Array、Set、Map、Date等内嵌类型时,可以观测到通过API调用带来的变化。详见观察变化
  • @Param支持null、undefined以及联合类型。

使用案例

@Entry
@ComponentV2
struct Index {
  @Local count: number = 0;
  @Local message: string = "Hello";
  @Local flag: boolean = false;
  build() {
    Column() {
      Text(`Local ${this.count}`)
      Text(`Local ${this.message}`)
      Text(`Local ${this.flag}`)
      Button("change Local")
        .onClick(()=>{
          // 对数据源的更改会同步给子组件
          this.count++;
          this.message += " World";
          this.flag = !this.flag;
      })
      Child({
        count: this.count,
        message: this.message,
        flag: this.flag
      })
    }
  }
}
@ComponentV2
struct Child {
  @Require @Param count: number;
  @Require @Param message: string;
  @Require @Param flag: boolean;
  build() {
    Column() {
      Text(`Param ${this.count}`)
      Text(`Param ${this.message}`)
      Text(`Param ${this.flag}`)
    }
  }
}

@Once:初始化同步一次

为了实现仅从外部初始化一次、不接受后续同步变化的能力,开发者可以使用@Once装饰器搭配@Param装饰器使用。

@Once装饰器仅在变量初始化时接受外部传入值进行初始化,当后续数据源更改时,不会将修改同步给子组件:

  • @Once必须搭配@Param装饰器使用,单独使用或搭配其他装饰器使用都是不允许的。
  • @Once不影响@Param的观测能力,仅针对数据源的变化做拦截。
  • @Once与@Param装饰变量的先后顺序不影响实际功能。

使用场景:

用于比如异步获取数据等场景

@Event装饰器:组件输出

用于子组件像父组件传递事件要求更改数据等操作

@Event用于装饰组件对外输出的方法:

  • @Event装饰的回调方法中参数以及返回值由开发者决定。
  • @Event装饰非回调类型的变量不会生效。当@Event没有初始化时,会自动生成一个空的函数作为默认回调。
  • 当@Event未被外部初始化,但本地有默认值时,会使用本地默认的函数进行处理。
@Entry
@ComponentV2
struct Index {
  @Local title: string = "Titile One";
  @Local fontColor: Color = Color.Red;

  build() {
    Column() {
      Child({
        title: this.title,
        fontColor: this.fontColor,
        changeFactory: (type: number) => {
          if (type == 1) {
            this.title = "Title One";
            this.fontColor = Color.Red;
          } else if (type == 2) {
            this.title = "Title Two";
            this.fontColor = Color.Green;
          }
        }
      })
    }
  }
}

@ComponentV2
struct Child {
  @Param title: string = '';
  @Param fontColor: Color = Color.Black;
  @Event changeFactory: (x: number) => void = (x: number) => {};

  build() {
    Column() {
      Text(`${this.title}`)
      Button("change to Title Two")
        .onClick(() => {
          this.changeFactory(2)
        })
      Button("change to Title One")
        .onClick(() => {
          this.changeFactory(1)
        })
    }
  }
}

@Monitor装饰器:状态变量修改监听

类似于@watch装饰器,用于监听状态变量的修改

@Monitor装饰器用于监听状态变量修改,使得状态变量具有深度监听的能力:

  • @Monitor装饰器支持在@ComponentV2装饰的自定义组件中使用,未被状态变量装饰器@Local@Param@Provider@Comsumer@Computed装饰的变量无法被@Monitor监听到变化。
  • @Monitor装饰器支持在类中与@ObservedV2、@Trace配合使用,不允许在未被@ObservedV2装饰的类中使用@Monitor装饰器。未被@Trace装饰的属性无法被@Monitor监听到变化。
  • 当观测的属性变化时,@Monitor装饰器定义的回调方法将被调用。判断属性是否变化使用的是严格相等(===),当严格相等为false的情况下,就会触发@Monitor的回调。当在一次事件中多次改变同一个属性时,将会使用初始值和最终值进行比较以判断是否变化。
  • 单个@Monitor装饰器能够同时监听多个属性的变化,当这些属性在一次事件中共同变化时,只会触发一次@Monitor的回调方法。
  • @Monitor装饰器具有深度监听的能力,能够监听嵌套类、多维数组、对象数组中指定项的变化。对于嵌套类、对象数组中成员属性变化的监听要求该类被@ObservedV2装饰且该属性被@Trace装饰。
  • @Watch装饰器类似,开发者需要自己定义回调函数,区别在于@Watch装饰器将函数名作为参数,而@Monitor直接装饰回调函数。@Monitor与@Watch的对比可以查看@Monitor与@Watch的对比
@Entry
@ComponentV2
struct Index {
  @Local message: string = "Hello World";
  @Local name: string = "Tom";
  @Local age: number = 24;
  @Monitor("message", "name")
  onStrChange(monitor: IMonitor) {
    monitor.dirty.forEach((path: string) => {
      console.log(`${path} changed from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`)
    })
  }
  build() {
    Column() {
      Button("change string")
        .onClick(() => {
          this.message += "!";
          this.name = "Jack";
      })
    }
  }
}

@Provider装饰器和@Consumer装饰器:跨组件层级双向同步

@Provider和@Consumer用于跨组件层级数据双向同步,可以使得开发者不拘泥于组件层级。

@Provider和@Consumer属于状态管理V2装饰器,所以只能在@ComponentV2中才能使用,在@Component中使用会编译报错。

@Provider,即数据提供方,其所有的子组件都可以通过@Consumer绑定相同的key来获取@Provider提供的数据。

@Consumer,即数据消费方,可以通过绑定同样的key获取其最近父节点的@Provider的数据,当查找不到@Provider的数据时,使用本地默认值。

@Provider和@Consumer装饰数据类型需要一致。

开发者在使用@Provider和@Consumer时要注意:

  • @Provider和@Consumer强依赖自定义组件层级,@Consumer所在组件会由于其父组件的不同,而被初始化为不同的值。
  • @Provider和@Consumer相当于把组件粘合在一起了,从组件独立角度,要减少使用@Provider和@Consumer。
// 参数可传别名,不传则默认为变量名
@ComponentV2 struct Parent {
    @Provider('alias') str: string = 'hello';   // has alias
}

@ComponentV2 struct Child {
    @Consumer('alias') str: string = 'world';   // use aliasName 'alias' to find Provider value 'hello'
}

具有双向绑定的特点

@Entry
@ComponentV2
struct Parent {
  @Provider() str: string = 'hello';

  build() {
    Column() {
      Button(this.str)
        .onClick(() => {
          this.str += '0';
        })
      Child()
    }
  }
}

@ComponentV2
struct Child {
  @Consumer() str: string = 'world';

  build() {
    Column() {
      Button(this.str)
        .onClick(() => {
          this.str += '0';
        })
    }
  }
}

@Computed装饰器:计算属性

@Computed装饰器:计算属性,在被计算的值变化的时候,只会计算一次。主要应用于解决UI多次重用该属性从而重复计算导致的性能问题。

@Entry
@ComponentV2
struct Index {
  @Local firstName: string = 'Li';
  @Local lastName: string = 'Hua';
  age: number = 20; // cannot trigger Computed

  @Computed
  get fullName() {
    console.info("---------Computed----------");
    return this.firstName + ' ' + this.lastName + this.age;
  }

  build() {
    Column() {
      Text(this.lastName + ' ' + this.firstName)
      Text(this.lastName + ' ' + this.firstName)
      Divider()
      Text(this.fullName)
      Text(this.fullName)
      Button('changed lastName').onClick(() => {
        this.lastName += 'a';
      })

      Button('changed age').onClick(() => {
        this.age++;  // cannot trigger Computed
      })
    }
  }
}

!!语法:双向绑定

!!双向绑定语法,是一个语法糖方便开发者实现数据双向绑定,用于初始化子组件的@Param和@Event。其中@Event方法名需要声明为“$”+ @Param属性名,详见使用场景

  • 如果父组件使用了!!双向绑定语法,则表明父组件的变化会同步给子组件,子组件的变化也会同步给父组件。
  • 如果父组件没有使用!!,则父组件发生的变化是单向的。
@Entry
@ComponentV2
struct Index {
  @Local value: number = 0;

  build() {
    Column() {
      Text(`${this.value}`)
      Button(`change value`).onClick(() => {
        this.value++;
      })
      Star({ value: this.value!! })
    }
  }
}

@ComponentV2
struct Star {
  @Param value: number = 0;
  @Event $value: (val: number) => void = (val: number) => {};

  build() {
    Column() {
      Text(`${this.value}`)
      Button(`change value `).onClick(() => {
        this.$value(10);
      })
    }
  }
}

是
Star({ value: this.value, $value: (val: number) => { this.value = val }})
的简写

渲染控制

在build函数中可以使用的条件语句

if/else:条件渲染

  • 支持if、else和else if语句。
  • if、else if后跟随的条件语句可以使用状态变量。
  • 允许在容器组件内使用,通过条件渲染语句构建不同的子组件。
  • 条件渲染语句在涉及到组件的父子关系时是“透明”的,当父组件和子组件之间存在一个或多个if语句时,必须遵守父组件关于子组件使用的规则。
  • 每个分支内部的构建函数必须遵循构建函数的规则,并创建一个或多个组件。无法创建组件的空构建函数会产生语法错误。
  • 某些容器组件限制子组件的类型或数量,将条件渲染语句用于这些组件内时,这些限制将同样应用于条件渲染语句内创建的组件。例如,Grid容器组件的子组件仅支持GridItem组件,在Grid内使用条件渲染语句时,条件渲染语句内仅允许使用GridItem组件。
@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--;
        })
    }
  }
}

ForEach:循环渲染

ForEach(
  arr: Array,
  itemGenerator: (item: Object, index: number) => void,
  keyGenerator?: (item: Object, index: number) => string
)
@Entry
@Component
struct Parent {
  @State simpleList: Array<string> = ['one', 'two', 'three'];

  build() {
    Row() {
      Column() {
        ForEach(this.simpleList, (item: string) => {
          ChildItem({ item: item })
        }, (item: string) => item)
      }
      .width('100%')
      .height('100%')
    }
    .height('100%')
    .backgroundColor(0xF1F3F5)
  }
}

@Component
struct ChildItem {
  @Prop item: string;

  build() {
    Text(this.item)
      .fontSize(50)
  }
}

LazyForEach:数据懒加载 虚拟列表

LazyForEach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。

类似于虚拟列表

LazyForEach文档

Repeat:循环渲染(推荐)

Repeat循环渲染和ForEach相比有两个区别,一是优化了部分更新场景下的渲染性能,二是组件生成函数的索引index由框架侧来维护。

Repeat组件基于数组类型数据来进行循环渲染,需要与容器组件配合使用,且接口返回的组件应当是允许包含在Repeat父容器组件中的子组件。例如,ListItem组件要求Repeat的父容器组件必须为List组件

@Entry
@Component
struct Parent {
  @State simpleList: Array<string> = ['one', 'two', 'three'];

  build() {
    Row() {
      Column() {
        Repeat<string>(this.simpleList)
          .each((obj: RepeatItem<string>)=>{
            ChildItem({ item: obj.item })
          })
          .key((item: string) => item)
      }
      .width('100%')
      .height('100%')
    }
    .height('100%')
    .backgroundColor(0xF1F3F5)
  }
}

@Component
struct ChildItem {
  @Prop item: string;

  build() {
    Text(this.item)
      .fontSize(50)
  }
}

ContentSlot:混合开发

用于渲染并管理Native层使用C-API创建的组件。

支持混合模式开发,当容器是ArkTS组件,子组件在Native侧创建时,推荐使用ContentSlot占位组件。

使用与native,不常用

文档