ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[HarmonyOS应用开发者基础认证](https://developer.huawei.com/consumer/cn/training/dev-cert-detail/101666948302721398) [HarmonyOS学习链接](https://www.html5iq.com/HarmonyOS/index) [可以下载使用谷歌浏览器插件:Smart TOC 生成目录](https://www.chajianxw.com/product-tool/11058.html) ## **一、HarmonyOS第一课:运行Hello World** ## **闯关习题** * DevEco Studio是开发HarmonyOS应用的一站式集成开发环境:正确 * main_pages.json存放页面page路径配置信息。正确 * 在stage模型中,下列配置文件属于AppScope文件夹的是?app.json5 * 如何在DevEco Studio中创建新项目? * 如果已打开项目,从DevEco Studio菜单选择'file>new>Create Project' * 如果第一次打开DevEco Studio,在欢迎页点击“Create new Project” * module.json5配置文件中,包含了以下哪些信息?ability的相关配置信息、模块名、模块类型 ## **1.1 工程级目录** 工程的目录结构如下。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114004.05887895835754685037375708831627:50001231000000:2800:37FDA3724BF0FFCEAF66529859557B101D5734568D59B537A7F1C7F68B980649.png?needInitFileName=true?needInitFileName=true) 其中详细如下: * AppScope中存放应用全局所需要的资源文件。 * entry是应用的主模块,存放HarmonyOS应用的代码、资源等。 * oh_modules是工程的依赖包,存放工程依赖的源文件。 * build-profile.json5是工程级配置信息,包括签名、产品配置等。 * hvigorfile.ts是工程级编译构建任务脚本,hvigor是基于任务管理机制实现的一款全新的自动化构建工具,主要提供任务注册编排,工程模型管理、配置管理等核心能力。 * oh-package.json5是工程级依赖配置文件,用于记录引入包的配置信息。 在AppScope,其中有resources文件夹和配置文件app.json5。AppScope>resources>base中包含element和media两个文件夹 * 其中element文件夹主要存放公共的字符串、布局文件等资源。 * media存放全局公共的多媒体资源文件。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114004.66991068605762837436772598690613:50001231000000:2800:B0912A0768CC652258DE63C2AA95B71553464E99DC045E5CADD6255D0127FA2E.png?needInitFileName=true?needInitFileName=true) ## **1.2 模块级目录** ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114005.30439888302738843459091078997510:50001231000000:2800:1846C2BAF2EC319E7C9926ACF088719DEEB05329FB3F8D23B42341AE1424D873.png?needInitFileName=true?needInitFileName=true) entry>src目录中主要包含总的main文件夹,单元测试目录ohosTest,以及模块级的配置文件。 * main文件夹中,ets文件夹用于存放ets代码,resources文件存放模块内的多媒体及布局文件等,module.json5文件为模块的配置文件。 * ohosTest是单元测试目录。 * build-profile.json5是模块级配置信息,包括编译构建配置项。 * hvigorfile.ts文件是模块级构建脚本。 * oh-package.json5是模块级依赖配置信息文件。 进入src>main>ets目录中,其分为entryability、pages两个文件夹。 * entryability存放ability文件,用于当前ability应用逻辑和生命周期管理。 * pages存放UI界面相关代码文件,初始会生成一个Index页面。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114005.55395222861315605930038112201035:50001231000000:2800:5EDE456E4383B183FA750A8B6EB3FDA550DBF837A8595259CFDC227CBF4AE68A.png?needInitFileName=true?needInitFileName=true) resources目录下存放模块公共的多媒体、字符串及布局文件等资源,分别存放在element、media文件夹中。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114005.62166781200500169814163464827141:50001231000000:2800:BA63DD9E3D1AFB0FE819E117D730E583828B005C5A37E855E0794C2338A8D28E.png?needInitFileName=true?needInitFileName=true) ## **1.3 app.json5** AppScope>app.json5是应用的全局的配置文件,用于存放应用公共的配置信息。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114005.97532016223915463239903576377060:50001231000000:2800:FBC56685FD6E983F0C565834D3ADFDFE103B0051CB10C75791A30BAE47D8A9E7.png?needInitFileName=true?needInitFileName=true) 其中配置信息如下: * bundleName是包名。 * vendor是应用程序供应商。 * versionCode是用于区分应用版本。 * versionName是版本号。 * icon对应于应用的显示图标。 * label是应用名。 ## **1.4 module.json5** module.json5配置文件中,包含了以下哪些信息? entry>src>main>module.json5是模块的配置文件,包含当前模块的配置信息。 :-: **表1**module.json5默认配置属性及描述 | 属性| 描述 | | :-- | :-- | | name| 该标签标识当前module的名字,module打包成hap后,表示hap的名称,标签值采用字符串表示(最大长度31个字节),该名称在整个应用要唯一。 | | type | 表示模块的类型,类型有三种,分别是entry、feature和har。| | srcEntry | 当前模块的入口文件路径。| | description | 当前模块的描述信息。 | | mainElement | 该标签标识hap的入口ability名称或者extension名称。只有配置为mainElement的ability或者extension才允许在服务中心露出。 | | deviceTypes | 该标签标识hap可以运行在哪类设备上,标签值采用字符串数组的表示 | | deliveryWithInstall | 标识当前Module是否在用户主动安装的时候安装,表示该Module对应的HAP是否跟随应用一起安装。- true:主动安装时安装。- false:主动安装时不安装。 | | installationFree | 标识当前Module是否支持免安装特性。\- true:表示支持免安装特性,且符合免安装约束。\- false:表示不支持免安装特性。 | |pages| 对应的是main\_pages.json文件,用于配置ability中用到的page信息。| | abilities | 是一个数组,存放当前模块中所有的ability元能力的配置信息,其中可以有多个ability。| ## **1.5 main_pages.json** src/main/resources/base/profile/main\_pages.json文件保存的是页面page的路径配置信息,所有需要进行路由跳转的page页面都要在这里进行配置。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114005.19949345694859208383881946076343:50001231000000:2800:647DF640864F5E7A8E14EAF4473367588A023B3440743294F48DCC765BBB9E78.png?needInitFileName=true?needInitFileName=true "点击放大") ## **二、HarmonyOS第一课:ArkTS开发语言介绍** ## **闯关习题** * 循环渲染ForEach可以从数据源中迭代获取数据,并为每个数组项创建相应的组件。正确 * @Link变量不能在组件内部进行初始化。正确 * 用哪一种装饰器修饰的struct表示该结构体具有组件化能力?@Component * 用哪一种装饰器修饰的自定义组件可作为页面入口组件?@Entry * 下面哪些函数是自定义组件的生命周期函数?aboutToAppear、aboutToDisappear、onPageShow、onPageHide、onBackPress * 下面哪些装饰器可以用于管理自定义组件中变量的状态?@State、@Link ## **2.1 TypeScript快速入门** ## **2.1.1 编程语言介绍** ArkTS是HarmonyOS优选的主力应用开发语言。它在TypeScript(简称TS)的基础上,匹配ArkUI框架,扩展了声明式UI、状态管理等相应的能力,让开发者以更简洁、更自然的方式开发跨端应用。要了解什么是ArkTS,我们首先要了解下ArkTS、TypeScript和JavaScript之间的关系: * JavaScript是一种属于网络的高级脚本语言,已经被广泛用于Web应用开发,常用来为网页添加各式各样的动态功能,为用户提供更流畅美观的浏览效果。 * TypeScript 是 JavaScript 的一个超集,它扩展了 JavaScript 的语法,通过在JavaScript的基础上添加静态类型定义构建而成,是一个开源的编程语言。 * ArkTS兼容TypeScript语言,拓展了声明式UI、状态管理、并发任务等能力。 由此可知,TypeScript是JavaScript的超集,ArkTS则是TypeScript的超集,他们的关系如下图所示: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114006.09789960580347920997827335333048:50001231000000:2800:E2A927D873B16EC199132033001B4EE99724EE28864E0E4F83502508B6FD4A23.png?needInitFileName=true?needInitFileName=true "点击放大") 在学习ArkTS声明式的相关语法之前,我们首先学习下TypeScript的基础语法。 ## **2.1.2 基础类型** TypeScript支持一些基础的数据类型,如布尔型、数组、字符串等,下文举例几个较为常用的数据类型,我们来了解下他们的基本使用。 `布尔值` TypeScript中可以使用boolean来表示这个变量是布尔值,可以赋值为true或者false。 ~~~ let isDone: boolean = false; ~~~ `数字` TypeScript里的所有数字都是浮点数,这些浮点数的类型是 number。除了支持十进制,还支持二进制、八进制、十六进制。 ~~~ let decLiteral: number = 2023;let binaryLiteral: number = 0b11111100111;let octalLiteral: number = 0o3747;let hexLiteral: number = 0x7e7; ~~~ `字符串` TypeScript里使用 string表示文本数据类型, 可以使用双引号( ")或单引号(')表示字符串。 ~~~ let name: string = "Jacky";name = "Tom";name = 'Mick'; ~~~ `数组` TypeScrip有两种方式可以定义数组。 第一种,可以在元素类型后面接上 \[\],表示由此类型元素组成的一个数组。 ~~~ let list: number[] = [1, 2, 3]; ~~~ 第二种方式是使用数组泛型,Array。 ~~~ let list: Array<number> = [1, 2, 3]; ~~~ `元组` 元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为 string和number类型的元组。 ~~~ let x: [string, number];x = ['hello', 10]; // OKx = [10, 'hello']; // Error ~~~ `枚举` enum类型是对JavaScript标准数据类型的一个补充,使用枚举类型可以为一组数值赋予友好的名字。 ~~~ enum Color {Red, Green, Blue};let c: Color = Color.Green; ~~~ `Unknown` 有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。那么我们可以使用unknown类型来标记这些变量。 ~~~ let notSure: unknown = 4;notSure = 'maybe a string instead';notSure = false; ~~~ `Void` 当一个函数没有返回值时,你通常会见到其返回值类型是 void。 ~~~ function test(): void { console.log('This is function is void');} ~~~ `Null 和 Undefined` TypeScript里,undefined和null两者各自有自己的类型分别叫做undefined和null。 ~~~ let u: undefined = undefined;let n: null = null; ~~~ `联合类型` 联合类型(Union Types)表示取值可以为多种类型中的一种。 ~~~ let myFavoriteNumber: string | number;myFavoriteNumber = 'seven';myFavoriteNumber = 7; ~~~ ## **2.1.3 条件语句** 条件语句用于基于不同的条件来执行不同的动作。TypeScript 条件语句是通过一条或多条语句的执行结果(True 或 False)来决定执行的代码块。 `if 语句` TypeScript if 语句由一个布尔表达式后跟一个或多个语句组成。 ~~~ var num:number = 5if (num > 0) { console.log('数字是正数') } ~~~ `if...else 语句` 一个 if 语句后可跟一个可选的 else 语句,else 语句在布尔表达式为 false 时执行。 ~~~ var num:number = 12; if (num % 2==0) { console.log('偶数'); } else { console.log('奇数'); } ~~~ `if...else if....else 语句` if...else if....else 语句在执行多个判断条件的时候很有用。 ~~~ var num:number = 2 if(num > 0) { console.log(num+' 是正数') } else if(num < 0) { console.log(num+' 是负数') } else { console.log(num+' 为0') } ~~~ `switch…case 语句` 一个 switch 语句允许测试一个变量等于多个值时的情况。每个值称为一个 case,且被测试的变量会对每个 switch case 进行检查。 ~~~ 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; } } ~~~ ## **2.1.4 函数** 函数是一组一起执行一个任务的语句,函数声明要告诉编译器函数的名称、返回类型和参数。TypeScript可以创建有名字的函数和匿名函数,其创建方法如下: ~~~ // 有名函数function add(x, y) { return x + y;} // 匿名函数let myAdd = function (x, y) { return x + y;}; ~~~ `为函数定义类型` 为了确保输入输出的准确性,我们可以为上面那个函数添加类型: ~~~ // 有名函数:给变量设置为number类型function add(x: number, y: number): number { return x + y;} // 匿名函数:给变量设置为number类型let myAdd = function (x: number, y: number): number { return x + y;}; ~~~ `可选参数` 在TypeScript里我们可以在参数名旁使用 ?实现可选参数的功能。 比如,我们想让lastName是可选的: ~~~ function buildName(firstName: string, lastName?: string) { if (lastName) return firstName + ' ' + lastName; else return firstName;} let result1 = buildName('Bob');let result2 = buildName('Bob', 'Adams'); ~~~ `剩余参数` 剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。 可以使用省略号( ...)进行定义: ~~~ function getEmployeeName(firstName: string, ...restOfName: string[]) { return firstName + ' ' + restOfName.join(' ');} let employeeName = getEmployeeName('Joseph', 'Samuel', 'Lucas', 'MacKinzie'); ~~~ ``箭头函数`` ES6版本的TypeScript提供了一个箭头函数,它是定义匿名函数的简写语法,用于函数表达式,它省略了function关键字。箭头函数的定义如下,其函数是一个语句块: ~~~ ( [param1, parma2,…param n] )=> { // 代码块} ~~~ 其中,括号内是函数的入参,可以有0到多个参数,箭头后是函数的代码块。我们可以将这个箭头函数赋值给一个变量,如下所示: ~~~ let arrowFun = ( [param1, parma2,…param n] )=> { // 代码块} ~~~ 如何要主动调用这个箭头函数,可以按如下方法去调用: ~~~ arrowFun(param1, parma2,…param n) ~~~ 接下来我们看看如何将我们熟悉的函数定义方式转换为箭头函数。我们可以定义一个判断正负数的函数,如下: ~~~ function testNumber(num: number) { if (num > 0) { console.log(num + ' 是正数'); } else if (num < 0) { console.log(num + ' 是负数'); } else { console.log(num + ' 为0'); }} ~~~ 其调用方法如下: ~~~ testNumber(1) //输出日志:1 是正数 ~~~ 如果将这个函数定义为箭头函数,定义如下所示: ~~~ 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 是负数 ~~~ 后面,我们在学习HarmonyOS应用开发时会经常用到箭头函数。例如,给一个按钮添加点击事件,其中onClick事件中的函数就是箭头函数。 ~~~ Button("Click Now") .onClick(() => { console.info("Button is click") }) ~~~ ## **2.1.5 类** TypeScript支持基于类的面向对象的编程方式,定义类的关键字为 class,后面紧跟类名。类描述了所创建的对象共同的属性和方法。 `类的定义` 例如,我们可以声明一个Person类,这个类有3个成员:一个是属性(包含name和age),一个是构造函数,一个是getPersonInfo方法,其定义如下所示。 ~~~ class Person { private name: string private age: number constructor(name: string, age: number) { this.name = name; this.age = age; } public getPersonInfo(): string { return `My name is ${this.name} and age is ${this.age}`; }} ~~~ 通过上面的Person类,我们可以定义一个人物Jacky并获取他的基本信息,其定义如下: ~~~ let person1 = new Person('Jacky', 18);person1.getPersonInfo(); ~~~ `继承` 继承就是子类继承父类的特征和行为,使得子类具有父类相同的行为。TypeScript中允许使用继承来扩展现有的类,对应的关键字为extends。 ~~~ 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}`; }} ~~~ 通过上面的Employee类,我们可以定义一个人物Tom,这里可以获取他的基本信息,也可以获取他的雇主信息,其定义如下: ~~~ let person2 = new Employee('Tom', 28, 'HuaWei');person2.getPersonInfo();person2.getEmployeeInfo(); ~~~ 在TypeScript中,有public、private、protected修饰符,其功能和具体使用场景大家可以参考TypeScript的相关学习资料,进行拓展学习。 ## **2.1.6 模块** 随着应用越来越大,通常要将代码拆分成多个文件,即所谓的模块(module)。模块可以相互加载,并可以使用特殊的指令 export 和 import 来交换功能,从另一个模块调用一个模块的函数。 两个模块之间的关系是通过在文件级别上使用 import 和 export 建立的。模块里面的变量、函数和类等在模块外部是不可见的,除非明确地使用 export 导出它们。类似地,我们必须通过 import 导入其他模块导出的变量、函数、类等。 `导出` 任何声明(比如变量,函数,类,类型别名或接口)都能够通过添加export关键字来导出,例如我们要把NewsData这个类导出,代码示意如下: ~~~ export class NewsData { title: string; content: string; imagesUrl: Array<NewsFile>; source: string; constructor(title: string, content: string, imagesUrl: Array<NewsFile>, source: string) { this.title = title; this.content = content; this.imagesUrl = imagesUrl; this.source = source; }} ~~~ `导入` 模块的导入操作与导出一样简单。 可以使用以下 import形式之一来导入其它模块中的导出内容。 ~~~ import { NewsData } from '../common/bean/NewsData'; ~~~ ## **2.1.7 迭代器** 当一个对象实现了Symbol.iterator属性时,我们认为它是可迭代的。一些内置的类型如Array,Map,Set,String,Int32Array,Uint32Array等都具有可迭代性。 `for..of 语句` for..of会遍历可迭代的对象,调用对象上的Symbol.iterator方法。 下面是在数组上使用for..of的简单例子: ~~~ let someArray = [1, "string", false]; for (let entry of someArray) { console.log(entry); // 1, "string", false} ~~~ `for..of vs. for..in 语句` for..of和for..in均可迭代一个列表,但是用于迭代的值却不同:for..in迭代的是对象的键,而for..of则迭代的是对象的值。 ~~~ 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"} ~~~ ## **2.2 浅析ArkTS的起源和演进** ## **2.2.1 引言** Mozilla创造了JS,Microsoft创建了TS,Huawei进一步推出了ArkTS。 从最初的基础的逻辑交互能力,到具备类型系统的高效工程开发能力,再到融合声明式UI、多维状态管理等丰富的应用开发能力,共同组成了相关的演进脉络。 ArkTS是HarmonyOS优选的主力应用开发语言。它在TypeScript(简称TS)的基础上,扩展了声明式UI、状态管理等相应的能力,让开发者可以以更简洁、更自然的方式开发高性能应用。TS是JavaScript(简称JS)的超集,ArkTS则是TS的超集。ArkTS会结合应用开发和运行的需求持续演进,包括但不限于引入分布式开发范式、并行和并发能力增强、类型系统增强等方面的语言特性。本期我们结合JS和TS以及相关的开发框架的发展,为大家介绍ArkTS的起源和演进思路。 ## **2.2.2 JS** JS语言由Mozilla创造,最初主要是为了解决页面中的逻辑交互问题,它和HTML(负责页面内容)、CSS(负责页面布局和样式)共同组成了Web页面/应用开发的基础。随着Web和浏览器的普及,以及Node.js进一步将JS扩展到了浏览器以外的环境,JS语言得到了飞速的发展。在2015年相关的标准组织ECMA发布了一个主要的版本ECMAScript 6(简称ES6),这个版本具备了较为完整的语言能力,包括类(Class)、模块(Module)、相关的语言基础API增强(Map/Set等)、箭头函数(Arrow Function)等。从2015年开始,ECMA每年都会发布一个标准版本,比如ES2016/ES2017/ES2018等,JS语言越来越成熟。 为了提升应用的开发效率,相应的JS前端框架也不断地涌现出来。其中比较典型的有Facebook发起的React.js,以及个人开发者尤雨溪发起的Vue.js。React和Vue的主要出发点都是将响应式编程的能力引入到应用开发中,实现数据和界面内容的自动关联处理。具体的实现方式上,React对JS做了一些扩展,引入了JSX(JavaScript XML)语法,可以将HTML的内容统一表示成JS来处理;Vue则是通过扩展的模板语法(Template)的方式来处理。 下面通过两个示例,为大家简要介绍React和Vue。(**示例来源于w3schools网站:**[https://www.w3schools.com/whatis/](https://www.w3schools.com/whatis/)) `1. React示例` **图1** React示例 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102104340.94108790062912697966750549293034:50001231000000:2800:DBC3B5AE63B6879EE21AF9E59D564ADAFE30E9D7CDFF6E6EA226D4128B2982C5.png) 以上代码描述了React如何在指定的页面元素(id为id01的div元素)中改变相应的字符串内容(从"Hello World!"到"Hello John Doe!")。其中第5行的ReactDOM.render()是React JS库提供的一个方法,它可以将相应的内容刷新到指定的HTML元素中。第6行是符合JSX语义的一段代码,它包含了一个类似HTML结构的字符串(...),以及一个表达数据绑定语义的字段({name}),会关联到第4行定义的name变量。通过这种方式,JSX把HTML的语义以及数据绑定机制和JS语言结合起来,可以方便地在JS语言中使用。 `2. Vue示例` **图2** Vue示例 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102104352.79912171437667408945886710827184:50001231000000:2800:112F46F30295D467BA9DFF79E4FF0970B0F988D51013AF8C2460142BE3BA0DC9.png) 以上Vue示例代码也描述了类似的功能。其中第1~3行是类似HTML的语法,描述一个id为app的div页面元素,其中的{{message}}是数据绑定的语义,在Vue中表示为Template。第6~9行是JS代码,描述了一个Vue对象,对应了上述的app页面元素以及所需的数据变量message的内容信息。第11~13行则是JS函数,它改变message变量的值为"John Doe"。执行这个函数时Vue会自动实现相应的UI界面刷新。 如上所示,React和Vue所表达的能力是类似的,不过侧重点稍微有所不同。React主要是基于JSX的语法,将类HTML的语法融合到JS语言中;Vue则是基于Template机制,在HTML的基础上扩展相应的语义。当然,上面这两个例子只是简要地描述了React和Vue的基础信息,更详细的语法以及CSS相关的使用等都没涉及。 从运行时的维度来看,基于React以及Vue的应用都可运行在Web引擎上。为了进一步提升相应的性能体验,2015年Facebook在React基础上推出了React Native, 在渲染架构上没有采用传统的Web引擎渲染路径,而是桥接到相应OS平台的原生UI组件上。2019年Facebook引入全新实现的JS引擎Hermes,并推出一系列架构改进来进一步提升React Native的性能体验。2016年阿里巴巴开源的Weex则是基于Vue做了一些类似的改进,也是采用了桥接到原生UI组件的渲染路径。 ## **2.2.3 TS** 随着JS生态的发展,如何更有效地支撑大型的应用工程开发变成了一个重要的课题。大型的应用工程一般会涉及较复杂的代码以及较多的团队协作,对语言的规范性,模块的复用性、扩展性以及相关的开发工具都提出了更高的要求。为此,Microsoft在JS的基础上,创建了TS语言,并在2014年正式发布了1.0版本。TS主要从以下几个方面做了进一步的增强: * 引入了类型系统,并提供了类型检查以及类型自动推导能力,可以进行编译时错误检查,有效的提升了代码的规范性以及错误检测范围和效率。 * 在类型系统基础上,引入了声明文件(Declaration Files)来管理接口或其他自定义类型。声明文件一般是以d.ts的形式来定义模块中的接口,这些接口和具体的实现做了相应的分离,有助于各模块之间的分工协作。另外,TS通过接口,泛型(Generics)等相关特性的支持,进一步增强了设计复杂的框架所需的扩展以及复用能力。 在工具层面,TS也有相应的编辑器、编译器、IDE(Integrated Development Environment)插件等相关的工具,来进一步提升开发效率。 TS在兼容JS生态方面也做了较好的平衡,TS应用通过相应编译器可以编译出纯JS应用,可以在标准的JS引擎上运行。同时,TS定位为JS的超集,即JS应用也是合法的TS应用。此外,在标准层面上,TS兼容ECMA的相应标准,并维护那些还未成为ECMA标准的新特性。 ## **2.2.4 ArkTS** 如上所述,基于JS的前端框架以及TS的引入,进一步提升了应用开发效率,但依然存在一些不足。 `从开发者维度来看:` 写一个应用需要了解三种语言(JS/TS、HTML和CSS)。这对Web开发者相对友好,但对非Web开发者来说,负担较重。 `从运行时维度来看:` * 在语言运行时方面,尽管TS有了类型的加持,但也只是用于编译时检查,然后通过TS Compiler转成JS,运行时引擎还是无法利用到基于类型系统的优化。 * 在渲染方面,主流Web引擎由于本身复杂度以及历史原因,性能、资源占用方面与常见OS原生框架都有一定的差距,尤其在移动平台上。React Native通过渲染架构的改进一定程度上提升了性能体验,但在平台渲染效果和能力的一致性,以及JS语言性能等方面还是存在一定的不足。 Google在2018年底推出的Flutter则走了另外一条路,结合新的语言Dart,引入新的声明式开发范式,基于Skia的自绘制引擎构建可跨平台的独立的渲染能力。这是一种较为创新的方案,不过也有几点不足: * Dart语言生态。尽管Dart语言2011年就已推出,传闻其目标是取代JS,但整个生态还是非常弱小,Dart语言发布7年后随着Flutter的推出才有所改善。整体而言,Dart和主流语言生态相比还是有非常大的差距。 * 开发范式。Flutter暴露了很多细粒度的Widget接口,整体开发的简洁度,开发门槛,尤其是和Apple推出的SwiftUI相比,存在一定的差距。 有意思的是,Google在2021年又推出了新的开发框架Jetpack Compose,结合了Kotlin的语言生态,设计了新的声明式UI开发范式。 2019年,我们在思考如何构建新的应用开发框架的时候,从以下几个维度进行了重点考虑: * 语言生态 * 开发效率 * 性能体验 * 跨设备/跨平台能力 由于JS/TS有比较完善的开发者生态,语言也比较中立友好,有相应的标准组织可以逐步演进,JS/TS语言成了比较自然的选择。以JS/TS为基础,在开发框架的维度,我们做了如下的架构演进设计: * 通过基于JS扩展的类Web开发范式,来支持主流的前端开发方式。同步的,在运行时方面,通过渲染引擎的增强(平台无关的自绘制机制、声明式UI后端设计、动态布局/多态UI组件等),语言编译器和运行时的优化增强(代码预编译、高效FFI-Foreign Function Interface、引擎极小化等),进一步提升相关的性能体验,并可部署到不同设备上(包括百KB级内存的轻量设备)。另外,通过平台适配层的设计,构建了跨OS平台的基础设施。 * 通过基于TS扩展的声明式UI开发范式,提供了更简洁更自然的开发体验。在运行时方面,在上述的基础上,结合语言运行时的类型优化,以及渲染运行时的扁平化流水线技术等,进一步提升性能体验。 **图3** ArkUI开发框架 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102104405.81053425986211736818236644416294:50001231000000:2800:C4BDB880E1D7503824BBA83A0D6B298C45167CE3A25BE6CDEEB552C343EF8AE5.png) 图3描述了ArkUI开发框架的整体架构,其中,基于TS扩展的声明式UI范式中所用的语言就是ArkTS。下面结合一个具体示例,从应用开发视角简单介绍下基于ArkTS的全新声明式开发范式。 如图4所示的代码示例,UI界面会显示两段文本和一个按钮,当开发者点击按钮时,文本内容会从'Hello World'变为‘Hello ArkUI’。 **图4** ArkTS声明式开发范式 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102104418.97167608875006783664909326988909:50001231000000:2800:B0A2A3474E494DE620CBF58C0DBB2F835EBDAC07C87E2376865D3CA5ED979896.png) `这个示例中所包含的ArkTS声明式开发范式的基本组成说明如下:` * 装饰器 用来装饰类、结构体、方法以及变量,赋予其特殊的含义,如上述示例中 @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框架的需求角度,ArkTS在TS的类型系统的基础上,做了进一步的扩展:**定义了各种装饰器、自定义组件和UI描述机制**,再配合UI开发框架中的UI内置组件、事件方法、属性方法等共同构成了应用开发的主体。在应用开发中,除了UI的结构化描述之外,还有一个重要的方面:**状态管理**。如上述示例中,用 @State 装饰过的变量 myText ,包含了一个基础的状态管理机制,即 myText 的值的变化会自动触发相应的 UI 变更 (Text组件)。**ArkUI 中进一步提供了多维度的状态管理机制**。和 UI 相关联的数据,不仅可以在组件内使用,还可以在不同组件层级间传递,比如父子组件之间,爷孙组件之间,也可以是全局范围内的传递,还可以是跨设备传递。另外,从数据的传递形式来看,可分为**只读的单向传递和可变更的双向传递**。开发者可以灵活的利用这些能力来实现数据和 UI 的联动。 总体而言,ArkUI开发框架通过扩展成熟语言、结合语法糖或者语言原生的元编程能力、以及UI组件、状态管理等方面设计了统一的UI开发范式,结合原生语言能力共同完成应用开发。这些构成了当前ArkTS基于TS的主要扩展。 `ArkUI完整的开发范式可参考这里:` [https://developer.harmonyos.com/cn/docs/documentation/doc-guides/arkui-overview-0000001281480754](https://developer.harmonyos.com/cn/docs/documentation/doc-guides/arkui-overview-0000001281480754) ## **2.3 ArkTS开发实践** ## **2.3.1 声明式UI基本概念** 应用界面是由一个个页面组成,ArkTS是由ArkUI框架提供,用于以声明式开发范式开发界面的语言。 声明式UI构建页面的过程,其实是组合组件的过程,声明式UI的思想,主要体现在两个方面: * 描述UI的呈现结果,而不关心过程 * 状态驱动视图更新 类似苹果的SwiftUI中通过组合视图View,安卓Jetpack Compose中通过组合@Composable函数,ArkUI作为HarmonyOS应用开发的UI开发框架,其使用ArkTS语言构建自定义组件,通过组合自定义组件完成页面的构建。 ## **2.3.2 自定义组件的组成** ArkTS通过struct声明组件名,并通过@Component和@Entry装饰器,来构成一个自定义组件。 使用@Entry和@Component装饰的自定义组件作为页面的入口,会在页面加载时首先进行渲染。 ~~~ @Entry@Componentstruct ToDoList {...} ~~~ 例如ToDoList组件对应如下整个代办页面。 **图1**ToDoList待办列表 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114005.08216763926514493509595362163178:50001231000000:2800:98A6F4B1B294C6C0CF94A70581AAA34BB495CBA86381BBA75488D1C71BA862FE.png?needInitFileName=true?needInitFileName=true) 使用@Component装饰的自定义组件,如ToDoItem这个自定义组件则对应如下内容,作为页面的组成部分。 ~~~ @Componentstruct ToDoItem {...} ~~~ **图2**ToDoItem ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114005.75932547756921928359662551668593:50001231000000:2800:22C9276700FF1ABEBB38DB4666197E4358D8F0C1D92BF8DF29A5C3D089D96762.png?needInitFileName=true?needInitFileName=true "点击放大") 在自定义组件内需要使用build方法来进行UI描述。 ~~~ @Entry@Component struct ToDoList ... build() { ... } } ~~~ build方法内可以容纳内置组件和其他自定义组件,如Column和Text都是内置组件,由ArkUI框架提供,ToDoItem为自定义组件,需要开发者使用ArkTS自行声明。 ~~~ @Entry@Componentstruct ToDoList { ... build() { Column(...) { Text(...) ... ForEach(...{ TodoItem(...) },...) } ... }} ~~~ ## **2.3.3 配置属性与布局** 自定义组件的组成使用基础组件和容器组件等内置组件进行组合。但有时内置组件的样式并不能满足我们的需求,ArkTS提供了属性方法用于描述界面的样式。属性方法支持以下使用方式: * 常量传递 例如使用fontSize(50)来配置字体大小。 ~~~ Text('Hello World') .fontSize(50) ~~~ * 变量传递 在组件内定义了相应的变量后,例如组件内部成员变量size,就可以使用this.size方式使用该变量。 ~~~ Text('Hello World') .fontSize(this.size) ~~~ * 链式调用 在配置多个属性时,ArkTS提供了链式调用的方式,通过'.'方式连续配置。 ~~~ Text('Hello World') .fontSize(this.size) .width(100) .height(100) ~~~ * 表达式传递 属性中还可以传入普通表达式以及三目运算表达式。 ~~~ Text('Hello World') .fontSize(this.size) .width(this.count + 100) .height(this.count % 2 === 0 ? 100 : 200) ~~~ * 内置枚举类型 除此之外,ArkTS中还提供了内置枚举类型,如Color,FontWeight等,例如设置fontColor改变字体颜色为红色,并私有fontWeight为加粗。 ~~~ 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描述。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114005.58356571013514207478782675626695:50001231000000:2800:1A728FE591D657B25137F57CF295D1589E6AB7DF739009B3FB31B2408E333324.png?needInitFileName=true?needInitFileName=true) 这里我们将介绍最基础的两个布局——列布局和行布局。 对于如下每一项的布局,两个元素为横向排列,选择Row布局 **图3**Row布局 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114005.80107629230495433313301244207697:50001231000000:2800:901203F2739F52DCA8641B6355F1A84C9A1F5A85BC9EAEA33057B79974F87017.png?needInitFileName=true?needInitFileName=true "点击放大") ~~~ Row() { Image($r('app.media.ic_default')) ... Text(this.content) ...}... ~~~ 类似下图所示的布局,整体都是从上往下纵向排列,适用的布局方式是Column列布局。 **图4**Column布局 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114005.83007411797840114697967429764752:50001231000000:2800:13F2BAA1FD7C5B4B34495E02FE1B63BAD400AD1D9B3D5ED510B961ED6B21C397.png?needInitFileName=true?needInitFileName=true "点击放大") ~~~ Column() { Text($r('app.string.page_title')) ... ForEach(this.totalTasks,(item) => { TodoItem({content:item}) },...) } ~~~ ## **2.3.4 改变组件状态** 实际开发中由于交互,页面的内容可能需要产生变化,以每一个ToDoItem为例,其在完成时的状态与未完成时的展示效果是不一样的。 **图5**不同状态的视图 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114005.78299383157035977569357248263864:50001231000000:2800:CCD45AB03A26600002E3FEE6B5EECB7E24E50040365FA0F4450EE8D9DA208728.png?needInitFileName=true?needInitFileName=true "点击放大") 声明式UI的特点就是UI是随数据更改而自动刷新的,我们这里定义了一个类型为boolean的变量isComplete,其被@State装饰后,框架内建立了数据和视图之间的绑定,其值的改变影响UI的显示。 ~~~ @State isComplete : boolean = false; ~~~ **图6**@State装饰器的作用 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114005.17768528278725865389151857006615:50001231000000:2800:D2EC4581F8620BF0AA2D417853D59E099FB0BC651785E5A227C033E78422C2CA.png?needInitFileName=true?needInitFileName=true "点击放大") 用圆圈和对勾这样两个图片,分别来表示该项是否完成,这部分涉及到内容的切换,需要使用条件渲染if / else语法来进行组件的显示与消失,当判断条件为真时,组件为已完成的状态,反之则为未完成。 ~~~ 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'))} ~~~ 由于两个Image的实现具有大量重复代码,ArkTS提供了@Builder装饰器,来修饰一个函数,快速生成布局内容,从而可以避免重复的UI描述内容。这里使用@Bulider声明了一个labelIcon的函数,参数为url,对应要传给Image的图片路径。 ~~~ @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'))} ~~~ 使用时只需要使用this关键字访问@Builder装饰的函数名,即可快速创建布局。 ~~~ if (this.isComplete) { this.labelIcon($r('app.media.ic_ok'))} else { this.labelIcon($r('app.media.ic_default'))} ~~~ 为了让待办项带给用户的体验更符合已完成的效果,给内容的字体也增加了相应的样式变化,这里使用了三目运算符来根据状态变化修改其透明度和文字样式,如opacity是控制透明度,decoration是文字是否有划线。通过isComplete的值来控制其变化。 ~~~ Text(this.content) ... .opacity(this.isComplete ? CommonConstants.OPACITY_COMPLETED : CommonConstants.OPACITY_DEFAULT) .decoration({ type: this.isComplete ? TextDecorationType.LineThrough : TextDecorationType.None }) ~~~ 最后,为了实现与用户交互的效果,在组件上添加了onClick点击事件,当用户点击该待办项时,数据isComplete的更改就能够触发UI的更新。 ~~~ @Componentstruct ToDoItem { @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; }) }} ~~~ ## **2.3.5 循环渲染列表数据** 刚刚只是完成了一个ToDoItem组件的开发,当我们有多条待办数据需要显示在页面时,就需要使用到ForEach循环渲染语法。 例如这里我们有五条待办数据需要展示在页面上。 ~~~ total_Tasks:Array<string> = [ '早起晨练', '准备早餐', '阅读名著', '学习ArkTS', '看剧放松'] ~~~ ForEach基本使用中,只需要了解要渲染的数据以及要生成的UI内容两个部分,例如这里要渲染的数组为以上的五条待办事项,要渲染的内容是ToDoItem这个自定义组件,也可以是其他内置组件。 **图7**ForEach基本使用 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114005.53506681941979099871827168247996:50001231000000:2800:1A3961DAFF82E047C4F91BDBF1FFA1A7CDE73CFE01E4CA61045D71A489F899B1.png?needInitFileName=true?needInitFileName=true "点击放大") ToDoItem这个自定义组件中,每一个ToDoItem要显示的文本参数content需要外部传入,参数传递使用花括号的形式,用content接受数组内的内容项item。 最终完成的代码及其效果如下。 ~~~ @Entry@Componentstruct ToDoList { ... build() { Row() { Column() { Text(...) ... ForEach(this.totalTasks,(item) => { TodoItem({content:item}) },...) } .width('100%') } .height('100%') } } ~~~ **图8**ToDoList页面 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114005.12138313563148947387408726256839:50001231000000:2800:C207F4B1FC5A190DF56160071004365C18A9B4210AD75447A27F7AECB5B2E0B4.png?needInitFileName=true?needInitFileName=true) ## **三、HarmonyOS第一课:应用程序框架** ## **闯关习题** * 一个应用只能有一个UIAbility。错误 * 创建的Empty Ability模板工程,初始会生成一个UIAbility文件。:正确 * 每调用一次router.pushUrl()方法,页面路由栈数量均会加1。:错误 * API9及以上,router.pushUrl()方法,默认的跳转页面使用的模式是哪一种?Standard(多实例模式) * UIAbility启动模式需要在module.json5文件中配置哪个字段?launchType * API9及以上,router.pushUrl()方法的mode参数可以配置为以下哪几种跳转页面使用的模式?Single单实例模式和Standard多实例模式。 * UIAbility的生命周期有哪几个状态?包括Create、Foreground、Background、Destroy四个状态 * UIAbility有哪几种的启动模式?支持singleton(单实例模式)、multiton(多实例模式)和 specified(指定实例模式)3种启动模式。 ## **3.1 UIAbility概述** UIAbility是一种包含用户界面的应用组件,主要用于和用户进行交互。UIAbility也是系统调度的单元,为应用提供窗口在其中绘制界面。 每一个UIAbility实例,都对应于一个最近任务列表中的任务。 **一个应用可以有一个UIAbility,也可以有多个UIAbility**,创建的Empty Ability模板工程,初始会生成一个UIAbility文件。 如下图所示。例如浏览器应用可以通过一个UIAbility结合多页面的形式让用户进行的搜索和浏览内容;而聊天应用增加一个“外卖功能”的场景,则可以将聊天应用中“外卖功能”的内容独立为一个UIAbility,当用户打开聊天应用的“外卖功能”,查看外卖订单详情,此时有新的聊天消息,即可以通过最近任务列表切换回到聊天窗口继续进行聊天对话。 一个UIAbility可以对应于多个页面,建议将一个独立的功能模块放到一个UIAbility中,以多页面的形式呈现。例如新闻应用在浏览内容的时候,可以进行多页面的跳转使用。 **图1**单UIAbility应用和多UIAbility应用 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114011.74239043062763697270334112578297:50001231000000:2800:AD40C7AD7AC5BCF7929E1032AC309D9EA2CE0388FAFD02F3520EB4303FC7B608.png?needInitFileName=true?needInitFileName=true "点击放大") ## **3.2 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页面,用于实现页面间的跳转和数据传递。 为了实现页面的跳转和数据传递,需要新建一个页面。在原有Index页面的基础上,新建一个页面,例如命名为Second.ets。 页面间的导航可以通过页面路由router模块来实现。页面路由模块根据页面url找到目标页面,从而实现跳转。通过页面路由模块,可以使用不同的url访问不同的页面,包括跳转到UIAbility内的指定页面、用UIAbility内的某个页面替换当前页面、返回上一页面或指定的页面等。具体使用方法请参见[ohos.router (页面路由)](https://developer.harmonyos.com/cn/docs/documentation/doc-references-V3/js-apis-router-0000001478061893-V3?catalogVersion=V3)。 ## **3.2.1 页面跳转和参数接收** 在使用页面路由之前,需要先导入router模块,如下代码所示。 ~~~ import router from '@ohos.router'; ~~~ 页面跳转的几种方式,根据需要选择一种方式跳转即可。 * 方式一:API9及以上,router.pushUrl()方法新增了mode参数,可以将mode参数配置为router.RouterMode.Single单实例模式和router.RouterMode.Standard多实例模式。 在单实例模式下:如果目标页面的url在页面栈中已经存在同url页面,离栈顶最近同url页面会被移动到栈顶,移动后的页面为新建页,原来的页面仍然存在栈中,页面栈的元素数量不变;如果目标页面的url在页面栈中不存在同url页面,按多实例模式跳转,页面栈的元素数量会加1。 DOC.NOTE 当页面栈的元素数量较大或者超过32时,可以通过调用router.clear()方法清除页面栈中的所有历史页面,仅保留当前页面作为栈顶页面。 ~~~ router.pushUrl({ url: 'pages/Second', params: { src: 'Index页面传来的数据', }}, router.RouterMode.Single) ~~~ * 方式二:API9及以上,router.replaceUrl()方法新增了mode参数,可以将mode参数配置为router.RouterMode.Single单实例模式和router.RouterMode.Standard多实例模式。 在单实例模式下:如果目标页面的url在页面栈中已经存在同url页面,离栈顶最近同url页面会被移动到栈顶,替换当前页面,并销毁被替换的当前页面,移动后的页面为新建页,页面栈的元素数量会减1;如果目标页面的url在页面栈中不存在同url页面,按多实例模式跳转,页面栈的元素数量不变。 ~~~ router.replaceUrl({ url: 'pages/Second', params: { src: 'Index页面传来的数据', }}, router.RouterMode.Single) ~~~ 已经实现了页面的跳转,接下来,在Second页面中如何进行自定义参数的接收呢? 通过调用router.getParams()方法获取Index页面传递过来的自定义参数。 ~~~ import router from '@ohos.router'; @Entry@Componentstruct Second { @State src: string = (router.getParams() as Record<string, string>)['src']; // 页面刷新展示 // ...} ~~~ 效果示意如下图所示。在Index页面中,点击“Next”后,即可从Index页面跳转到Second页面,并在Second页面中接收参数和进行页面刷新展示。 **图2**Index页面跳转到Second页面 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114011.91665771657297428088312711708057:50001231000000:2800:A616F5D416A99E30E4A1E9EEC0CBEC0C19CBD384C9A39A05D3B1E7A7788F6DEA.png?needInitFileName=true?needInitFileName=true "点击放大") ## **3.2.2 页面返回和参数接收** 经常还会遇到一个场景,在Second页面中,完成了一些功能操作之后,希望能返回到Index页面,那我们要如何实现呢? 在Second页面中,可以通过调用router.back()方法实现返回到上一个页面,或者在调用router.back()方法时增加可选的options参数(增加url参数)返回到指定页面。 DOC.NOTE * 调用router.back()返回的目标页面需要在页面栈中存在才能正常跳转。 * 例如调用router.pushUrl()方法跳转到Second页面,在Second页面可以通过调用router.back()方法返回到上一个页面。 * 例如调用router.clear()方法清空了页面栈中所有历史页面,仅保留当前页面,此时则无法通过调用router.back()方法返回到上一个页面。 * 返回上一个页面。 ~~~ router.back(); ~~~ * 返回到指定页面。 ~~~ router.back({ url: 'pages/Index' }); ~~~ 效果示意如下图所示。在Second页面中,点击“Back”后,即可从Second页面返回到Index页面。 **图3**Second页面返回到Index页面 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114011.14970941967153485432157339428475:50001231000000:2800:1984CB83B4E86684AB9A0CA2ADDB03A5D033142BFD47ADE34E895D4F41647B90.png?needInitFileName=true?needInitFileName=true "点击放大") 页面返回可以根据业务需要增加一个询问对话框。 即在调用router.back()方法之前,可以先调用router.enableBackPageAlert()方法开启页面返回询问对话框功能。 DOC.NOTE * router.enableBackPageAlert()方法开启页面返回询问对话框功能,只针对当前页面生效。例如在调用router.pushUrl()或者router.replaceUrl()方法,跳转后的页面均为新建页面,因此在页面返回之前均需要先调用router.enableBackPageAlert()方法之后,页面返回询问对话框功能才会生效。 * 如需关闭页面返回询问对话框功能,可以通过调用router.disableAlertBeforeBackPage()方法关闭该功能即可。 ~~~ router.enableBackPageAlert({ message: 'Message Info'}); router.back(); ~~~ 在Second页面中,调用router.back()方法返回上一个页面或者返回指定页面时,根据需要继续增加自定义参数,例如在返回时增加一个自定义参数src。 ~~~ router.back({ url: 'pages/Index', params: { src: 'Second页面传来的数据', }}) ~~~ 从Second页面返回到Index页面。在Index页面通过调用router.getParams()方法,获取Second页面传递过来的自定义参数。 DOC.NOTE 调用router.back()方法,不会新建页面,返回的是原来的页面,在原来页面中@State声明的变量不会重复声明,以及也不会触发页面的aboutToAppear()生命周期回调,因此无法直接在变量声明以及页面的aboutToAppear()生命周期回调中接收和解析router.back()传递过来的自定义参数。 可以放在业务需要的位置进行参数解析。示例代码在Index页面中的onPageShow()生命周期回调中进行参数的解析。 ~~~ import router from '@ohos.router';class routerParams { src:string constructor(str:string) { this.src = str }}@Entry@Componentstruct Index { @State src: string = ''; onPageShow() { this.src = (router.getParams() as routerParams).src } // 页面刷新展示 // ...} ~~~ 效果示意图如下图所示。在Second页面中,点击“Back”后,即可从Second页面返回到Index页面,并在Index页面中接收参数和进行页面刷新展示。 **图4**Second页面带参数返回Index页面 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114011.12623918692265902034557168065752:50001231000000:2800:D483457916CAA37618C00EAE6094FF948C45608A1D8A1A8F6B2121A51165DC23.png?needInitFileName=true?needInitFileName=true "点击放大") ## **3.3 UIAbility的生命周期** 当用户浏览、切换和返回到对应应用的时候,应用中的UIAbility实例会在其生命周期的不同状态之间转换。 UIAbility类提供了很多回调,通过这些回调可以知晓当前UIAbility的某个状态已经发生改变:例如UIAbility的创建和销毁,或者UIAbility发生了前后台的状态切换。 例如从桌面点击图库应用图标,到启动图库应用,应用的状态经过了从创建到前台展示的状态变化。如下图所示。 **图5**从桌面点击图库应用图标启动应用 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114011.24788759508988041583401965384025:50001231000000:2800:3C65B520FA6FADF05090E8E5B429BB4C3C26749CDD17D0DA6586D731FA2B9651.png?needInitFileName=true?needInitFileName=true "点击放大") 回到桌面,从最近任务列表,切换回到图库应用,应用的状态经过了从后台到前台展示的状态变化。如下图所示。 **图6**从最近任务列表切换回到图库应用 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114011.25455291479663923862214520328938:50001231000000:2800:D4E8F6454E5DA104334F70E8B3393056647F8CF308E351EBE6B3F7719F975587.png?needInitFileName=true?needInitFileName=true "点击放大") 在UIAbility的使用过程中,会有多种生命周期状态。掌握UIAbility的生命周期,对于应用的开发非常重要。 为了实现多设备形态上的裁剪和多窗口的可扩展性,系统对组件管理和窗口管理进行了解耦。UIAbility的生命周期包括Create、Foreground、Background、Destroy四个状态,WindowStageCreate和WindowStageDestroy为窗口管理器(WindowStage)在UIAbility中管理UI界面功能的两个生命周期回调,从而实现UIAbility与窗口之间的弱耦合。如下图所示。 **图7**UIAbility生命周期状态 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114011.85030804480791293473872381051767:50001231000000:2800:B4ADE6E5CD82D6A8D3D179B6610C6DD1ABB5549A115DAD982BCF7B672788A4FB.png?needInitFileName=true?needInitFileName=true) * Create状态,在UIAbility实例创建时触发,系统会调用onCreate回调。可以在onCreate回调中进行相关初始化操作。 ~~~ 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实例创建完成之后,在进入Foreground之前,系统会创建一个WindowStage。每一个UIAbility实例都对应持有一个WindowStage实例。 WindowStage为本地窗口管理器,用于管理窗口相关的内容,例如与界面相关的获焦/失焦、可见/不可见。 可以在onWindowStageCreate回调中,设置UI页面加载、设置WindowStage的事件订阅。 在onWindowStageCreate(windowStage)中通过loadContent接口设置应用要加载的页面,Window接口的使用详见[窗口开发指导](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/application-window-stage-0000001427584712-V3?catalogVersion=V3)。 ~~~ 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回调中,会触发获焦的事件回调,可以进行设置消息应用的背景颜色、高亮等操作。 * Foreground和Background状态,分别在UIAbility切换至前台或者切换至后台时触发。 分别对应于onForeground回调和onBackground回调。 onForeground回调,在UIAbility的UI页面可见之前,即UIAbility切换至前台时触发。可以在onForeground回调中申请系统需要的资源,或者重新申请在onBackground中释放的资源。 onBackground回调,在UIAbility的UI页面完全不可见之后,即UIAbility切换至后台时候触发。可以在onBackground回调中释放UI页面不可见时无用的资源,或者在此回调中执行较为耗时的操作,例如状态保存等。 ~~~ 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回调中停止定位功能,以节省系统的资源消耗。 * 前面我们了解了UIAbility实例创建时的onWindowStageCreate回调的相关作用。 对应于onWindowStageCreate回调。在UIAbility实例销毁之前,则会先进入onWindowStageDestroy回调,我们可以在该回调中释放UI页面资源。 ~~~ import UIAbility from '@ohos.app.ability.UIAbility';import window from '@ohos.window'; export default class EntryAbility extends UIAbility { // ... onWindowStageDestroy() { // 释放UI页面资源 // ... }} ~~~ 例如在onWindowStageCreate中设置的获焦/失焦等WindowStage订阅事件。 * Destroy状态,在UIAbility销毁时触发。可以在onDestroy回调中进行系统资源的释放、数据的保存等操作。 ~~~ import UIAbility from '@ohos.app.ability.UIAbility';import window from '@ohos.window'; export default class EntryAbility extends UIAbility { // ... onDestroy() { // 系统资源的释放、数据的保存等 // ... }} ~~~ 例如用户使用应用的程序退出功能,会调用UIAbilityContext的terminalSelf()方法,从而完成UIAbility销毁。或者用户使用最近任务列表关闭该UIAbility实例时,也会完成UIAbility的销毁。 ## **3.4 UIAbility的启动模式** 对于浏览器或者新闻等应用,用户在打开该应用,并浏览访问相关内容后,回到桌面,再次打开该应用,显示的仍然是用户当前访问的界面。 对于应用的分屏操作,用户希望使用两个不同应用(例如备忘录应用和图库应用)之间进行分屏,也希望能使用同一个应用(例如备忘录应用自身)进行分屏。 对于文档应用,用户从文档应用中打开一个文档内容,回到文档应用,继续打开同一个文档,希望打开的还是同一个文档内容。 基于以上场景的考虑,UIAbility当前支持singleton(单实例模式)、multiton(多实例模式)和specified(指定实例模式)3种启动模式。 对启动模式的详细说明如下: * singleton(单实例模式) singleton启动模式为单实例模式,也是默认情况下的启动模式。 每次调用startAbility()方法时,如果应用进程中该类型的UIAbility实例已经存在,则复用系统中的UIAbility实例。系统中只存在唯一一个该UIAbility实例,即在最近任务列表中只存在一个该类型的UIAbility实例。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114011.07840675822392264038881193804928:50001231000000:2800:B423B1AC64CC5CA43BC33B9D9969FB7EC82B4480063BCB7FA95D0BAA631B3891.gif?needInitFileName=true?needInitFileName=true) DOC.NOTE 应用的UIAbility实例已创建,该UIAbility配置为单实例模式,再次调用startAbility()方法启动该UIAbility实例。由于启动的还是原来的UIAbility实例,并未重新创建一个新的UIAbility实例,此时只会进入该UIAbility的onNewWant()回调,不会进入其onCreate()和onWindowStageCreate()生命周期回调。 singleton启动模式的开发使用,在module.json5文件中的“launchType”字段配置为“singleton”即可。 ~~~ { "module": { // ... "abilities": [ { "launchType": "singleton", // ... } ] }} ~~~ * multiton(多实例模式) multiton启动模式为多实例模式,每次调用startAbility()方法时,都会在应用进程中创建一个新的该类型UIAbility实例。即在最近任务列表中可以看到有多个该类型的UIAbility实例。这种情况下可以将UIAbility配置为multiton(多实例模式)。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114012.86234775954609048451037616082182:50001231000000:2800:5362370291ECB54CBAEA427175180C22700ED0F5D2B6BB488C25DD21C11D93B4.gif?needInitFileName=true?needInitFileName=true) multiton启动模式的开发使用,在module.json5配置文件中的launchType字段配置为multiton即可。 ~~~ { "module": { // ... "abilities": [ { "launchType": "multiton", // ... } ] }} ~~~ * specified(指定实例模式) specified启动模式为指定实例模式,针对一些特殊场景使用(例如文档应用中每次新建文档希望都能新建一个文档实例,重复打开一个已保存的文档希望打开的都是同一个文档实例)。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114012.88674820101476086219905247075726:50001231000000:2800:D471454BB1F0F17541D84A1B9C678FAA398C72DE5106DDD072027D9A40FE9D1A.gif?needInitFileName=true?needInitFileName=true) 例如有两个UIAbility:EntryAbility和SpecifiedAbility,SpecifiedAbility配置为指定实例模式启动,需要从EntryAbility的页面中启动SpecifiedAbility。 1. 在SpecifiedAbility中,将module.json5配置文件的launchType字段配置为specified。 ~~~ { "module": { // ... "abilities": [ { "launchType": "specified", // ... } ] }} ~~~ 2. 在创建UIAbility实例之前,开发者可以为该实例指定一个唯一的字符串Key,这样在调用startAbility()方法时,应用就可以根据指定的Key来识别响应请求的UIAbility实例。在EntryAbility中,调用startAbility()方法时,可以在want参数中增加一个自定义参数,例如instanceKey,以此来区分不同的UIAbility实例。 ~~~ // 在启动指定实例模式的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的UIAbilityContextlet 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实例并启动。 ~~~ 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 ''; }} ~~~ DOC.NOTE 1. 当应用的UIAbility实例已经被创建,并且配置为指定实例模式时,如果再次调用startAbility()方法启动该UIAbility实例,且AbilityStage的onAcceptWant()回调匹配到一个已创建的UIAbility实例,则系统会启动原来的UIAbility实例,并且不会重新创建一个新的UIAbility实例。此时,该UIAbility实例的onNewWant()回调会被触发,而不会触发onCreate()和onWindowStageCreate()生命周期回调。 2. DevEco Studio默认工程中未自动生成AbilityStage,AbilityStage文件的创建请参见AbilityStage组件容器。 例如在文档应用中,可以为不同的文档实例内容绑定不同的Key值。每次新建文档时,可以传入一个新的Key值(例如可以将文件的路径作为一个Key标识),此时AbilityStage中启动UIAbility时都会创建一个新的UIAbility实例;当新建的文档保存之后,回到桌面,或者新打开一个已保存的文档,回到桌面,此时再次打开该已保存的文档,此时AbilityStage中再次启动该UIAbility时,打开的仍然是之前原来已保存的文档界面。 以如下步骤所示进行举例说明。 1. 打开文件A,对应启动一个新的UIAbility实例,例如启动UIAbility实例1。 2. 在最近任务列表中关闭文件A的任务进程,此时UIAbility实例1被销毁,回到桌面,再次打开文件A,此时对应启动一个新的UIAbility实例,例如启动UIAbility实例2。 3. 回到桌面,打开文件B,此时对应启动一个新的UIAbility实例,例如启动UIAbility实例3。 4. 回到桌面,再次打开文件A,此时仍然启动之前的UIAbility实例2,因为系统会自动匹配UIAbility实例的Key值,如果存在与之匹配的Key,则会启动与之绑定的UIAbility实例。在此例中,之前启动的UIAbility实例2与文件A绑定的Key是相同的,因此系统会拉回UIAbility实例2并让其获焦,而不会创建新的实例。 ## **四、HarmonyOS第一课:从简单的页面开始** ## **闯关习题** * 在Column容器中的子组件默认是按照从上到下的垂直方向布局的,其主轴的方向是垂直方向,在Row容器中的组件默认是按照从左到右的水平方向布局的,其主轴的方向是水平方向。 正确 * List容器可以沿水平方向排列,也可以沿垂直方向排列。 正确 * 当Tabs组件的参数barPosition为BarPosition.End时,页签位于页面底部。 错误 * Resource是资源引用类型,用于设置组件属性的值,可以定义组件的颜色、文本大小、组件大小等属性。 正确 * 使用TextInput完成一个密码输入框,推荐设置type属性为下面哪个值?InputType.Password * 使用Image加载网络图片,需要以下那种权限?ohos.permission.INTERNET * 下面哪个组件层次结构是错误的?Grid>Row>GridItem * Row容器的主轴是水平方向,交叉轴是垂直方向,其参数类型为VerticalAlign (垂直对齐),VerticalAlign 定义了以下几种类型?Top、Bottom、Center * 下面哪些组件是容器组件?Column、Row * 关于Tabs组件页签的位置设置,下面描述正确的是?ABCD * A. 当barPosition为Start(默认值),vertical属性为false时(默认值),页签位于容器顶部。 * B. 当barPosition为Start(默认值) ,vertical属性为true时,页签位于容器左侧。 * C. 当barPosition为End ,vertical属性为false(默认值)时,页签位于容器底部。 * D. 当barPosition为End ,vertical属性为true时,页签位于容器右侧。 ## **4.1 常用基础组件** ## **4.1.1 组件介绍** 组件(Component)是界面搭建与显示的最小单位,HarmonyOS ArkUI声明式开发范式为开发者提供了丰富多样的UI组件,我们可以使用这些组件轻松的编写出更加丰富、漂亮的界面。 组件根据功能可以分为以下五大类:基础组件、容器组件、媒体组件、绘制组件、画布组件。其中基础组件是视图层的基本组成单元,包括Text、Image、TextInput、Button、LoadingProgress等,例如下面这个常用的登录界面就是由这些基础组件组合而成。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102114256.28802753566728898498304998943977:50001231000000:2800:FE20D3B6F279BC1450B3B4D172874836DEA69202B2A75554D752FA3EB9D671BC.png) 下面我们将分别介绍这些常用基础组件的使用。 ## **4.1.2 Text** Text组件用于在界面上展示一段文本信息,可以包含子组件Span。 `文本样式` 针对包含文本元素的组件,例如Text、Span、Button、TextInput等,可使用fontColor、fontSize、fontStyle、 fontWeight、fontFamily这些文本样式,分别设置文本的颜色、大小、样式、粗细以及字体,文本样式的属性如下表: |名称|参数类型|描述| | --- | --- | --- | |fontColor|ResourceColor|设置文本颜色。| |fontSize|Length | Resource设置文本尺寸,Length为number类型时,使用fp单位。| fontStyle|FontStyle设置文本的字体样式。默认值:FontStyle.Normal。 |fontWeight|number 、FontWeight 、string|设置文本的字体粗细,number类型取值[100, 900],取值间隔为100,默认为400,取值越大,字体越粗。string类型仅支持number类型取值的字符串形式,例如“400”,以及“bold”、“bolder”、“lighter”、“regular”、“medium”,分别对应FontWeight中相应的枚举值。默认值:FontWeight.Normal。 |fontFamily|string | Resource设置文本的字体列表。使用多个字体,使用“,”进行分割,优先级按顺序生效。例如:“Arial,sans-serif”。| 下面示例代码中包含两个Text组件,第一个使用的是默认样式,第二个给文本设置了一些文本样式。 ~~~ @Entry @Component struct TextDemo { build() { Row() { Column() { Text('HarmonyOS') Text('HarmonyOS') .fontColor(Color.Blue) .fontSize(20) .fontStyle(FontStyle.Italic) .fontWeight(FontWeight.Bold) .fontFamily('Arial') } .width('100%') } .backgroundColor(0xF1F3F5) .height('100%') } } ~~~ 效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102114323.57981255142134670738558204839276:50001231000000:2800:F3EE4576F8914D849A63F9C5ECBB4975EE604C6F1946761DEA0A651962DB4A54.png) 除了通用属性和文本样式设置,下面列举了一些Text组件的常用属性的使用。 `设置文本对齐方式` 使用textAlign属性可以设置文本的对齐方式,示例代码如下: ~~~ Text('HarmonyOS') .width(200) .textAlign(TextAlign.Start) .backgroundColor(0xE6F2FD) ~~~ textAlign参数类型为TextAlign,定义了以下几种类型: * Start(默认值):水平对齐首部。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102114348.49288669266616261684997326269610:50001231000000:2800:C83DAC3856051E7F08D9380E4E9B9C549791FC558BFAD923E1C2940103109326.png) * Center:水平居中对齐。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102114354.42991757464168023713723675951861:50001231000000:2800:752E6D300496CE068E18468EAAE7634E86193E63A4E550D0A3DC5CC3A19E7467.png) * End:水平对齐尾部。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102114402.58501849207326897212851960388072:50001231000000:2800:313C09B1F0D805695F59F5495D121C5018FCAE91DC4DC613FB8E40F91F25089D.png) `设置文本超长显示` 当文本内容较多超出了Text组件范围的时候,您可以使用textOverflow设置文本截取方式,需配合maxLines使用,单独设置不生效,maxLines用于设置文本显示最大行数。下面的示例代码将textOverflow设置为Ellipsis ,它将显示不下的文本用 “...” 表示: ~~~ 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) ~~~ 效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102114644.68621516400405905798472500772263:50001231000000:2800:A59BEB8CFE497FC541786416270A6AFBE858A57D1260B775BF6FF3B2E907264B.png) `设置文本装饰线` 使用decoration设置文本装饰线样式及其颜色,大家在浏览网页的时候经常可以看到装饰线,例如带有下划线超链接文本。decoration包含type和color两个参数,其中type用于设置装饰线样式,参数类型为TextDecorationType,color为可选参数。 下面的示例代码给文本设置了下划线,下划线颜色为黑色: ~~~ Text('HarmonyOS') .fontSize(20) .decoration({ type: TextDecorationType.Underline, color: Color.Black }) .backgroundColor(0xE6F2FD) ~~~ 效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102114652.69929054773100409790044510283833:50001231000000:2800:A94A866A6D98B51775AE12C3032607C04C117EAA8718BF4250850536BE1EF13E.png) TextDecorationTyp包含以下几种类型: * None:不使用文本装饰线。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102114659.61640987050459682152517426675687:50001231000000:2800:A8FD2B5031982209FE5EAD7FB83154041709083A37592BE677A3CFBF8E787579.png) * Overline:文字上划线修饰。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102114706.69305764948918217718838177409802:50001231000000:2800:0B6C095B820036BC7F50A6D475A92E0D00A7E74D2BDDB3D4351CAE0EFCA42141.png) * LineThrough:穿过文本的修饰线。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102114713.98751594289299554991883153173320:50001231000000:2800:B9264CD5F1ED6C83B00DF3FC8E801AEF11C480FA03C25595749274718306613B.png) * Underline:文字下划线修饰。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102114720.48060694093389960570703646806593:50001231000000:2800:0536F799D1C27DCC66B5E9F085C366E1EA8F4468B648B27A3CCE637EFFF70934.png) ## **4.1.3 Image** Image组件用来渲染展示图片,它可以让界面变得更加丰富多彩。只需要给Image组件设置图片地址、宽和高,图片就能加载出来,示例如下: ~~~ Image($r("app.media.icon")) .width(100) .height(100) ~~~ 效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102114731.39553189055211199745202206018464:50001231000000:2800:E0097635C3FE290FC9F65597FC978BCA530F5095D149BFB0A350FF57AEC2FA1A.png) `设置缩放类型` 为了使图片在页面中有更好的显示效果,有时候需要对图片进行缩放处理。您可以使用objectFit属性设置图片的缩放类型,objectFit的参数类型为ImageFit。 现有原始图片如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102114739.77512714800298501080868850482759:50001231000000:2800:8D788340321FFC1F46F5A06C88E506AD5E769F26D95717A5881C3FC1C09BBF76.png) 将图片加载到Image组件,设置宽高各100,设置objectFit为Cover(默认值),设置图片背景色为灰色0xCCCCCC。示例代码如下: ~~~ Image($r("app.media.image2")) .objectFit(ImageFit.Cover) .backgroundColor(0xCCCCCC) .width(100) .height(100) ~~~ 效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102114748.77856821710199152384867059247638:50001231000000:2800:397491140D28842CCE2BC0E60F5999E608782F715E5C294D5172CA7DF129558D.png) ImageFit包含以下几种类型: * Contain:保持宽高比进行缩小或者放大,使得图片完全显示在显示边界内。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102114756.85370236529918272452949887734853:50001231000000:2800:F979D2A17F79AB04523A87103269E8E3BB4C63A9BA65E6B1BFB298CED56610AA.png) * Cover(默认值):保持宽高比进行缩小或者放大,使得图片两边都大于或等于显示边界。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102114803.14650992439248534530091313002089:50001231000000:2800:E532173B3B6DD9E92259F72675697BCEAE09FE72485AEA9EFD5FE9794FF14521.png) * Auto:自适应显示。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102114822.02052799258986122197086618380763:50001231000000:2800:190ECE96A361A13874FAAEA025A05042FC3D8E342712FC156BA48E7CFEFD40D1.png) * Fill:不保持宽高比进行放大缩小,使得图片充满显示边界。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102114940.81869569958898798079916961462275:50001231000000:2800:5B61B3C66E719CA10CA11E6E7AE81BDC98D1A3F472A07ADE97C446AA04DAAE8D.png) * ScaleDown:保持宽高比显示,图片缩小或者保持不变。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102114951.52445007947325457633854339454055:50001231000000:2800:FA807656E11E50469F5271B238470C6851F0D4ED5498914110CA07EFE52A80DE.png) * None:保持原有尺寸显示。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102115000.02884099972463949183812198219733:50001231000000:2800:076F73E497FD71E0338C04471F3B5770E5CCB46D3EE80A6A7565E6C503550E5D.png) `加载网络图片` 比如浏览新闻的时候,图片一般从网络加载而来,Image组件支持加载网络图片,将图片地址换成网络图片地址进行加载。 ~~~ Image('https://www.example.com/xxx.png') ~~~ 为了成功加载网络图片,您需要在module.json5文件中申明网络访问权限。 ~~~ { "module" : { "requestPermissions":[ { "name": "ohos.permission.INTERNET" } ] } } ~~~ ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102115035.57901904356964270856139117822179:50001231000000:2800:38B8FF4B793C0D18A93C462CCD38A2644D9C09D65674F94484ECDE7556979346.png) 应用访问网络需要申请ohos.permission.INTERNET权限,因为HarmonyOS提供了一种访问控制机制即应用权限,用来保证这些数据或功能不会被不当或恶意使用。关于应用权限的的详细信息开发者可以参考:[访问控制](https://developer.harmonyos.com/cn/docs/documentation/doc-guides/accesstoken-overview-0000001333641125)。 ## **4.1.4 TextInput** TextInput组件用于输入单行文本,响应输入事件。TextInput的使用也非常广泛,例如应用登录账号密码、发送消息等。和Text组件一样,TextInput组件也支持文本样式设置,下面的示例代码实现了一个简单的输入框: ~~~ TextInput() .fontColor(Color.Blue) .fontSize(20) .fontStyle(FontStyle.Italic) .fontWeight(FontWeight.Bold) .fontFamily('Arial') ~~~ 效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102115050.52007374582615623658877407943036:50001231000000:2800:0E13911716C4593FFFE505D017280722EF6A8B33E0CEC9DFB56264D63094E2E3.png) `设置输入提示文本` 当我们平时使用输入框的时候,往往会有一些提示文字。例如登录账号的时候会有“请输入账号”这样的文本提示,当用户输入内容之后,提示文本就会消失,这种提示功能使用placeholder属性就可以轻松的实现。您还可以使用placeholderColor和placeholderFont分别设置提示文本的颜色和样式,示例代码如下: ~~~ TextInput({ placeholder: '请输入帐号' }) .placeholderColor(0x999999) .placeholderFont({ size: 20, weight: FontWeight.Medium, family: 'cursive', style: FontStyle.Italic }) ~~~ 效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102115100.71218216501593991658353961758535:50001231000000:2800:AA026BFE5F8E049506B4D2EEF7BF7DABE7284E5D4226C75AA32B57E124399DAA.gif) `设置输入类型` 可以使用type属性来设置输入框类型。例如密码输入框,一般输入密码的时候,为了用户密码安全,内容会显示为“......”,针对这种场景,将type属性设置为InputType.Password就可以实现。示例代码如下: ~~~ TextInput({ placeholder: '请输入密码' }) .type(InputType.Password) ~~~ 效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102115110.98778894209644360311002826080034:50001231000000:2800:8BE918EFF85332BB9140B61DFA66D7145E578618FA793045987D49CF092433EC.gif) type的参数类型为InputType,包含以下几种输入类型: * Normal:基本输入模式。支持输入数字、字母、下划线、空格、特殊字符。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102115236.92283096302002995875166741811262:50001231000000:2800:C8BC6EA9E6A997BEA2866CE6E0C2D310333C33A43E0076B191E91D366CF1ADA5.png) * Password:密码输入模式。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102115245.61142009918082527144769110156060:50001231000000:2800:7C742106E203659C40CB97B3A8EE5AF7AA76B74C81DA0AD15BFD3110C19E28C1.png) * Email:e-mail地址输入模式。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102115252.23834448972955999986156295718579:50001231000000:2800:FD5F0326431F311238418A1EF5C1C182CB7A017C49F06946CB376C1E9EA788A4.png) * Number:纯数字输入模式。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102115300.02746846170472180689146322192383:50001231000000:2800:EE622B2BECDD549786E554861A899563031A770C888553F3F274EB5DBF6C5D04.png) `设置光标位置` 可以使用TextInputController动态设置光位置,下面的示例代码使用TextInputController的caretPosition方法,将光标移动到了第二个字符后。 ~~~ @Entry @Component struct TextInputDemo { controller: TextInputController = new TextInputController() build() { Column() { TextInput({ controller: this.controller }) Button('设置光标位置') .onClick(() => { this.controller.caretPosition(2) }) } .height('100%') .backgroundColor(0xE6F2FD) } } ~~~ 效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102115317.35282590037382074976290780085823:50001231000000:2800:CC4870F4F8F97CC04C6F11016FD88D50323C29F810F9D24B1C43E38E82A2BDEB.gif) `获取输入文本` 我们可以给TextInput设置onChange事件,输入文本发生变化时触发回调,下面示例代码中的value为实时获取用户输入的文本信息。 ~~~ @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) } } ~~~ 效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102115338.69547694354764992883763360730107:50001231000000:2800:BA25C8408214B902ECBD215DABB7916AED4E7FB78B8F280799A83B607AA7C909.gif) ## **4.1.5 Button** Button组件主要用来响应点击操作,可以包含子组件。下面的示例代码实现了一个“登录按钮”: ~~~ Button('登录', { type: ButtonType.Capsule, stateEffect: true }) .width('90%') .height(40) .fontSize(16) .fontWeight(FontWeight.Medium) .backgroundColor('#007DFF') ~~~ 效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102115354.26733108824112602628872699815029:50001231000000:2800:3DB0F3BDDC39FA9DA9B6E2CBA0E80BE64853C6EB2527A6EE2B245EE38E99F134.png) `设置按钮样式` type用于定义按钮样式,示例代码中ButtonType.Capsule表示胶囊形按钮;stateEffect用于设置按钮按下时是否开启切换效果,当状态置为false时,点击效果关闭,默认值为true。 我们可以设置多种样式的Button,除了Capsule可以以设置Normal和Circle: * Capsule:胶囊型按钮(圆角默认为高度的一半)。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102115404.84177998820036585609688095430669:50001231000000:2800:24CD642B18947F8B32434F65CD7E2C91A4DFF48A1E4017520A161AEDA3ED30F0.png) * Circle:圆形按钮。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102115412.39174965009162190745462310963210:50001231000000:2800:60EFB66EA62ED61D60B28BCFB7C3F290964FDB1893DD428AF61531F600F69D3D.png) * Normal:普通按钮(默认不带圆角)。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102115421.19102972822842807234366477791218:50001231000000:2800:A1E76374955D7EDDC93058ED603C1232459C8CD2AFBF7B4EC96D650A6D80AF3F.png) `设置按钮点击事件` 可以给Button绑定onClick事件,每当用户点击Button的时候,就会回调执行onClick方法,调用里面的逻辑代码。 ~~~ Button('登录', { type: ButtonType.Capsule, stateEffect: true }) ... .onClick(() => { // 处理点击事件逻辑 }) ~~~ `包含子组件` Button组件可以包含子组件,让您可以开发出更丰富多样的Button,下面的示例代码中Button组件包含了一个Image组件: ~~~ Button({ type: ButtonType.Circle, stateEffect: true }) { Image($r('app.media.icon_delete')) .width(30) .height(30) } .width(55) .height(55) .backgroundColor(0x317aff) ~~~ 效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102115439.25988537294891479063435944038115:50001231000000:2800:7FB346C30BFCECCBDB87006E52813894BCCD0DCF5041B524F5B5C59F851D0DAA.png) ## **4.1.6 LoadingProgress** LoadingProgress组件用于显示加载进展,比如应用的登录界面,当我们点击登录的时候,显示的“正在登录”的进度条状态。LoadingProgress的使用非常简单,只需要设置颜色和宽高就可以了。 ~~~ LoadingProgress() .color(Color.Blue) .height(60) .width(60) ~~~ 效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102115448.33909926886752830021497148543403:50001231000000:2800:98D471A991979097448D44923AF569FB44492907377352F8752E055DDECD23C2.gif) ## **4.1.7 使用资源引用类型** Resource是资源引用类型,用于设置组件属性的值。推荐大家优先使用Resource类型,将资源文件(字符串、图片、音频等)统一存放于resources目录下,便于开发者统一维护。同时系统可以根据当前配置加载合适的资源,例如,开发者可以根据屏幕尺寸呈现不同的布局效果,或根据语言设置提供不同的字符串。 例如下面的这段代码,直接在代码中写入了字符串和数字这样的硬编码。 ~~~ Button('登录', { type: ButtonType.Capsule, stateEffect: true }) .width(300) .height(40) .fontSize(16) .fontWeight(FontWeight.Medium) .backgroundColor('#007DFF') ~~~ 我们可以将这些硬编码写到entry/src/main/resources下的资源文件中。 在string.json中定义Button显示的文本。 ~~~ { "string": [ { "name": "login_text", "value": "登录" } ] } ~~~ 在float.json中定义Button的宽高和字体大小。 ~~~ { "float": [ { "name": "button_width", "value": "300vp" }, { "name": "button_height", "value": "40vp" }, { "name": "login_fontSize", "value": "18fp" } ] } ~~~ 在color.json中定义Button的背景颜色。 ~~~ { "color": [ { "name": "button_color", "value": "#1890ff" } ] } ~~~ 然后在Button组件通过“$r('app.type.name')”的形式引用应用资源。app代表应用内resources目录中定义的资源;type代表资源类型(或资源的存放位置),可以取“color”、“float”、“string”、“plural”、“media”;name代表资源命名,由开发者定义资源时确定。 ~~~ 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')) ~~~ ## **4.1.8 参考资料** 常用基础的组件的更多使用方法可以参考: * [Text](https://developer.harmonyos.com/cn/docs/documentation/doc-references/ts-basic-components-text-0000001333720953) * [Image](https://developer.harmonyos.com/cn/docs/documentation/doc-references/ts-basic-components-image-0000001281001226) * [TextInput](https://developer.harmonyos.com/cn/docs/documentation/doc-references/ts-basic-components-textinput-0000001333321201) * [Button](https://developer.harmonyos.com/cn/docs/documentation/doc-references/ts-basic-components-button-0000001281480682) * [LoadingProgress](https://developer.harmonyos.com/cn/docs/documentation/doc-references/ts-basic-components-loadingprogress-0000001281361106) 引用资源类型的使用可以参考: * [资源访问](https://developer.harmonyos.com/cn/docs/documentation/doc-guides/resource-categories-and-access-0000001435940589) ## **4.2 Column&Row组件的使用** ## **4.2.1 概述** 一个丰富的页面需要很多组件组成,那么,我们如何才能让这些组件有条不紊地在页面上布局呢?这就需要借助容器组件来实现。 容器组件是一种比较特殊的组件,它可以包含其他的组件,而且按照一定的规律布局,帮助开发者生成精美的页面。容器组件除了放置基础组件外,也可以放置容器组件,通过多层布局的嵌套,可以布局出更丰富的页面。 ArkTS为我们提供了丰富的容器组件来布局页面,本文将以构建登录页面为例,介绍Column和Row组件的属性与使用。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102115843.56869299768090628795048614168558:50001231000000:2800:FF53A5245D2813CE1EB2CBCE12EC3C1B5311CE160DCB25180A25008353C9FE49.png) ## **4.2.2 组件介绍** `布局容器概念` 线性布局容器表示按照垂直方向或者水平方向排列子组件的容器,ArkTS提供了Column和Row容器来实现线性布局。 * Column表示沿垂直方向布局的容器。 * Row表示沿水平方向布局的容器。 `主轴和交叉轴概念` 在布局容器中,默认存在两根轴,分别是主轴和交叉轴,这两个轴始终是相互垂直的。不同的容器中主轴的方向不一样的。 * **主轴**:在Column容器中的子组件是按照从上到下的垂直方向布局的,其主轴的方向是垂直方向;在Row容器中的组件是按照从左到右的水平方向布局的,其主轴的方向是水平方向。 图2-1 Column容器&Row容器主轴 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102115909.22873051359993638219253132361587:50001231000000:2800:DD239797D663EF0C454A0344974110E18232305E2A48C465CB8104AB7AF3B76D.png) * **交叉轴**:与主轴垂直相交的轴线,如果主轴是垂直方向,则交叉轴就是水平方向;如果主轴是水平方向,则交叉轴是垂直方向。 图2-2 Column容器&Row容器交叉轴 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102115920.30331924582664597826254415316555:50001231000000:2800:3531A948E66A8AEFBC7ACD4C60C8412D31B655FF90C52DCA0F06B4E8C6B1DE16.png) `属性介绍` 了解布局容器的主轴和交叉轴,主要是为了让大家更好地理解子组件在主轴和交叉轴的排列方式。 接下来,我们将详细讲解Column和Row容器的两个属性justifyContent和alignItems。 | 属性名称| 描述 | | --- | --- | | justifyContent | 设置子组件在主轴方向上的对齐格式。 | | alignItems | 设置子组件在交叉轴方向上的对齐格式。| 1. 主轴方向的对齐(justifyContent) 子组件在主轴方向上的对齐使用justifyContent属性来设置,其参数类型是[FlexAlign](https://developer.harmonyos.com/cn/docs/documentation/doc-references/ts-appendix-enums-0000001281201130#ZH-CN_TOPIC_0000001281201130__flexalign)。FlexAlign定义了以下几种类型: * Start:元素在主轴方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102120034.55002488649666187375452743115722:50001231000000:2800:976A64244DB1E53DBC7A14E86E8F112B367438881EA17072BCD99E5055D7AFC7.png) * Center:元素在主轴方向中心对齐,第一个元素与行首的距离以及最后一个元素与行尾距离相同。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102120046.20470396574913051754264941328891:50001231000000:2800:4649A1983ADC5F536F5BFB50A8BD04A252B37F9AB8E77022FAFF8BAB5AAD0452.png) * End:元素在主轴方向尾部对齐,最后一个元素与行尾对齐,其他元素与后一个对齐。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102120054.44350106670787534871004252237249:50001231000000:2800:597FEE07999060038975E0A285F22F073C2136A3C0E4CA01233DFF0B42A7C4E7.png) * SpaceBetween:元素在主轴方向均匀分配弹性元素,相邻元素之间距离相同。 第一个元素与行首对齐,最后一个元素与行尾对齐。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102120104.61306639232139542053660772535451:50001231000000:2800:FACFD1B00005797C0D61BF2251FF367148A168AC095028E9D9E75BE7E1B3B265.png) * SpaceAround:元素在主轴方向均匀分配弹性元素,相邻元素之间距离相同。 第一个元素到行首的距离和最后一个元素到行尾的距离是相邻元素之间距离的一半。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102120116.52750004019093798708346178362676:50001231000000:2800:DA370AD7DEA8F643106B77791FA2F7615794BEB0131FB0592BC29E1CFB26CDE8.png) * SpaceEvenly:元素在主轴方向等间距布局,无论是相邻元素还是边界元素到容器的间距都一样。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102120152.44741357072809756514190316665580:50001231000000:2800:8E31F05211B1741DCA78E90C063CDEB3CA69288D8F7382A2FA3BC771E446FE9E.png) 2. 交叉轴方向的对齐(alignItems) 子组件在交叉轴方向上的对齐方式使用alignItems属性来设置。 Column容器的主轴是垂直方向,交叉轴是水平方向,其参数类型为HorizontalAlign(水平对齐),HorizontalAlign定义了以下几种类型: * Start:设置子组件在水平方向上按照起始端对齐。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102120211.31994376220784208696781894804465:50001231000000:2800:A8A75840C5F94CCB04EB7AC2DB6B149D2F5D5D1ED3235EFE280BC5626540BDE3.png) * Center(默认值):设置子组件在水平方向上居中对齐。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102120220.73077653691423267775452046266338:50001231000000:2800:83604C691A921D5058905270A365B0680FF1439AE1F633AD43B6E771FCFFCB13.png) * End:设置子组件在水平方向上按照末端对齐。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102120231.65240250845776209940244577183907:50001231000000:2800:94F40FCB4DE8E3960D9AF78074611ACB80ABD642D779C0EC4F8932A662CF5853.png) Row容器的主轴是水平方向,交叉轴是垂直方向,其参数类型为VerticalAlign(垂直对齐),VerticalAlign定义了以下几种类型: * Top:设置子组件在垂直方向上居顶部对齐。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102120244.63498934647274355019879968073116:50001231000000:2800:31FACC5011AC5DFE44996316747E28DFEBDCC20262ABAC9B6ADAFB8ACD78165D.png) * Center(默认值):设置子组件在竖直方向上居中对齐。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102120303.35457043976848012752874761084865:50001231000000:2800:08FB1D45111107EFDC181C535AD817C051FE8D668AB08B096554009E57B96E5D.png) * Bottom:设置子组件在竖直方向上居底部对齐。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102120311.16840084438584689920641238749604:50001231000000:2800:F0091640561F333A2BB462FCF6420DCB08670B2D1EFE4B0722BFCE88CA4C0859.png) `接口介绍` 接下来,我们介绍Column和Row容器的接口。 | 容器组件| 接口| | --- | --- | | Column| Column(value?:{space?: string | number}) | | Row | Row(value?:{space?: string | number})| Column和Row容器的接口都有一个可选参数space,表示子组件在主轴方向上的间距。 效果如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102120341.29998493080685429299217527032153:50001231000000:2800:F722E0E366A541021F8FF33486CD9177134A9F1C588090C7669DEE216D0E92FE.png) ## **4.2.3 组件使用** 我们来具体讲解如何高效的使用Column和Row容器组件来构建这个登录页面。 当我们从设计同学那拿到一个页面设计图时,我们需要对页面进行拆解,先确定页面的布局,再分析页面上的内容分别使用哪些组件来实现。 我们仔细分析这个登录页面。在静态布局中,组件整体是从上到下布局的,因此构建该页面可以使用Column来构建。在此基础上,我们可以看到有部分内容在水平方向上由几个基础组件构成,例如页面中间的短信验证码登录与忘记密码以及页面最下方的其他方式登录,那么构建这些内容的时候,可以在Column组件中嵌套Row组件,继而在Row组件中实现水平方向的布局。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102120349.33899634270625091234160890072156:50001231000000:2800:4959D75213902E59FC842C767ADF8643A9B8B50D2ECBF4E84A0DB82D4766B133.png) 根据上述页面拆解,在Column容器里,依次是Image、Text、TextInput、Button等基础组件,还有两组组件是使用Row容器组件来实现的,主要代码如下: ~~~ @Entry @Component export struct LoginPage { build() { Column() { Image($r('app.media.logo')) ... Text($r('app.string.login_page')) ... Text($r('app.string.login_more')) ... TextInput({ placeholder: $r('app.string.account') }) ... TextInput({ placeholder: $r('app.string.password') }) ... Row() { Text($r(…)) Text($r(…)) } Button($r('app.string.login'), { type: ButtonType.Capsule, stateEffect: true }) ... Row() { this.imageButton($r(…)) this.imageButton($r(…)) this.imageButton($r(…)) } ... } ... } } ~~~ 我们详细看一下使用Row容器的两组组件。 两个文本组件展示的内容是按水平方向布局的,使用两端对齐的方式。这里我们使用Row容器组件,并且需要配置主轴上(水平方向)的对齐格式justifyContent为FlexAlign.SpaceBetween(两端对齐)。 ~~~ Row() { Text($r(…)) Text($r(…)) } .justifyContent(FlexAlign.SpaceBetween) .width('100%') ~~~ 其他登录方式的三个按钮也是按水平方向布局的,同样使用Row容器组件。这里按钮的间距是一致的,我们可以通过配置可选参数space来设置按钮间距,使子组件间距一致。 ~~~ Row({ space: CommonConstants.LOGIN_METHODS_SPACE }) { this.imageButton($r(…)) this.imageButton($r(…)) this.imageButton($r(…)) } ~~~ 至此,你已经完成这个登录页面的简单布局实现了。你可以参考[《常用组件与布局》](https://developer.huawei.com/consumer/cn/codelabsPortal/carddetails/tutorials_ArkTSComponents)这个Codelab验证一下页面效果。 另外,你也可以通过[《常用布局容器对齐方式》](https://developer.huawei.com/consumer/cn/codelabsPortal/carddetails/tutorials_LayoutAlignETS)这个Codelab来深入学习Column、Row、Flex、Stack容器组件的对齐方式,掌握更多布局容器的使用方法。 ## **4.2.4 参考链接** * Column组件的相关API参考:[Column组件](https://developer.harmonyos.com/cn/docs/documentation/doc-references/ts-container-column-0000001333641085)。 * Row组件的相关API参考:[Row组件](https://developer.harmonyos.com/cn/docs/documentation/doc-references/ts-container-row-0000001281480714)。 ## **4.3 List组件和Grid组件的使用** ## **4.3.1 简介** 在我们常用的手机应用中,经常会见到一些数据列表,如设置页面、通讯录、商品列表等。下图中两个页面都包含列表,“首页”页面中包含两个网格布局,“商城”页面中包含一个商品列表。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114021.76298852609843510545142880169923:50001231000000:2800:2137B63F852C293D664B758EE1E1D4C3EE9D38B33213FD815E191D6425308960.png?needInitFileName=true?needInitFileName=true "点击放大") 上图中的列表中都包含一系列相同宽度的列表项,连续、多行呈现同类数据,例如图片和文本。常见的列表有线性列表(List列表)和网格布局(Grid列表): ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114021.24614355659251483508034402327799:50001231000000:2800:BD15FDDE47541155D6AD9C5BF53249AB1373264F661177558DE65AE348B57E8D.png?needInitFileName=true?needInitFileName=true "点击放大") 为了帮助开发者构建包含列表的应用,ArkUI提供了List组件和Grid组件,开发者使用List和Grid组件能够很轻松的完成一些列表页面。 ## **4.3.2 List组件的使用** `List组件简介` List是很常用的滚动类容器组件,一般和子组件ListItem一起使用,List列表中的每一个列表项对应一个ListItem组件。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114021.96205742196885500306668913604627:50001231000000:2800:5C79A273B85F365326E589CCC436F8D7513B041EEA45D01C2BBA18B47CB3F747.png?needInitFileName=true?needInitFileName=true "点击放大") `使用ForEeach渲染列表` 列表往往由多个列表项组成,所以我们需要在List组件中使用多个ListItem组件来构建列表,这就会导致代码的冗余。使用循环渲染(ForEach)遍历数组的方式构建列表,可以减少重复代码,示例代码如下: ~~~ @Entry@Componentstruct 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) }} ~~~ 效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114021.99873170533581670005602323955528:50001231000000:2800:6AC5121B60676A6DA174E1131282C66DCC9A5F0929A1ED40E622EB4707A69FC4.png?needInitFileName=true?needInitFileName=true "点击放大") `设置列表分割线` List组件子组件ListItem之间默认是没有分割线的,部分场景子组件ListItem间需要设置分割线,这时候您可以使用List组件的divider属性。divider属性包含四个参数: * strokeWidth: 分割线的线宽。 * color: 分割线的颜色。 * startMargin:分割线距离列表侧边起始端的距离。 * endMargin: 分割线距离列表侧边结束端的距离。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114021.70899408000972728202421604905907:50001231000000:2800:E1FF3F55C2015193E71725692CDA33590276D4079935932CF9307BBA153B23FD.png?needInitFileName=true?needInitFileName=true "点击放大") `List列表滚动事件监听` List组件提供了一系列事件方法用来监听列表的滚动,您可以根据需要,监听这些事件来做一些操作: * onScroll:列表滑动时触发,返回值scrollOffset为滑动偏移量,scrollState为当前滑动状态。 * onScrollIndex:列表滑动时触发,返回值分别为滑动起始位置索引值与滑动结束位置索引值。 * onReachStart:列表到达起始位置时触发。 * onReachEnd:列表到底末尾位置时触发。 * onScrollStop:列表滑动停止时触发。 使用示例代码如下: ~~~ 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排列方向` List组件里面的列表项默认是按垂直方向排列的,如果您想让列表沿水平方向排列,您可以将List组件的listDirection属性设置为Axis.Horizontal。 listDirection参数类型是[Axis](https://developer.harmonyos.com/cn/docs/documentation/doc-references-V3/ts-appendix-enums-0000001478061741-V3?catalogVersion=V3#ZH-CN_TOPIC_0000001478061741__axis),定义了以下两种类型: * Vertical(默认值):子组件ListItem在List容器组件中呈纵向排列。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114021.26794363778619638674061157585930:50001231000000:2800:291F9F7FFFEC929BE74D8CFDCEF41C8B78C19AC0412BB1A08BB8E091125985EE.png?needInitFileName=true?needInitFileName=true) * Horizontal:子组件ListItem在List容器组件中呈横向排列。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114022.50652940074991976805062285871306:50001231000000:2800:6824367FCCB4EED18E566F9D7B67C364B93767894D38E518A0B535607C73AC5D.png?needInitFileName=true?needInitFileName=true "点击放大") ## **4.3.3 Grid组件的使用** `Grid组件简介` Grid组件为网格容器,是一种网格列表,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局。Grid组件一般和子组件GridItem一起使用,Grid列表中的每一个条目对应一个GridItem组件。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114022.77512977599829805485453715488693:50001231000000:2800:CE05196127051B0013E7E0C4C548E240A220B6D159E6CC1882430C512A39091A.png?needInitFileName=true?needInitFileName=true) `使用ForEach渲染网格布局` 和List组件一样,Grid组件也可以使用ForEach来渲染多个列表项GridItem,我们通过下面的这段示例代码来介绍Grid组件的使用。 ~~~ @Entry@Componentstruct 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) }} ~~~ 示例代码中创建了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。示例代码效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114022.17665645714348674469081257729838:50001231000000:2800:879489F0B0BA8E7D0CD604D58C09FAD81A4E8A71660A50A01DEE3E8BE2C08577.png?needInitFileName=true?needInitFileName=true) 上面构建的网格布局使用了固定的行数和列数,所以构建出的网格是不可滚动的。然而有时候因为内容较多,我们通过滚动的方式来显示更多的内容,就需要一个可以滚动的网格布局。我们只需要设置rowsTemplate和columnsTemplate中的一个即可。 将示例代码中GridItem的高度设置为固定值,例如100;仅设置columnsTemplate属性,不设置rowsTemplate属性,就可以实现Grid列表的滚动: ~~~ 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来监听列表的滚动。 ## **4.3.4 列表性能优化** 开发者在使用长列表时,如果直接采用循环渲染方式,会一次性加载所有的列表元素,从而导致页面启动时间过长,影响用户体验,推荐通过以下方式来进行列表性能优化: [使用数据懒加载](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/ui-ts-performance-improvement-recommendation-0000001477981001-V3#ZH-CN_TOPIC_0000001477981001__%E6%8E%A8%E8%8D%90%E4%BD%BF%E7%94%A8%E6%95%B0%E6%8D%AE%E6%87%92%E5%8A%A0%E8%BD%BD) [设置list组件的宽高](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/ui-ts-performance-improvement-recommendation-0000001477981001-V3#ZH-CN_TOPIC_0000001477981001__%E8%AE%BE%E7%BD%AElist%E7%BB%84%E4%BB%B6%E7%9A%84%E5%AE%BD%E9%AB%98) ## **4.3.5 参考链接** 1. List组件的相关API参考:[List组件](https://developer.harmonyos.com/cn/docs/documentation/doc-references-V3/ts-container-list-0000001477981213-V3?catalogVersion=V3)。 2. Grid组件的相关API参考:[Grid组件](https://developer.harmonyos.com/cn/docs/documentation/doc-references-V3/ts-container-grid-0000001478341161-V3?catalogVersion=V3)。 3. 循环渲染(ForEach):[循环渲染](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/arkts-rendering-control-0000001427744548-V3?catalogVersion=V3#ZH-CN_TOPIC_0000001427744548__%E5%BE%AA%E7%8E%AF%E6%B8%B2%E6%9F%93)。 ## **4.4 Tabs组件的使用** ## **4.4.1 概述** 在我们常用的应用中,经常会有视图内容切换的场景,来展示更加丰富的内容。比如下面这个页面,点击底部的页签的选项,可以实现“首页”和“我的” 两个内容视图的切换。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114006.15810054681671435100170427657354:50001231000000:2800:E9BDCF9EAC3F9EBFCC7F002F5D2C732C85BAEB85269A48A40EF4E5DFF2A7CA58.png?needInitFileName=true?needInitFileName=true) ArkUI开发框架提供了一种页签容器组件Tabs,开发者通过Tabs组件可以很容易的实现内容视图的切换。页签容器Tabs的形式多种多样,不同的页面设计页签不一样,可以把页签设置在底部、顶部或者侧边。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114006.46447288653153728154552503552279:50001231000000:2800:1144177FA18D0F78CC134D77DA5068F7A78EB98D8FA083A9C3162E288D254510.png?needInitFileName=true?needInitFileName=true "点击放大") 本文将详细介绍Tabs组件的使用。 ## **4.4.2 Tabs组件的简单使用** Tabs组件仅可包含子组件TabContent,每一个页签对应一个内容视图即TabContent组件。下面的示例代码构建了一个简单的页签页面: ~~~ @Entry@Componentstruct 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%') }} ~~~ 效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114006.29388684682955911574804670483565:50001231000000:2800:3EFBA20D3549581C44141A3D234D80BC46D486E6CE793FBBC861ED176F07DF66.png?needInitFileName=true?needInitFileName=true) 上面示例代码中,Tabs组件中包含4个子组件TabContent,通过TabContent的tabBar属性设置TabBar的显示内容。使用通用属性width和height设置了Tabs组件的宽高,使用barWidth和barHeight设置了TabBar的宽度和高度。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114006.79264385194341570090819155451771:50001231000000:2800:A2CE99D2A7C909EC0C35C13B543FD391AA55A4004E975C4679AA45CE5BFC2CA9.png?needInitFileName=true?needInitFileName=true "点击放大") 说明 * TabContent组件不支持设置通用宽度属性,其宽度默认撑满Tabs父组件。 * TabContent组件不支持设置通用高度属性,其高度由Tabs父组件高度与TabBar组件高度决定。 ## **4.4.3 设置TabBar布局模式** 因为Tabs的布局模式默认是Fixed的,所以Tabs的页签是不可滑动的。当页签比较多的时候,可能会导致页签显示不全,将布局模式设置为Scrollable的话,可以实现页签的滚动。 Tabs的布局模式有Fixed(默认)和Scrollable两种: * BarMode.Fixed:所有TabBar平均分配barWidth宽度(纵向时平均分配barHeight高度),页签不可滚动,效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114006.00460165163455564647201027087937:50001231000000:2800:94F3210B44CB210AE136DA49EC478CA31CF08F979C5431AECEC3A39D7E001C64.png?needInitFileName=true?needInitFileName=true "点击放大") * BarMode.Scrollable:每一个TabBar均使用实际布局宽度,超过总长度(横向Tabs的barWidth,纵向Tabs的barHeight)后可滑动。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114006.14498940122308999777083020551597:50001231000000:2800:55787BB5B63B27802B5972AB686754E17AAF5F4C8E5D276C8B9D6C225B7DE855.png?needInitFileName=true?needInitFileName=true "点击放大") * 当页签比较多的时候,可以滑动页签,下面的示例代码将barMode设置为BarMode.Scrollable,实现了可滚动的页签: ~~~ @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%') } } } ~~~ ## **4.4.4 设置TabBar位置和排列方向** Tabs组件页签默认显示在顶部,某些场景下您可能希望Tabs页签出现在底部或者侧边,您可以使用Tabs组件接口中的参数barPosition设置页签位置。此外页签显示位置还与vertical属性相关联,vertical属性用于设置页签的排列方向,当vertical的属性值为false(默认值)时页签横向排列,为true时页签纵向排列。 barPosition的值可以设置为BarPosition.Start(默认值)和BarPosition.End: * BarPosition.Start vertical属性方法设置为false(默认值)时,页签位于容器顶部。 ~~~ Tabs({ barPosition: BarPosition.Start }) { ...}.vertical(false) .barWidth('100%') .barHeight(60) ~~~ 效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114006.62395530800278543062807409174448:50001231000000:2800:4203E893971E6BBECF8248462719E91115086765AA25E0BB89EACF09374FE6AC.png?needInitFileName=true?needInitFileName=true) vertical属性方法设置为true时,页签位于容器左侧。 ~~~ Tabs({ barPosition: BarPosition.Start }) { ...}.vertical(true) .barWidth(100) .barHeight(200) ~~~ 效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114006.83138558673583645639387597842606:50001231000000:2800:7E259ED98DEE865CFF39CEB5C8F65908876B532C3AE41E85B81E7F43DA5311E2.png?needInitFileName=true?needInitFileName=true) * BarPosition.End vertical属性方法设置为false时,页签位于容器底部。 ~~~ Tabs({ barPosition: BarPosition.End }) { ...}.vertical(false) .barWidth('100%') .barHeight(60) ~~~ 效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114006.05108002382405768723664985207344:50001231000000:2800:0348FDFA2E0DBF13049E7CBEF16A39A787B3F28BD92CD31DA433F7C3A19A8FA2.png?needInitFileName=true?needInitFileName=true) vertical属性方法设置为true时,页签位于容器右侧。 ~~~ Tabs({ barPosition: BarPosition.End}) { ...}.vertical(true) .barWidth(100) .barHeight(200) ~~~ 效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114006.04492209501668721336598242374605:50001231000000:2800:98598526A504AFC8EFD7918CD72936A0BD29771D53DDB6F74EF50F975E61A768.png?needInitFileName=true?needInitFileName=true) ## **4.4.5 自定义TabBar样式** TabBar的默认显示效果如下所示: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114007.57593322016245242510238779899423:50001231000000:2800:FFC6D0D6E2F086D34F1604E5942C502354571C066F2532F28D2F25538B368B2F.png?needInitFileName=true?needInitFileName=true "点击放大") 往往开发过程中,UX给我们的设计效果可能并不是这样的,比如下面的这种底部页签效果: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114007.72898712019039007392373201818437:50001231000000:2800:F1B0F2CEE7AEBA54CD61BA50E076745B89930EF879881CAF646D7D6E559B3F54.png?needInitFileName=true?needInitFileName=true) TabContent的tabBar属性除了支持string类型,还支持使用@Builder装饰器修饰的函数。您可以使用@Builder装饰器,构造一个生成自定义TabBar样式的函数,实现上面的底部页签效果,示例代码如下: ~~~ @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; }) } } ~~~ 示例代码中将barPosition的值设置为BarPosition.End,使页签显示在底部。使用@Builder修饰TabBuilder函数,生成由Image和Text组成的页签。同时也给Tabs组件设置了TabsController控制器,当点击某个页签时,调用changeIndex方法进行页签内容切换。 最后还需要给Tabs添加onChange事件,Tab页签切换后触发该事件,这样当我们左右滑动内容视图的时候,页签样式也会跟着改变。 ## **4.4.6 参考** * Tabs组件的更多属性和参数的使用,可以参考API:[Tabs](https://developer.harmonyos.com/cn/docs/documentation/doc-references-V3/ts-container-tabs-0000001478181433-V3?catalogVersion=V3)。 * @Builder装饰器的使用,可以参考:[@Builder](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/arkts-dynamic-ui-elememt-building-0000001427584592-V3?catalogVersion=V3#ZH-CN_TOPIC_0000001427584592__builder)。 ## **五、HarmonyOS第一课:构建更加丰富的页面** ## **闯关习题** @State修饰的属性不允许在本地进行初始化。错误 @CustomDialog装饰器用于装饰自定义弹窗组件,使得弹窗可以自定义内容及样式。正确 将Video组件的controls属性设置为false时,不会显示控制视频播放的控制栏。正确 @Prop修饰的属性值发生变化时,此状态变化不会传递到其父组件。正确 使用Video组件播放网络视频时,需要以下哪种权限?ohos.permission.INTERNET 下列哪种组合方式可以实现子组件从父子组件单向状态同步。@State和@Prop 下列哪些状态装饰器修饰的属性必须在本地进行初始化。@State、@Provide ArkUI提供了下面哪些弹窗功能。 AlertDialog、TextPickerDialog、DatePickerDialog、@CustomDialog、TimePickerDialog ## **5.1 管理组件状态** ## **5.1.1 概述** 在应用中,界面通常都是动态的。如图1所示,在子目标列表中,当用户点击目标一,目标一会呈现展开状态,再次点击目标一,目标一呈现收起状态。界面会根据不同的状态展示不一样的效果。 **图1**展开/收起目标项 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114007.50352989855908807575143613709720:50001231000000:2800:60603B31427EA970E49A5131A2F8E26EF56B951C270F3B24D94F21CBE1DF31D1.gif?needInitFileName=true?needInitFileName=true) ArkUI作为一种声明式UI,具有状态驱动UI更新的特点。当用户进行界面交互或有外部事件引起状态改变时,状态的变化会触发组件自动更新。所以在ArkUI中,我们只需要通过一个变量来记录状态。当改变状态的时候,ArkUI就会自动更新界面中受影响的部分。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114007.39160377706025210406744915044142:50001231000000:2800:B918D7FE99474ACEE4D33A18924AC4F844396C02399DCE7646A2626B5C75B02C.png?needInitFileName=true?needInitFileName=true "点击放大") ArkUI框架提供了多种管理状态的装饰器来修饰变量,使用这些装饰器修饰的变量即称为状态变量。 在组件范围传递的状态管理常见的场景如下: | **场景** | **装饰器**| | :-- | :-- | | 组件内的状态管理 | @State| | 从父组件单向同步状态| @Prop | | 与父组件双向同步状态 | @Link| | 跨组件层级双向同步状态| @Provide和@Consume| 在组件内使用@State装饰器来修饰变量,可以使组件根据不同的状态来呈现不同的效果。若当前组件的状态需要通过其父组件传递而来,此时需要使用@Prop装饰器;若是父子组件状态需要相互绑定进行双向同步,则需要使用@Link装饰器。使用@Provide和@Consume装饰器可以实现跨组件层级双向同步状态。 在实际应用开发中,应用会根据需要封装数据模型。如果需要观察嵌套类对象属性变化,需要使用@Observed和@ObjectLink装饰器,因为上述表格中的装饰器只能观察到对象的第一层属性变化。@Observed和@ObjectLink装饰器的具体使用方法可参考[@Observed装饰器和@ObjectLink装饰器:嵌套类对象属性变化](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/arkts-observed-and-objectlink-0000001473697338-V3?catalogVersion=V3)。 另外,当状态改变,需要对状态变化进行监听做一些相应的操作时,可以使用@Watch装饰器来修饰状态。 ## **5.1.2 组件内的状态管理:@State** 实际开发中由于交互,组件的内容呈现可能产生变化。当需要在组件内使用状态来控制UI的不同呈现方式时,可以使用@State装饰器。以任务管理应用为例,当点击子目标列表的其中一项,列表项会展开。当再次点击同一项,列表项会收起。所以,对于某一个列表项来说,它的呈现方式会受列表项是否展开这个状态影响。 **图2**展开/收起目标项 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114007.52317835111317836092836111367073:50001231000000:2800:77E1EB146DBD2A34191F8F111ABE3848912C0710348AEEE23BFF9F178B646E05.gif?needInitFileName=true?needInitFileName=true) 将是否展开这个状态定义为isExpanded变量,当其值为false表示目标项收起,值为true时表示目标项展开。 此时,需要使用@State装饰器修饰isExpanded,使其成为目标项内部的状态变量。通过@State装饰后,框架内部会建立数据与视图间的绑定, 当isExpanded状态变化时,目标项会随之展开或收起。 **图3**定义是否展开状态 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114007.45649197960477681904864887845876:50001231000000:2800:B054BCF110986B242961805CC08005E929E031C1D9B6978A10751182BBFEDAE6.png?needInitFileName=true?needInitFileName=true "点击放大") 其具体实现只要用@State修饰isExpanded变量,定义是否展开状态。然后通过条件渲染,实现是否显示进度调整面板和列表项的高度变化。最后,监听列表项的点击事件,在onClick回调中改变isExpanded状态。 这样就实现了对相同列表项点击时,列表项的展开和收起功能。当用户反复点击同一个列表项时,组件内的isExpanded状态变化,列表项会自动更新。 ~~~ @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; ... }) ... } } ~~~ ## **5.1.3 从父组件单向同步状态:@Prop** 当子组件中的状态依赖从父组件传递而来时,需要使用@Prop装饰器,@Prop修饰的变量可以和其父组件中的状态建立单向同步关系。当父组件中状态变化时,该状态值也会更新至@Prop修饰的变量;对@Prop修饰的变量的修改不会影响其父组件中的状态。 **图4**列表的编辑模式 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114007.48656041274936249580000818927673:50001231000000:2800:545F77297CB59671CE844B2356DC04F677EECCB7260706EC948D5EA1D7B3E6D3.gif?needInitFileName=true?needInitFileName=true) 如图4所示,在目标管理应用中,当用户点击子目标列表的“编辑”文本,列表进入编辑模式,点击取消,列表退出编辑模式。 整个列表是自定义组件TargetList,顶部是文本显示区域,主要是Text组件,底部是一个Button组件。中间区域则是用来显示每个目标项,目标项是自定义组件TargetListItem。 从图中可以看出,TargetListItem是TargetList的子组件。TargetList是TargetListItem父组件。 **图5**TargetList和TargetListItem ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114007.45427026185552780164054661698684:50001231000000:2800:BAAF65FBDED4B6E3EEFEE7F6A2777BB86BB79E15F84C75F7B39D1038F3FFBC27.png?needInitFileName=true?needInitFileName=true) 对于父组件TargetList,其顶部显示的文本和底部按钮会随编辑模式的变化而变化,因此父组件拥有编辑模式状态。 对于子组件TargetListItem,其最右侧是否预留位置和显示勾选框也会随编辑模式变化,因此子组件也拥有编辑模式状态。 但是是否进入编辑模式,其触发点是在用户点击列表的“编辑”或取消按钮,状态变化的源头仅在于父组件TargetList。当父组件TargetList中的编辑模式变化时,子组件TargetListItem的编辑模式状态需要随之变化。 **图6**从父组件单向同步isEditMode状态 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114007.63145098253182931658469736073510:50001231000000:2800:6CFF8A17BCDC00E88591606139F6E152096A2C6C7682B2AACAFC418D0F586720.png?needInitFileName=true?needInitFileName=true) 在父组件TargetList中可以定义一个是否进入编辑模式的状态,即用@State修饰isEditMode。@State修饰的变量不仅是组件内部的状态,也可以作为子组件单向或双向同步的数据源。ArkUI提供了@Prop装饰器,@Prop修饰的变量可以和其父组件中的状态建立单向同步关系,所以用@Prop修饰子组件TargetListItem中的isEditMode变量。 在父组件TargetList中,用@State修饰isEditMode,定义编辑模式状态。然后利用条件渲染实现根据是否进入编辑模式,显示不同的文本和按钮。同时,在父组件中需要在用户点击时改变状态,触发界面更新。 当点击“编辑”事件发生时,进入编辑模式,显示取消、全选文本和勾选框,同时显示删除按钮;当点击“取消”事件发生时,退出编辑模式,显示“编辑”文本和“添加子目标”按钮。 ~~~ @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变量,定义子组件的编辑模式状态。然后同样根据是否进入编辑模式,控制目标项最右侧是否预留位置和显示勾选框。 ~~~ @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。 ~~~ @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) } ... } ... } } ~~~ ## **5.1.4 与父组件双向同步状态:@Link** 若是父子组件状态需要相互绑定进行双向同步时,可以使用@Link装饰器。父组件中用于初始化子组件@Link变量的必须是在父组件中定义的状态变量。 **图7**切换目标项 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114007.08063644355014687331552094225475:50001231000000:2800:700D8227A3EE3E40CE49ABF3211D80EA553F8DFE60EC799FAB6301AFDC74E91B.gif?needInitFileName=true?needInitFileName=true) 在目标管理应用中,当用户点击同一个目标,目标项会展开或者收起。当用户点击不同的目标项时,除了被点击的目标项展开,同时前一次被点击的目标项会收起。 如图7所示,当目标一展开时,点击目标三,目标三会展开,同时目标一会收起。再点击目标一时,目标一展开,同时目标三会收起。 从目标一切换到目标三的流程中,关键在于最后目标一的收起,当点击目标三时,目标一需要知道点击了目标三,目标一才会收起。 **图8**子目标列表目标项位置索引 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114007.00725185809807510601169603208881:50001231000000:2800:9CDC1827744D8BD687EC1E24F477FEFF30D288149478EC31B6C17EB7D652DC66.png?needInitFileName=true?needInitFileName=true "点击放大") 在子目标列表中,每个列表项都有其位置索引值index属性,表示目标项在列表中的位置。index从0开始,即第一个目标项的索引值为0,第二个目标项的索引值为1,以此类推。此外,clickIndex用来记录被点击的目标项索引。当点击目标一时,clickIndex为0,点击目标三时,clickIndex为2。 在父组件子目标列表和每个子组件目标项中都拥有clickIndex状态。当目标一展开时,clickIndex为0。此时点击目标三,目标三的clickIndex变为2,只要其父组件子目标列表感知到clickIndex状态变化,同时将此变化传递给目标一。目标一的clickIndex即可同步改变为2,即目标一感知到此时点击了目标三。 **图9**与父组件双向同步clickIndex状态 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114008.93339135166396342543737983202826:50001231000000:2800:A2FE86136BDF79F5E868578F5B13E6B5D869F9507B42FB81B1FB59203FC111EA.png?needInitFileName=true?needInitFileName=true "点击放大") 将列表和目标项对应到列表组件TargetList和列表项TargetListItem。首先,需要在父组件TargetList中定义clickIndex状态。 若此时子组件中的clickIndex用@Prop装饰器修饰,当子组件中clickIndex变化时,父组件无法感知,因为@Prop装饰器建立的是从父组件到子组件的单向同步关系。 ArkUI提供了@Link装饰器,用于与父组件双向同步状态。当子组件TargetListItem中的clickIndex用@Link修饰,可与父组件TargetList中的clickIndex建立双向同步关系。 ~~~ @Component export default struct TargetList { @State clickIndex: number = CommonConstants.DEFAULT_CLICK_INDEX; ... TargetListItem({ clickIndex: $clickIndex, ... }) ... } ~~~ 首先,在父组件TargetList中用@State装饰器定义点击的目标项索引状态。然后,在子组件TargetListItem中用@Link装饰器定义clickIndex,当点击目标项时,clickIndex更新为当前目标索引值。 完成在父子组件中定义状态后,最关键的就是要建立父子组件的双向关联关系。在父组件中使用子组件时,将父组件的clickIndex传递给子组件的clickIndex。其中父组件的clickIndex加上$表示传递的是引用。 ~~~ @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; ... }) ... } } ~~~ 当目标一感知到点击了目标三时,还需要将目标一收起,切换列表项的功能才是完整的。此时,目标一感知到clickIndex变为2,需要判断与目标一本身的位置索引值0不相等,从而将目标一收起。此时,就需要用到ArkUI中监听状态变化@Watch的能力。用@Watch修饰的状态,当状态发生变化时,会触发声明时定义的回调。 我们给TargetListItem的中的clickIndex状态加上@Watch("onClickIndexChanged")。这表示需要监听clickIndex状态的变化。当clickIndex状态变化时,将触发onClickIndexChanged回调:如果点击的列表项索引不等于当前列表项索引,则将isExpanded状态置为false,从而收起该目标项。 ## **5.1.5 跨组件层级双向同步状态:@Provide和@Consume** ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114008.98879574764239285336519579985124:50001231000000:2800:7FAB420652A38B6314548097054ABC5B9743D9F41C67657F52C5005ED395565E.png?needInitFileName=true?needInitFileName=true) 跨组件层级双向同步状态是指@Provide修饰的状态变量自动对提供者组件的所有后代组件可用,后代组件通过使用@Consume装饰的变量来获得对提供的状态变量的访问。@Provide作为数据的提供方,可以更新其子孙节点的数据,并触发页面渲染。@Consume在感知到@Provide数据的更新后,会触发当前自定义组件的重新渲染。 使用@Provide的好处是开发者不需要多次将变量在组件间传递。@Provide和@Consume的具体使用方法请参见开发指南:[@Provide装饰器和@Consume装饰器:与后代组件双向同步](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/arkts-provide-and-consume-0000001473857338-V3?catalogVersion=V3)。 ## **5.1.6 参考** 更多状态管理场景和相关知识请参考开发指南:[状态管理](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/arkts-state-management-overview-0000001524537145-V3?catalogVersion=V3)。 ## **5.2 Video组件的使用** ## **5.2.1 概述** 在手机、平板或是智慧屏这些终端设备上,媒体功能可以算作是我们最常用的场景之一。无论是实现音频的播放、录制、采集,还是视频的播放、切换、循环,亦或是相机的预览、拍照等功能,媒体组件都是必不可少的。以视频功能为例,在应用开发过程中,我们需要通过ArkUI提供的Video组件为应用增加基础的视频播放功能。借助Video组件,我们可以实现视频的播放功能并控制其播放状态。常见的视频播放场景包括观看网络上的较为流行的短视频,也包括查看我们存储在本地的视频内容。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114008.58383607879892627431132066000424:50001231000000:2800:E14F8E0A7DB56B8FA1D052EDE49E4D7C3598670D401FD7DFED4686CA3FC308D6.png?needInitFileName=true?needInitFileName=true) 本文将结合《简易视频播放器(ArkTS)》这个Codelab,对Video组件的参数、属性及事件进行介绍,然后通过组件的属性调用和事件回调阐明Video组件的基本使用方法,最后结合Video组件使用过程中的常见问题讲解自定义控制器的使用。 ## **5.2.2 Video组件用法介绍** `Video组件参数介绍` Video组件的接口表达形式为: ~~~ Video(value: {src?: string | Resource, currentProgressRate?: number | string |PlaybackSpeed, previewUri?: string |PixelMap | Resource, controller?: VideoController}) ~~~ 其中包含四个可选参数,src、currentProgressRate、previewUri和controller。 * src表示视频播放源的路径,可以支持本地视频路径和网络路径。使用网络地址时,如https,需要注意的是需要在module.json5文件中申请网络权限。在使用本地资源播放时,当使用本地视频地址我们可以使用媒体库管理模块medialibrary来查询公共媒体库中的视频文件,示例代码如下: ~~~ 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表示视频控制器。 参数的具体描述如下表: | 参数名| 参数类型| 必填| | :-- | :-- | :-- | | src | string\Resource | 否| | currentProgressRate| number \ string \ PlaybackSpeed8+ | 否| | previewUri | string \PixelMap8+ \ Resource|否| | controller| VideoController| 否 | 说明 视频支持的规格是:mp4、mkv、webm、TS。 下面我们通过具体的例子来说明参数的使用方法,我们选择播放本地视频,视频未播放时的预览图片路径也为本地,代码实现如下: ~~~ @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 }) ... VideoSlider({ controller: this.controller }) } } } ~~~ 效果如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114008.96443894124163118085489349855543:50001231000000:2800:710F1AB15816BC79619840A28E687A50783FE4D62B22C0D0F763E111A4394F2B.png?needInitFileName=true?needInitFileName=true) `Video组件属性介绍` 除了支持组件的尺寸设置、位置设置等通用属性外,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(保持宽高比进行缩小或者放大,使得图片两边都大于或等于显示边界),其他效果(如自适应显示、保持原有尺寸显示、不保持宽高比进行缩放等)可以根据具体使用场景/设备来进行选择。 在Codelab示例中体现了controls、autoplay和loop属性的配置,示例代码如下: ``` @Component export struct VideoPlayer { private source: string | Resource; private controller: VideoController; ... build() { Column() { Video({ controller: this.controller }) .controls(false) //不显示控制栏 .autoPlay(false) // 手动点击播放 .loop(false) // 关闭循环播放 ... } } } ``` 效果如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114008.22034190109683083044144924925615:50001231000000:2800:F5DC490C05E427658F1904CC94137AF9A131585C401952FEC9370EF0219F32BB.png?needInitFileName=true?needInitFileName=true) ### Video组件回调事件介绍 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)| 在全屏播放与非全屏播放状态之间切换时触发该事件| 在Codelab中我们以更新事件、准备事件、失败事件以及点击事件为回调为例进行演示,代码实现如下: ~~~ 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秒。onError事件在视频播放失败时触发,在CommonConstants.ets中定义了常量类MESSAGE,所以在视频播放失败时会显示“请检查网络”。 ~~~ const MESSAGE: string = '请检查网络' ~~~ ## **5.2.2 自定义控制器的组成与实现** `自定义控制器的组成` Video组件的原生控制器样式相对固定,当我们对页面的布局色调的一致性有所要求,或者在拖动进度条的同时需要显示其百分比进度时,原生控制器就无法满足需要了。如下图右侧的效果需要使用自定义控制器实现,接下来我们看一下自定义控制器的组成。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114008.34725310254114274042500806358448:50001231000000:2800:46143ADEC3EE2106CC5BF83D49527471872A8DB7F6D277CE8BD81ACCF65712EA.png?needInitFileName=true?needInitFileName=true "点击放大") 为了实现自定义控制器的进度显示等功能,我们需要通过Row容器实现控制器的整体布局,然后借由Text组件来显示视频的播放起始时间、进度时间以及视频总时长,最后通过滑动进度条Slider组件来实现视频进度条的效果,代码如下: ~~~ @Componentexport struct VideoSlider { ... build() { Row(...) { Image(...) Text(...) Slider(...) Text(...) } ... }} ~~~ `自定义控制器的实现` 自定义控制器容器内嵌套了视频播放时间Text组件、滑动器Slider组件以及视频总时长Text组件 3个横向排列的组件,其中Text组件在之前的基础组件课程中已经有过详细介绍,这里就不再进行赘述。需要强调的是两个Text组件显示的时长是由Slider组件的onChange(callback: (value: number, mode: SliderChangeMode) => void)回调事件来进行传递的,而Text组件的数值与视频播放进度数值value则是通过@Provide与 @Consume装饰器进行的数据联动,实现效果可见图片下方黑色控制栏部分,具体代码步骤及代码如下: `获取/计算视频时长` ~~~ 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}`; ... }; ~~~ `设置进度条参数及属性` ~~~ 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组件。在视频开始播放前,播放时间默认为00:00,随着视频播放,时间需要不断更新为当前进度时间。所以左侧的Text组件我们不仅需要读取时间,还需要为其添加数据联动。这里,我们就是通过为Video组件添加onUpdate事件来实现的,在视频播放过程中会不断调用changeSliderTime方法获取当前的播放时间并进行计算及单位转化,从而不断刷新进度条的值,也就是控制器左侧的播放进度时间Text组件。 ~~~ Video({...}) ... .onUpdate((event) => { this.currentTime = event.time; this.currentStringTime = changeSliderTime(this.currentTime) }) ~~~ ~~~ 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滑动时就会触发该事件回调,从而实现将视频定位到进度条当前刷新位置,完成时长组件渲染与视频播放进度数据联动。 ~~~ Slider({...}) .onChange((value: number, mode: SliderChangeMode) => { sliderOnchange.call(this, value, mode); }) ~~~ ~~~ export function sliderOnchange(value: number, mode: SliderChangeMode) { this.currentTime = parseInt(value.toString()); this.controller.setCurrentTime(parseInt(value.toString()), SeekMode.Accurate); ... }; ~~~ 到这里我们就实现了自定义控制器的构建,两个Text组件显示的时长是由Slider组件的onChange回调事件来进行传递的,而Text组件的数值与视频播放进度数值value则通过是onUpdate与onChange事件并借由@Provide @Consume装饰器进行的数据联动。 ## **5.2.3 参考链接** * Video组件的更多属性和参数的使用,可以参考API:[Video](https://developer.harmonyos.com/cn/docs/documentation/doc-references-V3/ts-media-components-video-0000001427902484-V3?catalogVersion=V3)。 * ## **5.3 给您的应用添加弹窗** ## **5.3.1 概述** 在我们日常使用应用的时候,可能会进行一些敏感的操作,比如删除联系人,这时候我们给应用添加弹窗来提示用户是否需要执行该操作,如下图所示: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114009.81266630475599466259176175375297:50001231000000:2800:F5A0952D592407AC3AFAB2CF7F41409D389C190B0F8EAD9E55B9429C1DE13213.png?needInitFileName=true?needInitFileName=true "点击放大") 弹窗是一种模态窗口,通常用来展示用户当前需要的或用户必须关注的信息或操作。在弹出框消失之前,用户无法操作其他界面内容。ArkUI为我们提供了丰富的弹窗功能,弹窗按照功能可以分为以下两类: * 确认类:例如警告弹窗AlertDialog。 * 选择类:包括文本选择弹窗TextPickerDialog 、日期滑动选择弹窗DatePickerDialog、时间滑动选择弹窗TimePickerDialog等。 您可以根据业务场景,选择不同类型的弹窗。部分弹窗效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114009.02006314208798124153612926629630:50001231000000:2800:ABE98C27C05EC327B72237D9B573DEC9A7C1510CFD86C77DF61153F1EBBF765D.png?needInitFileName=true?needInitFileName=true "点击放大") 此外,如果上述弹窗还不能满足您的需求,或者需要对弹窗的布局和样式进行自定义,您还可以使用自定义弹窗CustomDialog。 下文将分别介绍AlertDialog 、TextPickerDialog 、DatePickerDialog以及CustomDialog的使用。 ## **5.3.2 警告弹窗** 警告弹窗AlertDialog由以下三部分区域构成,对应下面的示意图: 1. 标题区:为可选的。 2. 内容区:显示提示消息。 3. 操作按钮区:用户做”确认“或者”取消“等操作。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114009.08583160946403332888990227475910:50001231000000:2800:20C90F4598DE791028199ADD260C7B89388EC4F22911B6F75643884907D8AB82.png?needInitFileName=true?needInitFileName=true "点击放大") 以下示例代码,演示了如何使用AlertDialog 实现上图所示的警告弹窗。AlertDialog可以设置两个操作按钮,示例代码中分别使用primaryButton和secondaryButton实现了“取消”和“删除”操作按钮,操作按钮可以通过action响应点击事件。 ~~~ 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'); } } ) }) ~~~ 此外,您还可以使用AlertDialog,构建只包含一个操作按钮的确认弹窗,使用confirm响应操作按钮回调。 ~~~ 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') } } ) ~~~ ## **5.3.3 选择类弹窗** 选择类弹窗用于方便用户选择相关数据,比如选择喜欢吃的水果、出生日期等等。下面我们以TextPickerDialog和DatePickerDialog为例,来介绍选择类弹窗的使用。 `文本选择弹窗` TextPickerDialog为文本滑动选择器弹窗,根据指定的选择范围创建文本选择器,展示在弹窗上,例如下面这段示例代码使用TextPickerDialog实现了一个水果选择弹窗。示例代码中使用selected指定了弹窗的初始选择项索引为2,对应的数据为“香蕉”。当用户点击“确定”操作按钮后,会触发onAccept事件回调,在回调中将选中的值,传递给宿主中的select变量。 ~~~ @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%') } } ~~~ 效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114009.06203127778312855358092695067169:50001231000000:2800:3CCD9ED5BE2B0AE318165A94B62BFD39360CD682734333DD41CAA028AB3A709A.png?needInitFileName=true?needInitFileName=true) `日期选择弹窗` 下面我们介绍另一种常用的选择类弹窗DatePickerDialog,它是日期滑动选择器弹窗,根据指定的日期范围创建日期滑动选择器,展示在弹窗上。DatePickerDialog的使用非常广泛,比如当我们需要输入个人出生日期的时候,就可以使用DatePickerDialog。下面的示例代码实现了一个日期选择弹窗: ~~~ @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%') } } ~~~ 效果图如下: ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114009.43114074270197359477544761452334:50001231000000:2800:262CAFA928F19AFA4C1E24FB0B2AEEDFA63FAC09A2B94A91C49CCF5A1B87524C.png?needInitFileName=true?needInitFileName=true) ## **5.3.4 自定义弹窗** 自定义弹窗的使用更加灵活,适用于更多的业务场景,在自定义弹窗中您可以自定义弹窗内容,构建更加丰富的弹窗界面。自定义弹窗的界面可以通过装饰器@CustomDialog定义的组件来实现,然后结合CustomDialogController来控制自定义弹窗的显示和隐藏。下面我们通过一个兴趣爱好的选择框来介绍自定义弹窗的使用。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20231219114009.14616045610526211938768708679974:50001231000000:2800:BF9222E4BBD64C060BEDECA9B48C58808806FC3B2FE10588E88BB1E1542F0C78.png?needInitFileName=true?needInitFileName=true) 从上面的效果图可以看出,这个选择框是一个多选的列表弹窗,我们可以使用装饰器@CustomDialog,结合List组件来完成这个弹窗布局,实现步骤如下: 1. 初始化弹窗数据。 先准备好资源文件和数据实体类。其中资源文件stringarray.json创建在resources/base/element目录下,文件根节点为strarray。 ~~~ { "strarray": [ { "name": "hobbies_data", "value": [ { "value": "Soccer" }, { "value": "Badminton" }, { "value": "Travelling" }, ... ] } ] } ~~~ 实体类HobbyBean用来封装自定义弹窗中的"兴趣爱好"数据。 ``` export default class HobbyBean { label: string; isChecked: boolean; } ``` 然后创建一个ArkTS文件CustomDialogWidget,用来封装自定义弹窗,使用装饰器@CustomDialog修饰CustomDialogWidget表示这是一个自定义弹窗。使用资源管理对象manager获取数据,并将数据封装到hobbyBeans。 ~~~ @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。 ~~~ @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(具体实现可以参考《构建多种样式弹窗》Codelab源码)的点击事件中,调用customDialogController的open方法,用于显示弹窗。 ~~~ @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(); } }) ... } } ~~~ ## **5.3.5 参考** 关于更多弹窗,您可以参考: [警告弹窗](https://developer.harmonyos.com/cn/docs/documentation/doc-references-V3/ts-methods-alert-dialog-box-0000001478341185-V3?catalogVersion=V3) [列表选择弹窗](https://developer.harmonyos.com/cn/docs/documentation/doc-references-V3/ts-methods-action-sheet-0000001478061737-V3?catalogVersion=V3) [自定义弹窗](https://developer.harmonyos.com/cn/docs/documentation/doc-references-V3/ts-methods-custom-dialog-box-0000001477981237-V3?catalogVersion=V3) [日期滑动选择弹窗](https://developer.harmonyos.com/cn/docs/documentation/doc-references-V3/ts-methods-datepicker-dialog-0000001427902500-V3?catalogVersion=V3) [时间滑动选择弹窗](https://developer.harmonyos.com/cn/docs/documentation/doc-references-V3/ts-methods-timepicker-dialog-0000001428061780-V3?catalogVersion=V3) [文本滑动选择弹窗](https://developer.harmonyos.com/cn/docs/documentation/doc-references-V3/ts-methods-textpicker-dialog-0000001427584912-V3?catalogVersion=V3) ## **六、给应用添加动画** ## **闯关习题** 属性动画中产生动画的属性可以在任意位置声明。错误 属性动画中改变属性时需触发UI状态更新。正确 属性animation可以在哪些组件中使用?基础组件和容器组件 属性动画中如何设置反向播放?PlayMode.Reverse 下面哪种情况不会回调onFinish函数?iterations设置为 -1 属性动画中关于animation参数说法错误的是?参数delay不能大于duration 属性动画支持哪些属性?width、rotate、opacity、scale 属性动画中animation的参数有哪些?playMode、curve、delay、onFinish ## **6.1 属性动画的使用** ## **6.1.1 概述** 属性动画,是最为基础的动画,其功能强大、使用场景多,应用范围较广。常用于如下场景中: * 一、页面布局发生变化。例如添加、删除部分组件元素。 * 二、页面元素的可见性和位置发生变化。例如显示或者隐藏部分元素,或者将部分元素从一端移动到另外一端。 * 三、页面中图形图片元素动起来。例如使页面中的静态图片动起来。 简单来说,属性动画是组件的通用属性发生改变时而产生的属性渐变效果。如下图所示,其原理是,当组件的通用属性发生改变时,组件状态由初始状态逐渐变为结束状态的过程中,会创建多个连续的中间状态,逐帧播放后,就会形成属性渐变效果,从而形成动画。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102140125.30776267042224696432056505610220:50001231000000:2800:C687D4A94C174B70FF789C69919B0E052A06867898F4D3EC21A1FD70AA858E4B.png) 属性动画的使用方式也非常简单,只需要给组件(包括基础组件和容器组件)添加animation属性,并设置好参数,如下代码所示: ~~~ Image($r('app.media.image1')) .animation({ duration: 1000, tempo: 1.0, delay: 0, curve: Curve.Linear, playMode: PlayMode.Normal, iterations: 1 }) ~~~ ## **6.1.2 创建属性动画页面** 如下图所示,在该下拉刷新动画场景中,一共有6个属性动画。头部中的五个图标的移动放大动画中,每个图标都是单独的一个动画,其共同组合成一个刷新等待动画。最后是下方组件上移的一个移动动画。为方便理解,图中下方的内容将以图片来代替实际应用的功能页面。 图2-1 **:示例动画** ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221103195705.09518356622801048800456761033244:50001231000000:2800:70089B858FC528F98930773EBB3F12369CC19C7ADBD5551FF3DDB4E6E342186B.gif) 该6个属性动画创建方式类似,以五个图标放大移动动画的为例来讲解如何创建属性动画。 首先,创建一个头部刷新组件RefreshAnimHeader,在其中自定义一个图标组件AttrAnimIcons,用Image组件将资源图标引入,并设置好样式,如下所示: ~~~ @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状态更新。 ~~~ @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(); }) } } ~~~ 运行代码,即可看到五个图标的移动放大动画效果。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102140215.95436691968376446727824039360503:50001231000000:2800:9FAA7180774BC8A094CAA362FE8B05632C7B132E77DAD1B04C5BCE60C447EE8C.png) 1、animation属性作用域。animation自身也是组件的一个属性,其作用域为animation之前。即产生属性动画的属性须在animation之前声明,其后声明的将不会产生属性动画。以示例中的五个图标动画为例,我们期望产生动画的属性为Image组件的width属性,故该属性width需在animation属性之前声明。如果将该属性width在animation之后声明,则不会产生动画效果。 2、产生属性动画的属性变化时需触发UI状态更新。在本示例中,产生动画的属性width,其值是通过变量iconWidth从30变为100,故该变量iconWidth的改变需触发UI状态更新。 3、产生属性动画的属性本身需满足一定的要求,并非任何属性都可以产生属性动画。目前支持的属性包括width、height、position、opacity、backgroundColor、scale、rotate、translate等 ## **6.1.3 属性动画参数调整** 属性动画中animation的参数如下: |属性名称| 属性类型| 默认值 | 描述| | --- | --- | --- | --- | | duration| number | 1000| 动画时长,单位为毫秒,默认时长为1000毫秒。| | temp | 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...)正向播放。 | 本文以参数delay和onFinish为例来演示和讲解属性动画的参数调整。其他参数的效果可自行尝试。 `延时播放时间delay的设置` 在单个的组件元素的属性动画中,一般不设置参数delay的值。而在需要设置时,往往是需要在动画开始前做一些准备工作,具体依场景而定,本文在此不讨论。 在由多个组件元素的属性动画组合的动画中,例如示例动画中的五个图标的属性动画组合而成的刷新等待动画,通过设置参数delay的值,可以使各个组件元素之间按照一定的秩序依次播放,形成跌宕起伏、鳞次栉比的动画效果。在此场景中,该值的大小又与duration相关联。 该如何设置各个图标的参数delay的值呢? 在设置delay值之前,我们先理解一个概念:延时间距。其意思是两个图标组件的延时参数delay的差值,即:delay2-delay1=延时间距。要想实现五个图标之间以良好的秩序先后移动放大,各个图标之间的延时间距是一样的,例如延时间距为100ms时,此五个图标的延时delay可以分别设置为100ms、200ms、300ms、400ms、500ms。 在实际开发场景中,我们该如何确定延时间距呢? 在此有个经验可以参考:在延时间距不超过动画时长duration时,总延时间距越接近duration,秩序性越好。其中,总延时间距为延时间距与图标数量的乘积,即:延时间距\*图标数量=总延时间距。 故此,我们在设置参数delay时,需要确定动画时长duration的值。该值默认为1000ms,具体时长可依据具体的业务需要来决定。 在本示例动画中,图标动画时长duration为2000ms,故延时间距为2000ms/5=400ms,五个图标的延时参数delay可分别设置为400ms、800ms、1200ms、1600ms、2000ms。其效果如示例图所示,图标先后秩序明显,视觉效果良好。 `onFinish回调函数的使用` 参数onFinish与参数iterations有关。当参数iterations播放结束时,会调用onFinish函数来进行后续的业务处理。例如提示动画播放结束。 ~~~ 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.Normal, iterations: 1, onFinish: () => { prompt.showToast({ message:"动画播放结束!!!" }) } }) ~~~ ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102140234.85135546535521901605966886990679:50001231000000:2800:53FDBD1A0F28CE8FAEBCBD2EB3C0A91B543FDEE3D48E0C557D1B60F051883528.png) 当iterations设置为-1时,表示无限次播放,则onFinish回调函数不会被调用。 ## **6.1.4 关闭属性动画页面** 此处需要将关闭属性动画区别开来: * 属性动画关闭,是指动画播放结束,但是动画组件依然存在并显示在页面上。 * 关闭属性动画页面,是指将动画的组件删除或者隐藏起来。 在本示例动画中,指将头部刷新组件RefreshAnimHeader隐藏起来。该如何实现呢? 首先,在组件RefreshAnimHeader中添加变量state,并用@Consume监听其值的变化,同时添加条件渲染逻辑,根据state的值来判断是否需要关闭。当state变为IDLE状态时,表示需要关闭属性动画页面。 ~~~ @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(); }) } } ~~~ 其次,在本示例中,通过下方图片的上移属性动画来关闭刷新组件RefreshAnimHeader。在组件RefreshComponent中,通过@Consume与组件RefreshAnimHeader的@Consume进行间接绑定,修改state变量的值为IDLE状态即可关闭属性动画页面。 ~~~ @Component export default struct RefreshComponent { @Consume(RefreshConstants.REFRESH_STATE_TAG) @Watch('onStateChanged') state: RefreshState; build() { List({ scroller: this.listController }) { ListItem() { ... } } .animation({ curve: Curve.Smooth, duration: RefreshConstants.REFRESH_HEADER_ANIM_DURATION, playMode: PlayMode.Normal, onFinish: () => { if (this.headerOffset === -RefreshConstants.REFRESH_HEADER_HEIGHT) { this.state = RefreshState.IDLE; } } }) } ~~~ ## **6.1.5 参考** 具体代码信息请参考Codelab:自定义下拉刷新动画(ArkTS) ## **七、HarmonyOS第一课:从网络获取数据** ## **7.1 Web组件的使用** ## **7.1.1 概述** 相信大家都遇到过这样的场景,有时候我们点击应用的页面,会跳转到一个类似浏览器加载的页面,加载完成后,才显示这个页面的具体内容,这个加载和显示网页的过程通常都是浏览器的任务。 ArkUI为我们提供了Web组件来加载网页,借助它我们就相当于在自己的应用程序里嵌入一个浏览器,从而非常轻松地展示各种各样的网页。 ![](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/103/404/958/0260086000103404958.20221102130140.00495404175221314986597538603428:50001231000000:2800:B4214971A06540516367B23A49156D31CA6D59512F73173BAD42BC0CF819E3A2.png) 本文将为您介绍Web组件一些常用API的使用。 ## **7.1.2 加载网页**