鸿蒙应用开发需要使用配套的IDE——HUAWEI DevEco Studio。 DevEco Studio基于IntelliJ IDEA Community(IDEA社区版)构建,为鸿蒙应用提供了一站式开发环境,集成了开发、运行、调试以及发布应用的各项功能。
以下是具体的安装步骤:
deveco-studio-3.1.0.501.exe
,进入安装向导,点击next
即可next
next
Install
开始安装Finish
完成安装安装完成后,可按以下步骤完成初始化配置
Agree
表示同意nodejs
和ohpm
选择合适的安装路径,完成后点击Next
即可注:
nodejs
用于为鸿蒙应用的自动化构建工具提供运行环境。ohpm
(OpenHarmony Package Manager)是鸿蒙生态第三方库的包管理工具,支持共享包的发布、安装和依赖管理。HarmonyOS SDK
选择安装目录,完成后点击Next
Accept
表示同意,完成后点击Next
Next
Finish
完成配置Configure
下的Settings
。Theme
中可选择主题下面我们创建第一个鸿蒙应用项目
Create Project
Empty Ability
即可。可能需要调整的配置项如下,其余保持默认即可,配置完成后,点击Finish
即可
项目结构相对复杂,先简单了解即可,随之后序学习的深入再逐步为大家介绍
DevEco Studio提供了多种方式用于运行项目,包括预览、模拟器和真机运行。下面逐一演示
Previewer
预览用于查看应用的UI界面效果,方便随时调整界面UI布局。只需打开需要预览的页面文件,例如下图中的Index.ets
,然后点击IDE右侧的Perviewer
即可看到预览效果。
Previewer
预览器主要用于查看界面UI效果,如需对项目进行更加深入的测试,可以使用模拟器运行项目。初次使用需要先安装模拟器,安装步骤如下
Tools
菜单下的Device Manager
,打开设备管理器Install
,安装模拟器Install
后,会首先下载模拟器相关的SDK,下载完成后点击Finish
即可。New Emulator
。Phone
,完成后点击Next
api9
版本Finish
Next
Finish
,至此模拟器安装完毕使用模拟器运行应用时,会占用电脑较多的资源,并且有些功能无法进行测试。当模拟器不满足要求时,可选择真机运行。真机运行的步骤如下
Harmony OS
系统的手机,系统版本最好为4.0.0
及以上,系统版本可在设置/关于手机中查看始终允许使用这台计算机进行调试
,然后点击确定
Open signing configs
进行配置即可。Sign In
HarmonyOS 应用的主要开发语言是 ArkTS,它由 TypeScript(简称TS)扩展而来,在继承TypeScript语法的基础上进行了一系列优化,使开发者能够以更简洁、更自然的方式开发应用。值得注意的是,TypeScript 本身也是由另一门语言 JavaScript 扩展而来。因此三者的关系如下图所示
TypeScript提供了一个线上的 Playground 供练习使用,地址为https://www.typescriptlang.org/zh/play。
除去线上的运行环境,我们也可以在本地搭建一个 TS 的运行环境。
VSCode是一款轻量级、开源且功能丰富的集成开发环境(IDE),支持多种编程语言,具有强大的插件系统。下载地址为:https://code.visualstudio.com/
Code Runner是一款在VSCode中使用的插件,它提供了简便的代码执行功能,支持多种编程语言,使开发者能够快速运行和调试代码片段。
ts-node是一个 TypeScript 的运行环境,它允许我们直接运行 TypeScript 代码。ts-node的安装和运行依赖于Node.js环境,因此在安装ts-node之前,我们需要准备好Node.js环境。
准备Node.js环境需要完成以下两步操作
安装Node.js由于前边在部署DevEco Studio时,已经下载并安装了Node.js,因此这一步可以略过。配置环境变量为了方便在终端执行Node.js相关的命令,我们需要将Node.js的安装目录加入到Path
环境变量下,具体操作如下首先在DevEco Studio的设置界面查看Node.js的安装目录然后打开环境变量配置面板,按下
Win
+R
,唤起运行窗口,之后运行命令sysdm.cpl
之后点击高级选项卡,并点击环境变量
然后在系统变量中选中
Path
,并点击编辑之后点击新建,并填入Node.js的安装目录,完成后点击确定。
在配置完Node.js环境后,便可在终端执行以下命令来安装ts-node了。
npm install -g ts-node
注:完成后需要重新启动VSCode,另其重新加载环境变量和相关依赖。
在完成上述环境的准备后,就可以编写Typescript程序并运行了,具体操作如下
首先在合适的位置创建一个工程目录,例如D:\workspace\hello-ts
,然后使用VSCode打开目录
之后创建Typescript文件,点击New File
注意,文件的后缀为.ts
之后就可以编写Typescript代码并运行了
let
用于声明变量,而const
用于声明常量,两者的区别是变量在赋值后可以修改,而常量在赋值后便不能再修改。
const b:number = 200;
如果一个变量或常量的声明包含了初始值,TS 便可以根据初始值进行类型推断,此时我们就可以不显式指定其类型,例如
let c = 60;console.log(typeof c); //number
number`表示数字,包括整数和浮点数,例如: `100`、`-33` 、`2.5`、`-3.9let a :number = 100let b :number = -33let c :number = 2.5let d :number = -3.9
string`表示字符串,例如: `你好`、`hellolet a:string = '你好'let b:string = 'hello'
boolean`表示布尔值,可选值为:`true`、`falselet isOpen:boolean = truelet isDone:boolean = false
数组类型定义由两部分组成,元素类型[]
,例如number[]
表示数字数组,string[]
表示字符串数组,数组类型的变量可由数组字面量——[item1,item2,item3]
进行初始化。
let a: number[] = []let b: string[] = ['你好', 'hello']
在TS中,对象(object)是一种一系列由属性名称和属性值组成的数据结构,例如姓名:'张三', 年龄:10, 性别:'男'
。对象类型的声明需要包含所有属性的名称及类型,例如{name: string, age: number, gender: string}
,对象类型的变量可以通过对象字面量——{name:'张三', age:10, gender:'男'}
进行初始化。
let person: {name:string, age:number, gender:string} = {name:'张三', age:10, gender:'男'};
声明函数的基础语法如下
可选参数通过参数名后的?
进行标识,如以下案例中的gender?
参数。
function getPersonInfo(name: string, age: number, gender?: string): string { if (gender === undefined) { gender = '未知' } return `name:${name},age:${age},gender:${gender}`; } let p1 = getPersonInfo('zhagnsan', 10, '男') let p2 = getPersonInfo('lisi', 15); console.log(p1); console.log(p2);
注:调用函数时,未传递可选参数,则该参数的值为undefined
。
可在函数的参数列表为参数指定默认值,如以下案例中的gender: string='未知'
参数。
function getPersonInfo(name: string, age: number, gender: string='未知'): string { return `name:${name},age:${age},gender:${gender}`; } let p1 = getPersonInfo('zhagnsan', 10, '男') let p2 = getPersonInfo('lisi', 15); console.log(p1); console.log(p2);
一个函数可能用于处理不同类型的值,这种情况可以使用联合类型,例如以下案例中的message: number | string
function printNumberOrString(message: number | string) { console.log(message) } printNumberOrString('a') printNumberOrString(1)
若函数需要处理任意类型的值,则可以使用any
类型,例如以下案例中的message: any
function print(message:any) { console.log(message) } print('a') print(1) print(true)
若函数没有返回值,则可以使用void
作为返回值类型,其含义为空。
function test(): void {console.log('hello');}
函数的返回值类型可根据函数内容推断出来,因此可以省略不写。
function test() {console.log('hello');}function sum(a: number, b: number) {return a + b;}
匿名函数的语法结构简洁,特别适用于简单且仅需一次性使用的场景。
let numbers: number[] = [1, 2, 3, 4, 5] numbers.forEach(function (number) { console.log(number); })
注意:匿名函数能够根据上下文推断出参数类型,因此参数类型可以省略。
匿名函数的语法还可以进一步的简化,只保留参数列表和函数体两个核心部分,两者用=>
符号连接。
let numbers: number[] = [1, 2, 3, 4, 5] numbers.forEach((num) => { console.log(num) })
类(class)是面向对象编程语言中的一个重要概念。
面向对象编程(Object-Oriented Programming,简称OOP)是一种编程范式,其核心理念在于将程序中的数据与操作数据的方法有机地组织成对象,从而使程序结构更加模块化和易于理解。通过对象之间的协同合作,实现更为复杂的程序功能。
类(class)是对象的蓝图或模板,它定义了对象的属性(数据)和行为(方法)。通过类可以创建多个具有相似结构和行为的对象。例如定义一个 Person
类,其对象可以有张三
、李四
等等。
定义类的语法如下图所示
代码如下:
class Person { id: number; name: string; age: number = 18; constructor(id: number, name: string) { this.id = id; this.name = name; } introduce(): string { return `hello,I am ${this.name},and I am ${this.age} years old` } }
创建对象的关键字为new
,具体语法如下
let person = new Person(1,'zhangsan');
console.log(person.name); //读 person.name = 'lisi'; //写 console.log(person.name);
对象创建后,便可通过对象调用类中声明的方法,如下
let intro = person.introduce(); console.log(intro);
Typescript 中的类中可以包含静态成员(静态属性和静态方法),静态成员隶属于类本身,而不属于某个对象实例。静态成员通用用于定义一些常量,或者工具方法。
定义静态成员需要使用static
关键字。
class Constants{ static count:number=1; } class Utils{ static toLowerCase(str:string){ return str.toLowerCase(); } } console.log(Constants.count); console.log(Utils.toLowerCase('Hello World'));
静态成员无需通过对象实例访问,直接通过类本身访问即可。
console.log(Constants.count); console.log(Utils.toLowerCase('Hello World'));
继承是面向对象编程中的重要机制,允许一个类(子类或派生类)继承另一个类(父类或基类)的属性和方法。子类可以直接使用父类的特性,并根据需要添加新的特性或覆盖现有的特性。这种机制赋予面向对象程序良好的扩展性。
下面通过一个例子演示继承的特性
class Student extends Person {classNumber: string;constructor(id: number, name: string, classNumber: string) {super(id, name);this.classNumber = classNumber;}introduce(): string {return super.introduce()+`, and I am a student`;}}let student = new Student(1,'xiaoming','三年二班');console.log(student.introduce());
注意:
extends
super()
调用父类构造器对继承自父类的属性进行初始化。this
关键字访问继承自父类的属性和方法。super
关键字访问父类定义的方法。访问修饰符(Access Modifiers)用于控制类成员(属性、方法等)的可访问性。TypeScript提供了三种访问修饰符,分别是private、protected和public。
class Person {private id: number;protected name: string;public age: number;constructor(id: number, name: string, age: number) {this.id = id;this.name = name;this.age = age;}}class Student extends Person {}
说明:
接口(interface)是面向对象编程中的另一个重要概念。接口通常会作为一种契约或规范让类(class)去遵守,确保类实现某些特定的行为或功能。
接口使用interface
关键字定义,通常情况下,接口中只会包含属性和方法的声明,而不包含具体的实现细节,具体的细节由其实现类完成。
interface Person { id: number; name: string; age: number; introduce(): void; }
接口的实现需要用到implements
关键字,实现类中,需要包含接口属性的赋值逻辑,以及接口方法的实现逻辑。
class Student implements Person { id: number; name: string; age: number; constructor(id: number, name: string, age: number) { this.id = id; this.name = name; this.age = age; } introduce(): void { console.log('Hello,I am a student'); } }
多态是面相对象编程中的一个重要概念,它可以使同一类型的对象具有不同的行为。下面我们通过一个具体的案例来体会多态这一概念
首先,再创建一个Person
接口的实现类Teacher
,如下
class Teacher implements Person {id: number;name: string;age: number;constructor(id: number, name: string, age: number) {this.id = id;this.name = name;this.age = age;}introduce(): void {console.log('Hello,I am a teacher');}}
然后分别创建一个Student
对象和一个Teacher
对象,注意两个对象的类型均可以设置Person
,如下
let p1: Person = new Student(1, 'zhangsan', 17);let p2: Person = new Teacher(2, 'lisi', 35);
最后分别调用p1
和p2
的introduce()
方法,你会发现,同样是Person
类型的两个对象,调用同一个introduce()
方法时,表现出了不同的行为,这就是多态。
p1.introduce();//Hello,I am a studentp2.introduce();//Hello,I am a teacher
在传统的面向对象编程的场景中,接口主要用于设计和组织代码,使代码更加容易扩展和维护。下面举例说明。
假如现在需要实现一个订单支付系统,按照面向对象编程的习惯,首先需要定义一个订单类(Order),如下
class Order {totalAmount: number;constructor(totalAmount: number) {this.totalAmount = totalAmount;}pay() {console.log(`AliPay:${this.totalAmount}`);}}
很容易预想到,这个系统将来可能需要支持其他的支付方式,为了方便代码支持新的支付方式,我们可以对代码进行如下改造。
首先定义一个支付策略的接口,接口中声明一个pay
方法,用来规范实现类必须实现支付逻辑。
interface PaymentStrategy {pay(amount: number): void;}
然后在订单类中增加一个PaymentStrategy
的属性,并且在订单类中的pay
方法中调用PaymentStrategy
的pay
方法,如下
class Order {totalAmount: number;paymentStrategy: PaymentStrategy;constructor(totalAmount: number, paymentStrategy: PaymentStrategy) {this.totalAmount = totalAmount;this.paymentStrategy = paymentStrategy;}pay() {this.paymentStrategy.pay(this.totalAmount);}}
这样改造完之后,就可以很容易的在不改变现有代码的情况下,支持新的支付方式了。
比如现在需要支持AliPay
,那我们就可以创建AliPay
这个类(class)并实现(implement)PaymentStrategy
这个接口,如下
class AliPay implements PaymentStrategy {pay(amount: number): void {console.log(`AliPay:${amount}`);}}
这样一来,之后创建的订单就可以使用AliPay
这个支付方式了。
let order = new Order(1000,new AliPay());order.pay();
TypeScript 中的接口是一个非常灵活的概念,除了用作类的规范之外,也常用于直接描述对象的类型,例如,现有一个变量的定义如下
let person: {name:string, age:number, gender:string} = {name:'张三', age:10, gender:'男'};
可以看到变量的值为一个一般对象,变量的类型为{name:string, age:number, gender:string}
,此时就可以声明一个接口来描述该对象的类型,如下
interface Person {name: string;age: number;gender: string;}let person: Person = {name:'张三', age:10, gender:'男'};
枚举(Enumeration)是编程语言中常见的一种数据类型,其主要功能是定义一组有限的选项,例如,方向(上、下、左、右)或季节(春、夏、秋、冬)等概念都可以使用枚举类型定义。
枚举的定义需使用enum
关键字,如下
enum Season { SPRING, SUMMER, AUTUMN, WINTER }
枚举的使用记住两个原则即可
像访问对象属性一样访问枚举值,例如Season.SPRING
枚举值的类型为enum
的名称,例如Season.SPRING
和Season.SUMMER
等值的类型都是Season
let spring:Season = Season.SPRING;
现需要编写一个函数move
,其功能是根据输入的方向(上、下、左、右)进行移动,此时就可以先使用枚举定义好所有可能的输入选项,如下
enum Direction { UP, BOTTOM, LEFT, RIGHT }
move
函数的实现如下
function move(direction: Direction) { if(direction===Direction.UP){ console.log('向上移动'); }else if(direction===Direction.BOTTOM){ console.log('向下移动'); }else if(direction===Direction.LEFT){ console.log('向左移动'); }else{ console.log('向右移动'); } } move(Direction.UP);
在TypeScript 中,枚举实际上是一个对象,而每个枚举值都是该对象的一个属性,并且每个属性都有具体的值,属性值只支持两种类型——数字或字符串。
默认情况下,每个属性的值都是数字,并且从 0
开始递增,例如上述案例中的Direction
枚举中,Direction.UP
的值为0
,Direction.BOTTOM
的值为1
,依次类推,具体如下
console.log(Direction.UP) //0console.log(Direction.BOTTOM) //1console.log(Direction.LEFT) //2console.log(Direction.RIGHT) //3
除了使用默认的数字作为属性的值,我们还能手动为每个属性赋值,例如
enum Direction {UP = 1,BOTTOM = 2,LEFT = 3,RIGHT = 4}console.log(Direction.UP) //1console.log(Direction.BOTTOM) //2console.log(Direction.LEFT) //3console.log(Direction.RIGHT) //4
再例如
enum Direction {UP = 'up',BOTTOM = 'bottom',LEFT = 'left',RIGHT = 'right'}console.log(Direction.UP) //upconsole.log(Direction.BOTTOM) //bottomconsole.log(Direction.LEFT) //leftconsole.log(Direction.RIGHT) //right
通过为枚举属性赋值,可以赋予枚举属性一些更有意义的信息,例如以下枚举
enum Color {Red = 0xFF0000,Green = 0x00FF00,Blue = 0x0000FF}enum FontSize {Small = 12,Medium = 16,Large = 20,ExtraLarge = 24}
模块化是指将复杂的程序拆解为多个独立的文件单元,每个文件被称为一个模块。在 TypeScript 中,默认情况下,每个模块都拥有自己的作用域,这意味着在一个模块中声明的任何内容(如变量、函数、类等)在该模块外部是不可见的。为了在一个模块中使用其他模块的内容,必须对这些内容进行导入、导出。
导出须使用export
关键字,语法如下
export function hello() { console.log('hello module A'); } export const str = 'hello world'; const num = 1;
导入须使用import
关键字,语法如下
import { hello, str } from './moduleA'; hello(); console.log(str);
若多个模块中具有命名相同的变量、函数等内容,将这些内容导入到同一模块下就会出现命名冲突。例如,在上述案例的基础上,又增加了一个 moduleC,内容如下
export function hello() {console.log('hello module C');}export const str = 'module C';
moduleB 同时引入 moduleA 和 moduleC 的内容,如下,显然就会出命名冲突
import { hello, str } from "./moduleA";import { hello, str } from "./moduleC";hello() //?console.log(str); //?
有多种方式可以用来解决命名冲突,下面逐一介绍
语法如下
import { hello as helloFromA, str as strFromA } from "./moduleA"; import { hello as helloFromC, str as strFromC } from "./moduleC"; helloFromA(); console.log(strFromA); helloFromC(); console.log(strFromC);
上述导入重命名的方式能够很好的解决命名冲突的问题,但是当冲突内容较多时,这种写法会比较冗长。除了导入重命名外,还可以将某个模块的内容统一导入到一个模块对象上,这样就能简洁有效的解决命名冲突的问题了,具体语法如下
import * as A from "./moduleA"; import * as C from "./moduleC"; A.hello(); console.log(A.str); C.hello(); console.log(C.str);
除了上述导入导出的语法之外,还有一种语法,叫做默认导入导出,这种语法相对简洁一些。
默认导出允许一个模块指定一个(最多一个)默认的导出项,语法如下
export default function hello(){ console.log('moduleA'); }
由于每个模块最多有一个默认导出,因此默认导入无需关注导入项的原始名称,并且无需使用{}
。
import helloFromA from "./moduleA";
由于默认导入时无需关注导入项的名称,所以默认导出支持匿名内容,比如匿名函数,语法如下
export default function () { console.log('moduleB'); }
ArkTS 在继承了Typescript语法的基础上,主要扩展了声明式UI开发相关的能力。
声明式UI是一种编写用户界面的范式。下面通过一个具体案例来学习这种开发范式,假如现在要实现如下界面
按照声明式UI的开发范式,首先需要分析和定义页面的各种状态,并声明相应的状态变量用于表示不同的状态。
当前案例中,界面共有两个状态,分别是开灯和关灯状态,所以我们可以使用一个boolean
类型的变量来表示这两个状态,true
表示开灯,false
表示关灯。如下:
@State isOn: boolean = false;
说明:@State
用于声明该变量为状态变量。
在分析完界面状态后,我们需要准确的描述界面在不同状态下的显示效果。
在当前案例中,具体逻辑如下图所示
在明确了界面在不同状态下的显示效果后,我们只需修改状态变量的值,就能触发界面的更新。
在当前案例中,若我们将isOn
的值改为true
,那么界面上就会显示开灯的图片,否则就会显示关灯的图片。
为了实现点击按钮开/关灯的效果,我们可以为按钮绑定点击事件:
isOn
的值改为true
。isOn
的值改为false
。以上就是声明式UI开发范式的大致流程,下面为大家总结一下声明式UI的核心思想
开发者只需描述在界面在不同状态下要呈现的最终效果,而无需关注界面变化的具体过程。
开发者只需修改状态变量的值,界面就会自动更新。
在鸿蒙开发中,组件是构成界面的最小单元,我们所看到的界面,都是由众多组件组合而成的,所以编写界面其实就是组合组件的过程,ArkTS提供了很多的内置组件,例如:Text
、Button
、Image
等等;并且ArkTS还支持自定义组件,让开发者可根据具体需求自定义组件中的内容。
案例的最终效果如下图所示
案例的完整代码见Demos/entry/src/main/ets/pages/helloworld/light/solution/Light.ets
@Entry@Componentstruct LightPage {@State isOn: boolean = false;build() {Column({ space: 20 }) {if (this.isOn) {Image('pages/helloworld/light/solution/images/img_light.png').height(300).width(300).borderRadius(20)} else {Image('pages/helloworld/light/solution/images/img_dark.png').height(300).width(300).borderRadius(20)}Row({ space: 50 }) {Button('关灯').onClick(() => {this.isOn = false})Button('开灯').onClick(() => {this.isOn = true;})}}.height('100%').width('100%').justifyContent(FlexAlign.Center)}}
下面通过一个相对简单的案例来系统的学习 ArkTS 声明组件的语法,案例的最终效果如下,完整代码见Demos/entry/src/main/ets/pages/helloworld/delete/DeleteButton.ets
声明组件的完整语法如下图所示
各部分语法说明如下
如果组件的定义包含参数,可在组件名称后面的()
中配置相应参数。各组件支持的参数,可查看 API 文档,查看方式如下
Show in API Reference
,就会弹出 API 文档如果组件支持子组件配置,可在()
后的{}
中添加子组件,若不支持子组件,则不需要写{}
。
属性方法用于配置组件的样式和其他属性,可以在组件声明的末尾进行链式调用。各组件支持的属性可查看 API 文档,除去每个组件的专有属性,还有各组件都能配置的通用属性,通用属性也可通过 API 文档查看。
事件方法用于为组件绑定交互事件,可以在组件声明的末尾进行链式调用。各组件的支持的事件可查看 API 文档,除去每个组件的专有事件,还有各组件都支持的通用事件,通用事件也可通过 API 文档查看。
除去系统预置的组件外,ArkTS 还支持自定义组件。使用自定义组件,可使代码的结构更加清晰,并且能提高代码的复用性。
自定义组件的语法如下图所示
各部分语法说明如下:
**struct**
关键字struct
是ArkTS新增的用于自定义组件或者自定义弹窗的关键字。其声明的数据结构和TS中的类十分相似,可包含属性和方法。
**build**
方法build()
方法用于声明自定义组件的UI结构。
组件属性可用作自定义组件的参数,使得自定义组件更为通用。
**@Compnent**
装饰器@Component
装饰器用于装饰struct
关键字声明的数据结构。struct
被@Component
装饰后才具备组件化的能力。
注: 装饰器是Typescript中的一种特殊语法,常用于装饰类、方法、属性,用于修改或扩展其原有的行为。
在学完自定义组件的语法之后,我们会发现前文案例中的每个页面实际上都是一个自定义组件。但是和自定义组件的语法相比,前边的每个案例还会多出一个@Entry
装饰器,那@Entry
的作用又是啥呢?
在鸿蒙应用中,每个页面都是由一些列组件组合而成的,并且这些组件都是逐层嵌套的,因此这些组件最终形成了一个组件树的结构,如下图所示
我们前边所编写的每个页面就相当于是组件树的根节点,而@Entry
装饰器的作用就是标识该组件为组件树的根节点,也就是一个页面的入口组件。
现在需要对前文的开/关灯的案例做出如下改造,由于两个按钮的结构十分相似,所以可考虑自定义一个按钮组件,然后进行复用。
案例的完整代码见:Demos/entry/src/main/ets/pages/helloworld/custom/solution/Light.ets
条件渲染可根据应用的不同状态渲染不同的UI界面,例如前文的开/关灯案例,以及以下的播放/暂停案例,均可使用条件渲染实现。
案例的完整代码见:Demos/entry/src/main/ets/pages/helloworld/condition/solution/PlayAndPausePage.ets
条件渲染的语法如下
if (...){//UI描述}else if (...){//UI描述}else{//UI描述}
循环渲染可使用ForEach
语句基于一个数组来快速渲染一个组件列表,例如以下案例中的选项列表就可通过循环渲染实现。
案例的完整代码见:Demos/entry/src/main/ets/pages/helloworld/foreach/solution/FruitPage.ets
ForEach
循环渲染的语法如下
ForEach(arr: any[],itemGenerator: (item: any, index?: number) => void,keyGenerator?: (item: any, index?: number) => string)
各参数的含义如下
需要进行循环渲染的数据源,必须为数组类型,例如上述案例中的
@State options: string[] = ["苹果", "桃子", "香蕉", "橘子"];
组件生成函数,用于为arr
数组中的每个元素创建对应的组件。该函数可接收两个参数,分别是
arr
数组中的数据项arr
数组中的数据项的索引例如上述案例中的
(item: string) => { Button(item) .width(100) .backgroundColor(Color.Green) .onClick(() => { this.answer = item; }) }
key生成函数,用于为arr
数组中的每个数据项生成唯一的key。
key的作用
ForEach
在数组发生变化(修改数组元素或者向数组增加或删除元素)时,需要重新渲染组件列表,在重新渲染时,它会尽量复用原来的组件对象,而不是为每个元素都重新创建组件对象。key的作用就是辅助ForEach
完成组件对象的复用。具体逻辑如下:ForEach
在进行初次渲染时,会使用keyGenerator为数组中的每个元素生成一个唯一的key,并将key作为组件对象的标识。当数组发生变化导致ForEach
需要重新渲染时,ForEach
会再次使用keyGenerator为每个元素重新生成一遍key,然后ForEach
会检查新生成的key在上次渲染时是否已经存在,若存在,ForEach
就会认为这个key对应的数组元素没有发生变化,那它就会直接复用这个key所对应的组件对象;若不存在,ForEach
就会认为这个key对应的元素发生了变化,或者该元素为新增元素,此时,就会为该元素重新创建一个组件对象。
开发者可以通过keyGenerator函数自定义key的生成规则。如果开发者没有定义keyGenerator函数,则系统会使用默认的key生成函数,即
(item: any, index: number) => { return index + '__' + JSON.stringify(item); }
在某些情况下默认的key生成函数,会导致界面渲染效率低下,此时可考虑通过keyGenerator函数自定义生成逻辑,例如如下场景
状态变量数组定义如下
@State arr:string[] = ["zhangsan","lisi","wangwu"]
ForEach语句如下
Column(){ ForEach(this.arr,(item)=>{ Text(item) }) }
初次渲染时,每个元素对应的key依次为0__"zhagnsan"
、1__"lisi"
、2__"wangwu"
。若现有一个操作是向arr
数组头部插入新的元素,例如新元素为wanger,按照默认的key生成逻辑,插入新元素之后每个元素的key就会依次变为0__"wanger"
、1__"zhagnsan"
、2__"lisi"
、3__"wangwu"
,也就是所有元素的key都发生了变化,因此UI界面更新时需要为每个元素都重新创建组件对象,即便原有的元素没有发生变化也无法复用之前的组件,这样一来就导致了性能浪费。此时我们就可以考虑提供第三个参数,如下
Column(){ ForEach(this.arr, (item)=>{ Text(item) }, item => JSON.stringify(item)) }
Image
为图片组件,用于在应用中显示图片。
**Image**
组件的参数类型为string | Resource | media.PixelMap
,相关案例见Demos/entry/src/main/ets/pages/component/image/parameter/solution/ImageParameter.ets
。
下面对三种参数类型逐一进行介绍。
**string**
类型string
类型的参数用于通过路径的方式引用图片,包括本地图片和网络图片。
Image('images/demo.jpg')
注意:使用这种方式引入本地图片,需要将图片置于ets
目录下,并且需要为Image
组件提供图片相对于ets
目录的路径。
Image('http://xxx/xxx.jpg')
**注意:**真机中运行的鸿蒙应用,访问网络图片需要配置网络访问权限,不过在预览器和模拟器中测试时不受限制。权限配置相关的内容在后续章节会系统介绍。
**Resource**
类型Resource
类型的参数用于引入 resources 目录下的图片。
resources目录用于统一存放应用所需的各种资源,包括图片、音频、视频、文本等等。下面简要介绍 resources 目录的用法,首先需要了解 resources 的目录结构,如下
resources 目录下,用于存放资源的子目录有(element、media、profile)和rawfile。下面分别介绍
element、media、profile(element、media、profile)可存在多种版本,用于适配不同的环境,例如语言环境(zh_CN和en_US)、系统主题(dark和light)、设备类型(phone 和 tablet) 等等。我们可以为上述每种环境各自准备一套资源文件,每种环境对应 resources 下的一个目录,例如上述的 zh_CN 和 en_US。我们在使用resources下的资源时,无需指定具体的环境版本,系统会根据设备所处的环境自动选择匹配的版本,例如当设备系统语言为中文时,则会使用zh_CN目录下的资源,为英文时,则会使用en_US目录下的资源。若没有与当前所处环境相对应的版本,则使用 base 目录下资源**。各目录存储的具体资源如下media存放媒体资源,包括图片、音频、视频等文件。element存放用于描述页面元素的尺寸、颜色、样式等的各种类型的值,每种类型的值都定义在一个相应的 JSON 文件中。profile存放自定义配置文件。rawfile用于存储任意格式的原始文件,需要注意的是rawfile**不会根据设备所处的环境去匹配不同的资源。
总结:
resources目录下可用于存放图片的目录有**resources/*/**media 以及 **resources/**rawfile,两个目录下图片的使用方式有所不同,下面逐一介绍
该目录下的资源,需要使用$r('app.media.<filename>')
的方式引用。
注意:无需指定具体版本,系统会自动根据所处环境选择相应版本
例如上图中的img.png图片,可通过$r('app.media.img')
引用。需要注意的是$r()
的返回值即为 **Resource**
类型,因此可以直接将$r('app.media.img')
作为 Image 组件的参数,例如Image($r('app.media.img'))
。
该目录下的资源,可通过$rawfile('path/to/your/file')
的方式引用,文件的路径为相对于 rawfile 的路径。例如上图中的icon.png,须使用$rawfile('icon.png)
引用。需要注意的是,$rawfile()
的返回值也是Resource类型,因此其也可以直接作为 Image 组件的参数,如Image($rawfile('icon.png))
。
PixelMap指的是图片的像素位图,其通常是一个二维数组,数组中的每个元素对应着图片中的一个像素,其包含了该像素的颜色等信息。像素位图主要用于图片编辑的场景,例如
图片尺寸可通过width()
方法和height()
方法进行设置。例如
Image($r('app.media.img')).width(100).height(100)
两个方法可接收的参数类型均为string | number | Resource
,相关案例见Demos/entry/src/main/ets/pages/component/image/attribute/size/solution/ImageSize.ets
。
下面对三种参数类型逐一进行介绍。
**string**
类型string
类型的参数可为百分比,例如'100%'
,或者为具体尺寸,例如'100px'
。
具体尺寸的单位,常用的有两个,分别是px
和vp
,下面逐个介绍
前置知识(屏幕参数)想要理解上述尺寸单位,需要大家先掌握屏幕相关的几个参数像素屏幕显示的最小单位,屏幕上的一个小亮点称为一个像素。分辨率屏幕上横向和纵向的像素数量。尺寸屏幕对角线的长度,以英寸(inches
)为单位。像素密度像素密度是每英寸屏幕上的像素数量,通常以PPI
(Pixels Per Inch)表示,计算公式如下。较高的像素密度意味着在相同尺寸的屏幕上有更多的像素,从而提供更加清晰和细腻的图像。如下图所示
**px(Pixel)**
物理像素,以像素个数来定义图像尺寸。这种方式的弊端是,在不同像素密度的屏幕上,相同的像素个数对应的物理尺寸是不同的。这样一来就会导致我们的应用在不同设备上显示的尺寸可能不同。如下图所示
**vp(Virtual Pixel)**
为了保证一致的观感,我们可以使用虚拟像素作为单位。虚拟像素是一种可根据屏幕像素密度灵活缩放的单位。1vp相当于像素密度为160ppi的屏幕上的1px。在不同像素密度的屏幕上,HarmonyOS会根据如下公式将虚拟像素换算为对应的物理像素
根据上述公式,不难看出,使用虚拟像素作为单位时,同一尺寸,在像素密度低的屏幕上(单个像素的物理尺寸大),对应的物理像素会更少,相反在像素密度高的屏幕上(单个像素的物理尺寸小),对应的物理像素会更多。因此就能在不同像素密度的屏幕上,获得基本一致的观感了。如下图所示
**number**
类型number
类型的参数,默认以vp
作为单位。
**Resource**
类型Resource
类型参数用于引用resources下的element目录中定义的数值。
引用element目录中的数值,同样需要使用$r()
函数。要了解具体语法,需要先熟悉element目录下的文件内容。
前文提到过,element目录中可保存各种类型的数值,且每种类型的值分别定义在一个JSON文件中。文件中的内容为键值对(name-value
)的形式。具体内容如下
{ "string": [ { "name": "module_desc", "value": "模块描述" }, { "name": "greeting", "value": "你好" } ] }``{ "integer": [ { "name": "width", "value": 150 }, { "name": "height", "value": 150 } ] }
我们可以通过 name
引用相应的 value
。具体语法为$r('app.<data_type>.<name>')
。
注意:无需指定具体版本,系统会自动根据所处环境选择相应版本
例如上述的 greeting
的值,可通过$r('app.string.greeting')
引用,width
的值可通过$r('app.integer.width')
。
当图片的原始大小与Image组件不同时,可通过objectFit()
方法来设置图片的显示效果。该方法的参数类型为ImageFit
枚举类型,可选的枚举值如下
名称 | 描述 |
---|---|
ImageFit.None | 保持原有尺寸显示,不做任何缩放,超出显示区域的部分不显示。 |
ImageFit.Contain | 保持宽高比进行缩小或者放大,使得显示区域刚好包含整个图片。 |
ImageFit.Cover | 保持宽高比进行缩小或者放大,使得图片刚好完全覆盖显示区域。 |
ImageFit.Fill | 不保持宽高比进行放大缩小,使得图片充满显示区域。 |
ImageFit.ScaleDown | 保持宽高比进行缩小或不变(不会放大),使得图片完全显示在显示区域内。 |
ImageFit.Auto | 自适应显示 |
各选项的效果如下图所示
相关案例见:Demos/entry/src/main/ets/pages/component/image/attribute/objectFit/solution/ImageObjectFit.ets
当原图分辨率较低并且需要放大显示时,图片会模糊并出现锯齿。如下图所示
这时可以使用interpolation()
方法对图片进行插值,使图片显示得更清晰。该方法的参数为ImageInterpolation
枚举类型,可选的值有
名称 | 描述 |
---|---|
ImageInterpolation.None | 不使用图片插值。 |
ImageInterpolation.High | 高质量插值,可能会影响图片渲染的速度。 |
ImageInterpolation.Medium | 中等质量插值。 |
ImageInterpolation.Low | 低等质量插值。 |
各选项效果如下图所示
相关案例见:Demos/entry/src/main/ets/pages/component/image/attribute/interpolation/solution/Interpolation.ets
**Text**
为文本组件,用于显示文字内容。
**Text**
组件的参数类型为string | Resource
,下面分别对两个参数类型进行介绍:
Text('我是一段文本')
Resource
类型的参数用于引用 resources/*/element
目录中定义的字符串,同样需要使用$r()
引用。
例如resources/base/element
目录中有一个string.json
文件,内容如下
{ "string": [ { "name": "greeting", "value": "你好" } ] }
此时我们便可通过如下方式引用并显示greeting
的内容。
Text($r('app.string.greeting'))
相关案例见Demos/entry/src/main/ets/pages/component/text/parameter/solution/TextParameterPage.ets
字体大小可通过fontSize()
方法进行设置,该方法的参数类型为string | number| Resource
,下面逐一介绍
string
类型的参数可用于指定字体大小的具体单位,例如fontSize('100px')
,字体大小的单位支持px
、fp
。其中fp(font pixel)
与vp
类似,具体大小也会随屏幕的像素密度变化而变化。
number
类型的参数,默认以fp
作为单位。
Resource
类型参数用于引用resources下的element目录中定义的数值。
相关案例见Demos/entry/src/main/ets/pages/component/text/attribute/fontsize/solution/FontSizePage.ets
字体粗细可通过fontWeight()
方法进行设置,该方法参数类型为number | FontWeight | string
,下面逐一介绍
number
类型的取值范围是[100,900]
,取值间隔为100
,默认为400
,取值越大,字体越粗。
FontWeight
为枚举类型,可选枚举值如下
名称描述FontWeight.Lighter
字体较细。FontWeight.Normal
字体粗细正常。FontWeight.Regular
字体粗细正常。FontWeight.Medium
字体粗细适中。FontWeight.Bold
字体较粗。FontWeight.Bolder
字体非常粗。
string
类型的参数仅支持number
类型和FontWeight
类型参数的字符串形式,例如例如'100'
和bold
。
相关案例见Demos/entry/src/main/ets/pages/component/text/attribute/fontweight/solution/FontWeightPage.ets
字体颜色可通过fontColor()
方法进行设置,该方法参数类型为Color | string | number | Resource
,下面逐一介绍
Color`为枚举类型,其中包含了多种常用颜色,例如`Color.Green
string`类型的参数可用于设置 **rgb** 格式的颜色,具体写法可以为`'rgb(0, 128, 0)'`或者`'#008000'
number`类型的参数用于使用16进制的数字设置 **rgb** 格式的颜色,具体写法为`0x008000
Resource
类型的参数用于应用resources下的element目录中定义的值。
相关案例见Demos/entry/src/main/ets/pages/component/text/attribute/fontcolor/solution/FontColor.ets
文本对齐方向可通过textAlign()
方法进行设置,该方法的参数为枚举类型TextAlign
,可选的枚举值如下
名称 | 描述 |
---|---|
TextAlign.Start | 首部对齐 |
TextAlign.Center | 居中对齐 |
TextAlign.End | 尾部对齐 |
各选项效果如下
相关案例见Demos/entry/src/main/ets/pages/component/text/attribute/textalign/solution/TextAlignPage.ets
可使用maxLines()
方法控制文本的最大行数,当内容超出最大行数时,可使用textOverflow()
方法处理超出部分,该方法的参数类型为{ overflow: TextOverflow }
,其中TextOverflow
为枚举类型,可用枚举值有
名称 | 描述 |
---|---|
TextOverflow.Clip | 文本超长时,进行裁剪显示。 |
TextOverflow.Ellipsis | 文本超长时,显示不下的文本用省略号代替。 |
各选项效果如下
相关案例见Demos/entry/src/main/ets/pages/component/text/attribute/textoverflow/solution/TextOverFlowPage.ets
Button
为按钮组件,通常用于响应用户的点击操作。
Button
组件有两种使用方式,分别是不包含子组件和包含子组件,两种方式下,Button
组件所需的参数有所不同,下面分别介绍
不包含子组件时,Button
组件所需的参数如下
Button(label?: string, options?: { type?: ButtonType, stateEffect?: boolean })
label
为按钮上显示的文字内容。
options.type
为按钮形状,该属性的类型ButtonType
,可选的枚举值有
名称描述效果ButtonType.Capsule
胶囊形状ButtonType.Circle
圆形ButtonType.Normal
普通形状
options.stateEffect
表示是否开启点击效果,点击效果如下
子组件会作为按钮上显示的内容,可以是图片、文字等。这种方式下,Button
组件就不在需要label
参数了,具体如下
Button(options?: {type?: ButtonType, stateEffect?: boolean})
相关案例见:Demos/entry/src/main/ets/pages/component/button/parameter/solution/ButtonParameter.ets
按钮的颜色可使用backgroundColor()
方法进行设置,例如
Button('绿色按钮').backgroundColor(Color.Green)
按钮的边框圆角大小可使用borderRadius()
方法进行设置,例如
Button('圆角按钮', { type: ButtonType.Normal }).borderRadius(10)
相关案例见:Demos/entry/src/main/ets/pages/component/button/attribute/ButtonAttributePage.ets
对于Button组件而言,最为常用的就是点击事件,可以通过onClick()
方法为按钮绑定点击事件,该方法的参数为一个回调函数,当按钮被点击时,就会触发该回调函数,例如
Button('点击事件').onClick(() => {console.log('我被点击了')})
相关案例见:Demos/entry/src/main/ets/pages/component/button/event/ButtonEventPage.ets
**Toggle**
为切换按钮组件,一般用于两种状态之间的切换,例如下图中的蓝牙开关。
Toggle
组件的参数定义如下
Toggle(options: { type: ToggleType, isOn?: boolean })
type
属性用于设置Toggle
组件的类型,可通过ToggleType
枚举类型进行设置,可选的枚举值如下
名称描述效果ToggleType.Switch
开关ToggleType.Checkbox
复选框ToggleType.Button
按钮
isOn
属性用于设置Toggle
组件的状态,例如
相关案例见:Demos/entry/src/main/ets/pages/component/toggle/parameter/ToggleParameter.ets
可使用selectedColor()
方法设置Toggle
组件在选中(或打开)状态下的背景色,例如
可使用设置switchPointColor()
方法设置Switch类型的Toggle组件中的圆形滑块颜色,例如
相关案例见:Demos/entry/src/main/ets/pages/component/toggle/attribute/ToggleAttributePage.ets
Toggle
组件常用的事件为change事件,每当Toggle
组件的状态发生变化,就会触发change
事件。开发者可通过onChange()
方法为Toggle
组件绑定change事件,该方法参数为一个回调函数,具体定义如下
onChange(callback: (isOn: boolean) => void)
当Toggle
组件的状态由关闭切换为打开时,isOn
为true
,从打开切换为关闭时,isOn
为false
。
相关案例见:Demos/entry/src/main/ets/pages/component/toggle/event/solution/Light.ets
TextInput
为文本输入组件,用于接收用户输入的文本内容。
TextInput
组件的参数定义如下
TextInput(value?:{placeholder?: string|Resource , text?: string|Resource})
placeholder
属性用于设置无输入时的提示文本,效果如下
text
用于设置输入框当前的文本内容,效果如下
相关案例见:Demos/entry/src/main/ets/pages/component/input/parameter/TextInputParameter.ets
可通过type()
方法设置输入框的类型,该方法的参数为InputType
枚举类型,可选的枚举值有
名称 | 描述 |
---|---|
InputType.Normal | 基本输入模式 |
InputType.Password | 密码输入模式 |
InputType.Number | 纯数字输入模式 |
可通过caretColor()
方法设置光标的颜色,效果如下
可通过placeholderFont()
和placeholderColor()
方法设置 placeholder 的样式,其中placeholderFont()
用于设置字体,包括字体大小、字体粗细等,placeholderColor()
用于设置字体颜色,效果如下
输入文本的样式可通过fontSize()
、fontWeight()
、fontColor()
等通用属性方法进行设置。
相关案例见:Demos/entry/src/main/ets/pages/component/input/attribute/TextInputAttribute.ets
每当输入的内容发生变化,就会触发 change 事件,开发者可使用onChange()
方法为TextInput
组件绑定 change 事件,该方法的参数定义如下
onChange(callback: (value: string) => void)
其中value
为最新内容。
焦点事件包括获得焦点和失去焦点两个事件,当输入框获得焦点时,会触发 focus 事件,失去焦点时,会触发 blur 事件,开发者可使用onFocus()
和onBlur()
方法为 TextInput
组件绑定相关事件,两个方法的参数定义如下
onFocus(event: () => void)onBlur(event: () => void)
相关案例见:Demos/entry/src/main/ets/pages/component/input/event/TextInputEvent.ets
Progress
为进度条组件,用于显示各种进度。
Progress
组件的参数定义如下
Progress(options: {value: number, total?: number, type?: ProgressType})
value
属性用于设置当前进度值。
total
属性用于设置总值。
type
属性用于设置进度条类型,可通过ProgressType
枚举类型进行设置,可选的枚举值如下
名称描述效果ProgressType.Linear
线性样式ProgressType.Ring
环形无刻度样式ProgressType.Eclipse
月食样式ProgressType.ScaleRing
环形有刻度样式ProgressType.Capsule
胶囊样式
相关案例见:Demos/entry/src/main/ets/pages/component/progress/parameter/ProgressParameter.ets
可通过style()
调整进度条的样式,例如进度条的宽度,该方法的参数类型定义如下
style({strokeWidth?: string|number|Resource,scaleCount?: number,scaleWidth?: string|number|Resource})
strokeWidth
属性用于设置进度条的宽度,默认值为4vp
。该属性可用于Linear
、Ring
、ScaleRing
三种类型,效果如下
scaleCount
属性用于设置ScaleRing
的刻度数,默认值为120
。效果如下
scaleCount
属性用于设置ScaleRing
的刻度线的宽度,默认值为2vp
。效果如下
相关案例见:Demos/entry/src/main/ets/pages/component/progress/attribute/style/ProgressStyle.ets
进度条的颜色可通过color()
和backgroundColor()
方法进行设置,其中color()
用于设置前景色,backgroundColor()
用于设置背景色,例如
相关案例见:Demos/entry/src/main/ets/pages/component/progress/attribute/color/ProgressColor.ets
弹窗是移动应用中常见的一种用户界面元素,常用于显示一些重要的信息、提示用户进行操作或收集用户输入。ArkTS提供了多种内置的弹窗供开发者使用,除此之外还支持自定义弹窗,来满足各种不同的需求。
Toast(消息提示),常用于显示一些简短的消息或提示,一般会在短暂停留后自动消失。具体效果如下
可使用@ohos.promptAction
模块中的showToast()
方法显示 Toast 提示,使用时需要先导入@ohos.promptAction
模块,如下
import promptAction from '@ohos.promptAction'
showToast()
方法的参数定义如下
showToast(options: { message: string | Resource,duration?: number,bottom?: string | number})
message
属性用于设置提示信息
duration
属性用于设置提示信息停留时长,单位为毫秒,取值范围是[1500,10000]
bottom
属性用于设置提示信息到底部的距离
相关案例见:Demos/entry/src/main/ets/pages/component/dialog/toast/ToastPage.ets
AlertDialog(警告对话框)用于向用户发出警告或确认操作的提示,确保用户在敏感操作前进行确认。具体效果如下
相关案例见:Demos/entry/src/main/ets/pages/component/dialog/alertDialog/solution/AlertDialogPage.ets
可使用全局方法AlertDialog.show()
显示警告对话框,具体用法可参考相关案例或者官方文档。
ActionSheet(操作列表弹窗)用于提供一组选项给用户选择,用户从中选择后,可执行相应的操作。具体效果如下
相关案例见:Demos/entry/src/main/ets/pages/component/dialog/actionSheet/solution/ActionSheetPage.ets
可使用全局方法ActionSheet.show()
显示操作列表弹窗,具体用法可参考相关案例或者官方文档。
选择器弹窗用于让用户从一个列表中选择一个具体的值。ArkTS
内置了多种选择器弹窗,例如文本选择器、日期选择器、时间选择器等等,各选择器效果如下
相关案例见:Demos/entry/src/main/ets/pages/component/dialog/picker/textPickerDialog/solution/TextPickerDialogPage.ets
相关案例见:Demos/entry/src/main/ets/pages/component/dialog/picker/datePickerDialog/solution/DatePickerDialogPage.ets
相关案例见:Demos/entry/src/main/ets/pages/component/dialog/picker/timePickerDialog/solution/TimePickerDialogPage.ets
具体用法可参考相关案例或者官方文档,各选择器的官方文档地址如下
当现有组件不满足要求时,可考虑自定义弹窗,自定义弹窗允许开发者自定义弹窗内容和样式。例如
相关案例见:Demos/entry/src/main/ets/pages/component/dialog/custom/solution/CustomDialogPage.ets
显示自定义弹窗需要使用CustomDialogController
,具体用法可参考相关案例或者官方文档。
当多个组件具有相同的样式时,若每个组件都单独设置,将会有大量的重复代码。为避免重复代码,开发者可使用@Styles
或者@Extend
装饰器将多条样式设置提炼成一个方法,然后直接在各组件声明的位置进行调用,这样就能完成样式的复用。
@Styles
方法可定义在组件内或者全局,具体语法如下
@Entry @Component struct StylesPage { build() { Column() { Row({ space: 50 }) { Button('确认') .type(ButtonType.Normal) .backgroundColor(Color.Green) .compButtonStyle() //复用样式 .onClick(() => console.log('确认')) Button('取消') .type(ButtonType.Normal) .backgroundColor(Color.Gray) .compButtonStyle() //复用样式 .onClick(() => console.log('取消')) } }.width('100%') .height('100%') .justifyContent(FlexAlign.Center) } //组件内样式定义 @Styles compButtonStyle() { .width(100) .height(40) .borderRadius(10) } }
@Entry @Component struct StylesPage { build() { Column() { Row({ space: 50 }) { Button('确认') .type(ButtonType.Normal) .backgroundColor(Color.Green) .globalButtonStyle() //复用样式 .onClick(() => console.log('确认')) Button('取消') .type(ButtonType.Normal) .backgroundColor(Color.Gray) .globalButtonStyle() //复用样式 .onClick(() => console.log('取消')) } }.width('100%') .height('100%') .justifyContent(FlexAlign.Center) } } //全局样式定义 @Styles function globalButtonStyle() { .width(100) .height(40) .borderRadius(10) }
注意
@Styles
方法只能在当前组件中使用,全局的@Styles
方法目前只允许在当前的.ets文件
中使用@Styles
方法时不需要使用function
关键字,全局的@Styles
方法需要使用function
关键字@Styles
方法中只能包含通用属性方法和通用事件方法@Styles
方法不支持参数相关案例见:Demos/entry/src/main/ets/pages/component/reuse/styles/solution/StylesPage.ets
@Extend
装饰的方法同样可用于组件样式的复用,与@Styles
不同的是,@Extend
方法只能定义在全局。并且@Extend
方法只能用于指定类型的组件,例如以下方法只能用于Button组件(可以理解为是Button组件的扩展样式)
@Extend(Button) function buttonStyle(){...}
由于@Extend
方法只能用于指定类型的组件,因此方法中可包含指定组件的专有属性方法和专有事件方法。另外,@Extend
方法还支持参数,具体语法如下
@Entry@Componentstruct ExtendPage {build() {Column() {Row({ space: 50 }) {Button('确认').buttonExtendStyle(Color.Green, () => console.log('确认')) //复用样式Button('取消').buttonExtendStyle(Color.Gray, () => console.log('取消')) //复用样式}}.width('100%').height('100%').justifyContent(FlexAlign.Center)}}//样式定义@Extend(Button) function buttonExtendStyle(color: Color, callback: () => void) {.width(100).height(40).borderRadius(10).type(ButtonType.Normal).backgroundColor(color).onClick(callback)}
总结
@Extend
方法只能定义在全局,使用范围目前只限于当前的.ets
文件@Extend
方法用于特定类型的组件,因此可包含该组件的专有属性方法和专有事件方法@Extend
方法支持参数相关案例见:Demos/entry/src/main/ets/pages/component/reuse/extend/solution/ExtendPage.ets
当页面有多个相同的UI结构时,若每个都单独声明,同样会有大量重复的代码。为避免重复代码,可以将相同的UI结构提炼为一个自定义组件,完成UI结构的复用。
除此之外,ArkTS还提供了一种更轻量的UI结构复用机制@Builder
方法,开发者可以将重复使用的UI元素抽象成一个@Builder
方法,该方法可在build()
方法中调用多次,以完成UI结构的复用。
@Builder
方法同样可以定义在组件内或者全局,具体语法如下
@Entry @Component struct BuilderPage { build() { Column() { Row({ space: 50 }) { //复用UI结构 this.compButtonBuilder($r('app.media.icon_edit'), '编辑', () => console.log('编辑')) this.compButtonBuilder($r('app.media.icon_send'), '发送', () => console.log('发送')) } }.width('100%') .height('100%') .justifyContent(FlexAlign.Center) } //定义UI结构 @Builder compButtonBuilder(icon: Resource, text: string, callback: () => void) { Button() { Row({ space: 10 }) { Image(icon) .width(25) .height(25) Text(text) .fontColor(Color.White) .fontSize(25) } }.width(120) .height(50) .onClick(callback) } }
@Entry @Component struct BuilderPage { build() { Column() { Row({ space: 50 }) { //复用UI结构 globalButtonBuilder($r('app.media.icon_edit'), '编辑', () => console.log('编辑')) globalButtonBuilder($r('app.media.icon_send'), '发送', () => console.log('发送')) } }.width('100%') .height('100%') .justifyContent(FlexAlign.Center) } } //定义UI结构 @Builder function globalButtonBuilder(icon: Resource, text: string, callback: () => void) { Button() { Row({ space: 10 }) { Image(icon) .width(25) .height(25) Text(text) .fontColor(Color.White) .fontSize(25) } }.width(120) .height(50) .onClick(callback) }
注意
@Builder
方法可通过this
访问当前组件的属性和方法,而全局的@Builder
方法则不能@Builder
方法只能用于当前组件,全局的@Builder
方法导出(export
)后,可用于整个应用。相关案例见:Demos/entry/src/main/ets/pages/component/reuse/builder/syntax/solution/BuilderPage.ets
@Builder
方法具有两种参数传递机制——按值传递和按引用传递。当只有一个参数且参数为对象字面量时为按引用传递,其余情况均为按值传递。
按引用传递时,若传递的参数为状态变量,则状态变量的变化将会触发@Builder
方法内部UI的刷新;按值传递时则不会。
相关案例见:Demos/entry/src/main/ets/pages/component/reuse/builder/parameter/BuilderParameterPage.ets
@Builder
方法和自定义组件虽然都可以实现UI复用的效果,但是两者还是有着本质的区别的,其中最为显著的一个区别就是自定义组件可以定义自己的状态变量,而@Builder
方法则不能。以下案例中,每个待办事项的UI结构都相同,因此可考虑将其提炼为一个自定义组件或者@Builder
方法,但是由于每个待办事项均有已完成和未完成两种状态,因此需要为每个待办事项都定义一个状态变量,所以此时就只能使用自定义组件而不能使用@Builder
方法。
总结
若复用的UI结构没有状态,推荐使用@Builder
方法,否则使用自定义组件。
相关案例见:Demos/entry/src/main/ets/pages/component/reuse/builder/difference/DifferencePage.ets
@BuilderParam
用于装饰自定义组件(struct)中的属性,其装饰的属性可作为一个UI结构的占位符,待创建该组件时,可通过参数为其传入具体的内容。(其作用类似于Vue框架中的slot)。
组件定义 | UI结构定义 | 组件创建 |
---|---|---|
@Component struct Container { //@BuilderParam属性 @BuilderParam content: () => void build() { Column() { Text('其他内容') //其他内容 this.content(); //占位符 Button('其他内容') //其他内容 } } } | @Builder function contentBuilder1() { ... } @Builder function contentBuilder2() { ... } @Builder function contentBuilder3() { ... } | Container({ content: contentBuilder1 }) Container({ content: contentBuilder2 }) Container({ content: contentBuilder3 }) |
下面通过一个案例展示@BuilderParam
的具体用法,例如,现需要实现一个通用的卡片组件,如下图所示
卡片中显示的内容不固定,例如
具体实现步骤如下:
卡片组件定义
卡片组件定义 | 效果图 |
---|---|
@Component struct Card { @BuilderParam content: () => void; //@BuilderParam属性 build() { Column() { this.content(); //占位符 }.width('90%') .padding(10) .borderRadius(10) .shadow({ radius: 20 }) } } |
卡片内容定义
卡片内容定义 | 效果图 |
---|---|
@Builder function imageBuilder() { Column({ space: 10 }) { Image($r('app.media.img_harmony')) .width(300) .height(150) Text('鸿蒙操作系统') } } | ![]() |
创建卡片组件
创建卡片组件 | 效果图 |
---|---|
Card({ content: imageBuilder }) | ![]() |
另外,如果一个组件中只定义了一个@BuilderParam
属性,那么创建该组件时,也可直接通过**"子组件"**的方式传入具体的UI结构,例如
创建卡片组件 | 效果图 |
---|---|
Card() { Column({ space: 10 }) { Text('鸿蒙操作系统') .fontSize(25) .fontWeight(FontWeight.Bold) Text('鸿蒙操作系统是...') } } | ![]() |
相关案例见:Demos/entry/src/main/ets/pages/component/reuse/builderParam/BuilderParamPage.ets
布局是指对页面组件进行排列和定位的过程,其目的是有效地组织和展示页面内容,会涉及到组件的大小、位置以及它们之间的相互关系等等。
在鸿蒙应用中,页面上的每个组件都可以看做是一个矩形的盒子,这个盒子包含了内容区域(content)、边框(border)、内边距(padding)和外边距(margin),各部分内容如下图所示
其中margin、padding和border均可使用同名的属性方法进行设置,各方法定义如下
margin(value: { top?:Length, right?:Length, bottom?:Length, left?:Length } | Length )
说明:Length
=string | number | Resource
当参数类型为Length
时,四个方向的边距同时生效
padding(value: { top?:Length, right?:Length, bottom?:Length, left?:Length } | Length )
border(value: {width?:Length, color?:ResourceColor, radius?:Length, style?:BorderStyle })
各属性含义如下
width
属性表示边框宽度
color
属性表示边框颜色
radius
属性表示边框圆角半径
style
属性表示边框样式,可通过BorderStyle
这一枚举类型进行设置,可选的枚举值有
名称描述效果BorderStyle.Dotted
显示为一系列圆点BorderStyle.Dashed
显示为一系列短的方形虚线BorderStyle.Solid
显示为一条实线
线性布局(LinearLayout
)是开发中最常用的布局,可通过容器组件Column
和Row
构建,其子组件会在垂直或者水平方向上进行线性排列,具体效果如下图所示
![]() | |
---|---|
说明
Column和Row容器均有两个轴线,分别是主轴和交叉轴
**Row**
容器主轴为横向,**Column**
容器主轴为纵向。**Row**
容器交叉轴为纵向,**Column**
容器交叉轴为横向。Column和Row容器的参数类型为{space?: string | number}
,开发者可通过space
属性调整子元素在主轴方向上的间距,效果如下
![]() | ![]() |
---|---|
相关案例见:Demos/entry/src/main/ets/pages/layout/linear/parameter/space/SpacePage.ets
子元素沿主轴方向的排列方式可以通过justifyContent()
方法进行设置,其参数类型为枚举类型FlexAlign
,可选的枚举值有
名称 | Start | Center | End | SpaceBetween | SpaceAround | SpaceEvenly |
---|---|---|---|---|---|---|
描述 | 头部对齐 | 居中对齐 | 尾部对齐 | 均匀分布,首尾两项两端对齐,中间元素等间距分布 | 均匀分布,所有子元素两侧都留有相同的空间 | 均匀分布,所有子元素之间以及首尾两元素到两端的距离都相等 |
效果(以Column容器为例) | ![]() | ![]() | ![]() | ![]() | ![]() |
相关案例见:Demos/entry/src/main/ets/pages/layout/linear/attribute/justifyContent/JustifyContent.ets
子元素沿交叉轴方向的对齐方式可以通过alignItems()
方法进行设置,其参数类型,对于Column
容器来讲是HorizontalAlign
,对于Row
容器来讲是VerticalAlign
,两者都是枚举类型,可选择枚举值也都相同,具体内容如下
名称 | Start | Center | End |
---|---|---|---|
描述 | 头部对齐 | 居中对齐 | 尾部对齐 |
效果(以Column容器为例) | ![]() | ![]() | ![]() |
相关案例见:Demos/entry/src/main/ets/pages/layout/linear/attribute/alignItems/AlignItemsPage.ets
Blank可作为Column和Row容器的子组件,该组件不显示任何内容,并且会始终充满容器主轴方向上的剩余空间,效果如下:
相关案例见:Demos/entry/src/main/ets/pages/layout/linear/tips/blank/BlankPage.ets
layoutWeight
属性可用于Column和Row容器的子组件,其作用是配置子组件在主轴方向上的尺寸权重。配置该属性后,子组件沿主轴方向的尺寸设置(width
或height
)将失效,具体尺寸根据权重进行计算,计算规则如下图所示:
相关案例见:Demos/entry/src/main/ets/pages/layout/linear/tips/layoutWeight/LayoutWeightPage.ets
层叠布局是指将多个组件沿垂直于屏幕的方向堆叠在一起,类似于图层的叠加。以下效果都可以通过层叠布局实现
![]() | ![]() | ![]() |
---|---|---|
层叠布局可通过Stack容器组件实现,其子元素会按照其添加顺序依次叠加在一起,后添加的子元素位于先添加的子元素之上。具体效果如下
代码 | 效果 |
---|---|
Stack() { Row() .width(250) .height(250) .backgroundColor('#107B02') //绿色 .shadow({radius:50}) Row() .width(200) .height(200) .backgroundColor('#E66826') //橙色 .shadow({radius:50}) Row() .width(150) .height(150) .backgroundColor('#255FA7') //蓝色 .shadow({radius:50}) } .width(300) .height(300) .backgroundColor('#E5E5E5') //灰色 | ![]() |
相关案例见:Demos/entry/src/main/ets/pages/layout/stack/basis/StackPage.ets
Stack组件的参数类型为{ alignContent?: Alignment }
,alignContent
用于设置子组件的对齐方式,该属性可通过枚举类型Alignment
进行设置,可选的枚举值及其效果如下图所示
![]() | ![]() | ![]() |
---|---|---|
![]() | ![]() | ![]() |
![]() | ![]() | ![]() |
该参数的一个实际使用场景如下:
相关案例见:Demos/entry/src/main/ets/pages/layout/stack/parameter/alignContent/AlignContentPage.ets
Stack容器中子组件的层级除了可按照添加顺序决定,还能通过zIndex()
进行手动的设置,zIndex
的值越大,层级越高。
代码 | 效果 |
---|---|
Stack() { Row() .width(150) .height(150) .backgroundColor('#255FA7') //蓝色 .shadow({ radius: 50 }) .zIndex(3) Row() .width(200) .height(200) .backgroundColor('#E66826') //橙色 .shadow({ radius: 50 }) .zIndex(2) Row() .width(250) .height(250) .backgroundColor('#107B02') //绿色 .shadow({ radius: 50 }) .zIndex(1) }.width(300) .height(300) .backgroundColor('#E5E5E5') //灰色 | ![]() |
相关案例见:Demos/entry/src/main/ets/pages/layout/stack/tips/zIndex/ZIndexPage.ets
Stack容器的子组件可使用position()
方法进行更精确的定位,该方法可设置子组件左上角相对于Stack容器左上角的偏移量,具体效果如下
代码 | 效果 |
---|---|
Stack() { Image($r('app.media.img_avatar')) .width('100%') .height('100%') Image($r('app.media.icon_v')) .width(60) .height(60) .position({ x: 140, y: 140 }) } .width(200) .height(200) | ![]() |
相关案例见:Demos/entry/src/main/ets/pages/layout/stack/tips/position/PositionPage.ets
弹性布局(Flex)的效果类似于线性布局(Column/Row),也会使子元素呈线性排列,但是弹性布局在子元素的排列、对齐和剩余空间的分配等方面更加灵活。
Flex组件的参数定义如下,下面逐一介绍每个属性
Flex(value?: { direction?: FlexDirection, justifyContent?: FlexAlign, alignItems?: ItemAlign, wrap?: FlexWrap, alignContent?: FlexAlign })
dirction
用于设置Flex容器的布局方向,即子元素的排列方向,其类型FlexDirection
为枚举类型,可选的枚举值如下
名称 | Row | RowReverse | Column | ColumnReverse |
---|---|---|---|---|
描述 | 水平方向,元素从左到右排列 | 水平方向,元素从右到左排列 | 垂直方向,元素从上到下排列 | 垂直方向,元素从下到上排列 |
效果 |
相关案例见:Demos/entry/src/main/ets/pages/layout/flex/parameter/direction/DirectionPage.ets
Flex容器中也有主轴和交叉轴两个概念,其中主轴方向与direction
一致,交叉轴与主轴垂直,具体方向如下
direction | Row | RowReverse | Column | ColumnReverse |
---|---|---|---|---|
主轴和交叉轴 |
**justifyContent**
参数的作用同Column/Row容器的justifyContent()
完全相同,也是用于设置子元素在主轴方向的排列方式,其类型同样为FlexAlign
,可选的枚举值如下
名称 | 描述 | 效果(以direction=Row 为例) |
---|---|---|
Start | 分布在起始端 | |
Center | 居中 | |
End | 分布在结束端 | |
SpaceBetween | 均匀分布,首尾两项两端对齐,中间元素等间距分布 | |
SpaceAround | 均匀分布,所有子元素两侧都留有相同的空间 | |
SpaceEvenly | 均匀分布,所有子元素之间以及首尾两元素到两端的距离都相等 |
相关案例见:Demos/entry/src/main/ets/pages/layout/flex/parameter/justifyContent/JustifyContentPage.ets
alignItems
参数的作用同Column/Row容器的alignItems()
相同,也是用于设置子元素在交叉轴的对齐方式。但该参数的类型与Column/Row容器的alignItems()
方法不同,为ItemAlign
,可选的枚举值有
名称 | 描述 | 效果(以direction=Row 为例) |
---|---|---|
Start | 启始端对齐 | |
Center | 居中对齐 | |
End | 结束端对齐 | |
Stretch | 拉伸到容器尺寸 | |
BaseLine | 沿文本基线对齐(限于Text文本组件)基线是西文书法或印刷学中的一个概念,它指的是多数字母底部的那条线,如下图所示![]() |
相关案例见:Demos/entry/src/main/ets/pages/layout/flex/parameter/alignItems/AlignItemsPage.ets
默认情况下,Flex容器的子组件,都排在一条线(主轴)上。当子组件在主轴方向的尺寸之和大于Flex容器时,为适应容器尺寸,所有子组件的尺寸都会自动收缩。如果需要保持子组件的尺寸不收缩,也可选择令子组件**换行(列)**显示。
wrap
属性的作用就是控制如何换行,该属性的类型FlexWrap
为枚举类型,可选的枚举值如下
名称 | 描述 | 效果(以direction=Row 为例) |
---|---|---|
NoWrap | 不换行 | ![]() |
Wrap | 换行,每一行子组件按照主轴方向排列 | ![]() |
WrapReverse | 换行,每一行子元素按照主轴反方向排列 | ![]() |
相关案例见:Demos/entry/src/main/ets/pages/layout/flex/parameter/wrap/WrapPage.ets
当Flex容器中包含多行(列)时,可使用alignContent
设置多行在交叉轴的排列方式,该属性的类型为FlexAlign
,可选的枚举值如下
名称 | Start | Center | End | SpaceBetween | SpaceAround | SpaceEvenly |
---|---|---|---|---|---|---|
描述 | 分布在起始端 | 居中 | 分布在终点端 | 均匀分布,首尾两项两端对齐,中间元素等间距分布 | 均匀分布,所有子元素两侧都留有相同的空间 | 均匀分布,所有子元素之间以及首尾两元素到两端的距离都相等 |
效果(以direction=Row 为例) | ![]() | ![]() | ![]() | ![]() |
相关案例见:Demos/entry/src/main/ets/pages/layout/flex/parameter/alignContent/AlignContentPage.ets
Flex容器的子组件可以使用alignSelf()
方法单独设置自己的交叉轴对齐方式,并且其优先级高于Flex容器alignItems
。具体效果如下
说明:
alignSelf()
的参数类型和alignItems()
相同,均为ItemAlign
枚举类型,且各枚举值的含义也完全相同。
代码 | 效果 |
---|---|
Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Start }) { Text('1') .width(100) .height(100) .itemTextStyle() Text('2') .width(100) .height(200) .itemTextStyle() Text('3') .width(100) .height(150) .itemTextStyle() .alignSelf(ItemAlign.End) //单独设置交叉轴对齐方式 }.height(300) .width('95%') .flexStyle() | ![]() |
相关案例见:Demos/entry/src/main/ets/pages/layout/flex/attribute/alignSelf/AlignSelfPage.ets
弹性布局的显著特点之一是子组件沿主轴方向的尺寸具有弹性,即子组件的大小能够随着Flex容器尺寸的变化而自动伸缩。这种弹性特性使得Flex布局能够使子组件更灵活地适应不同的屏幕尺寸和设备。和自适应伸缩的相关的属性有flexShrink
、flexGrow
和flexBasis
,下面逐一介绍
flexShrink
用于设置父容器空间不足时,子组件的压缩比例,尺寸的具体计算逻辑如下
代码效果//Flex容器主轴尺寸为240,子组件主轴尺寸之和为100*3=300 Flex() { //尺寸=100 Text('1') .width(100) .height(100) .flexShrink(0) //不压缩 //主轴尺寸=100-(300-240)*(1/3)=80 Text('2') .width(100) .height(100) .flexShrink(1) //压缩比例为1 //主轴尺寸=100-(300-240)*(2/3)=60 Text('3') .width(100) .height(100) .flexShrink(2) //压缩比例为2 }.height(150) .width(240)
相关案例见:
Demos/entry/src/main/ets/pages/layout/flex/attribute/flexShrink/FlexShrinkPage.ets
flexGrow
用于设置父容器空间充足时,组件瓜分剩余空间的比例,尺寸的具体计算逻辑如下
代码效果Flex() { //尺寸=100 Text('1') .width(100) .height(100) .flexGrow(0) //不拉伸 //主轴尺寸=100+(360-300)*(1/3)=120 Text('2') .width(100) .height(100) .flexGrow(1) //拉伸比例为1 //主轴尺寸=100+(360-300)*(2/3)=140 Text('3') .width(100) .height(100) .flexGrow(2) //拉伸比例为2 }.height(150) .width(360)
相关案例见:
Demos/entry/src/main/ets/pages/layout/flex/attribute/flexGrow/FlexGrowPage.ets
flexBasis
用于设置子组件沿主轴方向的尺寸,相当于width
或者height
的作用。若设置了flexBasis
,则以flexBasis
为准,否则以widht
或者height
为准。
网格布局(Grid
)是一种强大的布局方案,它将页面划分为行和列组成的网格,然后将页面内容在二维网格中进行自由的定位,以下效果都可通过网格布局实现
![]() | ![]() | ![]() |
---|---|---|
网格布局的容器组件为 Grid,子组件为 GridItem,具体语法如下
代码 | 效果 |
---|---|
Grid() { GridItem() { Text('GridItem') } GridItem() { Text('GridItem') } GridItem() { Text('GridItem') } GridItem() { Text('GridItem') } ...... } | ![]() |
Grid组件支持自定义行数和列数以及每行和每列的尺寸大小,上述内容需要使用rowsTemplate()
方法和columnsTemplate()
方法进行设置,具体用法如下
代码 | 效果 |
---|---|
Grid() { ForEach([1, 2, 3, 4, 5, 6, 7, 8, 9], (item) => { GridItem() { Text(item.toString()) .itemTextStyle() } }) } .width(320) .height(240) .rowsTemplate('1fr 1fr 1fr') .columnsTemplate('1fr 2fr 1fr') .gridStyle() | ![]() |
说明:
fr
为 fraction(比例、分数) 的缩写。fr
的个数表示网格布局的行数或列数,fr
前面的数值大小,表示该行或列的尺寸占比。
相关案例见:Demos/entry/src/main/ets/pages/layout/grid/attribute/basic/GridBasic.ets
GridItem组件支持横跨几行或者几列,如下图所示
可以使用columnStart()
、columnEnd()
、rowStart()
和rowEnd()
方法设置 GridItem 组件所占的单元格,其中rowStart
和rowEnd
属性表示当前子组件的起始行号和终点行号,columnStart
和columnEnd
属性表示指定当前子组件的起始列号和终点列号。
说明:
Grid容器中的行号和列号均从0开始。
具体用法如下,
代码 | 效果 |
---|---|
Grid() { GridItem() { Text('1') .itemTextStyle() }.rowStart(0).rowEnd(1).columnStart(0).columnEnd(1) GridItem() { Text('2') .itemTextStyle() } GridItem() { Text('3') .itemTextStyle() } GridItem() { Text('4') .itemTextStyle() } GridItem() { Text('5') .itemTextStyle() }.columnStart(1).columnEnd(2) } .width(320) .height(240) .rowsTemplate('1fr 1fr 1fr') .columnsTemplate('1fr 2fr 1fr') .gridStyle() | ![]() |
相关案例见:Demos/entry/src/main/ets/pages/layout/grid/attribute/startAndEnd/StartAndEndPage.ets
使用rowsGap()
和columnsGap()
属性,可以控制行列间距,具体用法如下
代码 | 效果 |
---|---|
Grid() { ...... } .columnsGap(20) .rowsGap(20) | ![]() |
相关案例见:Demos/entry/src/main/ets/pages/layout/grid/attribute/gap/GridGap.ets
使用网格布局实现如下布局效果
相关案例见:Demos/entry/src/main/ets/pages/layout/grid/calculator/solution/CalculatorPage.ets
List是一个功能强大的容器组件,使用List可以轻松高效地显示结构化、可滚动的列表信息,例如通讯录、新闻列表等等。
List容器的子组件为ListItem或者ListItemGroup,其中,ListItem表示单个列表项,ListItemGroup用于列表数据的分组展示,其子组件也是ListItem,具体用法如下
代码 | 效果 |
---|---|
List() { // 列表项 ListItem() {......} ListItem() {......} ListItem() {......} ListItem() {......} ListItem() {......} ListItem() {......} ListItem() {......} ListItem() {......} ListItem() {......} ListItem() {......} ListItem() {......} ListItem() {......} } | ![]() |
List() { // 列表组 ListItemGroup(){ //列表项 ListItem(){......} ListItem(){......} } ListItemGroup(){ ListItem(){......} ListItem(){......} } ListItemGroup(){ ListItem(){......} ListItem(){......} } } | ![]() |
List 组件的参数定义如下,下面逐一介绍每个参数
List(value?:{space?: number | string, scroller?: Scroller})
space
参数用于设置列表项的间距,如下图所示
scroller
参数用于绑定列表滚动控制器(Scroller),Scroller可以控制列表的滚动,例如令列表返回顶部
相关案例见:Demos/entry/src/main/ets/pages/layout/list/parameter/scroller/solution/ScrollerPage.ets
使用listDirection()
方法可以设置列表的主轴方向(即列表的排列和滚动方向),其参数类型为枚举类型**Axis**
,可选的枚举值如下
名称 | 描述 | 效果 |
---|---|---|
Axis.Vertical | 纵向 | ![]() |
Axis.Horizontal | 横向 | ![]() |
使用alignListItem()
方法可以设置子组件在交叉轴方向的对齐方式,其参数类型为枚举类型ListItemAlign
,可选的枚举值有
名称 | ListItemAlign.Start | ListItemAlign.Center | ListItemAlign.End |
---|---|---|---|
描述 | 交叉轴起始端对齐 | 交叉轴居中对齐 | 交叉轴末端对齐 |
效果(以listDirection=``Axis.Vertical 为例) | ![]() | ![]() | ![]() |
相关案例见:Demos/entry/src/main/ets/pages/layout/list/attribute/align/AlignPage.ets
使用divider()
属性可设置列表元素分割线样式,该方法的参数定义如下
divider(value: {strokeWidth: Length, color?: ResourceColor, startMargin?: Length, endMargin?: Length})
各参数的含义如下
参数 | 含义 |
---|---|
strokeWidth | 分割线线宽 |
color | 分割线颜色 |
startMargin | 分割线起始端到列表侧边距离(如下图所示) |
endMargin | 分割线末端到列表侧边距离(如下图所示) |
相关案例见:Demos/entry/src/main/ets/pages/layout/list/attribute/divider/DividerPage.ets
使用scrollBar()
方法可以设置滚动条状态,该方法的参数类型为枚举类型BarState
,可选的枚举值如下
名称 | 描述 |
---|---|
BarState.Off | 不显示 |
BarState.On | 常驻显示 |
BarState.Auto | 按需显示(触摸时显示,2s后消失) |
相关案例见:Demos/entry/src/main/ets/pages/layout/list/attribute/scrollBar/ScrollBarPage.ets
上述只是List列表布局最为常用的一些功能,其余功能可参考官方文档。
前文提到过,声明式 UI 的一个典型特征是通过状态数据的变化驱动组件视图的刷新,因此状态数据的有效管理在整个开发过程中显得至关重要。尽管对于单个组件而言,状态数据的管理并不复杂,但在实际应用场景中,整个页面往往由一个复杂的组件树构成。每个组件可能都需要维护自己的状态,并且这些状态还可能需要在组件之间进行共享,可先通过以下联系人的案例体会一下组件之间的状态共享。
相关案例见:Demos/entry/src/main/ets/pages/state/contacts/solution/ContactList.ets
为了方便开发者管理组件状态,ArkTS 提供了一系列状态相关的装饰器,例如@State
,@Prop
,@Link
,@Provide
和@Consume
等等。
@State
用于装饰当前组件的状态变量,@State
装饰的变量发生变化时会驱动当前组件的视图刷新。
注意:@State
装饰的变量必须进行本地初始化。
具体语法如下
@State count:number = 1;
相关案例见:Demos/entry/src/main/ets/pages/state/basic/state/StatePage.ets
@Prop
也可用于装饰状态变量,@Prop
装饰的变量发生变化时也会驱动当前组件的视图刷新,除此之外,@Prop
装饰的变量还可以同步父组件中的状态变量,但只能单向同步,也就是父组件的状态变化会自动同步到子组件,而子组件的变化不会同步到父组件。
注意:@Prop
装饰的变量不允许本地初始化,只能通过父组件向子组件传参进行初始化。
具体语法如下
父组件 | 子组件 |
---|---|
@Entry @Component struct Parent{ @State count:number = 1; build(){ Column(){ Child({count:this.count}); } } } | @Component export struct Child{ @Prop count:number; build(){ Text(this.count.toString()); } } |
相关案例见:Demos/entry/src/main/ets/pages/state/basic/prop/PropPage.ets
@Link
也可用于装饰状态变量,@Link
装饰的变量发生变化时也会驱动当前组件的视图刷新,除此之外,@Link
变量同样会同步父组件状态,并且能够双向同步。也就是父组件的变化会同步到子组件,子组件的变化也会同步到父组件。
注意:@Link
装饰的变量不允许本地初始化,只能由父组件通过传参进行初始化,并且父组件必须使用$变量名
的方式传参,以表示传递的是变量的引用。
具体语法如下
父组件 | 子组件 |
---|---|
@Entry @Component struct Parent{ @State count:number = 1; build(){ Column(){ Child({count:$count}); } } } | @Component export struct Child{ @Link count:number; build(){ Text(this.count.toString()); } } |
相关案例见:Demos/entry/src/main/ets/pages/state/basic/link/LinkPage.ets
@Provide
和@Consume
用于跨组件层级传递状态信息,其中@Provide
用于装饰祖先组件的状态变量,@Consume
用于装饰后代组件的状态变量。可以理解为祖先组件提供(Provide)状态信息供后代组件消费(Consume),并且祖先和后代的状态信息可以实现双向同步。
注意:@Provide
装饰变量必须进行本地初始化,而@Consume
装饰的变量不允许进行本地初始化。另外,@Provide
和@Consume
装饰的变量不是通过父组件向子组件传参的方式进行绑定的,而是通过相同的变量名进行绑定的。
具体语法如下
祖先组件 | 父组件 | 子组件 |
---|---|---|
@Entry @Component struct GrandParent { @Provide count: number = 1; build() { Column() { Parent() } } } | @Component struct Parent { build() { Column() { Child() } } } | @Component struct Child { @Consume count:number; build() { Column() { Text(this.count.toString()); } } } |
除了通过变量名进行绑定,还可通过变量的别名进行绑定,具体语法如下
祖先组件 | 父组件 | 子组件 |
---|---|---|
@Entry @Component struct GrandParent { @Provide('count') parentCount: number = 1; build() { Column() { Parent() } } } | @Component struct Parent { build() { Column() { Child() } } } | @Component struct Child { @Consume('count') childCount:number; build() { Column() { Text(this.count.toString()); } } } |
相关案例见:Demos/entry/src/main/ets/pages/state/basic/procon/ProConPage.ets
在了解了上述装饰器的基本语法和功能之后,我们还须深入学习每种装饰器的具体使用规则。
@State
允许装饰的变量类型有string
、number
、boolean
、object
、class
和enum
类型,以及这些类型的数组。
并不是@State
状态变量的所有更改都会引起UI的刷新,只有可以被框架观察到的修改才会引起UI刷新。能被框架观察到的变化如下
当@State
装饰的变量类型为boolean
、string
、number
类型时,可以观察到赋值的变化
例如
//状态变量定义@State count:number = 1;//状态变量操作this.count++; //可以观察到
相关案例见:Demos/entry/src/main/ets/pages/state/advanced/state/number/NumberState.ets
当@State
装饰的变量类型为class或者object时,可以观察到变量自身赋值的变化,和其属性赋值的变化。需要注意的是,若某个属性也为 class 或者 object,则嵌套属性的变化是观察不到的。
例如
//类型定义class Employee {name: string;age: number;job: Job;constructor(name: string, age: number, job: Job) {this.name = name;this.age = age;this.job = job;}}class Job {name: string;salary: number;constructor(name: string, salary: number) {this.name = name;this.salary = salary;}}//状态定义@State employee: Employee = new Employee('张三', 28, new Job('销售', 8000))//状态操作employee = new Employee('李四', 26, new Job('行政', 6000))//状态变量重新赋值,可以观察到employee.age++;//修改状态变量的属性,可以观察到employee.job.salary++;//修改状态变量的属性的属性,不可以观察到
相关案例见:Demos/entry/src/main/ets/pages/state/advanced/state/class/ClassState.ets
当@State
装饰的变量类型为数组时,可以观察到数组本身赋值的变化,和其元素的添加、删除及更新的变化,若元素类型为 class 或者 object 时,元素属性的变化,是观察不到的。
例如
//类型定义export class Person {name: string;age: number;constructor(name: string, age: number) {this.name = name;this.age = age;}}//状态定义@State persons: Person[] = [new Person('张三', 19), new Person('李四', 20)];//状态操作persons = [];//状态变量重新赋值,可以观察到persons.push(new Person('王五',21));//新增数组元素,可以观察到persons[0]=new Person('张三',22);//对数组元素重新赋值,可以观察到persons[1].age++;//修改数组元素的属性,不可以观察到
相关案例见:Demos/entry/src/main/ets/pages/state/advanced/state/array/ArrayState.ets
对于**class**
、**object**
和数组类型,框架仅能观察到@State
变量第一层属性的变化,例如employee.age++
,persons[0]=new Person('张三',22)
,但第二层属性的变化是观察不到的,例如employee.job.salary++
和persons[1].age++
。
@Prop
允许装饰的变量类型有string
、number
、boolean
、enum
,注意不支持class
、object
和数组。
当装饰的类型是允许的类型,即string
、number
、boolean
、enum
类型时,所有赋值的变化都可以观察到。
@State
)@Link
允许装饰的变量类型有string
、number
、boolean
、object
、class
和enum
类型,以及这些类型的数组。
@State
)@State
)@Provide
和@Consume
允许装饰的变量类型有string
、number
、boolean
、object
、class
和enum
类型,以及这些类型的数组。
@State
)前文所述的装饰器都仅能观察到状态变量第一层的变化,而第二层的变化是观察不到的。如需观察到这些状态变量第二层的变化,则需要用到@ObjectLink
和@Observed
装饰器。
具体用法为:
@ObjectLink
装饰。@Observed
装饰器进行装饰。@Observedexport class Job {name: string;salary: number;constructor(name: string, salary: number) {this.name = name;this.salary = salary;}}
相关案例见:Demos/entry/src/main/ets/pages/state/advanced/objectlink/solution/ClassState.ets
在完成状态管理相关装饰器的理论学习后,下面通过一个实际案例来巩固和加深大家对这些装饰器的理解。
该案例要实现的是一个通讯录(联系人列表),需要支持添加联系人、批量删除联系人、收藏联系人等功能,具体效果如下
相关案例见:Demos/entry/src/main/ets/pages/state/contacts/solution/ContactList.ets
该案例已经具备部分初始代码,初始效果如下
相关案例见:Demos/entry/src/main/ets/pages/state/contacts/practice/ContactList.ets
联系人列表状态变量的定义如下
@State persons: Person[] = [new Person(getRandomName(), getRandomPhone())];
注意:getRandomName()
和getRandomPhone()
方法为项目预先提供的工具方法,直接调用即可。
其中联系人的类定义如下
let nextId = 1;export class Person {id: number;//编号name: string;//姓名phone: string;//手机号码isStar: boolean;//是否收藏constructor(name, phone) {this.id = nextId++;this.name = name;this.phone = phone;this.isStar = false;}}
**注意:**每次创建(new
)新的对象,id
字段都会自动+1
。
要求点击新增按钮(+
)时,添加一名随机联系人。
为新增按钮绑定点击事件,每当点击发生时,向状态数组push
一个新元素即可。
五角星为收藏按钮,空心代表未收藏,实心代表已收藏,具体效果如下,要求可以在收藏和未收藏之间切换
未收藏收藏
将五角星图片的显示和person
对象的isStar
属性绑定,当isStar
为true
时,显示实心五角星,当isStar
为false
时,显示空心五角星。同时需要为图片绑定点击事件,每当点击图片时,将isStar
属性的值在true
和false
之间切换。需要注意的是,person
对象的isStar
属性属于状态变量persons
状态变量的第二层属性,因此该属性的变化不会被框架观察到。为解决 该问题,需要将单个联系人抽取为一个子组件(ContactItem
),并使用@ObjectLink
和@Observed
装饰器。
点击联系人时,相应联系人展开并显示详细信息(手机号码),再次点击收起详细信息,收起和展示的效果如下
收起效果展开效果
为联系人子组件(ContactItem)设置一个@State isExpand:boolean
状态变量,然后将详细信息(手机号码)的显示与isExpand
变量绑定,并为子组件绑定点击事件,每次点击时,都将isExpand
的值在 true
和 false
之间进行切换。
要求同一时刻只能有一个联系人信息展开,也就是在展开一个联系人的同时,需要收起其他联系人。
在父组件中声明一个状态变量@State currentExpandId:number
,记录当前展开的联系人id,同时在每个子组件声明@Link currentExpandId
。每当一个联系人被点击展开时,就将currentExpandId
设置为该联系人的id,此时所有子组件就能获知当前展开的联系人 id 了。为了实现展开一个联系人的同时收起其他联系人的效果,还需为每个子组件中都设置一个监听函数,用来监听currentExpandId
的变化,每当其发生变化,就判断currentExpandId
和当前组件的联系人 id 是否相同,不相同就将当前组件的isExpand
属性设置为false
以收起详细信息。
监听函数需要使用@Watch
装饰器。@Watch
装饰器的语法如下
struct Comp{ @Link @Watch('onCountChange') count: number; onCountChange() { ... } }
说明@Watch
装饰器常位于@State
、@Prop
、@Link
后边。@Watch
装饰器的参数为监听函数的名称。每当监听的状态变量发生变化,监听函数就会被调用。
为实现批量删除的功能,需先实现多选功能。要求点击选择
按钮时,进入选择模式,此时,每个联系人左侧均出现一个复选框(☑️),并且选择
按钮变为取消
,同时列表底部出现删除
按钮。当点击取消
按钮时,须恢复到普通模式。
普通模式****选择模式
需要在父组件ContactList
中定义一个状态变量@State isSelectMode:boolean
,并将选择
/取消
按钮的切换和删除
按钮的显示和isSelectMode
进行绑定。除此之外,也需要在子组件ContactItem
中定义@Prop isSelectMode
,并将复选框的显示与isSelectMode
进行绑定。
在完成多选操作后,点击删除按钮,需要将选中的联系人从persons
数组中移除。
需要在父组件ContactList
中定义一个状态变量,@State selectedIdList:number[]
,同时在子组件ContactItem
中定义@Link selectedIdList
。当某个子组件被选中时,需要将其id
push到selectedIdList
数组中。当点击删除
按钮时,从persons
中移除selectedIdList
包含的元素即可。
到目前为止,我们已经掌握了常用组件、常用布局以及组件状态管理,下面我们完成HarmonyOS 4.0实战项目之单词打卡的第1、2章。
标签页(tabs)是移动端应用中常见的用户界面设计元素,用于在有限的屏幕空间内管理和容纳多个页面或视图。在鸿蒙应用开发中,我们可以利用Tabs组件实现这一功能。
Tabs组件的基本用法如下
代码 | 效果 |
---|---|
Tabs() { TabContent() { Text('首页内容') } .tabBar('首页') TabContent() { Text('发现内容') } .tabBar('发现') TabContent() { Text('我的内容') } .tabBar("我的") } | ![]() |
其中,TabContent()
组件表示每个标签页的内容,其属性tabBar
表示其对应的页签。
相关案例见:Demos/entry/src/main/ets/pages/tabs/basic/TabsPage.ets
导航栏的位置可通过vertical()
和barPosition()
进行设置。vertical()
用于设置导航栏是否垂直排列,其参数为boolean
类型,默认值为false
。barPosition()
用于设置导航栏的位置,参数为枚举类型BarPosition
,可选的枚举值有BarPosition.Start
和BarPosition.End
。两个方法配合使用,可实现如下各种效果
vertical | false | false | true | true |
---|---|---|---|---|
barPosition | Start | End | Start | End |
效果 |
相关案例见:Demos/entry/src/main/ets/pages/tabs/barPosition/BarPositionPage.ets
当标签页较多时,屏幕的宽度可能无法容纳所有的所有的页签,如下图所示
此时可以使用barMode()
方法将导航栏设置为可滚动的。该方法的参数为枚举类型BarMode
,可选的枚举值有BarMode.Scrollable
和BarMode.Fixed
。
相关案例见:Demos/entry/src/main/ets/pages/tabs/barMode/BarModePage.ets
默认的导航栏页签只有文字内容,如需更加复杂的页签,如下图所示:
上述效果可通过自定义导航栏实现,具体内容可参考以下案例
相关案例见:Demos/entry/src/main/ets/pages/tabs/barCustom/solution/BarCustomPage.ets
动画可以在UI界面发生变化时,提供渐变过渡效果,提升用户体验。动画的实现原理是通过在一段时间内连续播放一系列静止画面(帧),从而产生流畅的视觉效果。
在鸿蒙应用中,实现动画效果非常方便,我们只需明确组件的初始状态和结束状态,并配置一些动画相关的参数(动画时长),系统就会自动的生成中间的过度状态,并呈现出动画效果。
在鸿蒙应用中,开发者可以为组件配置两类动画效果,一是组件的布局相关属性(如尺寸、位置等)发生变化时的布局更新动画,二是组件出现或消失时的组件转场动画,各自的效果如下图所示
布局更新动画 | 组件转场动画 |
---|---|
![]() | ![]() |
布局更新动画可通过两种方式实现,分别是显式动画和属性动画。
animateTo()
是一个全局的动画函数,该函数可用于触发动画效果,定义如下
animateTo(value: AnimateParam, event: () => void): void
该函数共有两个参数,分别是
该参数用于设置动画时长、属性值变化曲线等等,其类型为AnimateParam
,其包含的属性有
名称类型描述durationnumber动画持续时间,单位为毫秒,默认值为1000curveCurve动画曲线delaynumber延迟播放,单位为毫秒,默认值为0iterationsnumber循环播放次数,默认值为1playModePlayMode动画播放模式onFinish() => void动效播放完成回调
该函数用于修改组件的属性,由该函数导致的组件布局变化,都会产生动画效果。
相关案例见:Demos/entry/src/main/ets/pages/animation/animateTo/AnimateToPage.ets
animation()
是一个组件的属性方法,该方法同样可用于实现动画效果。使用该方法实现动画效果时需要注意两点,第一点是该方法的参数,其类型为AnimateParam
,用于配置动画时长、动画曲线等参数。第二点是该属性方法的位置,用于产生动画效果的属性方法必须位于animation()
之前,例如
Image($r('app.media.img_atguigu')).width(this.length).height(this.length).animation({ duration: 500}).margin({ left: this.marginLeft })
配置完animation()
属性后,只要我们修改其之前的属性,就会产生相应的动画效果。
相关案例见:Demos/entry/src/main/ets/pages/animation/animation/AnimationPage.ets
组件转场效果需要通过transition()
属性和animateTo()
方法来实现。其中transition()
属性方法来设置组件的转场效果,支持的效果有平移、透明度、旋转、缩放等,animateTo()
用于触发上述的组件转场动画效果,具体用法如下
@State flag:boolean = false; //状态变量,用于控制组件出现或消失if(this.flag){Text('hello world').transition(...) //transition()用于设置组件的转场效果}Button('出现').onClick(() => {//animateTo()用于触发动画效果animateTo({ duration: 5000, curve: Curve.Linear, iterations: 1 }, () => {this.flag = true;})})
transition()
方法的参数类型为TransitionOptions
,其包含的属性有
参数名称 | 参数类型 | 参数描述 | 效果 |
---|---|---|---|
type | TransitionType | 用于声明动画效果用于组件出现还是消失,可选值有Insert 、Delete 和All | |
opacity | number | 用于设置透明度变化效果。为出现动画起点的透明度或消失动画终点的透明度。取值范围为[0, 1],默认值为1。 | ![]() |
translate | `{ x?:number | string, y?:number | string }` |
scale | `{ x? : number, y? : number, centerX? : number | string, centerY? : number | string }` |
rotate | `{ x?: number, y?: number, z?: number, angle: number | string, centerX?: number | string, centerY?: number |
相关案例见:Demos/entry/src/main/ets/pages/animation/transition/TransitionPage.ets
页面路由用于实现应用程序中不同页面之间的跳转。HarmonyOS提供了router模块,用于实现页面路由功能。通过该模块,开发者可以轻松实现页面跳转、页面返回等功能。
router模块提供了两种跳转模式,分别是router.pushUrl()
和router.replaceUrl()
,这两种模式的区别在于是否保留当前页的状态。pushUrl()
会将当前页面压入历史页面栈,因此使用该方法跳转到目标页面之后,还可以再返回。而replaceUrl()
,会直接销毁当前页面并释放资源,然后用目标页替换当前页,因此使用该方法跳转到目标页面后,不能返回。
上述两方法的基础用法如下:
第一步:注册页面
页面注册后才能进行跳转,具体操作如下,例如现有一个页面的路径为src/main/ets/pages/SourcePage.ets
,我们在src/main/resources/base/profile/main_pages.json
文件增加如下配置即可
{"src": ["pages/Index","pages/SourcePage", //新增配置]}
注意:
在创建页面时,若选择新建Page
而不是ArkTs File
,则IDE会自动完成页面的注册。
第二步:导入router模块
import router from '@ohos.router'
之后便可使用上述两个方法完页面的跳转
router.pushUrl({ url: 'pages/router/pushAndReplace/TargetPage' })router.replaceUrl({ url: 'pages/router/pushAndReplace/TargetPage' })
相关案例见:Demos/entry/src/main/ets/pages/router/pushAndReplace/SourcePage.ets
返回页面使用router.back()
方法即可,直接调用router.back()
将返回上一页面,除此之外该方法也支持返回指定页面,例如
router.back({url:'pages/Index'})
注意:
若指定的页面不在历史页面栈中,也就是用户并未浏览过指定页面,那么将无法回到指定页面。
相关案例见:Demos/entry/src/main/ets/pages/router/back/FromPage.ets
在进行页面跳转时,如果需要传递一些数据给目标页面,可以在调用上述方式时,添加一个params
属性,并指定一个对象作为参数,例如
router.pushUrl({url: 'pages/router/pushAndReplace/TargetPage',params: {id: 10,name: 'zhangsan',age: '20'}})
目标页面可通过router.getParams()
方法获取参数,例如
let params = router.getParams();let id = params['id'];let name = params['name'];let age = params['age'];
相关案例见:Demos/entry/src/main/ets/pages/router/params/StartPage.ets
在鸿蒙应用中,每个自定义组件从创建到销毁,都会经历一系列的步骤,在这个过程中,系统会在一些特定的阶段运行一些特定函数,这些函数就被称为生命周期钩子函数,利用这些钩子函数,开发者可以在组件创建或者销毁时执行一些自己的代码,例如在组件出现之前从后台服务器请求数据。
普通组件生命周期函数
普通组件的生命周期函数只有如下两个:
aboutToAppear()
:该函数会在组件出现之前被调用,具体时机为创建自定义组件的实例之后,执行build()
方法之前。aboutToDisappear()
:该函数会在组件销毁之前被调用。页面入口组件生命周期函数
页面入口组件(使用@Entry
装饰的组件)的生命周期函数函数如下五个:
aboutToAppear()
:该函数会在组件出现之前被调用,具体时机为创建自定义组件的实例之后,执行build()
方法之前。aboutToDisappear()
:该函数会在组件销毁之前被调用。onPageShow()
:页面每次显示时都会被调用一次,包括路由、应用从后台进入前台等场景。onPageHide()
:页面每次隐藏时都会被调用一次,包括路由、应用从前台进入后台等场景。onBackPress()
:当用后点击返回按钮时会被调用。具体用法可参考以下案例
相关案例见:Demos/entry/src/main/ets/pages/lifecircle/PageA.ets
到目前为止,我们已经掌握了Tabs组件、组件动画、组件生命周期以及页面路由,下面我们完成HarmonyOS 4.0实战项目之单词打卡的第3、4章。
鸿蒙系统提供了http模块用于发送http请求,另外,OpenHarmony社区基于该模块将前端开发中常用的网络请求库axios移植到了鸿蒙系统,因此我们也可以在鸿蒙系统中使用axios发送http请求,下面重点为大家介绍axios的用法。
在课程资料中找到单词打卡项目的后台服务器,安装并启动。程序启动后,会在本地启动一个HTTP服务,并显示该服务的接口文档以及接口地址,如下图所示:
需要注意的是,为保证真机也能访问该HTTP服务,需要将个人电脑和真机接入同一个局域网,这样真机就能通过局域网IP访问HTTP服务了,上图中显示的接口地址就是个人电脑在局域网中的地址。
另外,模拟器和真机一样,也需要通过局域网访问该服务。不同的是,模拟器会自动加入主机所在的网络,因此使用模拟器时,只需将个人电脑加入任一局域网即可。
默认情况下,应用只能访问有限的系统资源,若应用需要访问一些受保护的数据(照片、通讯录、位置等)或者功能(打电话、发短信、联网等),需要先申请相应的权限。鸿蒙系统的权限列表可参考官方文档。
权限的申请可分为如下两步
第一步:声明所需权限
开发者需要在entry/src/main/module.json5
文件中声明所需权限,具体格式如下
{"module": {......"requestPermissions": [{"name": 'ohos.permission.******'}]}}
第二步:申请授权
由于网络访问权限的授权方式为system_grant,因此只需完成第一步即可,具体内容如下
{"module": {......"requestPermissions": [{"name": 'ohos.permission.INTERNET'}]}}
axios相当于鸿蒙应用项目的一个第三方库,鸿蒙应用项目使用ohpm管理(包括安装、卸载等)第三方库。除了axios,还有很多其他的第三方库可供开发者使用,所有的第三方库都收录在OpenHarmony三方库中心仓中。
为方便执行ohpm命令,需先将ohpm的安装目录添加到操作系统的Path
环境变量下,具体操作如下
第一步:查看ohpm安装目录
打开Deveco Studio设置界面,搜索ohpm
第二步:配置环境变量
将上述目录添加到Path
环境变量
完成上述环境变量的配置之后,便可在任意目录下执行ohpm命令了。
点击Deveco Studio底部的Terminal选项卡,启动终端
之后在终端执行如下命令即可
ohpm i @ohos/axios
第一步:导入axios
import axios from '@ohos/axios'
第二步:创建axios实例
const instance = axios.create({baseURL: 'http://<ip>:<port>',timeout: 2000})
**注意:**需要根据实际情况替换上述的ip地址和端口号
第三步:发送http请求
创建axios实例后,便可通过该实例的api来发送各种http请求,常用的api定义如下
api | 功能 |
---|---|
get(url, config?): Promise | 发送GET请求 |
delete(url, config?): Promise | 发送DELETE请求 |
post(url, data?, config?): Promise | 发送POST请求 |
put(url, data?, config?): Promise | 发送PUT请求 |
第四步:获取请求结果
上述api的返回值类型均为Promise
,Promise
是JavaScript中用于表示异步操作结果的对象,若操作成功,其中会包含具体结果,若操作失败,其会包含错误的原因。在实际应用中,开发者可以通过该对象的then()
方法来处理操作成功时的结果,通过catch()
方法来处理操作失败的情况,例如
get(...).then((response:AxiosResponse)=>{//处理请求成功的结果...}).catch((error:AxiosError)=>{//处理请求失败的错误...})
AxiosResponse是axios定义的响应结果类型,默认情况下,通过axios发送的所有请求,其成功的响应结果都是该类型。其包含的属性如下
{//服务器返回的响应结果data: {},//服务器响应的 HTTP 状态码status: 200,//服务器响应的 HTTP 状态信息statusText: 'OK',// 服务器响应头headers: {},//axios请求的配置信息config: {},//生成此响应的请求信息request: {}}
因此,response.data
才是服务器返回的真实数据,具体处理逻辑如下
get(...).then((response:AxiosResponse)=>{//获取服务器返回的数据let data = response.data;//处理服务器返回的数据...}).catch((error:AxiosError)=>{//处理请求失败的错误...})
相关案例见:Demos/entry/src/main/ets/pages/axios/solution/AxiosPage.ets
axios可以分别为请求和响应配置拦截器,请求拦截器可在请求发送前进行拦截,响应拦截器可以在then()
或者catch()
方法执行前进行拦截,如下图所示
在拦截器中,开发者可以对请求的参数或者响应的结果做一些统一的处理,比如在请求拦截器中统一为所有请求增加token
这一Header,在响应拦截器中统一处理错误响应。
拦截器的配置方式如下
请求拦截器
// 添加请求拦截器instance.interceptors.request.use((config:InternalAxiosRequestConfig) => {// 对请求数据做点什么return config;}, (error:AxiosError) => {// 对请求错误做些什么return Promise.reject(error);});
响应拦截器
// 添加响应拦截器instance.interceptors.response.use((response:AxiosResponse)=> {// 对响应数据做点什么return response;}, (error:AxiosError)=> {// 对响应错误做点什么return Promise.reject(error);});
第5章为大家介绍了一系列状态共享相关的装饰器(@State
、@Prop
、@Link
、@Provide
、@Consume
等),但是这些装饰器仅能在两个组件之间共享状态,如果开发者需要在一个页面内的所有组件中共享状态,或者是在多个页面之间共享状态,这些装饰器便不再适用了,此时我们需要的就是应用级状态管理功能。
LocalStorage用于存储页面级的状态数据,位于LocalStorage中的状态数据可以在一个页面内的所有组件中共享,其用法如下
第一步:创建LocalStorage实例,并初始化状态变量
let storage = new LocalStorage({ count: 0 });
第二步:将LocalStorage实例绑定到页面的入口组件
@Entry(storage)@Componentstruct Parent {build(){......}}
第三步:在页面内的组件访问LocalStorage
ArkTs提供了两个装饰器用于访问LocalStorage,分别是@LocalStorageProp
和@LocalStorageLink
,前者可以和LocalStorage实现单向同步,后者可以和LocalStorage实现双向同步,具体用法如下
父组件 | 子组件 |
---|---|
let storage = new LocalStorage({ count: 0 }); @Entry(storage) @Component struct Parent { //与storage中的count属性双向同步 @LocalStorageLink('count') count: number = 0; build(){ Child() } } | @Component struct Child { //与storage中的count属性单向同步 @LocalStorageProp('count') count: number = 0; build(){ ... } } |
相关案例见:Demos/entry/src/main/ets/pages/storage/localStorage/LocalStoragePage.ets
AppStorage用于存储应用级的状态数据,位于AppStorage中的状态数据可以在整个应用的所有组件中共享,其用法如下
第一步:初始化状态变量
AppStorage.SetOrCreate('count', 0)
第二步:在整个应用内的组件中访问AppStorage
ArkTs提供了两个装饰器用于访问AppStorage实例,分别是@StorageProp
和@StorageLink
,前者可以和AppStorage实现单向同步,后者可以和AppStorage实现双向同步,具体用法如下
PageOne | PageTwo |
---|---|
AppStorage.SetOrCreate('count', 0) @Entry @Component struct PageOne { //与AppStorage中的count属性双向同步 @StorageLink('count') count: number = 0; build(){ ... } } | @Entry @Component struct PageTwo { //与AppStorage中的count属性单向同步 @StorageProp('count') count: number = 0; build(){ ... } } |
相关案例见:Demos/entry/src/main/ets/pages/storage/appStorage/PageOne.ets
LocalStorage和AppStorage都是将状态数据保存在内存中,应用一旦退出,数据就会被清理,如果需要对数据进行持久化存储,就需要用到PersistentStorage,其用法如下
PersistentStorage可以将指定的AppStorage中的属性保存到磁盘中,并且PersistentStorage和AppStorage的该属性会自动建立双向同步,开发者不能直接访问PersistentStorage中的属性,而只能通过AppStorage进行访问,具体操作如下
PersistentStorage.PersistProp('count', 0);@Entry@Componentstruct Index {@StorageLink('count') count: number = 0build() {Row() {Column() {// 应用退出时会保存当前结果。重新启动后,会显示上一次的保存结果Text(`${this.count}`).onClick(() => {this.count += 1;})}}}}
到目前为止,我们已经掌握了网络请求、应用级状态管理,下面我们完成HarmonyOS 4.0实战项目之单词打卡的第5、6、7、8章
应用模型是HarmonyOS为开发者提供的应用程序所需能力的抽象提炼,它提供了应用程序必备的组件和运行机制。有了应用模型,开发者可以基于一套统一的模型进行应用开发,使应用开发更简单、高效。
随着系统的演进发展,HarmonyOS先后提供了两种应用模型
Stage模型贯穿一个鸿蒙应用的整个生命周期,从源码到编译再到运行,都会涉及到Stage模型相关的概念。
UIAblitity是Stage模型中最常用的组件,其生命周期函数及调用时机如下
onCreate()
:当UIAblitity实例创建完成时被调用,可在该函数中进行应用的初始化操作,例如定义全局变量onForeground()
:当UIAblitity实例切换至前台时被调用,可在该函数中加载所需的系统资源onBackground()
:当UIAblitity实例切换至后台时被调用,可在函数中释放系统资源,以节省系统资源的消耗onDestroy()
:当UIAblitity实例销毁时被调用,可在该函数中释放系统资源,保存数据等另外WindowStage也有两个生命周期函数,如下
onWindowStageCreate()
:当WindowStage实例创建时被调用,可在该函数中设置应用加载的初始页面onWindowStageDestroy()
:当WindowStage实例销毁时被调用上述函数的调用时序如下图所示
基于Stage模型创建的工程,有两个重要的配置文件,一个是应用级的配置文件app.json5
,另一个是模块级的module.json5
。
app.json5
用于配置整个应用的基本信息,包括应用的包名、开发厂商、版本号等。
module.json5
用于配置当前模块的各项信息,包括基本信息(例如名称、类型、描述、支持的设备类型等)、Ablitity组件信息、所需权限等
具体内容可参考官方文档:app.json5配置文件、module.json5配置文件