鸿蒙开发教程

HarmonyOS 项目目录介绍

ArkTS 基本语法详解

HarmonyOS 4.0 以后可以使用 ArkTS 或者 HTML/CSS/JS 技术来开发 HarmonyOS 应用,而 ArkTS 是 HarmonyOS 优选的主力应用开发语言。

ArkTS 围绕应用开发在 TypeScript(简称 TS)生态基础上做了进一步扩展,继承了 TS 的所有特性,是 TS 的超集。因此,在学习 ArkTS 语言之前,建议首先具备 TS 语言开发能力。

当前,ArkTS 在 TS 的基础上主要扩展了如下能力:

  • 基本语法:ArkTS 定义了声明式 UI 描述、自定义组件和动态扩展 UI 元素的能力,再配合 ArkUI 开发框架中的系统组件及其相关的事件方法、属性方法等共同构成了 UI 开发的主体。
  • 状态管理:ArkTS 提供了多维度的状态管理机制。在 UI 开发框架中,与 UI 相关联的数据可以在组件内使用,也可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,还可以在应用全局范围内传递或跨设备传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活的利用这些能力来实现数据和 UI 的联动。
  • 渲染控制:ArkTS 提供了渲染控制的能力。条件渲染可根据应用的不同状态,渲染对应状态下的 UI 内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件。数据懒加载从数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。

未来,ArkTS 会结合应用开发/运行的需求持续演进,逐步提供并行和并发能力增强、系统类型增强、分布式开发范式等更多特性。

ArkTS 基本组成

  • 装饰器: 用于装饰类、结构、方法以及变量,并赋予其特殊的含义。如上述示例中 @Entry、@Component 和 @State 都是装饰器,@Component 表示自定义组件,@Entry 表示该自定义组件为入口组件,@State 表示组件中的状态变量,状态变量变化会触发 UI 刷新。
  • 自定义组件:可复用的 UI 单元,可组合其它组件,如上述被 @Component 装饰的 struct Index。
  • UI 描述:以声明式的方式来描述 UI 的结构,例如 build() 方法中的代码块 。
  • 系统组件:ArkUI 框架中默认内置的基础和容器组件,可直接被开发者调用,比如示例中的 Column、Text、Divider、Button。
  • 事件方法:组件可以通过链式调用设置多个事件的响应逻辑,如跟随在 Button 后面的 onClick()。
  • 属性方法:组件可以通过链式调用配置多项属性,如 fontSize()、width()、height()、backgroundColor() 等。

系统组件、属性方法、事件方法具体使用可参考基于 ArkTS 的声明式开发范式

ArkTS 布局结构

布局的结构通常是分层级的,代表了用户界面中的整体架构。一个常见的页面结构如下所示:

为实现上述效果,需要在页面中声明对应的元素。其中,Page 表示页面的根节点,Column/Row 等元素为系统组件。针对不同的页面结构,ArkUI 提供了不同的布局组件来帮助我们实现对应布局的效果,例如 Row 用于实现线性布局等。

ArkTS 数据类型

TypeScript 支持一些基础的数据类型,如布尔型、数组、字符串等。

数字

TypeScript 里的所有数字都是浮点数,这些浮点数的类型是 number。除了支持十进制,还支持二进制、八进制、十六进制。

1
2
3
4
5
// 数值类型
let num1: number = 18 // 十进制
let num2: number = 0b10111 // ob 二进制
let num3: number = 0o14 // 0o 八进制
let num4: number = 0x1f // 0x 十六进制

字符串

TypeScript 里使用 string 表示文本数据类型, 可以使用双引号( “ )或单引号( ‘ )表示字符串或者反引号(`)。

反引号中可以配合 ${} 解析变量。

1
2
3
4
// 字符串类型
let str1: string = 'HarmonyOS Next不支持Android 应用了'
let str2: string = "ArkTS"
let str3: string = `马总今年${num1}岁了`

布尔值

TypeScript 中可以使用 boolean 来表示这个变量是布尔值,可以赋值为 true 或者 false。

1
2
3
// 布尔类型 true、false
let stateOn: boolean = true
let stateOff: boolean = false

联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种。如果一个数据可能有多重类型,或者当下还没想好用哪个类型 …

1
2
3
4
let flag: string | number | boolean
flag = '1'
flag = 1
flag = true

数组

TypeScrip 有两种方式可以定义数组。

1
2
3
4
5
// 第一种是使用数组泛型,Array<元素类型>。
let course1: Array<string> = ['Flutter', "HarmonyOS", `Golang`]

// 第二种是在元素类型后面接上 [],表示由此类型元素组成的一个数组。
let course2: string[] = ['Flutter', "HarmonyOS", `Golang`]

枚举

enum 类型是对 JavaScript 标准数据类型的一个补充,使用枚举类型可以为一组数值赋予友好的名字。

1
2
enum Color {Red, Green, Blue}
let c: Color = Color.Green

元组

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为 string 和 number 类型的元组。

1
2
3
let x: [string, number]
x = ['hello', 10] // OK
x = [10, 'hello'] // Error

Any(不推荐使用)

any 类型,表示的是变量可以是任意类型,相当于关闭了 ts 的类型检测功能,不建议使用。如果是 any 类型,则允许被赋值为任意类型。

1
2
3
4
5
6
7
8
let a: any = 666
a = "Semlinker" // 通过,any 类型可以被赋任意值
a = false // 通过,any 类型可以被赋任意值
a = 66 // 通过,any 类型可以被赋任意值
a = undefined // 通过,any 类型可以被赋任意值
a = null // 通过,any 类型可以被赋任意值
a = [] // 通过,any 类型可以被赋任意值
a = {} // 通过,any 类型可以被赋任意值

Unknown

unknown 与 any 一样,也相当于关闭了 ts 的类型检测功能。有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。那么我们可以使用 unknown 类型来标记这些变量。(同 any )。

1
2
3
let notSure: unknown = 4
notSure = 'maybe a string instead'
notSure = false

Void

当一个函数没有返回值时,你通常会见到其返回值类型是 void。

1
2
3
function test(): void {
console.log('This is function is void')
}

Null 和 Undefined

TypeScript 里,null 和 undefined 两者各自有自己的类型分别叫做 null 和 undefined。

1
2
let u: undefined = undefined
let n: null = null

ArkTS 渲染控制

if/else:条件渲染

  • 支持 if、else 和 else if 语句。
  • if、else if 后跟随的条件语句可以使用状态变量。
  • 允许在容器组件内使用,通过条件渲染语句构建不同的子组件。
  • 当 if、else if 后跟随的状态判断中使用的状态变量值变化时,条件渲染语句会进行更新条件可以包括 Typescript 表达式。
1
2
3
4
5
if (条件表达式) {
组件内容1
} else {
组件内容2
}

ForEach:循环渲染

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

1
2
3
4
5
6
7
8
9
10
/**
* @param arr 要遍历的数组
* @param itemGenerator 页面的生成函数
* @param keyGenerator 键生成函数,提供唯一标识,如果后续数组中数据发生变化,会判断元素的唯一标识是否发生变化,有变更再去做渲染,这样减少了不必要的渲染,提高了页面的渲染效率(可选参数,如果不填会会有默认的键生成函数,生成规则是用角标拼上数组元素的数据)
*/
ForEach(
arr:Array<any>,
itemGenerator:(item: any, index: number) => void,
keyGenerator ? : (item: any, index: number) => string
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
let arr: number[] = [1, 2, 4, 8, 16]

@Entry
@Component
struct ArrayPage {

arr2: string[] = ['HarmonyOS', 'Flutter', 'Android']

build() {
List({ space: 10 }) {
// 访问全局变量 arr 不要加 this
ForEach(arr, (item: number, key: number) => {
ListItem() {
Text(`${item}--${key}`)
.fontSize(20)
.width('100%')
.height(50)
.textAlign(TextAlign.Center)
.backgroundColor("#eee")
.borderRadius(10)
}
}, (item: number) => item.toString())

// 访问局部变量 arr2 要加 this
ForEach(this.arr2, (item: string) => {
ListItem() {
Text(item)
.fontSize(20)
.width('100%')
.height(50)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Gray)
.borderRadius(10)
}
}, (item: string) => item)
}.width('100%')
.height('100%')
.padding(10)
}
}

实际开发中尽量不要使用全局变量,组件中使用全局变量不需要加 this。

LazyForEach:数据懒加载

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

基础组件

Image

应用中显示图片需要使用 Image 组件实现,Image 支持多种图片格式,包括 png、jpg、bmp、svg 和 gif,具体用法请参考 Image 组件。

Image 通过调用接口来创建,接口调用形式如下:

1
Image(src: PixelMap | ResourceStr | DrawableDescriptor)

该接口通过图片数据源获取图片,支持本地图片和网络图片的渲染展示。其中,src 是图片的数据源,加载方式请参考加载图片资源

存档图类型数据源

存档图类型的数据源可以分为本地资源、网络资源、Resource 资源、媒体库资源和 base64。

  • 本地资源

    创建文件夹,将本地图片放入 ets 文件夹下的任意位置。

    Image 组件引入本地图片路径,即可显示图片(根目录为 ets 文件夹)。

    1
    2
    Image('images/view.jpg')
    .width(200)
  • 网络资源

    引入网络图片需申请权限 ohos.permission.INTERNET,具体申请方式请参考声明权限。此时,Image 组件的 src 参数为网络图片的链接。

    Image 组件首次加载网络图片时,需要请求网络资源,非首次加载时,默认从缓存中直接读取图片,更多图片缓存设置请参考 setImageCacheCountsetImageRawDataCacheSizesetImageFileCacheSize

    1、在 src/main/module.json5 中申请网络权限。

    1
    2
    3
    4
    5
    "requestPermissions": [
    {
       "name": "ohos.permission.INTERNET"
    }
    ]

    2、加载网络图片。

    1
    Image('https://gitee.com/zch0304/images/raw/master/note/test_icon1.jpg')
  • Resource 资源

    使用资源格式可以跨包/跨模块引入图片,resources 文件夹下的图片都可以通过 $r 资源接口读取到并转换到 Resource 格式。

    1
    2
    // src/main/resources/base/media/icon.png
    Image($r('app.media.icon'))

    还可以将图片放在 rawfile 文件夹下。

    1
    2
    // src/main/resources/rawfile/example1.png
    Image($rawfile('example1.png'))
  • 媒体库 file://data/storage

    支持 file://路径前缀的字符串,用于访问通过媒体库提供的图片路径。

    1、调用接口获取图库的照片 url。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    import picker from '@ohos.file.picker';
    import { BusinessError } from '@ohos.base';

    @Entry
    @Component
    struct Index {
    @State imgDatas: string[] = [];
    // 获取照片 url 集
    getAllImg() {
    try {
    let PhotoSelectOptions:picker.PhotoSelectOptions = new picker.PhotoSelectOptions();
    PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
    PhotoSelectOptions.maxSelectNumber = 5;
    let photoPicker:picker.PhotoViewPicker = new picker.PhotoViewPicker();
    photoPicker.select(PhotoSelectOptions).then((PhotoSelectResult:picker.PhotoSelectResult) => {
    this.imgDatas = PhotoSelectResult.photoUris;
    console.info('PhotoViewPicker.select successfully, PhotoSelectResult uri: ' + JSON.stringify(PhotoSelectResult));
    }).catch((err:Error) => {
    let message = (err as BusinessError).message;
    let code = (err as BusinessError).code;
    console.error(`PhotoViewPicker.select failed with. Code: ${code}, message: ${message}`);
    });
    } catch (err) {
    let message = (err as BusinessError).message;
    let code = (err as BusinessError).code;
    console.error(`PhotoViewPicker failed with. Code: ${code}, message: ${message}`); }
    }

    // aboutToAppear 中调用上述函数,获取图库的所有图片 url,存在 imgDatas 中
    async aboutToAppear() {
    this.getAllImg();
    }
    // 使用 imgDatas 的 url 加载图片。
    build() {
    Column() {
    Grid() {
    ForEach(this.imgDatas, (item:string) => {
    GridItem() {
    Image(item)
    .width(200)
    }
    }, (item:string):string => JSON.stringify(item))
    }
    }.width('100%').height('100%')
    }
    }

    2、从媒体库获取的 url 格式通常如下。

    1
    2
    Image('file://media/Photos/5')
    .width(200)
  • base64

    路径格式为 data:image/[png|jpeg|bmp|webp];base64,[base64 data],其中 [base64 data] 为 Base64 字符串数据。

    Base64 格式字符串可用于存储图片的像素数据,在网页上使用较为广泛。

多媒体像素图

PixelMap 是图片解码后的像素图,具体用法请参考图片开发指导。以下示例将加载的网络图片返回的数据解码成 PixelMap 格式,再显示在 Image 组件上。

1、创建 PixelMap 状态变量。

1
@State image: PixelMap | undefined = undefined;

2、引用多媒体。

请求网络图片,解码编码 PixelMap。

a. 引用网络权限与媒体库权限。

1
2
3
4
import http from '@ohos.net.http';
import ResponseCode from '@ohos.net.http';
import image from '@ohos.multimedia.image';
import { BusinessError } from '@ohos.base';

b. 填写网络图片地址。

1
2
3
4
5
6
7
8
9
10
let OutData: http.HttpResponse
http.createHttp().request("https://www.example.com/xxx.png",
(error: BusinessError, data: http.HttpResponse) => {
if (error) {
console.error(`http reqeust failed with. Code: ${error.code}, message: ${error.message}`);
} else {
OutData = data
}
}
)

c. 将网络地址成功返回的数据,编码转码成 pixelMap 的图片格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
let code: http.ResponseCode | number = OutData.responseCode
if (ResponseCode.ResponseCode.OK === code) {
let imageData: ArrayBuffer = OutData.result as ArrayBuffer;
let imageSource: image.ImageSource = image.createImageSource(imageData);

class tmp {
height: number = 100
width: number = 100
}

let si: tmp = new tmp()
let options: Record<string, number | boolean | tmp> = {
'alphaType': 0, // 透明度
'editable': false, // 是否可编辑
'pixelFormat': 3, // 像素格式
'scaleMode': 1, // 缩略值
'size': { height: 100, width: 100 }
} // 创建图片大小

class imagetmp {
image: PixelMap | undefined = undefined
set(val: PixelMap) {
this.image = val
}
}

imageSource.createPixelMap(options).then((pixelMap: PixelMap) => {
let im = new imagetmp()
im.set(pixelMap)
})
}

d. 显示图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class htp{
httpRequest: Function | undefined = undefined
set(){
if(this.httpRequest){
this.httpRequest()
}
}
}
Button("获取网络图片")
.onClick(() => {
let sethtp = new htp()
sethtp.set()
})
Image(this.image).height(100).width(100)

显示矢量图

Image 组件可显示矢量图(svg 格式的图片),支持的 svg 标签为:svg、rect、circle、ellipse、path、line、polyline、polygon 和 animate。

svg 格式的图片可以使用 fillColor 属性改变图片的绘制颜色。

1
2
3
Image($r('app.media.cloud'))
.width(50)
.fillColor(Color.Blue)

设置图片缩放类型

通过 objectFit 属性使图片缩放到高度和宽度确定的框内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@Entry
@Component
struct MyComponent {
scroller: Scroller = new Scroller()

build() {
Scroll(this.scroller) {
Column() {
Row() {
Image($r('app.media.img_2'))
.width(200)
.height(150)
.border({ width: 1 })
// 保持宽高比进行缩小或者放大,使得图片完全显示在显示边界内。
.objectFit(ImageFit.Contain)
.margin(15)
.overlay('Contain', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.ic_img_2'))
.width(200)
.height(150)
.border({ width: 1 })
.objectFit(ImageFit.Cover)
.margin(15)
// 保持宽高比进行缩小或者放大,使得图片两边都大于或等于显示边界。
.overlay('Cover', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.img_2'))
.width(200)
.height(150)
.border({ width: 1 })
// 自适应显示。
.objectFit(ImageFit.Auto)
.margin(15)
.overlay('Auto', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
}

Row() {
Image($r('app.media.img_2'))
.width(200)
.height(150)
.border({ width: 1 })
.objectFit(ImageFit.Fill)
.margin(15)
// 不保持宽高比进行放大缩小,使得图片充满显示边界。
.overlay('Fill', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.img_2'))
.width(200)
.height(150)
.border({ width: 1 })
// 保持宽高比显示,图片缩小或者保持不变。
.objectFit(ImageFit.ScaleDown)
.margin(15)
.overlay('ScaleDown', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.img_2'))
.width(200)
.height(150)
.border({ width: 1 })
// 保持原有尺寸显示。
.objectFit(ImageFit.None)
.margin(15)
.overlay('None', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
}
}
}
}
}

图片插值

当原图分辨率较低并且放大显示时,图片会模糊出现锯齿。这时可以使用 interpolation 属性对图片进行插值,使图片显示得更清晰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Entry
@Component
struct Index {
build() {
Column() {
Row() {
Image($r('app.media.grass'))
.width('40%')
.interpolation(ImageInterpolation.None)
.borderWidth(1)
.overlay("Interpolation.None", { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
.margin(10)
Image($r('app.media.grass'))
.width('40%')
.interpolation(ImageInterpolation.Low)
.borderWidth(1)
.overlay("Interpolation.Low", { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
.margin(10)
}.width('100%')
.justifyContent(FlexAlign.Center)

Row() {
Image($r('app.media.grass'))
.width('40%')
.interpolation(ImageInterpolation.Medium)
.borderWidth(1)
.overlay("Interpolation.Medium", { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
.margin(10)
Image($r('app.media.grass'))
.width('40%')
.interpolation(ImageInterpolation.High)
.borderWidth(1)
.overlay("Interpolation.High", { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
.margin(10)
}.width('100%')
.justifyContent(FlexAlign.Center)
}
.height('100%')
}
}

设置图片重复样式

通过 objectRepeat 属性设置图片的重复样式方式,重复样式请参考 ImageRepeat 枚举说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Entry
@Component
struct MyComponent {
build() {
Column({ space: 10 }) {
Row({ space: 5 }) {
Image($r('app.media.ic_public_favor_filled_1'))
.width(110)
.height(115)
.border({ width: 1 })
.objectRepeat(ImageRepeat.XY)
.objectFit(ImageFit.ScaleDown)
// 在水平轴和竖直轴上同时重复绘制图片
.overlay('ImageRepeat.XY', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.ic_public_favor_filled_1'))
.width(110)
.height(115)
.border({ width: 1 })
.objectRepeat(ImageRepeat.Y)
.objectFit(ImageFit.ScaleDown)
// 只在竖直轴上重复绘制图片
.overlay('ImageRepeat.Y', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.ic_public_favor_filled_1'))
.width(110)
.height(115)
.border({ width: 1 })
.objectRepeat(ImageRepeat.X)
.objectFit(ImageFit.ScaleDown)
// 只在水平轴上重复绘制图片
.overlay('ImageRepeat.X', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
}
}.height(150).width('100%').padding(8)
}
}

设置图片渲染模式

通过 renderMode 属性设置图片的渲染模式为原色或黑白。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Entry
@Component
struct MyComponent {
build() {
Column({ space: 10 }) {
Row({ space: 50 }) {
Image($r('app.media.example'))
// 设置图片的渲染模式为原色
.renderMode(ImageRenderMode.Original)
.width(100)
.height(100)
.border({ width: 1 })
// overlay 是通用属性,用于在组件上显示说明文字
.overlay('Original', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.example'))
// 设置图片的渲染模式为黑白
.renderMode(ImageRenderMode.Template)
.width(100)
.height(100)
.border({ width: 1 })
.overlay('Template', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
}
}.height(150).width('100%').padding({ top: 20,right: 10 })
}
}

设置图片解码尺寸

通过 sourceSize 属性设置图片解码尺寸,降低图片的分辨率。

原图尺寸为 1280x960,该示例将图片解码为 40x40 和 90x90。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Entry
@Component
struct Index {
build() {
Column() {
Row({ space: 50 }) {
Image($r('app.media.example'))
.sourceSize({
width: 40,
height: 40
})
.objectFit(ImageFit.ScaleDown)
.aspectRatio(1)
.width('25%')
.border({ width: 1 })
.overlay('width:40 height:40', { align: Alignment.Bottom, offset: { x: 0, y: 40 } })
Image($r('app.media.example'))
.sourceSize({
width: 90,
height: 90
})
.objectFit(ImageFit.ScaleDown)
.width('25%')
.aspectRatio(1)
.border({ width: 1 })
.overlay('width:90 height:90', { align: Alignment.Bottom, offset: { x: 0, y: 40 } })
}.height(150).width('100%').padding(20)
}
}
}

为图片添加滤镜效果

通过 colorFilter 修改图片的像素颜色,为图片添加滤镜。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entry
@Component
struct Index {
build() {
Column() {
Row() {
Image($r('app.media.example'))
.width('40%')
.margin(10)
Image($r('app.media.example'))
.width('40%')
.colorFilter(
[1, 1, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, 1, 0])
.margin(10)
}.width('100%')
.justifyContent(FlexAlign.Center)
}
}
}

同步加载图片

一般情况下,图片加载流程会异步进行,以避免阻塞主线程,影响 UI 交互。但是特定情况下,图片刷新时会出现闪烁,这时可以使用 syncLoad 属性,使图片同步加载,从而避免出现闪烁。不建议图片加载较长时间时使用,会导致页面无法响应。

1
2
Image($r('app.media.icon'))
.syncLoad(true)

事件调用

通过在 Image 组件上绑定 onComplete 事件,图片加载成功后可以获取图片的必要信息。如果图片加载失败,也可以通过绑定 onError 回调来获得结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Entry
@Component
struct MyComponent {
@State widthValue: number = 0
@State heightValue: number = 0
@State componentWidth: number = 0
@State componentHeight: number = 0

build() {
Column() {
Row() {
Image($r('app.media.ic_img_2'))
.width(200)
.height(150)
.margin(15)
.onComplete(msg => {
if(msg){
this.widthValue = msg.width
this.heightValue = msg.height
this.componentWidth = msg.componentWidth
this.componentHeight = msg.componentHeight
}
})
// 图片获取失败,打印结果
.onError(() => {
console.info('load image fail')
})
.overlay('\nwidth: ' + String(this.widthValue) + ', height: ' + String(this.heightValue) + '\ncomponentWidth: ' + String(this.componentWidth) + '\ncomponentHeight: ' + String(this.componentHeight), {
align: Alignment.Bottom,
offset: { x: 0, y: 60 }
})
}
}
}
}

布局组件

ArkTS 通用属性

ArkTS 通用属性用于设置组件的宽高、边距。

名称 参数说明 描述
width Length 设置组件自身的宽度,缺省时使用元素自身内容需要 的宽度。若子组件的宽大于父组件的宽,则会画出父 组件的范围。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
height Length 设置组件自身的高度,缺省时使用元素自身内容需要 的高度。若子组件的高大于父组件的高,则会画出父 组件的范围。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
size { width ? : Length, height ? : Length } 设置高宽尺寸。
padding Padding | Length 设置内边距属性。参数为 Length 类型时,四个方向内边距同时生效。默认值:0。padding 设置百分比时,上下左右内边距均以父容器的 width 作为基础值。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
margin Margin | Length 设置外边距属性。参数为 Length 类型时,四个方向外边距同时生效。默认值:0。margin 设置百分比时,上下左右外边距均以父容器的 width 作为基础值。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
constraintSize { minWidth ? : Length, maxWidth ? : Length, minHeight ? : Length, maxHeight ? : Length } 设置约束尺寸,组件布局时,进行尺寸范围限制。 constraintSize 的优先级高于 Width 和 Height。若设置的 minWidth 大于 maxWidth,则 minWidth 生效, minHeight 与 maxHeight 同理。默认值:{ minWidth: 0, maxWidth: Infinity, minHeight: 0, maxHeight: Infinity }。

Length 长度类型,用于描述尺寸单位。

类型 说明
string 需要显式指定像素单位,如 ‘10px’,也可设置百分比字符串,如 ‘100%’。
number 默认单位 vp。
Resource 资源引用类型,引入系统资源或者应用资源中的尺寸。

vp 为虚拟像素单位:vp 使用虚拟像素,使元素在不同密度的设备上具有一致的视觉体量。

fp 字体像素单位:字体像素(font pixel)大小默认情况下与 vp 相同,即默认情况下 1 fp = 1 vp。如果用户在设置中选择了更大的字体,字体的实际显示大小就会在 vp 的基础上乘以用户设置的缩放系数,即 1 fp = 1 vp * 缩放系数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@Entry
@Component
struct Index {
build() {
Column({ space: 10 }) {
Row() {
// 宽度 200,高度 200,外边距 20(蓝色区域),内边距 10(白色区域)
Row() {
Row().size({
width: '100%', height: '100%'
}).backgroundColor(Color.Yellow)
}
.width(200)
.height(200)
.padding(10)
.margin(20)
.backgroundColor(Color.White)
}.backgroundColor(Color.Blue)

Text(`This is a text.This is a text.This is a text.This is a text.This is a text`)
.width('90%')
.backgroundColor(Color.Orange)
.constraintSize({ maxWidth: 200 })

// 父容器尺寸确定时,设置了 layoutWeight 的子元素在主轴布局尺寸按照权重进行分配,忽略本身尺寸设置。
Row() {
// 权重 1,占主轴剩余空间 1/3
Text('layoutWeight(1)')
.size({ height: 110 })
.backgroundColor(Color.Orange)
.textAlign(TextAlign.Center)
.layoutWeight(1)

// 权重 2,占主轴剩余空间 2/3
Text('layoutWeight(2)')
.size({
width: '30%', height: 110
})
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.layoutWeight(2)

// 未设置 layoutWeight 属性,组件按照自身尺寸渲染
Text('no layoutWeight')
.size({
width: '33%', height: 110
})
.backgroundColor(Color.Brown)
.textAlign(TextAlign.Center)
}.size({ width: '100%', height: 140 })
.backgroundColor(0xCCCCCC)
}
.width('100%')
.height('100%')
.margin({ top: 5 })
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Start)
}
}

Column 详解

Column 是沿垂直方向布局的容器,可以包含多个子组件,多个子组件会在垂直方向上按照顺序排列。

Column 接口

1
Column(value?: {space?: string | number})

参数:

参数名 参数类型 必填 参数描述
space string | number 纵向布局元素垂直方向间距。从 API version 9 开始,space 为负数或者 justifyContent 设置为 FlexAlign.SpaceBetween、 FlexAlign.SpaceAround、FlexAlign.SpaceEvenly 时不生效。默认值:0,单位 vp。说明:可选值为大于等于 0 的数字,或者可以转换为数字的字符串。

Column 属性

名称 参数类型 描述
alignItems HorizontalAlign 设置子组件在水平方向上的对齐格式。默认值:HorizontalAlign.Center 从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
justifyContent8+ FlexAlign 设置子组件在垂直方向上的对齐格式。默认值:FlexAlign.Start 从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Entry
@Component
struct Index {
build() {
Column({ space: 10 }) {
Row() {
($r('app.media.app_icon')).width(100).height(100)
}
.width(140)
.height(140)
.backgroundColor(Color.Red)
.alignItems(VerticalAlign.Center)
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
.margin({ top: 5 })
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Start)
}
}

Row 详解

Row 为沿水平方向布局容器,可以包含多个子组件,多个子组件会在水平方向上按照顺序排列。

Row 接口

1
Row(value?: {space?: string | number})

参数:

参数 名 参数类 型 必 填 参数描述
space string | number 横向布局元素间距。从 API version 9 开始,space 为负数或者 justifyContent 设置为 FlexAlign.SpaceBetween、FlexAlign.SpaceAround、FlexAlign.SpaceEvenly 时不生效。默认值:0,单位 vp。说明:可选值为大于等于 0 的数字,或者可以转换为 数字的字符串。

Row 属性

名称 参数类型 描述
alignItems VerticalAlign 设置子组件在垂直方向上的对齐格式。默认值:VerticalAlign.Center 从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
justifyContent8+ FlexAlign 设置子组件在水平方向上的对齐格式。默认值:FlexAlign.Start 从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。

自定义组件

自定义组件传值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Entry
@Component
struct Index {
build() {
Column({ space: 10 }) {
Container()
Container({ color: Color.Orange, icon: $r('app.media.user') })
Container({ color: Color.Brown, icon: $r('app.media.settings') })
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.SpaceBetween)
}
}

@Component
struct Container {
color: Color = Color.Red
icon: Resource = $r('app.media.app_icon')

build() {
Row() {
Image(this.icon).width(100).height(100)
}
.width(140)
.height(140)
.backgroundColor(this.color)
.alignItems(VerticalAlign.Center)
.justifyContent(FlexAlign.Center)
}
}

Row、Column 结合 layoutWeight 实现弹性布局

ArkTS 中使用 Row、Column 结合 layoutWeight 属性可以实现弹性布局。

水平弹性布局

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entry
@Component
struct Index {
build() {
Row() {
Row().height(100).backgroundColor(Color.Red).layoutWeight(1)
Row().height(100).backgroundColor(Color.Green).layoutWeight(1)
Row() {
Text('no layoutWeight').textAlign(TextAlign.Center).width('100%')
}.height(100).backgroundColor(Color.Orange).size({ height: 100, width: 200 })
}.size({ width: '100%', height: 100 })
}
}

垂直弹性布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Entry
@Component
struct Index {
build() {
Column({ space: 10 }) {
Row() {
Image("https://gitee.com/zch0304/images/raw/master/note/test_icon1.jpg").objectFit(ImageFit.Fill)
}.size({ width: '100%', height: 160 })

Row({ space: 10 }) {
Row() {
Image("https://gitee.com/zch0304/images/raw/master/note/test_icon2.jpg").objectFit(ImageFit.Fill)
}.layoutWeight(2)

Row() {
Column({ space: 10 }) {
Row() {
Image("https://gitee.com/zch0304/images/raw/master/note/test_icon3.jpg").objectFit(ImageFit.Fill)
}.layoutWeight(1).width('100%')

Row() {
Image("https://gitee.com/zch0304/images/raw/master/note/test_icon4.jpg").objectFit(ImageFit.Fill)
}.layoutWeight(1).width('100%')
}
}.layoutWeight(1)
}.size({ width: '100%', height: 140 })
}
.margin(5)
}
}

Stack 定位组件

Stack 组件可以实现容器堆叠,子组件按照顺序依次入栈,后一个子组件覆盖前一个子组件。

Stack 接口

1
Stack(value?: { alignContent?: Alignment })

参数:

参数名 参数类型 必 填 参数描述
alignContent Alignment 设置子组件在容器内的对齐方式。默认值:Alignment.Center。

Stack 基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Entry
@Component
struct Index {
build() {
Column({ space: 10 }) {
Stack({ alignContent: Alignment.BottomStart }) {
Row() {
Text("HarmonyOS").fontSize(40).textAlign(TextAlign.Center)
}.width(300).height(300).backgroundColor(Color.Orange)

Row().width(100).height(100).backgroundColor(Color.Blue)
}
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
}
}

Stack 子组件中 zIndex 控制层级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Entry
@Component
struct Index {
build() {
Stack({ alignContent: Alignment.BottomStart }) {
Column() {
Text("Stack子元素1").fontSize(20)
}.width(100).height(100).backgroundColor(Color.Orange).zIndex(2)

Column() {
Text("Stack子元素2").fontSize(20)
}.width(150).height(150).backgroundColor(Color.Yellow).zIndex(1)

Column() {
Text("Stack子元素3").fontSize(20)
}.width(200).height(200).backgroundColor(Color.Pink)
}.width(350).height(350).backgroundColor(Color.Gray)
}
}

Stack 结合 List 实现动态列表

要实现的功能:
1、实现浮动按钮。
2、点击按钮让列表的数字加一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Entry
@Component
struct Index {
@State list: number[] = [1, 2, 3]

build() {
Column() {
Stack({ alignContent: Alignment.BottomEnd }) {
List({ space: 10 }) {
ForEach(this.list, (item: number) => {
ListItem() {
Text(`${item}`)
.fontSize(20)
.width('100%')
.height(50)
.backgroundColor('#eee')
.textAlign(TextAlign.Center)
}
})
}.width('100%').height('100%').padding(10)

Button({ type: ButtonType.Circle, stateEffect: true }) {
Text('+').fontSize(40).fontColor(Color.White)
}.width(55).height(55).margin({ right: 20, bottom: 20 }).onClick(() => {
this.doAdd()
})
}
}.width('100%').height('100%').alignItems(HorizontalAlign.Center)
}

doAdd() {
this.list.push(this.list[this.list.length-1] + 1)
}
}

Stack 结合 List 实现浮动导航

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Entry
@Component
struct Index {
@State list: number[] = [1, 2, 3]

build() {
Column() {
Stack({ alignContent: Alignment.BottomEnd }) {
Stack({ alignContent: Alignment.TopEnd }) {
List({ space: 10 }) {
ForEach(this.list, (item: number) => {
ListItem() {
Text(`${item}`)
.fontSize(20)
.width('100%')
.height(50)
.backgroundColor('#eee')
.textAlign(TextAlign.Center)
}
})
}.width('100%').height('100%').padding({ left: 10, top: 70, right: 10, bottom: 10 })

Row() {
Text("导航").fontSize(16).textAlign(TextAlign.Center).width('100%')
}.width('100%').height(60).backgroundColor(Color.Orange)
}

Button({ type: ButtonType.Circle, stateEffect: true }) {
Text('+').fontSize(40).fontColor(Color.White)
}.width(55).height(55).margin({ right: 20, bottom: 20 }).onClick(() => {
this.doAdd()
})
}
}.width('100%').height('100%').alignItems(HorizontalAlign.Center)
}

doAdd() {
this.list.push(this.list[this.list.length-1] + 1)
}
}

弹性布局(Flex)

Flex 弹性布局可以更加方便的对容器中的子元素进行排列、对齐和分配剩余空间等。单行的 Flex 跟 Row 组件的表现几乎一致,单列的 Flex 则跟 Column 组件表现几乎一致。但 Row 与 Column 都是单行单列的,Flex 则可以突破了这个限制,当主轴上空间不足时,则向纵轴上去扩展显示。

Flex 接口

1
Flex(value?: { direction?: FlexDirection, wrap?: FlexWrap, justifyContent?: FlexAlign, alignItems?: ItemAlign, alignContent?: FlexAlign })

标准 Flex 布局容器。从 API version 9 开始,该接口支持在 ArkTS 片中使用。

Flex 参数

参数名 参数类型 必 填 默认值 参数描述
direction FlexDirection FlexDirection.Row 子组件在 Flex 容器上排列的方向,即主轴的方向。
wrap FlexWrap FlexWrap.NoWrap Flex 容器是单行/列还是多行/列排列。说明:在多行布局时,通过交叉轴方向,确认新行堆叠方向。
justifyContent FlexAlign FlexAlign.Start 子组件在 Flex 容器主轴上的对齐格式。
alignItems ItemAlign ItemAlign.Start 子组件在 Flex 容器交叉轴上的对齐格式。
alignContent FlexAlign FlexAlign.Start 交叉轴中有额外的空间时,多行内容的对齐方式。仅在 wrap 为 Wrap 或 WrapReverse 下生效。

Flex 轴的基本概念

  • 主轴:Flex 组件布局方向的轴线,子元素默认沿着主轴排列。主轴开始的位置称为主轴起始点,结束位置称为主轴结束点。
  • 交叉轴:垂直于主轴方向的轴线。交叉轴开始的位置称为交叉轴起始点,结束位置称为交叉轴结束点。

direction 参数控制布局方向

Flex 布局中通过 direction 可以改变布局方向,在弹性布局中,容器的子元素可以按照任意方向排列。通过设置参数 direction,可以决定主轴的方向,从而控制子组件的排列方向。

  • FlexDirection.Row:主轴为水平方向,子组件从起始端沿着水平方向开始排布。
  • FlexDirection.RowReverse:主轴为水平方向,子组件从终点端沿着 FlexDirection. Row 相反的方向开始排布。
  • FlexDirection.Column:主轴为垂直方向,子组件从起始端沿着垂直方向开始排布。
  • FlexDirection.ColumnReverse:主轴为垂直方向,子组件从终点端沿着 FlexDirection. Column 相反的方向开始排布。
1
2
3
4
5
6
7
8
9
10
11
@Entry
@Component
struct Index {
build() {
Flex({ direction: FlexDirection.Row }) {
Text('1').width('33%').height(50).backgroundColor(Color.Red)
Text('2').width('33%').height(50).backgroundColor(Color.Green)
Text('3').width('33%').height(50).backgroundColor(Color.Orange)
}.width('90%').height(70).padding(10).backgroundColor('#ccc')
}
}

wrap 参数控制布局换行

弹性布局分为单行布局和多行布局。默认情况下,Flex 容器中的子元素都排在一条线(又称“轴线”) 上。wrap 属性控制当子元素主轴尺寸之和大于容器主轴尺寸时,Flex 是单行布局还是多行布局。在多行布局时,通过交叉轴方向,确认新行堆叠方向。

FlexWrap. NoWrap(默认值):不换行。如果子组件的宽度总和大于父元素的宽度,则子组件会被压缩宽度。

1
2
3
4
5
6
7
8
9
10
11
@Entry
@Component
struct Index {
build() {
Flex({ wrap: FlexWrap.NoWrap }) {
Text('1').width('50%').height(50).backgroundColor(Color.Red)
Text('2').width('50%').height(50).backgroundColor(Color.Green)
Text('3').width('50%').height(50).backgroundColor(Color.Orange)
}.width('90%').padding(10).backgroundColor('#ccc')
}
}

FlexWrap. Wrap:换行,每一行子组件按照主轴方向排列。

1
2
3
4
5
6
7
8
9
10
11
@Entry
@Component
struct Index {
build() {
Flex({ wrap: FlexWrap.Wrap }) {
Text('1').width('50%').height(50).backgroundColor(Color.Red)
Text('2').width('50%').height(50).backgroundColor(Color.Green)
Text('3').width('50%').height(50).backgroundColor(Color.Orange)
}.width('90%').padding(10).backgroundColor('#ccc')
}
}

FlexWrap. WrapReverse:换行,每一行子组件按照主轴反方向排列。

1
2
3
4
5
6
7
8
9
10
11
@Entry
@Component
struct Index {
build() {
Flex({ wrap: FlexWrap.WrapReverse }) {
Text('1').width('50%').height(50).backgroundColor(Color.Red)
Text('2').width('50%').height(50).backgroundColor(Color.Green)
Text('3').width('50%').height(50).backgroundColor(Color.Orange)
}.width('90%').padding(10).backgroundColor('#ccc')
}
}

justifyContent 配置主轴对齐方式

FlexAlign.Start(默认值):子组件在主轴方向起始端对齐,第一个子组件与父元素边沿对齐,其他元素与前一个元素对齐。

FlexAlign.Center:子组件在主轴方向居中对齐。

FlexAlign.End:子组件在主轴方向终点端对齐,最后一个子组件与父元素边沿对齐,其它元素与后一个元素对齐。

FlexAlign.SpaceBetween:Flex 主轴方向均匀分配弹性元素,相邻子组件之间距离相同。第一个子组件和最后一个子组件与父元素边沿对齐。

FlexAlign.SpaceAround:Flex 主轴方向均匀分配弹性元素,相邻子组件之间距离相同。第一个子组件到主轴起始端的距离和最后一个子组件到主轴终点端的距离是相邻元素之间距离的一半。

FlexAlign.SpaceEvenly:Flex 主轴方向元素等间距布局,相邻子组件之间的间距、第一个子组件与主轴起始端的间距、最后一个子组件到主轴终点端的间距均相等。

alignContent 配置内容对齐

可以通过 alignContent 参数设置子组件各行在交叉轴剩余空间内的对齐方式,只在多行的 flex 布局中生效(仅在 wrap 为 Wrap 或 WrapReverse 下生效),可选值有:

FlexAlign.Start:子组件各行与交叉轴起点对齐,默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entry
@Component
struct Index {
build() {
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.Start }) {
Text('1').width('30%').height(20).backgroundColor(Color.Red)
Text('2').width('60%').height(20).backgroundColor(Color.Green)
Text('3').width('40%').height(20).backgroundColor(Color.Orange)
Text('4').width('30%').height(20).backgroundColor(Color.Pink)
Text('5').width('20%').height(20).backgroundColor(Color.Yellow)
}.height(100).backgroundColor('#ccc')
}
}

FlexAlign.Center:子组件各行在交叉轴方向居中对齐。

FlexAlign.End:子组件各行与交叉轴终点对齐。

FlexAlign.SpaceBetween:子组件各行与交叉轴两端对齐,各行间垂直间距平均分布。

FlexAlign.SpaceAround:子组件各行间距相等,是元素首尾行与交叉轴两端距离的两倍。

FlexAlign.SpaceEvenly:子组件各行间距,子组件首尾行与交叉轴两端距离都相等。

alignItems 参数控制交叉轴对齐方式

容器和子元素都可以设置交叉轴对齐方式,且子元素设置的对齐方式优先级较高。可以通过 Flex 组件的 alignItems 参数设置子组件在交叉轴的对齐方式。

ItemAlign.Auto:使用 Flex 容器中默认配置。

1
2
3
4
5
6
7
8
9
10
11
@Entry
@Component
struct Index {
build() {
Flex({ alignItems: ItemAlign.Auto }) {
Text('1').width('33%').height(20).backgroundColor(Color.Red)
Text('2').width('33%').height(40).backgroundColor(Color.Green)
Text('3').width('33%').height(60).backgroundColor(Color.Orange)
}.height(80).backgroundColor('#ccc')
}
}

ItemAlign.Start:交叉轴方向首部对齐,默认值。

ItemAlign.Center:交叉轴方向居中对齐。

ItemAlign.End:交叉轴方向底部对齐。

ItemAlign.Stretch:交叉轴方向拉伸填充,在未设置尺寸时,拉伸到容器尺寸。

ItemAlign. Baseline:交叉轴方向文本基线对齐。

Flex 自适应拉伸布局属性

Row、Column 结合 layoutWeight 可以实现自适应拉伸弹性布局。

1、flexGrow 属性

设置父容器的剩余空间分配给此属性所在组件的比例。用于“瓜分”父组件的剩余空间。

1
2
3
4
5
6
7
8
9
10
11
@Entry
@Component
struct Index {
build() {
Flex() {
Text('1').flexGrow(1).height(60).backgroundColor(Color.Red)
Text('2').flexGrow(2).height(60).backgroundColor(Color.Green)
Text('no flexGrow').width(100).height(60).backgroundColor(Color.Orange)
}.height(80).backgroundColor('#ccc')
}
}

父容器宽度 400vp,三个子组件原始宽度为 100vp,总和 300vp,剩余空间 100vp 根据 flexGrow 值的占比分配给子组件,未设置 flexGrow 的子组件不参与“瓜分”。 第一个元素以及第二个元素以 2:3 分配剩下的 100vp。第一个元素为 100vp+100vp2/5=140vp,第 二个元素为 100vp+100vp3/5=160vp。

2、flexShrink 属性

当父容器空间不足时,子组件的压缩比例。

1
2
3
4
5
6
7
8
9
10
11
@Entry
@Component
struct Index {
build() {
Flex() {
Text('flexShrink(3)').flexShrink(3).width(200).height(60).backgroundColor(Color.Red)
Text('no flexShrink').width(200).height(60).backgroundColor(Color.Green)
Text('flexShrink(1)').flexShrink(1).width(200).height(60).backgroundColor(Color.Orange)
}.width(400).height(80).backgroundColor('#ccc')
}
}

Flex 应用案例

弹性布局在开发场景中用例特别多,比如页面头部导航栏的均匀分布、页面框架的搭建、多行多列数据的排列等等。

1、热搜功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Entry
@Component
struct Index {
@State hotSearch: string[] = ['乒乓球', 'NBA直播', '世界杯', '广东城际铁路', 'Flutter教程', 'HarmonyOS', 'ArkTS']

build() {
Column() {
Text('热门搜索')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Start)
.padding(10)
.fontColor('#666')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.hotSearch, (item: string) => {
Text(`${item}`)
.fontSize(18)
.backgroundColor('#eee')
.padding({ left: 16, top: 10, right: 16, bottom: 10 })
.margin(10)
.borderRadius(12)
}, (item: string) => item)
}
}.width('100%').height('100%')
}
}

2、帮助列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
interface HelpListInterface {
title: string,
icon: Resource
}

@Entry
@Component
struct Index {
@State helpList: HelpListInterface[] = [
{
title: "我的订阅",
icon: $r("app.media.subscribe")
},
{
title: "常见问题",
icon: $r("app.media.problem")
},
{
title: "在线客服",
icon: $r("app.media.customer_service")
},
{
title: "意见反馈",
icon: $r("app.media.opinion")
},
{
title: "关怀模式",
icon: $r("app.media.give")
},
{
title: "会员中心",
icon: $r("app.media.user")
}
]

build() {
Column() {
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.helpList, (item: HelpListInterface) => {
Column() {
Image(item.icon).width(42).height(42)
Text(`${item.title}`).fontSize(15).fontColor('#666').padding({ top: 10 })
}.width('25%').padding(5).margin({ bottom: 10 })
}, (item: HelpListInterface) => item.title)
}
.margin(10)
.backgroundColor('#fff')
.borderRadius(10)
.padding({ top: 15, bottom: 5 })
}.width('100%').height('100%').backgroundColor('#eee')
}
}

相对布局(RelativeContainer)

相对布局组件,用于复杂场景中元素对齐的布局。

RelativeContainer 就是采用相对布局的容器,支持容器内部的子元素设置相对位置关系。子元素支持指定兄弟元素作为锚点,也支持指定父容器作为锚点,基于锚点做相对位置布局。下图是一个 RelativeContainer 的概念图,图中的虚线表示位置的依赖关系。

规则说明

1、容器内子组件区分水平方向,垂直方向:

​ (1)水平方向为 left,middle,right,对应容器的 HorizontalAlign.Start,HorizontalAlign.Center,HorizontalAlign.End。

​ (2)垂直方向为 top,center,bottom,对应容器的 VerticalAlign.Top,VerticalAlign.Center,VerticalAlign.Bottom。

2、子组件可以将容器或者其他子组件设为锚点:

​ (1)参与相对布局的容器内组件必须设置 id,不设置 id 的组件不显示,RelativeContainer 容器的固定 id 为 __container__。

​ (2)此子组件某一方向上的三个位置可以将容器或其它子组件的同方向三个位置为锚点,同方向上两个以上位置设置锚点以后会跳过第三个。

​ (3)前端页面设置的子组件尺寸大小不会受到相对布局规则的影响。子组件某个方向上设置两个或以上 alignRules 时不建议设置此方向尺寸大小。

​ (4)对齐后需要额外偏移可设置 offset

3、特殊情况:

​ (1)互相依赖,环形依赖时容器内子组件全部不绘制。

​ (2)同方向上两个以上位置设置锚点但锚点位置逆序时,此子组件大小为 0,即不绘制。

​ (3)容器不设置宽高时,容器与容器内子组件不绘制。

基本使用演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Entry
@Component
struct Index {
build() {
Column() {
RelativeContainer() {
Row()
.width(100)
.height(100)
.backgroundColor(Color.Orange)
.alignRules({
left: { anchor: '__container__', align: HorizontalAlign.Start },
top: { anchor: '__container__', align: VerticalAlign.Top }
})
.id('row1')
.offset({ x: 20, y: 20 })
}.width(200).height(200).border({ width: 2, color: Color.Green }).id('rc')
}.width('100%').height('100%')
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@Entry
@Component
struct Index {
build() {
RelativeContainer() {
Text('1')
.id('text1')
.width(100)
.height(100)
.fontSize(30)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Red)
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
Text('2')
.id('text2')
.width(100)
.height(100)
.fontSize(30)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Green)
.alignRules({
top: { anchor: 'text1', align: VerticalAlign.Bottom },
right: { anchor: 'text1', align: HorizontalAlign.Start }
})
Text('3')
.id('text3')
.width(100)
.height(100)
.fontSize(30)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Blue)
.alignRules({
top: { anchor: 'text1', align: VerticalAlign.Bottom },
left: { anchor: 'text1', align: HorizontalAlign.Start }
})
Text('4')
.id('text4')
.width(100)
.height(100)
.fontSize(30)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Yellow)
.alignRules({
top: { anchor: 'text1', align: VerticalAlign.Bottom },
left: { anchor: 'text1', align: HorizontalAlign.End }
})
Text('5')
.id('text5')
.width(100)
.height(100)
.fontSize(30)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Pink)
.alignRules({
top: { anchor: 'text3', align: VerticalAlign.Bottom },
left: { anchor: 'text3', align: HorizontalAlign.Start }
})
}
}
}

实现底部弹出按钮案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
@Entry
@Component
struct Index {
@State flag: boolean = false

build() {
Column() {
Stack({ alignContent: Alignment.BottomEnd }) {
List({ space: 10 }) {
ForEach([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], (item: number) => {
ListItem() {
Text(`${item}`)
.fontSize(30)
.width('100%')
.height(80)
.backgroundColor('#eee')
.borderRadius(10)
.textAlign(TextAlign.Center)
}
})
}.width('100%').height('100%').padding(10)

RelativeContainer() {
Button({ stateEffect: true }) {
Text('+').textAlign(TextAlign.Center).fontColor(Color.White).fontSize(30)
}
.id('add')
.width(80)
.height(80)
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
left: { anchor: '__container__', align: HorizontalAlign.Start }
})

if (this.flag) {
Button() {
Text('A').textAlign(TextAlign.Center).fontColor(Color.White).fontSize(30)
}
.id('A')
.width(80)
.height(80)
.backgroundColor(Color.Orange)
.alignRules({
bottom: { anchor: 'add', align: VerticalAlign.Top },
right: { anchor: 'add', align: HorizontalAlign.End }
})
.offset({ y: -30 })
.opacity(0.8)

Button() {
Text('B').textAlign(TextAlign.Center).fontColor(Color.White).fontSize(30)
}
.id('B')
.width(80)
.height(80)
.backgroundColor(Color.Gray)
.alignRules({
bottom: { anchor: 'add', align: VerticalAlign.Top },
right: { anchor: 'add', align: HorizontalAlign.Start }
})
.offset({ x: -10, y: -10 })
.opacity(0.8)

Button() {
Text('C').textAlign(TextAlign.Center).fontColor(Color.White).fontSize(30)
}
.id('C')
.width(80)
.height(80)
.backgroundColor(Color.Green)
.alignRules({
top: { anchor: 'add', align: VerticalAlign.Top },
right: { anchor: 'add', align: HorizontalAlign.Start }
})
.offset({ x: -30 })
.opacity(0.8)
}

}
.width(88)
.height(88)
.onClick(() => {
this.flag = !this.flag
})
}
}
}
}

List 组件详解

List 适合用于呈现同类数据类型或数据类型集,例如图片和文本。在列表中显示数据集合是许多应用程序中的常见要求(如通讯录、音乐列表、购物清单等)。

List 包含 ListItem、ListItemGroup 子组件。

List 接口

1
List(value?:{space?: number | string, initialIndex?: number, scroller?: Scroller})

从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。

参数:

参数名 参数类型 必填 参数描述
space number | string 子组件主轴方向的间隔。默认值:0。说明:设置为除 -1 外其它负数或百分比时,按默认值显示。space 参数值小于 List 分割线宽度时,子组件主轴方向的间隔取分割线宽度。
initialIndex number 设置当前 List 初次加载时视口起始位置显示的 item 的索引值。默认值:0。说明:设置为除 -1 外其它负数或超过了当前 List 最后一个 item 的索引值时视为无效取值,无效取值按默认值显示。
scroller Scroller 可滚动组件的控制器。用于与可滚动组件进行绑定。说明:不允许和其它滚动类组件绑定同一个滚动控制对象。

List 属性

名称 参数类型 描述
listDirection Axis 设置 List 组件排列方向。默认值:Axis.Vertical。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
divider { strokeWidth: Length; color?: ResourceColor; startMargin?: Length; endMargin?: Length; } | null 设置 ListItem 分割线样式,默认无分割线。- strokeWidth:分割线的线宽。- color:分割线的颜色。- startMargin:分割线与列表侧边起始端的距离。- endMargin:分割线与列表侧边结束端的距离。从 API version 9 开始,该接口支持在ArkTS卡片中使用。endMargin + startMargin 不能超过列宽度。startMargin 和 endMargin 不支持设置百分比。List 的分割线画在主轴方向两个子组件之间,第一个子组件上方和最后一个子组件下方不会绘制分割线。多列模式下,ListItem 与 ListItem 之间的分割线起始边距从每一列的交叉轴方向起始边开始计算,其它情况从 List 交叉轴方向起始边开始计算。
scrollBar BarState 设置滚动条状态。默认值:BarState.Off 从 API version 9 开始,该接口支持在 ArkTS卡片中使用。
cachedCount number 设置列表中 ListItem / ListItemGroup 的预加载数量,其中 ListItemGroup 将作为一个整体进行计算,ListItemGroup 中的所有 ListItem 会一次性全部加载出来。具体使用可参考减少应用白块说明。默认值:1。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。说明:单列模式下,会在 List 显示的 ListItem 前后各缓存 cachedCount 个 ListItem。多列模式下,会在 List 显示的 ListItem 前后各缓存 cachedCount 个 ListItem。
editMode(deprecated) boolean 声明当前 List 组件是否处于可编辑模式。可参考示例 3 实现删除选中的 list 项。从 API version9 开始废弃。默认值:false。
edgeEffect EdgeEffect 设置组件的滑动效果。默认值:EdgeEffect.Spring。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
chainAnimation boolean 设置当前 List 是否启用链式联动动效,开启后列表滑动以及顶部和底部拖拽时会有链式联动的效果。链式联动效果:List 内的 listitem 间隔一定距离,在基本的滑动交互行为下,主动对象驱动从动对象进行联动,驱动效果遵循弹簧物理动效。默认值:false。- false:不启用链式联动。- true:启用链式联动。从 API version 9 开始,该接口支持在 ArkTS 卡片中 使用。
multiSelectable8+ boolean 是否开启鼠标框选。默认值:false。- false:关闭框选。- true:开启框选。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
lanes9+ number | LengthConstrain 以列模式为例(listDirection 为 Axis.Vertical):lanes 用于决定 List 组件在交叉轴方向按几列布局。默认值:1。规则如下:- lanes 为指定的数量时,根据指定的数量与 List 组件的交叉轴尺寸除以列数作为列的宽度。- lanes 设置了 {minLength,maxLength} 时, 根据 List 组件的宽度自适应决定 lanes 数量(即列数),保证缩放过程中 lane 的宽度符合 {minLength,maxLength} 的限制。其中,minLength 条件会被优先满足,即优先保证符合 ListItem 的交叉轴尺寸符合最小限制。- lanes 设置了 {minLength,maxLength},如果父组件交叉轴方向尺寸约束为无穷大时,固定按一列排列,列宽度按显示区域内最大的 ListItem 计算。- ListItemGroup 在多列模式下也是独占一行,ListItemGroup 中的 ListItem 按照 List 组件的 lanes 属性设置值来布局。- lanes 设置了 {minLength,maxLength} 时,计算列数会按照 ListItemGroup 的交叉轴尺寸计算。当 ListItemGroup 交叉轴尺寸与 List 交叉轴尺寸不一致时,ListItemGroup 中的列数与 List 中的列数可能不一样。该接口支持在 ArkTS 卡片中使用。
alignListItem9+ ListItemAlign List 交叉轴方向宽度大于 ListItem 交叉轴宽度 * lanes 时,ListItem 在 List 交叉轴方向的布局方式,默认为首部对齐。默认值:ListItemAlign.Start。该接口支持在 ArkTS 卡片中使用。
sticky9+ StickyStyle 配合 ListItemGroup 组件使用,设置 ListItemGroup 中 header 和 footer 是否要吸顶或吸底。默认值:StickyStyle.None。该接口支持在 ArkTS 卡片中使用。说明:sticky 属性可设置为 StickyStyle.Header | StickyStyle.Footer 以同时支持 header 吸顶和 footer 吸底。

List 事件

名称 功能描述
onItemDelete(deprecated) (event: (index: number) => boolean) 当 List 组件在编辑模式时,点击 ListItem 右边出现的删除按钮时触发。从 API version9 开始废弃。- index:被删除的列表项的索引值。
onScroll(event: (scrollOffset: number, scrollState: ScrollState) => void) 列表滑动时触发。- scrollOffset:滑动偏移量。- scrollState:当前滑动状态。使用控制器调用 ScrollEdge 和 ScrollToIndex 时不会触发,其余情况有滚动就会触发该事件。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
onScrollIndex(event: (start: number, end: number) => void) 列表滑动时触发。计算索引值时,ListItemGroup 作为一个整体占一个索引值,不计算 ListItemGroup 内部 ListItem 的索引值。- start:滑动起始位置索引值。- end:滑动结束位置索引值。触发该事件的条件:列表初始化时会触发一次,List 显示区域内第一个子组件的索引值或后一个子组件的索引值有变化时会触发。List 的边缘效果为弹簧效果时,在 List 滑动到边缘继续滑动和松手回弹过程不会触发 onScrollIndex 事件。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
onReachStart(event: () => void) 列表到达起始位置时触发。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。说明:List 初始化时如果 initialIndex 为 0 会触发一次,List 滚动到起始位置时触发一次。List 边缘效果为弹簧效果时,滑动经过起始位置时触发一次,回弹回起始位置时再触发 一次。
onReachEnd(event: () => void) 列表到达末尾位置时触发。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。说明:List 边缘效果为弹簧效果时,滑动经过末尾位置时触发一次,回弹回末尾位置时再触发一次。
onScrollFrameBegin9+ (event: (offset: number, state: ScrollState) => { offsetRemain }) 列表开始滑动时触发,事件参数传入即将发生的滑动量,事件处理函数中可根据应用场景计算实际需要的滑动量并作为事件处理函数的返回值返回,列表将按照返回值的实际滑动量进行滑动。- offset:即将发生的滑动量,单位 vp。- state:当前滑动状态。- offsetRemain:实际滑动量,单位 vp。触发该事件的条件:手指拖动 List、List 惯性滑动时每帧开始时触发;List 超出边缘回弹、 使用滚动控制器的滚动不会触发。该接口支持在 ArkTS 卡片中使用。说明:当 listDirection 的值为 Axis.Vertical 时,返回垂直方向滑动量,当 listDirection 的值为 Axis.Horizontal 时,返回水平方向滑动量。
onScrollStart9+(event: () => void) 列表滑动开始时触发。手指拖动列表或列表的滚动条触发的滑动开始时,会触发该事件。使用 Scroller 滑动控制器触发的带动画的滑动,动画开始时会触发该事件。该接口支持在 ArkTS 卡片中使用。
onScrollStop(event: () => void) 列表滑动停止时触发。手拖动列表或列表的滚动条触发的滑动,手离开屏幕并且滑动停止时会触发该事件;使用 Scroller 滑动控制器触发的带动画的滑动,动画停止会触发该事件。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
onItemMove(event: (from: number, to: number) => boolean) 列表元素发生移动时触发。- from:移动前索引值。- to:移动后索引值。
onItemDragStart(event: (event: ItemDragInfo, itemIndex: number) => ((() => any) | void) 开始拖拽列表元素时触发。- event:见 ItemDragInfo 对象说明。- itemIndex:被拖拽列表元素索引值。
onItemDragEnter(event: (event: ItemDragInfo) => void) 拖拽进入列表元素范围内时触发。- event:见 ItemDragInfo 对象说明。
onItemDragMove(event: (event: ItemDragInfo, itemIndex: number, insertIndex: number) => void) 拖拽在列表元素范围内移动时触发。- event:见 ItemDragInfo 对象说明。- itemIndex:拖拽起始位置。- insertIndex:拖拽插入位置。
onItemDragLeave(event: (event: ItemDragInfo, itemIndex: number) => void) 拖拽离开列表元素时触发。- event:见 ItemDragInfo 对象说明。- itemIndex:拖拽离开的列表元素索引值。
onItemDrop(event: (event: ItemDragInfo, itemIndex: number, insertIndex: number, isSuccess: boolean) => void) 绑定该事件的列表元素可作为拖拽释放目标,当在列表元素内停止拖拽时触发。- event:见 ItemDragInfo 对象说明。- itemIndex:拖拽起始位置。- insertIndex:拖拽插入位置。- isSuccess:是否成功释放。说明:跨 List 拖拽时,当拖拽释放的位置绑定了 onItemDrop 时会返回 true,否则为 false。List 内部拖拽时,isSuccess 为 onItemMove 事件的返回值。

List 普通垂直列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Entry
@Component
struct Index {
private arr: number[] = [0, 1, 2, 3, 5, 6, 7, 8]

build() {
Column() {
List({ space: 10, initialIndex: 0 }) {
ForEach(this.arr, (item: number) => {
ListItem() {
Text(item.toString())
.width('100%')
.height(100)
.fontSize(24)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
}
}, (item: number) => item.toString())
}
.listDirection(Axis.Vertical) // 排列方向
.divider({ strokeWidth: 2, color: 0xFF0000, startMargin: 20, endMargin: 20 }) // 每行之间的分界线
.edgeEffect(EdgeEffect.None) // 滑动到边缘无效果
.width('100%')
.height('100%')
.padding(10)
}.width('100%').height('100%').backgroundColor(Color.Gray)
}
}

List 微信用户中心布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
interface UserListInterface {
title: string,
img: string
}

@Entry
@Component
struct Index {
@State list: UserListInterface[] = [
{
title: "服务",
img: "https://gitee.com/zch0304/images/raw/master/note/userlist/01.jpg"
},
{
title: "收藏",
img: "https://gitee.com/zch0304/images/raw/master/note/userlist/02.jpg"
},
{
title: "朋友圈",
img: "https://gitee.com/zch0304/images/raw/master/note/userlist/03.jpg"
},
{
title: "视频号",
img: "https://gitee.com/zch0304/images/raw/master/note/userlist/04.jpg"
},
{
title: "卡包",
img: "https://gitee.com/zch0304/images/raw/master/note/userlist/05.jpg"
},
{
title: "表情",
img: "https://gitee.com/zch0304/images/raw/master/note/userlist/06.jpg"
},
{
title: "设置",
img: "https://gitee.com/zch0304/images/raw/master/note/userlist/07.jpg"
}
]

build() {
Column() {
List({ space: 10, initialIndex: 0 }) {
ListItem() {
RelativeContainer() {
Image("https://gitee.com/zch0304/images/raw/master/note/test_icon4.jpg")
.id('avatar')
.width(80)
.height(80)
.objectFit(ImageFit.Fill)
.alignRules({
left: { anchor: '__container__', align: HorizontalAlign.Start },
top: { anchor: '__container__', align: VerticalAlign.Top }
})
.margin({ left: 20, top: 30 })

Text("明年今日")
.id('name')
.fontSize(18)
.alignRules({
left: { anchor: 'avatar', align: HorizontalAlign.End },
top: { anchor: 'avatar', align: VerticalAlign.Top }
})
.margin({ left: 10 })

Text("微信号:zhich")
.id('account')
.fontSize(14)
.alignRules({
left: { anchor: 'avatar', align: HorizontalAlign.End },
bottom: { anchor: 'avatar', align: VerticalAlign.Bottom }
})
.margin({ left: 10 })
}.width('100%').height('200vp').backgroundColor(Color.White)
}

ListItem() {
CustomItem({ model: this.list[0] })
}

ListItem() {
Column() {
ForEach(this.list, (item: UserListInterface, key) => {
if (key > 0 && key < this.list.length - 1) {
CustomItem({ model: item })
Divider()
.strokeWidth(1)
.color("#eee")
.padding({ left: 20, right: 20 })
}
}, (item: UserListInterface) => item.img)
}.backgroundColor(Color.White)
}

ListItem() {
CustomItem({ model: this.list[this.list.length-1] })
}

}
.edgeEffect(EdgeEffect.Spring)
.width('100%')
.height('100%')
}.width('100%').height('100%').backgroundColor(0xeeeeee)
}
}

@Component
struct CustomItem {
model: UserListInterface = { title: "", img: "" }

build() {
Row() {
Row() {
Image(this.model.img)
.width(28)
.height(28)
.objectFit(ImageFit.Cover)
Text(`${this.model.title}`).fontSize(16)
}

Image("https://gitee.com/zch0304/images/raw/master/note/userlist/arrow_forward.jpg")
.width(28)
.height(28)
}
.width('100%')
.backgroundColor(Color.White)
.padding(10)
.justifyContent(FlexAlign.SpaceBetween)
}
}

List 文章列表布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export class Article {
key: string;
title: string;
img: string;
author: string;
date: string;

constructor(key: string, title: string, img: string, author: string, date: string) {
this.key = key;
this.title = title;
this.img = img;
this.author = author;
this.date = date;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import { Article } from '../model/Article'

@Entry
@Component
struct Index {
@State articleList: Array<Article> = [
new Article(
'1',
'新手攻略|开启关怀模式,与家人更亲近~',
'https://gitee.com/zch0304/images/raw/master/note/article/01.png',
'国家电网',
'2024-12-2'
),
new Article(
'2',
'反诈课堂|光伏骗局套路多听我给您细细说!',
'https://gitee.com/zch0304/images/raw/master/note/article/02.png',
'国家电网',
'2024-4-2'
),
new Article(
'3',
'新手攻略| 联合办、网上办一次办,这些地方的用户注',
'https://gitee.com/zch0304/images/raw/master/note/article/03.png',
'国家电网',
'2024-1-12'
),
new Article(
'4',
'新手攻略|轻轻一点,电费一键查询',
'https://gitee.com/zch0304/images/raw/master/note/article/04.png',
'国家电网',
'2024-12-2'
),
new Article(
'5',
'关注|局地降温超16C!寒潮天气来袭,注意防寒保暖!',
'https://gitee.com/zch0304/images/raw/master/note/article/05.png',
'国家电网',
'2024-2-6'
),
]

build() {
Column() {
List() {
ForEach(this.articleList, (item: Article) => {
ListItem() {
Row() {
Column() {
Text(item.title).fontSize(16).fontWeight(FontWeight.Bold)
Text(`${item.author} ${item.date}`).fontSize(14)
}
.layoutWeight(1)
.height('100%')
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(HorizontalAlign.Start)

Image(item.img).width(100).height(68).margin({ left: 5 }).borderRadius(10)
}.height(80).alignItems(VerticalAlign.Center).padding({ bottom: 10, top: 10 })
}
}, (item: Article) => item.key)
}
.margin(10)
.padding(10)
.backgroundColor('#fff')
.divider({ strokeWidth: 1, color: '#eee' })
}.width('100%').height('100%').backgroundColor('#eee')
}
}

List 水平滑动列表

1
2
3
4
5
6
7
8
9
10
11
export class ListItemModel {
key: string;
title: string;
img: string

constructor(key: string, title: string, img: string) {
this.key = key;
this.title = title;
this.img = img;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import { ListItemModel } from '../model/ListItemModel'

@Entry
@Component
struct Index {
@State list: Array<ListItemModel> = [
new ListItemModel(
'1',
'关怀模式',
'https://gitee.com/zch0304/images/raw/master/note/article/01.png'
),
new ListItemModel(
'2',
'光伏骗局套',
'https://gitee.com/zch0304/images/raw/master/note/article/02.png'
),
new ListItemModel(
'3',
'网办小助手',
'https://gitee.com/zch0304/images/raw/master/note/article/03.png'
),
new ListItemModel(
'4',
'电费一键查询',
'https://gitee.com/zch0304/images/raw/master/note/article/04.png'
)
]

build() {
Column() {
List({ space: 10 }) {
ForEach(this.list, (item: ListItemModel) => {
ListItem() {
Column() {
Image(item.img).width(100).height(68).margin({ top: 10 }).borderRadius(10)
Text(item.title).fontSize(16).margin({ top: 10 })
}
.width(100)
.height(110)
.alignItems(HorizontalAlign.Center)
}
}, (item: ListItemModel) => item.key)
}
.width('100%')
.height(120)
.padding({ left: 10, right: 10 })
.listDirection(Axis.Horizontal)
.alignListItem(ListItemAlign.Center)
.border({ width: 1, color: '#ccc' })
}.width('100%').height('100%').backgroundColor('#eee')
}
}

list Scroller 控制滚动位置

List 组件初始化时,可以通过 scroller 参数绑定一个 Scroller 对象,进行列表的滚动控制。通过 Scroller 对象的 scrollToIndex 方法使列表滚动到指定的列表项索引位置。如实现列表返回顶部,可以设置 scrollToIndex 为 0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Entry
@Component
struct Index {
@State list: number[] = []
private listScroller: Scroller = new Scroller()

onPageShow(): void {
for (let i = 0; i < 10; i++) {
this.list.push(i)
}
}

build() {
Column() {
Stack({ alignContent: Alignment.BottomEnd }) {
List({ space: 20, scroller: this.listScroller }) {
ForEach(this.list, (item: number) => {
ListItem() {
Text(`${item}`)
.width('100%')
.height(160)
.fontSize(28)
.textAlign(TextAlign.Center)
.backgroundColor('#fff')
.borderRadius(20)
}
}, (item: number) => item.toString())
}.width('100%').height('100%')

Button() {
Image('https://gitee.com/zch0304/images/raw/master/note/article/arrow_top.png')
.width(30)
.height(30)
}
.width(80)
.height(80)
.margin({ bottom: 10 })
.backgroundColor('#ccc')
.onClick(() => {
this.listScroller.scrollToIndex(0)
})
}
}.width('100%').height('100%').backgroundColor('#eee').padding(12)
}
}

ListItem 的 swipeAction 响应列表项侧滑

侧滑菜单在许多应用中都很常见。例如,通讯类应用通常会给消息列表提供侧滑删除功能,即用户可以通过向左侧滑列表的某一项,再点击删除按钮删除消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
interface SwipeInterface {
username: string,
info: string,
date: string,
icon: string,
}

@Entry
@Component
struct Index {
@State list: SwipeInterface[] = [
{
username: "小明",
info: "吃饭了吗",
date: "10-23",
icon: "https://gitee.com/zch0304/images/raw/master/note/test_icon1.jpg"
},
{
username: "中明",
info: "你在哪里",
date: "10-22",
icon: "https://gitee.com/zch0304/images/raw/master/note/test_icon2.jpg"
},
{
username: "大明",
info: "现在出发",
date: "10-22",
icon: "https://gitee.com/zch0304/images/raw/master/note/test_icon3.jpg"
},
{
username: "明明",
info: "哈哈哈",
date: "10-21",
icon: "https://gitee.com/zch0304/images/raw/master/note/test_icon4.jpg"
},
]

build() {
Column() {
List({ space: 20 }) {
ForEach(this.list, (item: SwipeInterface, key) => {
ListItem() {
Row() {
Row() {
if (key == 1) {
Image(item.icon)
.width(60)
.height(60)
.margin({ right: 10 })
.borderRadius(100)
} else {
Badge({
count: 1,
position: BadgePosition.RightTop,
style: { badgeSize: 16, badgeColor: '#FA2A2D' }
}) {
Image(item.icon)
.width(60)
.height(60)
.margin({ right: 10 })
.borderRadius(100)
}
}
Column() {
Text(`${item.username}`).fontSize(16).fontWeight(FontWeight.Bold)
Text(`${item.info}`).fontSize(14).fontColor("#666")
}
.height('72%')
.alignItems(HorizontalAlign.Start)
.justifyContent(FlexAlign.SpaceBetween)
}

Text(`${item.date}`)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.height(60)
.margin({ bottom: 10, top: 10 })
.swipeAction({ end: this.itemEnd.bind(this, key) }) // 设置侧滑属性
})
}
.padding(10)
.divider({
strokeWidth: 1, color: '#eee'
})
}.width('100%').height('100%')
}

@Builder
itemEnd(index: number) {
// 侧滑后尾端出现的组件
Button({ type: ButtonType.Circle }) {
Image("https://gitee.com/zch0304/images/raw/master/note/delete.png")
.width(26)
.height(26)
}
.onClick(() => {
this.list.splice(index, 1);
})
.width(40)
.height(40)
.margin({ left: 20 })
.backgroundColor(Color.Red)
}
}

Badge 给列表项添加标记

添加标记是一种无干扰性且直观的方法,用于显示通知或将注意力集中到应用内的某个区域。例如,当消息列表接收到新消息时,通常对应的联系人头像的右上方会出现标记,提示有若干条未读消息。

1
2
3
4
5
6
7
8
9
Badge({
 count: 1,
 position: BadgePosition.RightTop,
 style: { badgeSize: 16, badgeColor: '#FA2A2D' }
}) {
 // Image组件实现消息联系人头像

 ...
}

ListItemGroup 汽车之家选车页面布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export class CarModel {
alphabet: string
carItem: Array<CarItemModel>

constructor(alphabet: string, carItem: Array<CarItemModel>) {
this.alphabet = alphabet
this.carItem = carItem
}
}

export class CarItemModel {
title: string

constructor(title: string) {
this.title = title
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { CarItemModel, CarModel } from '../model/CarModel'

const alphabets = ['#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']

@Entry
@Component
struct Index {
@State selectedIndex: number = 0
private listScroller: Scroller = new Scroller()
@State carList: CarModel[] = [
new CarModel(
"A",
[
new CarItemModel("奥迪"),
new CarItemModel("奥拓"),
new CarItemModel("爱驰"),
new CarItemModel("阿尔特")
]
),
new CarModel(
"B",
[
new CarItemModel("奔驰"),
new CarItemModel("比亚迪"),
new CarItemModel("宝马"),
new CarItemModel("保时捷"),
new CarItemModel("标致")
]
),
new CarModel(
"C",
[
new CarItemModel("长安"),
new CarItemModel("长城"),
new CarItemModel("宝马"),
new CarItemModel("曹操汽车"),
new CarItemModel("成功汽车")
]
),
new CarModel(
"D",
[
new CarItemModel("大众"),
new CarItemModel("东风"),
new CarItemModel("大运"),
new CarItemModel("东南"),
new CarItemModel("大帝")
]
),
new CarModel(
"F",
[
new CarItemModel("丰田"),
new CarItemModel("福特"),
new CarItemModel("法拉利")
]
)
]

build() {
Column() {
Stack({ alignContent: Alignment.End }) {
List({ scroller: this.listScroller }) {
ForEach(this.carList, (car: CarModel) => {
ListItemGroup({ header: this.itemHead(car.alphabet) }) {
// 循环渲染 ListItem
ForEach(car.carItem, (item: CarItemModel) => {
ListItem() {
Text(`${item.title}`).width('100%').height(60).fontSize(16)
}.padding({ left: 10 })
})
}
})
}
.sticky(StickyStyle.Header) // 设置吸顶,实现粘性标题效果
.onScrollIndex((firstIndex: number) => { // 获取滚动的索引值
this.selectedIndex = firstIndex
})

// 字母表索引组件
AlphabetIndexer({ arrayValue: alphabets, selected: 0 })
.selected(this.selectedIndex)
.onSelect((index: number) => {
this.listScroller.scrollToIndex(index - 1)
})
}
}.width('100%').height('100%')
}

@Builder
itemHead(text: string) {
// 列表分组的头部组件,对应联系人分组 A、B 等位置的组件
Text(text)
.fontSize(22)
.width('100%')
.padding(5)
.backgroundColor('#fff1f3f5')
}
}

Grid 网格布局

网格容器,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局 。

Grid 接口

参数:

参数名 参数类型 必填 参数描述
scroller Scroller 可滚动组件的控制器。用于与可滚动组件进行绑定。说明:不允许和其它滚动类组件绑定同一个滚动控制对象。

Grid 属性

名称 参数类型 描述
columnsTemplate string 设置当前网格布局列的数量,不设置时默认 1 列。例如, ‘1fr 1fr 2fr’ 是将父组件分 3 列,将父组件允许的宽分为 4 等份,第一列占 1 份,第二列占 1 份,第三列占 2 份。说明:设置为 ‘0fr’ 时,该列的列宽为 0,不显示 GridItem。设置为其它非法值时,GridItem 显示为固定 1 列。
rowsTemplate string 设置当前网格布局行的数量,不设置时默认 1 行。例如, ‘1fr 1fr 2fr’ 是将父组件分 3 行,将父组件允许的高分为 4 等份,第一行占 1 份,第二行占 1 份,第三行占 2 份。说明:设置为 ‘0fr’ 时,该行的行高为 0,这一行不显示 GridItem。设置为其它非法值时,GridItem 显示为固定 1 行。
columnsGap Length 设置列与列的间距。默认值:0。说明:设置为小于 0 的值时,按默认值显示。
rowsGap Length 设置行与行的间距。默认值:0。说明:设置为小于 0 的值时,按默认值显示。
scrollBar BarState 设置滚动条状态。默认值:BarState.Off。
scrollBarColor string | number | Color 设置滚动条的颜色。
scrollBarWidth string | number 设置滚动条的宽度。宽度设置后,滚动条正常状态和按压状态宽度均为滚动条的宽度值。默认值:4,单位:vp。
cachedCount number 设置预加载的 GridItem 的数量,只在 LazyForEach 中生效。具体使用可参考减少应用白块说明。默认值:1。说明:设置缓存后会在 Grid 显示区域上下各缓存 cachedCount * 列数个 GridItem。LazyForEach 超出显示和缓存范围的 GridItem 会被释放。设置为小于 0 的值时,按默认值显示。
editMode8+ boolean 设置 Grid 是否进入编辑模式,进入编辑模式可以拖拽 Grid 组件内部 GridItem。默认值:false。
layoutDirection8+ GridDirection 设置布局的主轴方向。默认值:GridDirection.Row。
maxCount8+ number 当 layoutDirection 是 Row/RowReverse 时,表示可显示的最大列数。当 layoutDirection 是 Column/ColumnReverse 时,表示可显示的最大行数。默认值:Infinity。说明:当 maxCount 小于 minCount 时,maxCount 和 minCount 都按默认值处理。设置为小于 0 的值时,按默认值显示。
minCount8+ number 当 layoutDirection 是 Row/RowReverse 时,表示可显示的最小列数。当 layoutDirection 是 Column/ColumnReverse 时,表示可显示的最小行数。默认值:1。说明:设置为小于 0 的值时,按默认值显示。
cellLength8+ number 当 layoutDirection 是 Row/RowReverse 时,表示一行的高度。当 layoutDirection 是 Column/ColumnReverse 时,表示一列的宽度。默认值:第一个元素的大小。
multiSelectable8+ boolean 是否开启鼠标框选。默认值:false。- false:关闭框选。- true:开启框选。
supportAnimation8+ boolean 是否支持动画。当前支持 GridItem 拖拽动画。默认值:false。

Grid 组件根据 rowsTemplate、columnsTemplate 属性的设置情况,可分为以下三种布局模式:

  • 行、列数量与占比同时设置:Grid 只展示固定行列数的元素,其余元素不展示,且 Grid 不可滚动。
  • 只设置行、列数量与占比中的一个:元素按照设置的方向进行排布,超出的元素可通过滚动的方式展示。
  • 行列数量与占比都不设置:元素在布局方向上排布,其行列数由布局方向、单个网格的宽高等多个属性共同决定。超出行列容纳范围的元素不展示,且 Grid 不可滚动。

固定数量的网格

行、列数量与占比同时设置:Grid 只展示固定行列数的元素,其余元素不展示,且 Grid 不可滚动。通过设置行列数量与尺寸占比可以确定网格布局的整体排列方式。Grid 组件提供了 rowsTemplate 和 columnsTemplate 属性用于设置网格布局行列数量与尺寸占比。rowsTemplate 和 columnsTemplate 属性值是一个由多个空格和 ‘数字+fr’ 间隔拼接的字符串,fr 的个数即网格布局的行或列数,fr 前面的数值大小,用于计算该行或列在网格布局宽度上的占比,最终决定该行或列的宽度。

如上图所示,构建的是一个三行三列的的网格布局,其在垂直方向上分为三等份,每行占一份;在水平方向上分为四等份,第一列占一份,第二列占两份,第三列占一份。 只要将 rowsTemplate 的值为 ‘1fr 1fr 1fr’,同时将 columnsTemplate 的值为 ‘1fr 2fr 1fr’,即可实现上述网格布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Entry
@Component
struct Index {
private arr: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15']

build() {
Column() {
Grid() {
ForEach(this.arr, (item: string) => {
GridItem() {
Text(item)
.width('100%')
.height('100%')
.fontSize(16)
.fontColor(Color.White)
.backgroundColor(Color.Blue)
.textAlign(TextAlign.Center)
}
}, (item: string) => item)
}
.height(300)
.columnsTemplate('1fr 2fr 1fr')
.rowsTemplate('1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
}.width('100%').padding(12).backgroundColor('#eee')
}
}

上下滚动的网格

只设置行、列数量与占比中的一个:元素按照设置的方向进行排布,超出的元素可通过滚动的方式展示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Entry
@Component
struct Index {
private arr: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15']

build() {
Column() {
Grid() {
ForEach(this.arr, (item: string) => {
GridItem() {
Text(item)
.width('100%')
.height(160)
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#1e90ff')
.textAlign(TextAlign.Center)
}
}, (item: string) => item)
}
.columnsTemplate('1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
}.width('100%').padding(12).backgroundColor('#eee')
}
}

不均匀网格布局

在单个网格单元中,rowStart 和 rowEnd 属性表示指定当前元素起始行号和终点行号,columnStart 和 columnEnd 属性表示指定当前元素的起始列号和终点列号。 区块 3 横跨第三列和第四列,只要将区块 3 对应 GridItem 的 columnStart 和 columnEnd 设为 3 和 4。区块 4 横跨第二行和第三行,只要将区块 3 对应 GridItem 的 rowStart 和 rowEnd 设为 1 和 2。整个区块的高度是由 Grid 的高度决定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
@Entry
@Component
struct Index {
private arr: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15']

build() {
Column() {
Grid() {
GridItem() {
Text('1')
.width('100%')
.height('100%')
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#1e90ff')
.textAlign(TextAlign.Center)
}

GridItem() {
Text('2')
.width('100%')
.height('100%')
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#1e90ff')
.textAlign(TextAlign.Center)
}

GridItem() {
Text('3')
.width('100%')
.height('100%')
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#1e90ff')
.textAlign(TextAlign.Center)
}.columnStart(3).columnEnd(4)

GridItem() {
Text('4')
.width('100%')
.height('100%')
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#1e90ff')
.textAlign(TextAlign.Center)
}.rowStart(2).rowEnd(3)

GridItem() {
Text('5')
.width('100%')
.height('100%')
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#1e90ff')
.textAlign(TextAlign.Center)
}

GridItem() {
Text('6')
.width('100%')
.height('100%')
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#1e90ff')
.textAlign(TextAlign.Center)
}

GridItem() {
Text('7')
.width('100%')
.height('100%')
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#1e90ff')
.textAlign(TextAlign.Center)
}

GridItem() {
Text('8')
.width('100%')
.height('100%')
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#1e90ff')
.textAlign(TextAlign.Center)
}.columnStart(1).columnEnd(4)
}
.height(300)
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
}.width('100%').padding(12).backgroundColor('#eee')
}
}

水平滚动的网格

可滚动的网格布局常用在文件管理、购物或视频列表等页面中如下图所示。在设置 Grid 的行列数量与占比时,如果仅设置行、列数量与占比中的一个,即仅设置 rowsTemplate 或仅设置 columnsTemplate 属性,网格单元按照设置的方向排列,超出 Grid 显示区域后,Grid 拥有可滚动能力。

如果设置的是 columnsTemplate,Grid 的滚动方向为垂直方向;如果设置的是 rowsTemplate,Grid 的滚动方向为水平方向。

如上图所示的横向可滚动网格布局,只要设置 rowsTemplate 属性的值且不设置 columnsTemplate 属性,当内容超出 Grid 组件宽度时,Grid 可横向滚动进行内容展示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Entry
@Component
struct Index {
@State arr: Array<string> = ['直播', '进口', '分类', '充值', '领券', '抽奖', '会员', '抽奖', '积分', '更多']
@State colors: Array<Color> = [
Color.Brown,
Color.Red,
Color.Orange,
Color.Blue,
Color.Grey,
Color.Pink,
Color.Red,
Color.Brown,
Color.Orange,
Color.Blue,
]

build() {
Column() {
Grid() {
ForEach(this.arr, (item: string, index) => {
GridItem() {
Row() {
Text(`${item}`)
.width(68)
.height(68)
.fontSize(16)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.backgroundColor(this.colors[index])
.borderRadius(68)
}.height('100%')
}.width('25%')
}, (item: string) => item)
}
.height(168)
.rowsTemplate('1fr 1fr')
}.width('100%').padding(12).backgroundColor('#eee')
}
}

组件导航和页面路由

组件导航

详细介绍请查看官方文档

Tabs

Tabs 组件的接口

参数:

参数名 参数类型 必填 参数描述
barPosition BarPosition 设置 Tabs 的页签位置。默认值:BarPosition.Start。
index number 设置当前显示页签的索引。默认值:0。说明:设置为小于 0 的值时按默认值显示。可选值为 [0, TabContent 子节点数量 - 1]。设置不同值时,默认生效切换动效,可以设置 animationDuration 为 0 关闭动画。
controller TabsController 设置 Tabs 控制器。
Tabs 组件属性
名称 参数类型 描述
vertical boolean 设置为 false 是为横向 Tabs,设置为 true 时为纵向 Tabs。默认值:false。
scrollable boolean 设置为 true 时可以通过滑动页面进行页面切换,为 false 时不可滑动切换页面。默认值:true。
barMode BarMode TabBar 布局模式,具体描述见 BarMode 枚举说明。默认值:BarMode.Fixed。Scrollable 每一个 TabBar 均使用实际布局宽度,超过总长度(横向 Tabs 的 barWidth,纵向 Tabs 的 barHeight)后可滑动。 Fixed 所有 TabBar 平均分配 barWidth 宽度(纵向时平均分配 barHeight 高度)。
barWidth number | Length8+ TabBar 的宽度值。默认值:未设置带样式的 TabBar 且 vertical 属性为 false 时,默认值为 Tabs 的宽度。未设置带样式的 TabBar 且 vertical 属性为 true 时,默认值为 56vp。 设置 SubTabbarStyle 样式且 vertical 属性为 false 时,默认值为 Tabs 的宽度。设置 SubTabbarStyle 样式且 vertical 属性为 true 时,默认值为 56vp。设置 BottomTabbarStyle 样式且 vertical 属性为 true 时,默认值为 96vp。设置 BottomTabbarStyle 样式且 vertical 属性为 false 时,默认值为 Tabs 的宽度。说明:设置为小于 0 或大于 Tabs 宽度值时,按默认值显示。
barHeight number | Length8+ TabBar 的高度值。默认值:未设置带样式的 TabBar 且 vertical 属性为 false 时,默认值为 56vp。未设置带样式的 TabBar 且 vertical 属性为 true 时,默认值为 Tabs 的高度。 设置 SubTabbarStyle 样式且 vertical 属性为 false 时,默认值为 56vp。设置 SubTabbarStyle 样式且 vertical 属性为 true 时,默认值为 Tabs 的高度。设置 BottomTabbarStyle 样式且 vertical 属性为 true 时,默认值为 Tabs 的高度。设置 BottomTabbarStyle 样式且 vertical 属性为 false 时,默认值为 56vp。说明:设置为小于 0 或大于 Tabs 高度值时,按默认值显示。
animationDuration number 点击 TabBar 页签切换 TabContent 的动画时长。不设置时,点击 TabBar 页签切换 TabContent 无动画。默认值:300。说明:该参数不支持百分比设置;设置为小于 0 时, 按默认值 300ms 显示。
基本布局

Tabs 组件的页面组成包含两个部分,分别是 TabContent 和 TabBar。TabContent 是内容页,TabBar 是导航页签栏,页面结构如下图所示,根据不同的导航类型,布局会有区别,可以分为底部导航、顶部导航、侧边导航,其导航栏分别位于底部、顶部和侧边。

提示:

TabContent 组件不支持设置通用宽度属性,其宽度默认撑满 Tabs 父组件。

TabContent 组件不支持设置通用高度属性,其高度由 Tabs 父组件高度与 TabBar 组件高度决定。

顶部导航

Tabs 组件默认的 barPosition 参数为 Start,即顶部导航模式。

1
2
Tabs({ barPosition: BarPosition.Start }) {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Entry
@Component
struct Index {
build() {
Tabs() {
TabContent() {
Text('首页的内容').fontSize(30)
}
.tabBar('首页')

TabContent() {
Text('推荐的内容').fontSize(30)
}
.tabBar('推荐')

TabContent() {
Text('发现的内容').fontSize(30)
}
.tabBar('发现')

TabContent() {
Text('我的内容').fontSize(30)
}
.tabBar('我的')
}.backgroundColor('#eee')
}
}
底部导航

底部导航是应用中最常见的一种导航方式。底部导航位于应用一级页面的底部,用户打开应用,能够分清整个应用的功能分类,以及页签对应的内容,并且其位于底部更加方便用户单手操作。底部导航一般作为应用的主导航形式存在,其作用是将用户关心的内容按照功能进行分类,迎合用户使用习惯,方便在不同模块间的内容切换。

设置 barPosition 为 End 即可将导航栏设置在底部。

1
2
Tabs({ barPosition: BarPosition.End }) {
}
侧边导航

侧边导航是手机应用较为少见的一种导航模式,更多适用于平板横屏界面,用于对应用进行导航操作,由于用户的视觉习惯是从左到右,侧边导航栏默认为左侧侧边栏。

实现侧边导航栏需要设置 Tabs 的属性 vertical 为 true。在底部导航和顶部导航实现中,其默认值为 false,表明内容页和导航栏垂直方向排列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Entry
@Component
struct Index {
build() {
Tabs() {
TabContent() {
Text('首页的内容').fontSize(30)
}
.tabBar('首页')

TabContent() {
Text('推荐的内容').fontSize(30)
}
.tabBar('推荐')

TabContent() {
Text('发现的内容').fontSize(30)
}
.tabBar('发现')

TabContent() {
Text('我的内容').fontSize(30)
}
.tabBar('我的')
}.vertical(true).barWidth(100).barHeight(200).backgroundColor('#eee')
}
}
限制导航栏的滑动切换

默认情况下,导航栏都支持滑动切换,在一些内容信息量需要进行多级分类的页面,如支持底部导航 + 顶部导航组合的情况下,底部导航栏的滑动效果与顶部导航出现冲突,此时需要限制底部导航的滑动,避免引起不好的用户体验。

控制滑动切换的属性为 scrollable,默认值为 true,表示可以滑动,若要限制滑动切换页签则需要设置为 false。

1
2
Tabs() {
}.scrollable(false)
可以滚动导航栏

滚动导航栏可以用于顶部导航栏或者侧边导航栏的设置,内容分类较多,屏幕宽度无法容纳所有分类页签的情况下,需要使用可滚动的导航栏,支持用户点击和滑动来加载隐藏的页签内容。

滚动导航栏需要设置 Tabs 组件的 barMode 属性,默认情况下其值为 Fixed,表示为固定导航栏,设置为 Scrollable 即可设置为可滚动导航栏。

1
2
Tabs() {
}.barMode(BarMode.Scrollable)
自定义导航栏

对于底部导航栏,一般作为应用主页面功能区分,为了更好的用户体验,会组合文字以及对应语义图标表示页签内容,这种情况下,需要自定义导航页签的样式。

系统默认情况下采用了下划线标志当前活跃的页签,而自定义导航栏需要自行实现相应的样式,用于区分当前活跃页签和未活跃页签。

设置自定义导航栏需要使用 tabBar 的参数,以其支持的 CustomBuilder 的方式传入自定义的函数组件样式。例如这里声明 TabBuilder 的自定义函数组件,传入参数包括页签文字 title,对应位置 index,以及选中状态和未选中状态的图片资源。通过当前活跃的 currentIndex 和页签对应的 targetIndex 匹配与否,决定 UI 显示的样式。

在 TabContent 对应 tabBar 属性中传入自定义函数组件,并传递相应的参数。

在不使用自定义导航栏时,系统默认的 Tabs 会实现切换逻辑。在使用了自定义导航栏后,切换页签的逻辑需要手动实现。即用户点击对应页签时,屏幕需要显示相应的内容页 。

切换指定页签需要使用 TabsController,TabsController 是 Tabs 组件的控制器,用于控制 Tabs 组件进行页签切换。通过 TabsController 的 changeIndex 方法来实现跳转至指定索引值对应的 TabContent 内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@Entry
@Component
struct Index {
@State currentIndex: number = 0
private imgUrlArr: Array<string> = [
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_home_sel.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_home_nor.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_recommend_sel.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_recommend_nor.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_discover_sel.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_discover_nor.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_mine_sel.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_mine_nor.png',
]
private controller = new TabsController()

build() {
Tabs({ barPosition: BarPosition.End, controller: this.controller }) {
TabContent() {
Text('首页的内容').fontSize(30)
}
.tabBar(this.TabBuilder('首页', 0, this.imgUrlArr[0], this.imgUrlArr[1]))

TabContent() {
Text('推荐的内容').fontSize(30)
}
.tabBar(this.TabBuilder('推荐', 1, this.imgUrlArr[2], this.imgUrlArr[3]))

TabContent() {
Text('发现的内容').fontSize(30)
}
.tabBar(this.TabBuilder('发现', 2, this.imgUrlArr[4], this.imgUrlArr[5]))

TabContent() {
Text('我的内容').fontSize(30)
}
.tabBar(this.TabBuilder('我的', 3, this.imgUrlArr[6], this.imgUrlArr[7]))
}
.backgroundColor('#eee')
.onChange((index: number) => {
this.changePage(index)
})
}

// 自定义导航页签的样式
@Builder
TabBuilder(title: string, targetIndex: number, selImg: string, norImg: string) {
Column() {
Image(this.currentIndex == targetIndex ? selImg : norImg)
.size({ width: 25, height: 25 })
Text(title)
.fontColor(this.currentIndex == targetIndex ? '#1296db' : '#707070')
.margin({ top: 5 })
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.changePage(targetIndex)
})
}

changePage(pageIndex: number) {
this.currentIndex = pageIndex
this.controller.changeIndex(pageIndex)
}
}
Tabs 页面模块化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import { HomePage } from './tabs/HomePage'
import { RecommendPage } from './tabs/RecommendPage'
import { DiscoveryPage } from './tabs/DiscoveryPage'
import { MinePage } from './tabs/MinePage'

@Entry
@Component
struct Index {
@State currentIndex: number = 0
private imgUrlArr: Array<string> = [
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_home_sel.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_home_nor.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_recommend_sel.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_recommend_nor.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_discover_sel.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_discover_nor.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_mine_sel.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_mine_nor.png',
]
private controller = new TabsController()

build() {
Tabs({ barPosition: BarPosition.End, controller: this.controller }) {
TabContent() {
HomePage()
}
.tabBar(this.TabBuilder('首页', 0, this.imgUrlArr[0], this.imgUrlArr[1]))

TabContent() {
RecommendPage()
}
.tabBar(this.TabBuilder('推荐', 1, this.imgUrlArr[2], this.imgUrlArr[3]))

TabContent() {
DiscoveryPage()
}
.tabBar(this.TabBuilder('发现', 2, this.imgUrlArr[4], this.imgUrlArr[5]))

TabContent() {
MinePage()
}
.tabBar(this.TabBuilder('我的', 3, this.imgUrlArr[6], this.imgUrlArr[7]))
}
.backgroundColor('#eee')
.onChange((index: number) => {
this.changePage(index)
})
}

// 自定义导航页签的样式
@Builder
TabBuilder(title: string, targetIndex: number, selImg: string, norImg: string) {
Column() {
Image(this.currentIndex == targetIndex ? selImg : norImg)
.size({ width: 25, height: 25 })
Text(title)
.fontColor(this.currentIndex == targetIndex ? '#1296db' : '#707070')
.margin({ top: 5 })
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.changePage(targetIndex)
})
}

changePage(pageIndex: number) {
this.currentIndex = pageIndex
this.controller.changeIndex(pageIndex)
}
}
1
2
3
4
5
6
7
8
9
10
import { AppBar } from '../widget/AppBar'

@Component
export struct HomePage {
build() {
Column() {
AppBar({ title: '首页' })
}.width('100%').height('100%')
}
}
1
2
3
4
5
6
7
8
9
10
import { AppBar } from '../widget/AppBar'

@Component
export struct RecommendPage {
build() {
Column() {
AppBar({ title: '推荐' })
}.width('100%').height('100%')
}
}
1
2
3
4
5
6
7
8
9
10
import { AppBar } from '../widget/AppBar'

@Component
export struct DiscoveryPage {
build() {
Column() {
AppBar({ title: '发现' })
}.width('100%').height('100%')
}
}
1
2
3
4
5
6
7
8
9
10
import { AppBar } from '../widget/AppBar'

@Component
export struct MinePage {
build() {
Column() {
AppBar({ title: '我的' })
}.width('100%').height('100%')
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
export struct AppBar {
title: string = ""

build() {
Text(this.title)
.size({ width: '100%', height: 50 })
.backgroundColor('#d81e06')
.fontColor('#ffffff')
.textAlign(TextAlign.Center)
.fontSize(18)
}
}

页面路由(router)

页面路由指在应用程序中实现不同页面之间的跳转和数据传递。HarmonyOS 提供了 Router 模块,通过不同的 url 地址,可以方便地进行页面路由,轻松地访问不同的页面 。

Router 适用于模块间与模块内页面切换,通过每个页面的 url 实现模块间解耦。模块内页面跳转时,为了实现更好的转场动效场景不建议使用该模块,推荐使用 Navigation

路由跳转的几种方法

Router 模块提供了两种跳转模式,分别是 router.pushUrl() 和 router.replaceUrl()。这两种模式决定了目标页是否会替换当前页。

  • pushUrl:目标页不会替换当前页,而是压入页面栈。这样可以保留当前页的状态,并且可以通过返回键或者调用 router.back() 方法返回到当前页。
  • replaceUrl:目标页会替换当前页,并销毁当前页。这样可以释放当前页的资源,并且无法返回到当前页。

页面栈的最大容量为 32 个页面。如果超过这个限制,可以调用 router.clear() 方法清空历史页面栈,释放内存空间。

同时,Router 模块提供了两种实例模式,分别是 Standard 和 Single。这两种模式决定了目标 url 是否会对应多个实例。

  • Standard:标准实例模式,也是默认情况下的实例模式。每次调用该方法都会新建一个目标页,并压入栈顶。
  • Single:单实例模式。即如果目标页的 url 在页面栈中已经存在同 url 页面,则离栈顶最近的同 url 页面会被移动到栈顶,并重新加载;如果目标页的 url 在页面栈中不存在同 url 页面,则按照标准模式跳转。
pushUrl + Standard模式

希望从主页点击一个按钮,跳转到另一个页面。同时,需要保留主页在页面栈中,以便返回时恢复状态。这种场景下,可以使用 pushUrl() 方法,并且使用 Standard 实例模式(或者省略)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import router from '@ohos.router'

@Entry
@Component
struct Index {
build() {
Column() {
Button("新闻页").onClick(() => {
this.onJumpClick()
})
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
}

onJumpClick() {
router.pushUrl({ url: 'pages/News' }, router.RouterMode.Standard, (err) => {
if (err) {
console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`)
return
}
console.info('Invoke pushUrl succeeded.');
})
}
}
replaceUrl+ Standard模式

有一个登录页(Login)和一个个人中心页(Profile),希望从登录页成功登录后,跳转到个人中心页。同时,销毁登录页,在返回时直接退出应用。这种场景下,可以使用 replaceUrl() 方法,并且使用 Standard 实例模式(或者省略)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import router from '@ohos.router'

@Entry
@Component
struct Login {
build() {
Column() {
Button("个人中心页").onClick(() => {
this.onJumpClick()
})
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
}

onJumpClick() {
router.replaceUrl({ url: 'pages/Profile' }, router.RouterMode.Standard, (err) => {
if (err) {
console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`)
return
}
console.info('Invoke pushUrl succeeded.');
})
}
}
pushUrl + Single模式

有一个设置页(Setting)和一个主题切换页(Theme),希望从设置页点击主题选项,跳转到主题切换页。同时,需要保证每次只有一个主题切换页存在于页面栈中,在返回时直接回到设置页。这种场景下,可以使用 pushUrl() 方法,并且使用 Single 实例模式。

replaceUrl + Single模式

有一个搜索结果列表页(SearchResult)和一个搜索结果详情页(SearchDetail),希望从搜索结果列表页点击某一项结果,跳转到搜索结果详情页。同时,如果该结果已经被查看过,则不需要再新建一个详情页,而是直接跳转到已经存在的详情页。这种场景下,可以使用 replaceUrl() 方法,并且使用 Single 实例模式。

路由跳转传值

如果需要在跳转时传递一些数据给目标页,则可以在调用 Router 模块的方法时,添加一个 params 属性,并指定一个对象作为参数。例如:

1
2
3
4
5
6
7
8
export class DataModel {
id: number = 0
info: DataModelInfo | null = null
}

export class DataModelInfo {
name: string | null = null
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import router from '@ohos.router'
import { DataModel } from '../model/DataModel'

@Entry
@Component
struct Index {
build() {
Column() {
Button("新闻页").onClick(() => {
this.onJumpClick()
})
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
}

onJumpClick() {
let param: DataModel = {
id: 123,
info: {
name: '张三'
}
};
router.pushUrl({ url: 'pages/News', params: param }, (err) => {
if (err) {
console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`)
return
}
console.info('Invoke pushUrl succeeded.');
})
}
}

在目标页面中,可以通过调用 Router 模块的 getParams() 方法来获取传递过来的参数。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import router from '@ohos.router'
import { DataModel } from '../model/DataModel'

@Entry
@Component
struct News {
@State message: string = '新闻页面'

onPageShow(): void {
const dataModel = router.getParams() as DataModel
const id = dataModel.id
const name = dataModel.info?.name
}

build() {
}
}

页面返回

当用户在一个页面完成操作后,通常需要返回到上一个页面或者指定页面,这就需要用到页面返回功能。在返回的过程中,可能需要将数据传递给目标页面,这就需要用到数据传递功能。

可以使用以下几种方式返回页面:

  • 方式一:返回到上一个页面。

    1
    2
    import router from '@ohos.router'
    router.back();

    这种方式会返回到上一个页面,即上一个页面在页面栈中的位置。但是,上一个页面必须存在于页面栈中才能够返回,否则该方法将无效。

  • 方式二:返回到指定页面。

    返回普通页面:

    1
    2
    3
    4
    import router from '@ohos.router'
    router.back({
    url: 'pages/Home'
    });

    返回命名路由页面:

    1
    2
    3
    4
    import router from '@ohos.router'
    router.back({
    url: 'myPage' // myPage 为返回的命名路由页面别名
    });

    这种方式可以返回到指定页面,需要指定目标页面的路径。目标页面必须存在于页面栈中才能够返回。

  • 方式三:返回到指定页面,并传递自定义参数信息。

    返回到普通页面:

    1
    2
    3
    4
    5
    6
    7
    import router from '@ohos.router'
    router.back({
    url: 'pages/Home',
    params: {
    info: '来自Home页'
    }
    });

    返回命名路由页面:

    1
    2
    3
    4
    5
    6
    7
    import router from '@ohos.router'
    router.back({
    url: 'myPage', // myPage 为返回的命名路由页面别名
    params: {
    info: '来自Home页'
    }
    });

    这种方式不仅可以返回到指定页面,还可以在返回的同时传递自定义参数信息。这些参数信息可以在目标页面中通过调用 router.getParams() 方法进行获取和解析。

在目标页面中,在需要获取参数的位置调用 router.getParams() 方法即可,例如在 onPageShow() 生命周期回调中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import router from '@ohos.router'

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

onPageShow() {
const params = router.getParams() as Record<string, string>; // 获取传递过来的参数对象
if (params) {
const info: string = params.info as string; // 获取 info 属性的值
}
}
...
}

当使用 router.back() 方法返回到指定页面时,原栈顶页面(包括)到指定页面(不包括)之间的所有页面栈都将从栈中弹出并销毁。另外,如果使用 router.back() 方法返回到原来的页面,原页面不会被重复创建,因此使用 @State 声明的变量不会重复声明,也不会触发页面的 aboutToAppear() 生命周期回调。如果需要在原页面中使用返回页面传递的自定义参数,可以在需要的位置进行参数解析。例如,在 onPageShow() 生命周期回调中进行参数解析。

页面返回前增加一个询问框

在开发应用时,为了避免用户误操作或者丢失数据,有时候需要在用户从一个页面返回到另一个页面之前,弹出一个询问框,让用户确认是否要执行这个操作。

系统默认询问框

如果想要在目标界面开启页面返回询问框,需要在调用 router.back() 方法之前,通过调用 router.showAlertBeforeBackPage() 方法设置返回询问框的信息。例如,在支付页面中定义一个返回按钮的点击事件处理函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import router from '@ohos.router'
import { BusinessError } from '@ohos.base'

// 定义一个返回按钮的点击事件处理函数
function onBackClick(): void {
// 调用 router.showAlertBeforeBackPage() 方法,设置返回询问框的信息
try {
router.showAlertBeforeBackPage({
message: '您还没有完成支付,确定要返回吗?' // 设置询问框的内容
});
} catch (err) {
let message = (err as BusinessError).message
let code = (err as BusinessError).code
console.error(`Invoke showAlertBeforeBackPage failed, code is ${code}, message is ${message}`)
}

// 调用 router.back() 方法,返回上一个页面
router.back()
}

其中,router.showAlertBeforeBackPage() 方法接收一个对象作为参数,该对象包含以下属性:

  • message:string 类型,表示询问框的内容。

如果调用成功,则会在目标界面开启页面返回询问框;如果调用失败,则会抛出异常,并通过 err.code 和 err.message 获取错误码和错误信息。

当用户点击“返回”按钮时,会弹出确认对话框,询问用户是否确认返回。选择“取消”将停留在当前页目标页面;选择“确认”将触发 router.back() 方法,并根据参数决定如何执行跳转。

自定义询问框

自定义询问框的方式,可以使用弹窗或者自定义弹窗实现。这样可以让应用界面与系统默认询问框有所区别,提高应用的用户体验度。本文以弹窗为例,介绍如何实现自定义询问框。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import router from '@ohos.router'
import promptAction from '@ohos.promptAction'
import { BusinessError } from '@ohos.base'

function onBackClick() {
// 弹出自定义的询问框
promptAction.showDialog({
message: '您还没有完成支付,确定要返回吗?',
buttons: [
{
text: '取消',
color: '#FF0000'
},
{
text: '确认',
color: '#0099FF'
}
]
}).then((result:promptAction.ShowDialogSuccessResponse) => {
if (result.index === 0) {
// 用户点击了“取消”按钮
console.info('User canceled the operation.')
} else if (result.index === 1) {
// 用户点击了“确认”按钮
console.info('User confirmed the operation.')
// 调用 router.back() 方法,返回上一个页面
router.back();
}
}).catch((err:Error) => {
let message = (err as BusinessError).message
let code = (err as BusinessError).code
console.error(`Invoke showDialog failed, code is ${code}, message is ${message}`)
})
}

当用户点击“返回”按钮时,会弹出自定义的询问框,询问用户是否确认返回。选择“取消”将停留在当前页目标页面;选择“确认”将触发 router.back() 方法,并根据参数决定如何执行跳转。

命名路由

在开发中为了跳转到共享包 Har 或者 Hsp 中的页面(即共享包中路由跳转),可以使用 router.pushNamedRoute() 来实现。

在想要跳转到的共享包 Har 或者 Hsp 页面里,给 @Entry 修饰的自定义组件 命名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// library/src/main/ets/pages/Index.ets
// library 为新建共享包自定义的名字
@Entry({ routeName: 'myPage' })
@Component
export struct MyComponent {
build() {
Row() {
Column() {
Text('Library Page')
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}

配置成功后需要在跳转的页面中引入命名路由的页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import router from '@ohos.router'
import { BusinessError } from '@ohos.base'
import('@ohos/library/src/main/ets/pages/Index') // 引入共享包中的命名路由页面
@Entry
@Component
struct Index {
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text('Hello World')
.fontSize(50)
.fontWeight(FontWeight.Bold)
.margin({ top: 20 })
.backgroundColor('#ccc')
.onClick(() => { // 点击跳转到其他共享包中的页面
try {
router.pushNamedRoute({
name: 'myPage',
params: {
data1: 'message',
data2: {
data3: [123, 456, 789]
}
}
})
} catch (err) {
let message = (err as BusinessError).message
let code = (err as BusinessError).code
console.error(`pushNamedRoute failed, code is ${code}, message is ${message}`);
}
})
}
.width('100%')
.height('100%')
}
}

使用命名路由方式跳转时,需要在当前应用包的 oh-package.json5 文件中配置依赖。例如:

1
2
3
4
"dependencies": {
"@ohos/library": "file:../library",
...
}

状态管理

管理组件拥有的状态

@State、@Prop、@Link、@Provide、@Consume、@Observed、@ObjectLink 和 @Watch 用于管理页面级变量的状态。

装饰器 装饰内容 说明
@State 基本数据类型,类,数组 修饰的状态数据被修改时会触发组件的 build 方法进行UI界面更新。
@Prop 基本数据类型 修改后的状态数据用于在父组件和子组件之间建立单向数据依赖关系。修改父组件关联数据时,更新当前组件的 UI。
@Link 基本数据类型,类,数组 父子组件之间的双向数据绑定,父组件的内部状态数据作为数据源,任何一方所做的修改都会反映给另一方。
@Provide 基本数据类型,类,数组 @Provide 作为数据的提供方,可以更新其子孙节点的数据,并触发页面渲染。
@Consume 基本数据类型,类,数组 @Consume 装饰的变量在感知到 @Provide 装饰的变量更新后,会触发当前自定义组件的重新渲染。
@Observed @Observed 应用于类,表示该类中的数据变更被 UI 页面管理。(用不到)
@ObjectLink 被 @Observed 所装饰类的对象 装饰的状态数据被修改时,在父组件或者其它兄弟组件内与它关联的状态数据所在的组件都会更新 UI。(用不到)
@Watch 基本数据类型,类,数组 @Watch 应用于对状态变量的监听。如果开发者需要关注某个状态变量的值是否改变,可以使用 @Watch 为状态变量设置回调函数。

@State

修饰的状态数据被修改时会触发组件的 build 方法进行UI界面更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Entry
@Component
struct Index {
@State count: number = 0

build() {
Row() {
Text(`${this.count}`).fontSize(30)
Box({ count: this.count, color: Color.Pink }).margin({ left: 10 })
Box({ count: this.count, color: Color.Brown }).margin({ left: 10 })
Button() {
Text("+").fontWeight(FontWeight.Bold).fontSize(30).fontColor(Color.White)
}
.width(60)
.height(60)
.margin({ left: 50 })
.onClick(() => {
this.count++
})
}
.width('100%')
.height('100%')
.backgroundColor('#eee')
.justifyContent(FlexAlign.Center)
}
}

@Component
struct Box {
count: number = 0
color: Color = Color.Black

build() {
Column() {
Text(`${this.count}`).fontSize(30).fontColor(Color.White)
}
.width(60)
.height(60)
.backgroundColor(this.color)
.justifyContent(FlexAlign.Center)
}
}

Index 组件的 count 改变了,不会改变 Box 组件的 count。

@Prop

修改后的状态数据用于在父组件和子组件之间建立单向数据依赖关系。修改父组件关联数据时,更新当前组件的 UI。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Entry
@Component
struct Index {
@State count: number = 0

build() {
Row() {
Text(`${this.count}`).fontSize(30)
Box({ count: this.count, color: Color.Pink }).margin({ left: 10 })
Box({ count: this.count, color: Color.Brown }).margin({ left: 10 })
Button() {
Text("+").fontWeight(FontWeight.Bold).fontSize(30).fontColor(Color.White)
}
.width(60)
.height(60)
.margin({ left: 50 })
.onClick(() => {
this.count++
})
}
.width('100%')
.height('100%')
.backgroundColor('#eee')
.justifyContent(FlexAlign.Center)
}
}

@Component
struct Box {
@Prop count: number
color: Color = Color.Black

build() {
Column() {
Text(`${this.count}`).fontSize(30).fontColor(Color.White)
}
.width(60)
.height(60)
.backgroundColor(this.color)
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.count++ // 只有当前组件有效,无法改变父组件的 count
})
}
}

Index 组件的 count 改变了,会改变到 Box 组件的 count。但是 Box 组件的 count 改变了,不会改变到 Index 组件的 count。

注意:传参使用 $。

父子组件之间的双向数据绑定,父组件的内部状态数据作为数据源,任何一方所做的修改都会反映给另一方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Entry
@Component
struct Index {
@State count: number = 0

build() {
Row() {
Text(`${this.count}`).fontSize(30)
Box({ count: $count, color: Color.Pink }).margin({ left: 10 })
Box({ count: $count, color: Color.Brown }).margin({ left: 10 })
Button() {
Text("+").fontWeight(FontWeight.Bold).fontSize(30).fontColor(Color.White)
}
.width(60)
.height(60)
.margin({ left: 50 })
.onClick(() => {
this.count++
})
}
.width('100%')
.height('100%')
.backgroundColor('#eee')
.justifyContent(FlexAlign.Center)
}
}

@Component
struct Box {
@Link count: number
color: Color = Color.Black

build() {
Column() {
Text(`${this.count}`).fontSize(30).fontColor(Color.White)
}
.width(60)
.height(60)
.backgroundColor(this.color)
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.count++ // 当前组件有效,也改变父组件的 count
})
}
}

Index 组件和 Box 组件的 count 相互绑定,改变一方的 count 也会改变到另一方的 count。

@Provide @Consume

@Provide 和 @Consume,应用于与后代组件的双向数据同步,应用于状态数据在多个层级之间传递的场景。不同于上文提到的父子组件之间通过命名参数机制传递,@Provide 和 @Consume 摆脱参数传递机制的束缚,实现跨层级传递。

Index 组件调用了 SecondBox 组件,SecondBox 组件调用了 ThirdBox 组件,我们要实现的功能是 Index 组件和 ThirdBox 组件直接的数据相互绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Entry
@Component
struct Index {
@Provide('count') count: number = 0

build() {
Row() {
Text(`${this.count}`).fontSize(30)
SecondBox({ color: Color.Pink }).margin({ left: 10 })
SecondBox({ color: Color.Brown }).margin({ left: 10 })
Button() {
Text("+").fontWeight(FontWeight.Bold).fontSize(30).fontColor(Color.White)
}
.width(60)
.height(60)
.margin({ left: 50 })
.onClick(() => {
this.count++
})
}
.width('100%')
.height('100%')
.backgroundColor('#eee')
.justifyContent(FlexAlign.Center)
}
}

@Component
struct SecondBox {
color: Color = Color.Black

build() {
Column() {
ThirdBox()
}
.width(60)
.height(60)
.backgroundColor(this.color)
.justifyContent(FlexAlign.Center)
}
}

@Component
struct ThirdBox {
@Consume('count') count: number

build() {
Text(`${this.count}`).fontSize(30).fontColor(Color.White)
.onClick(() => {
this.count++
})
}
}

Index 组件到 ThirdBox 组件之间的 count 相互绑定。

@Watch

@Watch 用于监听状态变量的变化,当状态变量变化时,@Watch 的回调方法将被调用。@Watch 在 ArkUI 框架内部判断数值有无更新使用的是严格相等(===),遵循严格相等规范。当在严格相等为 false 的情况下,就会触发 @Watch 的回调。

@Watch 可用于购物车计算总价,或者实现计算器功能等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@Entry
@Component
struct Index {
@State @Watch('change') count: number = 1
@State @Watch('change') pow: number = 2
@State res: number = 1

change() {
this.res = Math.pow(this.count, this.pow)
}

build() {
Column() {
Row() {
Text('基数:').fontSize(20)
TextInput({ text: this.count.toString() })
.type(InputType.Number)
.layoutWeight(1)
.height(40)
.borderRadius(10)
.onChange((value) => {
if (value == null || value == '' || value == undefined) {
value = '0'
}
this.count = parseInt(value)
})
.margin({ bottom: 20 })
}

Row() {
Text('次幂:').fontSize(20)
TextInput({ text: this.pow.toString() })
.type(InputType.Number)
.layoutWeight(1)
.height(40)
.borderRadius(10)
.onChange((value) => {
if (value == null || value == '' || value == undefined) {
value = '0'
}
this.pow = parseInt(value)
})
.margin({ bottom: 20 })
}

Row() {
Text('结果:' + this.res).fontSize(30)
}
}
.width('100%')
.height('100%')
.padding(20)
.backgroundColor('#eee')
}
}

管理应用拥有的状态

概述

上一节介绍的装饰器仅能在页面内,即一个组件树上共享状态变量。如果要实现应用级的,或者多个页面的状态数据共享,就需要用到应用级别的状态管理的概念。ArkTS 根据不同特性,提供了多种应用状态管理的能力:

  • LocalStorage:页面级UI状态存储,通常用于 UIAbility 内、页面间的状态共享。
  • AppStorage:特殊的单例 LocalStorage 对象,由 UI 框架在应用程序启动时创建,为应用程序 UI 状态属性提供中央存储。
  • PersistentStorage:持久化存储 UI 状态,通常和 AppStorage 配合使用,选择 AppStorage 存储的数据写入磁盘,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同。
  • Environment:应用程序运行的设备的环境参数,环境参数会同步到 AppStorage 中,可以和 AppStorage 搭配使用。

LocalStorage:页面级 UI 状态存储

LocalStorage 是页面级的 UI 状态存储,通过 @Entry 装饰器接收的参数可以在页面内共享同一个 LocalStorage 实例。LocalStorage 支持 UIAbility 实例内多个页面间状态共享。

概述

LocalStorage 是 ArkTS 为构建页面级别状态变量提供存储的内存内“数据库”。

  • 应用程序可以创建多个 LocalStorage 实例,LocalStorage 实例可以在页面内共享,也可以通过 GetShared 接口,实现跨页面、UIAbility 实例内共享。
  • 组件树的根节点,即被 @Entry 装饰的 @Component,可以被分配一个 LocalStorage 实例,此组件的所有子组件实例将自动获得对该 LocalStorage 实例的访问权限。
  • 被 @Component 装饰的组件最多可以访问一个 LocalStorage 实例和 AppStorage,未被 @Entry 装饰的组件不可被独立分配 LocalStorage 实例,只能接受父组件通过 @Entry 传递来的 LocalStorage 实例。一个LocalStorage 实例在组件树上可以被分配给多个组件。
  • LocalStorage 中的所有属性都是可变的。

应用程序决定 LocalStorage 对象的生命周期。当应用释放最后一个指向 LocalStorage 的引用时,比如销毁最后一个自定义组件,LocalStorage 将被 JS Engine 垃圾回收。

LocalStorage 根据与 @Component 装饰的组件的同步类型不同,提供了两个装饰器:

  • @LocalStorageProp:@LocalStorageProp 装饰的变量与 LocalStorage 中给定属性建立单向同步关系。
  • @LocalStorageLink:@LocalStorageLink 装饰的变量与 LocalStorage 中给定属性建立双向同步关系。
限制条件
  • LocalStorage 创建后,命名属性的类型不可更改。后续调用 Set 时必须使用相同类型的值。
  • LocalStorage 是页面级存储,getShared 接口仅能获取当前 Stage 通过 windowStage.loadContent 传入的 LocalStorage 实例,否则返回 undefined。例子可见 将 LocalStorage 实例从 UIAbility 共享到一个或多个视图
@LocalStorageProp

要建立 LocalStorage 和自定义组件的联系,需要使用 @LocalStorageProp 和 @LocalStorageLink 装饰器。使用 @LocalStorageProp(key)/@LocalStorageLink(key) 装饰组件内的变量,key 标识了 LocalStorage 的属性。

当自定义组件初始化的时候,@LocalStorageProp(key)/@LocalStorageLink(key) 装饰的变量会通过给定的 key,绑定 LocalStorage 对应的属性,完成初始化。本地初始化是必要的,因为无法保证 LocalStorage 一定存在给定的 key。

@LocalStorageProp(key) 是和 LocalStorage 中 key 对应的属性建立单向数据同步,修改 @LocalStorageProp(key) 在本地的值,不会同步回 LocalStorage 中。相反,如果 LocalStorage 中 key 对应的属性值发生改变,例如通过 set 接口对 LocalStorage 中的值进行修改,改变会同步给 @LocalStorageProp(key),并覆盖掉本地的值。

装饰器使用规则说明:

@LocalStorageProp变量装饰器 说明
装饰器参数 key:常量字符串,必填(字符串需要有引号)。
允许装饰的变量类型 Object、class、string、number、boolean、enum类型,以及这些类型的数组。API12及以上支持Map、Set、Date类型。嵌套类型的场景请参考观察变化和行为表现。类型必须被指定,建议和LocalStorage中对应属性类型相同,否则会发生类型隐式转换,从而导致应用行为异常。不支持any,API12及以上支持undefined和null类型。API12及以上支持上述支持类型的联合类型,比如string | number, string | undefined 或者 ClassA | null,示例见LocalStorage支持联合类型注意当使用undefined和null的时候,建议显式指定类型,遵循TypeScript类型校验,比如:@LocalStorageProp(“AA”) a: number | null = null是推荐的,不推荐@LocalStorageProp(“AA”) a: number = null。
同步类型 单向同步:从LocalStorage的对应属性到组件的状态变量。组件本地的修改是允许的,但是LocalStorage中给定的属性一旦发生变化,将覆盖本地的修改。
被装饰变量的初始值 必须指定,如果LocalStorage实例中不存在属性,则用该初始值初始化该属性,并存入LocalStorage中。

变量的传递/访问规则说明:

传递/访问 说明
从父节点初始化和更新 禁止,@LocalStorageProp不支持从父节点初始化,只能从LocalStorage中key对应的属性初始化,如果没有对应key的话,将使用本地默认值初始化。
初始化子节点 支持,可用于初始化@State、@Link、@Prop、@Provide。
是否支持组件外访问 否。

观察变化和行为表现:

观察变化

  • 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
  • 当装饰的数据类型为class或者Object时,可以观察到对象整体赋值和对象属性变化(详见从ui内部使用localstorage)。
  • 当装饰的对象是array时,可以观察到数组添加、删除、更新数组单元的变化。
  • 当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。详见装饰Date类型变量
  • 当装饰的变量是Map时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。详见装饰Map类型变量
  • 当装饰的变量是Set时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。详见装饰Set类型变量

框架行为

  • 被@LocalStorageProp装饰的变量的值的变化不会同步回LocalStorage里。
  • @LocalStorageProp装饰的变量变化会使当前自定义组件中关联的组件刷新。
  • LocalStorage(key)中值的变化会引发所有被@LocalStorageProp对应key装饰的变量的变化,会覆盖@LocalStorageProp本地的改变。

如果需要将自定义组件的状态变量的更新同步回 LocalStorage,就需要用到 @LocalStorageLink。

@LocalStorageLink(key) 是和 LocalStorage 中 key 对应的属性建立双向数据同步

  • 本地修改发生,该修改会被写回 LocalStorage 中。

  • LocalStorage 中的修改发生后,该修改会被同步到所有绑定 LocalStorage 对应 key 的属性上,包括单向(@LocalStorageProp 和通过 prop 创建的单向绑定变量)、双向(@LocalStorageLink 和通过 link 创建的双向绑定变量)变量。

装饰器使用规则说明:

@LocalStorageLink变量装饰器 说明
装饰器参数 key:常量字符串,必填(字符串需要有引号)。
允许装饰的变量类型 Object、class、string、number、boolean、enum类型,以及这些类型的数组。API12及以上支持Map、Set、Date类型。嵌套类型的场景请参考观察变化和行为表现。类型必须被指定,建议和LocalStorage中对应属性类型相同,否则会发生类型隐式转换,从而导致应用行为异常。不支持any,API12及以上支持undefined和null类型。API12及以上支持上述支持类型的联合类型,比如string | number, string | undefined 或者 ClassA | null,示例见LocalStorage支持联合类型注意当使用undefined和null的时候,建议显式指定类型,遵循TypeScript类型校验,比如:@LocalStorageLink(“AA”) a: number | null = null是推荐的,不推荐@LocalStorageLink(“AA”) a: number = null。
同步类型 双向同步:从LocalStorage的对应属性到自定义组件,从自定义组件到LocalStorage对应属性。
被装饰变量的初始值 必须指定,如果LocalStorage实例中不存在属性,则用该初始值初始化该属性,并存入LocalStorage中。

变量的传递/访问规则说明:

传递/访问 说明
从父节点初始化和更新 禁止,@LocalStorageLink不支持从父节点初始化,只能从LocalStorage中key对应的属性初始化,如果没有对应key的话,将使用本地默认值初始化。
初始化子节点 支持,可用于初始化@State、@Link、@Prop、@Provide。
是否支持组件外访问 否。

观察变化和行为表现:

观察变化

  • 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
  • 当装饰的数据类型为class或者Object时,可以观察到对象整体赋值和对象属性变化(详见从ui内部使用localstorage)。
  • 当装饰的对象是array时,可以观察到数组添加、删除、更新数组单元的变化。
  • 当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。详见装饰Date类型变量
  • 当装饰的变量是Map时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。详见装饰Map类型变量
  • 当装饰的变量是Set时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。详见装饰Set类型变量

框架行为

  1. 当@LocalStorageLink(key)装饰的数值改变被观察到时,修改将被同步回LocalStorage对应属性键值key的属性中。
  2. LocalStorage中属性键值key对应的数据一旦改变,属性键值key绑定的所有的数据(包括双向@LocalStorageLink和单向@LocalStorageProp)都将同步修改。
  3. 当@LocalStorageLink(key)装饰的数据本身是状态变量,它的改变不仅仅会同步回LocalStorage中,还会引起所属的自定义组件的重新渲染。
使用场景
应用逻辑使用 LocalStorage
1
2
3
4
5
6
7
8
9
let para: Record<string,number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage(para); // 创建新实例并使用给定对象初始化
let propA: number | undefined = storage.get('PropA') // propA == 47
let link1: SubscribedAbstractProperty<number> = storage.link('PropA'); // link1.get() == 47
let link2: SubscribedAbstractProperty<number> = storage.link('PropA'); // link2.get() == 47
let prop: SubscribedAbstractProperty<number> = storage.prop('PropA'); // prop.get() == 47
link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48
prop.set(1); // one-way sync: prop.get() == 1; but link1.get() == link2.get() == 48
link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49
从 UI 内部使用 LocalStorage

除了应用程序逻辑使用 LocalStorage,还可以借助 LocalStorage 相关的两个装饰器 @LocalStorageProp 和 @LocalStorageLink,在 UI 组件内部获取到 LocalStorage 实例中存储的状态变量。

本示例以 @LocalStorageLink 为例,展示了:

  • 使用构造函数创建 LocalStorage 实例 storage;
  • 使用 @Entry 装饰器将 storage 添加到 CompA 顶层组件中;
  • @LocalStorageLink 绑定 LocalStorage 对给定的属性,建立双向数据同步。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class PropB {
code: number;

constructor(code: number) {
this.code = code;
}
}
// 创建新实例并使用给定对象初始化
let para: Record<string, number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage(para);
storage.setOrCreate('PropB', new PropB(50));

@Component
struct Child {
// @LocalStorageLink变量装饰器与LocalStorage中的'PropA'属性建立双向绑定
@LocalStorageLink('PropA') childLinkNumber: number = 1;
// @LocalStorageLink变量装饰器与LocalStorage中的'PropB'属性建立双向绑定
@LocalStorageLink('PropB') childLinkObject: PropB = new PropB(0);

build() {
Column() {
Button(`Child from LocalStorage ${this.childLinkNumber}`) // 更改将同步至LocalStorage中的'PropA'以及Parent.parentLinkNumber
.onClick(() => {
this.childLinkNumber += 1;
})
Button(`Child from LocalStorage ${this.childLinkObject.code}`) // 更改将同步至LocalStorage中的'PropB'以及Parent.parentLinkObject.code
.onClick(() => {
this.childLinkObject.code += 1;
})
}
}
}
// 使LocalStorage可从@Component组件访问
@Entry(storage)
@Component
struct CompA {
// @LocalStorageLink变量装饰器与LocalStorage中的'PropA'属性建立双向绑定
@LocalStorageLink('PropA') parentLinkNumber: number = 1;
// @LocalStorageLink变量装饰器与LocalStorage中的'PropB'属性建立双向绑定
@LocalStorageLink('PropB') parentLinkObject: PropB = new PropB(0);

build() {
Column({ space: 15 }) {
Button(`Parent from LocalStorage ${this.parentLinkNumber}`) // initial value from LocalStorage will be 47, because 'PropA' initialized already
.onClick(() => {
this.parentLinkNumber += 1;
})

Button(`Parent from LocalStorage ${this.parentLinkObject.code}`) // initial value from LocalStorage will be 50, because 'PropB' initialized already
.onClick(() => {
this.parentLinkObject.code += 1;
})
// @Component子组件自动获得对CompA LocalStorage实例的访问权限。
Child()
}
}
}
@LocalStorageProp 和 LocalStorage 单向同步的简单场景

在下面的示例中,CompA 组件和 Child 组件分别在本地创建了与 storage 的 ‘PropA’ 对应属性的单向同步的数据,我们可以看到:

  • CompA 中对 this.storageProp1 的修改,只会在 CompA 中生效,并没有同步回 storage;
  • Child 组件中,Text 绑定的 storageProp2 依旧显示 47。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 创建新实例并使用给定对象初始化
let para: Record<string, number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage(para);
// 使LocalStorage可从@Component组件访问
@Entry(storage)
@Component
struct CompA {
// @LocalStorageProp变量装饰器与LocalStorage中的'PropA'属性建立单向绑定
@LocalStorageProp('PropA') storageProp1: number = 1;

build() {
Column({ space: 15 }) {
// 点击后从47开始加1,只改变当前组件显示的storageProp1,不会同步到LocalStorage中
Button(`Parent from LocalStorage ${this.storageProp1}`)
.onClick(() => {
this.storageProp1 += 1
})
Child()
}
}
}

@Component
struct Child {
// @LocalStorageProp变量装饰器与LocalStorage中的'PropA'属性建立单向绑定
@LocalStorageProp('PropA') storageProp2: number = 2;

build() {
Column({ space: 15 }) {
// 当CompA改变时,当前storageProp2不会改变,显示47
Text(`Parent from LocalStorage ${this.storageProp2}`)
}
}
}

下面的示例展示了 @LocalStorageLink 装饰的数据和 LocalStorage 双向同步的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 构造LocalStorage实例
let para: Record<string, number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage(para);
// 调用link(api9以上)接口构造'PropA'的双向同步数据,linkToPropA 是全局变量
let linkToPropA: SubscribedAbstractProperty<object> = storage.link('PropA');

@Entry(storage)
@Component
struct CompA {

// @LocalStorageLink('PropA')在CompA自定义组件中创建'PropA'的双向同步数据,初始值为47,因为在构造LocalStorage已经给“PropA”设置47
@LocalStorageLink('PropA') storageLink: number = 1;

build() {
Column() {
Text(`incr @LocalStorageLink variable`)
// 点击“incr @LocalStorageLink variable”,this.storageLink加1,改变同步回storage,全局变量linkToPropA也会同步改变

.onClick(() => {
this.storageLink += 1
})

// 并不建议在组件内使用全局变量linkToPropA.get(),因为可能会有生命周期不同引起的错误。
Text(`@LocalStorageLink: ${this.storageLink} - linkToPropA: ${linkToPropA.get()}`)
}
}
}
兄弟组件之间同步状态变量

下面的示例展示了通过 @LocalStorageLink 双向同步兄弟组件之间的状态。

先看 Parent 自定义组件中发生的变化:

  1. 点击 “playCount ${this.playCount} dec by 1”,this.playCount 减 1,修改同步回 LocalStorage 中,Child 组件中的 playCountLink 绑定的组件会同步刷新;
  2. 点击 “countStorage ${this.playCount} incr by 1”,调用 LocalStorage 的 set 接口,更新 LocalStorage 中 “countStorage” 对应的属性,Child 组件中的 playCountLink 绑定的组件会同步刷新;
  3. Text 组件 “playCount in LocalStorage for debug ${storage.get(‘countStorage’)}” 没有同步刷新,因为 storage.get(‘countStorage’) 返回的是常规变量,常规变量的更新并不会引起 Text 组件的重新渲染。

Child 自定义组件中的变化:

  1. playCountLink 的刷新会同步回 LocalStorage,并且引起兄弟组件和父组件相应的刷新。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
let ls: Record<string, number> = { 'countStorage': 1 }
let storage: LocalStorage = new LocalStorage(ls);

@Component
struct Child {
// 子组件实例的名字
label: string = 'no name';
// 和LocalStorage中“countStorage”的双向绑定数据
@LocalStorageLink('countStorage') playCountLink: number = 0;

build() {
Row() {
Text(this.label)
.width(50).height(60).fontSize(12)
Text(`playCountLink ${this.playCountLink}: inc by 1`)
.onClick(() => {
this.playCountLink += 1;
})
.width(200).height(60).fontSize(12)
}.width(300).height(60)
}
}

@Entry(storage)
@Component
struct Parent {
@LocalStorageLink('countStorage') playCount: number = 0;

build() {
Column() {
Row() {
Text('Parent')
.width(50).height(60).fontSize(12)
Text(`playCount ${this.playCount} dec by 1`)
.onClick(() => {
this.playCount -= 1;
})
.width(250).height(60).fontSize(12)
}.width(300).height(60)

Row() {
Text('LocalStorage')
.width(50).height(60).fontSize(12)
Text(`countStorage ${this.playCount} incr by 1`)
.onClick(() => {
storage.set<number | undefined>('countStorage', Number(storage.get<number>('countStorage')) + 1);
})
.width(250).height(60).fontSize(12)
}.width(300).height(60)

Child({ label: 'ChildA' })
Child({ label: 'ChildB' })

Text(`playCount in LocalStorage for debug ${storage.get<number>('countStorage')}`)
.width(300).height(60).fontSize(12)
}
}
}
将 LocalStorage 实例从 UIAbility 共享到一个或多个视图

上面的实例中,LocalStorage 的实例仅仅在一个 @Entry 装饰的组件和其所属的子组件(一个页面)中共享,如果希望其在多个视图中共享,可以在所属 UIAbility 中创建 LocalStorage 实例,并调用 windowStage.loadContent

1
2
3
4
5
6
7
8
9
10
11
12
// EntryAbility.ets
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';

export default class EntryAbility extends UIAbility {
para:Record<string, number> = { 'PropA': 47 };
storage: LocalStorage = new LocalStorage(this.para);

onWindowStageCreate(windowStage: window.WindowStage) {
windowStage.loadContent('pages/Index', this.storage);
}
}

说明:

在 UI 页面通过 getShared 接口获取通过 loadContent 共享的 LocalStorage 实例。

LocalStorage.getShared() 只在模拟器或者实机上才有效,在 Previewer 预览器中使用不生效。

在下面的用例中,Index 页面中的 propA 通过 getShared() 方法获取到共享的 LocalStorage 实例。点击 Button 跳转到 Page 页面,点击 Change propA 改变 propA 的值,back 回 Index 页面后,页面中 propA 的值也同步修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// index.ets
import router from '@ohos.router';

// 通过getShared接口获取stage共享的LocalStorage实例
let storage = LocalStorage.getShared()

@Entry(storage)
@Component
struct Index {
// can access LocalStorage instance using
// @LocalStorageLink/Prop decorated variables
@LocalStorageLink('PropA') propA: number = 1;

build() {
Row() {
Column() {
Text(`${this.propA}`)
.fontSize(50)
.fontWeight(FontWeight.Bold)
Button("To Page")
.onClick(() => {
router.pushUrl({
url: 'pages/Page'
})
})
}
.width('100%')
}
.height('100%')
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Page.ets
import router from '@ohos.router';

let storage = LocalStorage.getShared()

@Entry(storage)
@Component
struct Page {
@LocalStorageLink('PropA') propA: number = 2;

build() {
Row() {
Column() {
Text(`${this.propA}`)
.fontSize(50)
.fontWeight(FontWeight.Bold)

Button("Change propA")
.onClick(() => {
this.propA = 100;
})

Button("Back Index")
.onClick(() => {
router.back()
})
}
.width('100%')
}
}
}

说明:

对于开发者更建议使用这个方式来构建 LocalStorage 的实例,并且在创建 LocalStorage 实例的时候就写入默认值,因为默认值可以作为运行异常的备份,也可以用作页面的单元测试。

自定义组件接收LocalStorage实例

除了根节点可通过@Entry来接收LocalStorage实例,自定义组件(子节点)也可以通过构造参数来传递LocalStorage实例。

本示例以@LocalStorageLink为例,展示了:

  • 父组件中的Text,显示LocalStorage实例localStorage1中PropA的值为“PropA”。
  • Child组件中,Text绑定的PropB,显示LocalStorage实例localStorage2中PropB的值为“PropB”。

说明:

从API version 12开始,自定义组件支持接收LocalStorage实例。

当自定义组件作为子节点,定义了成员属性时,LocalStorage实例必须要放在第二个参数位置传递,否则会报类型不匹配的编译问题。

当在自定义组件中定义了属性时,暂时不支持只有一个LocalStorage实例作为入参。如果没定义属性,可以只传入一个LocalStorage实例作为入参

如果定义的属性不需要从父组件初始化变量,则第一个参数需要传{}

作为构造参数传给子组件的LocalStorage实例在初始化时就会被决定,可以通过@LocalStorageLink或者LocalStorage的API修改LocalStorage实例中保存的属性值,但LocalStorage实例自身不能被动态修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
let localStorage1: LocalStorage = new LocalStorage();
localStorage1.setOrCreate('PropA', 'PropA');

let localStorage2: LocalStorage = new LocalStorage();
localStorage2.setOrCreate('PropB', 'PropB');

@Entry(localStorage1)
@Component
struct Index {
// 'PropA',和localStorage1中'PropA'的双向同步
@LocalStorageLink('PropA') PropA: string = 'Hello World';
@State count: number = 0;

build() {
Row() {
Column() {
Text(this.PropA)
.fontSize(50)
.fontWeight(FontWeight.Bold)
// 使用LocalStorage 实例localStorage2
Child({ count: this.count }, localStorage2)
}
.width('100%')
}
.height('100%')
}
}

@Component
struct Child {
@Link count: number;
// 'Hello World',和localStorage2中'PropB'的双向同步,localStorage2中没有'PropB',则使用默认值'Hello World'
@LocalStorageLink('PropB') PropB: string = 'Hello World';

build() {
Text(this.PropB)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
}
当自定义组件没有定义属性时,可以只传入一个LocalStorage实例作为入参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
let localStorage1: LocalStorage = new LocalStorage();
localStorage1.setOrCreate('PropA', 'PropA');

let localStorage2: LocalStorage = new LocalStorage();
localStorage2.setOrCreate('PropB', 'PropB');

@Entry(localStorage1)
@Component
struct Index {
// 'PropA',和localStorage1中'PropA'的双向同步
@LocalStorageLink('PropA') PropA: string = 'Hello World';
@State count: number = 0;

build() {
Row() {
Column() {
Text(this.PropA)
.fontSize(50)
.fontWeight(FontWeight.Bold)
// 使用LocalStorage 实例localStorage2
Child(localStorage2)
}
.width('100%')
}
.height('100%')
}
}

@Component
struct Child {
build() {
Text("hello")
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
}
当定义的属性不需要从父组件初始化变量时,第一个参数需要传{}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
let localStorage1: LocalStorage = new LocalStorage();
localStorage1.setOrCreate('PropA', 'PropA');

let localStorage2: LocalStorage = new LocalStorage();
localStorage2.setOrCreate('PropB', 'PropB');

@Entry(localStorage1)
@Component
struct Index {
// 'PropA',和localStorage1中'PropA'的双向同步
@LocalStorageLink('PropA') PropA: string = 'Hello World';
@State count: number = 0;

build() {
Row() {
Column() {
Text(this.PropA)
.fontSize(50)
.fontWeight(FontWeight.Bold)
// 使用LocalStorage 实例localStorage2
Child({}, localStorage2)
}
.width('100%')
}
.height('100%')
}
}

@Component
struct Child {
@State count: number = 5;
// 'Hello World',和localStorage2中'PropB'的双向同步,localStorage2中没有'PropB',则使用默认值'Hello World'
@LocalStorageLink('PropB') PropB: string = 'Hello World';

build() {
Text(this.PropB)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
}

可以通过传递不同的LocalStorage实例给自定义组件,从而实现在navigation跳转到不同的页面时,绑定不同的LocalStorage实例,显示对应绑定的值。

本示例以@LocalStorageLink为例,展示了:

  • 点击父组件中的Button “Next Page”,创建并跳转到name为”pageOne”的子页面,Text显示信息为LocalStorage实例localStorageA中绑定的PropA的值,为”PropA”。
  • 继续点击页面上的Button “Next Page”,创建并跳转到name为”pageTwo”的子页面,Text显示信息为LocalStorage实例localStorageB中绑定的PropB的值,为”PropB”。
  • 继续点击页面上的Button “Next Page”,创建并跳转到name为”pageTree”的子页面,Text显示信息为LocalStorage实例localStorageC中绑定的PropC的值,为”PropC”。
  • 继续点击页面上的Button “Next Page”,创建并跳转到name为”pageOne”的子页面,Text显示信息为LocalStorage实例localStorageA中绑定的PropA的值,为”PropA”。
  • NavigationContentMsgStack自定义组件中的Text组件,共享对应自定义组件树上LocalStorage实例绑定的PropA的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
let localStorageA: LocalStorage = new LocalStorage();
localStorageA.setOrCreate('PropA', 'PropA');

let localStorageB: LocalStorage = new LocalStorage();
localStorageB.setOrCreate('PropB', 'PropB');

let localStorageC: LocalStorage = new LocalStorage();
localStorageC.setOrCreate('PropC', 'PropC');

@Entry
@Component
struct MyNavigationTestStack {
@Provide('pageInfo') pageInfo: NavPathStack = new NavPathStack();

@Builder
PageMap(name: string) {
if (name === 'pageOne') {
// 传递不同的LocalStorage实例
pageOneStack({}, localStorageA)
} else if (name === 'pageTwo') {
pageTwoStack({}, localStorageB)
} else if (name === 'pageThree') {
pageThreeStack({}, localStorageC)
}
}

build() {
Column({ space: 5 }) {
Navigation(this.pageInfo) {
Column() {
Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.pageInfo.pushPath({ name: 'pageOne' }); //将name指定的NavDestination页面信息入栈
})
}
}.title('NavIndex')
.navDestination(this.PageMap)
.mode(NavigationMode.Stack)
.borderWidth(1)
}
}
}

@Component
struct pageOneStack {
@Consume('pageInfo') pageInfo: NavPathStack;
@LocalStorageLink('PropA') PropA: string = 'Hello World';

build() {
NavDestination() {
Column() {
NavigationContentMsgStack()
// 显示绑定的LocalStorage中PropA的值'PropA'
Text(`${this.PropA}`)
Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.pageInfo.pushPathByName('pageTwo', null);
})
}.width('100%').height('100%')
}.title('pageOne')
.onBackPressed(() => {
this.pageInfo.pop();
return true;
})
}
}

@Component
struct pageTwoStack {
@Consume('pageInfo') pageInfo: NavPathStack;
@LocalStorageLink('PropB') PropB: string = 'Hello World';

build() {
NavDestination() {
Column() {
NavigationContentMsgStack()
// 绑定的LocalStorage中没有PropA,显示本地初始化的值 'Hello World'
Text(`${this.PropB}`)
Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.pageInfo.pushPathByName('pageThree', null);
})

}.width('100%').height('100%')
}.title('pageTwo')
.onBackPressed(() => {
this.pageInfo.pop();
return true;
})
}
}

@Component
struct pageThreeStack {
@Consume('pageInfo') pageInfo: NavPathStack;
@LocalStorageLink('PropC') PropC: string = 'pageThreeStack';

build() {
NavDestination() {
Column() {
NavigationContentMsgStack()

// 绑定的LocalStorage中没有PropA,显示本地初始化的值 'pageThreeStack'
Text(`${this.PropC}`)
Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.pageInfo.pushPathByName('pageOne', null);
})

}.width('100%').height('100%')
}.title('pageThree')
.onBackPressed(() => {
this.pageInfo.pop();
return true;
})
}
}

@Component
struct NavigationContentMsgStack {
@LocalStorageLink('PropA') PropA: string = 'Hello';

build() {
Column() {
Text(`${this.PropA}`)
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
}
}
LocalStorage支持联合类型

在下面的示例中,变量A的类型为number | null,变量B的类型为number | undefined。Text组件初始化分别显示为null和undefined,点击切换为数字,再次点击切换回null和undefined。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Component
struct LocalStorLink {
@LocalStorageLink("AA") A: number | null = null;
@LocalStorageLink("BB") B: number | undefined = undefined;

build() {
Column() {
Text("@LocalStorageLink接口初始化,@LocalStorageLink取值")
Text(this.A + "").fontSize(20).onClick(() => {
this.A ? this.A = null : this.A = 1;
})
Text(this.B + "").fontSize(20).onClick(() => {
this.B ? this.B = undefined : this.B = 1;
})
}
.borderWidth(3).borderColor(Color.Green)

}
}

@Component
struct LocalStorProp {
@LocalStorageProp("AAA") A: number | null = null;
@LocalStorageProp("BBB") B: number | undefined = undefined;

build() {
Column() {
Text("@LocalStorageProp接口初始化,@LocalStorageProp取值")
Text(this.A + "").fontSize(20).onClick(() => {
this.A ? this.A = null : this.A = 1;
})
Text(this.B + "").fontSize(20).onClick(() => {
this.B ? this.B = undefined : this.B = 1;
})
}
.borderWidth(3).borderColor(Color.Yellow)

}
}

let storage1: LocalStorage = new LocalStorage();

@Entry(storage1)
@Component
struct TestCase3 {
build() {
Row() {
Column() {
LocalStorLink()
LocalStorProp()
}
.width('100%')
}
.height('100%')
}
}
装饰Date类型变量

说明:

从API version 12开始,LocalStorage支持Date类型。

在下面的示例中,@LocalStorageLink装饰的selectedDate类型为Date,点击Button改变selectedDate的值,视图会随之刷新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Entry
@Component
struct LocalDateSample {
@LocalStorageLink("date") selectedDate: Date = new Date('2021-08-08');

build() {
Column() {
Button('set selectedDate to 2023-07-08')
.margin(10)
.onClick(() => {
this.selectedDate = new Date('2023-07-08');
})
Button('increase the year by 1')
.margin(10)
.onClick(() => {
this.selectedDate.setFullYear(this.selectedDate.getFullYear() + 1);
})
Button('increase the month by 1')
.margin(10)
.onClick(() => {
this.selectedDate.setMonth(this.selectedDate.getMonth() + 1);
})
Button('increase the day by 1')
.margin(10)
.onClick(() => {
this.selectedDate.setDate(this.selectedDate.getDate() + 1);
})
DatePicker({
start: new Date('1970-1-1'),
end: new Date('2100-1-1'),
selected: $$this.selectedDate
})
}.width('100%')
}
}
装饰Map类型变量

说明:

从API version 12开始,LocalStorage支持Map类型。

在下面的示例中,@LocalStorageLink装饰的message类型为Map<number, string>,点击Button改变message的值,视图会随之刷新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Entry
@Component
struct LocalMapSample {
@LocalStorageLink("map") message: Map<number, string> = new Map([[0, "a"], [1, "b"], [3, "c"]]);

build() {
Row() {
Column() {
ForEach(Array.from(this.message.entries()), (item: [number, string]) => {
Text(`${item[0]}`).fontSize(30)
Text(`${item[1]}`).fontSize(30)
Divider()
})
Button('init map').onClick(() => {
this.message = new Map([[0, "a"], [1, "b"], [3, "c"]]);
})
Button('set new one').onClick(() => {
this.message.set(4, "d");
})
Button('clear').onClick(() => {
this.message.clear();
})
Button('replace the existing one').onClick(() => {
this.message.set(0, "aa");
})
Button('delete the existing one').onClick(() => {
this.message.delete(0);
})
}
.width('100%')
}
.height('100%')
}
}
装饰Set类型变量

说明:

从API version 12开始,LocalStorage支持Set类型。

在下面的示例中,@LocalStorageLink装饰的memberSet类型为Set,点击Button改变memberSet的值,视图会随之刷新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Entry
@Component
struct LocalSetSample {
@LocalStorageLink("set") memberSet: Set<number> = new Set([0, 1, 2, 3, 4]);

build() {
Row() {
Column() {
ForEach(Array.from(this.memberSet.entries()), (item: [number, string]) => {
Text(`${item[0]}`)
.fontSize(30)
Divider()
})
Button('init set')
.onClick(() => {
this.memberSet = new Set([0, 1, 2, 3, 4]);
})
Button('set new one')
.onClick(() => {
this.memberSet.add(5);
})
Button('clear')
.onClick(() => {
this.memberSet.clear();
})
Button('delete the first one')
.onClick(() => {
this.memberSet.delete(0);
})
}
.width('100%')
}
.height('100%')
}
}

AppStorage:应用全局的 UI 状态存储

AppStorage 是应用全局的 UI 状态存储,是和应用的进程绑定的,由 UI 框架在应用程序启动时创建,为应用程序 UI 状态属性提供中央存储。

和 AppStorage 不同的是,LocalStorage 是页面级的,通常应用于页面内的数据共享。而 AppStorage 是应用级的全局状态共享,还相当于整个应用的“中枢”,持久化数据 PersistentStorage环境变量 Environment 都是通过 AppStorage 中转,才可以和 UI 交互。

概述

AppStorage 是在应用启动的时候会被创建的单例。它的目的是为了提供应用状态数据的中心存储,这些状态数据在应用级别都是可访问的。AppStorage 将在应用运行过程保留其属性。属性通过唯一的键字符串值访问。

AppStorage 可以和 UI 组件同步,且可以在应用业务逻辑中被访问。

AppStorage 支持应用的主线程内多个 UIAbility 实例间的状态共享。

AppStorage 中的属性可以被双向同步,数据可以是存在于本地或远程设备上,并具有不同的功能,比如数据持久化(详见 PersistentStorage)。这些数据是通过业务逻辑中实现,与 UI 解耦,如果希望这些数据在 UI 中使用,需要用到 @StorageProp@StorageLink

@StorageProp

要建立 AppStorage 和自定义组件的联系,需要使用 @StorageProp 和 @StorageLink 装饰器。使用 @StorageProp(key)/@StorageLink(key) 装饰组件内的变量,key 标识了 AppStorage 的属性。

当自定义组件初始化的时候,会使用 AppStorage 中对应 key 的属性值将 @StorageProp(key)/@StorageLink(key) 装饰的变量初始化。由于应用逻辑的差异,无法确认是否在组件初始化之前向 AppStorage 实例中存入了对应的属性,所以 AppStorage 不一定存在 key 对应的属性,因此 @StorageProp(key)/@StorageLink(key) 装饰的变量进行本地初始化是必要的。

@StorageProp(key) 是和 AppStorage 中 key 对应的属性建立单向数据同步,允许本地改变,但是对于 @StorageProp,本地的修改永远不会同步回 AppStorage 中,相反,如果 AppStorage 给定 key 的属性发生改变,改变会被同步给 @StorageProp,并覆盖掉本地的修改。

装饰器使用规则说明

@StorageProp变量装饰器 说明
装饰器参数 key:常量字符串,必填(字符串需要有引号)。
允许装饰的变量类型 Object、class、string、number、boolean、enum类型,以及这些类型的数组。API12及以上支持Map、Set、Date类型。嵌套类型的场景请参考观察变化和行为表现。类型必须被指定,建议和AppStorage中对应属性类型相同,否则会发生类型隐式转换,从而导致应用行为异常。不支持any,API12及以上支持undefined和null类型。API12及以上支持上述支持类型的联合类型,比如string | number, string | undefined 或者 ClassA | null,示例见AppStorage支持联合类型注意当使用undefined和null的时候,建议显式指定类型,遵循TypeScript类型校验,比如:@StorageProp(“AA”) a: number | null = null是推荐的,不推荐@StorageProp(“AA”) a: number = null。
同步类型 单向同步:从AppStorage的对应属性到组件的状态变量。组件本地的修改是允许的,但是AppStorage中给定的属性一旦发生变化,将覆盖本地的修改。
被装饰变量的初始值 必须指定,如果AppStorage实例中不存在属性,则用该初始值初始化该属性,并存入AppStorage中。

变量的传递/访问规则说明

传递/访问 说明
从父节点初始化和更新 禁止,@StorageProp不支持从父节点初始化,只能AppStorage中key对应的属性初始化,如果没有对应key的话,将使用本地默认值初始化
初始化子节点 支持,可用于初始化@State、@Link、@Prop、@Provide。
是否支持组件外访问 否。

观察变化和行为表现

观察变化

  • 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
  • 当装饰的数据类型为class或者Object时,可以观察到对象整体赋值和对象属性变化(详见从ui内部使用appstorage和localstorage)。
  • 当装饰的对象是array时,可以观察到数组添加、删除、更新数组单元的变化。
  • 当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。详见装饰Date类型变量
  • 当装饰的变量是Map时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。详见装饰Map类型变量
  • 当装饰的变量是Set时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。详见装饰Set类型变量

框架行为

  • 当@StorageProp(key)装饰的数值改变被观察到时,修改不会被同步回AppStorage对应属性键值key的属性中。
  • 当前@StorageProp(key)单向绑定的数据会被修改,即仅限于当前组件的私有成员变量改变,其他的绑定该key的数据不会同步改变。
  • 当@StorageProp(key)装饰的数据本身是状态变量,它的改变虽然不会同步回AppStorage中,但是会引起所属的自定义组件的重新渲染。
  • 当AppStorage中key对应的属性发生改变时,会同步给所有@StorageProp(key)装饰的数据,@StorageProp(key)本地的修改将被覆盖。

@StorageLink(key)是和AppStorage中key对应的属性建立双向数据同步

  1. 本地修改发生,该修改会被写回AppStorage中;
  2. AppStorage中的修改发生后,该修改会被同步到所有绑定AppStorage对应key的属性上,包括单向(@StorageProp和通过Prop创建的单向绑定变量)、双向(@StorageLink和通过Link创建的双向绑定变量)变量和其他实例(比如PersistentStorage)。

装饰器使用规则说明

@StorageLink变量装饰器 说明
装饰器参数 key:常量字符串,必填(字符串需要有引号)。
允许装饰的变量类型 Object、class、string、number、boolean、enum类型,以及这些类型的数组。API12及以上支持Map、Set、Date类型。嵌套类型的场景请参考观察变化和行为表现。类型必须被指定,建议和AppStorage中对应属性类型相同,否则会发生类型隐式转换,从而导致应用行为异常。不支持any,API12及以上支持undefined和null类型。API12及以上支持上述支持类型的联合类型,比如string | number, string | undefined 或者 ClassA | null,示例见AppStorage支持联合类型注意当使用undefined和null的时候,建议显式指定类型,遵循TypeScript类型校验,比如:@StorageLink(“AA”) a: number | null = null是推荐的,不推荐@StorageLink(“AA”) a: number = null。
同步类型 双向同步:从AppStorage的对应属性到自定义组件,从自定义组件到AppStorage对应属性。
被装饰变量的初始值 必须指定,如果AppStorage实例中不存在属性,则用该初始值初始化该属性,并存入AppStorage中。

变量的传递/访问规则说明

传递/访问 说明
从父节点初始化和更新 禁止。
初始化子节点 支持,可用于初始化常规变量、@State、@Link、@Prop、@Provide。
是否支持组件外访问 否。

观察变化和行为表现

观察变化

  • 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
  • 当装饰的数据类型为class或者Object时,可以观察到对象整体赋值和对象属性变化(详见从ui内部使用appstorage和localstorage)。
  • 当装饰的对象是array时,可以观察到数组添加、删除、更新数组单元的变化。
  • 当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。详见装饰Date类型变量
  • 当装饰的变量是Map时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。详见装饰Map类型变量
  • 当装饰的变量是Set时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。详见装饰Set类型变量

框架行为

  1. 当@StorageLink(key)装饰的数值改变被观察到时,修改将被同步回AppStorage对应属性键值key的属性中。
  2. AppStorage中属性键值key对应的数据一旦改变,属性键值key绑定的所有的数据(包括双向@StorageLink和单向@StorageProp)都将同步修改。
  3. 当@StorageLink(key)装饰的数据本身是状态变量,它的改变不仅仅会同步回AppStorage中,还会引起所属的自定义组件的重新渲染。
使用场景
从应用逻辑使用AppStorage和LocalStorage

AppStorage是单例,它的所有API都是静态的,使用方法类似于LocalStorage中对应的非静态方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
AppStorage.setOrCreate('PropA', 47);

let storage: LocalStorage = new LocalStorage();
storage.setOrCreate('PropA',17);
let propA: number | undefined = AppStorage.get('PropA') // propA in AppStorage == 47, propA in LocalStorage == 17
let link1: SubscribedAbstractProperty<number> = AppStorage.link('PropA'); // link1.get() == 47
let link2: SubscribedAbstractProperty<number> = AppStorage.link('PropA'); // link2.get() == 47
let prop: SubscribedAbstractProperty<number> = AppStorage.prop('PropA'); // prop.get() == 47

link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48
prop.set(1); // one-way sync: prop.get() == 1; but link1.get() == link2.get() == 48
link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49

storage.get<number>('PropA') // == 17
storage.set('PropA', 101);
storage.get<number>('PropA') // == 101

AppStorage.get<number>('PropA') // == 49
link1.get() // == 49
link2.get() // == 49
prop.get() // == 49
从UI内部使用AppStorage和LocalStorage

@StorageLink变量装饰器与AppStorage配合使用,正如@LocalStorageLink与LocalStorage配合使用一样。此装饰器使用AppStorage中的属性创建双向数据同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class PropB {
code: number;

constructor(code: number) {
this.code = code;
}
}

AppStorage.setOrCreate('PropA', 47);
AppStorage.setOrCreate('PropB', new PropB(50));
let storage = new LocalStorage();
storage.setOrCreate('PropA', 48);
storage.setOrCreate('PropB', new PropB(100));

@Entry(storage)
@Component
struct CompA {
@StorageLink('PropA') storageLink: number = 1;
@LocalStorageLink('PropA') localStorageLink: number = 1;
@StorageLink('PropB') storageLinkObject: PropB = new PropB(1);
@LocalStorageLink('PropB') localStorageLinkObject: PropB = new PropB(1);

build() {
Column({ space: 20 }) {
Text(`From AppStorage ${this.storageLink}`)
.onClick(() => {
this.storageLink += 1;
})

Text(`From LocalStorage ${this.localStorageLink}`)
.onClick(() => {
this.localStorageLink += 1;
})

Text(`From AppStorage ${this.storageLinkObject.code}`)
.onClick(() => {
this.storageLinkObject.code += 1;
})

Text(`From LocalStorage ${this.localStorageLinkObject.code}`)
.onClick(() => {
this.localStorageLinkObject.code += 1;
})
}
}
}
不建议借助@StorageLink的双向同步机制实现事件通知

不建议开发者使用@StorageLink和AppStorage的双向同步的机制来实现事件通知,因为AppStorage中的变量可能绑定在多个不同页面的组件中,但事件通知则不一定需要通知到所有的这些组件。并且,当这些@StorageLink装饰的变量在UI中使用时,会触发UI刷新,带来不必要的性能影响。

示例代码中,TapImage中的点击事件,会触发AppStorage中tapIndex对应属性的改变。因为@StorageLink是双向同步,修改会同步回AppStorage中,所以,所有绑定AppStorage的tapIndex自定义组件里都能感知到tapIndex的变化。使用@Watch监听到tapIndex的变化后,修改状态变量tapColor从而触发UI刷新(此处tapIndex并未直接绑定在UI上,因此tapIndex的变化不会直接触发UI刷新)。

使用该机制来实现事件通知需要确保AppStorage中的变量尽量不要直接绑定在UI上,且需要控制@Watch函数的复杂度(如果@Watch函数执行时间长,会影响UI刷新效率)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// xxx.ets
class ViewData {
title: string;
uri: Resource;
color: Color = Color.Black;

constructor(title: string, uri: Resource) {
this.title = title;
this.uri = uri
}
}

@Entry
@Component
struct Gallery2 {
dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))]
scroller: Scroller = new Scroller()

build() {
Column() {
Grid(this.scroller) {
ForEach(this.dataList, (item: ViewData, index?: number) => {
GridItem() {
TapImage({
uri: item.uri,
index: index
})
}.aspectRatio(1)

}, (item: ViewData, index?: number) => {
return JSON.stringify(item) + index;
})
}.columnsTemplate('1fr 1fr')
}

}
}

@Component
export struct TapImage {
@StorageLink('tapIndex') @Watch('onTapIndexChange') tapIndex: number = -1;
@State tapColor: Color = Color.Black;
private index: number = 0;
private uri: Resource = {
id: 0,
type: 0,
moduleName: "",
bundleName: ""
};

// 判断是否被选中
onTapIndexChange() {
if (this.tapIndex >= 0 && this.index === this.tapIndex) {
console.info(`tapindex: ${this.tapIndex}, index: ${this.index}, red`)
this.tapColor = Color.Red;
} else {
console.info(`tapindex: ${this.tapIndex}, index: ${this.index}, black`)
this.tapColor = Color.Black;
}
}

build() {
Column() {
Image(this.uri)
.objectFit(ImageFit.Cover)
.onClick(() => {
this.tapIndex = this.index;
})
.border({ width: 5, style: BorderStyle.Dotted, color: this.tapColor })
}

}
}

相比借助@StorageLink的双向同步机制实现事件通知,开发者可以使用emit订阅某个事件并接收事件回调的方式来减少开销,增强代码的可读性。

说明:

emit接口不支持在Previewer预览器中使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// xxx.ets
import emitter from '@ohos.events.emitter';

let NextID: number = 0;

class ViewData {
title: string;
uri: Resource;
color: Color = Color.Black;
id: number;

constructor(title: string, uri: Resource) {
this.title = title;
this.uri = uri
this.id = NextID++;
}
}

@Entry
@Component
struct Gallery2 {
dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))]
scroller: Scroller = new Scroller()
private preIndex: number = -1

build() {
Column() {
Grid(this.scroller) {
ForEach(this.dataList, (item: ViewData) => {
GridItem() {
TapImage({
uri: item.uri,
index: item.id
})
}.aspectRatio(1)
.onClick(() => {
if (this.preIndex === item.id) {
return
}
let innerEvent: emitter.InnerEvent = { eventId: item.id }
// 选中态:黑变红
let eventData: emitter.EventData = {
data: {
"colorTag": 1
}
}
emitter.emit(innerEvent, eventData)

if (this.preIndex != -1) {
console.info(`preIndex: ${this.preIndex}, index: ${item.id}, black`)
let innerEvent: emitter.InnerEvent = { eventId: this.preIndex }
// 取消选中态:红变黑
let eventData: emitter.EventData = {
data: {
"colorTag": 0
}
}
emitter.emit(innerEvent, eventData)
}
this.preIndex = item.id
})
}, (item: ViewData) => JSON.stringify(item))
}.columnsTemplate('1fr 1fr')
}

}
}

@Component
export struct TapImage {
@State tapColor: Color = Color.Black;
private index: number = 0;
private uri: Resource = {
id: 0,
type: 0,
moduleName: "",
bundleName: ""
};

onTapIndexChange(colorTag: emitter.EventData) {
if (colorTag.data != null) {
this.tapColor = colorTag.data.colorTag ? Color.Red : Color.Black
}
}

aboutToAppear() {
//定义事件ID
let innerEvent: emitter.InnerEvent = { eventId: this.index }
emitter.on(innerEvent, data => {
this.onTapIndexChange(data)
})
}

build() {
Column() {
Image(this.uri)
.objectFit(ImageFit.Cover)
.border({ width: 5, style: BorderStyle.Dotted, color: this.tapColor })
}
}
}

以上通知事件逻辑简单,也可以简化成三元表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// xxx.ets
class ViewData {
title: string;
uri: Resource;
color: Color = Color.Black;

constructor(title: string, uri: Resource) {
this.title = title;
this.uri = uri
}
}

@Entry
@Component
struct Gallery2 {
dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))]
scroller: Scroller = new Scroller()

build() {
Column() {
Grid(this.scroller) {
ForEach(this.dataList, (item: ViewData, index?: number) => {
GridItem() {
TapImage({
uri: item.uri,
index: index
})
}.aspectRatio(1)

}, (item: ViewData, index?: number) => {
return JSON.stringify(item) + index;
})
}.columnsTemplate('1fr 1fr')
}

}
}

@Component
export struct TapImage {
@StorageLink('tapIndex') tapIndex: number = -1;
private index: number = 0;
private uri: Resource = {
id: 0,
type: 0,
moduleName: "",
bundleName: ""
};

build() {
Column() {
Image(this.uri)
.objectFit(ImageFit.Cover)
.onClick(() => {
this.tapIndex = this.index;
})
.border({
width: 5,
style: BorderStyle.Dotted,
color: (this.tapIndex >= 0 && this.index === this.tapIndex) ? Color.Red : Color.Black
})
}
}
}
AppStorage支持联合类型

在下面的示例中,变量A的类型为number | null,变量B的类型为number | undefined。Text组件初始化分别显示为null和undefined,点击切换为数字,再次点击切换回null和undefined。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Component
struct StorLink {
@StorageLink("AA") A: number | null = null;
@StorageLink("BB") B: number | undefined = undefined;

build() {
Column() {
Text("@StorageLink接口初始化,@StorageLink取值")
Text(this.A + "").fontSize(20).onClick(() => {
this.A ? this.A = null : this.A = 1;
})
Text(this.B + "").fontSize(20).onClick(() => {
this.B ? this.B = undefined : this.B = 1;
})
}
.borderWidth(3).borderColor(Color.Red)

}
}

@Component
struct StorProp {
@StorageProp("AAA") A: number | null = null;
@StorageProp("BBB") B: number | undefined = undefined;

build() {
Column() {
Text("@StorageProp接口初始化,@StorageProp取值")
Text(this.A + "").fontSize(20).onClick(() => {
this.A ? this.A = null : this.A = 1;
})
Text(this.B + "").fontSize(20).onClick(() => {
this.B ? this.B = undefined : this.B = 1;
})
}
.borderWidth(3).borderColor(Color.Blue)
}
}

@Entry
@Component
struct TestCase3 {
build() {
Row() {
Column() {
StorLink()
StorProp()
}
.width('100%')
}
.height('100%')
}
}
装饰Date类型变量

说明:

从API version 12开始,AppStorage支持Date类型。

在下面的示例中,@StorageLink装饰的selectedDate类型为Date,点击Button改变selectedDate的值,视图会随之刷新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Entry
@Component
struct DateSample {
@StorageLink("date") selectedDate: Date = new Date('2021-08-08');

build() {
Column() {
Button('set selectedDate to 2023-07-08')
.margin(10)
.onClick(() => {
AppStorage.setOrCreate("date", new Date('2023-07-08'));
})
Button('increase the year by 1')
.margin(10)
.onClick(() => {
this.selectedDate.setFullYear(this.selectedDate.getFullYear() + 1);
})
Button('increase the month by 1')
.margin(10)
.onClick(() => {
this.selectedDate.setMonth(this.selectedDate.getMonth() + 1);
})
Button('increase the day by 1')
.margin(10)
.onClick(() => {
this.selectedDate.setDate(this.selectedDate.getDate() + 1);
})
DatePicker({
start: new Date('1970-1-1'),
end: new Date('2100-1-1'),
selected: $$this.selectedDate
})
}.width('100%')
}
}
装饰Map类型变量

说明:

从API version 12开始,AppStorage支持Map类型。

在下面的示例中,@StorageLink装饰的message类型为Map<number, string>,点击Button改变message的值,视图会随之刷新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Entry
@Component
struct MapSample {
@StorageLink("map") message: Map<number, string> = new Map([[0, "a"], [1, "b"], [3, "c"]]);

build() {
Row() {
Column() {
ForEach(Array.from(this.message.entries()), (item: [number, string]) => {
Text(`${item[0]}`).fontSize(30)
Text(`${item[1]}`).fontSize(30)
Divider()
})
Button('init map').onClick(() => {
this.message = new Map([[0, "a"], [1, "b"], [3, "c"]]);
})
Button('set new one').onClick(() => {
this.message.set(4, "d");
})
Button('clear').onClick(() => {
this.message.clear();
})
Button('replace the existing one').onClick(() => {
this.message.set(0, "aa");
})
Button('delete the existing one').onClick(() => {
AppStorage.get<Map<number, string>>("map")?.delete(0);
})
}
.width('100%')
}
.height('100%')
}
}
装饰Set类型变量

说明:

从API version 12开始,AppStorage支持Set类型。

在下面的示例中,@StorageLink装饰的memberSet类型为Set,点击Button改变memberSet的值,视图会随之刷新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Entry
@Component
struct SetSample {
@StorageLink("set") memberSet: Set<number> = new Set([0, 1, 2, 3, 4]);

build() {
Row() {
Column() {
ForEach(Array.from(this.memberSet.entries()), (item: [number, string]) => {
Text(`${item[0]}`)
.fontSize(30)
Divider()
})
Button('init set')
.onClick(() => {
this.memberSet = new Set([0, 1, 2, 3, 4]);
})
Button('set new one')
.onClick(() => {
AppStorage.get<Set<number>>("set")?.add(5);
})
Button('clear')
.onClick(() => {
this.memberSet.clear();
})
Button('delete the first one')
.onClick(() => {
this.memberSet.delete(0);
})
}
.width('100%')
}
.height('100%')
}
}
限制条件

AppStorage与PersistentStorage以及Environment配合使用时,需要注意以下几点:

  • 在AppStorage中创建属性后,调用PersistentStorage.persistProp()接口时,会使用在AppStorage中已经存在的值,并覆盖PersistentStorage中的同名属性,所以建议要使用相反的调用顺序,反例可见在PersistentStorage之前访问AppStorage中的属性
  • 如果在AppStorage中已经创建属性后,再调用Environment.envProp()创建同名的属性,会调用失败。因为AppStorage已经有同名属性,Environment环境变量不会再写入AppStorage中,所以建议AppStorage中属性不要使用Environment预置环境变量名。
  • 状态装饰器装饰的变量,改变会引起UI的渲染更新,如果改变的变量不是用于UI更新,只是用于消息传递,推荐使用 emitter方式。例子可见不建议借助@StorageLink的双向同步机制实现事件通知

PersistentStorage:持久化存储UI状态

PersistentStorage是应用程序中的可选单例对象。此对象的作用是持久化存储选定的AppStorage属性,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同。

概述

PersistentStorage将选定的AppStorage属性保留在设备磁盘上。应用程序通过API,以决定哪些AppStorage属性应借助PersistentStorage持久化。UI和业务逻辑不直接访问PersistentStorage中的属性,所有属性访问都是对AppStorage的访问,AppStorage中的更改会自动同步到PersistentStorage。

PersistentStorage和AppStorage中的属性建立双向同步。应用开发通常通过AppStorage访问PersistentStorage,另外还有一些接口可以用于管理持久化属性,但是业务逻辑始终是通过AppStorage获取和设置属性的。

限制条件

PersistentStorage允许的类型和值有:

  • number, string, boolean, enum 等简单类型。
  • 可以被JSON.stringify()和JSON.parse()重构的对象,以及对象的属性方法不支持持久化。
  • API12及以上支持Map类型,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。且更新的值被持久化存储。详见装饰Map类型变量
  • API12及以上支持Set类型,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。且更新的值被持久化存储。详见装饰Set类型变量
  • API12及以上支持Date类型,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。且更新的值被持久化存储。详见装饰Date类型变量
  • API12及以上支持undefined 和 null。
  • API12及以上支持联合类型

PersistentStorage不允许的类型和值有:

  • 不支持嵌套对象(对象数组,对象的属性是对象等)。因为目前框架无法检测AppStorage中嵌套对象(包括数组)值的变化,所以无法写回到PersistentStorage中。

持久化数据是一个相对缓慢的操作,应用程序应避免以下情况:

  • 持久化大型数据集。
  • 持久化经常变化的变量。

PersistentStorage的持久化变量最好是小于2kb的数据,不要大量的数据持久化,因为PersistentStorage写入磁盘的操作是同步的,大量的数据本地化读写会同步在UI线程中执行,影响UI渲染性能。如果开发者需要存储大量的数据,建议使用数据库api。

PersistentStorage和UI实例相关联,持久化操作需要在UI实例初始化成功后(即loadContent传入的回调被调用时)才可以被调用,早于该时机调用会导致持久化失败。

1
2
3
4
5
6
7
8
9
// EntryAbility.ets
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
return;
}
PersistentStorage.persistProp('aProp', 47);
});
}
使用场景
从AppStorage中访问PersistentStorage初始化的属性
  1. 初始化PersistentStorage:

    1
    PersistentStorage.persistProp('aProp', 47);
  2. 在AppStorage获取对应属性:

    1
    AppStorage.get<number>('aProp'); // returns 47

    或在组件内部定义:

    1
    @StorageLink('aProp') aProp: number = 48;

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PersistentStorage.persistProp('aProp', 47);

@Entry
@Component
struct Index {
@State message: string = 'Hello World'
@StorageLink('aProp') aProp: number = 48

build() {
Row() {
Column() {
Text(this.message)
// 应用退出时会保存当前结果。重新启动后,会显示上一次的保存结果
Text(`${this.aProp}`)
.onClick(() => {
this.aProp += 1;
})
}
}
}
}
  • 新应用安装后首次启动运行:

    1. 调用persistProp初始化PersistentStorage,首先查询在PersistentStorage本地文件中是否存在“aProp”,查询结果为不存在,因为应用是第一次安装。
    2. 接着查询属性“aProp”在AppStorage中是否存在,依旧不存在。
    3. 在AppStorge中创建名为“aProp”的number类型属性,属性初始值是定义的默认值47。
    4. PersistentStorage将属性“aProp”和值47写入磁盘,AppStorage中“aProp”对应的值和其后续的更改将被持久化。
    5. 在Index组件中创建状态变量@StorageLink(‘aProp’) aProp,和AppStorage中“aProp”双向绑定,在创建的过程中会在AppStorage中查找,成功找到“aProp”,所以使用其在AppStorage找到的值47。
  • 触发点击事件后:

    1. 状态变量@StorageLink(‘aProp’) aProp改变,触发Text组件重新刷新。
    2. @StorageLink装饰的变量是和AppStorage中建立双向同步的,所以@StorageLink(‘aProp’) aProp的变化会被同步回AppStorage中。
    3. AppStorage中“aProp”属性的改变会同步到所有绑定该“aProp”的单向或者双向变量,在本示例中没有其他的绑定“aProp”的变量。
    4. 因为“aProp”对应的属性已经被持久化,所以在AppStorage中“aProp”的改变会触发PersistentStorage,将新的改变写入本地磁盘。后续启动应用:
  • 后续启动应用:

    1. 执行PersistentStorage.persistProp(‘aProp’, 47),在首先查询在PersistentStorage本地文件查询“aProp”属性,成功查询到。
    2. 将在PersistentStorage查询到的值写入AppStorage中。
    3. 在Index组件里,@StorageLink绑定的“aProp”为PersistentStorage写入AppStorage中的值,即为上一次退出应用存入的值。
在PersistentStorage之前访问AppStorage中的属性

该示例为反例。在调用PersistentStorage.persistProp或者persistProps之前使用接口访问AppStorage中的属性是错误的,因为这样的调用顺序会丢失上一次应用程序运行中的属性值:

1
2
let aProp = AppStorage.setOrCreate('aProp', 47);
PersistentStorage.persistProp('aProp', 48);

应用在非首次运行时,先执行AppStorage.setOrCreate(‘aProp’, 47):属性“aProp”在AppStorage中创建,其类型为number,其值设置为指定的默认值47。“aProp”是持久化的属性,所以会被写回PersistentStorage磁盘中,PersistentStorage存储的上次退出应用的值丢失。

PersistentStorage.persistProp(‘aProp’, 48):在PersistentStorage中查找到“aProp”,值为刚刚使用AppStorage接口写入的47。

在PersistentStorage之后访问AppStorage中的属性

开发者可以先判断是否需要覆盖上一次保存在PersistentStorage中的值,如果需要覆盖,再调用AppStorage的接口进行修改,如果不需要覆盖,则不调用AppStorage的接口。

1
2
3
4
5
PersistentStorage.persistProp('aProp', 48);
if (AppStorage.get('aProp') > 50) {
// 如果PersistentStorage存储的值超过50,设置为47
AppStorage.setOrCreate('aProp',47);
}

示例代码在读取PersistentStorage储存的数据后判断“aProp”的值是否大于50,如果大于50的话使用AppStorage的接口设置为47。

支持联合类型

PersistentStorage支持联合类型和undefined和null,在下面的示例中,使用persistProp方法初始化”P”为undefined。通过@StorageLink(“P”)绑定变量p,类型为number | undefined | null,点击Button改变P的值,视图会随之刷新。且P的值被持久化存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
PersistentStorage.persistProp("P", undefined);

@Entry
@Component
struct TestCase6 {
@StorageLink("P") p: number | undefined | null = 10;

build() {
Row() {
Column() {
Text(this.p + "")
.fontSize(50)
.fontWeight(FontWeight.Bold)
Button("changeToNumber").onClick(() => {
this.p = 10;
})
Button("changeTo undefined").onClick(() => {
this.p = undefined;
})
Button("changeTo null").onClick(() => {
this.p = null;
})
}
.width('100%')
}
.height('100%')
}
}
装饰Date类型变量

在下面的示例中,@StorageLink装饰的persistedDate类型为Date,点击Button改变persistedDate的值,视图会随之刷新。且persistedDate的值被持久化存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
PersistentStorage.persistProp("persistedDate", new Date());

@Entry
@Component
struct PersistedDate {
@StorageLink("persistedDate") persistedDate: Date = new Date();

updateDate() {
this.persistedDate = new Date();
}

build() {
List() {
ListItem() {
Column() {
Text(`Persisted Date is ${this.persistedDate.toString()}`)
.margin(20)

Text(`Persisted Date month is ${this.persistedDate.getMonth()}`)
.margin(20)

Text(`Persisted Date day is ${this.persistedDate.getDay()}`)
.margin(20)

Text(`Persisted Date time is ${this.persistedDate.toLocaleTimeString()}`)
.margin(20)

Button() {
Text('Update Date')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
}
.type(ButtonType.Capsule)
.margin({
top: 20
})
.backgroundColor('#0D9FFB')
.width('60%')
.height('5%')
.onClick(() => {
this.updateDate();
})

}.width('100%')
}
}
}
}
装饰Map类型变量

在下面的示例中,@StorageLink装饰的persistedMapString类型为Map<number, string>,点击Button改变persistedMapString的值,视图会随之刷新。且persistedMapString的值被持久化存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
PersistentStorage.persistProp("persistedMapString", new Map<number, string>([]));

@Entry
@Component
struct PersistedMap {
@StorageLink("persistedMapString") persistedMapString: Map<number, string> = new Map<number, string>([]);

persistMapString() {
this.persistedMapString = new Map<number, string>([[3, "one"], [6, "two"], [9, "three"]]);
}

build() {
List() {
ListItem() {
Column() {
Text(`Persisted Map String is `)
.margin(20)
ForEach(Array.from(this.persistedMapString.entries()), (item: [number, string]) => {
Text(`${item[0]} ${item[1]}`)
})

Button() {
Text('Persist Map String')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
}
.type(ButtonType.Capsule)
.margin({
top: 20
})
.backgroundColor('#0D9FFB')
.width('60%')
.height('5%')
.onClick(() => {
this.persistMapString();
})

}.width('100%')
}
}
}
}
装饰Set类型变量

在下面的示例中,@StorageLink装饰的persistedSet类型为Set,点击Button改变persistedSet的值,视图会随之刷新。且persistedSet的值被持久化存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
PersistentStorage.persistProp("persistedSet", new Set<number>([]));

@Entry
@Component
struct PersistedSet {
@StorageLink("persistedSet") persistedSet: Set<number> = new Set<number>([]);

persistSet() {
this.persistedSet = new Set<number>([33, 1, 3]);
}

clearSet() {
this.persistedSet.clear();
}

build() {
List() {
ListItem() {
Column() {
Text(`Persisted Set is `)
.margin(20)
ForEach(Array.from(this.persistedSet.entries()), (item: [number, string]) => {
Text(`${item[1]}`)
})

Button() {
Text('Persist Set')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
}
.type(ButtonType.Capsule)
.margin({
top: 20
})
.backgroundColor('#0D9FFB')
.width('60%')
.height('5%')
.onClick(() => {
this.persistSet();
})

Button() {
Text('Persist Clear')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
}
.type(ButtonType.Capsule)
.margin({
top: 20
})
.backgroundColor('#0D9FFB')
.width('60%')
.height('5%')
.onClick(() => {
this.clearSet();
})

}
.width('100%')
}
}
}
}

Environment:设备环境查询

开发者如果需要应用程序运行的设备的环境参数,以此来作出不同的场景判断,比如多语言,暗黑模式等,需要用到Environment设备环境查询。

Environment是ArkUI框架在应用程序启动时创建的单例对象。它为AppStorage提供了一系列描述应用程序运行状态的属性。Environment的所有属性都是不可变的(即应用不可写入),所有的属性都是简单类型。

Environment内置参数
数据类型 描述
accessibilityEnabled boolean 获取无障碍屏幕读取是否启用。
colorMode ColorMode 色彩模型类型:选项为ColorMode.LIGHT: 浅色,ColorMode.DARK: 深色。
fontScale number 字体大小比例,范围: [0.85, 1.45]。
fontWeightScale number 字体粗细程度,范围: [0.6, 1.6]。
layoutDirection LayoutDirection 布局方向类型:包括LayoutDirection.LTR: 从左到右,LayoutDirection.RTL: 从右到左。
languageCode string 当前系统语言值,取值必须为小写字母, 例如zh。
使用场景
从UI中访问Environment参数
  • 从UI中访问Environment参数

    1
    2
    // 将设备的语言code存入AppStorage,默认值为en
    Environment.envProp('languageCode', 'en');
  • 可以使用@StorageProp链接到Component中。

    1
    @StorageProp('languageCode') lang : string = 'en';

设备环境到Component的更新链:Environment –> AppStorage –>Component。

说明:

@StorageProp关联的环境参数可以在本地更改,但不能同步回AppStorage中,因为应用对环境变量参数是不可写的,只能在Environment中查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 将设备languageCode存入AppStorage中
Environment.envProp('languageCode', 'en');

@Entry
@Component
struct Index {
@StorageProp('languageCode') languageCode: string = 'en';

build() {
Row() {
Column() {
// 输出当前设备的languageCode
Text(this.languageCode)
}
}
}
}
应用逻辑使用Environment
1
2
3
4
5
6
7
8
9
10
// 使用Environment.EnvProp将设备运行languageCode存入AppStorage中;
Environment.envProp('languageCode', 'en');
// 从AppStorage获取单向绑定的languageCode的变量
const lang: SubscribedAbstractProperty<string> = AppStorage.prop('languageCode');

if (lang.get() === 'zh') {
console.info('你好');
} else {
console.info('Hello!');
}
限制条件

Environment和UIContext相关联,需要在UIContext明确的时候才可以调用。可以通过在runScopedTask里明确上下文。如果没有在UIContext明确的地方调用,将导致无法查询到设备环境数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// EntryAbility.ets
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';

export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage) {
windowStage.loadContent('pages/Index');
let window = windowStage.getMainWindow()
window.then(window => {
let uicontext = window.getUIContext()
uicontext.runScopedTask(() => {
Environment.envProp('languageCode', 'en');
})
})
}
}