Apin

blogs

HarmonyOS开发学习

  1. 开发工具
    1. DevEco Studio
    2. ArkUI 开发框架
  2. 开发基础知识
    1. UI 框架
    2. 应用模型
  3. ArkTS 开发
    1. Typescript 入门
      1. 基础类型
      2. 条件语句
      3. 函数
      4. 模块
      5. 迭代器
    2. ArkTS 开发实践
      1. 声明式 UI 基本概念
      2. 自定义组件的构成
      3. 配置属性与布局
      4. 改变组件状态
      5. 循环渲染列表数据
  4. 应用程序入口 UIAbility
    1. UIAbility 概述
    2. 页面跳转和数据传递
      1. 页面跳转和参数接收
      2. 页面返回和参数接收
    3. UIAbility 生命周期
    4. UIAbility 启动模式
  5. 组件详解
    1. 常用基础组件
      1. Text
      2. Image
      3. TextInput
      4. Button
      5. LoadingProgress
      6. $r(‘x.x.x’)
    2. Column&Row 组件
      1. 概念介绍
        1. 布局容器概念
        2. 主轴和交叉轴概念
      2. 属性介绍
        1. 主轴方向的对齐
        2. 交叉轴方向的对齐
      3. 接口介绍
    3. List&Grid 组件
      1. List 组件
        1. ForEach 渲染列表
        2. 列表分割线
        3. 滚动事件监听
        4. 排列方向
      2. Grid 组件
        1. ForEach 渲染网格布局
      3. 列表性能优化
        1. 使用数据懒加载
        2. 设置 list 组件的宽高
    4. Tabs 组件
      1. Tabs 简单使用
      2. TabBar 布局模式
      3. TabBar 位置和排列方向
      4. 自定义 TabBar 样式
  6. 组件进阶
    1. 管理组件状态
      1. @State
      2. @Prop
      3. @Link
      4. @Provide&@Consume
    2. Video 组件
      1. 参数与属性
      2. 回调事件
      3. 自定义控制器
    3. 添加弹窗
      1. 警告弹窗
      2. 文本选择弹窗
      3. 日期选择弹窗
      4. 自定义弹窗
    4. 属性动画
      1. 创建
      2. 参数调整
      3. 关闭
    5. Web 组件
      1. 加载网页
      2. 网页缩放
      3. 回调事件
      4. JavaScript 交互
      5. 页面导航
      6. 调试网络应用
      7. 发起 HTTP 请求
  7. 数据管理
    1. 首选项
    2. 常用接口
  8. 通知&提醒
    1. 应用通知消息
      1. 形式与结构
      2. 通知管理
        1. 创建通知
        2. 更新通知
        3. 移除通知
      3. 按钮&行为意图
        1. 按钮
        2. 添加行为意图
      4. 通知 slot
      5. 通知组
    2. 后台代理提醒
  9. 三方库
    1. 常用三方库
    2. @ohos/lottie
      1. 安装与卸载
      2. 库的使用
  10. 云开发
    1. 工程模板
    2. 工程结构
    3. 工程创建与配置

HarmonyOS 是一款面向万物互联时代的、全新的分布式操作系统;有三大特性:

  • 硬件互助,资源共享;
  • 一次开发,多端部署;
  • 统一OS,弹性部署;

HarmonyOS 整体遵从分层设计,在多设备部署场景下,支持根据实际需求裁剪某些非必要的子系统或功能/模块:


开发工具


DevEco Studio

模块级(Ohos)结构目录:

  • AppScope:存放整个应用公共的信息与资源;

  • Entry:默认的初始模块;

    子目录 描述
    ets 存放编写的代码文件
    configuration 存放相应模块的配置文件
    resource 对应模块内的公共资源
  • configuration:存放工程应用级的配置文件;


ArkUI 开发框架

下图描述了 ArkUI 开发框架的整体架构:

其中,基于 TS 扩展的声明式 UI 范式中所用的语言就是 ArkTS;

例如,UI 界面会显示两段文本和一个按钮,当开发者点击按钮时,文本内容会从 “Hello World” 变为 “Hello ArkUI”:

  • 装饰器

    用来装饰类、结构体、方法以及变量,赋予其特殊的含义,如上述示例中 @Entry、@Component、@State 都是装饰器;

    具体而言,@Component 表示这是个自定义组件;@Entry 则表示这是个入口组件;@State 表示组件中的状态变量,此状态变化会引起 UI 变更;

  • 自定义组件

    可复用的 UI 单元,可组合其它组件,如上述被 @Component 装饰的 struct Hello;

  • UI 描述

    声明式的方式来描述 UI 的结构,如上述 build() 方法内部的代码块;

  • 内置组件

    框架中默认内置的基础和布局组件,可直接被开发者调用,比如示例中的 Column、Text、Divider、Button;

  • 事件方法

    用于添加组件对事件的响应逻辑,统一通过事件方法进行设置,如跟随在 Button 后面的 onClick();

  • 属性方法

    用于组件属性的配置,统一通过属性方法进行设置,如 fontSize()、width()、height()、color() 等,可通过链式调用的方式设置多项属性;

从 UI 框架的需求角度,定义了各种装饰器、自定义组件和UI描述机制,再配合 UI 开发框架中的 UI 内置组件、事件方法、属性方法等共同构成了应用开发的主体;

在应用开发中,除了 UI 的结构化描述之外,还有一个重要的方面:状态管理。例如,用 @State 装饰过的变量 myText 包含了一个基础的状态管理机制,即 myText 的值的变化会自动触发相应的 UI 变更(Text组件);

从数据的传递形式来看,可分为只读的单向传递可变更的双向传递


开发基础知识

在开始之前,需要了解有关 HarmonyOS 应用的一些基本概念:UI 框架的简单说明、应用模型的基本概念;


UI 框架

HarmonyOS 提供了一套 UI 开发框架,即方舟开发框架(ArkUI 框架)。方舟开发框架可为开发者提供应用 UI 开发所必需的能力,比如多种组件、布局计算、动画能力、UI交互、绘制等。

方舟开发框架针对不同目的和技术背景的开发者提供了两种开发范式,分别是基于 ArkTS 的声明式开发范式和兼容 JS 的类 Web 开发范式。以下是两种开发范式的简单对比:

开发范式名称 语言生态 UI更新方式 适用场景 适用人群
声明式开发范式 ArkTS 语言 数据驱动更新 复杂度较大、团队合作度较高的程序 移动系统应用开发人员、系统应用开发人员
类 Web 开发范式 JS 语言 数据驱动更新 界面较为简单的程序应用和卡片 Web 前端开发人员

应用模型

应用模型是 HarmonyOS 为开发者提供的应用程序所需能力的抽象提炼,它提供了应用程序必备的组件和运行机制。有了应用模型,开发者可以基于一套统一的模型进行应用开发,使应用开发更简单、高效。

随着系统的演进发展,HarmonyOS 先后提供了两种应用模型:

  • FA(Feature Ability)模型:HarmonyOS API 7 开始支持的模型,已经不再主推;
  • Stage 模型:HarmonyOS API 9 开始新增的模型,是目前主推且会长期演进的模型。在该模型中,由于提供了AbilityStage、WindowStage等类作为应用组件和Window窗口的“舞台”,因此称这种应用模型为Stage模型;

ArkTS 开发

ArkTS 是 HarmonyOS 优选的主力应用开发语言;

ArkTS 是由 ArkUI 框架提供,用于以声明式开发范式开发界面的语言;

它在 TypeScript 的基础上,匹配 ArkUI 框架,扩展了声明式 UI、状态管理、并发任务等相应的能力,让开发者以更简洁、更自然的方式开发跨端应用;

即,TypeScript 是 JavaScript 的超集,ArkTS 则是 TypeScript 的超集;·


Typescript 入门

TypeScript 是 JavaScript 的一个超集,它扩展了 JavaScript 的语法,通过在 JavaScript 的基础上添加静态类型定义构建而成,是一个开源的编程语言;


基础类型

TypeScript 中定义变量的格式:let 变量名: 类型 = 值;,其中类型是对 JS 的扩展;

类型 描述 举例
布尔值 boolean 赋值为 true 或 false let isDone: boolean = false;
数字 number 所有数字都是浮点数,支持多种进制 let decLiteral: number = 2023;
let binaryLiteral: number = 0b11111100111;
let octalLiteral: number = 0o3747;
let hexLiteral: number = 0x7e7;
字符串 string 文本数据类型 let name: string = "Jacky";
数组 两种定义方法:
1、在元素类型后面接上 [];
2、使用数组泛型,Array<元素类型>;
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];
元组 表示一个已知元素数量和类型的数组,各元素的类型不必相同 let x: [string, number] = ['hello', 10];
枚举 enum 对 JavaScript 标准数据类型的一个补充,可以为一组数值赋予变量名 enum Color {Red, Green, Blue};
let c: Color = Color.Green;
Unknown 使变量跳过编译阶段的检查 let notSure: unknown = 4;
notSure = false;
Void 函数没有返回值 function test(): void { ... }
Null、Undefined undefined 和 null 各自的类型 let u: undefined = undefined;
let n: null = null;
联合类型 表示取值可以为多种类型中的一种 `let myFavoriteNumber: string

条件语句

通过一条或多条语句的执行结果(True 或 False)来决定执行的代码块;

  • if 语句

    1
    2
    3
    4
    5
    6
    7
    8
    var num:number = 2 
    if(num > 0) {
    console.log(num+' 是正数')
    } else if(num < 0) {
    console.log(num+' 是负数')
    } else {
    console.log(num+' 为0')
    }
  • switch…case 语句

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    var grade:string = 'A'; 
    switch(grade) {
    case 'A': {
    console.log('优');
    break;
    }
    case 'B': {
    console.log('良');
    break;
    }
    case 'C': {
    console.log('及格');
    break;
    }
    case 'D': {
    console.log('不及格');
    break;
    }
    default: {
    console.log('非法输入');
    break;
    }
    }

函数

函数需要声明函数的名称、返回类型和参数;

  • 有名函数:给变量设置为number类型;

    1
    2
    3
    function add(x: number, y: number): number {
    return x + y;
    }
  • 匿名函数:给变量设置为number类型;

    1
    2
    3
    let myAdd = function (x: number, y: number): number {
    return x + y;
    };

同时为了确保输入输出的准确性,在以上的示例中为函数添加了类型;

  1. 可选参数:在参数名旁使用(?);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function buildName(firstName: string, lastName?: string) {
    if (lastName)
    return firstName + ' ' + lastName;
    else
    return firstName;
    }

    let result1 = buildName('Bob');
    let result2 = buildName('Bob', 'Adams');
  2. 剩余参数:会被当做个数不限的可选参数,使用省略号(…)进行定义;

    1
    2
    3
    4
    5
    function getEmployeeName(firstName: string, ...restOfName: string[]) {
    return firstName + ' ' + restOfName.join(' ');
    }

    let employeeName = getEmployeeName('Joseph', 'Samuel', 'Lucas', 'MacKinzie');
  3. 箭头函数:它是定义匿名函数的简写语法,用于函数表达式;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let testArrowFun = (num: number) => {
    if (num > 0) {
    console.log(num + ' 是正数');
    } else if (num < 0) {
    console.log(num + ' 是负数');
    } else {
    console.log(num + ' 为0');
    }
    }

    testArrowFun(-1) //输出日志:-1 是负数

    另外,在 (param1, param2) => expression 中,主体部分是一个表达式,而不是代码块,其中的表达式会被作为函数的返回值;

    1
    private arr: string[] = new Array(16).fill('').map((_, index) => `item ${index}`);

TypeScript 支持基于类的面向对象的编程方式,定义类的关键字为 class,后面紧跟类名;

类描述了所创建的对象共同的属性方法,示例:

1
2
3
4
5
6
7
8
9
10
11
12
class Employee extends Person {
private department: string

constructor(name: string, age: number, department: string) {
super(name, age);
this.department = department;
}

public getEmployeeInfo(): string {
return this.getPersonInfo() + ` and work in ${this.department}`;
}
}

TypeScript 中允许使用继承来扩展现有的类,对应的关键字为 extends;


模块

模块(module)可以相互加载,使用指令 export 和 import 来交换功能,从另一个模块调用一个模块的函数;

模块里面的变量、函数和类等在模块外部是不可见的,除非明确地使用 export 导出它们;

  1. 导出

    任何声明(变量、函数、类、类型别名、接口)都能通过 export 关键字来导出;

    例如,导出 NewsData 类:

    1
    2
    3
    4
    5
    6
    7
    export class NewsData {
    ...

    constructor(...) {
    ...
    }
    }
  2. 导入

    可以使用以下 import 形式之一来导入其它模块中的内容;

    1
    import { NewsData } from '../common/bean/NewsData';

迭代器

当一个对象实现了 Symbol.iterator 属性时,我们认为它是可迭代的;

一些内置的类型如 Array、Map、Set、String、Int32Array、Uint32Array 等都具有可迭代性;

for..of 和 for..in 均可迭代一个列表,但是用于迭代的值却不同:

  • for..in 迭代的是对象的
  • for..of 迭代的是对象的
1
2
3
4
5
6
7
8
9
let list = [4, 5, 6];

for (let i in list) {
console.log(i); // "0", "1", "2",
}

for (let i of list) {
console.log(i); // "4", "5", "6"
}

ArkTS 开发实践

这里只做一个整体概念的整理,理解开发基本思想,不实现具体案例;以下是目的效果图:


声明式 UI 基本概念

应用界面是由一个个页面组成;

声明式 UI 构建页面的过程,其实是组合组件的过程,声明式 UI 的思想,主要体现在两个方面:

  • 描述 UI 的呈现结果,而不关心过程;
  • 状态驱动试图更新;

自定义组件的构成

使用 @Entry 和 @Component 装饰的自定义组件会作为页面的入口,在页面加载时首先渲染;

注意:使用 @Entry 装饰的组件只是当前页面的默认入口组件,将首先创建并呈现;默认最先显示的页面则是 Index 页面

1
2
3
@Entry
@Component
struct ToDoList {...}

使用 @Component 装饰的自定义组件,将 ToDoItem 这个自定义组件作为页面的组成部分:

1
2
@Component
struct ToDoItem {...}

在自定义组件内使用 build 方法来进行 UI 描述,可以容纳内置组件和其他自定义组件

  • 如 Column 和 Text 都是内置组件,由 ArkUI 框架提供;

    内置组件包含基础组件容器组件等;

  • ToDoItem 为自定义组件,需要开发者使用 ArkTS 自行声明;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entry
@Component
struct ToDoList {
...
build() {
Column(...) {
Text(...)
ForEach(...{
ToDoItem(...)
},...)
}
...
}
}

配置属性与布局

ArkTS 提供了属性方法用于描述界面的样式:

  • 常量传递:

    1
    Text('Hello World').fontSize(50)
  • 变量传递:使用组件内已经定义的变量

    1
    Text('Hello World').fontSize(this.size)
  • 链式调用:配置多个属性

    1
    Text('Hello World').fontSize(this.size).width(100).height(100)
  • 表达式传递:传入普通表达式以及三目运算表达式

    1
    2
    3
    4
    Text('Hello World')
    .fontSize(this.size)
    .width(this.count + 100)
    .height(this.count % 2 === 0 ? 100 : 200)
  • 内置枚举类型:ArkTS 中提供了内置枚举类型

    1
    2
    3
    4
    5
    6
    Text('Hello World')
    .fontSize(this.size)
    .width(this.count + 100)
    .height(this.count % 2 === 0 ? 100 : 200)
    .fontColor(Color.Red)
    .fontWeight(FontWeight.Bold)

ArkUI 中的布局容器有很多种,在不同的适用场景选择不同的布局容器实现,ArkTS 使用容器组件采用花括号语法,内部放置 UI 描述:

  • 行布局:Row() 元素为横向排列

    1
    2
    3
    4
    5
    Row() {
    Image($r('app.media.ic_default'))
    Text(this.content)
    ...
    }
  • 列布局:Column() 整体从上往下纵向排列

    1
    2
    3
    4
    5
    6
    7
    Column() {
    Text($r('app.string.page_title'))
    ...
    ForEach(this.totalTasks,(item) => {
    TodoItem({content:item})
    },...)
    }

改变组件状态

声明式 UI 的特点就是 UI 是随数据更改而自动刷新的;

这里定义了一个类型为 boolean 的变量 isComplete,其被 @State 装饰后,框架内建立了数据和视图之间的绑定,其值的改变影响 UI 的显示;

1
@State isComplete : boolean = false;

首先,用圆圈和对勾两张图片,分别表示该项是否完成,这部分涉及到内容的切换,需要使用条件渲染:

1
2
3
4
5
6
7
8
9
10
11
12
13
if (this.isComplete) {
Image($r('app.media.ic_ok'))
.objectFit(ImageFit.Contain)
.width($r('app.float.checkbox_width'))
.height($r('app.float.checkbox_width'))
.margin($r('app.float.checkbox_margin'))
} else {
Image($r('app.media.ic_default'))
.objectFit(ImageFit.Contain)
.width($r('app.float.checkbox_width'))
.height($r('app.float.checkbox_width'))
.margin($r('app.float.checkbox_margin'))
}

ArkUI 提供了一种更轻量的 UI 元素复用机制 @Builder,@Builder 所装饰的函数遵循 build() 函数语法规则,开发者可以将重复使用的 UI 元素抽象成一个方法,在 build 方法里调用;

由于两个 Image 的实现具有大量重复代码,ArkTS 提供了 @Builder 装饰器,来修饰一个函数,快速生成布局内容,从而可以避免重复的UI描述内容;

  1. 这里使用 @Bulider 声明了一个 labelIcon 的函数,参数为 url,对应要传给 Image 的图片路径;

    1
    2
    3
    4
    5
    6
    7
    @Builder labelIcon(url) {
    Image(url)
    .objectFit(ImageFit.Contain)
    .width($r('app.float.checkbox_width'))
    .height($r('app.float.checkbox_width'))
    .margin($r('app.float.checkbox_margin'))
    }
  2. 使用时只需要使用 this 关键字访问 @Builder 装饰的函数名,即可快速创建布局:

    1
    2
    3
    4
    5
    if (this.isComplete) {
    this.labelIcon($r('app.media.ic_ok'))
    } else {
    this.labelIcon($r('app.media.ic_default'))
    }

然后,使用了三目运算符给内容的字体增加相应的样式变化:

1
2
3
4
5
6
Text(this.content)
...
.opacity(this.isComplete ? CommonConstants.OPACITY_COMPLETED : CommonConstants.OPACITY_DEFAULT)
//透明度
.decoration({ type: this.isComplete ? TextDecorationType.LineThrough : TextDecorationType.None })
//文字是否有划线

最后,为了实现与用户交互的效果,在组件上添加了 onClick 点击事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
struct ToDoItem {
private content?: string;
@State isComplete : boolean = false;
@Builder labelIcon(icon) {...}
...
build() {
Row() {
if (this.isComplete) {
this.labelIcon($r('app.media.ic_ok'))
} else {
this.labelIcon($r('app.media.ic_default'))
}
...
}
...
.onClick(() => {
this.isComplete= !this.isComplete;
})
}
}

循环渲染列表数据

以上只是完成一个 ToDoItem 组件的开发,当有多条代办数据需要显示在页面时,就需要使用到 ForEach 循环渲染语法;

ForEach 基本使用中,只需要了解要渲染的数据以及要生成的 UI 内容两个部分;

例如,这里要渲染的数组为以上的五条待办事项:

1
2
3
4
5
6
7
total_Tasks:Array<string> = [
'早起晨练',
'准备早餐',
'阅读名著',
'学习ArkTS',
'看剧放松'
]

ToDoItem 这个自定义组件中,每一个 ToDoItem 要显示的文本参数 content 需要外部传入,参数传递使用花括号的形式,用 content 接受数组内的内容项 item:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Entry
@Component
struct ToDoList {
...
build() {
Row() {
Column() {
Text(...)
...
ForEach(this.totalTasks,(item) => {
TodoItem({content:item})
},...)
}
.width('100%')
}
.height('100%')
}
}

应用程序入口 UIAbility


UIAbility 概述

UIAbility 是一种包含用户界面的应用组件,主要用于和用户进行交互;

UIAbility 也是系统调度的单元,为应用提供窗口在其中绘制界面;

每一个 UIAbility 实例,都对应于一个最近任务列表中的任务;

一个应用可以有一个 UIAbility,也可以有多个 UIAbility;

例如,在聊天应用中增加一个“外卖功能”,并将其独立为一个 UIAbility;当用户打开聊天应用的“外卖功能”查看外卖订单详情,此时有新的聊天消息,即可以通过最近任务列表切换回到聊天窗口继续进行聊天对话:

一个 UIAbility 可以对应于多个页面,建议将一个独立的功能模块放到一个 UIAbility 中,以多页面的形式呈现;例如,新闻应用在浏览内容的时候,可以进行多页面的跳转使用;


页面跳转和数据传递

UIAbility 的数据传递包括有 UIAbility 内页面的跳转和数据传递UIAbility 间的数据跳转和数据传递,这里主要讲解 UIAbility 内页面的跳转和数据传递;

在一个应用包含一个 UIAbility 的场景下,可以通过新建多个页面来实现和丰富应用的内容;这会涉及到 UIAbility 内页面的新建以及 UIAbility 内页面的跳转和数据传递;

打开 DevEco Studio,选择一个 Empty Ability 工程模板,创建一个工程,例如命名为 MyApplication:

  • 在 src/main/ets/entryability 目录下,初始会生成一个 UIAbility 文件 EntryAbility.ts;可以在 EntryAbility.ts 文件中根据业务需要实现 UIAbility 的生命周期回调内容;
  • 在 src/main/ets/pages 目录下,会生成一个 Index 页面;这也是基于 UIAbility 实现的应用的入口页面;可以在 Index 页面中根据业务需要实现入口页面的功能;
  • 在 src/main/ets/pages 目录下,右键 New->Page,新建一个 Second 页面,用于实现页面间的跳转和数据传递;

页面间的导航可以通过页面路由 router 模块来实现;通过页面路由模块,可以使用不同的 url 访问不同的页面,包括跳转指定页面、替换当前页面、返回上一页面等;

在使用页面路由之前,需要先导入 router 模块,如下代码所示:

1
import router from '@ohos.router';

页面跳转和参数接收

以下是页面跳转的几种方式:

  • 方式一:API9 及以上,router.pushUrl() 方法新增了 mode 参数,可以将 mode 参数配置为 router.RouterMode.Single 单实例模式和 router.RouterMode.Standard 多实例模式;

    说明:当页面栈的元素数量较大或者超过 32 时,可以通过调用 router.clear() 方法清除页面栈中的所有历史页面,仅保留当前页面作为栈顶页面;

    1
    2
    3
    4
    5
    6
    router.pushUrl({
    url: 'pages/Second',
    params: {
    src: 'Index页面传来的数据',
    }
    }, router.RouterMode.Single)
  • 方式二:API9 及以上,router.replaceUrl() 方法同样新增了 mode 参数;

    1
    2
    3
    4
    5
    6
    router.replaceUrl({
    url: 'pages/Second',
    params: {
    src: 'Index页面传来的数据',
    }
    }, router.RouterMode.Single)
名称 说明
Standard 多实例模式(默认);
目标页面会被添加到页面栈顶,无论栈中是否存在相同 url 的页面;
Single 单实例模式;
如果目标页面的 url 已经存在于页面栈中,则该 url 页面移动到栈顶;
如果目标页面的 url 在页面栈中不存在相同 url 页面,则按照默认的多实例模式进行跳转;

通过调用 router.getParams() 方法获取 Index 页面传递过来的自定义参数:

1
2
3
4
5
6
7
8
9
import router from '@ohos.router';

@Entry
@Component
struct Second {
@State src: string = (router.getParams() as Record<string, string>)['src'];
// 页面刷新展示
// ...
}

注意:根据编程实际结果,Row 和 Column 布局有居中包裹的特性;


页面返回和参数接收

在 Second 页面中,可以通过调用 router.back() 方法实现返回到上一个页面,或者在调用 router.back() 方法时增加可选的 options 参数(增加 url 参数)返回到指定页面;

  • 返回上一个页面:

    1
    router.back();
  • 返回到指定页面:

    1
    router.back({ url: 'pages/Index' });

注意:调用 router.back() 返回的目标页面需要在页面栈中存在才能正常跳转;

在调用 router.back() 方法之前,可以先调用 router.enableBackPageAlert()router.hideAlertBeforeBackPage() 方法开启或关闭页面返回询问对话框功能;

同时,调用 router.back() 方法返回时,可以根据需要继续增加自定义参数:

1
2
3
4
5
6
7
8
9
10
router.showAlertBeforeBackPage({
message: 'Message Info'
});

router.back({
url: 'pages/Index',
params: {
src: 'Second页面传来的数据',
}
})

由于调用 router.back() 方法,不会新建页面(push 和 replace 都会新建),返回的是原来的页面,在原来页面中 @State 声明的变量不会重复声明也不会触发页面的 aboutToAppear() 生命周期回调,因此无法直接在变量声明以及页面的 aboutToAppear() 生命周期回调中接收和解析 router.back() 传递过来的自定义参数;

可以放在业务需要的位置进行参数解析;例如,在 Index 页面中的 onPageShow() 生命周期回调中进行参数的解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import router from '@ohos.router';
class routerParams {
src:string
constructor(str:string) {
this.src = str
}
}
@Entry
@Component
struct Index {
@State src: string = '';
onPageShow() { //每次页面显示都会被调用
this.src = (router.getParams() as routerParams).src
}
// 页面刷新展示
// ...
}

UIAbility 生命周期

在 UIAbility 的使用过程中,会有多种生命周期状态,应用中的 UIAbility 实例会在其生命周期的不同状态之间转换;同时,UIAbility 类提供了很多回调,通过这些回调可以知晓当前 UIAbility 的某个状态已经发生改变;

为了实现多设备形态上的裁剪和多窗口的可扩展性,系统对组件管理窗口管理进行了解耦;UIAbility 的生命周期包括 Create、Foreground、Background、Destroy 四个状态,WindowStageCreate 和 WindowStageDestroy 为窗口管理器(WindowStage)在 UIAbility 中管理 UI 界面功能的两个生命周期回调,从而实现 UIAbility 与窗口之间的弱耦合:

  • Create 状态,在 UIAbility 实例创建时触发,系统会调用 onCreate 回调;可以在 onCreate 回调中进行相关初始化操作:

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

    export default class EntryAbility extends UIAbility {
    onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
    // 应用初始化
    // ...
    }
    // ...
    }

    例如,用户打开电池管理应用,在应用加载过程中,在UI页面可见之前,可以在 onCreate 回调中读取当前系统的电量情况,用于后续的 UI 页面展示;

  • 每一个 UIAbility 实例都对应一个 WindowStage 实例,用于管理窗口相关的内容,例如,获焦/失焦、可见/不可见;

    可以在 onWindowStageCreate 回调中,设置 UI 页面加载、设置 WindowStage 的事件订阅;

    在 onWindowStageCreate(windowStage)中通过 loadContent 接口设置应用要加载的页面;

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

    export default class EntryAbility extends UIAbility {
    // ...

    onWindowStageCreate(windowStage: window.WindowStage) {
    // 设置UI页面加载
    // 设置WindowStage的事件订阅(获焦/失焦、可见/不可见)
    // ...

    windowStage.loadContent('pages/Index', (err, data) => {
    // ...
    });
    }
    // ...
    }

    例如,用户正在打游戏的时候收到一个消息通知,以弹窗的形式显示在游戏应用上方;此时,游戏应用就从获焦切换到了失焦状态,消息应用切换到了获焦状态;对于消息应用,在 onWindowStageCreate 回调中,会触发获焦的事件回调,可以进行设置消息应用的背景颜色、高亮等操作;

  • ForegroundBackground 状态,分别在 UIAbility 切换至前台或后台时触发,即 onForeground 回调和 onBackground 回调;

    1. onForeground 回调,在 UIAbility 的 UI 页面可见之前,即 UIAbility 切换至前台时触发;可以在 onForeground 回调中申请系统需要的资源,或者重新申请在 onBackground 中释放的资源;

    2. onBackground 回调,在 UIAbility 的 UI 页面完全不可见之后,即 UIAbility 切换至后台时候触发;可以在 onBackground 回调中释放 UI 页面不可见时无用的资源,或者在此回调中执行较为耗时的操作,例如状态保存等;

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

    export default class EntryAbility extends UIAbility {
    // ...

    onForeground() {
    // 申请系统需要的资源,或者重新申请在onBackground中释放的资源
    // ...
    }

    onBackground() {
    // 释放UI页面不可见时无用的资源,或者在此回调中执行较为耗时的操作
    // 例如状态保存等
    // ...
    }
    }

    例如,用户打开地图应用查看当前地理位置的时候,假设地图应用已获得用户的定位权限授权;在 UI 页面显示之前,可以在 onForeground 回调中打开定位功能,从而获取到当前的位置信息;当地图应用切换到后台状态,可以在 onBackground 回调中停止定位功能,以节省系统的资源消耗;

  • 对应于 onWindowStageCreate 回调,在 UIAbility 实例销毁之前,则会先进入 onWindowStageDestroy 回调,可以在该回调中释放 UI 页面资源;

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

    export default class EntryAbility extends UIAbility {
    // ...

    onWindowStageDestroy() {
    // 释放UI页面资源
    // ...
    }
    }
  • Destroy 状态,在 UIAbility 销毁时触发;可以在 onDestroy 回调中进行系统资源的释放、数据的保存等操作;

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

    export default class EntryAbility extends UIAbility {
    // ...

    onDestroy() {
    // 系统资源的释放、数据的保存等
    // ...
    }
    }

UIAbility 启动模式

  • 对于浏览器或者新闻等应用,用户在打开该应用,并浏览访问相关内容后,回到桌面,再次打开该应用,显示的仍然是用户当前访问的界面;

  • 对于应用的分屏操作,用户希望使用两个不同应用之间进行分屏,也希望能使用同一个应用进行分屏;

  • 对于文档应用,用户从文档应用中打开一个文档内容,回到文档应用,继续打开同一个文档,希望打开的还是同一个文档内容;

基于以上场景的考虑,UIAbility 当前支持 singleton(单实例模式)、multiton(多实例模式)和 specified(指定实例模式)3 种启动模式;

对启动模式的详细说明如下:

  • singleton(单实例模式,默认):

    每次调用 startAbility() 方法时,如果应用进程中该类型的 UIAbility 实例已经存在,则复用系统中的 UIAbility 实例,使系统中只存在唯一该 UIAbility 实例,也即任务列表中唯一;

    由于未创建一个新的 UIAbility 实例,因此只会进入 onNewWant() 回调,而不会进入 onCreate()onWindowStageCreate() 生命周期回调;

    在 module.json5 文件中的 launchType 字段配置为 singleton:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    "module": {
    // ...
    "abilities": [
    {
    "launchType": "singleton",
    // ...
    }
    ]
    }
    }
  • multiton(多实例模式):

    每次调用 startAbility() 方法时,都会在应用进程中创建一个新的该类型 UIAbility 实例;

    在 module.json5 配置文件中的 launchType 字段配置为 multiton;

  • specified(指定实例模式):

    针对一些特殊场景使用;例如,文档应用中每次新建文档希望都能新建一个文档实例,重复打开一个已保存的文档希望打开的都是同一个文档实例;

    存在 EntryAbility 和 SpecifiedAbility,SpecifiedAbility 配置为指定实例模式启动,且需要从 EntryAbility 的页面中启动;

    1. 在 SpecifiedAbility 中,将 module.json5 配置文件的 launchType 字段配置为 specified;

    2. 在创建 UIAbility 实例之前,开发者可以为该实例指定一个唯一的字符串 Key,这样在调用 startAbility() 方法时,应用就可以根据指定的 Key 来识别响应请求的 UIAbility 实例;

      在 EntryAbility 中,调用 startAbility() 方法时,可以在 want 参数中增加一个自定义参数,例如 instanceKey,以此来区分不同的 UIAbility 实例;

      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
      // gan 果然根本没办法运行起来 @ohos.base甚至是openHarmony的API Context怎么获取也不清楚 吐了 抽象教程
      // 在启动指定实例模式的UIAbility时,给每一个UIAbility实例配置一个独立的Key标识
      // 例如在文档使用场景中,可以用文档路径作为Key标识
      import common from '@ohos.app.ability.common';
      import Want from '@ohos.app.ability.Want';
      import { BusinessError } from '@ohos.base';

      function getInstance() {
      return 'key';
      }

      let context:common.UIAbilityContext = ...; // context为调用方UIAbility的UIAbilityContext
      let want: Want = {
      deviceId: '', // deviceId为空表示本设备
      bundleName: 'com.example.myapplication',
      abilityName: 'SpecifiedAbility',
      moduleName: 'specified', // moduleName非必选
      parameters: { // 自定义信息
      instanceKey: getInstance(),
      },
      }

      context.startAbility(want).then(() => {
      console.info('Succeeded in starting ability.');
      }).catch((err: BusinessError) => {
      console.error(`Failed to start ability. Code is ${err.code}, message is ${err.message}`);
      })
    3. 由于 SpecifiedAbility 的启动模式被配置为指定实例启动模式,因此在 SpecifiedAbility 启动之前,会先进入对应的 AbilityStage 的 onAcceptWant() 生命周期回调中,以获取该 UIAbility 实例的Key值;然后系统会自动匹配:

      • 如果存在与该 UIAbility 实例匹配的 Key,则会启动与之绑定的 UIAbility 实例,并进入该 UIAbility 实例的 onNewWant() 回调函数;
      • 否则会创建一个新的 UIAbility 实例,并进入该 UIAbility 实例的 onCreate() 回调函数和 onWindowStageCreate() 回调函数;

      示例代码中,通过实现 onAcceptWant() 生命周期回调函数,解析传入的 want 参数,获取自定义参数 instanceKey;业务逻辑会根据这个参数返回一个字符串 Key,用于标识当前 UIAbility 实例;如果返回的 Key 已经对应一个已启动的 UIAbility 实例,系统会将该 UIAbility 实例拉回前台并获焦,而不会创建新的实例;如果返回的 Key 没有对应已启动的 UIAbility 实例,则系统会创建新的 UIAbility 实例并启动;

      注意:DevEco Studio 默认工程中未自动生成 AbilityStage,需先创建 AbilityStage 文件;

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      import AbilityStage from '@ohos.app.ability.AbilityStage';
      import Want from '@ohos.app.ability.Want';

      export default class MyAbilityStage extends AbilityStage {
      onAcceptWant(want: Want): string {
      // 在被调用方的AbilityStage中,针对启动模式为specified的UIAbility返回一个UIAbility实例对应的一个Key值
      // 当前示例指的是module1 Module的SpecifiedAbility
      if (want.abilityName === 'SpecifiedAbility') {
      // 返回的字符串Key标识为自定义拼接的字符串内容
      if (want.parameters) {
      return `SpecifiedAbilityInstance_${want.parameters.instanceKey}`;
      }
      }
      return '';
      }
      }

组件详解


常用基础组件

组件(Component)是界面搭建与显示的最小单位,HarmonyOS ArkUI 声明式开发范式为开发者提供了丰富多样的 UI 组件;

组件根据功能可以分为以下五大类:基础组件容器组件媒体组件绘制组件画布组件

其中,基础组件是视图层的基本组成单元,包括 Text、Image、TextInput、Button、LoadingProgress 等;


Text

用于在界面上展示一段文本信息,可以包含子组件 Span;

  • 文本样式

    针对包含文本元素的组件,例如 Text、Span、Button、TextInput 等,都可设置文本样式;

    名称 参数类型 描述
    fontColor ResourceColor 设置文本颜色
    fontSize Length、Resource 设置文本尺寸,Length 为 number 类型时,使用 fp 单位
    fontStyle FontStyle 设置文本的字体样式;默认值:FontStyle.Normal
    fontWeight number、FontWeight、string 设置文本的字体粗细;默认值:FontWeight.Normal
    number 类型取值 [100, 900],取值间隔为 100,默认 400
    string 类型支持 “400” 以及 “bold”、“bolder”、“lighter”、“regular”、“medium”
    fontFamily string、Resource 设置文本的字体列表;使用多个字体,使用“,”进行分割,优先级按顺序生效
  • 设置文本对齐方式

    textAlign 参数类型为 TextAlign,定义了以下几种类型:TextAlign.Start、TextAlign.Center、TextAlign.End;

  • 设置文本超长显示

    当文本内容超出 Text 组件范围时,可以使用 textOverflow 设置文本截取方式,需配合 maxLines 使用;

    例如,将 textOverflow 设置为 Ellipsis,它将显示不下的文本用 “…” 表示:

    1
    2
    3
    4
    5
    Text('This is the text content of Text Component This is the text content of Text Component')
    .fontSize(16)
    .maxLines(1)
    .textOverflow({overflow:TextOverflow.Ellipsis})
    .backgroundColor(0xE6F2FD)
  • 设置文本装饰线

    decoration 包含 typecolor 两个参数,其中 type 用于设置装饰线样式,参数类型为 TextDecorationType,color 为可选参数;

    TextDecorationTyp 包含以下几种类型:None、Overline、LineThrough、Underline;

    例如,设置黑色下划线:

    1
    2
    3
    4
    Text('HarmonyOS')
    .fontSize(20)
    .decoration({ type: TextDecorationType.Underline, color: Color.Black })
    .backgroundColor(0xE6F2FD)

Image

用于渲染展示图片,需设置图片地址、宽和高:

1
2
3
4
5
Image($r("app.media.image2"))
//.objectFit(ImageFit.Cover)
//.backgroundColor(0xCCCCCC)
.width(100)
.height(100)
  • 设置缩放类型

    可以使用 objectFit 属性设置图片的缩放类型,objectFit 的参数类型为 ImageFit;

    类型 描述
    Contain 保持宽高比进行缩小或者放大,使得图片完全显示在显示边界内
    Cover(默认值) 保持宽高比进行缩小或者放大,使得图片两边都大于或等于显示边界
    Auto 自适应显示
    Fill 不保持宽高比进行放大缩小,使得图片充满显示边界
    ScaleDown 保持宽高比显示,图片缩小或者保持不变
    None 保持原有尺寸显示
  • 加载网络图片

    Image 组件支持加载网络图片,将图片地址换成网络图片地址进行加载;

    1
    Image('https://www.example.com/xxx.png')

    为了成功加载网络图片,需要先在 module.json5 文件中申明网络访问权限;HarmonyOS 提供了一种访问控制机制即应用权限,用来保证这些数据或功能不会被不当或恶意使用;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    "module" : {
    "requestPermissions":[
    {
    "name": "ohos.permission.INTERNET"
    }
    ]
    }
    }

TextInput

用于输入单行文本,响应输入事件,支持文本样式设置:

1
2
3
4
5
6
TextInput()
.fontColor(Color.Blue)
.fontSize(20)
.fontStyle(FontStyle.Italic)
.fontWeight(FontWeight.Bold)
.fontFamily('Arial')
  • 设置输入提示文本

    使用 placeholder 属性设置文本,并可以使用 placeholderColor 和 placeholderFont 分别设置提示文本的颜色和样式:

    1
    2
    3
    TextInput({ placeholder: '请输入帐号' })
    .placeholderColor(0x999999)
    .placeholderFont({ size: 20, weight: FontWeight.Medium, family: 'cursive', style: FontStyle.Italic })
  • 设置输入类型

    使用 type 属性来设置输入框类型:

    1
    2
    TextInput({ placeholder: '请输入密码' })
    .type(InputType.Password)

    type 的参数类型为 InputType,包含以下几种输入类型:

    类型 说明
    Normal 基本输入模式;支持输入数字、字母、下划线、空格、特殊字符
    Password 密码输入模式
    Email e-mail 地址输入模式
    Number 纯数字输入模式
  • 设置光标位置

    可以使用 TextInputController 动态设置光位置;

    例如,使用 TextInputController 的 caretPosition() 方法,将光标移动到了第二个字符后:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Entry
    @Component
    struct TextInputDemo {
    controller: TextInputController = new TextInputController()

    build() {
    Column() {
    TextInput({ controller: this.controller })
    Button('设置光标位置')
    .onClick(() => {
    this.controller.caretPosition(2)
    })
    }
    .height('100%')
    .backgroundColor(0xE6F2FD)
    }
    }
  • 获取输入文本

    给 TextInput 设置 onChange 事件,输入文本发生变化时触发回调;

    例如,value 为实时获取用户输入的文本信息:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Entry
    @Component
    struct TextInputDemo {
    @State text: string = ''

    build() {
    Column() {
    TextInput({ placeholder: '请输入账号' })
    .caretColor(Color.Blue)
    .onChange((value: string) => {
    this.text = value
    })
    Text(this.text)
    }
    .alignItems(HorizontalAlign.Center)
    .padding(12)
    .backgroundColor(0xE6F2FD)
    }
    }

Button

用于响应点击操作,可以包含子组件:

1
2
3
4
5
6
Button('登录', { type: ButtonType.Capsule, stateEffect: true })
.width('90%')
.height(40)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.backgroundColor('#007DFF')

其中,ButtonType.Capsule 表示胶囊形按钮;stateEffect 用于设置按钮按下时是否开启切换效果,默认为 true;

  • 设置按钮样式

    用于定义按钮样式;

    样式 说明
    Capsule 胶囊型按钮(圆角默认为高度的一半)
    Circle 圆形按钮
    Normal 普通按钮(默认不带圆角)
  • 设置按钮点击事件

    可以给 Button 绑定 onClick 事件,每当用户点击 Button 的时候,就会回调里面的代码;

    1
    2
    3
    4
    5
    Button('登录', { type: ButtonType.Capsule, stateEffect: true })
    ...
    .onClick(() => {
    // 处理点击事件逻辑
    })
  • 包含子组件

    Button 组件可以包含子组件,让开发者可以开发出更丰富多样的 Button;

    例如,Button 组件中包含了一个 Image 组件:

    1
    2
    3
    4
    5
    6
    7
    8
    Button({ type: ButtonType.Circle, stateEffect: true }) {
    Image($r('app.media.icon_delete'))
    .width(30)
    .height(30)
    }
    .width(55)
    .height(55)
    .backgroundColor(0x317aff)

LoadingProgress

用于显示加载进展,需要设置颜色和宽高;例如,显示的“正在登录”的进度条状态;

1
2
3
4
LoadingProgress()
.color(Color.Blue)
.height(60)
.width(60)

$r(‘x.x.x’)

即资源引用类型,用于设置组件属性的值;

推荐优先使用 Resource 类型,将资源文件(字符串、图片、音频等)统一存放于 resources 目录下,便于开发者统一维护;同时系统可以根据当前配置加载合适的资源,例如,开发者可以根据屏幕尺寸呈现不同的布局效果,或根据语言设置提供不同的字符串;

例如,直接在代码中写入了字符串和数字这样的硬编码:

1
2
3
4
5
6
Button('登录', { type: ButtonType.Capsule, stateEffect: true })
.width(300)
.height(40)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.backgroundColor('#007DFF')

开发者可以将这些硬编码写到 entry/src/main/resources 下的资源文件中;

  • 在 string.json 中定义 Button 显示的文本:

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "string": [
    {
    "name": "login_text",
    "value": "登录"
    }
    ]
    }
  • 在 float.json 中定义 Button 的宽高和字体大小:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {
    "float": [
    {
    "name": "button_width",
    "value": "300vp"
    },
    {
    "name": "button_height",
    "value": "40vp"
    },
    {
    "name": "login_fontSize",
    "value": "18fp"
    }
    ]
    }
  • 在 color.json 中定义 Button 的背景颜色:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
        {
    "color": [
    {
    "name": "button_color",
    "value": "#1890ff"
    }
    ]
    }

    **最后**,在 Button 组件通过 `$r('app.type.name')` 的形式引用应用资源;

    - app 代表应用内 resources 目录中定义的资源;
    - type 代表资源类型(或资源的存放位置),可以取“color”、“float”、“string”、“plural”、“media”;
    - name 代表资源命名,由开发者定义资源时确定;

    ```typescript
    Button($r('app.string.login_text'), { type: ButtonType.Capsule })
    .width($r('app.float.button_width'))
    .height($r('app.float.button_height'))
    .fontSize($r('app.float.login_fontSize'))
    .backgroundColor($r('app.color.button_color'))

Column&Row 组件

容器组件是一种比较特殊的组件,它可以包含其他的组件,而且按照一定的规律布局,帮助开发者生成精美的页面;容器组件除了放置基础组件外,也可以放置容器组件,通过多层布局的嵌套,可以布局出更丰富的页面;

ArkTS 提供了丰富的容器组件来布局页面,本文将以构建登录页面为例,介绍 Column 和 Row 组件的属性与使用;


概念介绍


布局容器概念

线性布局容器表示按照垂直方向或者水平方向排列子组件的容器,ArkTS 提供了 Column 和 Row 容器来实现线性布局;

  • Column 表示沿垂直方向布局的容器;
  • Row 表示沿水平方向布局的容器;

主轴和交叉轴概念

在布局容器中,默认存在两根轴,分别是主轴和交叉轴,这两个轴始终是相互垂直的;不同的容器中主轴的方向不一样的;

  • 主轴:Column 容器的主轴的方向是垂直方向;Row 容器的主轴的方向是水平方向;
  • 交叉轴:与主轴垂直相交的轴线;

属性介绍

以下将详细讲解 Column 和 Row 容器的两个属性 justifyContent 和 alignItems:

属性名称 描述
justifyContent 设置子组件在主轴方向上的对齐格式
alignItems 设置子组件在交叉轴方向上的对齐格式

主轴方向的对齐

子组件在主轴方向上的对齐使用 justifyContent 属性来设置,其参数类型是 FlexAlign;

1
2
3
4
5
6
Row() {
Text($r(…))
Text($r(…))
}
.justifyContent(FlexAlign.SpaceBetween)
.width('100%')

FlexAlign 定义了以下几种类型:

  • Start:元素在主轴方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐;

  • Center:元素在主轴方向中心对齐,第一个元素与行首的距离以及最后一个元素与行尾距离相同;

  • End:元素在主轴方向尾部对齐,最后一个元素与行尾对齐,其他元素与后一个对齐;

  • SpaceBetween:元素在主轴方向均匀分配弹性元素,相邻元素之间距离相同;且第一个元素与行首对齐,最后一个元素与行尾对齐;

  • SpaceAround:元素在主轴方向均匀分配弹性元素,相邻元素之间距离相同;第一个元素到行首的距离和最后一个元素到行尾的距离是相邻元素之间距离的一半;

  • SpaceEvenly:元素在主轴方向等间距布局,无论是相邻元素还是边界元素到容器的间距都一样;


交叉轴方向的对齐

子组件在交叉轴方向上的对齐方式使用 alignItems 属性来设置;

Column 容器的交叉轴是水平方向,其参数类型为 HorizontalAlign(水平对齐);Row 容器的交叉轴是垂直方向,其参数类型为 VerticalAlign(垂直对齐);

HorizontalAlign 和 VerticalAlign 分别定义了以下几种类型(括号内为 VerticalAlign 的类型):

  • Start(Top):设置子组件在水平方向上按照起始端对齐;

  • Center(默认值):设置子组件在水平方向上居中对齐;

  • End(Bottom):设置子组件在水平方向上按照末端对齐;


接口介绍

容器组件 接口
Column Column(value?:{space?: string | number})
Row Row(value?:{space?: string | number})

Column 和 Row 容器的接口都有一个可选参数 space,表示子组件在主轴方向上的间距;


List&Grid 组件

为了帮助开发者构建包含列表的应用,ArkUI 提供了 List 组件和 Grid 组件;


List 组件

List 是很常用的滚动类容器组件,一般和子组件 ListItem 一起使用,List 列表中的每一个列表项对应一个 ListItem 组件;


ForEach 渲染列表

使用循环渲染(ForEach)遍历数组的方式构建列表,可以减少重复代码:

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 ListDemo {
private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

build() {
Column() {
List({ space: 10 }) {
ForEach(this.arr, (item: number) => {
ListItem() {
Text(`${item}`)
.width('100%')
.height(100)
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(0x007DFF)
}
}, item => item)
}
}
.padding(12)
.height('100%')
.backgroundColor(0xF1F3F5)
}
}

在 ForEach 函数的第三个参数中,item => item 是一个回调函数,也可以称为身份函数或者恒等函数;它指定了如何提取每个元素的键值;在以上代码中,它简单地返回每个元素本身,因此被遍历的数组中的每个元素都会作为其自身的键值;


列表分割线

使用 List 组件的 divider 属性设置分割线(默认不存在),其包含四个参数:

参数 说明
strokeWidth 分割线的线宽
color 分割线的颜色
startMargin 分割线距离列表侧边起始端的距离
endMargin 分割线距离列表侧边结束端的距离

滚动事件监听

List 组件提供了一系列事件方法用来监听列表的滚动,开发者可以根据需要,监听这些事件来做一些操作:

事件方法 说明
onScroll 列表滑动时触发,返回值 scrollOffset 为滑动偏移量,scrollState 为当前滑动状态
onScrollIndex 列表滑动时触发,返回值分别为滑动起始位置索引值与滑动结束位置索引值
onReachStart 列表到达起始位置时触发
onReachEnd 列表到底末尾位置时触发
onScrollStop 列表滑动停止时触发

示例代码如下:

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
List({ space: 10 }) {
ForEach(this.arr, (item) => {
ListItem() {
Text(`${item}`)
...
}
}, item => item)
}
.onScrollIndex((firstIndex: number, lastIndex: number) => {
console.info('first' + firstIndex)
console.info('last' + lastIndex)
})
.onScroll((scrollOffset: number, scrollState: ScrollState) => {
console.info('scrollOffset' + scrollOffset)
console.info('scrollState' + scrollState)
})
.onReachStart(() => {
console.info('onReachStart')
})
.onReachEnd(() => {
console.info('onReachEnd')
})
.onScrollStop(() => {
console.info('onScrollStop')
})

排列方向

使用 List 组件的 listDirection 属性进行设置;

listDirection 参数类型是 Axis,定义了以下两种类型:

  • Vertical(默认值):子组件 ListItem 在 List 容器组件中呈纵向排列;
  • Horizontal:子组件 ListItem 在 List 容器组件中呈横向排列;

例如,Axis.Horizontal;


Grid 组件

Grid 组件为网格容器,是一种网格列表,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局;Grid 组件一般和子组件 GridItem 一起使用,Grid 列表中的每一个条目对应一个 GridItem 组件;


ForEach 渲染网格布局

和 List 组件一样,Grid 组件也可以使用 ForEach 来渲染多个列表项 GridItem;

例如,创建 16 个 GridItem 列表项:

  • 同时设置 columnsTemplate 的值为 ‘1fr 1fr 1fr 1fr’,表示这个网格为 4 列,将 Grid 允许的宽分为 4 等分,每列占 1 份;
  • 并 rowsTemplate 的值为 ‘1fr 1fr 1fr 1fr’,表示这个网格为 4 行,将 Grid 允许的高分为 4 等分,每行占 1 份;

这样就构成了一个 4 行 4 列的网格列表,然后使用 columnsGap 设置列间距为 10vp,使用 rowsGap 设置行间距也为 10vp:

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 GridExample {
// 定义一个长度为16的数组
private arr: string[] = new Array(16).fill('').map((_, index) => `item ${index}`);

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

以上代码中,使用了固定的行数和列数,所以构建出的网格是不可滚动的;

为了通过滚动的方式来显示更多的内容,开发者只需要设置 rowsTemplate 和 columnsTemplate 其中的一个;

例如,将 GridItem 的高度设置为固定值,仅设置 columnsTemplate 属性,不设置 rowsTemplate 属性,就可以实现 Grid 列表的滚动:

1
2
3
4
5
6
7
8
9
10
11
12
13
Grid() {
ForEach(this.arr, (item: string) => {
GridItem() {
Text(item)
.height(100)
...
}
}, item => item)
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
.height(300)

此外,Grid 像 List 一样也可以使用 onScrollIndex 来监听列表的滚动;


列表性能优化

开发者在使用长列表时,如果直接采用循环渲染方式,会一次性加载所有的列表元素,一方面会导致页面启动时间过长,影响用户体验,另一方面也会增加服务器的压力和流量,加重系统负担;推荐通过以下方式来进行列表性能优化;


使用数据懒加载

使用数据懒加载,从数据源中按需迭代加载数据并创建相应组件:

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
class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = []

public totalCount(): number {
return 0
}

public getData(index: number): any {
return undefined
}

registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
console.info('add listener')
this.listeners.push(listener)
}
}

unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
console.info('remove listener')
this.listeners.splice(pos, 1)
}
}

notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded()
})
}

notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index)
})
}

notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index)
})
}

notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index)
})
}

notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to)
})
}
}

class MyDataSource extends BasicDataSource {
private dataArray: string[] = ['item value: 0', 'item value: 1', 'item value: 2']

public totalCount(): number {
return this.dataArray.length
}

public getData(index: number): any {
return this.dataArray[index]
}

public addData(index: number, data: string): void {
this.dataArray.splice(index, 0, data)
this.notifyDataAdd(index)
}

public pushData(data: string): void {
this.dataArray.push(data)
this.notifyDataAdd(this.dataArray.length - 1)
}
}

@Entry
@Component
struct MyComponent {
private data: MyDataSource = new MyDataSource()

build() {
List() {
LazyForEach(this.data, (item: string) => {
ListItem() {
Row() {
Text(item).fontSize(20).margin({ left: 10 })
}
}
.onClick(() => {
this.data.pushData('item value: ' + this.data.totalCount())
})
}, item => item)
}
}
}

上述代码在页面加载时仅初始化加载三个列表元素,之后每点击一次列表元素,将增加一个列表元素;


设置 list 组件的宽高

以下是 Scroll 容器组件嵌套 List 组件加载长列表时的各种情况:

情况 效果
List 没有设置宽高 布局 List 的所有子组件
List 设置宽高 布局 List 显示区域内的子组件
List 使用 ForEach 加载子组件时,无论是否设置 List 的宽高 都会加载所有子组件
List 使用 LazyForEach 加载子组件时,没有设置 List 的宽高 会加载所有子组件
List 使用 LazyForEach 加载子组件时,设置了 List 的宽高 会加载 List 显示区域内的子组件

因此,在以下代码(存在 Scroll)中设置了 List 子组件的宽高:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...  // BasicDataSource 和 MyDataSource

@Entry
@Component
struct MyComponent {
private data: MyDataSource = new MyDataSource()

build() {
Scroll() {
List() {
LazyForEach(this.data, (item: string, index: number) => {
ListItem() {
Text('item value: ' + item + (index + 1)).fontSize(20).margin(10)
}.width('100%')
})
}.width('100%').height(500)
}.backgroundColor(Color.Pink)
}
}

Tabs 组件

ArkUI 开发框架提供了一种页签容器组件 Tabs,开发者通过 Tabs 组件可以很容易的实现内容视图的切换;其形式多种多样,不同的页面设计页签不一样,可以把页签设置在底部、顶部或者侧边;


Tabs 简单使用

Tabs 组件仅可包含子组件 TabContent,每一个页签对应一个内容视图,即 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
@Entry
@Component
struct TabsExample {
private controller: TabsController = new TabsController()

build() {
Column() {
Tabs({ barPosition: BarPosition.Start, controller: this.controller }) {
TabContent() {
Column().width('100%').height('100%').backgroundColor(Color.Green)
}
.tabBar('green')

TabContent() {
Column().width('100%').height('100%').backgroundColor(Color.Blue)
}
.tabBar('blue')

TabContent() {
Column().width('100%').height('100%').backgroundColor(Color.Yellow)
}
.tabBar('yellow')

TabContent() {
Column().width('100%').height('100%').backgroundColor(Color.Pink)
}
.tabBar('pink')
}
.barWidth('100%') // 设置TabBar宽度
.barHeight(60) // 设置TabBar高度
.width('100%') // 设置Tabs组件宽度
.height('100%') // 设置Tabs组件高度
.backgroundColor(0xF5F5F5) // 设置Tabs组件背景颜色
}
.width('100%')
.height('100%')
}
}

以上代码中,Tabs 组件中包含 4 个子组件 TabContent,通过 TabContent 的 tabBar 属性设置 TabBar 的显示内容;使用通用属性 width 和 height 设置了 Tabs 组件的宽高,使用 barWidth 和 barHeight 设置了 TabBar 的宽度和高度;

  • TabContent 组件不支持设置通用宽度属性,其宽度默认撑满 Tabs 父组件;
  • TabContent 组件不支持设置通用高度属性,其高度由 Tabs 父组件高度与 TabBar 组件高度决定;

TabBar 布局模式

因为 Tabs 的布局模式默认是 Fixed 的,所以 Tabs 的页签是不可滑动的;当页签比较多的时候,可能会导致页签显示不全,将布局模式设置为 Scrollable 的话,可以实现页签的滚动;

Tabs 的布局模式有 Fixed(默认)和 Scrollable 两种:

  • BarMode.Fixed:所有 TabBar 平均分配 barWidth 宽度(纵向时平均分配 barHeight 高度),页签不可滚动,效果图如下:

  • BarMode.Scrollable:每一个 TabBar 均使用实际布局宽度,超过总长度后可滑动:

    例如,将 barMode 设置为 BarMode.Scrollable,实现可滚动的页签:

    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 TabsExample {
    private controller: TabsController = new TabsController()

    build() {
    Column() {
    Tabs({ barPosition: BarPosition.Start, controller: this.controller }) {
    TabContent() {
    Column()
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Green)
    }
    .tabBar('green')

    TabContent() {
    Column()
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Blue)
    }
    .tabBar('blue')

    ...

    }
    .barMode(BarMode.Scrollable)
    .barWidth('100%')
    .barHeight(60)
    .width('100%')
    .height('100%')
    }
    }
    }

TabBar 位置和排列方向

使用 Tabs 组件接口中的参数 barPosition 设置页签位置,可以设置为 BarPosition.Start(默认值)和 BarPosition.End;

使用 vertical 属性设置页签的排列方向,当 vertical 的属性值为 false(默认值)时页签横向排列,为 true 时页签纵向排列;

  • BarPosition.Start:

    vertical 属性方法设置为 false 时,页签位于容器顶部:

    1
    Tabs({ barPosition: BarPosition.Start }) {...}.vertical(false).barWidth('100%').barHeight(60)

    vertical 属性方法设置为 true 时,页签位于容器左侧:

    1
    Tabs({ barPosition: BarPosition.Start }) {...}.vertical(true).barWidth(100).barHeight(200)
  • BarPosition.End:

    vertical 属性方法设置为 false 时,页签位于容器底部:

    1
    Tabs({ barPosition: BarPosition.End }) {...}.vertical(false).barWidth('100%').barHeight(60)

    vertical 属性方法设置为 true 时,页签位于容器右侧:

    1
    Tabs({ barPosition: BarPosition.End}) {...}.vertical(true).barWidth(100).barHeight(200)

自定义 TabBar 样式

目标样式如下:

TabContent 的 tabBar 属性除了支持 string 类型,还支持使用 @Builder 装饰器修饰的函数;

开发者可以使用 @Builder 装饰器,构造一个生成自定义 TabBar 样式的函数,实现上面的底部页签效果:

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
@Entry
@Component
struct TabsExample {
@State currentIndex: number = 0;
private tabsController: TabsController = new TabsController();

@Builder TabBuilder(title: string, targetIndex: number, selectedImg: Resource, normalImg: Resource) {
Column() {
Image(this.currentIndex === targetIndex ? selectedImg : normalImg)
.size({ width: 25, height: 25 })
Text(title)
.fontColor(this.currentIndex === targetIndex ? '#1698CE' : '#6B6B6B')
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.currentIndex = targetIndex;
this.tabsController.changeIndex(this.currentIndex);
})
}

build() {
Tabs({ barPosition: BarPosition.End, controller: this.tabsController }) {
TabContent() {
Column().width('100%').height('100%').backgroundColor('#00CB87')
}
.tabBar(this.TabBuilder('首页', 0, $r('app.media.home_selected'), $r('app.media.home_normal')))

TabContent() {
Column().width('100%').height('100%').backgroundColor('#007DFF')
}
.tabBar(this.TabBuilder('我的', 1, $r('app.media.mine_selected'), $r('app.media.mine_normal')))
}
.barWidth('100%')
.barHeight(50)
.onChange((index: number) => {
this.currentIndex = index;
})
}
}
  • 使用 @Builder 修饰 TabBuilder 函数,生成由 Image 和 Text 组成的页签;
  • 同时,给 Tabs 组件设置了 TabsController 控制器,当点击某个页签时,调用 changeIndex 方法进行页签内容切换;
  • 最后,给 Tabs 添加 onChange 事件,当左右滑动内容视图的时候,页签样式也会跟着改变;

组件进阶


管理组件状态

ArkUI 作为一种声明式 UI,具有状态驱动 UI 更新的特点;当用户进行界面交互或有外部事件引起状态改变时,状态的变化会触发组件自动更新;

所以在 ArkUI 中,开发者只需要通过一个变量来记录状态,ArkUI 会根据该变量自动更新界面中受影响的部分;

ArkUI 框架提供了多种管理状态的装饰器来修饰变量,使用这些装饰器修饰的变量即称为状态变量

在组件范围传递的状态管理常见的场景如下:

场景 装饰器
组件内的状态管理 @State
从父组件单向同步状态 @Prop
与父组件双向同步状态 @Link
跨组件层级双向同步状态 @Provide 和 @Consume

在实际应用开发中,应用会根据需要封装数据模型;如果需要观察嵌套类对象属性变化,需要使用 @Observed 和 @ObjectLink 装饰器,因为上述表格中的装饰器只能观察到对象的第一层属性变化;

另外,当状态改变,需要对状态变化进行监听做一些相应的操作时,可以使用 @Watch 装饰器来修饰状态;


@State

当需要在组件内使用状态来控制 UI 的不同呈现方式时,可以使用 @State 装饰器;

例如,当点击子目标列表的其中一项,列表项会展开,当再次点击同一项,列表项会收起;

通过 @State 装饰后,框架内部会建立数据与视图间的绑定,当 isExpanded 状态变化时,目标项会随之展开或收起;

其具体实现只要用 @State 修饰 isExpanded 变量,定义是否展开状态;然后通过条件渲染,实现是否显示进度调整面板和列表项的高度变化;最后,监听列表项的点击事件,在 onClick 回调中改变 isExpanded 状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
export default struct TargetListItem {
@State isExpanded: boolean = false;
...

build() {
...
Column() {
...
if (this.isExpanded) {
Blank()
ProgressEditPanel(...)
}
}
.height(this.isExpanded ? $r('app.float.expanded_item_height') : $r('app.float.list_item_height'))
.onClick(() => {
...
this.isExpanded = !this.isExpanded;
...
})
...
}
}

@Prop

当子组件中的状态依赖从父组件传递而来时,需要使用 @Prop 装饰器,@Prop 修饰的变量可以和其父组件中的状态建立单向同步关系;当父组件中状态变化时,该状态值也会更新至 @Prop 修饰的变量;对 @Prop 修饰的变量的修改不会影响其父组件中的状态;

例如,在目标管理应用中,当点击“编辑”事件发生时,进入编辑模式,显示取消、全选文本和勾选框,同时显示删除按钮;当点击“取消”事件发生时,退出编辑模式,显示“编辑”文本和“添加子目标”按钮;

整个列表是自定义组件 TargetList,顶部是 Text 组件,底部是一个 Button 组件,中间区域是自定义组件 TargetListItem;

  • 对于父组件 TargetList,其顶部显示的文本和底部按钮会随编辑模式的变化而变化,因此父组件拥有编辑模式状态;

  • 对于子组件 TargetListItem,其最右侧是否预留位置和显示勾选框也会随编辑模式变化,因此子组件也拥有编辑模式状态;

@State 修饰的变量不仅是组件内部的状态,也可以作为子组件单向或双向同步的数据源;

首先,在父组件 TargetList 中,用 @State 修饰 isEditMode,定义编辑模式状态;然后利用条件渲染实现根据是否进入编辑模式,显示不同的文本和按钮;同时,在父组件中需要在用户点击时改变状态,触发界面更新;

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
@Component
export default struct TargetList {
@State isEditMode: boolean = false;
...
build() {
Column() {
Row() {
...
if (this.isEditMode) {
Text($r('app.string.cancel_button'))
.onClick(() => {
this.isEditMode = false;
...
})
...
Text($r('app.string.select_all_button'))...
Checkbox()...
} else {
Text($r('app.string.edit_button'))
.onClick(() => {
this.isEditMode = true;
})
...
}
...
}
...
List({ space: CommonConstants.LIST_SPACE }) {
ForEach(this.targetData, (item: TaskItemBean, index: number) => {
ListItem() {
TargetListItem({
isEditMode: this.isEditMode,
...
})
}
}, (item, index) => JSON.stringify(item) + index)
}
...
if (this.isEditMode) {
Button($r('app.string.delete_button'))
} else {
Button($r('app.string.add_task'))
}
}
...
}
}

然后,在子组件 TargetListItem 中,使用 @Prop 修饰子组件的 isEditMode 变量,定义子组件的编辑模式状态;然后同样根据是否进入编辑模式,控制目标项最右侧是否预留位置和显示勾选框;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
export default struct TargetListItem {
@Prop isEditMode: boolean;
...
Column() {
...
}
.padding({
...
right: this.isEditMode ? $r('app.float.list_edit_padding') : $r('app.float.list_padding')
})
...

if (this.isEditMode) {
Row() {
Checkbox()...
}
}
...
}

最后,在父组件中使用子组件时,将父组件的编辑模式状态 this.isEditMode 传递给子组件的编辑模式状态 isEditMode;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
export default struct TargetList {
@State isEditMode: boolean = false;
...
build() {
Column() {
...
List({ space: CommonConstants.LIST_SPACE }) {
ForEach(this.targetData, (item: TaskItemBean, index: number) => {
ListItem() {
TargetListItem({
isEditMode: this.isEditMode,
...
})
}
}, (item, index) => JSON.stringify(item) + index)
}
...
}
...
}
}

使用 @Link 装饰器,相互绑定父子组件状态进行双向同步;

例如,在目标管理应用中,当用户点击同一个目标,目标项会展开或者收起;当用户点击不同的目标项时,除了被点击的目标项展开,同时前一次被点击的目标项会收起;

  • 在子目标列表中,每个列表项都有其位置索引值 index 属性,表示目标项在列表中的位置;
  • 此外,在父组件和每个子组件中都设置 clickIndex 以记录被点击的目标项索引,感知变化,传递变化;

首先,需要在父组件 TargetList 中定义 clickIndex 状态;其中,父组件的 clickIndex 加上 $ 表示传递的是引用,由于双向同步的独特,不使用 this;

1
2
3
4
5
6
7
8
9
10
@Component
export default struct TargetList {
@State clickIndex: number = CommonConstants.DEFAULT_CLICK_INDEX;
...
TargetListItem({
clickIndex: $clickIndex,
...
})
...
}

然后,在子组件 TargetListItem 中用 @Link 装饰器定义 clickIndex,当点击目标项时,clickIndex 更新为当前目标索引值;

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
@Component
export default struct TargetListItem {
@Link @Watch('onClickIndexChanged') clickIndex: number;
@State isExpanded: boolean = false
...

onClickIndexChanged() {
if (this.clickIndex != this.index) {
this.isExpanded = false;
}
}

build() {
...
Column() {
...
}
.onClick(() => {
...
this.clickIndex = this.index;
...
})
...
}
}

给 TargetListItem 中的 clickIndex 状态加上 @Watch("onClickIndexChanged"),表示监听 clickIndex 状态的变化,当 clickIndex 状态变化时,将触发 onClickIndexChanged 回调;


@Provide&@Consume

跨组件层级双向同步状态是指 @Provide 修饰的状态变量自动对提供者组件的所有后代组件可用,后代组件通过使用 @Consume 装饰的变量来获得对提供的状态变量的访问;

使用 @Provide 的好处是开发者不需要多次将变量在组件间传递;


Video 组件

媒体功能是用户最常用的场景之一;

ArkUI 提供的 Video 组件为应用增加基础的视频播放功能;借助 Video 组件,开发者可以实现视频的播放功能并控制其播放状态;

视频支持的规格是:mp4、mkv、webm、TS;


参数与属性

Video 组件的接口表达形式为:

1
Video(value: {src?: string | Resource, currentProgressRate?: number | string |PlaybackSpeed, previewUri?: string |PixelMap | Resource, controller?: VideoController})

其中包含四个可选参数

  • src:表示视频播放源的路径;

    使用网络地址时,如 https,需要在 module.json5 文件中申请网络权限;

    使用本地地址时,可以使用媒体库管理模块 medialibrary 来查询公共媒体库中的视频文件;代码如下:

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

    async queryMediaVideo() {
    let option = {
    // 根据媒体类型检索
    selections: mediaLibrary.FileKey.MEDIA_TYPE + '=?',
    // 媒体类型为视频
    selectionArgs: [mediaLibrary.MediaType.VIDEO.toString()]
    };
    let media = mediaLibrary.getMediaLibrary(getContext(this));
    // 获取资源文件
    const fetchFileResult = await media.getFileAssets(option);
    // 以获取的第一个文件为例获取视频地址
    let fileAsset = await fetchFileResult.getFirstObject();
    this.source = fileAsset.uri
    }

    为了方便功能演示,示例中媒体资源需存放在 resources 下的 rawfile 文件夹里;

  • currentProgressRate:表示视频播放倍速;

    其参数类型为 number,取值支持 0.75,1.0,1.25,1.75,2.0,默认值为 1.0倍速;

  • previewUri:表示视频未播放时的预览图片路径;

  • controller:表示视频控制器;

除了支持组件的尺寸设置、位置设置等通用属性外,Video 组件还支持五个私有属性

名称 参数类型 描述
muted boolean 是否静音;默认值:false
autoPlay boolean 是否自动播放;默认值:false
controls boolean 控制视频播放的控制栏是否显示;默认值:true
objectFit ImageFit 设置视频显示模式;默认值:Cover
loop boolean 是否单个视频循环播放;默认值:false

其中,objectFit 中视频显示模式包括 Contain、Cover、Auto、Fill、ScaleDown、None 6 种模式,默认情况下使用 ImageFit.Cover(保持宽高比进行缩小或者放大,使得图片两边都大于或等于显示边界),其他效果(如自适应显示、保持原有尺寸显示、不保持宽高比进行缩放等)可以根据具体使用场景/设备来进行选择;

选择播放本地视频,视频未播放时的预览图片路径也为本地,代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
export struct VideoPlayer {
private source: string | Resource;
private controller: VideoController;
private previewUris: Resource = $r('app.media.preview');
...

build() {
Column() {
Video({
src: this.source,
previewUri: this.previewUris,
controller: this.controller
})
.controls(false) //不显示控制栏
.autoPlay(false) // 手动点击播放
.loop(false) // 关闭循环播放
...
VideoSlider({ controller: this.controller })
}
}
}

回调事件

Video 组件能够支持常规的点击、触摸等通用事件,同时也支持 onStart、onPause、onFinish、onError 等事件,具体事件的功能描述见下表:

事件名称 功能描述
onStart(event:() => void) 播放时触发该事件
onPause(event:() => void) 暂停时触发该事件
onFinish(event:() => void) 播放结束时触发该事件
onError(event:() => void) 播放失败时触发该事件
onPrepared(callback:(event?: { duration: number }) => void) 视频准备完成时触发该事件,通过 duration 可以获取视频时长,单位为s
onSeeking(callback:(event?: { time: number }) => void) 操作进度条过程时上报时间信息,单位为 s
onSeeked(callback:(event?: { time: number }) => void) 操作进度条完成后,上报播放时间信息,单位为 s
onUpdate(callback:(event?: { time: number }) => void) 播放进度变化时触发该事件,单位为 s,更新时间间隔为 250ms
onFullscreenChange(callback:(event?: { fullscreen: boolean }) => void) 在全屏播放与非全屏播放状态之间切换时触发该事件

以更新事件、准备事件、失败事件以及点击事件为回调,代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Video({ ... })
.onUpdate((event) => {
this.currentTime = event.time;
this.currentStringTime = changeSliderTime(this.currentTime); //更新事件
})
.onPrepared((event) => {
prepared.call(this, event); //准备事件
})
.onError(() => {
prompt.showToast({
duration: COMMON_NUM_DURATION, //播放失败事件
message: MESSAGE
});
...
})

其中,onUpdate 更新事件在播放进度变化时触发,从 event 中可以获取当前播放进度,从而更新进度条显示事件,比如视频播放时间从24秒更新到30秒;


自定义控制器

Video 组件的原生控制器样式相对固定,当我们对页面的布局色调的一致性有所要求,或者在拖动进度条的同时需要显示其百分比进度时,原生控制器就无法满足需要了;

为了实现自定义控制器的进度显示等功能,我们需要通过 Row 容器实现控制器的整体布局,然后借由 Text 组件来显示视频的播放起始时间、进度时间以及视频总时长,最后通过滑动进度条 Slider 组件来实现视频进度条的效果,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
export struct VideoSlider {
...
build() {
Row(...) {
Image(...)
Text(...)
Slider(...)
Text(...)
}
...
}
}

需要强调的是两个 Text 组件显示的时长是由 Slider 组件的 onChange() 回调事件来进行传递的,而 Text 组件的数值与视频播放进度数值 value 则是通过 @Provide 与 @Consume 装饰器进行的数据联动,具体代码步骤及代码如下:

  • 获取/计算视频时长:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    export function prepared(event) {
    this.durationTime = event.duration;
    let second: number = event.duration % COMMON_NUM_MINUTE;
    let min: number = parseInt((event.duration / COMMON_NUM_MINUTE).toString());
    let head = min < COMMON_NUM_DOUBLE ? `${ZERO_STR}${min}` : min;
    let end = second < COMMON_NUM_DOUBLE ? `${ZERO_STR}${second}` : second;
    this.durationStringTime = `${head}${SPLIT}${end}`;
    ...
    };
  • 设置进度条参数及属性:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Slider({
    value: this.currentTime,
    min: 0,
    max: this.durationTime,
    step: 1,
    style: SliderStyle.OutSet
    })
    .blockColor($r('app.color.white'))
    .width(STRING_PERCENT.SLIDER_WITH)
    .trackColor(Color.Gray)
    .selectedColor($r('app.color.white'))
    .showSteps(true)
    .showTips(true)
    .trackThickness(this.isOpacity ? SMALL_TRACK_THICK_NESS : BIG_TRACK_THICK_NESS)
    .onChange((value: number, mode: SliderChangeMode) => {...})
  • 计算当前进度播放时间及添加 onUpdate 回调:

    在播放视频时更新时间进度,为左侧的 Text 组件添加数据联动;

    为 Video 组件添加 onUpdate 事件,在视频播放过程中会不断调用 changeSliderTime 方法获取当前的播放时间并进行计算及单位转化,从而不断刷新进度条的值;

    1
    2
    3
    4
    5
    6
    Video({...})
    ...
    .onUpdate((event) => {
    this.currentTime = event.time;
    this.currentStringTime = changeSliderTime(this.currentTime)
    })
    1
    2
    3
    4
    5
    6
    7
    8
    export function changeSliderTime(value: number): string {
    let second: number = value % COMMON_NUM_MINUTE;
    let min: number = parseInt((value / COMMON_NUM_MINUTE).toString());
    let head = min < COMMON_NUM_DOUBLE ? `${ZERO_STR}${min}` : min;
    let end = second < COMMON_NUM_DOUBLE ? `${ZERO_STR}${second}` : second;
    let nowTime = `${head}${SPLIT}${end}`;
    return nowTime;
    };
  • 指定视频播放进度及添加 onChange 事件回调:

    如需手动进行进度条的拖动,则需要在 Slider 组件中指定播放进度,并为 Slider 组件添加 onChange 事件回调;Slider 滑动时就会触发该事件回调,从而实现将视频定位到进度条当前刷新位置,完成时长组件渲染与视频播放进度数据联动;

    1
    2
    3
    4
    Slider({...})
    .onChange((value: number, mode: SliderChangeMode) => {
    sliderOnchange.call(this, value, mode);
    })
    1
    2
    3
    4
    5
    export function sliderOnchange(value: number, mode: SliderChangeMode) {
    this.currentTime = parseInt(value.toString());
    this.controller.setCurrentTime(parseInt(value.toString()), SeekMode.Accurate);
    ...
    };

添加弹窗

弹窗是一种模态窗口,通常用来展示用户当前需要的或用户必须关注的信息或操作;在弹出框消失之前,用户无法操作其他界面内容;

例如,在执行一些敏感的操作时,比如删除联系人,应该给应用添加弹窗来提示用户是否需要执行该操作;

ArkUI 提供了丰富的弹窗功能,按照功能可以分为以下两类:

  • 确认类:例如警告弹窗 AlertDialog;
  • 选择类:包括文本选择弹窗 TextPickerDialog、日期滑动选择弹窗 DatePickerDialog、时间滑动选择弹窗 TimePickerDialog 等;

开发者可以根据业务场景,选择不同类型的弹窗;部分弹窗效果图如下:

此外,如果需要对弹窗的布局和样式进行自定义,可以使用自定义弹窗 CustomDialog;


警告弹窗

警告弹窗 AlertDialog 由以下三部分区域构成,对应下面的示意图:

  1. 标题区:为可选的;
  2. 内容区:显示提示消息;
  3. 操作按钮区:用户做”确认“或者”取消“等操作;

例如,分别使用 primaryButton 和 secondaryButton 实现“取消”和“删除”操作按钮,操作按钮可以通过 action 响应点击事件:

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
Button('点击显示弹窗')
.onClick(() => {
AlertDialog.show(
{
title: '删除联系人', // 标题
message: '是否需要删除所选联系人?', // 内容
autoCancel: false, // 点击遮障层时,是否关闭弹窗。
alignment: DialogAlignment.Bottom, // 弹窗在竖直方向的对齐方式
offset: { dx: 0, dy: -20 }, // 弹窗相对alignment位置的偏移量
primaryButton: {
value: '取消',
action: () => {
console.info('Callback when the first button is clicked');
}
},
secondaryButton: {
value: '删除',
fontColor: '#D94838',
action: () => {
console.info('Callback when the second button is clicked');
}
},
cancel: () => { // 点击遮障层关闭dialog时的回调
console.info('Closed callbacks');
}
}
)
})

例如,构建只包含一个操作按钮的确认弹窗,使用 confirm 响应操作按钮回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
AlertDialog.show(
{
title: '提示',
message: '提示信息',
autoCancel: true,
alignment: DialogAlignment.Bottom,
offset: { dx: 0, dy: -20 },
confirm: {
value: '确认',
action: () => {
console.info('Callback when confirm button is clicked');
}
},
cancel: () => {
console.info('Closed callbacks')
}
}
)

文本选择弹窗

例如,使用 selected 指定弹窗的初始选择项索引为 2;当用户点击“确定”操作按钮后,触发 onAccept 事件回调,将选中的值传递给宿主中的 select 变量;

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
@Entry
@Component
struct TextPickerDialogDemo {
@State select: number = 2;
private fruits: string[] = ['苹果', '橘子', '香蕉', '猕猴桃', '西瓜'];

build() {
Column() {
Button('TextPickerDialog')
.margin(20)
.onClick(() => {
TextPickerDialog.show({
range: this.fruits, // 设置文本选择器的选择范围
selected: this.select, // 设置初始选中项的索引值。
onAccept: (value: TextPickerResult) => { // 点击弹窗中的“确定”按钮时触发该回调。
// 设置select为按下确定按钮时候的选中项index,这样当弹窗再次弹出时显示选中的是上一次确定的选项
this.select = value.index;
console.info("TextPickerDialog:onAccept()" + JSON.stringify(value));
},
onCancel: () => { // 点击弹窗中的“取消”按钮时触发该回调。
console.info("TextPickerDialog:onCancel()");
},
onChange: (value: TextPickerResult) => { // 滑动弹窗中的选择器使当前选中项改变时触发该回调。
console.info('TextPickerDialog:onChange()' + JSON.stringify(value));
}
})
})
}
.width('100%')
}
}

日期选择弹窗

DatePickerDialog 的使用非常广泛,例如,输入个人出生日期;

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 DatePickerDialogDemo {
selectedDate: Date = new Date('2010-1-1');

build() {
Column() {
Button("DatePickerDialog")
.margin(20)
.onClick(() => {
DatePickerDialog.show({
start: new Date('1900-1-1'), // 设置选择器的起始日期
end: new Date('2023-12-31'), // 设置选择器的结束日期
selected: this.selectedDate, // 设置当前选中的日期
lunar: false,
onAccept: (value: DatePickerResult) => { // 点击弹窗中的“确定”按钮时触发该回调
// 通过Date的setFullYear方法设置按下确定按钮时的日期,这样当弹窗再次弹出时显示选中的是上一次确定的日期
this.selectedDate.setFullYear(value.year, value.month, value.day)
console.info('DatePickerDialog:onAccept()' + JSON.stringify(value))
},
onCancel: () => { // 点击弹窗中的“取消”按钮时触发该回调
console.info('DatePickerDialog:onCancel()')
},
onChange: (value: DatePickerResult) => { // 滑动弹窗中的滑动选择器使当前选中项改变时触发该回调
console.info('DatePickerDialog:onChange()' + JSON.stringify(value))
}
})
})
}
.width('100%')
}
}

自定义弹窗

自定义弹窗的界面可以通过装饰器 @CustomDialog 定义的组件来实现,然后结合 CustomDialogController 来控制自定义弹窗的显示和隐藏;

目标实现效果:

使用装饰器 @CustomDialog,结合 List 组件来完成这个弹窗布局,实现步骤如下:

  1. 初始化弹窗数据:

    先准备好资源文件数据实体类

    其中资源文件 stringarray.json 创建在 resources/base/element 目录下,文件根节点为 strarray;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    {
    "strarray": [
    {
    "name": "hobbies_data",
    "value": [
    {
    "value": "Soccer"
    },
    {
    "value": "Badminton"
    },
    {
    "value": "Travelling"
    },
    ...
    ]
    }
    ]
    }

    实体类 HobbyBean 用来封装自定义弹窗中的”兴趣爱好”数据:

    1
    2
    3
    4
    export default class HobbyBean {
    label: string;
    isChecked: boolean;
    }

    然后创建一个 ArkTS 文件 CustomDialogWidget,用来封装自定义弹窗;

    • 使用装饰器 @CustomDialog 修饰 CustomDialogWidget 表示这是一个自定义弹窗;
    • 使用资源管理对象 manager 获取数据,并将数据封装到 hobbyBeans;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @CustomDialog
    export default struct CustomDialogWidget {
    @State hobbyBeans: HobbyBean[] = [];

    aboutToAppear() { // 弹窗初始化
    let context: Context = getContext(this);
    let manager = context.resourceManager;
    manager.getStringArrayValue($r('app.strarray.hobbies_data'), (error, hobbyResult) => {
    ...
    hobbyResult.forEach((hobbyItem: string) => {
    let hobbyBean = new HobbyBean();
    hobbyBean.label = hobbyItem;
    hobbyBean.isChecked = false;
    this.hobbyBeans.push(hobbyBean);
    });
    });
    }

    build() {...}
    }
  2. 创建弹窗组件:

    controller 对象用于控制弹窗的控制和隐藏,hobbies 表示弹窗选中的数据结果;setHobbiesValue 方法用于筛选出被选中的数据,赋值给 hobbies;

    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
    @CustomDialog
    export default struct CustomDialogWidget {
    @State hobbyBeans: HobbyBean[] = [];
    @Link hobbies: string;
    private controller?: CustomDialogController;

    aboutToAppear() {...}

    setHobbiesValue(hobbyBeans: HobbyBean[]) {
    let hobbiesText: string = '';
    hobbiesText = hobbyBeans.filter((isCheckItem: HobbyBean) =>
    isCheckItem?.isChecked)
    .map((checkedItem: HobbyBean) => {
    return checkedItem.label;
    }).join(',');
    this.hobbies = hobbiesText;
    }

    build() {
    Column() {
    Text($r('app.string.text_title_hobbies'))...
    List() {
    ForEach(this.hobbyBeans, (itemHobby: HobbyBean) => {
    ListItem() {
    Row() {
    Text(itemHobby.label)...
    Toggle({ type: ToggleType.Checkbox, isOn: false })...
    .onChange((isCheck) => {
    itemHobby.isChecked = isCheck;
    })
    }
    }
    }, itemHobby => itemHobby.label)
    }

    Row() {
    Button($r('app.string.cancel_button'))...
    .onClick(() => {
    this.controller?.close();
    })
    Button($r('app.string.definite_button'))...
    .onClick(() => {
    this.setHobbiesValue(this.hobbyBeans);
    this.controller?.close();
    })
    }
    }
    }
    }
  3. 使用自定义弹窗:

    在自定义弹窗的使用页面 HomePage 中先定义一个变量 hobbies,使用装饰器 @State 修饰,和自定义弹窗中的 @Link 装饰器修饰的变量进行双向绑定;

    然后使用 alignment 和 offset 设置弹窗的位置在屏幕底部,并且距离底部 20vp;

    最后在自定义组件 TextCommonWidget 的点击事件中,调用 customDialogController 的 open 方法,用于显示弹窗;

    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 HomePage {
    customDialogController: CustomDialogController = new CustomDialogController({
    builder: CustomDialogWidget({
    onConfirm: this.setHobbiesValue.bind(this),
    }),
    alignment: DialogAlignment.Bottom,
    customStyle: true,
    offset: { dx: 0,dy: -20 }
    });

    setHobbiesValue(hobbyArray: HobbyBean[]) {...}

    build() {
    ...
    TextCommonWidget({
    ...
    title: $r('app.string.title_hobbies'),
    content: $hobby,
    onItemClick: () => {
    this.customDialogController.open();
    }
    })
    ...
    }
    }

属性动画

属性动画,是最为基础的动画,其功能强大、使用场景多,应用范围较广;常用于如下场景中:

  1. 页面布局发生变化;例如添加、删除部分组件元素;
  2. 页面元素的可见性和位置发生变化;例如显示或者隐藏部分元素,或者将部分元素从一端移动到另外一端;
  3. 页面中图形图片元素动起来;例如使页面中的静态图片动起来;

简单来说,属性动画是组件的通用属性发生改变时而产生的属性渐变效果;其原理是,当组件的通用属性发生改变时,组件状态由初始状态逐渐变为结束状态的过程中,会创建多个连续的中间状态,逐帧播放后,就会形成属性渐变效果,从而形成动画;


创建

目标效果如下,以五个图标放大移动动画的为例来讲解如何创建属性动画;

首先,创建一个头部刷新组件 RefreshAnimHeader,在其中自定义一个图标组件 AttrAnimIcons,用 Image 组件将资源图标引入,并设置好样式,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
export default struct RefreshAnimHeader {
...
@Builder AttrAnimIcons(iconItem) {
Image(iconItem.imgRes)
.width(this.iconWidth)
.position({ x: iconItem.posX })
.objectFit(ImageFit.Contain)
.animation({
duration: 2000,
tempo: 3.0,
delay: iconItem.delay,
curve: Curve.Linear,
playMode: PlayMode.Alternate,
iterations: -1
})
}
...
}

然后,在 build 方法中使用 Row 容器组件,将自定义的图标组件引入,并设置好样式,同时定义组件状态 iconWidth,添加 onApper 事件,修改 iconWidth 的值,使其从 30 变为 100,以触发 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
@Component
export default struct RefreshAnimHeader {
...
@State iconWidth: number = 30;
private onStateCheck() {
if (this.state === RefreshState.REFRESHING) {
this.iconWidth = 100;
} else {
this.iconWidth = 30;
}
}
build() {
Row() {
ForEach(CommonConstants.REFRESH_HEADER_FEATURE, (iconItem) => {
this.AttrAnimIcons(iconItem)
}, item => item.toString())
}
.width("100%")
.height("100%")
.onAppear(() => {
this.onStateCheck();
})
}
}

注意:

  1. animation 属性作用域:其作用域为 animation 之前,即产生属性动画的属性须在 animation 之前声明,其后声明的将不会产生属性动画;

    示例中,产生动画的属性为 Image 组件的 width 属性,故该属性 width 需在 animation 属性之前声明;

  2. 产生属性动画的属性变化时需触发 UI 状态更新;

    示例中,产生动画的属性 width,@State 修饰的状态变量 iconWidth 从 30 变为 100;

  3. 产生属性动画的属性本身需满足一定的要求,并非任何属性都可以产生属性动画;

    目前支持的属性包括 width、height、position、opacity、backgroundColor、scale、rotate、translate 等;


参数调整

属性动画中 animation 的参数如下:

属性名称 属性类型 默认值 描述
duration number 1000 动画时长,单位为毫秒,默认时长为 1000 毫秒
tempo number 1.0 动画的播放速度,值越大动画播放越快,值越小播放越慢,为 0 时无动画效果
curve Curve Curve.Linear 动画变化曲线,默认曲线为线性
delay number 0 延时播放时间,单位为毫秒,默认不延时播放
iterations number 1 播放次数,默认一次,设置为 -1 时表示无限次播放
playMode PlayMode PlayMode.Normal 设置动画播放模式,默认播放完成后重头开始播放
onFinish function - 动画播放结束时回调该函数
  • 其中变化曲线 curve 枚举值为:

    名称 描述
    Linear 表示动画从头到尾的速度都是相同的
    Ease 表示动画以低速开始,然后加快,在结束前变慢,CubicBezier(0.25, 0.1, 0.25, 1.0)
    EaseIn 表示动画以低速开始,CubicBezier(0.42, 0.0, 1.0, 1.0)
    EaseOut 表示动画以低速结束,CubicBezier(0.0, 0.0, 0.58, 1.0)
    EaseInOut 表示动画以低速开始和结束,CubicBezier(0.42, 0.0, 0.58, 1.0)
    FastOutSlowIn 标准曲线,cubic-bezier(0.4, 0.0, 0.2, 1.0)
    LinearOutSlowIn 减速曲线,cubic-bezier(0.0, 0.0, 0.2, 1.0)
    FastOutLinearIn 加速曲线,cubic-bezier(0.4, 0.0, 1.0, 1.0)
    ExtremeDeceleration 急缓曲线,cubic-bezier(0.0, 0.0, 0.0, 1.0)
    Sharp 锐利曲线,cubic-bezier(0.33, 0.0, 0.67, 1.0)
    Rhythm 节奏曲线,cubic-bezier(0.7, 0.0, 0.2, 1.0)
    Smooth 平滑曲线,cubic-bezier(0.4, 0.0, 0.4, 1.0)
    Friction 阻尼曲线,CubicBezier(0.2, 0.0, 0.2, 1.0)
  • 播放模式 playMode 枚举值为:

    名称 描述
    Normal 动画按正常播放
    Reverse 动画反向播放
    Alternate 动画在奇数次(1、3、5…)正向播放,在偶数次(2、4、6…)反向播放
    AlternateReverse 动画在奇数次(1、3、5…)反向播放,在偶数次(2、4、6…)正向播放

注意:

在延时间距不超过动画时长 duration 时,总延时间距越接近 duration,秩序性越好,视觉效果越好;

示例中,图标动画时长 duration 为 2000ms,故延时间距为 2000ms/5=400ms,五个图标的延时参数 delay 可分别设置为 400ms、800ms、1200ms、1600ms、2000ms;

当根据 iterations 播放结束时,会调用 onFinish 进行后续的业务处理;例如,提示动画播放结束:

1
2
3
4
5
6
7
8
Image(iconItem.imgRes)
...
.animation({
...
onFinish: () => {
prompt.showToast({ message:"动画播放结束!!!" })
}
})

关闭

目标效果如下,指将头部刷新组件 RefreshAnimHeader 隐藏起来;

首先,在组件 RefreshAnimHeader 中添加状态变量 state,同时添加条件渲染逻辑,根据 state 的值来判断是否需要关闭;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
export default struct RefreshAnimHeader {
@Consume(RefreshConstants.REFRESH_STATE_TAG) @Watch('onStateCheck') state: RefreshState;
build() {
Row() {
if (this.state !== RefreshState.IDLE) { // start or stop animation when idle state.
ForEach(CommonConstants.REFRESH_HEADER_FEATURE, (iconItem) => {
this.AttrAnimIcons(iconItem)
}, item => item.toString()}
}
}
.width(CommonConstants.FULL_LENGTH)
.height(CommonConstants.FULL_LENGTH)
.onAppear(() => {
this.onStateCheck();
})
}
}

然后,为下方列表添加上移属性动画,通过修改 state 变量的值为 IDLE 状态,关闭属性动画页面;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
export default struct RefreshComponent {
@Consume(RefreshConstants.REFRESH_STATE_TAG) @Watch('onStateChanged') state: RefreshState;
build() {
List({ scroller: this.listController }) {
...
}
...
.animation({
...
onFinish: () => {
if (this.headerOffset === -RefreshConstants.REFRESH_HEADER_HEIGHT) {
this.state = RefreshState.IDLE;
}
}
})
}
}

Web 组件

ArkUI 提供了 Web 组件来加载网页,相当于在自己的应用程序里嵌入一个浏览器,从而非常轻松地展示各种各样的网页;

本文将介绍 Web 组件一些常用 API 的使用;


加载网页

  • 加载在线网页

    创建一个 Web 组件,传入两个参数,其中 src 指定引用的网页路径,controller 为组件的控制器;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Entry
    @Component
    struct WebComponent {
    controller: WebController = new WebController();
    build() {
    Column() {
    Web({ src: 'https://developer.harmonyos.com/', controller: this.controller })
    }
    }
    }

    访问在线网页时,需要先在 module.json5 文件中申明网络访问权限:ohos.permission.INTERNET;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    "module" : {
    "requestPermissions":[
    {
    "name": "ohos.permission.INTERNET"
    }
    ]
    }
    }
  • 加载本地网页

    首先在 main/resources/rawfile 目录下创建一个 HTML 文件,然后通过 $rawfile 引用本地网页资源:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Entry
    @Component
    struct SecondPage {
    controller: WebController = new WebController();

    build() {
    Column() {
    Web({ src: $rawfile('index.html'), controller: this.controller })
    }
    }
    }

网页缩放

部分网页不能很好的适配屏幕,开发者可以根据需要给 Web 组件设置 zoomAccess 属性,设置是否支持手势进行缩放,默认允许;

1
2
Web({ src:'www.example.com', controller:this.controller })
.zoomAccess(true)

还可以使用 zoom(factor: number) 方法用于设置网站的缩放比例,其中 factor 表示缩放倍数;

例如,当点击一次按钮时,页面放大为原来的 1.5 倍;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entry
@Component
struct WebComponent {
controller: WebController = new WebController();
factor: number = 1.5;

build() {
Column() {
Button('zoom')
.onClick(() => {
this.controller.zoom(this.factor);
})
Web({ src: 'www.example.com', controller: this.controller })
}
}
}

注意:只有网页自身支持缩放,才能在Web组件里面进行缩放;

如果需要对文本进行缩放,可以使用 textZoomAtio(textZoomAtio: number) 方法,其中 textZoomAtio 用于设置页面的文本缩放百分比,默认值为 100,表示 100%;

例如,将文本放大为原来的 1.5 倍:

1
2
Web({ src:'www.example.com', controller:this.controller })
.textZoomAtio(150)

回调事件

Web 组件还提供了处理 Javascript 的对话框、网页加载进度及各种通知与请求事件的方法;例如:

  • onProgressChange 可以监听网页的加载进度;
  • onPageEnd 在网页加载完成时触发该回调,且只在主 frame 触发;
  • onConfirm 则在网页触发 confirm 告警弹窗时触发回调;

在 onAlert 或 onConfirm 的回调方法中处理并响应 Web 组件中网页的警告弹窗(JS confirm)事件;

以 confirm 弹窗为例,在网页触发 onConfirm() 时,显示一个 AlertDialog 弹窗:

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 WebComponent {
controller:WebController = new WebController();
build() {
Column() {
Web({ src:$rawfile('index.html'), controller:this.controller })
.onConfirm((event) => {
AlertDialog.show({
title: 'title',
message: event.message,
confirm: {
value: 'onAlert',
action: () => {
event.result.handleConfirm();
}
},
cancel: () => {
event.result.handleCancel();
}
})
return true;
}
}
}
}

(存疑)当 onConfirm 回调返回 false 时,触发默认弹窗;当回调返回 true 时,系统应用可以调用系统弹窗能力(包括确认和取消),并且需要根据用户的确认或取消操作调用 JsResult 通知 Web 组件;(存疑)


JavaScript 交互

在开发专为适配 Web 组件的网页时,可以实现 Web 组件和 JavaScript 代码之间的交互;Web 组件可以调用 JavaScript 方法,JavaScript 也可以调用 Web 组件里面的方法;

  1. 启用 JavaScript:

    如果想要被加载的网页在 Web 组件中运行 JavaScript,则必须先启用 JavaScript 功能,默认开启;

    1
    2
    Web({ src:'https://www.example.com', controller:this.controller })
    .javaScriptAccess(true)
  2. Web 组件调用 JS 方法:

    在 onPageEnd 事件中添加 runJavaScript 方法,页面加载完执行 HTML 中的 JavaScript 脚本,并将结果返回给 Web 组件;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Entry
    @Component
    struct WebComponent {
    controller: WebController = new WebController();
    @State webResult: string = ''
    build() {
    Column() {
    Text(this.webResult).fontSize(20)
    Web({ src: $rawfile('index.html'), controller: this.controller })
    .javaScriptAccess(true)
    .onPageEnd(e => {
    this.controller.runJavaScript({
    script: 'test()',
    callback: (result: string)=> {
    this.webResult = result;
    }});
    })
    }
    }
    }
  3. JS 调用 Web 组件方法:

    首先,使用 registerJavaScriptProxy 方法将 Web 组件中的 JavaScript 对象注入到 window 对象中;

    然后,调用 refresh 方法使 registerJavaScriptProxy 方法生效,网页中的 JS 可以直接调用该对象;

    例如,将 ets 文件中的对象 testObj 注入到了 window 对象中:

    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 WebComponent{
    @State dataFromHtml: string = ''
    controller: WebController = new WebController()
    testObj = {
    test: (data) => {
    this.dataFromHtml = data;
    return 'ArkUI Web Component';
    },
    toString: () => {
    console.log('Web Component toString');
    }
    }

    build() {
    Column() {
    Text(this.dataFromHtml).fontSize(20)
    Row() {
    Button('Register JavaScript To Window').onClick(() => {
    this.controller.registerJavaScriptProxy({
    object: this.testObj,
    name: 'objName',
    methodList: ['test', 'toString'],
    });
    this.controller.refresh();
    })
    }

    Web({ src: $rawfile('index.html'), controller: this.controller })
    .javaScriptAccess(true)
    }
    }
    }

    其中,object 表示参与注册的对象;name 表示注册对象的名称为 objName,与 window 中调用的对象名一致;methodList 表示参与注册的应用侧 JavaScript 对象的方法;

    网页代码可以直接使用 objName 调用 methodList 中对应的方法,例如 test、toString;


页面导航

使用 backward() 返回上一页面,使用 forward() 前进一个页面,使用 refresh() 刷新页面,使用 clearHistory() 清除历史记录,使用 accessBackward() 检查当前页面是否有后退来时记录,使用 accessForward() 检查是否存在前进历史记录;

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
@Entry
@Component
struct Page5 {
controller: WebController = new WebController();

build() {
Column() {
Row() {
Button("前进").onClick(() => {
this.controller.forward();
})
Button("后退").onClick(() => {
this.controller.backward();
})
Button("刷新").onClick(() => {
this.controller.refresh();
})
Button("停止").onClick(() => {
this.controller.stop();
})
Button("清除历史").onClick(() => {
this.controller.clearHistory();
})
}
.padding(12)
.backgroundColor(Color.Gray)
.width('100%')

Web({ src: 'https://developer.harmonyos.com/', controller: this.controller })
}
.height('100%')
}
}

调试网络应用

当在网页中使用 console 打印日志时,HarmonyOS 系统都会调用相应的 onConsole 方法,获取到网页日志信息;

例如,在 Web 组件中使用 onConsole 输出网页中的日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Entry
@Component
struct WebComponent {
controller: WebController = new WebController();
build() {
Column() {
Web({ src: $rawfile('index.html'), controller: this.controller })
.onConsole((event) => {
console.log('getMessage:' + event.message.getMessage());
console.log('getMessageLevel:' + event.message.getMessageLevel());
return false;
})
}
}
}

event 的内容为 ConsoleMessage,可以使用 getMessageLevel() 查询消息级别以确定消息的严重性,然后根据自身业务采取相应的操作;


发起 HTTP 请求

HTTP 数据请求功能主要由 http 模块提供,包括发起请求、中断请求、订阅/取消订阅 HTTP Response Header 事件等;

在进行网络请求前,需要先在 module.json5 文件中申明网络访问权限;

1
2
3
4
5
6
7
8
9
{
"module" : {
"requestPermissions":[
{
"name": "ohos.permission.INTERNET"
}
]
}
}

可以按照以下步骤完成 HTTP 数据请求:

  1. 导入 http 模块:

    1
    import http from '@ohos.net.http';
  2. 创建 httpRequest 对象:

    使用 createHttp() 创建 httpRequest 对象,包括常用的一些网络请求方法,比如 request、destroy、on(‘headerReceive’) 等;

    1
    let httpRequest = http.createHttp();

    注意,每一个 httpRequest 对象对应一个 http 请求任务,不可复用;

  3. 订阅请求头(可选):

    用于订阅 http 响应头,此接口会比 request 请求先返回,可以根据业务需要订阅此消息;

    1
    2
    3
    httpRequest.on('headersReceive', (header) => {
    console.info('header: ' + JSON.stringify(header));
    });
  4. 发起 http 请求:

    http 模块支持常用的 POST 和 GET 等方法,封装在 RequestMethod 中;

    调用 request 方法发起网络请求,需要传入两个参数:

    • 第一个是请求的 url 地址;
    • 第二个是可选参数,类型为 HttpRequestOptions,用于定义可选参数的类型和取值范围,包含请求方式、连接超时时间、请求头字段等;

    使用 Get 请求,参数内容需要拼接到 URL 中进行发送;

    例如,在 url 后面拼接两个自定义参数,分别命名为 param1 和 param2,值分别为 value1 和 value2:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    let url= "https://EXAMPLE_URL?param1=v1&param2=v2";
    let promise = httpRequest.request(
    // 请求url地址
    url,
    {
    // 请求方式
    method: http.RequestMethod.GET,
    // 可选,默认为60s
    connectTimeout: 60000,
    // 可选,默认为60s
    readTimeout: 60000,
    // 开发者根据自身业务需要添加header字段
    header: {
    'Content-Type': 'application/json'
    }
    });

    POST 请求参数需要添加到 extraData 里面;

    例如,在 extraData 里面定义添加了两个自定义参数 param1 和 param2,值分别为 value1 和 value2:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    let url = "https://EXAMPLE_URL";
    let promise = httpRequest.request(
    // 请求url地址
    url,
    {
    // 请求方式
    method: http.RequestMethod.POST,
    // 请求的额外数据。
    extraData: {
    "param1": "value1",
    "param2": "value2",
    },
    // 可选,默认为60s
    connectTimeout: 60000,
    // 可选,默认为60s
    readTimeout: 60000,
    // 开发者根据自身业务需要添加header字段
    header: {
    'Content-Type': 'application/json'
    }
    });
  5. 处理响应结果:

    data 为网络请求返回的结果,err 为请求异常时返回的结果;

    1
    2
    3
    4
    5
    6
    7
    8
    promise.then((data) => { 
    if (data.responseCode === http.ResponseCode.OK) {
    console.info('Result:' + data.result);
    console.info('code:' + data.responseCode);
    }
    }).catch((err) => {
    console.info('error:' + JSON.stringify(err));
    });

    其中,data.responseCode 为 http 请求返回的状态码,例如,http.ResponseCode.OK(即 200)表示请求成功;

    data.result 为服务器返回的业务数据,开发者可以根据自身业务场景解析此数据;


数据管理

首选项是 HarmonyOS 提供的数据管理能力之一;


首选项

首选项为应用提供 Key-Value 键值型的数据存储能力,支持应用持久化轻量级数据,并对其进行增删改查等;该存储对象中的数据会被缓存在内存中,因此它可以获得更快的存取速度;

首选项的特点是:

  1. 以 Key-Value 形式存储数据;

    Key 是不重复的关键字,Value 是数据值;

  2. 非关系型数据库;

区别于关系型数据库,它不保证遵循 ACID 特性,数据之间无关系;

  • 进程中每个文件仅存在一个 Preferences 实例,应用获取到实例后,可以从中读取数据,或者将数据存入实例中;
  • 通过调用 flush 方法可以将实例中的数据回写到文件里;

与关系数据库的区别:

分类 关系型数据库 首选项
数据库类型 关系型 非关系型
使用场景 提供复杂场景下的本地数据库管理机制 对 Key-Value 结构的数据进行存取和持久化操作
存储方式 SQLite 数据库 文件
约束与限制 1.连接池最大 4 个;2.同一时间只支持一个写操作; 1.建议数据不超一万条;2.Key 为 string 型;

常用接口

常用接口有:保存数据(put)、获取数据(get)、是否包含指定的 key(has)、删除数据(delete)、数据持久化(flush)等;

常用接口使用前提

  1. 需要导入 @ohos.data.preferences 模块到 PreferencesUtil 开发环境中,实例名字命名为 dataPreferences,同时定义两个常量 PREFERENCES_NAME 和 KEY_APP_FONT_SIZE(把常用接口封装在 PreferencesUtil 工具类里面,方便后面代码直接调用):

    1
    2
    3
    4
    5
    // PreferencesUtil.ets
    import dataPreferences from '@ohos.data.preferences';
    ...
    const PREFERENCES_NAME = 'myPreferences'; // 首选项名字
    const KEY_APP_FONT_SIZE = 'appFontSize'; // 首选项Key字段
  2. 需要在 entryAbility 的 onCreate 方法获取首选项实例,以便后续能进行保存、读取、删除等操作,获取实例需要上下文 context 和文件名字 PREFERENCES_NAME:

    1
    2
    3
    4
    5
    6
    7
    8
    // entryAbility.ets  
    onCreate(want, launchParam) {
    Logger.info(TAG, 'onCreate');
    globalThis.abilityWant = want;
    // 创建首选项
    PreferencesUtil.createFontPreferences(this.context);
    ...
    }
    1
    2
    3
    4
    5
    6
    7
    8
    // PreferencesUtil.ets  
    createFontPreferences(context) {
    globalThis.getFontPreferences = (() => {
    // 获取首选项实例
    let preferences: Promise = dataPreferences.getPreferences(context, PREFERENCES_NAME);
    return preferences;
    });
    }

关于 globalThis,提供了一个标准的方式来获取不同环境下的全局 this 对象;

  • 以上代码,直接调用了当前类 PreferencesUtil 的方法,返回的实例另作存储;

  • 实际开发中,可以调用存储在 GlobalContext 实例(类似中介)中的首选项实例的方法;

例如,自定义 GlobalContext 类并创建实例,其中设置首选项实例:

1
GlobalContext.getContext().setObject('getFontPreferences', fontPreferences);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// GlobalContext.ets
export class GlobalContext {
private constructor() { }
private static instance: GlobalContext;
private _objects = new Map<string, Object>();

public static getContext(): GlobalContext {
if (!GlobalContext.instance) {
GlobalContext.instance = new GlobalContext();
}
return GlobalContext.instance;
}

getObject(value: string): Object | undefined {
return this._objects.get(value);
}

setObject(key: string, objectClass: Object): void {
this._objects.set(key, objectClass);
}
}

保存数据(put)

  1. 在 entryAbility 的 onCreate 方法,调用 PreferencesUtil.saveDefaultFontSize 保存默认数据,先用 has 方法判断当前 key 是否有存在,如果没有就通过 put 方法把用户数据保存起来,再通过 flush 方法把数据保存到文件:

    1
    2
    3
    4
    5
    6
    7
    8
    // entryAbility.ets  
    onCreate(want, launchParam) {
    Logger.info(TAG, 'onCreate');
    globalThis.abilityWant = want;
    ...
    // 设置字体默认大小
    PreferencesUtil.saveDefaultFontSize(Constants.SET_SIZE_STANDARD);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // PreferencesUtil.ets    
    saveDefaultFontSize(fontSize: number) {
    globalThis.getFontPreferences().then((preferences) => {
    // 判断保存的key是否存在
    preferences.has(KEY_APP_FONT_SIZE).then(async (isExist) => {
    Logger.info(TAG, 'preferences has changeFontSize is ' + isExist);
    if (!isExist) {
    // 保存数据
    await preferences.put(KEY_APP_FONT_SIZE, fontSize);
    preferences.flush();
    }
    }).catch((err) => {
    Logger.error(TAG, 'Has the value failed with err: ' + err);
    });
    }).catch((err) => {
    Logger.error(TAG, 'Get the preferences failed, err: ' + err);
    });
    }
  2. 在 SetFontSizePage 页面,当移动 Slider 滑动条时,把当前进度值通过 PreferencesUtil.saveChangeFontSize 方法保存起来,再通过 flush 方法把数据保存到文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // SetFontSizePage.ets
    build() {
    Row() {
    Slider({
    ...
    })
    .onChange((value: number) => {
    // 保存当前进度值
    PreferencesUtil.saveChangeFontSize(this.changeFontSize);
    })
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // PreferencesUtil.ets 
    saveChangeFontSize(fontSize: number) {
    globalThis.getFontPreferences().then(async (preferences) => {
    // 保存数据
    await preferences.put(KEY_APP_FONT_SIZE, fontSize);
    preferences.flush();
    }).catch((err) => {
    Logger.error(TAG, 'put the preferences failed, err: ' + err);
    });
    }

获取数据(get)

在 HomePage 的 onPageShow 方法,调用 PreferencesUtil.getChangeFontSize 方法获取用户数据,调用 get 方法获取,把的到的结果赋值给变量 fontSize,通过 return 方式把值返回去:

1
2
3
4
5
6
7
// HomePage.ets
onPageShow() {
PreferencesUtil.getChangeFontSize().then((value) => {
this.changeFontSize = value;
Logger.info(TAG, 'Get the value of changeFontSize: ' + this.changeFontSize);
});
}
1
2
3
4
5
6
7
// PreferencesUtil.ets 
async getChangeFontSize() {
let fontSize: number = 0;
const preferences = await globalThis.getFontPreferences();
fontSize = await preferences.get(KEY_APP_FONT_SIZE, fontSize);
return fontSize;
}

async、await(Promise 语法糖)说明:

async 用于申明异步 function;await 表示等待异步逻辑执行完成;

规则:

  1. async 和 await 是配对使用的,await 存在于 async 的内部;
  2. await 表示在这里等待一个 promise 返回,再接下来执行;
  3. await 后面跟着的应该是一个 promise 对象;

async、await 将异步强行转换为同步处理;即,异步编程的最高境界就是不关心它是否是异步;

是否包含指定的 key(has)

通过 has 方法判断首选项中是否包含指定的 key,保证指定的 key 不会被重复保存:

1
2
3
4
5
6
7
8
9
10
11
12
13
// PreferencesUtil.ets    
saveDefaultFontSize(fontSize: number) {
globalThis.getFontPreferences().then((preferences) => {
// 判断保存的key是否存在
preferences.has(KEY_APP_FONT_SIZE).then(async (isExist) => {
Logger.info(TAG, 'preferences has changeFontSize is ' + isExist);
}).catch((err) => {
Logger.error(TAG, 'Has the value failed with err: ' + err);
});
}).catch((err) => {
Logger.error(TAG, 'Get the preferences failed, err: ' + err);
});
}

数据持久化(flush)

通过 flush 方法把应用数据保存到文件中,使得应用数据保存期限变长:

1
2
3
4
5
6
7
8
9
10
11
// PreferencesUtil.ets 
saveChangeFontSize(fontSize: number) {
globalThis.getFontPreferences().then(async (preferences) => {
// 保存数据
await preferences.put(KEY_APP_FONT_SIZE, fontSize);
// 数据持久化
preferences.flush();
}).catch((err) => {
Logger.error(TAG, 'put the preferences failed, err: ' + err);
});
}

删除数据(delete)

删除首选项数据需要获取 preferences 实例,用 delete 方法删除指定的 key 所对应的值,通过 Promise 异步回调是否删除成功:

1
2
3
4
5
6
7
8
9
10
11
// PreferencesUtil.ets 
async deleteChangeFontSize() {
const preferences: dataPreferences.Preferences = await globalThis.getFontPreferences();
// 删除数据
let deleteValue = preferences.delete(KEY_APP_FONT_SIZE);
deleteValue.then(() => {
Logger.info(TAG, 'Succeeded in deleting the key appFontSize.');
}).catch((err) => {
Logger.error(TAG, 'Failed to delete the key appFontSize. Cause: ' + err);
});
}

通知&提醒


应用通知消息

通知旨在让用户以合适的方式及时获得有用的新消息,帮助用户高效地处理任务;

应用可以通过通知接口发送通知消息,用户可以通过通知栏查看通知内容,也可以点击通知来打开应用,通知主要有以下使用场景:

  • 显示接收到的短消息、即时消息等;
  • 显示应用的推送消息,如广告、版本更新等;
  • 显示当前正在进行的事件,如下载等;

形式与结构

通知会在不同场景以不同形式提示用户,例如,通知在状态栏上显示为图标、在通知栏上会显示通知详细信息;重要的信息还可以使用横幅通知,浮动在界面顶部显示;

下面以基础的文本通知为例,介绍通知的基本结构:

组成 说明
1. 通知小图标 表示通知的功能与类型
2. 通知名称 应用名称或功能名称
3. 时间 发送通知的时间,系统默认显示
4. 展开箭头 点击标题区,展开被折叠的内容和按钮。若无折叠的内容和按钮,不显示此箭头
5. 内容标题 描述简明概要
6. 内容详情 描述具体内容或详情

通知管理

在创建通知前需要先导入 notificationManager 模块,该模块提供通知管理的能力,包括发布、取消发布通知,创建、获取、移除通知通道等能力;

1
import notification from '@ohos.notificationManager';

创建通知

基础类型通知

基础类型通知主要应用于发送短信息、提示信息、广告推送等,支持普通文本类型、长文本类型、多行文本类型和图片类型,可以通过 contentType 指定通知的内容类型;

  • 普通文本类型通知,需要设置 contentType 类型为 ContentType.NOTIFICATION_CONTENT_BASIC_TEXT;

    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 notification from '@ohos.notificationManager';

    @Entry
    @Component
    struct NotificationDemo {
    publishNotification() {
    let notificationRequest: notification.NotificationRequest = { // 描述通知的请求
    id: 1, // 通知ID
    slotType: notification.SlotType.SERVICE_INFORMATION,
    content: { // 通知内容
    contentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, // 普通文本类型通知
    normal: { // 基本类型通知内容
    title: '通知内容标题',
    text: '通知内容详情',
    additionalText: '通知附加内容', // 通知附加内容,是对通知内容的补充。
    }
    }
    }
    notification.publish(notificationRequest).then(() => { // 发布通知
    console.info('publish success');
    }).catch((err) => {
    console.error(`publish failed, dcode:${err.code}, message:${err.message}`);
    });
    }

    build() {
    Column() {
    Button('发送通知')
    .onClick(() => {
    this.publishNotification()
    })
    }
    .width('100%')
    }
    }
  • 图片类型通知,需要设置 contentType 类型为 ContentType.NOTIFICATION_CONTENT_PICTURE;

    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
    import notification from '@ohos.notificationManager';
    import image from '@ohos.multimedia.image';

    @Entry
    @Component
    struct NotificationTest1 {
    async publishPictureNotification() {
    // 将资源图片转化为PixelMap对象
    let resourceManager = getContext(this).resourceManager;
    let imageArray = await resourceManager.getMediaContent($r('app.media.bigPicture').id);
    let imageResource = image.createImageSource(imageArray.buffer);
    let pixelMap = await imageResource.createPixelMap();

    let notificationRequest: notification.NotificationRequest = { // 描述通知的请求
    id: 1,
    content: {
    contentType: notification.ContentType.NOTIFICATION_CONTENT_PICTURE,
    picture: {
    title: '好物热销中', // 通知内容标题
    text: '展开查看详情', // 通知内容
    expandedTitle: '今日热门推荐', // 通知展开时的内容标题
    briefText: '这里一定有您喜欢的', // 通知概要内容,是对通知内容的总结
    picture: pixelMap // 通知的图片内容
    }
    }
    }

    notification.publish(notificationRequest).then(() => { // 发布通知
    console.info('publish success');
    }).catch((err) => {
    console.error(`publish failed, dcode:${err.code}, message:${err.message}`);
    });
    }

    build() {
    Column() {
    Button('发送大图通知')
    .onClick(() => {
    this.publishPictureNotification()
    })
    }
    .width('100%')
    }
    }

进度类型通知

进度条通知也是常见的通知类型,主要应用于文件下载、事务处理进度显示,且目前系统模板仅支持进度条模板:

在发布进度类型通知前需要查询系统是否支持进度条模板;

1
2
3
4
5
6
7
8
notification.isSupportTemplate('downloadTemplate').then((data) => {
console.info(`[ANS] isSupportTemplate success`);
// isSupportTpl的值为true表示支持支持downloadTemplate模板类通知,false表示不支持
let isSupportTpl: boolean = data;
// ...
}).catch((err) => {
console.error(`[ANS] isSupportTemplate failed, error[${err}]`);
});

构造进度条模板,name 字段当前需要固定配置为 downloadTemplate;

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
let template = {
name: 'downloadTemplate',
data: {
progressValue: 60, // 当前进度值
progressMaxValue: 100 // 最大进度值
}
}

let notificationRequest = {
id: 1,
content: {
contentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: '文件下载:music.mp4',
text: 'senTemplate',
additionalText: '60%'
}
},
template: template
}
// 发布通知
notification.publish(notificationRequest).then(() => {
console.info(`publish success`);
}).catch(error => {
console.error(`[ANS] publish failed, code is ${error.code}, message is ${error.message}`);
})

更新通知

在发出通知后,可以使用之前相同通知的 ID,再次调用 notification.publish 来实现通知的更新;如果之前的通知是关闭的,将会创建新通知;


移除通知

  • 通过通知 ID 取消已发布的通知;

    1
    notification.cancel(notificationId)
  • 取消所有已发布的通知;

    1
    notification.cancelAll()

按钮&行为意图


按钮

最多可以给通知添加三个按钮,便于用户快速响应,比如关闭提醒;

给操作按钮添加行为意图,来响应点击事件;例如,发布公共事件或拉起一个 UIAbility;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var notificationRequest = {
id: 1,
slotType: notification.SlotType.SOCIAL_COMMUNICATION,
content: {
contentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: '张三',
text: '吃饭了吗'
}
},
actionButtons: [
{
title: '回复',
wantAgent: wantAgentObj
}
]
};

添加行为意图

WantAgent 提供了封装行为意图的能力,这里的行为意图主要是指拉起指定的应用组件发布公共事件等能力;

给通知添加行为意图后,点击通知后可以拉起指定的 UIAbility 或发布公共事件,按照以下步骤来实现:

  1. 导入模块:

    1
    2
    import notification from '@ohos.notificationManager';
    import wantAgent from '@ohos.app.ability.wantAgent';
  2. 创建 WantAgentInfo 信息:

    场景一:拉起 UIAbility:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var wantAgentInfo = {
    wants: [
    {
    bundleName: "com.example.notification",
    abilityName: "EntryAbility"
    }
    ],
    operationType: wantAgent.OperationType.START_ABILITY,
    requestCode: 100
    }

    场景二:发布公共事件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let wantAgentInfo = {
    wants: [
    {
    action: 'event_name', // 设置事件名
    parameters: {},
    }
    ],
    operationType: wantAgent.OperationType.SEND_COMMON_EVENT,
    requestCode: 100,
    wantAgentFlags: [wantAgent.WantAgentFlags.CONSTANT_FLAG],
    }
  3. 创建 WantAgent 对象:

    1
    2
    3
    4
    5
    6
    7
    8
    let wantAgentObj = null; 
    wantAgent.getWantAgent(wantAgentInfo)
    .then((data) => {
    wantAgentObj = data;
    })
    .catch((err) => {
    console.error(`get wantAgent failed because ${JSON.stringify(err)}`);
    })
  4. 构造 NotificationRequest 对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var notificationRequest = {
    id: 1,
    content: {
    contentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
    normal: {
    title: "通知标题",
    text: "通知内容"
    }
    },
    wantAgent: wantAgentObj
    };
  5. 发布 WantAgent 通知:

    1
    2
    3
    4
    5
    notification.publish(notificationRequest).then(() => { // 发布通知
    console.info("publish success");
    }).catch((err) => {
    console.error(`publish failed, code is ${err.code}, message is ${err.message}`);
    });

    用户通过点击通知栏上的通知,即可触发 WantAgent 的动作;


通知 slot

通过通知通道(slot),可让通知有不同的表现形式,主要有以下几种:

类型 说明
SlotType.SOCIAL_COMMUNICATION 社交类型,状态栏中显示通知图标,有横幅和提示音
SlotType.SERVICE_INFORMATION 服务类型,状态栏中显示通知图标,没有横幅但有提示音
SlotType.CONTENT_INFORMATION 内容类型,状态栏中显示通知图标,没有横幅或提示音
SlotType.OTHER_TYPES 其它类型,状态栏中不显示通知图标,没有横幅或提示音

使用 slotType 实现,设置 slotType 为 SlotType.SOCIAL_COMMUNICATION,表示为社交类型通知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let imageArray = await getContext(this).resourceManager.getMediaContent($r('app.media.largeIcon').id);
let imageResource = image.createImageSource(imageArray.buffer);
let opts = { desiredSize: { height: 72, width: 72 } };
let largePixelMap = await imageResource.createPixelMap(opts);
let notificationRequest: notification.NotificationRequest = { // 描述通知的请求
id: 1, // 通知ID
content: { // 通知内容
contentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, // 普通文本类型通知
slotType: notification.SlotType.SOCIAL_COMMUNICATION,
normal: { // 基本类型通知内容
title: '张三', // 通知内容标题。
text: '等会下班一起吃饭哦', // 通知内容
}
},
largeIcon: largePixelMap // 通知大图标。可选字段,大小不超过30KB。
}

通知组

将不同类型的通知分为不同的组,以便用户更好的管理;当同组的通知有多条的时候,会自动折叠起来;

例如,当通知栏里有聊天消息通知和商品推荐通知时,只需要给字段 groupName 设置不同的值可以将通知分为不同的组;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let notifyId = 0;

let chatRequest: notification.NotificationRequest = {
id: notifyId++,
groupName:'ChatGroup',
content: {
...
}
};

let productRequest: notification.NotificationRequest = {
id: notifyId++,
groupName: 'ProductGroup',
content: {
...
}
};

后台代理提醒

应用可能需要在指定的时刻,向用户发送一些业务提醒通知;例如,购物类应用希望在指定时间点提醒用户有优惠活动;

为此,HarmonyOS 提供了后台代理提醒功能,拥有统一的提醒管理能力,在应用退居后台或退出后,计时和提醒通知功能被系统后台代理接管;

后台代理提醒业务类型:

  • 倒计时类:基于倒计时的提醒功能,适用于短时的计时提醒业务;
  • 日历类:基于日历的提醒功能,适用于较长时间的提醒业务;
  • 闹钟类:基于时钟的提醒功能,适用于指定时刻的提醒业务;

后台代理提醒服务通过 reminderAgentManager 模块提供提醒定义、创建提醒、取消提醒等能力;

例如,新增一个 9 点的喝水提醒,过程如图:

在整个流程中,应用仅需:

  1. 使用 reminderAgentManager 模块的 ReminderRequest 类定义提醒实例;
  2. 使用 reminderAgentManager 模块的 publishReminder 接口发布提醒;

而无需关注计时和通知发布等功能如何实现;

前置条件,实现功能的前提如下:

  • 添加后台代理提醒使用权限;

    1
    2
    3
    4
    5
    6
    7
    8
    "module": {
    ...
    "requestPermissions": [
    {
    "name": "ohos.permission.PUBLISH_AGENT_REMINDER"
    }
    ]
    }
  • 导入后台代理提醒 reminderAgentManager 模块,将此模块命名为 reminderAgent;

    1
    import reminderAgent from '@ohos.reminderAgentManager';

新增提醒,实现步骤如下:

  1. 用 reminderAgent.ReminderRequest 类定义提醒实例;

  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
    import reminderAgent from '@ohos.reminderAgentManager';
    ...

    export class ReminderService {
    public addReminder(alarmItem: ReminderItem, callback?: (reminderId: number) => void) {
    let reminder = this.initReminder(alarmItem);
    reminderAgent.publishReminder(reminder, (err, reminderId) => {
    if (callback != null) {
    callback(reminderId);
    }
    });
    }

    private initReminder(item: ReminderItem): reminderAgent.ReminderRequestAlarm {
    return {
    reminderType: item.remindType,
    hour: item.hour,
    minute: item.minute,
    daysOfWeek: item.repeatDays,
    title: item.name,
    ringDuration: item.duration * Constants.DEFAULT_TOTAL_MINUTE,
    snoozeTimes: item.intervalTimes,
    timeInterval: item.intervalMinute,
    actionButton: [
    {
    title: '关闭',
    type: reminderAgent.ActionButtonType.ACTION_BUTTON_TYPE_CLOSE
    },
    ...
    ],
    wantAgent: {
    pkgName: globalThis.bundleName,
    abilityName: globalThis.abilityName
    },
    notificationId: item.notificationId,
    ...
    }
    }

    ...
    }

删除提醒,可以调用 cancelReminder() 接口实现;

1
2
3
4
5
6
7
8
9
10
import reminderAgent from '@ohos.reminderAgentManager';
...

export class ReminderService {
public deleteReminder(reminderId: number) {
reminderAgent.cancelReminder(reminderId);
}

...
}

修改提醒,则需要先进行旧提醒的删除,再新增新的提醒;

1
2
3
4
5
6
7
8
9
10
11
12
13
public async setAlarmRemind(alarmItem: AlarmItem) {
let index = await this.findAlarmWithId(alarmItem.id);
if (index !== Constants.DEFAULT_NUMBER_NEGATIVE) {
this.reminderService.deleteReminder(alarmItem.id);
} else {
...
}

this.reminderService.addReminder(alarmItem, (newId) => {
alarmItem.id = newId;
...
})
}

三方库

三方库在系统能力的基础上,提供了更加方便的使用,在许多场景下,能够极大提升开发者的开发效率;

目前提供了两种途径获取开源三方库:

  1. 通过访问 Gitee 网站开源社区获取:

    在 Gitee 中,搜索 OpenHarmony-TPC 仓库,在 tpc_resource 中对三方库进行了资源汇总;

  2. 通过 OpenHarmony 三方库中心仓获取:

    进入 OpenHarmony 三方库中心仓,根据类型或者直接搜索寻找需要的三方库;


常用三方库

常用的三方库可以分为 UI、动画、网络、图片、多媒体、数据存储、安全、工具等;例如:

  • UI 库:

    @ohos/textlayoutbuilder:可以定制任一样式的文本构建工具,包括字体间距、大小、颜色、富文本高亮显示等;

    @ohos/roundedimageview:可以生成圆角矩形、或者椭圆形等图片形状;

  • 网络库:

    @ohos/axios:可以运行在 node.js 和浏览器中,基于 Axios 原库 v1.3.4 版本进行适配,并沿用其现有用法和特性;

  • 动画库:

    @ohos/lottie:可以解析 Adobe After Effects 软件通过 Bodymovin 插件导出的 json 格式的动画,并在移动设备上进行本地渲染;

    @ohos/svg:可以解析 SVG 图片并渲染到页面上;


@ohos/lottie

@ohos/lottie 是基于 lottie-web 开发,集成在三方库社区内的开源版本,是 HarmonyOS 系统中复杂动画的一种解决方案;

动画是传达想法和创造更好的用户交互体验的工具,常见使用动画的场景如下:

  • 启动动画:APP logo 动画的播放;
  • 加载动画:网络请求的 loading 动画;
  • 上下拉刷新动画:请求更多资源时的刷新动画;
  • 按钮动画:切换按钮、编辑按钮、播放按钮等按钮的切换过渡动画;
  • 视图转场动画:一些场景的转场添加动画能够提升用户体验;

@ohos/lottie 提供了使用 JSON 动画文件的解决方案,开发者可以在原生应用中像使用静态图像一样使用动画,而不用关注动画的实现过程,并且 @ohos/lottie 具有一套完整的 API 控制动画的行为,可以让动画更具有交互性;


安装与卸载

  • 安装 @ohos/lottie:

    通过 ohpm 执行对应的指令,将 lottie 安装到项目中;

    1
    ohpm install @ohos/lottie
  • 卸载 @ohos/lottie:

    通过 ohpm 执行卸载指令,将 lottie 从项目中删除,其程序包和配置信息将会从项目中移除;

    1
    ohpm uninstall @ohos/lottie

库的使用

  • @ohos/lottie 的引入:

    通过 import 指令在项目中引入 @ohos/lottie 到文件中;

    1
    import lottie from '@ohos/lottie';
  • 构建 Canvas 画布:

    @ohos/lottie 解析 JSON 动画文件的数据需要基于 Canvas 画布进行 2D 渲染,所以在加载 JSON 动画之前,要先初始化渲染上下文,并在画面中创建 Canvas 画布区域,将对应的渲染上下文 renderingContext 传递给 Canvas;

    1
    2
    3
    4
    5
    6
    7
    // 初始化渲染上下文  
    private renderingSettings: RenderingContextSettings = new RenderingContextSettings(true) // 设置开启抗锯齿
    private renderingContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.renderingSettings) // 创建2D渲染上下文

    // 加载Canvas画布
    Canvas(this.renderingContext)
    ...
  • 使用 @ohos/lottie 加载 JSON 动画:

    在 loadAnimation 方法中需配置相应的初始设置,包括渲染上下文、渲染方式以及 JSON 动画资源的路径等;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 用animationItem实例接收
    let animationItem = lottie.loadAnimation({
    container: this.renderingContext, // 渲染上下文
    renderer: 'canvas', // 渲染方式
    loop: true, // 是否循环播放,默认true
    autoplay: true, // 是否自动播放,默认true
    path: 'common/lottie/data.json', // json路径
    })
    // 直接使用loadAnimation方法
    lottie.loadAnimation({ // 或者直接使用
    container: this.renderingContext, // 渲染上下文
    renderer: 'canvas', // 渲染方式
    loop: true, // 是否循环播放,默认true
    autoplay: true, // 是否自动播放,默认true
    path: 'common/lottie/data.json', // json路径
    })
  • @ohos/lottie 控制动画:

    其中,封装了包括状态控制、进度控制、播放设置控制和属性控制等多个 API,用户可以利用这些 API 完成对动画的控制,实现更加灵活的交互效果;

    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
    // 播放、暂停、停止、销毁  可以使用lottie,也可以使用animationItem实例进行控制
    lottie.play(); // 从目前停止的帧开始播放
    lottie.stop(); // 停止播放,回到第0帧
    lottie.pause(); // 暂停该动画,在当前帧停止并保持
    lottie.togglePause(); // 切换暂停/播放状态
    lottie.destroy(); // 删除该动画,移除相应的元素标签等。在unmount的时候,需要调用该方法

    // 播放进度控制
    animationItem.goToAndStop(value, isFrame); // 跳到某个时刻/帧并停止。isFrame(默认false)指示value表示帧还是时间(毫秒)
    animationItem.goToAndPlay(value, isFrame); // 跳到某个时刻/帧并进行播放
    animationItem.goToAndStop(30, true); // 例:跳转到第30帧并停止
    animationItem.goToAndPlay(300); // 例:跳转到第300毫秒并播放

    // 控制帧播放
    animationItem.setSegment(5,15); // 限定动画资源播放时的整体帧范围,即设置动画片段
    animationItem.resetSegments(5,15); // 重置播放的动画片段
    animationItem.playSegments(arr, forceFlag); // arr可以包含两个数字或者两个数字组成的数组,forceFlag表示是否立即强制播放该片段
    animationItem.playSegments([10,20], false); // 例:播放完之前的片段,播放10-20帧
    animationItem.playSegments([[5,15],[20,30]], true); //例: 直接播放5-15帧和20-30帧

    // 动画基本属性控制
    lottie.setSpeed(speed); // 设置播放速度,speed为1表示正常速度
    lottie.setDirection(direction); // 设置播放方向,1表示正向播放,-1表示反向播放

    // 获取动画帧数属性
    animationItem.getDuration(); //获取动画时长
  • 事件订阅:

    在一些特殊场景下,例如,开始加载动画或动画播放结束时,可能需要执行相应的操作;

    在 @ohos/lottie 中提供了事件订阅和取消订阅的功能,当触发对应的 event,会执行传入的回调函数,用户可以在回调函数中完成要实现的功能;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 订阅事件
    animationItem.addEventListener(event,function(){
    // TODO something
    })

    // 取消订阅事件
    animationItem.removeEventListener(event,function(){
    // TODO something
    })

    常见的 event 事件类型如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // event事件类型
    'enterFrame' // 每进入一帧就会触发
    'loopComplete' // 当前循环下播放(循环播放/非循环播放)结束时触发
    'complete' // 播放完成时触发
    'segmentStart' // 播放指定片段时触发,playSegments、resetSegments等方法刚开始播放指定片段时会发出,如果playSegments播放多个片段,多个片段最开始都会触发。
    'destroy' // 销毁动画时触发
    'data_ready' // 数据准备完成
    'DOMLoaded' // 动画相关dom已经被添加
    'error' // 出现错误
    'data_failed' // 数据加载失败
    ...

云开发

HarmonyOS 云开发是 DevEco Studio 新推出的功能,可以让开发者在一个项目工程中,使用一种语言完成端侧和云侧功能的开发;

基于 AppGallery Connect Serverless 构建的云侧能力,让开发者无需构建和管理云端资源,随需使用,大大提高构建应用/元服务的效率;

  • 认证服务:可以为应用快速构建安全可靠的用户认证系统;
  • 云函数:一方面将开发测试的对象聚焦到函数级别,可以大幅简化应用开发与运维相关的事务;另一方面通过云函数SDK,可以便捷操作云数据库、云存储等,提升业务功能构建的便利性;
  • 云数据库:在保证数据的可用性、可靠性、一致性,以及安全等特性基础上,能够实现数据在端云之间的无缝同步,可以帮助开发者快速构建端云、多端协同的应用;
  • 云存储:提供可伸缩、免维护的云端存储服务,可用于应用上传图片、音频、视频或者其他用户生成的内容;

随着应用功能越来越丰富,很多应用的运行都依赖云侧的支撑;相比于传统开发模式,云开发模式具备成本低、效率高、门槛低等优势;

区别点 传统开发模式 云开发模式
开发工具 端侧与云侧各需一套开发工具,云侧需自建服务器,工具成本高 DevEco Studio 一套开发工具即可支撑端侧与云侧同时开发,无需搭建服务器,工具成本低
开发人员 端侧与云侧要求不同的开发语言,技能要求高;需多人投入,且开发人员之间需持续、准确沟通,人力与沟通成本高、效率低 依托 AppGallery Connect(以下简称 AGC)Serverless 云服务开放的接口,端侧开发人员也能轻松开发云侧代码,大大降低开发门槛;开发人员数量少,降低人力成本,提高沟通效率。
运维 需自行构建运营与运维能力,成本高、负担重 直接接入 AGC Serverless 云服务,实现免运维,无运维成本或资源浪费

工程模板

当前 DevEco Studio 提供了两类工程模板:预置的通用云开发模板和从模板市场下载的云开发模板;

  • 通用云开发模板:提供了认证服务、云函数、云存储服务的示例工程;

  • 从模板市场下载的模板:基于业务场景,提供了特定场景下的常用功能;例如:商城模板:


工程结构

HarmonyOS 云开发工程分为三部分:

  • 端开发工程(Application):主要用于开发应用端侧的业务代码;
  • 云开发工程(CloudProgram):主要用于云侧功能的配置、开发、部署;
  • 端侧公共库(External Libraries):主要包含了JDK的扩展类库;

工程创建与配置

  1. 打开 DevEco Studio,菜单选择“File > New > Create Project”;

    • HarmonyOS 应用选择“Application”;
    • 元服务选择“Atomic Service”;
    • 模板选择“Empty Ability with CloudDev”;
  2. 填写工程信息后,点击“Next”;

  3. 点击“Sign in”使用华为开发者帐号登录工程;

  4. 选择应用/元服务所属的团队,系统将根据包名自动关联出 AppGallery Connect 上已创建的 HarmonyOS 应用或者元服务,点击“Next”;

  5. 关联成功后,如果帐号所属的团队尚未签署云开发相关协议,点击协议链接仔细阅读协议内容后,勾选同意协议,点击“Finish”,即可完成工程的创建;

  6. DevEco Studio 自动完成一些初始化配置;

    • 自动开通云开发相关服务,包括:认证服务、云函数、云数据库、云托管、API网关、云存储;

    • 端侧工程中自动集成agconnect-services.json配置文件和相关服务最新HarmonyOS SDK;

    • 云侧工程自动集成云数据库最新的Node.js Server SDK;