diff --git a/zh-cn/application-dev/Readme-CN.md b/zh-cn/application-dev/Readme-CN.md index b81af1f250ccdf126e45ebeccc44f7c29c02a975..405f0971488d733c9989dbf8bab377616e3c1cfb 100644 --- a/zh-cn/application-dev/Readme-CN.md +++ b/zh-cn/application-dev/Readme-CN.md @@ -39,21 +39,47 @@ - [app对象内部结构](quick-start/app-structure.md) - [deviceConfig内部结构](quick-start/deviceconfig-structure.md) - [module对象内部结构](quick-start/module-structure.md) - - [资源分类与访问](quick-start/resource-categories-and-access.md) + - [资源分类与访问](quick-start/resource-categories-and-access.md) - 学习ArkTS语言 - [初识ArkTS语言](quick-start/arkts-get-started.md) - - ArkTS语法(声明式UI) - - [基本UI描述](quick-start/arkts-basic-ui-description.md) - - 状态管理 - - [基本概念](quick-start/arkts-state-mgmt-concepts.md) - - [页面级变量的状态管理](quick-start/arkts-state-mgmt-page-level.md) - - [应用级变量的状态管理](quick-start/arkts-state-mgmt-application-level.md) - - [动态构建UI元素](quick-start/arkts-dynamic-ui-elememt-building.md) - - [渲染控制](quick-start/arkts-rendering-control.md) - - [使用限制与扩展](quick-start/arkts-restrictions-and-extensions.md) + - 基本语法 + - [基本语法概述](quick-start/arkts-basic-syntax-overview.md) + - [声明式UI描述](quick-start/arkts-declarative-ui-description.md) + - 自定义组件 + - [创建自定义组件](quick-start/arkts-create-custom-components.md) + - [页面和自定义组件生命周期](quick-start/arkts-page-custom-components-lifecycle.md) + - [\@Builder:自定义构建函数](quick-start/arkts-builder.md) + - [\@BuilderParam:引用\@Builder函数](quick-start/arkts-builderparam.md) + - [\@Styles:定义组件重用样式](quick-start/arkts-style.md) + - [\@Extend:定义扩展组件样式](quick-start/arkts-extend.md) + - [stateStyles:多态样式](quick-start/arkts-statestyles.md) + - 状态管理 + - [状态管理概述](quick-start/arkts-state-management-overview.md) + - 管理组件拥有的状态 + - [\@State:组件内状态](quick-start/arkts-state.md) + - [\@Prop:父子单向同步](quick-start/arkts-prop.md) + - [\@Link:父子双向同步](quick-start/arkts-link.md) + - [\@Provide和\@Consume:与后代组件双向同步](quick-start/arkts-provide-and-consume.md) + - [\@Observed和\@ObjectLink:嵌套类对象属性变化](quick-start/arkts-observed-and-objectlink.md) + - 管理应用拥有的状态 + - [管理应用拥有的状态概述](quick-start/arkts-application-state-management-overview.md) + - [LocalStorage:页面级UI状态存储](quick-start/arkts-localstorage.md) + - [AppStorage:应用全局的UI状态存储](quick-start/arkts-appstorage.md) + - [PersistentStorage:持久化存储UI状态](quick-start/arkts-persiststorage.md) + - [Environment:设备环境查询](quick-start/arkts-environment.md) + - 其他状态管理 + - [其他状态管理概述](quick-start/arkts-other-state-mgmt-functions-overview.md) + - [\@Watch:状态变量更改通知](quick-start/arkts-watch.md) + - [$$语法:内置组件双向同步](quick-start/arkts-two-way-sync.md) + - 渲染控制 + - [渲染控制概述](quick-start/arkts-rendering-control-overview.md) + - [if/else:条件渲染](quick-start/arkts-rendering-control-ifelse.md) + - [ForEach:循环渲染](quick-start/arkts-rendering-control-foreach.md) + - [LazyForEach:数据懒加载](quick-start/arkts-rendering-control-lazyforeach.md) - 开发 - [应用模型](application-models/Readme-CN.md) - [UI开发](ui/Readme-CN.md) + - [Web](web/Readme-CN.md) - [通知](notification/Readme-CN.md) - [窗口管理](windowmanager/Readme-CN.md) - [WebGL](webgl/Readme-CN.md) diff --git a/zh-cn/application-dev/application-dev-guide-for-gitee.md b/zh-cn/application-dev/application-dev-guide-for-gitee.md index b8320cd4c830d9fbcd6e012ce54dbfcf34c3248c..e52589b59cf3b4d1ebc76cc67417e0c4bdc5eff5 100644 --- a/zh-cn/application-dev/application-dev-guide-for-gitee.md +++ b/zh-cn/application-dev/application-dev-guide-for-gitee.md @@ -24,6 +24,8 @@ 所有应用都应该在这两个框架的基础之上进行功能的开发。 在此基础上,还提供了如下功能的开发指导: + +- [Web](web/web-component-overview.md) - [通知](notification/Readme-CN.md) - [窗口管理](windowmanager/Readme-CN.md) - [WebGL](webgl/Readme-CN.md) @@ -32,16 +34,16 @@ - [网络与连接](connectivity/Readme-CN.md) - [电话服务](telephony/Readme-CN.md) - [数据管理](database/Readme-CN.md) +- [文件管理](file-management/Readme-CN.md) - [任务管理](task-management/Readme-CN.md) - [设备管理](device/Readme-CN.md) - [设备使用信息统计](device-usage-statistics/Readme-CN.md) - [DFX](dfx/Readme-CN.md) - [国际化](internationalization/Readme-CN.md) - [应用测试](application-test/Readme-CN.md) +- [一次开发,多端部署](key-features/multi-device-app-dev/Readme-CN.md) - [IDL工具规格及使用说明书](IDL/idl-guidelines.md) - [Native API的相关指导](napi/Readme-CN.md) -- [文件管理](file-management/medialibrary-overview.md) -- [一次开发,多端部署](key-features/multi-device-app-dev/foreword.md) ### 工具 diff --git a/zh-cn/application-dev/application-dev-guide.md b/zh-cn/application-dev/application-dev-guide.md index 000525a35348eed5f6dc4f6d08862f2186569555..8340df1b0b9a61a684958a4c375d0c505ac3feea 100644 --- a/zh-cn/application-dev/application-dev-guide.md +++ b/zh-cn/application-dev/application-dev-guide.md @@ -24,15 +24,17 @@ 所有应用都应该在这两个框架的基础之上进行功能的开发。 在此基础上,还提供了如下功能的开发指导: + +- [Web](web/web-component-overview.md) - [通知](notification/notification-overview.md) - [窗口管理](windowmanager/window-overview.md) - [WebGL](webgl/webgl-overview.md) -- [媒体](media/audio-overview.md) +- [媒体](media/media-application-overview.md) - [安全](security/userauth-overview.md) - [网络与连接](connectivity/ipc-rpc-overview.md) - [电话服务](telephony/telephony-overview.md) - [数据管理](database/database-mdds-overview.md) -- [文件管理](file-management/medialibrary-overview.md) +- [文件管理](file-management/file-management-overview.md) - [任务管理](task-management/background-task-overview.md) - [设备管理](device/usb-overview.md) - [设备使用信息统计](device-usage-statistics/device-usage-statistics-overview.md) diff --git a/zh-cn/application-dev/file-management/Readme-CN.md b/zh-cn/application-dev/file-management/Readme-CN.md index 137b758b1584e243b863233b83831d7953f12ef5..ccf665fea3e8c62f374d221a021d0ddb076ee854 100644 --- a/zh-cn/application-dev/file-management/Readme-CN.md +++ b/zh-cn/application-dev/file-management/Readme-CN.md @@ -1,10 +1,23 @@ -# 文件管理 -- 媒体库管理 - - [媒体库开发概述](medialibrary-overview.md) - - [媒体资源使用指导](medialibrary-resource-guidelines.md) - - [文件路径使用指导](medialibrary-filepath-guidelines.md) - - [相册资源使用指导](medialibrary-album-guidelines.md) +# 文件 -- 文件访问框架 - - [用户公共文件访问框架概述](file-access-framework-overview.md) -- [文件选择器使用指导](filepicker-guidelines.md) \ No newline at end of file +- [文件管理概述](file-management-overview.md) +- 应用文件 + - [应用文件概述](app-file-overview.md) + - [应用沙箱目录](app-sandbox-directory.md) + - 应用文件访问与管理 + - [应用文件访问](app-file-access.md) + - [应用文件上传下载](app-file-upload-download.md) + - [应用及文件系统空间统计](app-fs-space-statistics.md) + - [向应用沙箱推送文件](send-file-to-app-sandbox.md) + - [应用文件分享](share-app-file.md) +- 用户文件 + - [用户文件概述](user-file-overview.md) + - 选择与保存用户文件(FilePicker) + - [选择用户文件](select-user-file.md) + - [保存用户文件](save-user-file.md) + - [开发用户文件管理器(仅对系统应用开放)](dev-user-file-manager.md) + - [管理外置存储设备(仅对系统应用开放)](manage-external-storage.md) +- 分布式文件系统 + - [分布式文件系统概述](distributed-fs-overview.md) + - [设置分布式文件数据等级](set-security-label.md) + - [跨设备文件访问](file-access-across-devices.md) diff --git a/zh-cn/application-dev/file-management/app-file-access.md b/zh-cn/application-dev/file-management/app-file-access.md new file mode 100644 index 0000000000000000000000000000000000000000..40bccca794a8d096000d6f874d7e9ea441b4be59 --- /dev/null +++ b/zh-cn/application-dev/file-management/app-file-access.md @@ -0,0 +1,171 @@ +# 应用文件访问 + +应用需要对应用文件目录下的应用文件进行查看、创建、读写、删除、移动、复制、获取属性等访问操作,下文介绍具体方法。 + +## 接口说明 + +开发者通过基础文件操作接口([ohos.file.fs](../reference/apis/js-apis-file-fs.md))实现应用文件访问能力,主要功能如下表所示。 + +**表1** 基础文件操作接口功能 + +| 接口名 | 功能 | 接口类型 | 支持同步 | 支持异步 | +| -------- | -------- | -------- | -------- | -------- | +| access | 检查文件是否存在 | 方法 | √ | √ | +| close | 关闭文件 | 方法 | √ | √ | +| copyFile | 复制文件 | 方法 | √ | √ | +| createStream | 基于文件路径打开文件流 | 方法 | √ | √ | +| listFile | 列出文件夹下所有文件名 | 方法 | √ | √ | +| mkdir | 创建目录 | 方法 | √ | √ | +| moveFile | 移动文件 | 方法 | √ | √ | +| open | 打开文件 | 方法 | √ | √ | +| read | 从文件读取数据 | 方法 | √ | √ | +| rename | 重命名文件或文件夹 | 方法 | √ | √ | +| rmdir | 删除整个目录 | 方法 | √ | √ | +| stat | 获取文件详细属性信息 | 方法 | √ | √ | +| unlink | 删除单个文件 | 方法 | √ | √ | +| write | 将数据写入文件 | 方法 | √ | √ | +| Stream.close | 关闭文件流 | 方法 | √ | √ | +| Stream.flush | 刷新文件流 | 方法 | √ | √ | +| Stream.write | 将数据写入流文件 | 方法 | √ | √ | +| Stream.read | 从流文件读取数据 | 方法 | √ | √ | +| File.fd | 获取文件描述符 | 属性 | √ | × | +| OpenMode | 设置文件打开标签 | 属性 | √ | × | +| Filter | 设置文件过滤配置项 | 类型 | × | × | + +## 开发示例 + +在对应用文件开始访问前,开发者需要[获取应用文件路径](../application-models/application-context-stage.md#获取应用开发路径)。以从UIAbilityContext获取HAP级别的文件路径为例进行说明,UIAbilityContext的获取方式请参见[获取UIAbility的上下文信息](../application-models/uiability-usage.md#获取uiability的上下文信息)。 + +下面介绍几种常用操作示例。 + +### 新建并读写一个文件 + +以下示例代码演示了如何新建一个文件并对其读写。 + +```ts +// pages/xxx.ets +import fs from '@ohos.file.fs'; +import common from '@ohos.app.ability.common'; + +function createFile() { + // 获取应用文件路径 + let context = getContext(this) as common.UIAbilityContext; + let filesDir = context.filesDir; + + // 新建并打开文件 + let file = fs.openSync(filesDir + '/test.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); + // 写入一段内容至文件 + let writeLen = fs.writeSync(file.fd, "Try to write str."); + console.info("The length of str is: " + writeLen); + // 从文件读取一段内容 + let buf = new ArrayBuffer(1024); + let readLen = fs.readSync(file.fd, buf, { offset: 0 }); + console.info("the content of file: " + String.fromCharCode.apply(null, new Uint8Array(buf.slice(0, readLen)))); + // 关闭文件 + fs.closeSync(file); +} +``` + +### 读取文件内容并写入到另一个文件 + + 以下示例代码演示了如何从一个文件读写内容到另一个文件。 + +```ts +// pages/xxx.ets +import fs from '@ohos.file.fs'; +import common from '@ohos.app.ability.common'; + +function readWriteFile() { + // 获取应用文件路径 + let context = getContext(this) as common.UIAbilityContext; + let filesDir = context.filesDir; + + // 打开文件 + let srcFile = fs.openSync(filesDir + '/test.txt', fs.OpenMode.READ_WRITE); + let destFile = fs.openSync(filesDir + '/destFile.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); + // 读取源文件内容并写入至目的文件 + let bufSize = 4096; + let readSize = 0; + let buf = new ArrayBuffer(bufSize); + let readLen = fs.readSync(srcFile.fd, buf, { offset: readSize }); + while (readLen > 0) { + readSize += readLen; + fs.writeSync(destFile.fd, buf); + readLen = fs.readSync(srcFile.fd, buf, { offset: readSize }); + } + // 关闭文件 + fs.closeSync(srcFile); + fs.closeSync(destFile); +} +``` + +> **说明:** +> +> 使用读写接口时,需注意可选项参数offset的设置。对于已存在且读写过的文件,文件偏移指针默认在上次读写操作的终止位置。 + +### 以流的形式读写文件 + +以下示例代码演示了如何使用流接口进行文件读写: + +```ts +// pages/xxx.ets +import fs from '@ohos.file.fs'; +import common from '@ohos.app.ability.common'; + +async function readWriteFileWithStream() { + // 获取应用文件路径 + let context = getContext(this) as common.UIAbilityContext; + let filesDir = context.filesDir; + + // 打开文件流 + let inputStream = fs.createStreamSync(filesDir + '/test.txt', 'r+'); + let outputStream = fs.createStreamSync(filesDir + '/destFile.txt', "w+"); + // 以流的形式读取源文件内容并写入目的文件 + let bufSize = 4096; + let readSize = 0; + let buf = new ArrayBuffer(bufSize); + let readLen = await inputStream.read(buf, { offset: readSize }); + readSize += readLen; + while (readLen > 0) { + await outputStream.write(buf); + readLen = await inputStream.read(buf, { offset: readSize }); + readSize += readLen; + } + // 关闭文件流 + inputStream.closeSync(); + outputStream.closeSync(); +} +``` + +> **说明:** +> 使用流接口时,需注意流的及时关闭。同时流的异步接口应严格遵循异步接口使用规范,避免同步、异步接口混用。流接口不支持并发读写。 + +### 查看文件列表 + +以下示例代码演示了如何查看文件列表: + +```ts +// 查看文件列表 +import fs from '@ohos.file.fs'; +import common from '@ohos.app.ability.common'; + +// 获取应用文件路径 +let context = getContext(this) as common.UIAbilityContext; +let filesDir = context.filesDir; + +// 查看文件列表 +let options = { + recursion: false, + listNum: 0, + filter: { + suffix: ['.png', '.jpg', '.txt'], // 匹配文件后缀名为'.png','.jpg','.txt' + displayName: ['test%'], // 匹配文件全名以'test'开头 + fileSizeOver: 0, // 匹配文件大小大于等于0 + lastModifiedAfter: new Date(0).getTime(), // 匹配文件最近修改时间在1970年1月1日之后 + }, +} +let files = fs.listFileSync(filesDir, options); +for (let i = 0; i < files.length; i++) { + console.info(`The name of file: ${files[i]}`); +} +``` diff --git a/zh-cn/application-dev/file-management/app-file-overview.md b/zh-cn/application-dev/file-management/app-file-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..e57eabfb1ac7d41b00ed473d29d416c0f097c987 --- /dev/null +++ b/zh-cn/application-dev/file-management/app-file-overview.md @@ -0,0 +1,11 @@ +# 应用文件概述 + +应用文件:文件所有者为应用,包括应用安装文件、应用资源文件、应用缓存文件等。 + +- 设备上应用所使用及存储的数据,以文件、键值对、数据库等形式保存在一个应用专属的目录内。该专属目录我们称为“应用文件目录”,该目录下所有数据以不同的文件格式存放,这些文件即应用文件。 + +- “应用文件目录”与一部分系统文件(应用运行必须使用的系统文件)所在的目录组成了一个集合,该集合称为“[应用沙箱目录](app-sandbox-directory.md)”,代表应用可见的所有目录范围。因此“应用文件目录”是在“应用沙箱目录”内的。 + +- 系统文件及其目录对于应用是只读的;应用仅能保存文件到“[应用文件目录](app-sandbox-directory.md#应用文件目录与应用文件路径)”下,根据目录的使用规范和注意事项来选择将数据保存到不同的子目录中。 + +下文将详细介绍应用沙箱、应用文件目录、应用文件访问与管理、应用文件分享等相关内容。 diff --git a/zh-cn/application-dev/file-management/app-file-upload-download.md b/zh-cn/application-dev/file-management/app-file-upload-download.md new file mode 100644 index 0000000000000000000000000000000000000000..c63f4c52a15c7389d45641977402f50bb9b46472 --- /dev/null +++ b/zh-cn/application-dev/file-management/app-file-upload-download.md @@ -0,0 +1,103 @@ +# 应用文件上传下载 + +应用可以将应用文件上传到网络服务器,也可以从网络服务器下载网络资源文件到本地应用文件目录。 + +## 上传应用文件 + +开发者可以使用上传下载模块([ohos.request](../reference/apis/js-apis-request.md))的上传接口将本地文件上传。文件上传过程使用系统服务代理完成。 + +> **说明:** +> 当前上传应用文件功能,仅支持上传应用缓存文件路径(cacheDir)下的文件。 +> +> 使用上传下载模块,需[申请相关权限](../security/accesstoken-guidelines.md):ohos.permission.INTERNET。 + +以下示例代码演示了如何将应用缓存文件路径下的文件上传至网络服务器。 + +```ts +// pages/xxx.ets +import common from '@ohos.app.ability.common'; +import fs from '@ohos.file.fs'; +import request from '@ohos.request'; + +// 获取应用文件路径 +let context = getContext(this) as common.UIAbilityContext; +let cacheDir = context.cacheDir; + +// 新建一个本地应用文件 +let file = fs.openSync(cacheDir + '/test.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); +fs.writeSync(file.fd, 'upload file test'); +fs.closeSync(file); + +// 上传任务配置项 +let uploadConfig = { + url: 'https://xxx', + header: { key1: 'value1', key2: 'value2' }, + method: 'POST', + files: [ + { filename: 'test.txt', name: 'test', uri: 'internal://cache/test.txt', type: 'txt' } + ], + data: [ + { name: 'name', value: 'value' } + ] +} + +// 将本地应用文件上传至网络服务器 +try { + request.uploadFile(context, uploadConfig) + .then((uploadTask) => { + uploadTask.on('complete', (taskStates) => { + for (let i = 0; i < taskStates.length; i++) { + console.info(`upload complete taskState: ${JSON.stringify(taskStates[i])}'); + } + }); + }) + .catch((err) => { + console.error(`Invoke uploadFile failed, code is ${err.code}, message is ${err.message}`); + }) +} catch (err) { + console.error(`Invoke uploadFile failed, code is ${err.code}, message is ${err.message}`); +} +``` + +## 下载网络资源文件至应用文件目录 + +开发者可以使用上传下载模块([ohos.request](../reference/apis/js-apis-request.md))的下载接口将网络资源文件下载到应用文件目录。对已下载的网络资源文件,开发者可以使用基础文件IO接口([ohos.file.fs](../reference/apis/js-apis-file-fs.md))对其进行访问,使用方式与[应用文件访问](app-file-access.md)一致。文件下载过程使用系统服务代理完成。 + +> **说明:** +> 当前网络资源文件仅支持下载至应用文件目录。 +> +> 使用上传下载模块,需[申请相关权限](../security/accesstoken-guidelines.md):ohos.permission.INTERNET。 + +以下示例代码演示了如何将网络资源文件下载到应用文件目录: + +``` +// pages/xxx.ets +// 将网络资源文件下载到应用文件目录并读取一段内容 +import common from '@ohos.app.ability.common'; +import fs from '@ohos.file.fs'; +import request from '@ohos.request'; + +// 获取应用文件路径 +let context = getContext(this) as common.UIAbilityContext; +let filesDir = context.filesDir; + +try { + request.downloadFile(context, { + url: 'https://xxxx/xxxx.txt', + filePath: filesDir + '/xxxx.txt' + }).then((downloadTask) => { + downloadTask.on('complete', () => { + console.info('download complete'); + let file = fs.openSync(filesDir + '/xxxx.txt', fs.OpenMode.READ_WRITE); + let buf = new ArrayBuffer(1024); + let readLen = fs.readSync(file.fd, buf); + console.info(`The content of file: ${String.fromCharCode.apply(null, new Uint8Array(buf.slice(0, readLen)))}`); + fs.closeSync(file); + }) + }).catch((err) => { + console.error(`Invoke downloadTask failed, code is ${err.code}, message is ${err.message}`); + }); +} catch (err) { + console.error(`Invoke downloadFile failed, code is ${err.code}, message is ${err.message}`); +} +``` diff --git a/zh-cn/application-dev/file-management/app-fs-space-statistics.md b/zh-cn/application-dev/file-management/app-fs-space-statistics.md new file mode 100644 index 0000000000000000000000000000000000000000..abf59f277bea29c68ba83979ad9fe61d0cd55b69 --- /dev/null +++ b/zh-cn/application-dev/file-management/app-fs-space-statistics.md @@ -0,0 +1,54 @@ +# 应用及文件系统空间统计 + +在系统中,可能出现系统空间不够或者cacheDir等目录受系统配额限制等情况,需要应用开发者关注系统剩余空间,同时控制应用自身占用的空间大小。 + +## 接口说明 + +API的详细介绍请参见[ohos.file.statvfs](../reference/apis/js-apis-file-statvfs.md)、[ohos.file.storageStatistics](../reference/apis/js-apis-file-storage-statistics.md)。 + +**表1** 文件系统空间和应用空间统计 + +| 模块 | 接口名 | 功能 | +| -------- | -------- | -------- | +| \@ohos.file.storageStatistic | getCurrentBundleStats | 获取当前应用的存储空间大小(单位为Byte)。 | +| \@ohos.file.statvfs | getFreeSize | 获取指定文件系统的剩余空间大小(单位为Byte)。 | +| \@ohos.file.statvfs | getTotalSize | 获取指定文件系统的总空间大小(单位为Byte)。 | + +**表2** 应用空间统计 + +| BundleStats属性 | 含义 | 统计路径 | +| -------- | -------- | -------- | +| appSize | 应用安装文件大小(单位为Byte) | 应用安装文件保存在以下目录:
/data/storage/el1/bundle | +| cacheSize | 应用缓存文件大小(单位为Byte) | 应用的缓存文件保存在以下目录:
/data/storage/el1/base/cache
/data/storage/el1/base/haps/entry/cache
/data/storage/el2/base/cache
/data/storage/el2/base/haps/entry/cache | +| dataSize | 应用文件存储大小(除应用安装文件和缓存文件)(单位为Byte) | 应用文件由本地文件、分布式文件以及数据库文件组成。
本地文件保存在以下目录(注意缓存文件目录为以下目录的子目录):
/data/storage/el1/base
/data/storage/el2/base
分布式文件保存在以下目录:
/data/storage/el2/distributedfiles
数据库文件保存在以下目录:
/data/storage/el1/database
/data/storage/el2/database | + +## 开发示例 + +- 获取文件系统数据分区剩余空间大小。 + + ```ts + import statvfs from '@ohos.file.statvfs'; + + let path = "/data"; + statvfs.getFreeSize(path, (err, number) => { + if (err) { + console.error(`Invoke getFreeSize failed, code is ${err.code}, message is ${err.message}`); + } else { + console.info(`Invoke getFreeSize succeeded, size is ${number}`); + } + }); + ``` + +- 获取当前应用的存储空间大小。 + + ```ts + import storageStatistics from "@ohos.file.storageStatistics"; + + storageStatistics.getCurrentBundleStats((err, bundleStats) => { + if (err) { + console.error(`Invoke getCurrentBundleStats failed, code is ${err.code}, message is ${err.message}`); + } else { + console.info(`Invoke getCurrentBundleStats succeeded, appsize is ${bundleStats.appSize}`); + } + }); + ``` diff --git a/zh-cn/application-dev/file-management/app-sandbox-directory.md b/zh-cn/application-dev/file-management/app-sandbox-directory.md new file mode 100644 index 0000000000000000000000000000000000000000..6aeb6746d3c50aac2c44d168507a5512b414c253 --- /dev/null +++ b/zh-cn/application-dev/file-management/app-sandbox-directory.md @@ -0,0 +1,91 @@ +# 应用沙箱目录 + +应用沙箱是一种以安全防护为目的的隔离机制,避免数据受到恶意路径穿越访问。在这种沙箱的保护机制下,应用可见的目录范围即为“应用沙箱目录”。 + +- 对于每个应用,系统会在内部存储空间映射出一个专属的“应用沙箱目录”,它是“[应用文件目录](app-file-overview.md)”与一部分系统文件(应用运行必需的少量系统文件)所在的目录组成的集合。 + +- 应用沙箱限制了应用可见的数据的最小范围。在“应用沙箱目录”中,应用仅能看到自己的应用文件以及少量的系统文件(应用运行必需的少量系统文件)。因此,本应用的文件也不为其他应用可见,从而保护了应用文件的安全。 + +- 应用可以在“[应用文件目录](app-file-overview.md)”下保存和处理自己的应用文件;系统文件及其目录对于应用是只读的;而应用若需访问[用户文件](user-file-overview.md),则需要通过特定API同时经过用户的相应授权才能进行。 + +下图展示了应用沙箱下,应用可访问的文件范围和方式。 + +**图1** 应用沙箱文件访问关系图   +![Application sandbox file access relationship](figures/application-sandbox-file-access-relationship.png) + +## 应用沙箱目录与应用沙箱路径 + +在应用沙箱保护机制下,应用无法获知除自身应用文件目录之外的其他应用或用户的数据目录位置及存在。同时,所有应用的目录可见范围均经过权限隔离与文件路径挂载隔离,形成了独立的路径视图,屏蔽了实际物理路径: + +- 如下图所示,在普通应用(也称三方应用)视角下,不仅可见的目录与文件数量限制到了最小范围,并且可见的目录与文件路径也与系统进程等其他进程看到的不同。我们将普通应用视角下看到的“应用沙箱目录”下某个文件或某个具体目录的路径,称为“应用沙箱路径”。 + +- 一般情况下,开发者的hdc shell环境等效于系统进程视角,因此“应用沙箱路径”与开发者使用hdc工具调试时看到的真实物理路径不同,其对应关系详见[应用沙箱路径和调试进程视角下的真实物理路径](send-file-to-app-sandbox.md#应用沙箱路径和调试进程视角下的真实物理路径)。 + +- 从实际物理路径推导物理路径与沙箱路径并不是1:1的映射关系,沙箱路径总是少于系统进程视角可见的物理路径。有些调试进程视角下的物理路径在对应的应用沙箱目录是无法找到的,而沙箱路径总是能够找到其对应的物理路径。 + +**图2** 应用沙箱路径(不同权限与角色的进程下可见的文件路径不同)   +![Application sandbox path](figures/application-sandbox-path.png) + +## 应用文件目录与应用文件路径 + +如前文所述,“应用沙箱目录”内分为两类:应用文件目录和系统文件目录。 + +系统文件目录对应用的可见范围由OpenHarmony系统预置,开发者无需关注。 + +在此主要介绍应用文件目录,如下图所示。应用文件目录下某个文件或某个具体目录的路径称为应用文件路径。应用文件目录下的各个文件路径,具备不同的属性和特征。 + +**图3** 应用文件目录结构图   +![Application file directory structure](figures/application-file-directory-structure.png) + +1. 一级目录data/:代表应用文件目录。 + +2. 二级目录storage/:代表本应用持久化文件目录。 + +3. 三级目录el1/、el2/:代表不同文件加密类型。 + - el1,设备级加密区:设备开机后即可访问的数据区。 + - el2,用户级加密区:设备开机后,需要至少一次解锁对应用户的锁屏界面(密码、指纹、人脸等方式或无密码状态)后,才能够访问的加密数据区。
+ 应用如无特殊需要,应将数据存放在el2加密目录下,以尽可能保证数据安全。但是对于某些场景,一些应用文件需要在用户解锁前就可被访问,例如时钟、闹铃、壁纸等,此时应用需要将这些文件存放到设备级加密区(el1)。切换应用文件加密类型目录的方法请参见[获取和修改加密分区](../application-models/application-context-stage.md#获取和修改加密分区)。 + +4. 四级、五级目录: + 通过ApplicationContext可以获取base下的files、cache、preferences、temp、distributedfiles等目录的应用文件路径,应用全局信息可以存放在这些目录下。 + + 通过UIAbilityContext、AbilityStageContext、ExtensionContext可以获取hap级别应用文件路径。HAP信息可以存放在这些目录下,存放在此目录的文件会跟随HAP的卸载而删除,不会影响app级别目录下的文件。在开发态,一个应用包含一个或者多个HAP,详见[Stage模型应用程序包结构](../quick-start/application-package-structure-stage.md)。 + + Context上下文获取及上述应用文件路径的获取,详见[应用上下文Context](../application-models/application-context-stage.md)。 + + > **说明:** + > - 禁止直接使用上图中四级目录之前的目录名组成的路径字符串,否则可能导致后续应用版本因应用文件路径变化导致不兼容问题。 + > + > - 应通过Context属性获取应用文件路径,包括但不限于上图中绿色背景的路径。 + + 应用文件路径具体说明及生命周期如下表所示。 + + **表1** 应用文件路径详细说明 + + | 目录名 | Context属性名称 | 类型 | 说明 | + | -------- | -------- | -------- | -------- | + | bundle | bundleCodeDir | 安装文件路径 | 应用安装后的app的hap资源包所在目录;随应用卸载而清理。 | + | base | NA | 本设备文件路径 | 应用在本设备上存放持久化数据的目录,子目录包含files/、cache/、temp/和haps/;随应用卸载而清理。 | + | database | databaseDir | 数据库路径 | 应用在el1加密条件下存放通过分布式数据库服务操作的文件目录;随应用卸载而清理。 | + | distributedfiles | distributedFilesDir | 分布式文件路径 | 应用在el2加密条件下存放分布式文件的目录,应用将文件放入该目录可分布式跨设备直接访问;随应用卸载而清理。 | + | files | filesDir | 应用通用文件路径 | 应用在本设备内部存储上通用的存放默认长期保存的文件路径;随应用卸载而清理。 | + | cache | cacheDir | 应用缓存文件路径 | 应用在本设备内部存储上用于缓存下载的文件或可重新生成的缓存文件的路径,应用cache目录大小超过配额或者系统空间达到一定条件,自动触发清理该目录下文件;用户通过系统空间管理类应用也可能触发清理该目录。应用需判断文件是否仍存在,决策是否需重新缓存该文件。 | + | preferences | preferencesDir | 应用首选项文件路径 | 应用在本设备内部存储上通过数据库API存储配置类或首选项的目录;随应用卸载而清理。 | + | temp | tempDir | 应用临时文件路径 | 应用在本设备内部存储上仅在应用运行期间产生和需要的文件,应用退出后即清理。 | + + 对于上述各类应用文件路径,常见使用场景如下: + + - 安装文件路径 + 可以用于存储应用的代码资源数据,主要包括应用安装的HAP资源包、可重复使用的库文件以及插件资源等。此路径下存储的代码资源数据可以被用于动态加载。 + - 数据库路径 + 仅用于保存应用的私有数据库数据,主要包括数据库文件等。此路径下仅适用于存储分布式数据库相关文件数据。 + - 分布式文件路径 + 可以用于保存应用分布式场景下的数据,主要包括应用多设备共享文件、应用多设备备份文件、应用多设备群组协助文件。此路径下存储这些数据,使得应用更加适合多设备使用场景。 + - 应用通用文件路径 + 可以用于保存应用的任何私有数据,主要包括用户持久性文件、图片、媒体文件以及日志文件等。此路径下存储这些数据,使得数据保持私有、安全且持久有效。 + - 应用缓存文件路径 + 可以用于保存应用的缓存数据,主要包括离线数据、图片缓存、数据库备份以及临时文件等。此路径下存储的数据可能会被系统自动清理,因此不要存储重要数据。 + - 应用首选项文件路径 + 可以用于保存应用的首选项数据,主要包括应用首选项文件以及配置文件等。此路径下仅适用于存储小量数据。 + - 应用临时文件路径 + 可以用于保存应用的临时生成的数据,主要包括数据库缓存、图片缓存、临时日志文件、以及下载的应用安装包文件等。此路径下存储使用后即可删除的数据。 diff --git a/zh-cn/application-dev/file-management/dev-user-file-manager.md b/zh-cn/application-dev/file-management/dev-user-file-manager.md new file mode 100644 index 0000000000000000000000000000000000000000..2875ae94323847cb2292cadcceefb66af40eea1e --- /dev/null +++ b/zh-cn/application-dev/file-management/dev-user-file-manager.md @@ -0,0 +1,146 @@ +# 开发用户文件管理器(仅对系统应用开放) + +OpenHarmony预置了FileManager文件管理器。系统应用开发者也可以根据需要,按以下指导自行开发文件管理器。 + +## 接口说明 + +开发用户文件管理器的相关API详细介绍请参见[API参考](../reference/apis/js-apis-fileAccess.md)。 + +## 开发步骤 + +1. 权限配置和导入模块。 + 申请ohos.permission.FILE_ACCESS_MANAGER和ohos.permission.GET_BUNDLE_INFO_PRIVILEGED权限,配置方式请参见[访问控制授权申请](../security/accesstoken-guidelines.md)。 + + > **说明:** + > + > ohos.permission.FILE_ACCESS_MANAGER是使用文件访问框架接口的基础权限。 + > + > ohos.permission.GET_BUNDLE_INFO_PRIVILEGED权限可以用于查询系统内当前支持的文件管理服务端应用信息。 + +2. 导入依赖模块。 + + ```ts + import fileAccess from '@ohos.file.fileAccess'; + import fileExtensionInfo from '@ohos.file.fileExtensionInfo'; + ``` + + 其中fileAccess提供了文件基础操作的API,fileExtensionInfo提供了应用开发的关键结构体。 + +3. 查询设备列表。 + 开发者可以获取当前系统所有文件管理服务端管理的设备属性,也可以获取某个文件管理服务端管理的设备属性。应用开发者可以按需过滤设备。 + + 在文件访问框架中,使用RootInfo用于表示设备的属性信息。以下示例可以获取所有设备的RootInfo。 + + ```ts + // 创建连接系统内所有文件管理服务端的helper对象 + let fileAccessHelperAllServer = null; + createFileAccessHelper() { + try { // this.context是EntryAbility传过来的Context + fileAccessHelperAllServer = fileAccess.createFileAccessHelper(this.context); + if (!fileAccessHelperAllServer) { + console.error("createFileAccessHelper interface returns an undefined object"); + } + } catch (error) { + console.error("createFileAccessHelper failed, errCode:" + error.code + ", errMessage:" + error.message); + } + } + async getRoots() { + let rootIterator = null; + let rootInfos = []; + let isDone = false; + try { + rootIterator = await fileAccessHelperAllServer.getRoots(); + if (!rootIterator) { + console.error("getRoots interface returns an undefined object"); + return; + } + while (!isDone) { + let result = rootIterator.next(); + console.info("next result = " + JSON.stringify(result)); + isDone = result.done; + if (!isDone) + rootinfos.push(result.value); + } + } catch (error) { + console.error("getRoots failed, errCode:" + error.code + ", errMessage:" + error.message); + } + } + ``` + +4. 浏览目录。 + 在文件访问框架中,使用FileInfo表示一个文件(目录)的基础信息。开发者可以使用listfile接口遍历下一级所有文件(目录)的迭代器对象;也可以通过scanfile过滤指定目录,获取满足条件的迭代器对象。 + + listfile和scanfile接口当前支持RootInfo对象调用,可用于支撑遍历下一级文件或过滤整个目录树。同时,接口也支持FileInfo对象调用,用于支撑遍历下一级文件或过滤指定目录。 + + ```ts + // 从根目录开始 + let rootInfo = rootinfos[0]; + let fileInfos = []; + let isDone = false; + let filter = {suffix : [".txt", ".jpg", ".xlsx"]}; // 设定过滤条件 + try { + let fileIterator = rootInfo.listFile(); // 遍历设备rootinfos[0]的根目录,返回迭代器对象 + // let fileIterator = rootInfo.scanFile(filter); // 过滤设备rootinfos[0]满足指定条件的文件信息,返回迭代对象 + if (!fileIterator) { + console.error("listFile interface returns an undefined object"); + return; + } + while (!isDone) { + let result = fileIterator.next(); + console.info("next result = " + JSON.stringify(result)); + isDone = result.done; + if (!isDone) + fileInfos.push(result.value); + } + } catch (error) { + console.error("listFile failed, errCode:" + error.code + ", errMessage:" + error.message); + } + + // 从指定的目录开始 + let fileInfoDir = fileInfos[0]; // fileInfoDir 表示某个目录信息 + let subFileInfos = []; + let isDone = false; + let filter = {suffix : [".txt", ".jpg", ".xlsx"]}; // 设定过滤条件 + try { + let fileIterator = fileInfoDir.listFile(); // 遍历特定的目录fileinfo,返回迭代器对象 + // let fileIterator = rootInfo.scanFile(filter); // 过滤特定的目录fileinfo,返回迭代器对象 + if (!fileIterator) { + console.error("listFile interface returns an undefined object"); + return; + } + while (!isDone) { + let result = fileIterator.next(); + console.info("next result = " + JSON.stringify(result)); + isDone = result.done; + if (!isDone) + subfileInfos.push(result.value); + } + } catch (error) { + console.error("listFile failed, errCode:" + error.code + ", errMessage:" + error.message); + } + ``` + +5. 操作文件或目录。 + 开发者可以集成文件访问框架的接口,完成一些用户行为,比如删除文件(目录)、重命名文件(目录)、新建文件(目录)、移动文件(目录)等。以下示例展示了如何创建一个文件,其他接口请参见[API参考](../reference/apis/js-apis-fileAccess.md)。 + + + ```ts + // 以本地设备为例 + // 创建文件 + // 示例代码sourceUri是Download目录的fileinfo中的URI + // 开发者应根据自己实际获取fileinfo的URI进行开发 + let sourceUri = "datashare:///media/file/6"; + let displayName = "file1"; + let fileUri = null; + try { + // fileAccessHelper 参考 fileAccess.createFileAccessHelper 示例代码获取 + fileUri = await fileAccessHelper.createFile(sourceUri, displayName); + if (!fileUri) { + console.error("createFile return undefined object"); + return; + } + console.info("createFile sucess, fileUri: " + JSON.stringify(fileUri)); + } catch (error) { + console.error("createFile failed, errCode:" + error.code + ", errMessage:" + error.message); + }; + ``` diff --git a/zh-cn/application-dev/file-management/distributed-fs-overview.md b/zh-cn/application-dev/file-management/distributed-fs-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..e9507db47ad53fe24e164ed054677adbcff24781 --- /dev/null +++ b/zh-cn/application-dev/file-management/distributed-fs-overview.md @@ -0,0 +1,41 @@ +# 分布式文件系统概述 + +分布式文件系统(hmdfs,Harmony Distributed File System)提供跨设备的文件访问能力,适用于如下场景: + +- 两台设备组网,用户可以利用一台设备上的编辑软件编辑另外一台设备上的文档。 + +- 平板保存的音乐,车载系统直接可见并可播放。 + +- 户外拍摄的照片,回家打开平板直接访问原设备拍摄的照片。 + + +hmdfs在分布式软总线动态组网的基础上,为网络上各个设备结点提供一个全局一致的访问视图,支持开发者通过基础文件系统接口进行读写访问,具有高性能、低延时等优点。 + + +## 分布式文件系统架构 + +![Distributed File System Architecture](figures/distributed-file-system-architecture.png) + +- distributedfile_daemon:主要负责设备上线监听、通过软总线建立链路,并根据分布式的设备安全等级执行不同的数据流转策略。 + +- hmdfs:实现在内核的网络文件系统,包括缓存管理、文件访问、元数据管理和冲突管理等。 + - 缓存管理 + - 设备分布式组网后,hmdfs提供文件的互访能力,但不会主动进行文件数据传输和拷贝。如果应用需要将数据保存到本地,需主动拷贝。 + - hmdfs保证Close-to-Open的一致性,即一端写关闭后,另外一端可以读取到最新数据,不保证文件内容的实时一致性。 + - 数据在远端写入,但是由于网络原因未及时回刷,文件系统会在下次网络接入时回刷本地,但是如果远端已修改则无法回刷。 + - 文件访问 + - 文件访问接口与本地一致([ohos.file.fs](../reference/apis/js-apis-file-fs.md))。 + - 如果文件在本地,则堆叠访问本地文件系统。 + - 如果文件在其他设备,则同步网络访问远端设备文件。 + > **说明:** + > + > symlink:不支持。 + - 元数据管理 + - 分布式组网下,文件一端创建、删除、修改,另一端可以“立即”查看到最新文件,看到速度取决于网络情况。 + - 远端设备离线后,该设备数据将不再在本端设备呈现。但由于设备离线的感知具有延迟,可能会造成部分消息4s超时,因此开发者需要考虑接口的网络超时或一些文件虽然可以看到,但实际设备可能已离线的场景。 + - 冲突处理 + - 本地与远端冲突 ,远端文件被重命名,看到的同名文件是本地同名文件,远端文件被重命名。 + - 远端多个设备冲突,以接入本设备ID为顺序,显示设备ID小的同名文件,其他文件被依次重命名。 + - 如果组网场景,目录树下已经有远端文件,创建同名文件,提示文件已存在。 + - 冲突文件显示_conflict_dev后依次加id,id从1自动递增。 + - 同名目录之间仅融合不存在冲突,文件和远端目录同名冲突,远端目录后缀加_remote_directory。 diff --git a/zh-cn/application-dev/file-management/figures/application-file-directory-structure.png b/zh-cn/application-dev/file-management/figures/application-file-directory-structure.png new file mode 100644 index 0000000000000000000000000000000000000000..9a4b622dbcd94afaaf13a4fc8bf22e9636d1337b Binary files /dev/null and b/zh-cn/application-dev/file-management/figures/application-file-directory-structure.png differ diff --git a/zh-cn/application-dev/file-management/figures/application-sandbox-file-access-relationship.png b/zh-cn/application-dev/file-management/figures/application-sandbox-file-access-relationship.png new file mode 100644 index 0000000000000000000000000000000000000000..cfbbf95d68b132af558f63e204f0d302751ddc44 Binary files /dev/null and b/zh-cn/application-dev/file-management/figures/application-sandbox-file-access-relationship.png differ diff --git a/zh-cn/application-dev/file-management/figures/application-sandbox-path.png b/zh-cn/application-dev/file-management/figures/application-sandbox-path.png new file mode 100644 index 0000000000000000000000000000000000000000..81beec6b260fc52f73680877407a1be1a10c139e Binary files /dev/null and b/zh-cn/application-dev/file-management/figures/application-sandbox-path.png differ diff --git a/zh-cn/application-dev/file-management/figures/distributed-file-system-architecture.png b/zh-cn/application-dev/file-management/figures/distributed-file-system-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..1c431cf3eced3fa9e6a4f29c705d27d95931d43f Binary files /dev/null and b/zh-cn/application-dev/file-management/figures/distributed-file-system-architecture.png differ diff --git a/zh-cn/application-dev/file-management/figures/external-storage-device-management.png b/zh-cn/application-dev/file-management/figures/external-storage-device-management.png new file mode 100644 index 0000000000000000000000000000000000000000..5ed8f5f41b84b3d2d7e1cc85363ff0d2233f61be Binary files /dev/null and b/zh-cn/application-dev/file-management/figures/external-storage-device-management.png differ diff --git a/zh-cn/application-dev/file-management/figures/file-classification-model.png b/zh-cn/application-dev/file-management/figures/file-classification-model.png new file mode 100644 index 0000000000000000000000000000000000000000..b8afd5937382104b948845da31cfe54fde9a8b32 Binary files /dev/null and b/zh-cn/application-dev/file-management/figures/file-classification-model.png differ diff --git a/zh-cn/application-dev/file-management/figures/user-file-access-framework.png b/zh-cn/application-dev/file-management/figures/user-file-access-framework.png new file mode 100644 index 0000000000000000000000000000000000000000..e324f9c15e7182d0a3dbb743b4728982f46d8682 Binary files /dev/null and b/zh-cn/application-dev/file-management/figures/user-file-access-framework.png differ diff --git "a/zh-cn/application-dev/file-management/figures/\345\205\254\345\205\261\346\226\207\344\273\266\346\223\215\344\275\234\345\261\202\346\254\241\345\233\276.png" "b/zh-cn/application-dev/file-management/figures/\345\205\254\345\205\261\346\226\207\344\273\266\346\223\215\344\275\234\345\261\202\346\254\241\345\233\276.png" deleted file mode 100644 index e3b0db7293ac9b2748ba5f4b625556a4e93c2d74..0000000000000000000000000000000000000000 Binary files "a/zh-cn/application-dev/file-management/figures/\345\205\254\345\205\261\346\226\207\344\273\266\346\223\215\344\275\234\345\261\202\346\254\241\345\233\276.png" and /dev/null differ diff --git "a/zh-cn/application-dev/file-management/figures/\345\205\254\345\205\261\346\226\207\344\273\266\350\256\277\351\227\256\346\241\206\346\236\266\346\225\260\346\215\256\346\265\201.png" "b/zh-cn/application-dev/file-management/figures/\345\205\254\345\205\261\346\226\207\344\273\266\350\256\277\351\227\256\346\241\206\346\236\266\346\225\260\346\215\256\346\265\201.png" deleted file mode 100644 index d3c6fd90b59712bf2728208f247d531b919b4b3b..0000000000000000000000000000000000000000 Binary files "a/zh-cn/application-dev/file-management/figures/\345\205\254\345\205\261\346\226\207\344\273\266\350\256\277\351\227\256\346\241\206\346\236\266\346\225\260\346\215\256\346\265\201.png" and /dev/null differ diff --git a/zh-cn/application-dev/file-management/file-access-across-devices.md b/zh-cn/application-dev/file-management/file-access-across-devices.md new file mode 100644 index 0000000000000000000000000000000000000000..8c76f4ed742505d7255c6fe9cf00d7d054104edd --- /dev/null +++ b/zh-cn/application-dev/file-management/file-access-across-devices.md @@ -0,0 +1,64 @@ +# 跨设备文件访问 + + +分布式文件系统为应用提供了跨设备文件访问的能力,开发者在多个设备安装同一应用时,通过[基础文件接口](app-file-access.md),可跨设备读写其他设备该应用分布式文件路径(/data/storage/el2/distributedfiles/)下的文件。例如:多设备数据流转的场景,设备组网互联之后,设备A上的应用可访问设备B同应用分布式路径下的文件,当期望应用文件被其他设备访问时,只需将文件移动到分布式文件路径即可。 + + +## 开发步骤 + +1. 完成分布式组网。 + 首先将需要进行跨设备访问的设备连接到同一局域网中,同帐号认证完成组网。 + +2. 访问跨设备文件。 + 同一应用不同设备之间实现跨设备文件访问,只需要将对应的文件放在应用沙箱的分布式文件路径即可。 + + 设备A上在分布式路径下创建测试文件,并写入内容。示例中的context的获取方式请参见[获取UIAbility的上下文信息](../application-models/uiability-usage.md#获取uiability的上下文信息)。 + + + ```ts + import fs from '@ohos.file.fs'; + + let context = ...; // 获取设备A的UIAbilityContext信息 + let pathDir = context.distributedFilesDir; + // 获取分布式目录的文件路径 + let filePath = pathDir + '/test.txt'; + + try { + // 在分布式目录下创建文件 + let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); + console.info('Succeeded in createing.'); + // 向文件中写入内容 + fs.writeSync(file.fd, 'content'); + // 关闭文件 + fs.closeSync(file.fd); + } catch (err) { + console.error(`Failed to openSync / writeSync / closeSync. Code: ${err.code}, message: ${err.message}`); + } + ``` + + 设备B上在分布式路径下读取测试文件。 + + + ```ts + import fs from '@ohos.file.fs'; + + let context = ...; // 获取设备B的UIAbilityContext信息 + let pathDir = context.distributedFilesDir; + // 获取分布式目录的文件路径 + let filePath = pathDir + '/test.txt'; + + try { + // 打开分布式目录下的文件 + let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE); + // 定义接收读取数据的缓存 + let buffer = new ArrayBuffer(4096); + // 读取文件的内容,返回值是读取到的字节个数 + let num = fs.readSync(file.fd, buffer, { + offset: 0 + }); + // 打印读取到的文件数据 + console.info('read result: ' + String.fromCharCode.apply(null, new Uint8Array(buffer.slice(0, num)))); + } catch (err) { + console.error(`Failed to openSync / readSync. Code: ${err.code}, message: ${err.message}`); + } + ``` diff --git a/zh-cn/application-dev/file-management/file-access-framework-overview.md b/zh-cn/application-dev/file-management/file-access-framework-overview.md deleted file mode 100644 index d67feda3017cccc04aa1570d86343809621164de..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/file-management/file-access-framework-overview.md +++ /dev/null @@ -1,44 +0,0 @@ -# 用户公共文件访问框架概述 - -在搭载OpenHarmony 3.2 (API 9)及更高版本的设备上,应用可以基于FileAccessFramework(简称为FAF)对本地公共文件、分布式设备文件、外部存储设备文件、多用户共享文件进行访问。 - -出于对用户数据的隐私安全考虑,目前此框架仅支持用户通过**文件管理器**和**文件选择器**对文件访问服务端进行操作,包括创建、打开、删除、重命名、移动等。 - -应用卸载,不会影响到用户数据,实际用户数据仍保留在对应设备中。 - -其它系统应用如需要访问本地公共文件,请参考使用[媒体库接口](medialibrary-filepath-guidelines.md)。 - -> **注意:** -> 1:如果应用是系统非管理类应用,比如:系统图库,请优先考虑直接使用媒体库接口,媒体库接口提供了一系列直接操作文件的接口。 -> 2:FAF 接口与媒体库接口原则上不能混用。 - -## FileAccessFramework机制介绍 -FAF依托于OpenHarmony上[ExtensionAbility机制](../application-models/extensionability-overview.md),实现了一套对外提供能力的统一接口。应用可以通过这套接口预览和操作公共文件,实现自己的逻辑。 - -有兴趣的开发者,可以预览我们的[源码仓](https://gitee.com/openharmony/filemanagement_user_file_service),提出您宝贵的意见。 - -基于FAF进行文件操作的全流程,包含以下几个元素,如图所示: - -**图1 公共文件操作层次图** - -![](figures/公共文件操作层次图.png) - -- **文件访问客户端应用** - 需要访问或操作公共文件的应用。通过拉起文件选择器,用户可以在可视化界面上进行文件操作。 -- **文件选择器应用** - 可以让用户访问所有共享数据集的系统应用。通过使用 FAF 的对上接口,完成各种文件操作。 -- **文件访问服务端应用** - 系统内支持将数据集进行共享的服务。目前有[UserFileManager](https://gitee.com/openharmony/multimedia_medialibrary_standard)、ExternalFileManager等。前者管理了本地磁盘、分布式设备的数据集,后者管理了SD卡、U盘等多种外置存储设备的数据集。开发者也可以基于 FAF 的服务端配置,共享自己的数据集。 - -FAF提供的主要功能: -- 可以让用户浏览系统内所有文件服务端应用提供的数据集,而不仅仅是单一应用的数据集。 -- 客户端应用不需要获取FAF的使用权限,直接通过选择器应用操作文件。 -- 支持访问多个临时挂载的设备,比如外置存储卡、分布式设备等。 - -## 数据模型 -FAF 中数据模型主要通过URI、FileInfo、RootInfo 进行传递。详情参考[fileExtension](../reference/apis/js-apis-fileExtensionInfo.md)。文件访问服务端应用可以通过 FileAccessExtensionAbility API,将自身的数据安全的共享出去。 - -**图2 公共文件访问框架数据流** - -![](figures/公共文件访问框架数据流.png) - -注意事项: -- 在FAF中,文件访问客户端和文件访问服务端并不直接交互。只需要具备拉起文件选择器应用的权限即可。 -- 文件选择器应用会为用户提供标准的文档访问界面,即使底层的文件访问服务端相互之间差异很大,一致性也不受影响。 \ No newline at end of file diff --git a/zh-cn/application-dev/file-management/file-management-overview.md b/zh-cn/application-dev/file-management/file-management-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..350f8ae54667c489f23a8ff939819f465c208a2c --- /dev/null +++ b/zh-cn/application-dev/file-management/file-management-overview.md @@ -0,0 +1,24 @@ +# 文件管理概述 + +在操作系统中,存在各种各样的数据,按数据结构可分为: + +- 结构化数据:能够用统一的数据模型加以描述的数据。常见的是各类数据库数据。在应用开发中,对结构化数据的开发活动隶属于数据管理模块。 + +- 非结构化数据:指数据结构不规则或不完整,没有预定义的数据结构/模型,不方便用数据库二维逻辑表来表现的数据。常见的是各类文件,如文档、图片、音频、视频等。在应用开发中,对非结构化数据的开发活动隶属于文件管理模块,将在下文展开介绍。 + +在文件管理模块中,按文件所有者的不同,有如下文件分类模型,其示意图如下面文件分类模型示意图: + +- [应用文件](app-file-overview.md):文件所有者为应用,包括应用安装文件、应用资源文件、应用缓存文件等。 + +- [用户文件](user-file-overview.md):文件所有者为登录到该终端设备的用户,包括用户私有的图片、视频、音频、文档等。 + +- 系统文件:与应用和用户无关的其他文件,包括公共库、设备文件、系统资源文件等。这类文件不需要开发者进行文件管理,本文不展开介绍。 + +按文件系统管理的文件存储位置(数据源位置)的不同,有如下文件系统分类模型: + +- 本地文件系统:提供本地设备或外置存储设备(如U盘、移动硬盘)的文件访问能力。本地文件系统是最基本的文件系统,本文不展开介绍。 + +- [分布式文件系统](distributed-fs-overview.md):提供跨设备的文件访问能力。所谓跨设备,指文件不一定存储在本地设备或外置存储设备,而是通过计算机网络与其他分布式设备相连。 + +**图1** 文件分类模型示意图 +![File classification model](figures/file-classification-model.png) diff --git a/zh-cn/application-dev/file-management/filepicker-guidelines.md b/zh-cn/application-dev/file-management/filepicker-guidelines.md deleted file mode 100644 index d8e64e27301703dfe16623dde201a784a780fc9c..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/file-management/filepicker-guidelines.md +++ /dev/null @@ -1,66 +0,0 @@ -# 文件选择器使用指导 - -文件选择器(FilePicker)是OpenHarmony中预置的系统应用,为用户提供文件选择及保存功能。具体实现可以参考[FilePicker代码仓库](https://gitee.com/openharmony/applications_filepicker)。 - -应用可以通过FilePicker的两种模式实现文件选择和文件保存的功能。 -- choose模式(文件选择):当应用需要选择并上传、发送设备中的文件(包括图片、音视频等媒体资源)时,可以选择该模式。拉起FilePicker的choose模式窗口,系统将弹出弹框供用户选择具体文件。用户通过界面选择目标文件并点击“上传”按钮,应用将接收到FilePicker传回的目标文件uri。 -- save模式(文件保存):当应用需要下载保存文件(包括图片、音视频等媒体资源)时,可以选择该模式。拉起FilePicker的save模式窗口,系统将弹出弹框供用户选择保存文件的目标路径。用户通过界面选择目标路径并点击“保存”按钮,应用将接收到FilePicker传回的已保存文件uri。 - -## 开发指导 - -> **说明:** -> FilePicker仅支持基于Stage模型开发的应用拉起。
-> Stage模型介绍请参考[应用模型解读](../application-models/application-model-description.md)。 - -使用不同的参数调用[AbilityContext.startAbilityForResult(want, options)](../reference/apis/js-apis-inner-application-uiAbilityContext.md#uiabilitycontextstartabilityforresult-1)可以拉起FilePicker不同模式的窗口。 - -开发者需要通过[Want](../reference/apis/js-apis-application-want.md)指定"bundleName"和"abilityName"来拉起FilePicker,具体写法可参见下方的示例代码。 - -同时,开发者还需要设置属性Want.parameters,来指定FilePicker拉起的模式以及文件保存的名称: -- 文件选择:仅需要设置FilePicker拉起的窗口模式为`'startMode': 'choose'`。 -- 文件保存:除了设置FilePicker拉起的窗口模式为`'startMode': 'save'`,还需要设置文件保存名称`'saveFile'`。 - -可以通过设置类型为[StartOptions](../reference/apis/js-apis-app-ability-startOptions.md)的入参options来指定弹出窗口样式,推荐设置为`windowMode: 102`,即自由悬浮形式窗口。 - -> **注意:** -> 1. save模式下,用户保存路径根据保存文件名执行强校验,用户保存路径参考[公共目录路径支持的文件格式](medialibrary-filepath-guidelines.md)。 -> 2. 用户选择目标文件保存路径后,如存在同名文件,FilePicker将弹窗提醒用户是否覆盖原文件。 - -ArkTS语言示例代码如下: -```ts -// 拉起FilePicker选择文件 -globalThis.context.startAbilityForResult( - { - action: "ohos.want.action.OPEN_FILE", - parameters: { - 'startMode': 'choose', //choose or save - } - }, - { windowMode: 102 } -) - -// 拉起FilePicker保存文件 -globalThis.context.startAbilityForResult( - { - action: "ohos.want.action.CREATE_FILE", - parameters: { - 'startMode': 'save', //choose or save - 'saveFile': 'test.jpg', - } - }, - { windowMode: 102 } -) - -// FilePicker返回给startAbilityForResult的数据 -let abilityResult = { - resultCode: resultCode, - want: { - parameters: { - 'startMode': startMode, - 'result': result - } - } -} -globalThis.context.terminateSelfWithResult(abilityResult) -``` - diff --git a/zh-cn/application-dev/file-management/manage-external-storage.md b/zh-cn/application-dev/file-management/manage-external-storage.md new file mode 100644 index 0000000000000000000000000000000000000000..dc53a27fb2f1c07b4e6c4983ac9d6bc9a763f20b --- /dev/null +++ b/zh-cn/application-dev/file-management/manage-external-storage.md @@ -0,0 +1,89 @@ +# 管理外置存储设备(仅对系统应用开放) + +外置存储设备具备可插拔属性,因此系统提供了设备插拔事件的监听及挂载功能,用于管理外置存储设备。 + +外置存储设备的管理由StorageManager和StorageDaemon两个服务完成。StorageDaemon实现底层的的监听挂载等功能;StorageManager则对系统应用提供状态变更通知、查询和管理能力。 + +**图1** 外置存储设备管理示意图   +![External storage device management](figures/external-storage-device-management.png) + +- 插入外卡时,StorageDaemon进程通过netlink监听获取到外卡插入事件,创建对应的磁盘设备以及卷设备,此时,已创建的卷设备状态为卸载状态(UNMOUNTED)。 + +- StorageDaemon进程在创建完卷设备后,会对卷设备进行检查,此时卷状态为检查状态(CHECKING)。 + - 检查成功后,会对卷设备进行挂载,挂载成功后,卷状态更改为挂载状态(MOUNTED),并通知StorageManager发送COMMON_EVENT_VOLUME_MOUNTED广播。 + - 检查失败,则返回卸载状态(UNMOUNTED)。 + +- 当卷设备处于挂载状态时: + - 拔出卷设备,会直接删除相关卷设备信息,并发送COMMON_EVENT_VOLUME_BAD_REMOVAL广播。 + - 当用户选择弹出时,卷状态设备更改为正在弹出状态(EJECTING),并发送COMMON_EVENT_VOLUME_EJECT广播。StorageDaemon进程将卷设备卸载成功后,卷状态更改为卸载状态(UNMOUNTED),并发送COMMON_EVENT_VOLUME_UNMOUNTED广播。 + +- 当卷设备处于卸载状态时,拔出卷设备会删除相关卷设备信息,并发送COMMON_EVENT_VOLUME_REMOVED广播。 + + +## 接口说明 + +外置存储设备管理相关API的详细介绍请参见[API参考](../reference/apis/js-apis-file-volumemanager.md)。 + +各类广播传递的相关参数,请见下表。 + +**表1** 广播传递的参数 + +| 广播类型 | 参数 | +| -------- | -------- | +| usual.event.data.VOLUME_REMOVED | id:卷设备ID
diskId:卷设备所属磁盘设备ID | +| usual.event.data.VOLUME_UNMOUNTED | id:卷设备ID
diskId:卷设备所属磁盘设备ID
volumeState:卷设备状态 | +| usual.event.data.VOLUME_MOUNTED | id:卷设备ID
diskId:卷设备所属磁盘设备ID
volumeState:卷设备状态
fsUuid:卷设备uuid
path:卷设备挂载路径 | +| usual.event.data.VOLUME_BAD_REMOVAL | id:卷设备ID
diskId:卷设备所属磁盘设备ID | +| usual.event.data.VOLUME_EJECT | id:卷设备ID
diskId:卷设备所属磁盘设备ID
volumeState:卷设备状态 | + + +## 开发步骤 + +开发者通过订阅卷设备相关的广播事件来感知外置存储的插入,通过广播传递的信息获取卷设备信息后可以对卷设备进行查询以及管理操作。 + +1. 获取权限。 + 订阅卷设备广播事件需要申请ohos.permission.STORAGE_MANAGER权限,配置方式请参见[配置文件权限声明](../security/accesstoken-guidelines.md#配置文件权限声明)。 + +2. 订阅广播事件。 + 需订阅的事件如下: + + - 卷设备移除:"usual.event.data.VOLUME_REMOVED" + - 卷设备卸载:"usual.event.data.VOLUME_UNMOUNTED" + - 卷设备挂载:"usual.event.data.VOLUME_MOUNTED" + - 卷设备异常移除:"usual.event.data.VOLUME_BAD_REMOVAL" + - 卷设备正在弹出:"usual.event.data.VOLUME_EJECT" + + + ```ts + import CommonEvent from '@ohos.commonEventManager'; + import volumeManager from '@ohos.file.volumeManager'; + + const subscribeInfo = { + events: [ + "usual.event.data.VOLUME_REMOVED", + "usual.event.data.VOLUME_UNMOUNTED", + "usual.event.data.VOLUME_MOUNTED", + "usual.event.data.VOLUME_BAD_REMOVAL", + "usual.event.data.VOLUME_EJECT" + ] + }; + let subscriber = await CommonEvent.createSubscriber(subscribeInfo); + ``` + +3. 收到广播通知后获取卷设备信息。 + + ```ts + CommonEvent.subscribe(subscriber, function (err, data) { + if (data.event === 'usual.event.data.VOLUME_MOUNTED') { + // 开发者可以通过广播传递的卷设备信息来管理卷设备 + let volId = data.parameters.id; + volumeManager.getVolumeById(volId, function(error, vol) { + if (error) { + console.error('volumeManager getVolumeById failed'); + } else { + console.info('volumeManager getVolumeById successfully, the volume state is ' + vol.state); + } + }) + } + }) + ``` diff --git a/zh-cn/application-dev/file-management/medialibrary-album-guidelines.md b/zh-cn/application-dev/file-management/medialibrary-album-guidelines.md deleted file mode 100644 index 0a697df0dc233a9a1500e868bde802aa7371976d..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/file-management/medialibrary-album-guidelines.md +++ /dev/null @@ -1,94 +0,0 @@ -# 相册资源使用指导 - -mediaLibrary提供相册相关的接口,供开发者创建、删除相册,获取相册中的图片资源等。 - -> **说明:** -> -> 在进行功能开发前,请开发者查阅[媒体库开发概述](medialibrary-overview.md),了解如何获取媒体库实例和如何申请媒体库功能开发相关权限。 - -为了保证应用的运行效率,大部分MediaLibrary调用都是异步的,对于异步调用的API均提供了callback和Promise两种方式,以下示例均采用Promise函数,更多方式可以查阅[API参考](../reference/apis/js-apis-medialibrary.md)。 - -## 获取相册中的图片/视频 - -获取相册中的图片、视频有两种方式: - -一是通过[MediaLibrary.getFileAssets](../reference/apis/js-apis-medialibrary.md#getfileassets7-1)指定相册以获取媒体资源,参考[获取指定相册的媒体资源](medialibrary-resource-guidelines.md#指定相册); - -二是通过[Album.getFileAssets](../reference/apis/js-apis-medialibrary.md#getfileassets7-3)使用相册Album实例获取媒体资源,参考[获取相册中的图片或视频](medialibrary-resource-guidelines.md#获取相册中的图片或视频)。 - -## 创建相册 - -通过[MediaLibrary.createAsset](../reference/apis/js-apis-medialibrary.md#createasset8-1)可以创建媒体资源,可以通过创建图片或视频文件时设置的相对路径,创建出相册。相对路径的命名即为相册名称。 - -**前提条件** - -- 获取媒体库mediaLibrary实例。 -- 申请媒体库读写权限“ohos.permission.WRITE_MEDIA”。 - -下面以创建相册myAlbum为例。 - -**开发步骤** - -1. 调用getPublicDirectory获取文件公共路径。 - - 获取文件公共路径的更多指导可参考[获取文件保存的公共目录](medialibrary-filepath-guidelines.md#获取文件保存的公共目录)。 - -2. 调用createAsset新建图片,并设置相对路径为path+'myAlbum/'。 - - 即在创建相册的同时,往里面放了一张图片。 - -```ts -async function example() { - let mediaType = mediaLibrary.MediaType.IMAGE; - let DIR_IMAGE = mediaLibrary.DirectoryType.DIR_IMAGE; - const context = getContext(this); - let media = mediaLibrary.getMediaLibrary(context); - const path = await media.getPublicDirectory(DIR_IMAGE); - //myAlbum为新建文件保存路径,也是新建相册的名称 - media.createAsset(mediaType, 'test.jpg', path + 'myAlbum/', (err, fileAsset) => { - if (fileAsset === undefined) { - console.error('createAlbum failed, message = ' + err); - } else { - console.info('createAlbum successfully, message = ' + JSON.stringify(fileAsset)); - } - }); -} -``` - -## 重命名相册 - -重命名修改的是相册的FileAsset.albumName属性,即相册名称。修改后再通过[Album.commitModify](../reference/apis/js-apis-medialibrary.md#commitmodify8-3)更新到数据库中。 - -**前提条件** - -- 获取媒体库mediaLibrary实例。 -- 申请媒体库读写权限“ohos.permission.WRITE_MEDIA”。 - -下面以重命名相册为“newAlbum“为例。 - -**开发步骤** - -1. 建立检索条件,用于获取目标相册。 -2. 调用getAlbums获取相册列表。 -3. 将相册重命名为“newAlbum“。 -4. 调用Album.commitModify将更新的相册属性修改到数据库中。 - -```ts -async function example() { - let AlbumNoArgsfetchOp = { - selections: '', - selectionArgs: [], - }; - const context = getContext(this); - let media = mediaLibrary.getMediaLibrary(context); - let albumList = await media.getAlbums(AlbumNoArgsfetchOp); - let album = albumList[0]; - album.albumName = 'newAlbum'; - //回调返回空 - album.commitModify().then(() => { - console.info("albumRename successfully"); - }).catch((err) => { - console.error("albumRename failed with error: " + err); - }); -} -``` diff --git a/zh-cn/application-dev/file-management/medialibrary-filepath-guidelines.md b/zh-cn/application-dev/file-management/medialibrary-filepath-guidelines.md deleted file mode 100644 index 74a844f946099633f0389ea383d135a48dbfa328..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/file-management/medialibrary-filepath-guidelines.md +++ /dev/null @@ -1,249 +0,0 @@ -# 文件路径使用指导 - -Openharmony上用户数据统一由媒体库进行管理,用户数据用户数据可以通过mediaLibrary提供的接口能力进行访问和操作。 - -> **说明:** -> -> 在进行功能开发前,请开发者查阅[媒体库开发概述](medialibrary-overview.md),了解如何获取媒体库实例和如何申请媒体库功能开发相关权限。 - -为了保证应用的运行效率,大部分MediaLibrary调用都是异步的,对于异步调用的API均提供了callback和Promise两种方式,以下示例均采用Promise函数,更多方式可以查阅[API参考](../reference/apis/js-apis-medialibrary.md)。 - -## 公共目录路径支持的文件格式 - -在使用文件路径进行开发之前,需要了解各公共目录路径支持的文件格式说明。 -> **注意:**
-> 下表仅表示系统能识别的文件类型,在具体的开发中,开发者需要关注对应接口支持的文件格式。
如image编码功能只支持.jpeg和.webp,解码功能只支持.jpg .png .gif .bmp .webp RAW。 - -| 目录路径 | 目录类型 | 媒体类型 | 说明 | 支持的文件格式 | -| ---------- | ------------- | ------------- | -------------- | ------------------------------------------------------------ | -| Camera/ | DIR_CAMERA | VIDEO amd IMAGE | 相机拍摄图片与录像的存放路径,目录与子目录下可以存放视频,图片类型文件。 | .bmp / .bm / .gif / .jpg /. jpeg / .jpe / .png / .webp / .raw / .svg / .heif / .mp4 / .3gp / .mpg / .mov / .webm / .mkv | -| Videos/ | DIR_VIDEO | VIDEO | 视频专有目录,目录与子目录下只可以存放视频类型文件。| .mp4 / .3gp / .mpg / .mov / .webm / .mkv | -| Pictures/ | DIR_IMAGE | IMAGE | 图片专有目录,目录与子目录下只可以存放图片类型文件。 | .bmp / .bm / .gif / .jpg /. jpeg / .jpe / .png / .webp / .raw / .svg / .heif | -| Audios/ | DIR_AUDIO | AUDIO |音频专有目录,目录与子目录下只可以存放音频类型文件。| .aac/.mp3/.flac/.wav/.ogg | -| Documents/ | DIR_DOCUMENTS | FILE |文档类型目录,目录与子目录下只可以存放音频,图片,视频以外类型文件。| - | -| Download/ | DIR_DOWNLOAD | ALLTYPE |下载文件存放目录,目录与子目录下文件类型不受限制。| - | - -## 获取文件保存的公共目录 - -不同类型的文件会保存到不同的公共目录下,可以通过接口[getPublicDirectory](../reference/apis/js-apis-medialibrary.md#getpublicdirectory8-1)来获取公共目录路径。 - -**前提条件** - -- 获取媒体库mediaLibrary实例。 -- 申请媒体库读权限"ohos.permission.READ_MEDIA"。 - -下面以获取Camera文件保存的公共目录为例。 - -```ts -async function example(){ - const context = getContext(this); - let media = mediaLibrary.getMediaLibrary(context); - let DIR_CAMERA = mediaLibrary.DirectoryType.DIR_CAMERA; - const dicResult = await media.getPublicDirectory(DIR_CAMERA); - if (dicResult == 'Camera/') { - console.info('mediaLibraryTest : getPublicDirectory passed'); - } else { - console.error('mediaLibraryTest : getPublicDirectory failed'); - } -} -``` - -## 沙箱与公共路径间文件的复制 - -OpenHarmony提供应用沙箱机制,增加目录可见性数据访问防线,减少了应用数据和用户隐私信息泄露,建立了更加严格安全的应用沙盒隔离能力。 - -放在公共路径下的文件,用户可以通过系统应用“文件管理”、“图库”访问,但应用沙箱内的文件,只有应用本身可以访问。 - -### 复制文件 - -通过接口[mediaLibrary.FileAsset.open](../reference/apis/js-apis-medialibrary.md#open8-1)可以打开公共路径文件。 - -通过接口[fs.open](../reference/apis/js-apis-file-fs.md#fsopen)可以打开沙箱路径文件,沙箱路径必须通过应用上下文context进行访问。 - -**前提条件** - -- 获取媒体库mediaLibrary实例。 -- 申请媒体库读写权限"ohos.permission.READ_MEDIA, ohos.permission.WRITE_MEDIA"。 -- 除了@ohos.multimedia.mediaLibrary外,还需要导入模块[@ohos.file.fs](../reference/apis/js-apis-file-fs.md)。 -- 测试文件 "testFile.txt" 已创建且有文件内容。 - -**开发步骤** - -1. 调用[context.filesDir](../reference/apis/js-apis-file-fs.md)获取应用沙箱路径。 -2. 调用MediaLibrary.getFileAssets和FetchFileResult.getFirstObject获取公共目录中的FileAsset实例。 -3. 调用fs.open打开沙箱路径文件。 -4. 调用fileAsset.open打开公共路径文件。 -5. 调用[fs.copyfile](../reference/apis/js-apis-file-fs.md#fscopyfile)复制文件。 -6. 调用fileAsset.close和[fs.close](../reference/apis/js-apis-file-fs.md#fsclose)关闭文件。 - -**示例1 将公共路径文件复制到沙箱路径下** - -```ts -async function copyPublic2Sandbox() { - try { - const context = getContext(this); - let media = mediaLibrary.getMediaLibrary(context); - let sandboxDirPath = context.filesDir; - let fileKeyObj = mediaLibrary.FileKey; - let fileAssetFetchOp = { - selections: fileKeyObj.DISPLAY_NAME + '= ?', - selectionArgs: ['testFile.txt'], - }; - let fetchResult = await media.getFileAssets(fileAssetFetchOp); - let fileAsset = await fetchResult.getFirstObject(); - - let fdPub = await fileAsset.open('rw'); - let fdSand = await fs.open(sandboxDirPath + '/testFile.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); - await fs.copyFile(fdPub, fdSand.fd); - - await fileAsset.close(fdPub); - await fs.close(fdSand.fd); - - let content_sand = await fs.readText(sandboxDirPath + '/testFile.txt'); - console.info('content read from sandbox file: ', content_sand) - } catch (err) { - console.info('[demo] copyPublic2Sandbox fail, err: ', err); - } -} -``` - -**示例2 将应用沙箱路径文件复制到公共路径** - -```ts -async function copySandbox2Public() { - const context = getContext(this); - let media = mediaLibrary.getMediaLibrary(context); - let sandboxDirPath = context.filesDir; - - let DIR_DOCUMENTS = mediaLibrary.DirectoryType.DIR_DOCUMENTS; - const publicDirPath = await media.getPublicDirectory(DIR_DOCUMENTS); - try { - let fileAsset = await media.createAsset(mediaLibrary.MediaType.FILE, 'testFile02.txt', publicDirPath); - console.info('createFile successfully, message = ' + fileAsset); - } catch (err) { - console.error('createFile failed, message = ' + err); - } - try { - let fileKeyObj = mediaLibrary.FileKey; - let fileAssetFetchOp = { - selections: fileKeyObj.DISPLAY_NAME + '= ?', - selectionArgs: ['testFile02.txt'], - }; - let fetchResult = await media.getFileAssets(fileAssetFetchOp); - var fileAsset = await fetchResult.getFirstObject(); - } catch (err) { - console.error('file asset get failed, message = ' + err); - } - let fdPub = await fileAsset.open('rw'); - let fdSand = await fs.open(sandboxDirPath + 'testFile.txt', fs.OpenMode.READ_WRITE); - await fs.copyFile(fdSand.fd, fdPub); - await fileAsset.close(fdPub); - await fs.close(fdSand.fd); - let fdPubRead = await fileAsset.open('rw'); - try { - let arrayBuffer = new ArrayBuffer(4096); - await fs.read(fdPubRead, arrayBuffer); - var content_pub = String.fromCharCode(...new Uint8Array(arrayBuffer)); - fileAsset.close(fdPubRead); - } catch (err) { - console.error('read text failed, message = ', err); - } - console.info('content read from public file: ', content_pub); -} -``` - -### 读写文件内容 - -通过[mediaLibrary](../reference/apis/js-apis-medialibrary.md)的接口FileAsset.open和FileAsset.close可以打开和关闭文件。通过[file.fs](../reference/apis/js-apis-file-fs.md)中的接口fs.read和fs.write可以读写文件。 - -**前提条件** - -- 获取媒体库mediaLibrary实例。 -- 申请媒体库读写权限"ohos.permission.READ_MEDIA, ohos.permission.WRITE_MEDIA"。 -- 除了@ohos.multimedia.mediaLibrary外,还需要导入模块[@ohos.file.fs](../reference/apis/js-apis-file-fs.md)。 - -**开发步骤** - -1. 创建用于读写示例的文件。 - -```ts -async function example() { - let mediaType = mediaLibrary.MediaType.FILE; - let DIR_DOCUMENTS = mediaLibrary.DirectoryType.DIR_DOCUMENTS; - const context = getContext(this); - let media = mediaLibrary.getMediaLibrary(context); - const path = await media.getPublicDirectory(DIR_DOCUMENTS); - media.createAsset(mediaType, "testFile.txt", path).then((asset) => { - console.info("createAsset successfully:" + JSON.stringify(asset)); - }).catch((err) => { - console.error("createAsset failed with error: " + err); - }); -} -``` - -2. 使用open打开文件。 - -3. 使用[fs.write](../reference/apis/js-apis-file-fs.md#fswrite)写入文件,以string形式传入写入数据。 - -4. 使用[fs.read](../reference/apis/js-apis-file-fs.md#fsread)读取文件,以 ArrayBuffer 形式保存读取结果。 - -5. 将ArrayBuffer转化为string,以string形式得到文件内容。 - -6. 使用close关闭文件。 - -**示例1 打开现有文件、向文件中写入** - -```ts -async function writeOnlyPromise() { - const context = getContext(this); - let media = mediaLibrary.getMediaLibrary(context); - let fileKeyObj = mediaLibrary.FileKey; - let fileAssetFetchOp = { - selections: fileKeyObj.DISPLAY_NAME + '= ?', - selectionArgs: ['testFile.txt'], - }; - let fetchResult = await media.getFileAssets(fileAssetFetchOp); - let fileAsset = await fetchResult.getFirstObject(); - console.info('fileAssetName: ', fileAsset.displayName); - - try { - let fd = await fileAsset.open('w'); - console.info('file descriptor: ', fd); - await fs.write(fd, "Write file test content."); - await fileAsset.close(fd); - } catch (err) { - console.error('write file failed, message = ', err); - } -} -``` - -**示例2 打开现有文件,读取文件内容** - -```ts -async function readOnlyPromise() { - const context = getContext(this); - let media = mediaLibrary.getMediaLibrary(context); - let fileKeyObj = mediaLibrary.FileKey; - let fileAssetFetchOp = { - selections: fileKeyObj.DISPLAY_NAME + '= ?' , - selectionArgs: ['testFile.txt'], - }; - let fetchResult = await media.getFileAssets(fileAssetFetchOp); - let fileAsset = await fetchResult.getFirstObject(); - console.info('fileAssetName: ', fileAsset.displayName); - - try { - let fd = await fileAsset.open('r'); - let arrayBuffer = new ArrayBuffer(4096); - await fs.read(fd, arrayBuffer); - let fileContent = String.fromCharCode(...new Uint8Array(arrayBuffer)); - globalThis.fileContent = fileContent; - globalThis.fileName = fileAsset.displayName; - console.info('file content: ', fileContent); - await fileAsset.close(fd); - } catch (err) { - console.error('read file failed, message = ', err); - } -} -``` - diff --git a/zh-cn/application-dev/file-management/medialibrary-overview.md b/zh-cn/application-dev/file-management/medialibrary-overview.md deleted file mode 100644 index 5ccc5310d3304afab08d7ac98858b32a93e9bb49..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/file-management/medialibrary-overview.md +++ /dev/null @@ -1,127 +0,0 @@ -# 媒体库开发概述 - -MediaLibrary提供媒体库相关能力,帮助开发者更方便地访问和修改媒体文件,具体分为: - -- [媒体资源(音频、视频、图片文件等)相关](medialibrary-resource-guidelines.md),包括: - - 查询指定媒体资源 - - 获取图片/视频 - - 获取图片/视频缩略图 - - 创建媒体资源 - - 重命名媒体资源 - - 将媒体资源放入回收站 -- [文件路径相关](medialibrary-filepath-guidelines.md),包括: - - 获取文件保存的公共目录 - - 沙箱与公共路径间文件的复制 - - 读写文件内容 -- [相册相关](medialibrary-album-guidelines.md),包括: - - 获取相册中的图片/视频 - - 创建相册 - - 重命名相册 - -> **说明:**
-> 本开发指导基于API Version 9,仅适用于Stage模型。 - -应用需要先获取媒体库实例,才能访问和修改用户等个人媒体数据信息。媒体库涉及用户个人数据信息,所以应用需要向用户申请媒体库读写操作权限才能保证功能的正常运行。在使用媒体库相关接口时如无其他注明则默认在工程代码的pages/index.ets或者其他自创的ets文件中使用 - -开发者在使用MediaLibrary进行功能开发前,请先掌握以下内容: - -- [获取媒体库实例](#获取媒体库实例) -- [申请媒体库功能相关权限](#申请媒体库功能相关权限) - -## 获取媒体库实例 - -应用需要使用应用上下文Context通过接口[getMediaLibrary](../reference/apis/js-apis-medialibrary.md#medialibrarygetmedialibrary8),获取媒体库实例,用于访问和修改用户等个人媒体数据信息(如音频、视频、图片、文档等)。 - -**开发步骤** - -1. 导入mediaLibrary模块以使用媒体库相关接口。 -2. 通过getContext获取应用上下文。 -3. 获取媒体库实例。 - -```ts -import mediaLibrary from '@ohos.multimedia.mediaLibrary'; - -const context = getContext(this); -let media = mediaLibrary.getMediaLibrary(context); -``` - -## 申请媒体库功能相关权限 - -媒体库的读写操作需要相应权限,在申请权限前,请保证符合[权限使用的基本原则](../security/accesstoken-overview.md#权限使用的基本原则)。涉及的权限如下表。 - -| 权限名 | 说明 | 授权方式 | -| ------------------------------ | ------------------------------------------ | ---------- | -| ohos.permission.READ_MEDIA | 允许应用读取用户外部存储中的媒体文件信息。 | user_grant | -| ohos.permission.WRITE_MEDIA | 允许应用读写用户外部存储中的媒体文件信息。 | user_grant | -| ohos.permission.MEDIA_LOCATION | 允许应用访问用户媒体文件中的地理位置信息。 | user_grant | - -以上权限的授权方式均为user_grant(用户授权),即开发者在module.json5文件中配置对应的权限后,需要使用接口[abilityAccessCtrl.requestPermissionsFromUser](../reference/apis/js-apis-abilityAccessCtrl.md#requestpermissionsfromuser9)去校验当前用户是否已授权。如果是,应用可以直接访问/操作目标对象;否则需要弹框向用户申请授权。 - -> **说明:**
即使用户曾经授予权限,应用在调用受此权限保护的接口前,也应该先检查是否有权限。不能把之前授予的状态持久化,因为用户在动态授予后还可以通过“设置”取消应用的权限。 - -**开发步骤** - -1. 在配置文件module.json5中声明权限。在配置文件的“module”标签内,增加“requestPermissions”标签,标签内容请根据实际情况填写。标签说明可参考[访问控制(权限)开发指导](../security/accesstoken-guidelines.md)。 - -```json -{ - "module": { - "requestPermissions": [ - { - "name": "ohos.permission.MEDIA_LOCATION", - "reason": "$string:reason", - "usedScene": { - "abilities": [ - "EntryAbility" - ], - "when": "always" - } - }, - { - "name": "ohos.permission.READ_MEDIA", - "reason": "$string:reason", - "usedScene": { - "abilities": [ - "EntryAbility" - ], - "when": "always" - } - }, - { - "name": "ohos.permission.WRITE_MEDIA", - "reason": "$string:reason", - "usedScene": { - "abilities": [ - "EntryAbility" - ], - "when": "always" - } - } - ] - } -} -``` - -2. 在Ability.ts中onWindowStageCreate里调用requestPermissionsFromUser进行权限校验,可以选择需要动态申请获取的权限自行添加相应代码 - -```ts -import UIAbility from '@ohos.app.ability.UIAbility'; -import abilityAccessCtrl, {Permissions} from '@ohos.abilityAccessCtrl'; - -export default class EntryAbility extends UIAbility { - onWindowStageCreate(windowStage) { - let list : Array = ['ohos.permission.READ_MEDIA', 'ohos.permission.WRITE_MEDIA']; - let permissionRequestResult; - let atManager = abilityAccessCtrl.createAtManager(); - atManager.requestPermissionsFromUser(this.context, list, (err, result) => { - if (err) { - console.error('requestPermissionsFromUserError: ' + JSON.stringify(err)); - } else { - permissionRequestResult = result; - console.info('permissionRequestResult: ' + JSON.stringify(permissionRequestResult)); - } - }); - } -} -``` - diff --git a/zh-cn/application-dev/file-management/medialibrary-resource-guidelines.md b/zh-cn/application-dev/file-management/medialibrary-resource-guidelines.md deleted file mode 100644 index 3d87b249e9d8b785b833f8cbd6f471f97f85b152..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/file-management/medialibrary-resource-guidelines.md +++ /dev/null @@ -1,386 +0,0 @@ -# 媒体资源使用指导 - -应用可以通过mediaLibrary的接口,进行媒体资源(音频、视频、图片文件等)相关操作。 - -> **说明:** -> -> 在进行功能开发前,请开发者查阅[媒体库开发概述](medialibrary-overview.md),了解如何获取媒体库实例和如何申请媒体库功能开发相关权限。 - -为了保证应用的运行效率,大部分MediaLibrary调用都是异步的,对于异步调用的API均提供了callback和Promise两种方式,以下示例均采用Promise函数,更多方式可以查阅[API参考](../reference/apis/js-apis-medialibrary.md)。 - -## 获取媒体资源 - -开发者可以根据特定的条件查询媒体资源,如指定类型、指定日期、指定相册等。 - -应用通过调用[MediaLibrary.getFileAssets](../reference/apis/js-apis-medialibrary.md#getfileassets7-1)获取媒体资源,并传入MediaFetchOptions对象指定检索条件。MediaFetchOptions.selections为检索条件,使用FileKey中的枚举值作为检索条件的列名;MediaFetchOptions.selectionArgs对应selections中检索条件列的值;除此之外,可以使用order(结果排序方式)、uri(文件URI)、networkId(注册设备网络ID)作为检索条件。 - -如果只想获取某个位置的对象(如第一个、最后一个、指定索引等),可以通过[FetchFileResult](../reference/apis/js-apis-medialibrary.md#fetchfileresult7)中的接口获取对应位置的媒体资源对象。本小节均采用getNextObject循环获取检索结果中的所有媒体资源。 - -**前提条件** - -- 获取媒体库mediaLibrary实例。 -- 申请媒体库读权限“ohos.permission.READ_MEDIA”。 - -### 指定媒体类型 - -下面以查询图片类型的媒体资源为例。 - -**开发步骤** - -selections: FileKey.MEDIA_TYPE,根据媒体类型检索。 - -selectionArgs: MediaType.IMAGE,媒体类型为图片。 - -```ts -async function example() { - let fileKeyObj = mediaLibrary.FileKey; - let fileType = mediaLibrary.MediaType.IMAGE; - let option = { - selections: fileKeyObj.MEDIA_TYPE + '= ?', - selectionArgs: [fileType.toString()], - }; - const context = getContext(this); - let media = mediaLibrary.getMediaLibrary(context); - const fetchFileResult = await media.getFileAssets(option); - fetchFileResult.getFirstObject().then(async (fileAsset) => { - console.log('getFirstObject.displayName : ' + fileAsset.displayName); - for (let i = 1; i < fetchFileResult.getCount(); i++) { - let fileAsset = await fetchFileResult.getNextObject(); - console.info('fileAsset.displayName ' + i + ': ' + fileAsset.displayName); - } - }).catch((err) => { - console.error('Failed to get first object: ' + err); - }); -} -``` - -### 指定日期 - -下面以查询指定添加日期至今的所有媒体资源为例。实际开发中可以设置添加日期、修改日期、拍摄日期。 - -selections: FileKey.DATE_ADDED,根据文件添加日期检索。 - -selectionArgs:2022-8-5,具体添加时间的字符串。 - -```ts -async function example() { - let fileKeyObj = mediaLibrary.FileKey; - let option = { - selections: fileKeyObj.DATE_ADDED + '> ?', - selectionArgs: ['2022-8-5'], - }; - const context = getContext(this); - let media = mediaLibrary.getMediaLibrary(context); - const fetchFileResult = await media.getFileAssets(option); - fetchFileResult.getFirstObject().then(async (fileAsset) => { - console.info('getFirstObject.displayName : ' + fileAsset.displayName); - for (let i = 1; i < fetchFileResult.getCount(); i++) { - let fileAsset = await fetchFileResult.getNextObject(); - console.info('fileAsset.displayName ' + i + ': ' + fileAsset.displayName); - } - }).catch((err) => { - console.error('Failed to get first object: ' + err); - }); -} -``` - -### 按指定顺序排列 - -下面以查询图片并按文件添加日期降序排列为例。实际开发中可以设置升序(ASC)和降序(DESC)。 - -order: FileKey.DATE_ADDED,根据文件添加日期排序;并设置排列顺序为DESC降序。 - -```ts -async function example() { - let fileKeyObj = mediaLibrary.FileKey; - let fileType = mediaLibrary.MediaType.IMAGE; - let option = { - selections: fileKeyObj.MEDIA_TYPE + '= ?', - selectionArgs: [fileType.toString()], - order: fileKeyObj.DATE_ADDED + " DESC", - }; - const context = getContext(this); - let media = mediaLibrary.getMediaLibrary(context); - const fetchFileResult = await media.getFileAssets(option); - fetchFileResult.getFirstObject().then(async (fileAsset) => { - console.info('getFirstObject.displayName : ' + fileAsset.displayName); - for (let i = 1; i < fetchFileResult.getCount(); i++) { - let fileAsset = await fetchFileResult.getNextObject(); - console.info('fileAsset.displayName ' + i + ': ' + fileAsset.displayName); - } - }).catch((err) => { - console.error('Failed to get first object: ' + err); - }); -} -``` - -### 指定相册 - -下面以指定相册myAlbum为例。 - -selections: FileKey.ALBUM_NAME,根据相册名称检索。 - -selectionArgs:'myAlbum',具体相册名称。 - -```ts -async function example() { - let fileKeyObj = mediaLibrary.FileKey; - let option = { - selections: fileKeyObj.ALBUM_NAME + '= ?', - selectionArgs: ['myAlbum'], - }; - const context = getContext(this); - let media = mediaLibrary.getMediaLibrary(context); - const fetchFileResult = await media.getFileAssets(option); - if (albumList.length > 0) { - fetchFileResult.getFirstObject().then((album) => { - console.info('getFirstObject.displayName : ' + album.albumName); - }).catch((err) => { - console.error('Failed to get first object: ' + err); - }); - } else { - console.info('getAlbum list is: 0'); - } -} -``` - -## 获取相册中的图片或视频 - -获取相册的媒体资源有两种方式,一是通过[MediaLibrary.getFileAssets](../reference/apis/js-apis-medialibrary.md#getfileassets7-1)指定相册以获取媒体资源,参考[获取指定相册的媒体资源](#指定相册);二是通过[Album.getFileAssets](../reference/apis/js-apis-medialibrary.md#getfileassets7-3)使用相册Album实例获取媒体资源。 - -**前提条件** - -- 获取媒体库mediaLibrary实例。 -- 申请媒体库读权限“ohos.permission.READ_MEDIA”。 - -**开发步骤** - -下面以获取相册名称为“新建相册1”的视频为例。 - -1. 建立检索条件,用于获取目的相册实例。 - -```ts -let fileKeyObj = mediaLibrary.FileKey; -let AlbumNoArgsFetchOp = { - selections: fileKeyObj.ALBUM_NAME + '= ?', - selectionArgs: ['新建相册1'] -} -``` - -2. 建立检索条件,用于获取目的相册下的视频资源。 - -```ts -let fileKeyObj = mediaLibrary.FileKey; -let videoType = mediaLibrary.MediaType.VIDEO; -let videoFetchOp = { - selections: fileKeyObj.MEDIA_TYPE + '= ?', - selectionArgs: [videoType.toString()], -} -``` - -3. 通过Album.getFileAssets获取对应的资源。 - -以下为**完整示例**。 - -```ts -async function getCameraImagePromise() { - const context = getContext(this); - let media = mediaLibrary.getMediaLibrary(context); - let fileKeyObj = mediaLibrary.FileKey; - let videoType = mediaLibrary.MediaType.VIDEO; - let videoFetchOp = { - selections: fileKeyObj.MEDIA_TYPE + '= ?', - selectionArgs: [videoType.toString()], - } - let AlbumNoArgsFetchOp = { - selections: fileKeyObj.ALBUM_NAME + '= ?', - selectionArgs: ['新建相册1'] - } - - let albumList = await media.getAlbums(AlbumNoArgsFetchOp); - if (albumList.length > 0) { - const album = albumList[0]; - let fetchFileResult = await album.getFileAssets(videoFetchOp); - let count = fetchFileResult.getCount(); - console.info("get mediaLibrary VIDEO number", count); - } else { - console.info('getAlbum list is: 0'); - } -} -``` - -## 获取图片/视频缩略图 - -通过接口[FileAsset.getThumbnail](../reference/apis/js-apis-medialibrary.md#getthumbnail8-2),传入缩略图尺寸,可以获取图片/视频缩略图。缩略图常用于UI界面展示。 - -**前提条件** - -- 获取媒体库mediaLibrary实例。 -- 申请媒体库读权限“ohos.permission.READ_MEDIA”。 - -### 获取某张图片的缩略图 - -当需要在相册展示图片/视频、编辑预览,应用需要获取某张图片的缩略图。 - -下面以获取相册第一张图片的缩略图为例,缩略图尺寸为720*720。 - -**开发步骤** - -1. 建立检索条件,用于获取目的相册下的图片资源。 -2. 调用getFileAssets获取目标图片资源。 -3. 调用getFirstObject获取第一张图片。 -4. 调用getThumbnail获取相册中图片的缩略图。 - -```ts -async function getFirstThumbnailPromise() { - const context = getContext(this); - let media = mediaLibrary.getMediaLibrary(context); - let fileKeyObj = mediaLibrary.FileKey; - let imageType = mediaLibrary.MediaType.IMAGE; - let imagesFetchOp = { - selections: fileKeyObj.MEDIA_TYPE + '= ?', - selectionArgs: [imageType.toString()], - } - - let size = { width: 720, height: 720 }; - const fetchFileResult = await media.getFileAssets(imagesFetchOp); - if (fetchFileResult === undefined) { - console.error("get image failed with error"); - return; - } else { - const asset = await fetchFileResult.getFirstObject(); - asset.getThumbnail(size).then((pixelMap) => { - pixelMap.getImageInfo().then((info) => { - console.info('get Thumbnail info: ' + "width: " + info.size.width + " height: " + info.size.height); - }).catch((err) => { - console.error("getImageInfo failed with error: " + err); - }); - }).catch((err) => { - console.error("getImageInfo failed with error: " + err); - }); - } -} -``` - -## 创建媒体资源 - -通过接口[MediaLibrary.createAsset](../reference/apis/js-apis-medialibrary.md#createasset8-1)可以创建媒体资源。 - -**前提条件** - -- 获取媒体库mediaLibrary实例。 -- 申请媒体库读写权限“ohos.permission.WRITE_MEDIA”。 -- [获取公共目录路径](medialibrary-filepath-guidelines.md)。 - -下面以创建文件类型(MediaType.FILE)的文件为例。 - -```ts -async function example() { - let mediaType = mediaLibrary.MediaType.FILE; - let DIR_DOCUMENTS = mediaLibrary.DirectoryType.DIR_DOCUMENTS; - const context = getContext(this); - let media = mediaLibrary.getMediaLibrary(context); - const path = await media.getPublicDirectory(DIR_DOCUMENTS); - media.createAsset(mediaType, "testFile.text", path).then((asset) => { - console.info("createAsset successfully:"+ JSON.stringify(asset)); - }).catch((err) => { - console.error("createAsset failed with error: " + err); - }); -} -``` - -## 将文件放入回收站 - -通过[FileAsset.trash](../reference/apis/js-apis-medialibrary.md#trash8)可以将文件放入回收站。 - -放入回收站的文件将会保存30天,在此期间,开发者可以将trash的入参isTrash设置为false将其恢复为正常文件;应用用户也可以通过系统应用“文件管理”或“图库”恢复文件。 - -**前提条件** - -- 获取媒体库mediaLibrary实例。 -- 申请媒体库读写权限“ohos.permission.WRITE_MEDIA”。 - -下面以将文件检索结果中第一个文件放入回收站为例。 - -**开发步骤** - -1. 建立检索条件,用于获取目的相册下的图片资源。 -2. 调用getFileAssets获取目标图片资源。 -3. 调用getFirstObject获取第一张图片,即要放入回收站的图片对象。 -4. 调用trash将文件放入回收站。 - -```ts -async function example() { - let fileKeyObj = mediaLibrary.FileKey; - let fileType = mediaLibrary.MediaType.FILE; - let option = { - selections: fileKeyObj.MEDIA_TYPE + '= ?', - selectionArgs: [fileType.toString()], - }; - const context = getContext(this); - let media = mediaLibrary.getMediaLibrary(context); - const fetchFileResult = await media.getFileAssets(option); - let asset = await fetchFileResult.getFirstObject(); - if (asset === undefined) { - console.error('asset not exist'); - return; - } - //回调为空 - asset.trash(true).then(() => { - console.info("trash successfully"); - }).catch((err) => { - console.error("trash failed with error: " + err); - }); -} -``` - -## 重命名媒体资源 - -重命名修改的是文件的FileAsset.displayName属性,即文件的显示文件名,包含文件后缀。 - -修改后再通过[FileAsset.commitModify](../reference/apis/js-apis-medialibrary.md#commitmodify8-1)更新到数据库中。 - -在重命名文件之前,需要先获取文件对象,可以通过[FetchFileResult](../reference/apis/js-apis-medialibrary.md#fetchfileresult7)中的接口获取对应位置的文件。 - -**前提条件** - -- 获取媒体库mediaLibrary实例。 -- 申请媒体库读写权限“ohos.permission.WRITE_MEDIA”。 - -下面以将文件检索结果中第一个文件重命名为“newImage.jpg”为例。 - -**开发步骤** - -1. 建立检索条件,用于获取目的相册下的图片资源。 -2. 调用getFileAssets获取目标图片资源。 -3. 调用getFirstObject获取第一张图片,即要重命名的图片对象。 -4. 将图片重命名为“newImage.jpg“。 -5. 调用FileAsset.commitModify将更新的图片属性修改到数据库中。 - -```ts -async function example() { - let fileKeyObj = mediaLibrary.FileKey; - let fileType = mediaLibrary.MediaType.IMAGE; - let option = { - selections: fileKeyObj.MEDIA_TYPE + '= ?', - selectionArgs: [fileType.toString()], - }; - const context = getContext(this); - let media = mediaLibrary.getMediaLibrary(context); - const fetchFileResult = await media.getFileAssets(option); - let asset = await fetchFileResult.getFirstObject(); - if (asset === undefined) { - console.error('asset not exist'); - return; - } - asset.displayName = 'newImage.jpg'; - //回调为空 - asset.commitModify((err) => { - if (err) { - console.error('fileRename Failed '); - return; - } - console.info('fileRename successful.'); - }); -} -``` diff --git a/zh-cn/application-dev/file-management/public_sys-resources/icon-caution.gif b/zh-cn/application-dev/file-management/public_sys-resources/icon-caution.gif new file mode 100644 index 0000000000000000000000000000000000000000..6e90d7cfc2193e39e10bb58c38d01a23f045d571 Binary files /dev/null and b/zh-cn/application-dev/file-management/public_sys-resources/icon-caution.gif differ diff --git a/zh-cn/application-dev/file-management/public_sys-resources/icon-danger.gif b/zh-cn/application-dev/file-management/public_sys-resources/icon-danger.gif new file mode 100644 index 0000000000000000000000000000000000000000..6e90d7cfc2193e39e10bb58c38d01a23f045d571 Binary files /dev/null and b/zh-cn/application-dev/file-management/public_sys-resources/icon-danger.gif differ diff --git a/zh-cn/application-dev/file-management/public_sys-resources/icon-note.gif b/zh-cn/application-dev/file-management/public_sys-resources/icon-note.gif new file mode 100644 index 0000000000000000000000000000000000000000..6314297e45c1de184204098efd4814d6dc8b1cda Binary files /dev/null and b/zh-cn/application-dev/file-management/public_sys-resources/icon-note.gif differ diff --git a/zh-cn/application-dev/file-management/public_sys-resources/icon-notice.gif b/zh-cn/application-dev/file-management/public_sys-resources/icon-notice.gif new file mode 100644 index 0000000000000000000000000000000000000000..86024f61b691400bea99e5b1f506d9d9aef36e27 Binary files /dev/null and b/zh-cn/application-dev/file-management/public_sys-resources/icon-notice.gif differ diff --git a/zh-cn/application-dev/file-management/public_sys-resources/icon-tip.gif b/zh-cn/application-dev/file-management/public_sys-resources/icon-tip.gif new file mode 100644 index 0000000000000000000000000000000000000000..93aa72053b510e456b149f36a0972703ea9999b7 Binary files /dev/null and b/zh-cn/application-dev/file-management/public_sys-resources/icon-tip.gif differ diff --git a/zh-cn/application-dev/file-management/public_sys-resources/icon-warning.gif b/zh-cn/application-dev/file-management/public_sys-resources/icon-warning.gif new file mode 100644 index 0000000000000000000000000000000000000000..6e90d7cfc2193e39e10bb58c38d01a23f045d571 Binary files /dev/null and b/zh-cn/application-dev/file-management/public_sys-resources/icon-warning.gif differ diff --git a/zh-cn/application-dev/file-management/save-user-file.md b/zh-cn/application-dev/file-management/save-user-file.md new file mode 100644 index 0000000000000000000000000000000000000000..72c3ae1d13d6372205381691527d644d166b22f3 --- /dev/null +++ b/zh-cn/application-dev/file-management/save-user-file.md @@ -0,0 +1,107 @@ +# 保存用户文件 + +在从网络下载文件到本地、或将已有用户文件另存为新的文件路径等场景下,需要使用FilePicker提供的保存用户文件的能力。 + +对音频、图片、视频、文档类文件的保存操作类似,均通过调用对应picker的save()接口并传入对应的saveOptions来实现。 + + +## 保存图片或视频类文件 + +1. 导入选择器模块。 + + ```ts + import picker from '@ohos.file.picker'; + ``` + +2. 创建图库保存选项实例。 + + ```ts + const photoSaveOptions = new picker.PhotoSaveOptions(); // 创建文件管理器保存选项实例 + photoSaveOptions.newFileNames = ["PhotoViewPicker01.jpg"]; // 保存文件名(可选) + ``` + +3. 创建图库选择器实例,调用[save()](../reference/apis/js-apis-file-picker.md#save)接口拉起FilePicker界面进行文件保存。 + 用户选择目标文件夹,用户选择与文件类型相对应的文件夹,即可完成文件保存操作。保存成功后,返回保存文档的URI。 + + ```ts + const photoViewPicker = new picker.PhotoViewPicker(); + photoViewPicker.save(photoSaveOptions) + .then(async (photoSaveResult) => { + let uri = photoSaveResult[0]; + // 获取到到图片或者视频文件的URI后进行文件读取等操作 + }) + .catch((err) => { + console.error(`Invoke documentPicker.select failed, code is ${err.code}, message is ${err.message}`); + }) + ``` + + +## 保存文档类文件 + +1. 导入选择器模块。 + + ```ts + import picker from '@ohos.file.picker'; + ``` + +2. 创建文档保存选项实例。 + + ```ts + const documentSaveOptions = new picker.DocumentSaveOptions(); // 创建文件管理器选项实例 + documentSaveOptions.newFileNames = ["DocumentViewPicker01.txt"]; // 保存文件名(可选) + ``` + +3. 创建文档选择器实例。调用[save()](../reference/apis/js-apis-file-picker.md#save-3)接口拉起FilePicker界面进行文件保存。 + 用户选择目标文件夹,用户选择与文件类型相对应的文件夹,即可完成文件保存操作。保存成功后,返回保存文档的URI。 + + > **说明:** + > + > 目前DocumentSelectOptions不支持参数配置,默认可以选择所有类型的用户文件。 + + + ```ts + const documentViewPicker = new picker.DocumentViewPicker(); // 创建文件选择器实例 + documentViewPicker.save(documentSaveOptions) + .then(async (documentSaveResult) => { + let uri = documentSaveResult[0]; + // 例如,可以根据获取的URI进行文件写入等操作 + }) + .catch((err) => { + console.error(`Invoke documentPicker.save failed, code is ${err.code}, message is ${err.message}`); + }) + ``` + + +## 保存音频类文件 + +1. 导入选择器模块。 + + ```ts + import picker from '@ohos.file.picker'; + ``` + +2. 创建音频保存选项实例。 + + ```ts + const audioSaveOptions = new picker.AudioSaveOptions(); // 创建文件管理器选项实例 + audioSaveOptions.newFileNames = ['AudioViewPicker01.mp3']; // 保存文件名(可选) + ``` + +3. 创建音频选择器实例。调用[save()](../reference/apis/js-apis-file-picker.md#save-6)接口拉起FilePicker界面进行文件保存。 + 用户选择目标文件夹,用户选择与文件类型相对应的文件夹,即可完成文件保存操作。保存成功后,返回保存文档的URI。 + > **说明:** + > + > 目前AudioSelectOptions不支持参数配置,默认可以选择所有类型的用户文件。 + + + ```ts + const audioViewPicker = new picker.AudioViewPicker(); + audioViewPicker.save(audioSaveOptions) + .then((audioSelectResult) => { + let uri = audioSelectResult[0]; + // 获取到到音频文件的URI后进行文件读取等操作 + }) + .catch((err) => { + console.error(`Invoke audioPicker.select failed, code is ${err.code}, message is ${err.message}`); + }) + ``` diff --git a/zh-cn/application-dev/file-management/select-user-file.md b/zh-cn/application-dev/file-management/select-user-file.md new file mode 100644 index 0000000000000000000000000000000000000000..85169ccb70466e9f604445b72a78dd37297f1b5c --- /dev/null +++ b/zh-cn/application-dev/file-management/select-user-file.md @@ -0,0 +1,120 @@ +# 选择用户文件 + +终端用户有时需要分享、保存一些图片、视频等用户文件,开发者需要在应用中支持此类使用场景。此时,开发者可以使用OpenHarmony系统预置的[文件选择器(FilePicker)](../reference/apis/js-apis-file-picker.md),实现用户文件选择及保存能力。 + + +根据用户文件的常见类型,文件选择器(FilePicker)分别提供以下接口: + + +- [PhotoViewPicker](../reference/apis/js-apis-file-picker.md#photoviewpicker):适用于图片或视频类文件的选择与保存。 + +- [DocumentViewPicker](../reference/apis/js-apis-file-picker.md#documentviewpicker):适用于文档类文件的选择与保存。 + +- [AudioViewPicker](../reference/apis/js-apis-file-picker.md#audioviewpicker):适用于音频类文件的选择与保存。 + +## 选择图片或视频类文件 + +1. 导入选择器模块。 + + ```ts + import picker from '@ohos.file.picker'; + ``` + +2. 创建图库选择选项实例。 + + ```ts + const photoSelectOptions = new picker.PhotoSelectOptions(); + ``` + +3. 选择媒体文件类型和选择媒体文件的最大数目。 + 以下示例以图片选择为例,媒体文件类型请参见[PhotoViewMIMETypes](../reference/apis/js-apis-file-picker.md#photoviewmimetypes)。 + + + ```ts + photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE; // 过滤选择媒体文件类型为IMAGE + photoSelectOptions.maxSelectNumber = 5; // 选择媒体文件的最大数目 + ``` + +4. 创建图库选择器实例,调用[select()](../reference/apis/js-apis-file-picker.md#select)接口拉起FilePicker界面进行文件选择。 + 文件选择成功后,返回[PhotoSelectResult](../reference/apis/js-apis-file-picker.md#photoselectresult)结果集,可以根据结果集中URI进行文件读取等操作。 + + + ```ts + const photoPicker = new picker.PhotoViewPicker(); + photoPicker.select(photoSelectOptions) + .then(async (photoSelectResult) => { + let uri = photoSelectResult.photoUris[0]; + // 获取到到图片或者视频文件的URI后进行文件读取等操作 + }) + .catch((err) => { + console.error(`Invoke documentPicker.select failed, code is ${err.code}, message is ${err.message}`); + }) + ``` + +## 选择文档类文件 + +1. 导入选择器模块。 + + ```ts + import picker from '@ohos.file.picker'; + ``` + +2. 创建文档选择选项实例。 + + ```ts + const documentSelectOptions = new picker.DocumentSelectOptions(); + ``` + +3. 创建文档选择器实例。调用[select()](../reference/apis/js-apis-file-picker.md#select-3)接口拉起FilePicker界面进行文件选择。 + 文件选择成功后,返回被选中文档的URI结果集。开发者可以根据结果集中URI做进一步的处理。 + > **说明:** + > + > 目前DocumentSelectOptions不支持参数配置,默认可以选择所有类型的用户文件。 + + + ```ts + const documentViewPicker = new picker.DocumentViewPicker(); // 创建文件选择器实例 + documentViewPicker.select(documentSelectOptions) + .then((documentSelectResult) => { + let uri = documentSelectResult[0]; + // 获取到到文档文件的URI后进行文件读取等操作 + }) + .catch((err) => { + console.error(`Invoke documentPicker.select failed, code is ${err.code}, message is ${err.message}`); + }) + ``` + +## 选择音频类文件 + +1. 导入选择器模块。 + + ```ts + import picker from '@ohos.file.picker'; + ``` + +2. 创建音频选择选项实例。 + + ```ts + const audioSelectOptions = new picker.AudioSelectOptions(); + ``` + +3. 创建音频选择器实例。调用[select()](../reference/apis/js-apis-file-picker.md#select-6)接口拉起FilePicker界面进行文件选择。 + 文件选择成功后,返回被选中音频的URI结果集。开发者可以根据结果集中URI做进一步的处理。 + + 例如通过[文件管理接口](../reference/apis/js-apis-file-fs.md)根据URI拿到音频资源的文件句柄(FD),再配合媒体服务实现音频播放的开发,具体请参考[音频播放开发指导](../media/audio-playback-overview.md)。 + > **说明:** + > + > 目前AudioSelectOptions不支持参数配置,默认可以选择所有类型的用户文件。 + + + ```ts + const audioViewPicker = new picker.AudioViewPicker(); + audioViewPicker.select(audioSelectOptions) + .then(audioSelectResult => { + let uri = audioSelectOptions[0]; + // 获取到到音频文件的URI后进行文件读取等操作 + }) + .catch((err) => { + console.error(`Invoke audioPicker.select failed, code is ${err.code}, message is ${err.message}`); + }) + ``` diff --git a/zh-cn/application-dev/file-management/send-file-to-app-sandbox.md b/zh-cn/application-dev/file-management/send-file-to-app-sandbox.md new file mode 100644 index 0000000000000000000000000000000000000000..7d9c1e8ef69be67b2f9cdbd25470ca4316e4dfe2 --- /dev/null +++ b/zh-cn/application-dev/file-management/send-file-to-app-sandbox.md @@ -0,0 +1,46 @@ +# 向应用沙箱推送文件 + +开发者在应用开发调试时,可能需要向应用沙箱下推送一些文件以期望在应用内访问或测试,此时有两种方式: + +1. 可以通过DevEco Studio向应用安装路径中放入目标文件,详见[应用安装资源访问](../quick-start/resource-categories-and-access.md#资源访问)。 + +2. 在具备设备环境时,可以使用另一种更为灵活的方式,通过hdc工具来向设备中应用沙箱路径推送文件。即本文介绍的内容。 + +但是hdc shell看到的调试进程下的文件路径与应用视角的应用沙箱路径不同,开发者需要先了解如下路径映射关系。 + +## 应用沙箱路径和调试进程视角下的真实物理路径 + +在应用沙箱路径下读写文件,经过映射转换,实际读写的是在hdc进程视角下看到真实物理路径中的应用文件,其对应关系如下表所示。 + +**表1** 应用沙箱路径与真实物理路径对应关系 + +| 应用沙箱路径 | 调试进程(hdc)视角下的实际路径 | 说明 | +| -------- | -------- | -------- | +| /data/storage/el1/bundle | /data/app/el1/bundle/public/<PACKAGENAME> | 应用安装包目录 | +| /data/storage/el1/base | /data/app/el1/<USERID>/base/<PACKAGENAME> | 应用el1级别加密数据目录 | +| /data/storage/el2/base | /data/app/el2/<USERID>/base/<PACKAGENAME> | 应用el2级别加密数据目录 | +| /data/storage/el1/database | /data/app/el1/<USERID>/database/<PACKAGENAME> | 应用el1级别加密数据库目录 | +| /data/storage/el2/database | /data/app/el2/<USERID>/database/<PACKAGENAME> | 应用el2级别加密数据库目录 | +| /data/storage/el2/distributedfiles | /mnt/hmdfs/<USERID>/account/merge_view/data/<PACKAGENAME> | 应用el2加密级别有帐号分布式数据融合目录 | + +## 开发示例 + +以应用包com.ohos.example为例,如果是在example的应用沙箱路径“/data/storage/el1/bundle”下读写文件,从上表可知,对应的真实物理路径为“/data/app/el1/bundle/public/<PACKAGENAME>”,即“/data/app/el1/bundle/public/com.ohos.example”。 + +推送命令示例如下: + +``` +hdc file send ${待推送文件的本地路径} /data/app/el1/bundle/public/com.ohos.example/ +``` + +## 切换应用沙箱视角 + +在调试过程中,如果权限不对或文件不存在,开发者需要从调试进程视角切换为应用视角,以便直观分析权限及文件目录问题。视角切换命令如下: + +``` +hdc shell // 进入shell +ps -ef|grep [hapName] // 通过ps命令找到对应应用的pid +nsenter -t [hapPid] -m /bin/sh // 通过上一步找到的应用pid进入对应应用的沙箱环境中 +``` + +执行完成后,即切换到了应用视角,该视角下的目录路径为应用沙箱路径,可以去排查沙箱路径相关问题。 diff --git a/zh-cn/application-dev/file-management/set-security-label.md b/zh-cn/application-dev/file-management/set-security-label.md new file mode 100644 index 0000000000000000000000000000000000000000..e2448bebb298661d8997540066381fbc40268a75 --- /dev/null +++ b/zh-cn/application-dev/file-management/set-security-label.md @@ -0,0 +1,41 @@ +# 设置分布式文件数据等级 + +不同设备本身的安全能力差异较大,一些小的嵌入式设备安全能力远弱于平板等设备类型。用户或者应用不同的文件数据有不同安全诉求,例如个人的健康信息和银行卡信息等不期望被弱设备读取。因此,OpenHarmony提供一套完整的数据分级、设备分级标准,并针对不同设备制定不同的数据流转策略。 + + +## 接口说明 + +API详细介绍请参见[ohos.file.securityLabel](../reference/apis/js-apis-file-securityLabel.md)。 + +**表1** 设置文件数据等级 + +| 接口名 | 功能 | 接口类型 | 支持同步 | 支持异步 | +| -------- | -------- | -------- | -------- | -------- | +| setSecurityLabel | 设置文件安全标签 | 方法 | √ | √ | +| getSecurityLabel | 获取文件安全标签 | 方法 | √ | √ | + +> **须知:** +> 1. 对于不满足安全等级的文件,跨设备仍然可以看到该文件,但是无权限打开访问该文件。 +> +> 2. 分布式文件系统的数据等级默认为S3,应用可以主动设置文件的安全等级。 + +## 开发示例 + +获取通用文件沙箱路径,并设置数据等级标签。示例中的context的获取方式请参见[获取UIAbility的上下文信息](../application-models/uiability-usage.md#获取uiability的上下文信息)。 + + +```ts +import securityLabel from '@ohos.file.securityLabel'; + +// 获取需要设备数据等级的文件沙箱路径 +let context = ...; // 获取UIAbilityContext信息 +let pathDir = context.filesDir; +let filePath = pathDir + '/test.txt'; + +// 设置文件的数据等级为s0 +securityLabel.setSecurityLabel(filePath, 's0').then(() => { + console.info('Succeeded in setSecurityLabeling.'); +}).catch((err) => { + console.error(`Failed to setSecurityLabel. Code: ${err.code}, message: ${err.message}`); +}); +``` diff --git a/zh-cn/application-dev/file-management/share-app-file.md b/zh-cn/application-dev/file-management/share-app-file.md new file mode 100644 index 0000000000000000000000000000000000000000..03631f61f54e2a13525ba6de72797634145d59a4 --- /dev/null +++ b/zh-cn/application-dev/file-management/share-app-file.md @@ -0,0 +1,147 @@ +# 应用文件分享 + +应用文件分享是应用之间通过分享URI(Uniform Resource Identifier)或文件描述符FD(File Descriptor)的方式,进行文件共享的过程。由于FD分享的文件关闭FD后,无法再打开分享文件,因此不推荐使用,本文重点介绍URI分享方式。 + +- 基于URI分享方式,应用可分享单个文件,通过[ohos.app.ability.wantConstant](../reference/apis/js-apis-app-ability-wantConstant.md#wantconstantflags)的wantConstant.Flags接口以只读或读写权限授权给其他应用。应用可通过[ohos.file.fs](../reference/apis/js-apis-file-fs.md#fsopen)的open()接口打开URI,并进行读写操作。当前OpenHarmony API 9仅支持临时授权,分享给其他应用的文件在被分享应用退出时权限被收回。 + +- 基于FD分享方式,应用可分享单个文件,通过ohos.file.fs的open接口以指定权限授权给其他应用。应用从Want中解析拿到FD后可通过ohos.file.fs的读写接口对文件进行读写。 + +开发者可以使用相关接口,[分享文件给其他应用](#分享文件给其他应用)或[使用其他应用分享的文件](#使用其他应用分享的文件)。 + +## 文件URI规范 + +文件URI的格式为: + + 格式为file://<bundleName>/<path>/\#networkid=<networkid> +- file:文件URI的标志。 + +- bundleName:该文件资源的属主。 + +- path:文件资源在应用沙箱中的路径。 + +- networkid:为可选项,用于分布式文件系统标志该文件资源所归属的设备;当不需要区分文件位置时,该选项可不填写。 + +## 分享文件给其他应用 + +在分享文件给其他应用前,开发者需要先[获取应用文件路径](../application-models/application-context-stage.md#获取应用开发路径)。 + +1. 获取文件在应用沙箱中的路径,并转换为文件URI。 + + ```ts + import UIAbility from '@ohos.app.ability.UIAbility'; + import fileuri from '@ohos.file.fileuri'; + import window from '@ohos.window'; + + export default class EntryAbility extends UIAbility { + onWindowStageCreate(windowStage: window.WindowStage) { + // 获取文件的沙箱路径 + let pathInSandbox = this.context.filesDir + "/test.txt"; + // 将沙箱路径转换为uri + let uri = fileuri.getUriFromPath(pathInSandbox); + // 获取的uri为"file://com.example.demo/data/storage/el2/base/files/test.txt" + } + } + ``` + +2. 设置获取文件的权限以及选择要分享的应用。 + 分享文件给其他应用需要使用[startAbility()](../reference/apis/js-apis-inner-application-uiAbilityContext.md#uiabilitycontextstartability)接口,将获取到的URI填充在want的参数uri中,标注URI的文件类型,type字段可参考[Want属性](../reference/apis/js-apis-app-ability-want.md#属性),并通过设置want的flag来设置对应的读写权限,action字段配置为wantConstant.Action.ACTION_SEND_DATA表示进行应用文件分享,开发示例如下。 + + > **说明:** + > + > 写权限分享时,同时授予读权限。 + + ```ts + import fileuri from '@ohos.file.fileuri'; + import window from '@ohos.window'; + import wantConstant from '@ohos.app.ability.wantConstant'; + import UIAbility from '@ohos.app.ability.UIAbility'; + + export default class EntryAbility extends UIAbility { + onWindowStageCreate(windowStage: window.WindowStage) { + // 获取文件沙箱路径 + let filePath = this.context.filesDir + '/test.txt'; + // 将沙箱路径转换为uri + let uri = fileuri.getUriFromPath(filePath); + let want = { + // 配置被分享文件的读写权限,例如对被分享应用进行读写授权 + flags: wantConstant.Flags.FLAG_AUTH_WRITE_URI_PERMISSION | wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION, + // 配置分享应用的隐式拉起规则 + action: 'ohos.want.action.sendData', + uri: uri, + type: 'text/plain' + } + this.context.startAbility(want) + .then(() => { + console.info('Invoke getCurrentBundleStats succeeded.'); + }) + .catch((err) => { + console.error(`Invoke startAbility failed, code is ${err.code}, message is ${err.message}`); + }); + } + + ... + } + ``` + +## 使用其他应用分享的文件 + +被分享应用需要在[module.json5配置文件](../quick-start/module-configuration-file.md)的actions标签的值配置为"ohos.want.action.sendData",表示接收应用分享文件,配置uris字段,表示接收URI的类型,即只接收其他应用分享该类型的URI,如下表示本应用只接收scheme为file,类型为txt的文件,示例如下。 + +```json +{ + "module": { + ... + "abilities": [ + { + ... + "skills": [ + { + ... + "actions": [ + "ohos.want.action.sendData" + ], + "uris": [ + { + "scheme": "file", + "type": "text/plain" + } + ] + } + ] + } + ] + } +} +``` + +被分享方的UIAbility被启动后,可以在其[onCreate()](../reference/apis/js-apis-app-ability-uiAbility.md#uiabilityoncreate)或者[onNewWant()](../reference/apis/js-apis-app-ability-uiAbility.md#uiabilityonnewwant)回调中获取传入的Want参数信息。 + +通过接口want的参数获取分享文件的URI,获取文件URI后通过fs.open()接口打开文件,获取对应的file对象后,可对文件进行读写操作。 + + +```ts +// xxx.ets +import fs from '@ohos.file.fs'; + +function getShareFile() { + try { + let want = ...; // 获取分享方传递过来的want信息 + + // 从want信息中获取uri字段 + let uri = want.uri; + if (uri == null || uri == undefined) { + console.info('uri is invalid'); + return; + } + try { + // 根据需要对被分享文件的URI进行相应操作。例如读写的方式打开URI获取file对象 + let file = fs.openSync(uri, fs.OpenMode.READ_WRITE); + console.info('open file successfully!'); + } catch (error) { + console.error(`Invoke openSync failed, code is ${error.code}, message is ${error.message}`); + } + } catch (error) { + console.error(`Invoke openSync failed, code is ${error.code}, message is ${error.message}`); + } +} +``` diff --git a/zh-cn/application-dev/file-management/user-file-overview.md b/zh-cn/application-dev/file-management/user-file-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..3a76a3d264ddda4b5616e16808807344f63d6f98 --- /dev/null +++ b/zh-cn/application-dev/file-management/user-file-overview.md @@ -0,0 +1,60 @@ +# 用户文件概述 + +用户文件:文件所有者为登录到该终端设备的用户,包括用户私有的图片、视频、音频、文档等。 + +1. 用户文件存放在用户目录下,归属于该设备上登录的用户。 + +2. 用户文件存储位置主要分为[内置存储](#内置存储)、[外置存储](#外置存储)。 + +3. 应用对用户文件的创建、访问、删除等行为,需要提前获取用户授权,或由用户操作完成。 + +OpenHarmony提供[用户文件访问框架](#用户文件访问框架),用于开发者访问和管理用户文件,将在下文详细介绍。 + +## 用户文件存储位置 + +### 内置存储 + +内置存储,是指用户文件存储在终端设备的内部存储设备(空间)上。内置存储设备无法被移除。内置存储的用户文件主要有: + +- 用户特有的文件:这部分文件归属于登录该设备的用户,不同用户登录后,仅可看到该用户自己的文件。 + 按照这些文件的特征/属性,以及用户/应用的使用习惯,可分为: + - 图片/视频类媒体文件 + 所具有的特征包括拍摄时间、地点、旋转角度、文件宽高等信息,以媒体文件的形式存储在系统中,通常是以所有文件、相册的形式对外呈现,不会展示其在系统中存储的具体位置。 + + - 音频类媒体文件 + 所具有的特征包括所属专辑、音频创作者、持续时间等信息,以媒体文件的形式存储在系统中,通常会以所有文件、专辑、作家等形式对外部呈现,不会展示其在系统中存储的具体位置。 + + - 其他文件(统称为文档类文件) + 以普通文件的形式存储在系统中,该类文件既包括普通的文本文件、压缩文件等,又包括以普通文件形式存储的图片/视频、音频文件,该类文件通常是以目录树的形式对外展示。 + +- 多用户共享的文件:用户可以通过将文件放在共享文件区,实现多个用户之间文件的共享访问。 + 共享文件区的文件,也是以普通文件的形式存储在系统中,以目录树的形式对外展示。 + +### 外置存储 + +外置存储,是指用户文件存储在外置可插拔设备上(如SD卡、U盘等)。外置存储设备上的文件,和内置存储设备共享区文件一样,可以被所有登录到系统中的用户看到。 + +外置存储设备具备可插拔属性,因此系统提供了设备插拔事件的监听及挂载功能,用于管理外置存储设备,具体可参考[管理外置存储设备(仅对系统应用开放)](manage-external-storage.md)。 + +外置存储设备上的文件,全部以普通文件的形式呈现,和内置存储设备上的文档类文件一样,采用目录树的形式对外展示。 + +## 用户文件访问框架 + +用户文件访问框架(File Access Framework)是一套提供给开发者访问和管理用户文件的基础框架。该框架依托于OpenHarmony的ExtensionAbility组件机制,提供了一套统一访问用户文件的方法和接口。 + +**图1** 用户文件访问框架示意图   +![User file access framework](figures/user-file-access-framework.png) + +- 各类系统应用或三方应用(即图中的文件访问客户端)若需访问用户文件,如选择一张照片或保存多个文档等,可以通过拉起“文件选择器应用”来实现。 + +- OpenHarmony系统预置了文件选择器应用FilePicker和文件管理器应用FileManager。 + - FilePicker:系统预置应用,提供文件访问客户端选择和保存文件的能力,且不需要配置任何权限。FilePicker的使用指导请参见[选择用户文件](select-user-file.md)。 + - FileManager:系统预置应用,终端用户可通过系统文件管理器实现查看文件、修改文件、删除文件(目录)、重命名文件(目录)、移动文件(目录)、创建文件(目录)等操作。 + + 对于系统应用开发者,还可以按需开发自己的文件选择器或文件管理器应用。其中,选择器功能是管理器的子集,本文目前提供了管理器的开发指导,请参见[开发用户文件管理器(仅对系统应用开放)](dev-user-file-manager.md)。 + +- File Access Framework(用户文件访问框架)的主要功能模块如下: + - File Access Helper:提供给文件管理器和文件选择器访问用户文件的API接口。 + - File Access ExtensionAbility:提供文件访问框架能力,由内卡文件管理服务UserFileManager和外卡文件管理服务ExternalFileManager组成,实现对应的文件访问功能。 + - UserFileManager:内卡文件管理服务,基于File Access ExtensionAbility框架实现,用于管理内置存储设备上的文件。 + - ExternalFileManager:外卡文件管理服务,基于File Access ExtensionAbility框架实现,用于管理外置存储设备上的文件。 diff --git a/zh-cn/application-dev/media/Readme-CN.md b/zh-cn/application-dev/media/Readme-CN.md index 7829383ad0546b76ef570725e8a248b72d3afb57..1f2d6d43b8c3686432e4b428f11bc023092e8b62 100755 --- a/zh-cn/application-dev/media/Readme-CN.md +++ b/zh-cn/application-dev/media/Readme-CN.md @@ -1,29 +1,60 @@ # 媒体 +- [媒体应用开发概述](media-application-overview.md) - 音视频 - - [音频开发概述](audio-overview.md) - - [音频渲染开发指导](audio-renderer.md) - - [音频流管理开发指导](audio-stream-manager.md) - - [音频采集开发指导](audio-capturer.md) - - [OpenSL ES播放开发指导](opensles-playback.md) - - [OpenSL ES录音开发指导](opensles-capture.md) - - [音频焦点模式开发指导](audio-interruptmode.md) - - [音量管理开发指导](audio-volume-manager.md) - - [路由、设备管理开发指导](audio-routing-manager.md) - - [音视频播放器开发指导(推荐使用)](avplayer-playback.md) - - [音视频录制开发指导(推荐使用)](avrecorder.md) - - [音频播放开发指导(待停用)](audio-playback.md) - - [音频录制开发指导(待停用)](audio-recorder.md) - - [视频播放开发指导(待停用)](video-playback.md) - - [视频录制开发指导(待停用)](video-recorder.md) - -- 媒体会话 - - [AVSession开发概述](avsession-overview.md) - - [AVSession开发指导](avsession-guidelines.md) - + - [音视频概述](av-overview.md) + - [AVPlayer和AVRecorder](avplayer-avrecorder-overview.md) + - 音频播放 + - [音频播放开发概述](audio-playback-overview.md) + - [使用AVPlayer开发音频播放功能](using-avplayer-for-playback.md) + - [使用AudioRenderer开发音频播放功能](using-audiorenderer-for-playback.md) + - [使用OpenSL ES开发音频播放功能](using-opensl-es-for-playback.md) + - [使用TonePlayer开发音频播放功能(仅对系统应用开放)](using-toneplayer-for-playback.md) + - [多音频播放的并发策略](audio-playback-concurrency.md) + - [播放音量管理](volume-management.md) + - [音频播放流管理](audio-playback-stream-management.md) + - [音频输出设备管理](audio-output-device-management.md) + - [分布式音频播放(仅对系统应用开放)](distributed-audio-playback.md) + - 音频录制 + - [音频录制开发概述](audio-recording-overview.md) + - [使用AVRecorder开发音频录制功能](using-avrecorder-for-recording.md) + - [使用AudioCapturer开发音频录制功能](using-audiocapturer-for-recording.md) + - [使用OpenSLES开发音频录制功能](using-opensl-es-for-recording.md) + - [管理麦克风](mic-management.md) + - [音频录制流管理](audio-recording-stream-management.md) + - [音频输入设备管理](audio-input-device-management.md) + - 音频通话 + - [音频通话开发概述](audio-call-overview.md) + - [开发音频通话功能](audio-call-development.md) + - [视频播放](video-playback.md) + - [视频录制](video-recording.md) +- 媒体会话(仅对系统应用开放) + - [媒体会话概述](avsession-overview.md) + - 本地媒体会话 + - [本地媒体会话概述](local-avsession-overview.md) + - [媒体会话提供方](using-avsession-developer.md) + - [媒体会话控制方](using-avsession-controller.md) + - 分布式媒体会话 + - [分布式媒体会话概述](distributed-avsession-overview.md) + - [使用分布式媒体会话](using-distributed-avsession.md) +- 相机(仅对系统应用开放) + - [相机开发概述](camera-overview.md) + - 相机开发指导 + - [开发准备](camera-preparation.md) + - [设备输入](camera-device-input.md) + - [会话管理](camera-session-management.md) + - [预览](camera-preview.md) + - [拍照](camera-shooting.md) + - [录像](camera-recording.md) + - [元数据](camera-metadata.md) + - 相机最佳实践 + - [拍照实现方案](camera-shooting-case.md) + - [录像实现方案](camera-recording-case.md) - 图片 - - [图片开发指导](image.md) - -- 相机 - - [相机开发指导](camera.md) - - [分布式相机开发指导](remote-camera.md) \ No newline at end of file + - [图片开发概述](image-overview.md) + - [图片解码](image-decoding.md) + - 图片处理 + - [图像变换](image-transformation.md) + - [位图操作](image-pixelmap-operation.md) + - [图片编码](image-encoding.md) + - [图片工具](image-tool.md) diff --git a/zh-cn/application-dev/media/audio-call-development.md b/zh-cn/application-dev/media/audio-call-development.md new file mode 100644 index 0000000000000000000000000000000000000000..fc3fa68da197a5fc1d72b6009328c20e07b65ad6 --- /dev/null +++ b/zh-cn/application-dev/media/audio-call-development.md @@ -0,0 +1,259 @@ +# 开发音频通话功能 + +在音频通话场景下,音频输出(播放对端声音)和音频输入(录制本端声音)会同时进行,应用可以通过使用AudioRenderer来实现音频输出,通过使用AudioCapturer来实现音频输入,同时使用AudioRenderer和AudioCapturer即可实现音频通话功能。 + +在音频通话开始和结束时,应用可以自行检查当前的[音频场景模式](audio-call-overview.md#音频场景模式)和[铃声模式](audio-call-overview.md#铃声模式),以便采取合适的音频管理及提示策略。 + +以下代码示范了同时使用AudioRenderer和AudioCapturer实现音频通话功能的基本过程,其中未包含音频通话数据的传输过程,实际开发中,需要将网络传输来的对端通话数据解码播放,此处仅以读取音频文件的数据代替;同时需要将本端录制的通话数据编码打包,通过网络发送给对端,此处仅以将数据写入音频文件代替。 + +## 使用AudioRenderer播放对端的通话声音 + + 该过程与[使用AudioRenderer开发音频播放功能](using-audiorenderer-for-playback.md)过程相似,关键区别在于audioRenderInfo参数和音频数据来源。audioRenderInfo参数中,音频内容类型需设置为语音,CONTENT_TYPE_SPEECH,音频流使用类型需设置为语音通信,STREAM_USAGE_VOICE_COMMUNICATION。 + +```ts +import audio from '@ohos.multimedia.audio'; +import fs from '@ohos.file.fs'; +const TAG = 'VoiceCallDemoForAudioRenderer'; +// 与使用AudioRenderer开发音频播放功能过程相似,关键区别在于audioRenderInfo参数和音频数据来源 +export default class VoiceCallDemoForAudioRenderer { + private renderModel = undefined; + private audioStreamInfo = { + samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, // 采样率 + channels: audio.AudioChannel.CHANNEL_2, // 通道数 + sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式 + encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式 + } + private audioRendererInfo = { + // 需使用通话场景相应的参数 + content: audio.ContentType.CONTENT_TYPE_SPEECH, // 音频内容类型:语音 + usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION, // 音频流使用类型:语音通信 + rendererFlags: 0 // 音频渲染器标志:默认为0即可 + } + private audioRendererOptions = { + streamInfo: this.audioStreamInfo, + rendererInfo: this.audioRendererInfo + } + // 初始化,创建实例,设置监听事件 + init() { + audio.createAudioRenderer(this.audioRendererOptions, (err, renderer) => { // 创建AudioRenderer实例 + if (!err) { + console.info(`${TAG}: creating AudioRenderer success`); + this.renderModel = renderer; + this.renderModel.on('stateChange', (state) => { // 设置监听事件,当转换到指定的状态时触发回调 + if (state == 1) { + console.info('audio renderer state is: STATE_PREPARED'); + } + if (state == 2) { + console.info('audio renderer state is: STATE_RUNNING'); + } + }); + this.renderModel.on('markReach', 1000, (position) => { // 订阅markReach事件,当渲染的帧数达到1000帧时触发回调 + if (position == 1000) { + console.info('ON Triggered successfully'); + } + }); + } else { + console.info(`${TAG}: creating AudioRenderer failed, error: ${err.message}`); + } + }); + } + // 开始一次音频渲染 + async start() { + let stateGroup = [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED]; + if (stateGroup.indexOf(this.renderModel.state) === -1) { // 当且仅当状态为prepared、paused和stopped之一时才能启动渲染 + console.error(TAG + 'start failed'); + return; + } + await this.renderModel.start(); // 启动渲染 + const bufferSize = await this.renderModel.getBufferSize(); + // 此处仅以读取音频文件的数据举例,实际音频通话开发中,需要读取的是通话对端传输来的音频数据 + let context = getContext(this); + let path = context.filesDir; + + const filePath = path + '/voice_call_data.wav'; // 沙箱路径,实际路径为/data/storage/el2/base/haps/entry/files/voice_call_data.wav + let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY); + let stat = await fs.stat(filePath); + let buf = new ArrayBuffer(bufferSize); + let len = stat.size % bufferSize === 0 ? Math.floor(stat.size / bufferSize) : Math.floor(stat.size / bufferSize + 1); + for (let i = 0; i < len; i++) { + let options = { + offset: i * bufferSize, + length: bufferSize + }; + let readsize = await fs.read(file.fd, buf, options); + // buf是要写入缓冲区的音频数据,在调用AudioRenderer.write()方法前可以进行音频数据的预处理,实现个性化的音频播放功能,AudioRenderer会读出写入缓冲区的音频数据进行渲染 + let writeSize = await new Promise((resolve, reject) => { + this.renderModel.write(buf, (err, writeSize) => { + if (err) { + reject(err); + } else { + resolve(writeSize); + } + }); + }); + if (this.renderModel.state === audio.AudioState.STATE_RELEASED) { // 如果渲染器状态为released,停止渲染 + fs.close(file); + await this.renderModel.stop(); + } + if (this.renderModel.state === audio.AudioState.STATE_RUNNING) { + if (i === len - 1) { // 如果音频文件已经被读取完,停止渲染 + fs.close(file); + await this.renderModel.stop(); + } + } + } + } + // 暂停渲染 + async pause() { + // 只有渲染器状态为running的时候才能暂停 + if (this.renderModel.state !== audio.AudioState.STATE_RUNNING) { + console.info('Renderer is not running'); + return; + } + await this.renderModel.pause(); // 暂停渲染 + if (this.renderModel.state === audio.AudioState.STATE_PAUSED) { + console.info('Renderer is paused.'); + } else { + console.error('Pausing renderer failed.'); + } + } + // 停止渲染 + async stop() { + // 只有渲染器状态为running或paused的时候才可以停止 + if (this.renderModel.state !== audio.AudioState.STATE_RUNNING && this.renderModel.state !== audio.AudioState.STATE_PAUSED) { + console.info('Renderer is not running or paused.'); + return; + } + await this.renderModel.stop(); // 停止渲染 + if (this.renderModel.state === audio.AudioState.STATE_STOPPED) { + console.info('Renderer stopped.'); + } else { + console.error('Stopping renderer failed.'); + } + } + // 销毁实例,释放资源 + async release() { + // 渲染器状态不是released状态,才能release + if (this.renderModel.state === audio.AudioState.STATE_RELEASED) { + console.info('Renderer already released'); + return; + } + await this.renderModel.release(); // 释放资源 + if (this.renderModel.state === audio.AudioState.STATE_RELEASED) { + console.info('Renderer released'); + } else { + console.error('Renderer release failed.'); + } + } +} +``` + +## 使用AudioCapturer录制本端的通话声音 + + 该过程与[使用AudioCapturer开发音频录制功能](using-audiocapturer-for-recording.md)过程相似,关键区别在于audioCapturerInfo参数和音频数据流向。audioCapturerInfo参数中音源类型需设置为语音通话,SOURCE_TYPE_VOICE_COMMUNICATION。 + +```ts +import audio from '@ohos.multimedia.audio'; +import fs from '@ohos.file.fs'; +const TAG = 'VoiceCallDemoForAudioCapturer'; +// 与使用AudioCapturer开发音频录制功能过程相似,关键区别在于audioCapturerInfo参数和音频数据流向 +export default class VoiceCallDemoForAudioCapturer { + private audioCapturer = undefined; + private audioStreamInfo = { + samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100, // 采样率 + channels: audio.AudioChannel.CHANNEL_1, // 通道数 + sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式 + encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式 + } + private audioCapturerInfo = { + // 需使用通话场景相应的参数 + source: audio.SourceType.SOURCE_TYPE_VOICE_COMMUNICATION, // 音源类型:语音通话 + capturerFlags: 0 // 音频采集器标志:默认为0即可 + } + private audioCapturerOptions = { + streamInfo: this.audioStreamInfo, + capturerInfo: this.audioCapturerInfo + } + // 初始化,创建实例,设置监听事件 + init() { + audio.createAudioCapturer(this.audioCapturerOptions, (err, capturer) => { // 创建AudioCapturer实例 + if (err) { + console.error(`Invoke createAudioCapturer failed, code is ${err.code}, message is ${err.message}`); + return; + } + console.info(`${TAG}: create AudioCapturer success`); + this.audioCapturer = capturer; + this.audioCapturer.on('markReach', 1000, (position) => { // 订阅markReach事件,当采集的帧数达到1000时触发回调 + if (position === 1000) { + console.info('ON Triggered successfully'); + } + }); + this.audioCapturer.on('periodReach', 2000, (position) => { // 订阅periodReach事件,当采集的帧数达到2000时触发回调 + if (position === 2000) { + console.info('ON Triggered successfully'); + } + }); + }); + } + // 开始一次音频采集 + async start() { + let stateGroup = [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED]; + if (stateGroup.indexOf(this.audioCapturer.state) === -1) { // 当且仅当状态为prepared、paused和stopped之一时才能启动采集 + console.error(`${TAG}: start failed`); + return; + } + await this.audioCapturer.start(); // 启动采集 + // 此处仅以将音频数据写入文件举例,实际音频通话开发中,需要将本端采集的音频数据编码打包,通过网络发送给通话对端 + let context = getContext(this); + const path = context.filesDir + '/voice_call_data.wav'; // 采集到的音频文件存储路径 + let file = fs.openSync(path, 0o2 | 0o100); // 如果文件不存在则创建文件 + let fd = file.fd; + let numBuffersToCapture = 150; // 循环写入150次 + let count = 0; + while (numBuffersToCapture) { + let bufferSize = await this.audioCapturer.getBufferSize(); + let buffer = await this.audioCapturer.read(bufferSize, true); + let options = { + offset: count * bufferSize, + length: bufferSize + }; + if (buffer === undefined) { + console.error(`${TAG}: read buffer failed`); + } else { + let number = fs.writeSync(fd, buffer, options); + console.info(`${TAG}: write date: ${number}`); + } + numBuffersToCapture--; + count++; + } + } + // 停止采集 + async stop() { + // 只有采集器状态为STATE_RUNNING或STATE_PAUSED的时候才可以停止 + if (this.audioCapturer.state !== audio.AudioState.STATE_RUNNING && this.audioCapturer.state !== audio.AudioState.STATE_PAUSED) { + console.info('Capturer is not running or paused'); + return; + } + await this.audioCapturer.stop(); // 停止采集 + if (this.audioCapturer.state === audio.AudioState.STATE_STOPPED) { + console.info('Capturer stopped'); + } else { + console.error('Capturer stop failed'); + } + } + // 销毁实例,释放资源 + async release() { + // 采集器状态不是STATE_RELEASED或STATE_NEW状态,才能release + if (this.audioCapturer.state === audio.AudioState.STATE_RELEASED || this.audioCapturer.state === audio.AudioState.STATE_NEW) { + console.info('Capturer already released'); + return; + } + await this.audioCapturer.release(); // 释放资源 + if (this.audioCapturer.state == audio.AudioState.STATE_RELEASED) { + console.info('Capturer released'); + } else { + console.error('Capturer release failed'); + } + } +} +``` diff --git a/zh-cn/application-dev/media/audio-call-overview.md b/zh-cn/application-dev/media/audio-call-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..93eaace16a24a560c7ff09c0d990e81d8f99489e --- /dev/null +++ b/zh-cn/application-dev/media/audio-call-overview.md @@ -0,0 +1,51 @@ +# 音频通话开发概述 + +常用的音频通话模式包括VOIP通话和蜂窝通话。 + +- VOIP通话: + VOIP(Voice over Internet Protocol)通话是指基于互联网协议(IP)进行通讯的一种语音通话技术。VOIP通话会将通话信息打包成数据包,通过网络进行传输,因此VOIP通话对网络要求较高,通话质量与网络连接速度紧密相关。 + +- 蜂窝通话(仅对系统应用开放) + 蜂窝通话是指传统的电话功能,由运营商提供服务,目前仅对系统应用开放,未向第三方应用提供开发接口。 + +在开发音频通话相关功能时,开发者可以根据实际情况,检查当前的[音频场景模式](#音频场景模式)和[铃声模式](#铃声模式),以使用相应的音频处理策略。 + +## 音频场景模式 + +应用使用音频通话相关功能时,系统会切换至与通话相关的音频场景模式([AudioScene](../reference/apis/js-apis-audio.md#audioscene8)),当前预置了多种音频场景,包括响铃、通话、语音聊天等,在不同的场景下,系统会采用不同的策略来处理音频。 + +如在蜂窝通话场景中会更注重人声的清晰度。系统会使用3A算法对音频数据进行预处理,抑制通话回声,消除背景噪音,调整音量范围,从而达到清晰人声的效果。3A算法,指声学回声消除(Acoustic Echo Cancelling, AEC)、背景噪声抑制(Acitve Noise Control, ANC)、自动增益控制(Automatic Gain Control, AGC)三种音频处理算法。 + +当前预置的四种音频场景: + +- AUDIO_SCENE_DEFAULT:默认音频场景,音频通话之外的场景均可使用。 + +- AUDIO_SCENE_RINGING:响铃音频场景,来电响铃时使用,仅对系统应用开放。 + +- AUDIO_SCENE_PHONE_CALL:蜂窝通话音频场景,蜂窝通话时使用,仅对系统应用开放。 + +- AUDIO_SCENE_VOICE_CHAT:语音聊天音频场景,VOIP通话时使用。 + +应用可通过[AudioManager](../reference/apis/js-apis-audio.md#audiomanager)的getAudioScene来获取当前的音频场景模式。当应用开始或结束使用音频通话相关功能时,可通过此方法检查系统是否已切换为合适的音频场景模式。 + +## 铃声模式 + +在用户进入到音频通话时,应用可以使用铃声或振动来提示用户。系统通过调整铃声模式([AudioRingMode](../reference/apis/js-apis-audio.md#audioringmode)),实现便捷地管理铃声音量,并调整设备的振动模式。 + +当前预置的三种铃声模式: + +- RINGER_MODE_SILENT:静音模式,此模式下铃声音量为零(即静音)。 + +- RINGER_MODE_VIBRATE:振动模式,此模式下铃声音量为零,设备振动开启(即响铃时静音,触发振动)。 + +- RINGER_MODE_NORMAL:响铃模式,此模式下铃声音量正常。 + +应用可以调用[AudioVolumeGroupManager](../reference/apis/js-apis-audio.md#audiovolumegroupmanager9)中的getRingerMode获取当前的铃声模式,以便采取合适的提示策略。 + +如果应用希望及时获取铃声模式的变化情况,可以通过AudioVolumeGroupManager中的on('ringerModeChange')监听铃声模式变化事件,使应用在铃声模式发生变化时及时收到通知,方便应用做出相应的调整。 + +## 通话场景音频设备切换 + +在通话场景下,系统会根据默认优先级选择合适的音频设备。应用可以根据需要,自主切换音频设备。 + +通信设备类型([CommunicationDeviceType](../reference/apis/js-apis-audio.md#communicationdevicetype9))是系统预置的可用于通话场景的设备,应用可以使用[AudioRoutingManager](../reference/apis/js-apis-audio.md#audioroutingmanager9)的isCommunicationDeviceActive函数获取指定通信设备的激活状态,并且可以使用AudioRoutingManager的setCommunicationDevice设置通信设备的激活状态,通过激活设备来实现通话场景音频设备的切换。 diff --git a/zh-cn/application-dev/media/audio-capturer.md b/zh-cn/application-dev/media/audio-capturer.md deleted file mode 100644 index 451362be0bc16f24039f8aabea0c1bc7e12eb15f..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/media/audio-capturer.md +++ /dev/null @@ -1,257 +0,0 @@ -# 音频采集开发指导 - -## 简介 - -AudioCapturer提供了用于获取原始音频文件的方法。开发者可以通过本指导了解应用如何通过AudioCapturer接口的调用实现音频数据的采集。 - -- **状态检查**:在进行应用开发的过程中,建议开发者通过on('stateChange')方法订阅AudioCapturer的状态变更。因为针对AudioCapturer的某些操作,仅在音频采集器在固定状态时才能执行。如果应用在音频采集器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。 - -## 运作机制 - -该模块提供了音频采集模块的状态变化示意图。 - -**图1** 音频采集状态变化示意图 - -![audio-capturer-state](figures/audio-capturer-state.png) - -**PREPARED状态:** 通过调用create()方法进入到该状态。
-**RUNNING状态:** 正在进行音频数据播放,可以在prepared状态通过调用start()方法进入此状态,也可以在stopped状态通过调用start()方法进入此状态。
-**STOPPED状态:** 在running状态可以通过stop()方法停止音频数据的播放。
-**RELEASED状态:** 在prepared和stop状态,用户均可通过release()方法释放掉所有占用的硬件和软件资源,并且不会再进入到其他的任何一种状态了。
- -## 约束与限制 - -开发者在进行音频数据采集功能开发前,需要先对所开发的应用配置麦克风权限(ohos.permission.MICROPHONE),配置方式请参见[访问控制授权申请](../security/accesstoken-guidelines.md#配置文件权限声明)。 - -## 开发指导 - -详细API含义可参考:[音频管理API文档AudioCapturer](../reference/apis/js-apis-audio.md#audiocapturer8) - -1. 使用createAudioCapturer()创建一个全局的AudioCapturer实例。 - - 在audioCapturerOptions中设置音频采集器的相关参数。该实例可用于音频采集、控制和获取采集状态,以及注册通知回调。 - - ```js - import audio from '@ohos.multimedia.audio'; - import fs from '@ohos.file.fs'; //便于步骤3 read函数调用 - - //音频渲染相关接口自测试 - @Entry - @Component - struct AudioRenderer { - @State message: string = 'Hello World' - private audioCapturer : audio.AudioCapturer; //供全局调用 - - async initAudioCapturer(){ - let audioStreamInfo = { - samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100, - channels: audio.AudioChannel.CHANNEL_1, - sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, - encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW - } - - let audioCapturerInfo = { - source: audio.SourceType.SOURCE_TYPE_MIC, - capturerFlags: 0 // 0是音频采集器的扩展标志位,默认为0 - } - - let audioCapturerOptions = { - streamInfo: audioStreamInfo, - capturerInfo: audioCapturerInfo - } - - this.audioCapturer = await audio.createAudioCapturer(audioCapturerOptions); - console.log('AudioRecLog: Create audio capturer success.'); - } - ``` - -2. 调用start()方法来启动/恢复采集任务。 - - 启动完成后,采集器状态将变更为STATE_RUNNING,然后应用可以开始读取缓冲区。 - - ```js - async startCapturer() { - let state = this.audioCapturer.state; - // Capturer start时的状态应该是STATE_PREPARED、STATE_PAUSED和STATE_STOPPED之一. - if (state == audio.AudioState.STATE_PREPARED || state == audio.AudioState.STATE_PAUSED || - state == audio.AudioState.STATE_STOPPED) { - await this.audioCapturer.start(); - state = this.audioCapturer.state; - if (state == audio.AudioState.STATE_RUNNING) { - console.info('AudioRecLog: Capturer started'); - } else { - console.error('AudioRecLog: Capturer start failed'); - } - } - } - ``` - -3. 读取采集器的音频数据并将其转换为字节流。重复调用read()方法读取数据,直到应用准备停止采集。 - - 参考以下示例,将采集到的数据写入文件。 - - ```js - async readData(){ - let state = this.audioCapturer.state; - // 只有状态为STATE_RUNNING的时候才可以read. - if (state != audio.AudioState.STATE_RUNNING) { - console.info('Capturer is not in a correct state to read'); - return; - } - const path = '/data/data/.pulse_dir/capture_js.wav'; // 采集到的音频文件存储路径 - let file = fs.openSync(path, 0o2); - let fd = file.fd; - if (file !== null) { - console.info('AudioRecLog: file created'); - } else { - console.info('AudioRecLog: file create : FAILED'); - return; - } - if (fd !== null) { - console.info('AudioRecLog: file fd opened in append mode'); - } - let numBuffersToCapture = 150; // 循环写入150次 - let count = 0; - while (numBuffersToCapture) { - this.bufferSize = await this.audioCapturer.getBufferSize(); - let buffer = await this.audioCapturer.read(this.bufferSize, true); - let options = { - offset: count * this.bufferSize, - length: this.bufferSize - } - if (typeof(buffer) == undefined) { - console.info('AudioRecLog: read buffer failed'); - } else { - let number = fs.writeSync(fd, buffer, options); - console.info(`AudioRecLog: data written: ${number}`); - } - numBuffersToCapture--; - count++; - } - } - ``` - -4. 采集完成后,调用stop方法,停止录制。 - - ```js - async StopCapturer() { - let state = this.audioCapturer.state; - // 只有采集器状态为STATE_RUNNING或STATE_PAUSED的时候才可以停止 - if (state != audio.AudioState.STATE_RUNNING && state != audio.AudioState.STATE_PAUSED) { - console.info('AudioRecLog: Capturer is not running or paused'); - return; - } - - await this.audioCapturer.stop(); - - state = this.audioCapturer.state; - if (state == audio.AudioState.STATE_STOPPED) { - console.info('AudioRecLog: Capturer stopped'); - } else { - console.error('AudioRecLog: Capturer stop failed'); - } - } - ``` - -5. 任务结束,调用release()方法释放相关资源。 - - ```js - async releaseCapturer() { - let state = this.audioCapturer.state; - // 采集器状态不是STATE_RELEASED或STATE_NEW状态,才能release - if (state == audio.AudioState.STATE_RELEASED || state == audio.AudioState.STATE_NEW) { - console.info('AudioRecLog: Capturer already released'); - return; - } - - await this.audioCapturer.release(); - - state = this.audioCapturer.state; - if (state == audio.AudioState.STATE_RELEASED) { - console.info('AudioRecLog: Capturer released'); - } else { - console.info('AudioRecLog: Capturer release failed'); - } - } - ``` - -6. (可选)获取采集器相关信息 - - 通过以下代码,可以获取采集器的相关信息。 - - ```js - async getAudioCapturerInfo(){ - // 获取当前采集器状态 - let state = this.audioCapturer.state; - // 获取采集器信息 - let audioCapturerInfo : audio.AudioCapturerInfo = await this.audioCapturer.getCapturerInfo(); - // 获取音频流信息 - let audioStreamInfo : audio.AudioStreamInfo = await this.audioCapturer.getStreamInfo(); - // 获取音频流ID - let audioStreamId : number = await this.audioCapturer.getAudioStreamId(); - // 获取纳秒形式的Unix时间戳 - let audioTime : number = await this.audioCapturer.getAudioTime(); - // 获取合理的最小缓冲区大小 - let bufferSize : number = await this.audioCapturer.getBufferSize(); - } - ``` - -7. (可选)使用on('markReach')方法订阅采集器标记到达事件,使用off('markReach')取消订阅事件。 - - 注册markReach监听后,当采集器采集的帧数到达设定值时,会触发回调并返回设定的值。 - - ```js - async markReach(){ - this.audioCapturer.on('markReach', 10, (reachNumber) => { - console.info('Mark reach event Received'); - console.info(`The Capturer reached frame: ${reachNumber}`); - }); - this.audioCapturer.off('markReach'); // 取消markReach事件的订阅,后续将无法监听到“标记到达”事件 - } - ``` - -8. (可选)使用on('periodReach')方法订阅采集器区间标记到达事件,使用off('periodReach')取消订阅事件。 - - 注册periodReach监听后,**每当**采集器采集的帧数到达设定值时,会触发回调并返回设定的值。 - - ```js - async periodReach(){ - this.audioCapturer.on('periodReach', 10, (reachNumber) => { - console.info('Period reach event Received'); - console.info(`In this period, the Capturer reached frame: ${reachNumber}`); - }); - this.audioCapturer.off('periodReach'); // 取消periodReach事件的订阅,后续将无法监听到“区间标记到达”事件 - } - ``` - -9. 如果应用需要在采集器状态更新时进行一些操作,可以订阅该事件,当采集器状态更新时,会受到一个包含有事件类型的回调。 - - ```js - async stateChange(){ - this.audioCapturer.on('stateChange', (state) => { - console.info(`AudioCapturerLog: Changed State to : ${state}`) - switch (state) { - case audio.AudioState.STATE_PREPARED: - console.info('--------CHANGE IN AUDIO STATE----------PREPARED--------------'); - console.info('Audio State is : Prepared'); - break; - case audio.AudioState.STATE_RUNNING: - console.info('--------CHANGE IN AUDIO STATE----------RUNNING--------------'); - console.info('Audio State is : Running'); - break; - case audio.AudioState.STATE_STOPPED: - console.info('--------CHANGE IN AUDIO STATE----------STOPPED--------------'); - console.info('Audio State is : stopped'); - break; - case audio.AudioState.STATE_RELEASED: - console.info('--------CHANGE IN AUDIO STATE----------RELEASED--------------'); - console.info('Audio State is : released'); - break; - default: - console.info('--------CHANGE IN AUDIO STATE----------INVALID--------------'); - console.info('Audio State is : invalid'); - break; - } - }); - } - ``` \ No newline at end of file diff --git a/zh-cn/application-dev/media/audio-input-device-management.md b/zh-cn/application-dev/media/audio-input-device-management.md new file mode 100644 index 0000000000000000000000000000000000000000..08b85e614f2a9c48ae4c8aba675219b5b95ec01e --- /dev/null +++ b/zh-cn/application-dev/media/audio-input-device-management.md @@ -0,0 +1,88 @@ +# 音频输入设备管理 + +有时设备同时连接多个音频输入设备,需要指定音频输入设备进行音频录制,此时需要使用AudioRoutingManager接口进行输入设备的管理,API说明可以参考[AudioRoutingManager API文档](../reference/apis/js-apis-audio.md#audioroutingmanager9)。 + +## 创建AudioRoutingManager实例 + +在使用AudioRoutingManager管理音频设备前,需要先导入模块并创建实例。 + +```ts +import audio from '@ohos.multimedia.audio'; // 导入audio模块 + +let audioManager = audio.getAudioManager(); // 需要先创建AudioManager实例 + +let audioRoutingManager = audioManager.getRoutingManager(); // 再调用AudioManager的方法创建AudioRoutingManager实例 +``` + +## 支持的音频输入设备类型 + +目前支持的音频输入设备见下表: + +| 名称 | 值 | 说明 | +| -------- | -------- | -------- | +| WIRED_HEADSET | 3 | 有线耳机,带麦克风。 | +| BLUETOOTH_SCO | 7 | 蓝牙设备SCO(Synchronous Connection Oriented)连接。 | +| MIC | 15 | 麦克风。 | +| USB_HEADSET | 22 | USB耳机,带麦克风。 | + +## 获取输入设备信息 + +使用getDevices()方法可以获取当前所有输入设备的信息。 + +```ts +audioRoutingManager.getDevices(audio.DeviceFlag.INPUT_DEVICES_FLAG).then((data) => { + console.info('Promise returned to indicate that the device list is obtained.'); +}); +``` + +## 监听设备连接状态变化 + +可以设置监听事件来监听设备连接状态的变化,当有设备连接或断开时触发回调: + +```ts +// 监听音频设备状态变化 +audioRoutingManager.on('deviceChange', audio.DeviceFlag.INPUT_DEVICES_FLAG, (deviceChanged) => { + console.info('device change type : ' + deviceChanged.type); // 设备连接状态变化,0为连接,1为断开连接 + console.info('device descriptor size : ' + deviceChanged.deviceDescriptors.length); + console.info('device change descriptor : ' + deviceChanged.deviceDescriptors[0].deviceRole); // 设备角色 + console.info('device change descriptor : ' + deviceChanged.deviceDescriptors[0].deviceType); // 设备类型 +}); + +// 取消监听音频设备状态变化 +audioRoutingManager.off('deviceChange', (deviceChanged) => { + console.info('Should be no callback.'); +}); +``` + +## 选择音频输入设备(仅对系统应用开放) + +选择音频输入设备,当前只能选择一个输入设备,以设备id作为唯一标识。AudioDeviceDescriptors的具体信息可以参考[AudioDeviceDescriptors](../reference/apis/js-apis-audio.md#audiodevicedescriptors)。 + +> **说明:** +> +> 用户可以选择连接一组音频设备(如一对蓝牙耳机),但系统侧只感知为一个设备,该组设备共用一个设备id。 + +```ts +let inputAudioDeviceDescriptor = [{ + deviceRole : audio.DeviceRole.INPUT_DEVICE, + deviceType : audio.DeviceType.EARPIECE, + id : 1, + name : "", + address : "", + sampleRates : [44100], + channelCounts : [2], + channelMasks : [0], + networkId : audio.LOCAL_NETWORK_ID, + interruptGroupId : 1, + volumeGroupId : 1, +}]; + +async function getRoutingManager(){ + audioRoutingManager.selectInputDevice(inputAudioDeviceDescriptor).then(() => { + console.info('Invoke selectInputDevice succeeded.'); + }).catch((err) => { + console.error(`Invoke selectInputDevice failed, code is ${err.code}, message is ${err.message}`); + }); +} + +``` diff --git a/zh-cn/application-dev/media/audio-interruptmode.md b/zh-cn/application-dev/media/audio-interruptmode.md deleted file mode 100644 index b6b112551bd1354f46df67d60700e429f9619677..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/media/audio-interruptmode.md +++ /dev/null @@ -1,52 +0,0 @@ -# 音频焦点模式开发指导 - -## 简介 -音频焦点模式指的是应用内,允许对多个声音的播放进行控制。
-音频应用可以在AudioRenderer下设置独立焦点模式、共享焦点模式。
-当设置在共享的模式下,多个音频共用一个会话ID;独立焦点模式下,每一个音频拥有单独会话ID。 - -- **异步操作**:为保证UI线程不被阻塞,大部分AudioRenderer调用都是异步的。对于每个API均提供了callback函数和Promise函数,以下示例均采用Promise函数。 - -## 开发指导 - -详细API含义可参考:[音频管理API文档AudioRenderer](../reference/apis/js-apis-audio.md#audiorenderer8) - -1. 使用createAudioRenderer()创建一个AudioRenderer实例。
- 在audioRendererOptions中设置相关参数。
- 该实例可用于音频渲染、控制和获取采集状态,以及注册通知回调。
- - ```js - import audio from '@ohos.multimedia.audio'; - - var audioStreamInfo = { - samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100, - channels: audio.AudioChannel.CHANNEL_1, - sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, - encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW - } - - var audioRendererInfo = { - content: audio.ContentType.CONTENT_TYPE_SPEECH, - usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION, - rendererFlags: 1 - } - - var audioRendererOptions = { - streamInfo: audioStreamInfo, - rendererInfo: audioRendererInfo - } - - let audioRenderer = await audio.createAudioRenderer(audioRendererOptions); - ``` - -2. 设置焦点模式。 - - 在AudioRenderer初始化完毕后,可以进行焦点模式的设置。
- - ```js - var mode_ = audio.InterruptMode.SHARE_MODE; - await this.audioRenderer.setInterruptMode(mode_).then(() => { - console.log('[JSAR] [SetInterruptMode] 设置: ' + (mode_ == 0 ? "共享模式":"独立焦点模式") + "成功" ); - }); - ``` - diff --git a/zh-cn/application-dev/media/audio-output-device-management.md b/zh-cn/application-dev/media/audio-output-device-management.md new file mode 100644 index 0000000000000000000000000000000000000000..45d9fd3d68a9dd0efe346ce6b0fc65272bc71b9e --- /dev/null +++ b/zh-cn/application-dev/media/audio-output-device-management.md @@ -0,0 +1,90 @@ +# 音频输出设备管理 + +有时设备同时连接多个音频输出设备,需要指定音频输出设备进行音频播放,此时需要使用AudioRoutingManager接口进行输出设备的管理,API说明可以参考[AudioRoutingManager API文档](../reference/apis/js-apis-audio.md#audiomanager)。 + +## 创建AudioRoutingManager实例 + +在使用AudioRoutingManager管理音频设备前,需要先导入模块并创建实例。 + +```ts +import audio from '@ohos.multimedia.audio'; // 导入audio模块 + +let audioManager = audio.getAudioManager(); // 需要先创建AudioManager实例 + +let audioRoutingManager = audioManager.getRoutingManager(); // 再调用AudioManager的方法创建AudioRoutingManager实例 +``` + +## 支持的音频输出设备类型 + +目前支持的音频输出设备见下表: + +| 名称 | 值 | 说明 | +| -------- | -------- | -------- | +| EARPIECE | 1 | 听筒。 | +| SPEAKER | 2 | 扬声器。 | +| WIRED_HEADSET | 3 | 有线耳机,带麦克风。 | +| WIRED_HEADPHONES | 4 | 有线耳机,无麦克风。 | +| BLUETOOTH_SCO | 7 | 蓝牙设备SCO(Synchronous Connection Oriented)连接。 | +| BLUETOOTH_A2DP | 8 | 蓝牙设备A2DP(Advanced Audio Distribution Profile)连接。 | +| USB_HEADSET | 22 | USB耳机,带麦克风。 | + +## 获取输出设备信息 + +使用getDevices()方法可以获取当前所有输出设备的信息。 + +```ts +audioRoutingManager.getDevices(audio.DeviceFlag.OUTPUT_DEVICES_FLAG).then((data) => { + console.info('Promise returned to indicate that the device list is obtained.'); +}); +``` + +## 监听设备连接状态变化 + +可以设置监听事件来监听设备连接状态的变化,当有设备连接或断开时触发回调: + +```ts +// 监听音频设备状态变化 +audioRoutingManager.on('deviceChange', audio.DeviceFlag.OUTPUT_DEVICES_FLAG, (deviceChanged) => { + console.info('device change type : ' + deviceChanged.type); // 设备连接状态变化,0为连接,1为断开连接 + console.info('device descriptor size : ' + deviceChanged.deviceDescriptors.length); + console.info('device change descriptor : ' + deviceChanged.deviceDescriptors[0].deviceRole); // 设备角色 + console.info('device change descriptor : ' + deviceChanged.deviceDescriptors[0].deviceType); // 设备类型 +}); + +// 取消监听音频设备状态变化 +audioRoutingManager.off('deviceChange', (deviceChanged) => { + console.info('Should be no callback.'); +}); +``` + +## 选择音频输出设备(仅对系统应用开放) + +选择音频输出设备,当前只能选择一个输出设备,以设备ID作为唯一标识。AudioDeviceDescriptors的具体信息可以参考[AudioDeviceDescriptors](../reference/apis/js-apis-audio.md#audiodevicedescriptors)。 + +> **说明:** +> +> 用户可以选择连接一组音频设备(如一对蓝牙耳机),但系统侧只感知为一个设备,该组设备共用一个设备ID。 + +```ts +let outputAudioDeviceDescriptor = [{ + deviceRole : audio.DeviceRole.OUTPUT_DEVICE, + deviceType : audio.DeviceType.SPEAKER, + id : 1, + name : "", + address : "", + sampleRates : [44100], + channelCounts : [2], + channelMasks : [0], + networkId : audio.LOCAL_NETWORK_ID, + interruptGroupId : 1, + volumeGroupId : 1, +}]; + +async function selectOutputDevice(){ + audioRoutingManager.selectOutputDevice(outputAudioDeviceDescriptor).then(() => { + console.info('Invoke selectOutputDevice succeeded.'); + }).catch((err) => { + console.error(`Invoke selectOutputDevice failed, code is ${err.code}, message is ${err.message}`); + }); +} +``` diff --git a/zh-cn/application-dev/media/audio-overview.md b/zh-cn/application-dev/media/audio-overview.md deleted file mode 100644 index e0ab05f2e46e16cac84fd1deb19257229912fb4b..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/media/audio-overview.md +++ /dev/null @@ -1,20 +0,0 @@ -# 音频开发概述 - -音频模块支持音频业务的开发,提供音频相关的功能,主要包括音频播放、音量管理等。 - -## 基本概念 - -- **采样**
- 采样是指将连续时域上的模拟信号按照一定的时间间隔采样,获取到离散时域上离散信号的过程。 - -- **采样率**
- 采样率为每秒从连续信号中提取并组成离散信号的采样次数,单位用赫兹(Hz)来表示。通常人耳能听到频率范围大约在20Hz~20kHz之间的声音。常用的音频采样频率有:8kHz、11.025kHz、22.05kHz、16kHz、37.8kHz、44.1kHz、48kHz、96kHz、192kHz等。 - -- **声道**
- 声道是指声音在录制或播放时在不同空间位置采集或回放的相互独立的音频信号,所以声道数也就是声音录制时的音源数量或回放时相应的扬声器数量。 - -- **音频帧**
- 音频数据是流式的,本身没有明确的一帧帧的概念,在实际的应用中,为了音频算法处理/传输的方便,一般约定俗成取2.5ms~60ms为单位的数据量为一帧音频。这个时间被称之为“采样时间”,其长度没有特别的标准,它是根据编解码器和具体应用的需求来决定的。 - -- **PCM**
- PCM(Pulse Code Modulation),即脉冲编码调制,是一种将模拟信号数字化的方法,是将时间连续、取值连续的模拟信号转换成时间离散、抽样值离散的数字信号的过程。 diff --git a/zh-cn/application-dev/media/audio-playback-concurrency.md b/zh-cn/application-dev/media/audio-playback-concurrency.md new file mode 100644 index 0000000000000000000000000000000000000000..d76da75cc0c8bb91f0aa0d57fc3a8ab43c2847a1 --- /dev/null +++ b/zh-cn/application-dev/media/audio-playback-concurrency.md @@ -0,0 +1,119 @@ +# 多音频播放的并发策略 + +## 音频打断策略 + +多音频并发,即多个音频流同时播放。此场景下,如果系统不加管控,会造成多个音频流混音播放,容易让用户感到嘈杂,造成不好的用户体验。为了解决这个问题,系统预设了音频打断策略,对多音频播放的并发进行管控,只有持有音频焦点的音频流才可以正常播放,避免多个音频流无序并发播放的现象出现。 + +当应用开始播放音频时,系统首先为相应的音频流申请音频焦点,获得焦点的音频流可以播放;若焦点申请被拒绝,则不能播放。在音频流播放的过程中,若被其他音频流打断,则会失去音频焦点。当音频流失去音频焦点时,只能暂停播放。在应用播放音频的过程中,这些动作均由系统自行完成,无需应用主动触发。但为了维持应用和系统的状态一致性,保证良好的用户体验,推荐应用[监听音频打断事件](#监听音频打断事件),并在收到音频打断事件([InterruptEvent](../reference/apis/js-apis-audio.md#interruptevent9))时做出相应处理。 + +为满足应用对多音频并发策略的不同需求,音频打断策略预设了两种焦点模式,针对同一应用创建的多个音频流,应用可通过设置[焦点模式](#焦点模式),选择由应用自主管控或由系统统一管控。 + +音频打断策略决定了应该对音频流采取何种操作,如暂停播放、继续播放、降低音量播放、恢复音量播放等,这些操作可能由系统或应用来执行。音频打断策略预置了两种[打断类型](#打断类型),用于区分音频打断事件(InterruptEvent)的执行者。 + +### 焦点模式 + +音频打断策略预设了两种焦点模式([InterruptMode](../reference/apis/js-apis-audio.md#interruptmode9)): + +- 共享焦点模式(SHARED_MODE):由同一应用创建的多个音频流,共享一个音频焦点。这些音频流之间的并发规则由应用自主决定,音频打断策略不会介入。当其他应用创建的音频流与该应用的音频流并发播放时,才会触发音频打断策略的管控。 + +- 独立焦点模式(INDEPENDENT_MODE):应用创建的每一个音频流均会独立拥有一个音频焦点,当多个音频流并发播放时,会触发音频打断策略的管控。 + +应用可以按需选择合适的焦点模式,在创建音频流时,系统默认采用共享焦点模式,应用可主动设置所需的模式。 + +设置焦点模式的方法: + +- 若[使用AVPlayer开发音频播放功能](using-avplayer-for-playback.md),则可以通过修改AVPlayer的[audioInterruptMode](../reference/apis/js-apis-media.md#avplayer9)属性进行设置。 + +- 若[使用AudioRenderer开发音频播放功能](using-audiorenderer-for-playback.md),则可以调用AudioRenderer的[setInterruptMode](../reference/apis/js-apis-audio.md#setinterruptmode9)函数进行设置。 + + +### 打断类型 + +音频打断策略(包括两种焦点模式)决定了应该对各个音频流采取何种操作,如暂停播放、继续播放、降低音量播放、恢复音量播放等。而针对这些操作的执行过程,根据执行者的不同,可以分为两种打断类型([InterruptForceType](../reference/apis/js-apis-audio.md#interruptforcetype9)): + +- 强制打断类型(INTERRUPT_FORCE):由系统进行操作,强制打断音频播放。 + +- 共享打断类型(INTERRUPT_SHARE):由应用进行操作,可以选择打断或忽略。 + +对于音频打断策略的执行,系统默认采用强制打断类型(INTERRUPT_FORCE),应用无法更改。但对于一些策略(如继续播放等),系统无法强制执行,所以这两种打断类型均可能出现。应用可根据音频打断事件(InterruptEvent)的成员变量forceType的值,获取该事件采用的打断类型。 + +在应用播放音频的过程中,系统自动为音频流执行申请焦点、持有焦点、释放焦点等动作,当发生音频打断事件时,系统强制对音频流执行暂停、停止、降低音量、恢复音量等操作,并向应用发送音频打断事件(InterruptEvent)回调。由于系统会强制改变音频流状态,为了维持应用和系统的状态一致性,保证良好的用户体验,推荐应用[监听音频打断事件](#监听音频打断事件),并在收到音频打断事件(InterruptEvent)时做出相应处理。 + +对于一些系统无法强制执行的操作(例如音频流继续播放的场景),会向应用发送包含了共享打断类型的音频打断事件,由应用自行执行相应操作,此时应用可以选择执行或忽略,系统不会干涉。 + +## 监听音频打断事件 + +在应用播放音频时,推荐应用监听音频打断事件,当音频打断事件发生时,系统会根据预设策略,对音频流做出相应的操作,并针对状态发生改变的音频流,向所属的应用发送音频打断事件。 + +应用收到音频打断事件后,需根据其内容提示,做出相应的处理,避免出现应用状态与预期效果不一致的问题。 + +监听音频打断事件的方法: + +- 若[使用AVPlayer开发音频播放功能](using-avplayer-for-playback.md),则可以调用AVPlayer的[on('audioInterrupt')](../reference/apis/js-apis-media.md#onaudiointerrupt9)函数进行监听,当收到音频打断事件(InterruptEvent)时,应用需根据其内容,做出相应的调整。 + +- 若[使用AudioRenderer开发音频播放功能](using-audiorenderer-for-playback.md),则可以调用AudioRenderer的[on('audioInterrupt')](../reference/apis/js-apis-audio.md#onaudiointerrupt9)函数进行监听,当收到音频打断事件(InterruptEvent)时,应用需根据其内容,做出相应的调整。 + + 为了带给用户更好的体验,针对不同的音频打断事件内容,应用需要做出相应的处理操作。此处以使用AudioRenderer开发音频播放功能为例,展示推荐应用采取的处理方法,提供伪代码供开发者参考(若使用AVPlayer开发音频播放功能,处理方法类似),具体的代码实现,开发者可结合实际情况编写,处理方法也可自行调整。 + +```ts +let isPlay; // 是否正在播放,实际开发中,对应与音频播放状态相关的模块 +let isDucked; //是否降低音量,实际开发中,对应与音频音量相关的模块 +let started; // 标识符,记录“开始播放(start)”操作是否成功 + +async function onAudioInterrupt(){ + // 此处以使用AudioRenderer开发音频播放功能举例,变量audioRenderer即为播放时创建的AudioRenderer实例。 + audioRenderer.on('audioInterrupt', async(interruptEvent) => { + // 在发生音频打断事件时,audioRenderer收到interruptEvent回调,此处根据其内容做相应处理 + // 先读取interruptEvent.forceType的类型,判断系统是否已强制执行相应操作 + // 再读取interruptEvent.hintType的类型,做出相应的处理 + if (interruptEvent.forceType === audio.InterruptForceType.INTERRUPT_FORCE) { + // 强制打断类型(INTERRUPT_FORCE):音频相关处理已由系统执行,应用需更新自身状态,做相应调整 + switch (interruptEvent.hintType) { + case audio.InterruptHint.INTERRUPT_HINT_PAUSE: + // 此分支表示系统已将音频流暂停(临时失去焦点),为保持状态一致,应用需切换至音频暂停状态 + // 临时失去焦点:待其他音频流释放音频焦点后,本音频流会收到resume对应的音频打断事件,到时可自行继续播放 + isPlay = false; // 此句为简化处理,代表应用切换至音频暂停状态的若干操作 + break; + case audio.InterruptHint.INTERRUPT_HINT_STOP: + // 此分支表示系统已将音频流停止(永久失去焦点),为保持状态一致,应用需切换至音频暂停状态 + // 永久失去焦点:后续不会再收到任何音频打断事件,若想恢复播放,需要用户主动触发。 + isPlay = false; // 此句为简化处理,代表应用切换至音频暂停状态的若干操作 + break; + case audio.InterruptHint.INTERRUPT_HINT_DUCK: + // 此分支表示系统已将音频音量降低(默认降到正常音量的20%),为保持状态一致,应用需切换至降低音量播放状态 + // 若应用不接受降低音量播放,可在此处选择其他处理方式,如主动暂停等 + isDucked = true; // 此句为简化处理,代表应用切换至降低音量播放状态的若干操作 + break; + case audio.InterruptHint.INTERRUPT_HINT_UNDUCK: + // 此分支表示系统已将音频音量恢复正常,为保持状态一致,应用需切换至正常音量播放状态 + isDucked = false; // 此句为简化处理,代表应用切换至正常音量播放状态的若干操作 + break; + default: + break; + } + } else if (interruptEvent.forceType === audio.InterruptForceType.INTERRUPT_SHARE) { + // 共享打断类型(INTERRUPT_SHARE):应用可自主选择执行相关操作或忽略音频打断事件 + switch (interruptEvent.hintType) { + case audio.InterruptHint.INTERRUPT_HINT_RESUME: + // 此分支表示临时失去焦点后被暂停的音频流此时可以继续播放,建议应用继续播放,切换至音频播放状态 + // 若应用此时不想继续播放,可以忽略此音频打断事件,不进行处理即可 + // 继续播放,此处主动执行start(),以标识符变量started记录start()的执行结果 + await audioRenderer.start().then(async function () { + started = true; // start()执行成功 + }).catch((err) => { + started = false; // start()执行失败 + }); + // 若start()执行成功,则切换至音频播放状态 + if (started) { + isPlay = true; // 此句为简化处理,代表应用切换至音频播放状态的若干操作 + } else { + // 音频继续播放执行失败 + } + break; + default: + break; + } + } + }); +} +``` diff --git a/zh-cn/application-dev/media/audio-playback-overview.md b/zh-cn/application-dev/media/audio-playback-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..6bace05096de29db9288869e1b53abed37fdb4b8 --- /dev/null +++ b/zh-cn/application-dev/media/audio-playback-overview.md @@ -0,0 +1,25 @@ +# 音频播放开发概述 + +## 如何选择音频播放开发方式 + +在OpenHarmony系统中,多种API都提供了音频播放开发的支持,不同的API适用于不同音频数据格式、音频资源来源、音频使用场景,甚至是不同开发语言。因此,选择合适的音频播放API,有助于降低开发工作量,实现更佳的音频播放效果。 + +- [AVPlayer](using-avplayer-for-playback.md):功能较完善的音频、视频播放ArkTS/JS API,集成了流媒体和本地资源解析、媒体资源解封装、音频解码和音频输出功能。可以用于直接播放mp3、m4a等格式的音频文件,不支持直接播放PCM格式文件。 + +- [AudioRenderer](using-audiorenderer-for-playback.md):用于音频输出的的ArkTS/JS API,仅支持PCM格式,需要应用需要持续写入音频数据进行工作。应用可以在输入前添加数据预处理,如设定音频文件的采样率、位宽等,要求开发者具备音频处理的基础知识,适用于更专业、更多样化的媒体播放应用开发。 + +- [OpenSLES](using-opensl-es-for-playback.md):一套跨平台标准化的音频Native API,目前阶段唯一的音频类Native API,同样提供音频输出能力,仅支持PCM格式,适用于从其他嵌入式平台移植,或依赖在Native层实现音频输出功能的播放应用使用。 + +- [TonePlayer](using-toneplayer-for-playback.md):拨号和回铃音播放ArkTS/JS API,只能在固定的类型范围内选择播放内容,无需输入媒体资源或音频数据,适用于拨号盘按键和通话回铃音的特定场景。该功能当前仅对系统应用开放。 + +- 在音频播放中,应用时常需要用到一些急促简短的音效,如相机快门音效、按键音效、游戏射击音效等,当前只能使用AVPlayer播放音频文件替代实现,在OpenHarmony后续版本将会推出相关接口来支持该场景。 + +## 开发音频播放应用须知 + +应用如果要实现后台播放或熄屏播放,需要同时满足: + +1. 使用媒体会话功能注册到系统内统一管理,否则在应用进入后台时,播放将被强制停止。具体参考[媒体会话开发指导](avsession-overview.md)。 + +2. 申请长时任务避免进入挂起(Suspend)状态。具体参考[长时任务开发指导](../task-management/continuous-task-dev-guide.md)。 + +当应用进入后台,播放被中断,如果被媒体会话管控,将打印日志“pause id”;如果没有该日志,则说明被长时任务管控。 diff --git a/zh-cn/application-dev/media/audio-playback-stream-management.md b/zh-cn/application-dev/media/audio-playback-stream-management.md new file mode 100644 index 0000000000000000000000000000000000000000..548c4b0b2c8640585631eb162ac97a9c82b8bf58 --- /dev/null +++ b/zh-cn/application-dev/media/audio-playback-stream-management.md @@ -0,0 +1,118 @@ +# 音频播放流管理 + +对于播放音频类的应用,开发者需要关注该应用的音频流的状态以做出相应的操作,比如监听到状态为播放中/暂停时,及时改变播放按钮的UI显示。 + +## 读取或监听应用内音频流状态变化 + +参考[使用AudioRenderer开发音频播放功能](using-audiorenderer-for-playback.md)或[audio.createAudioRenderer](../reference/apis/js-apis-audio.md#audiocreateaudiorenderer8),完成AudioRenderer的创建,然后可以通过以下两种方式查看音频流状态的变化: + +- 方法1:直接查看AudioRenderer的[state](../reference/apis/js-apis-audio.md#属性): + + ```ts + let audioRendererState = audioRenderer.state; + console.info(`Current state is: ${audioRendererState }`) + ``` + +- 方法2:注册stateChange监听AudioRenderer的状态变化: + + ```ts + audioRenderer.on('stateChange', (rendererState) => { + console.info(`State change to: ${rendererState}`) + }); + ``` + +获取state后可对照[AudioState](../reference/apis/js-apis-audio.md#audiostate8)来进行相应的操作,比如更改暂停播放按钮的显示等。 + +## 读取或监听所有音频流的变化 + +如果部分应用需要查询获取所有音频流的变化信息,可以通过AudioStreamManager读取或监听所有音频流的变化。 + +> **说明:** +> +> 对于标记为系统接口(system api)的音频流变化信息需要系统级别应用才可查看,若应用不是系统应用,将无法获取准确信息。 + +如下为音频流管理调用关系图: + +![Audio stream management invoking relationship](figures/audio-stream-mgmt-invoking-relationship.png) + +在进行应用开发的过程中,开发者需要使用getStreamManager()创建一个AudioStreamManager实例,进而通过该实例管理音频流。开发者可通过调用on('audioRendererChange')监听音频流的变化,在音频流状态变化、设备变化时获得通知。同时可通过off('audioRendererChange')取消相关事件的监听。另外,开发者可以主动调用getCurrentAudioRendererInfoArray()来查询播放流的唯一ID、播放流客户端的UID、音频流状态等信息。 + +详细API含义可参考[音频管理API文档AudioStreamManager](../reference/apis/js-apis-audio.md#audiostreammanager9)。 + +## 开发步骤及注意事项 + +1. 创建AudioStreamManager实例。 + 在使用AudioStreamManager的API前,需要使用getStreamManager()创建一个AudioStreamManager实例。 + + ```ts + import audio from '@ohos.multimedia.audio'; + let audioManager = audio.getAudioManager(); + let audioStreamManager = audioManager.getStreamManager(); + ``` + +2. 使用on('audioRendererChange')监听音频播放流的变化。 如果音频流监听应用需要在音频播放流状态变化、设备变化时获取通知,可以订阅该事件。 + + ```ts + audioStreamManager.on('audioRendererChange', (AudioRendererChangeInfoArray) => { + for (let i = 0; i < AudioRendererChangeInfoArray.length; i++) { + let AudioRendererChangeInfo = AudioRendererChangeInfoArray[i]; + console.info(`## RendererChange on is called for ${i} ##`); + console.info(`StreamId for ${i} is: ${AudioRendererChangeInfo.streamId}`); + console.info(`Content ${i} is: ${AudioRendererChangeInfo.rendererInfo.content}`); + console.info(`Stream ${i} is: ${AudioRendererChangeInfo.rendererInfo.usage}`); + console.info(`Flag ${i} is: ${AudioRendererChangeInfo.rendererInfo.rendererFlags}`); + for (let j = 0;j < AudioRendererChangeInfo.deviceDescriptors.length; j++) { + console.info(`Id: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].id}`); + console.info(`Type: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].deviceType}`); + console.info(`Role: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].deviceRole}`); + console.info(`Name: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].name}`); + console.info(`Address: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].address}`); + console.info(`SampleRates: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].sampleRates[0]}`); + console.info(`ChannelCount ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].channelCounts[0]}`); + console.info(`ChannelMask: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].channelMasks}`); + } + } + }); + ``` + +3. (可选)使用off('audioRendererChange')取消监听音频播放流变化。 + + ```ts + audioStreamManager.off('audioRendererChange'); + console.info('RendererChange Off is called '); + ``` + +4. (可选)使用getCurrentAudioRendererInfoArray()获取所有音频播放流的信息。 + 该接口可获取音频播放流唯一ID,音频播放客户端的UID,音频状态以及音频播放器的其他信息。 + > **说明:** + > + > 对所有音频流状态进行监听的应用需要[申请权限](../security/accesstoken-guidelines.md)ohos.permission.USE_BLUETOOTH,否则无法获得实际的设备名称和设备地址信息,查询到的设备名称和设备地址(蓝牙设备的相关属性)将为空字符串。 + + ```ts + async function getCurrentAudioRendererInfoArray(){ + await audioStreamManager.getCurrentAudioRendererInfoArray().then( function (AudioRendererChangeInfoArray) { + console.info(`getCurrentAudioRendererInfoArray Get Promise is called `); + if (AudioRendererChangeInfoArray != null) { + for (let i = 0; i < AudioRendererChangeInfoArray.length; i++) { + let AudioRendererChangeInfo = AudioRendererChangeInfoArray[i]; + console.info(`StreamId for ${i} is: ${AudioRendererChangeInfo.streamId}`); + console.info(`Content ${i} is: ${AudioRendererChangeInfo.rendererInfo.content}`); + console.info(`Stream ${i} is: ${AudioRendererChangeInfo.rendererInfo.usage}`); + console.info(`Flag ${i} is: ${AudioRendererChangeInfo.rendererInfo.rendererFlags}`); + for (let j = 0;j < AudioRendererChangeInfo.deviceDescriptors.length; j++) { + console.info(`Id: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].id}`); + console.info(`Type: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].deviceType}`); + console.info(`Role: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].deviceRole}`); + console.info(`Name: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].name}`); + console.info(`Address: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].address}`); + console.info(`SampleRates: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].sampleRates[0]}`); + console.info(`ChannelCount ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].channelCounts[0]}`); + console.info(`ChannelMask: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].channelMasks}`); + } + } + } + }).catch((err) => { + console.error(`Invoke getCurrentAudioRendererInfoArray failed, code is ${err.code}, message is ${err.message}`); + }); + } + ``` diff --git a/zh-cn/application-dev/media/audio-playback.md b/zh-cn/application-dev/media/audio-playback.md deleted file mode 100644 index 56148027bde421639c42e326a757b718f9bb8084..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/media/audio-playback.md +++ /dev/null @@ -1,252 +0,0 @@ -# 音频播放开发指导 - -## 简介 - -音频播放的主要工作是将音频数据转码为可听见的音频模拟信号,并通过输出设备进行播放,同时对播放任务进行管理,包括开始播放、暂停播放、停止播放、释放资源、设置音量、跳转播放位置、获取轨道信息等功能控制。 - -## 运作机制 - -该模块提供了音频播放状态变化示意图和音频播放外部模块交互图。 - -**图1** 音频播放状态变化示意图 - -![zh-ch_image_audio_state_machine](figures/zh-ch_image_audio_state_machine.png) - -**注意**:当前为Idle状态,设置src不会改变状态;且src设置成功后,不能再次设置其它src,需调用reset()接口后,才能重新设置src。 - - - -**图2** 音频播放外部模块交互图 - -![zh-ch_image_audio_player](figures/zh-ch_image_audio_player.png) - -**说明**:三方应用通过调用JS接口层提供的js接口实现相应功能时,框架层会通过Native Framework的媒体服务,调用音频部件,将软件解码后的音频数据输出至硬件接口层的音频HDI,实现音频播放功能。 - -## 开发指导 - -详细API含义可参考:[媒体服务API文档AudioPlayer](../reference/apis/js-apis-media.md#audioplayer) - -> **说明:** -> -> path路径在FA模型和Stage模型下的获取方式不同,示例代码中仅给出pathDir示例,具体的path路径请开发者根据实际情况获取。获取方式请参考[应用沙箱路径使用说明](../reference/apis/js-apis-fileio.md#使用说明)。 - -### 全流程场景 - -音频播放的全流程场景包含:创建实例,设置uri,播放音频,跳转播放位置,设置音量,暂停播放,获取轨道信息,停止播放,重置,释放资源等流程。 - -AudioPlayer支持的src媒体源输入类型可参考:[src属性说明](../reference/apis/js-apis-media.md#audioplayer_属性) - -```js -import media from '@ohos.multimedia.media' -import fs from '@ohos.file.fs' - -// 打印码流轨道信息 -function printfDescription(obj) { - for (let item in obj) { - let property = obj[item]; - console.info('audio key is ' + item); - console.info('audio value is ' + property); - } -} - -// 设置播放器回调函数 -function setCallBack(audioPlayer) { - audioPlayer.on('dataLoad', () => { // 设置'dataLoad'事件回调,src属性设置成功后,触发此回调 - console.info('audio set source success'); - audioPlayer.play(); // 需等待'dataLoad'事件回调完成后,才可调用play进行播放,触发'play'事件回调 - }); - audioPlayer.on('play', () => { // 设置'play'事件回调 - console.info('audio play success'); - audioPlayer.pause(); // 触发'pause'事件回调,暂停播放 - }); - audioPlayer.on('pause', () => { // 设置'pause'事件回调 - console.info('audio pause success'); - audioPlayer.seek(5000); // 触发'timeUpdate'事件回调,seek到5000ms处播放 - }); - audioPlayer.on('stop', () => { // 设置'stop'事件回调 - console.info('audio stop success'); - audioPlayer.reset(); // 触发'reset'事件回调后,重新设置src属性,可完成切歌 - }); - audioPlayer.on('reset', () => { // 设置'reset'事件回调 - console.info('audio reset success'); - audioPlayer.release(); // audioPlayer资源被销毁 - audioPlayer = undefined; - }); - audioPlayer.on('timeUpdate', (seekDoneTime) => { // 设置'timeUpdate'事件回调 - if (typeof(seekDoneTime) == 'undefined') { - console.info('audio seek fail'); - return; - } - console.info('audio seek success, and seek time is ' + seekDoneTime); - audioPlayer.setVolume(0.5); // 触发'volumeChange'事件回调 - }); - audioPlayer.on('volumeChange', () => { // 设置'volumeChange'事件回调 - console.info('audio volumeChange success'); - audioPlayer.getTrackDescription((error, arrlist) => { // 通过回调方式获取音频轨道信息 - if (typeof (arrlist) != 'undefined') { - for (let i = 0; i < arrlist.length; i++) { - printfDescription(arrlist[i]); - } - } else { - console.log(`audio getTrackDescription fail, error:${error.message}`); - } - audioPlayer.stop(); // 触发'stop'事件回调,停止播放 - }); - }); - audioPlayer.on('finish', () => { // 设置'finish'事件回调,播放完成触发 - console.info('audio play finish'); - }); - audioPlayer.on('error', (error) => { // 设置'error'事件回调 - console.info(`audio error called, errName is ${error.name}`); - console.info(`audio error called, errCode is ${error.code}`); - console.info(`audio error called, errMessage is ${error.message}`); - }); -} - -async function audioPlayerDemo() { - // 1. 创建实例 - let audioPlayer = media.createAudioPlayer(); - setCallBack(audioPlayer); // 设置事件回调 - // 2. 用户选择音频,设置uri - let fdPath = 'fd://' - let pathDir = "/data/storage/el2/base/haps/entry/files" // pathDir在FA模型和Stage模型的获取方式不同,请参考开发步骤首行的说明,根据实际情况自行获取。 - // path路径的码流可通过"hdc file send D:\xxx\01.mp3 /data/app/el2/100/base/ohos.acts.multimedia.audio.audioplayer/haps/entry/files" 命令,将其推送到设备上 - let path = pathDir + '/01.mp3' - let file = await fs.open(path); - fdPath = fdPath + '' + file.fd; - audioPlayer.src = fdPath; // 设置src属性,并触发'dataLoad'事件回调 -} -``` - -### 正常播放场景 - -```js -import media from '@ohos.multimedia.media' -import fs from '@ohos.file.fs' - -export class AudioDemo { - // 设置播放器回调函数 - setCallBack(audioPlayer) { - audioPlayer.on('dataLoad', () => { // 设置'dataLoad'事件回调,src属性设置成功后,触发此回调 - console.info('audio set source success'); - audioPlayer.play(); // 调用play方法开始播放,触发'play'事件回调 - }); - audioPlayer.on('play', () => { // 设置'play'事件回调 - console.info('audio play success'); - }); - audioPlayer.on('finish', () => { // 设置'finish'事件回调,播放完成触发 - console.info('audio play finish'); - audioPlayer.release(); // audioPlayer资源被销毁 - audioPlayer = undefined; - }); - } - - async audioPlayerDemo() { - let audioPlayer = media.createAudioPlayer(); // 创建一个音频播放实例 - this.setCallBack(audioPlayer); // 设置事件回调 - let fdPath = 'fd://' - let pathDir = "/data/storage/el2/base/haps/entry/files" // pathDir在FA模型和Stage模型的获取方式不同,请参考开发步骤首行的说明,根据实际情况自行获取。 - // path路径的码流可通过"hdc file send D:\xxx\01.mp3 /data/app/el2/100/base/ohos.acts.multimedia.audio.audioplayer/haps/entry/files" 命令,将其推送到设备上 - let path = pathDir + '/01.mp3' - let file = await fs.open(path); - fdPath = fdPath + '' + file.fd; - audioPlayer.src = fdPath; // 设置src属性,并触发'dataLoad'事件回调 - } -} -``` - -### 切歌场景 - -```js -import media from '@ohos.multimedia.media' -import fs from '@ohos.file.fs' - -export class AudioDemo { -// 设置播放器回调函数 - private isNextMusic = false; - setCallBack(audioPlayer) { - audioPlayer.on('dataLoad', () => { // 设置'dataLoad'事件回调,src属性设置成功后,触发此回调 - console.info('audio set source success'); - audioPlayer.play(); // 调用play方法开始播放,触发'play'事件回调 - }); - audioPlayer.on('play', () => { // 设置'play'事件回调 - console.info('audio play success'); - audioPlayer.reset(); // 调用reset方法,触发'reset'事件回调 - }); - audioPlayer.on('reset', () => { // 设置'reset'事件回调 - console.info('audio play success'); - if (!this.isNextMusic) { // 当isNextMusic 为false时,实现切歌功能 - this.nextMusic(audioPlayer); // 实现切歌功能 - } else { - audioPlayer.release(); // audioPlayer资源被销毁 - audioPlayer = undefined; - } - }); - } - - async nextMusic(audioPlayer) { - this.isNextMusic = true; - let nextFdPath = 'fd://' - let pathDir = "/data/storage/el2/base/haps/entry/files" // pathDir在FA模型和Stage模型的获取方式不同,请参考开发步骤首行的说明,根据实际情况自行获取。 - // path路径的码流可通过"hdc file send D:\xxx\02.mp3 /data/app/el2/100/base/ohos.acts.multimedia.audio.audioplayer/haps/entry/files" 命令,将其推送到设备上 - let nextpath = pathDir + '/02.mp3' - let nextFile = await fs.open(nextpath); - nextFdPath = nextFdPath + '' + nextFile.fd; - audioPlayer.src = nextFdPath; // 设置src属性,并重新触发触发'dataLoad'事件回调 - } - - async audioPlayerDemo() { - let audioPlayer = media.createAudioPlayer(); // 创建一个音频播放实例 - this.setCallBack(audioPlayer); // 设置事件回调 - let fdPath = 'fd://' - let pathDir = "/data/storage/el2/base/haps/entry/files" // pathDir在FA模型和Stage模型的获取方式不同,请参考开发步骤首行的说明,根据实际情况自行获取。 - // path路径的码流可通过"hdc file send D:\xxx\01.mp3 /data/app/el2/100/base/ohos.acts.multimedia.audio.audioplayer/haps/entry/files" 命令,将其推送到设备上 - let path = pathDir + '/01.mp3' - let file = await fs.open(path); - fdPath = fdPath + '' + file.fd; - audioPlayer.src = fdPath; // 设置src属性,并触发'dataLoad'事件回调 - } -} -``` - -### 单曲循环场景 - -```js -import media from '@ohos.multimedia.media' -import fs from '@ohos.file.fs' - -export class AudioDemo { - // 设置播放器回调函数 - setCallBack(audioPlayer) { - audioPlayer.on('dataLoad', () => { // 设置'dataLoad'事件回调,src属性设置成功后,触发此回调 - console.info('audio set source success'); - audioPlayer.loop = true; // 设置循环播放属性 - audioPlayer.play(); // 调用play方法开始播放,触发'play'事件回调 - }); - audioPlayer.on('play', () => { // 设置'play'事件回调,开始循环播放 - console.info('audio play success'); - }); - } - - async audioPlayerDemo() { - let audioPlayer = media.createAudioPlayer(); // 创建一个音频播放实例 - this.setCallBack(audioPlayer); // 设置事件回调 - let fdPath = 'fd://' - let pathDir = "/data/storage/el2/base/haps/entry/files" // pathDir在FA模型和Stage模型的获取方式不同,请参考开发步骤首行的说明,根据实际情况自行获取。 - // path路径的码流可通过"hdc file send D:\xxx\01.mp3 /data/app/el2/100/base/ohos.acts.multimedia.audio.audioplayer/haps/entry/files" 命令,将其推送到设备上 - let path = pathDir + '/01.mp3' - let file = await fs.open(path); - fdPath = fdPath + '' + file.fd; - audioPlayer.src = fdPath; // 设置src属性,并触发'dataLoad'事件回调 - } -} -``` - -## 相关实例 - -针对音频播放开发,有以下相关实例可供参考: - -- [`JsDistributedMusicPlayer:`分布式音乐播放(JS)(API10)(Full SDK)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/SuperFeature/DistributedAppDev/JsDistributedMusicPlayer) -- [`MediaCollections`:媒体管理合集(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/BasicFeature/FileManagement/MediaCollections) -- [`Recorder`: 录音机(ArkTS)(API9)(Full SDK)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/BasicFeature/Media/Recorder) -- [音频播放器(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/Media/Audio_OH_ETS) \ No newline at end of file diff --git a/zh-cn/application-dev/media/audio-recorder.md b/zh-cn/application-dev/media/audio-recorder.md deleted file mode 100644 index 1730280786d3dd42dd5545a8bd2573d5f25002be..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/media/audio-recorder.md +++ /dev/null @@ -1,204 +0,0 @@ -# 音频录制开发指导 - -## 简介 - -音频录制的主要工作是捕获音频信号,完成音频编码并保存到文件中,帮助开发者轻松实现音频录制功能。该模块允许调用者指定音频录制的采样率、声道数、编码格式、封装格式、输出文件的路径等参数。 - -## 运作机制 - -该模块提供了音频录制状态变化示意图和音频录制外部模块交互图。 - -**图1** 音频录制状态变化变化示意图 - -![zh-ch_image_audio_recorder_state_machine](figures/zh-ch_image_audio_recorder_state_machine.png) - - - -**图2** 音频录制外部模块交互图 - -![zh-ch_image_audio_recorder_zero](figures/zh-ch_image_audio_recorder_zero.png) - -**说明**:三方录音应用或录音机通过调用JS接口层提供的js接口实现相应功能时,框架层会通过Native Framework的媒体服务,调用音频部件获取通过音频HDI捕获的音频数据,再通过软件编码输出编码封装后的音频数据保存至文件中,实现音频录制功能。 - -## 约束与限制 - -开发者在进行录制功能开发前,需要先对所开发的应用配置麦克风权限(ohos.permission.MICROPHONE),权限配置相关内容可参考:[访问控制权限申请指导](../security/accesstoken-guidelines.md) - -## 开发指导 - -详细API含义可参考:[媒体服务API文档AudioRecorder](../reference/apis/js-apis-media.md#audiorecorder) - -### 全流程场景 - -音频录制的全流程场景包含:创建实例,设置录制参数,开始录制,暂停录制,恢复录制,停止录制,释放资源等流程。 - -```js -import media from '@ohos.multimedia.media' -import mediaLibrary from '@ohos.multimedia.mediaLibrary' -export class AudioRecorderDemo { - private testFdNumber; // 用于保存fd地址 - - // 设置音频录制相关回调函数 - setCallBack(audioRecorder) { - audioRecorder.on('prepare', () => { // 设置'prepare'事件回调 - console.log('prepare success'); - audioRecorder.start(); // 调用start方法开始录制,并触发start回调 - }); - audioRecorder.on('start', () => { // 设置'start'事件回调 - console.log('audio recorder start success'); - audioRecorder.pause(); // 调用pause方法暂停录制,并触发pause回调 - }); - audioRecorder.on('pause', () => { // 设置'pause'事件回调 - console.log('audio recorder pause success'); - audioRecorder.resume(); // 调用resume方法恢复录制,并触发resume回调 - }); - audioRecorder.on('resume', () => { // 设置'resume'事件回调 - console.log('audio recorder resume success'); - audioRecorder.stop(); // 调用stop方法停止录制,并触发stop回调 - }); - audioRecorder.on('stop', () => { // 设置'stop'事件回调 - console.log('audio recorder stop success'); - audioRecorder.reset(); // 调用reset方法重置录制,并触发reset回调 - }); - audioRecorder.on('reset', () => { // 设置'reset'事件回调 - console.log('audio recorder reset success'); - audioRecorder.release(); // 调用release方法,释放资源,并触发release回调 - }); - audioRecorder.on('release', () => { // 设置'release'事件回调 - console.log('audio recorder release success'); - audioRecorder = undefined; - }); - audioRecorder.on('error', (error) => { // 设置'error'事件回调 - console.info(`audio error called, errName is ${error.name}`); - console.info(`audio error called, errCode is ${error.code}`); - console.info(`audio error called, errMessage is ${error.message}`); - }); - } - - // pathName是传入的录制文件名,例如:01.mp3,生成后的文件地址:/storage/media/100/local/files/Video/01.mp3 - // 使用mediaLibrary需要添加以下权限, ohos.permission.MEDIA_LOCATION、ohos.permission.WRITE_MEDIA、ohos.permission.READ_MEDIA - async getFd(pathName) { - let displayName = pathName; - const mediaTest = mediaLibrary.getMediaLibrary(); - let fileKeyObj = mediaLibrary.FileKey; - let mediaType = mediaLibrary.MediaType.VIDEO; - let publicPath = await mediaTest.getPublicDirectory(mediaLibrary.DirectoryType.DIR_VIDEO); - let dataUri = await mediaTest.createAsset(mediaType, displayName, publicPath); - if (dataUri != undefined) { - let args = dataUri.id.toString(); - let fetchOp = { - selections : fileKeyObj.ID + "=?", - selectionArgs : [args], - } - let fetchFileResult = await mediaTest.getFileAssets(fetchOp); - let fileAsset = await fetchFileResult.getAllObject(); - let fdNumber = await fileAsset[0].open('Rw'); - this.testFdNumber = "fd://" + fdNumber.toString(); - } - } - - async audioRecorderDemo() { - // 1.创建实例 - let audioRecorder = media.createAudioRecorder(); - // 2.设置回调 - this.setCallBack(audioRecorder); - await this.getFd('01.mp3'); // 调用getFd方法获取需要录制文件的fd地址 - // 3.设置录制参数 - let audioRecorderConfig = { - audioEncodeBitRate : 22050, - audioSampleRate : 22050, - numberOfChannels : 2, - uri : this.testFdNumber, // testFdNumber由getFd生成 - location : { latitude : 30, longitude : 130}, - audioEncoderMime : media.CodecMimeType.AUDIO_AAC, - fileFormat : media.ContainerFormatType.CFT_MPEG_4A, - } - audioRecorder.prepare(audioRecorderConfig); // 调用prepare方法,触发prepare回调函数 - } -} -``` - -### 正常录制场景 - -与全流程场景不同,不包括暂停录制,恢复录制的过程。 - -```js -import media from '@ohos.multimedia.media' -import mediaLibrary from '@ohos.multimedia.mediaLibrary' -export class AudioRecorderDemo { - private testFdNumber; // 用于保存fd地址 - - // 设置音频录制相关回调函数 - setCallBack(audioRecorder) { - audioRecorder.on('prepare', () => { // 设置'prepare'事件回调 - console.log('prepare success'); - audioRecorder.start(); // 调用start方法开始录制,并触发start回调 - }); - audioRecorder.on('start', () => { // 设置'start'事件回调 - console.log('audio recorder start success'); - audioRecorder.stop(); // 调用stop方法停止录制,并触发stop回调 - }); - audioRecorder.on('stop', () => { // 设置'stop'事件回调 - console.log('audio recorder stop success'); - audioRecorder.release(); // 调用release方法,释放资源,并触发release回调 - }); - audioRecorder.on('release', () => { // 设置'release'事件回调 - console.log('audio recorder release success'); - audioRecorder = undefined; - }); - audioRecorder.on('error', (error) => { // 设置'error'事件回调 - console.info(`audio error called, errName is ${error.name}`); - console.info(`audio error called, errCode is ${error.code}`); - console.info(`audio error called, errMessage is ${error.message}`); - }); - } - - // pathName是传入的录制文件名,例如:01.mp3,生成后的文件地址:/storage/media/100/local/files/Video/01.mp3 - // 使用mediaLibrary需要添加以下权限, ohos.permission.MEDIA_LOCATION、ohos.permission.WRITE_MEDIA、ohos.permission.READ_MEDIA - async getFd(pathName) { - let displayName = pathName; - const mediaTest = mediaLibrary.getMediaLibrary(); - let fileKeyObj = mediaLibrary.FileKey; - let mediaType = mediaLibrary.MediaType.VIDEO; - let publicPath = await mediaTest.getPublicDirectory(mediaLibrary.DirectoryType.DIR_VIDEO); - let dataUri = await mediaTest.createAsset(mediaType, displayName, publicPath); - if (dataUri != undefined) { - let args = dataUri.id.toString(); - let fetchOp = { - selections : fileKeyObj.ID + "=?", - selectionArgs : [args], - } - let fetchFileResult = await mediaTest.getFileAssets(fetchOp); - let fileAsset = await fetchFileResult.getAllObject(); - let fdNumber = await fileAsset[0].open('Rw'); - this.testFdNumber = "fd://" + fdNumber.toString(); - } - } - - async audioRecorderDemo() { - // 1.创建实例 - let audioRecorder = media.createAudioRecorder(); - // 2.设置回调 - this.setCallBack(audioRecorder); - await this.getFd('01.mp3'); // 调用getFd方法获取需要录制文件的fd地址 - // 3.设置录制参数 - let audioRecorderConfig = { - audioEncodeBitRate : 22050, - audioSampleRate : 22050, - numberOfChannels : 2, - uri : this.testFdNumber, // testFdNumber由getFd生成 - location : { latitude : 30, longitude : 130}, - audioEncoderMime : media.CodecMimeType.AUDIO_AAC, - fileFormat : media.ContainerFormatType.CFT_MPEG_4A, - } - audioRecorder.prepare(audioRecorderConfig); // 调用prepare方法,触发prepare回调函数 - } -} -``` - -## 相关实例 - -针对音频录制开发,有以下相关实例可供参考: - -- [`Recorder:`录音机(ArkTS)(API9)(Full SDK)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/BasicFeature/Media/Recorder) -- [音频播放器(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/Media/Audio_OH_ETS) diff --git a/zh-cn/application-dev/media/audio-recording-overview.md b/zh-cn/application-dev/media/audio-recording-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..6b9dca9f9c6b40d7f0dc6c5a983df851d22d71b2 --- /dev/null +++ b/zh-cn/application-dev/media/audio-recording-overview.md @@ -0,0 +1,17 @@ +# 音频录制开发概述 + +## 如何选择音频录制开发方式 + +在OpenHarmony系统中,多种API都提供了音频录制开发的支持,不同的API适用于不同录音输出格式、音频使用场景或不同开发语言。因此,选择合适的音频录制API,有助于降低开发工作量,实现更佳的音频录制效果。 + +- [AVRecorder](using-avrecorder-for-recording.md):功能较完善的音频、视频录制ArkTS/JS API,集成了音频输入录制、音频编码和媒体封装的功能。开发者可以直接调用设备硬件如麦克风录音,并生成m4a音频文件。 + +- [AudioCapturer](using-audiocapturer-for-recording.md):用于音频输入的的ArkTS/JS API,仅支持PCM格式,需要应用持续读取音频数据进行工作。应用可以在音频输出后添加数据处理,要求开发者具备音频处理的基础知识,适用于更专业、更多样化的媒体播放应用开发。 + +- [OpenSLES](using-opensl-es-for-recording.md):一套跨平台标准化的音频Native API,目前阶段唯一的音频类Native API,同样提供音频输入原子能力,仅支持PCM格式,适用于从其他嵌入式平台移植,或依赖在Native层实现音频输入功能的录音应用使用。 + +## 开发音频录制应用须知 + +应用可以调用麦克风录制音频,但该行为属于隐私敏感行为,在调用麦克风前,需要先向用户申请权限“ohos.permission.MICROPHONE”。 + +权限申请的流程请参考[访问控制授权申请指导](../security/accesstoken-guidelines.md)。如何使用和管理麦克风请参考[管理麦克风](mic-management.md)。 diff --git a/zh-cn/application-dev/media/audio-recording-stream-management.md b/zh-cn/application-dev/media/audio-recording-stream-management.md new file mode 100644 index 0000000000000000000000000000000000000000..801876008a7824085479664edd8e721695591b19 --- /dev/null +++ b/zh-cn/application-dev/media/audio-recording-stream-management.md @@ -0,0 +1,115 @@ +# 音频录制流管理 + +对于播放音频类的应用,开发者需要关注该应用的音频流的状态以做出相应的操作,比如监听到状态为结束时,及时提示用户录制已结束。 + +## 读取或监听应用内音频流状态变化 + +参考[使用AudioCapturer开发音频录制功能](using-audiocapturer-for-recording.md)或[audio.createAudioCapturer](../reference/apis/js-apis-audio.md#audiocreateaudiocapturer8),完成AudioRenderer的创建,然后可以通过以下两种方式查看音频流状态的变化: + +- 方法1:直接查看AudioCapturer的[state](../reference/apis/js-apis-audio.md#属性): + + ```ts + let audioCapturerState = audioCapturer.state; + console.info(`Current state is: ${audioCapturerState }`) + ``` + +- 方法2:注册stateChange监听AudioCapturer的状态变化: + + ```ts + audioCapturer.on('stateChange', (capturerState) => { + console.info(`State change to: ${capturerState}`) + }); + ``` + +获取state后可对照[AudioState](../reference/apis/js-apis-audio.md#audiostate8)来进行相应的操作,比如显示录制结束的提示等。 + +## 读取或监听所有录制流的变化 + +如果部分应用需要查询获取所有音频流的变化信息,可以通过AudioStreamManager读取或监听所有音频流的变化。 + +> **说明:** +> +> 对于标记为系统接口(system api)的音频流变化信息需要系统级别应用才可查看,若应用不是系统应用,将无法获取准确信息。 + +如下为音频流管理调用关系图: + +![Invoking relationship of recording stream management](figures/invoking-relationship-recording-stream-mgmt.png) + +在进行应用开发的过程中,开发者需要使用getStreamManager()创建一个AudioStreamManager实例,进而通过该实例管理音频流。开发者可通过调用on('audioCapturerChange')监听音频流的变化,在音频流状态变化、设备变化时获得通知,同时可通过off('audioCapturerChange')取消相关事件的监听。另外,开发者可以通过主动调用getCurrentAudioCapturerInfoArray()查询录制流的唯一ID、录制流客户端的UID、以及流状态等信息。 + +详细API含义可参考[音频管理API文档AudioStreamManager](../reference/apis/js-apis-audio.md#audiostreammanager9)。 + + +## 开发步骤及注意事项 + +1. 创建AudioStreamManager实例。 + 在使用AudioStreamManager的API前,需要使用getStreamManager()创建一个AudioStreamManager实例。 + + ```ts + import audio from '@ohos.multimedia.audio'; + let audioManager = audio.getAudioManager(); + let audioStreamManager = audioManager.getStreamManager(); + ``` + +2. 使用on('audioCapturerChange')监听音频录制流更改事件。 如果音频流监听应用需要在音频录制流状态变化、设备变化时获取通知,可以订阅该事件。 + + ```ts + audioStreamManager.on('audioCapturerChange', (AudioCapturerChangeInfoArray) => { + for (let i = 0; i < AudioCapturerChangeInfoArray.length; i++) { + console.info(`## CapChange on is called for element ${i} ##`); + console.info(`StreamId for ${i} is: ${AudioCapturerChangeInfoArray[i].streamId}`); + console.info(`Source for ${i} is: ${AudioCapturerChangeInfoArray[i].capturerInfo.source}`); + console.info(`Flag ${i} is: ${AudioCapturerChangeInfoArray[i].capturerInfo.capturerFlags}`); + let devDescriptor = AudioCapturerChangeInfoArray[i].deviceDescriptors; + for (let j = 0; j < AudioCapturerChangeInfoArray[i].deviceDescriptors.length; j++) { + console.info(`Id: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].id}`); + console.info(`Type: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].deviceType}`); + console.info(`Role: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].deviceRole}`); + console.info(`Name: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].name}`); + console.info(`Address: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].address}`); + console.info(`SampleRates: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].sampleRates[0]}`); + console.info(`ChannelCounts ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].channelCounts[0]}`); + console.info(`ChannelMask: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].channelMasks}`); + } + } + }); + ``` + +3. (可选)使用off('audioCapturerChange')取消监听音频录制流变化。 + + ```ts + audioStreamManager.off('audioCapturerChange'); + console.info('CapturerChange Off is called'); + ``` + +4. (可选)使用getCurrentAudioCapturerInfoArray()获取当前音频录制流的信息。 + 该接口可获取音频录制流唯一ID,音频录制客户端的UID,音频状态以及音频捕获器的其他信息。 + > **说明:** + > 对所有音频流状态进行监听的应用需要[申请权限](../security/accesstoken-guidelines.md)ohos.permission.USE_BLUETOOTH,否则无法获得实际的设备名称和设备地址信息,查询到的设备名称和设备地址(蓝牙设备的相关属性)将为空字符串。 + + ```ts + async function getCurrentAudioCapturerInfoArray(){ + await audioStreamManager.getCurrentAudioCapturerInfoArray().then( function (AudioCapturerChangeInfoArray) { + console.info('getCurrentAudioCapturerInfoArray Get Promise Called '); + if (AudioCapturerChangeInfoArray != null) { + for (let i = 0; i < AudioCapturerChangeInfoArray.length; i++) { + console.info(`StreamId for ${i} is: ${AudioCapturerChangeInfoArray[i].streamId}`); + console.info(`Source for ${i} is: ${AudioCapturerChangeInfoArray[i].capturerInfo.source}`); + console.info(`Flag ${i} is: ${AudioCapturerChangeInfoArray[i].capturerInfo.capturerFlags}`); + for (let j = 0; j < AudioCapturerChangeInfoArray[i].deviceDescriptors.length; j++) { + console.info(`Id: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].id}`); + console.info(`Type: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].deviceType}`); + console.info(`Role: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].deviceRole}`); + console.info(`Name: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].name}`); + console.info(`Address: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].address}`); + console.info(`SampleRates: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].sampleRates[0]}`); + console.info(`ChannelCounts ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].channelCounts[0]}`); + console.info(`ChannelMask: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].channelMasks}`); + } + } + } + }).catch((err) => { + console.error(`Invoke getCurrentAudioCapturerInfoArray failed, code is ${err.code}, message is ${err.message}`); + }); + } + ``` diff --git a/zh-cn/application-dev/media/audio-renderer.md b/zh-cn/application-dev/media/audio-renderer.md deleted file mode 100644 index 0f9436fc3b7d9e015d71869dc9739e7f5124e154..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/media/audio-renderer.md +++ /dev/null @@ -1,522 +0,0 @@ -# 音频渲染开发指导 - -## 简介 - -AudioRenderer提供了渲染音频文件和控制播放的接口,开发者可以通过本指导,了解如何在输出设备中播放音频文件并管理播放任务。同时,AudioRenderer支持音频中断的功能。 -开发者在调用AudioRenderer提供的各个接口时,需要理解以下名词: - -- **音频中断**:当优先级较高的音频流需要播放时,AudioRenderer会中断优先级较低的流。例如,当用户在收听音乐时有来电,则优先级较低音乐播放将被暂停。 -- **状态检查**:在进行应用开发的过程中,建议开发者通过on('stateChange')方法订阅AudioRenderer的状态变更。因为针对AudioRenderer的某些操作,仅在音频播放器在固定状态时才能执行。如果应用在音频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。 -- **异步操作**:为保证UI线程不被阻塞,大部分AudioRenderer调用都是异步的。对于每个API均提供了callback函数和Promise函数,以下示例均采用Promise函数,更多方式可参考[音频管理API文档AudioRenderer](../reference/apis/js-apis-audio.md#audiorenderer8)。 -- **焦点模式**:OpenHarmony中有两种焦点模式:**共享焦点模式**和**独立焦点模式**。其中,共享焦点模式是指,同一个应用创建的所有AudioRenderer对象共享一个焦点对象,应用内部无焦点转移,因此无法触发回调通知;独立焦点模式与之相反,即同一个应用创建的每个AudioRenderer对象都拥有独立的焦点对象,会发生焦点抢占,当应用内部发生焦点抢占,将会发生焦点转移,原本拥有焦点的AudioRenderer对象会获取到相关的回调通知。需要注意的是,默认情况下,应用创建的都是共享焦点,开发者可以调用setInterruptMode()来设置创建的焦点模式,完整示例请参考开发指导14。 - -## 运作机制 - -该模块提供了音频渲染模块的状态变化示意 - -**图1** 音频渲染状态示意图 - -![audio-renderer-state](figures/audio-renderer-state.png) - -**PREPARED状态:** 通过调用create()方法进入到该状态。
-**RUNNING状态:** 正在进行音频数据播放,可以在prepared状态通过调用start()方法进入此状态,也可以在pause状态和stopped状态通过调用start()方法进入此状态。
-**PAUSED状态:** 在running状态可以通过pause()方法暂停音频数据的播放,暂停播放之后可以通过调用start()方法继续音频数据播放。
-**STOPPED状态:** 在paused状态可以通过调用stop()方法停止音频数据的播放,在running状态可以通过stop()方法停止音频数据的播放。
-**RELEASED状态:** 在prepared、paused、stop等状态,用户均可通过release()方法释放掉所有占用的硬件和软件资源,并且不会再进入到其他的任何一种状态了。
- -## 开发指导 - -详细API含义可参考:[音频管理API文档AudioRenderer](../reference/apis/js-apis-audio.md#audiorenderer8) - -1. 使用createAudioRenderer()创建一个全局的AudioRenderer实例,以便后续步骤使用。 - 在audioRendererOptions中设置相关参数。该实例可用于音频渲染、控制和获取渲染状态,以及注册通知回调。 - - ```js - import audio from '@ohos.multimedia.audio'; - import fs from '@ohos.file.fs'; - - //音频渲染相关接口自测试 - @Entry - @Component - struct AudioRenderer1129 { - private audioRenderer: audio.AudioRenderer; - private bufferSize;//便于步骤3 write函数调用使用 - private audioRenderer1: audio.AudioRenderer; //便于步骤14 完整示例调用使用 - private audioRenderer2: audio.AudioRenderer; //便于步骤14 完整示例调用使用 - - async initAudioRender(){ - let audioStreamInfo = { - samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100, - channels: audio.AudioChannel.CHANNEL_1, - sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, - encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW - } - let audioRendererInfo = { - content: audio.ContentType.CONTENT_TYPE_SPEECH, - usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION, - rendererFlags: 0 // 0是音频渲染器的扩展标志位,默认为0 - } - let audioRendererOptions = { - streamInfo: audioStreamInfo, - rendererInfo: audioRendererInfo - } - this.audioRenderer = await audio.createAudioRenderer(audioRendererOptions); - console.log("Create audio renderer success."); - } - } - ``` - -2. 调用start()方法来启动/恢复播放任务。 - - ```js - async startRenderer() { - let state = this.audioRenderer.state; - // Renderer start时的状态应该是STATE_PREPARED、STATE_PAUSED和STATE_STOPPED之一. - if (state != audio.AudioState.STATE_PREPARED && state != audio.AudioState.STATE_PAUSED && - state != audio.AudioState.STATE_STOPPED) { - console.info('Renderer is not in a correct state to start'); - return; - } - - await this.audioRenderer.start(); - - state = this.audioRenderer.state; - if (state == audio.AudioState.STATE_RUNNING) { - console.info('Renderer started'); - } else { - console.error('Renderer start failed'); - } - } - ``` - 启动完成后,渲染器状态将变更为STATE_RUNNING,然后应用可以开始读取缓冲区。 - - -3. 调用write()方法向缓冲区写入数据。 - - 将需要播放的音频数据读入缓冲区,重复调用write()方法写入。请注意引入“import fs from '@ohos.file.fs';”,具体请参考步骤1。 - - ```js - async writeData(){ - // 此处是渲染器的合理的最小缓冲区大小(也可以选择其它大小的缓冲区) - this.bufferSize = await this.audioRenderer.getBufferSize(); - let dir = globalThis.fileDir; //不可直接访问,没权限,切记!!!一定要使用沙箱路径 - const filePath = dir + '/file_example_WAV_2MG.wav'; // 需要渲染的音乐文件 实际路径为:/data/storage/el2/base/haps/entry/files/file_example_WAV_2MG.wav - console.info(`file filePath: ${ filePath}`); - - let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY); - let stat = await fs.stat(filePath); //音乐文件信息 - let buf = new ArrayBuffer(this.bufferSize); - let len = stat.size % this.bufferSize == 0 ? Math.floor(stat.size / this.bufferSize) : Math.floor(stat.size / this.bufferSize + 1); - for (let i = 0;i < len; i++) { - let options = { - offset: i * this.bufferSize, - length: this.bufferSize - } - let readsize = await fs.read(file.fd, buf, options) - let writeSize = await new Promise((resolve,reject)=>{ - this.audioRenderer.write(buf,(err,writeSize)=>{ - if(err){ - reject(err) - }else{ - resolve(writeSize) - } - }) - }) - } - - fs.close(file) - await this.audioRenderer.stop(); //停止渲染 - await this.audioRenderer.release(); //释放资源 - } - ``` - -4. (可选)调用pause()方法或stop()方法暂停/停止渲染音频数据。 - - ```js - async pauseRenderer() { - let state = this.audioRenderer.state; - // 只有渲染器状态为STATE_RUNNING的时候才能暂停 - if (state != audio.AudioState.STATE_RUNNING) { - console.info('Renderer is not running'); - return; - } - - await this.audioRenderer.pause(); - - state = this.audioRenderer.state; - if (state == audio.AudioState.STATE_PAUSED) { - console.info('Renderer paused'); - } else { - console.error('Renderer pause failed'); - } - } - - async stopRenderer() { - let state = this.audioRenderer.state; - // 只有渲染器状态为STATE_RUNNING或STATE_PAUSED的时候才可以停止 - if (state != audio.AudioState.STATE_RUNNING && state != audio.AudioState.STATE_PAUSED) { - console.info('Renderer is not running or paused'); - return; - } - - await this.audioRenderer.stop(); - - state = this.audioRenderer.state; - if (state == audio.AudioState.STATE_STOPPED) { - console.info('Renderer stopped'); - } else { - console.error('Renderer stop failed'); - } - } - ``` - -5. (可选)调用drain()方法清空缓冲区。 - - ```js - async drainRenderer() { - let state = this.audioRenderer.state; - // 只有渲染器状态为STATE_RUNNING的时候才能使用drain() - if (state != audio.AudioState.STATE_RUNNING) { - console.info('Renderer is not running'); - return; - } - - await this.audioRenderer.drain(); - state = this.audioRenderer.state; - } - ``` - -6. 任务完成,调用release()方法释放相关资源。 - - AudioRenderer会使用大量的系统资源,所以请确保完成相关任务后,进行资源释放。 - - ```js - async releaseRenderer() { - let state = this.audioRenderer.state; - // 渲染器状态不是STATE_RELEASED或STATE_NEW状态,才能release - if (state == audio.AudioState.STATE_RELEASED || state == audio.AudioState.STATE_NEW) { - console.info('Renderer already released'); - return; - } - await this.audioRenderer.release(); - - state = this.audioRenderer.state; - if (state == audio.AudioState.STATE_RELEASED) { - console.info('Renderer released'); - } else { - console.info('Renderer release failed'); - } - } - ``` - -7. (可选)获取渲染器相关信息 - - 通过以下代码,可以获取渲染器的相关信息。 - - ```js - async getRenderInfo(){ - // 获取当前渲染器状态 - let state = this.audioRenderer.state; - // 获取渲染器信息 - let audioRendererInfo : audio.AudioRendererInfo = await this.audioRenderer.getRendererInfo(); - // 获取音频流信息 - let audioStreamInfo : audio.AudioStreamInfo = await this.audioRenderer.getStreamInfo(); - // 获取音频流ID - let audioStreamId : number = await this.audioRenderer.getAudioStreamId(); - // 获取纳秒形式的Unix时间戳 - let audioTime : number = await this.audioRenderer.getAudioTime(); - // 获取合理的最小缓冲区大小 - let bufferSize : number = await this.audioRenderer.getBufferSize(); - // 获取渲染速率 - let renderRate : audio.AudioRendererRate = await this.audioRenderer.getRenderRate(); - } - ``` - -8. (可选)设置渲染器相关信息 - - 通过以下代码,可以设置渲染器的相关信息。 - - ```js - async setAudioRenderInfo(){ - // 设置渲染速率为正常速度 - let renderRate : audio.AudioRendererRate = audio.AudioRendererRate.RENDER_RATE_NORMAL; - await this.audioRenderer.setRenderRate(renderRate); - // 设置渲染器音频中断模式为SHARE_MODE - let interruptMode : audio.InterruptMode = audio.InterruptMode.SHARE_MODE; - await this.audioRenderer.setInterruptMode(interruptMode); - // 设置一个流的音量为0.5 - let volume : number = 0.5; - await this.audioRenderer.setVolume(volume); - } - ``` - -9. (可选)使用on('audioInterrupt')方法订阅渲染器音频中断事件,使用off('audioInterrupt')取消订阅事件。 - - 当优先级更高或相等的Stream-B请求激活并使用输出设备时,Stream-A被中断。 - - 在某些情况下,框架会采取暂停播放、降低音量等强制操作,并通过InterruptEvent通知应用。在其他情况下,应用可以自行对InterruptEvent做出响应。 - - 在音频中断的情况下,应用可能会碰到音频数据写入失败的问题。所以建议不感知、不处理中断的应用在写入音频数据前,使用audioRenderer.state检查播放器状态。而订阅音频中断事件,可以获取到更多详细信息,具体可参考[InterruptEvent](../reference/apis/js-apis-audio.md#interruptevent9)。 - - 需要说明的是,本模块的订阅音频中断事件与[AudioManager](../reference/apis/js-apis-audio.md#audiomanager)模块中的on('interrupt')稍有不同。自api9以来,on('interrupt')和off('interrupt')均被废弃。在AudioRenderer模块,当开发者需要监听焦点变化事件时,只需要调用on('audioInterrupt')函数,当应用内部的AudioRenderer对象在start\stop\pause等动作发生时,会主动请求焦点,从而发生焦点转移,相关的AudioRenderer对象即可获取到对应的回调信息。但对除AudioRenderer的其他对象,例如FM、语音唤醒等,应用不会创建对象,此时可调用AudioManager中的on('interrupt')获取焦点变化通知。 - - ```js - async subscribeAudioRender(){ - this.audioRenderer.on('audioInterrupt', (interruptEvent) => { - console.info('InterruptEvent Received'); - console.info(`InterruptType: ${interruptEvent.eventType}`); - console.info(`InterruptForceType: ${interruptEvent.forceType}`); - console.info(`AInterruptHint: ${interruptEvent.hintType}`); - - if (interruptEvent.forceType == audio.InterruptForceType.INTERRUPT_FORCE) { - switch (interruptEvent.hintType) { - // 音频框架发起的强制暂停操作,为防止数据丢失,此时应该停止数据的写操作 - case audio.InterruptHint.INTERRUPT_HINT_PAUSE: - console.info('isPlay is false'); - break; - // 音频框架发起的强制停止操作,为防止数据丢失,此时应该停止数据的写操作 - case audio.InterruptHint.INTERRUPT_HINT_STOP: - console.info('isPlay is false'); - break; - // 音频框架发起的强制降低音量操作 - case audio.InterruptHint.INTERRUPT_HINT_DUCK: - break; - // 音频框架发起的恢复音量操作 - case audio.InterruptHint.INTERRUPT_HINT_UNDUCK: - break; - } - } else if (interruptEvent.forceType == audio.InterruptForceType.INTERRUPT_SHARE) { - switch (interruptEvent.hintType) { - // 提醒App开始渲染 - case audio.InterruptHint.INTERRUPT_HINT_RESUME: - this.startRenderer(); - break; - // 提醒App音频流被中断,由App自主决定是否继续(此处选择暂停) - case audio.InterruptHint.INTERRUPT_HINT_PAUSE: - console.info('isPlay is false'); - this.pauseRenderer(); - break; - } - } - }); - } - ``` - -10. (可选)使用on('markReach')方法订阅渲染器标记到达事件,使用off('markReach')取消订阅事件。 - - 注册markReach监听后,当渲染器渲染的帧数到达设定值时,会触发回调并返回设定的值。 - - ```js - async markReach(){ - this.audioRenderer.on('markReach', 50, (position) => { - if (position == 50) { - console.info('ON Triggered successfully'); - } - }); - this.audioRenderer.off('markReach'); // 取消markReach事件的订阅,后续将无法监听到“标记到达”事件 - } - ``` - -11. (可选)使用on('periodReach')方法订阅渲染器区间标记到达事件,使用off('periodReach')取消订阅事件。 - - 注册periodReach监听后,**每当**渲染器渲染的帧数到达设定值时,会触发回调并返回设定的值。 - - ```js - async periodReach(){ - this.audioRenderer.on('periodReach',10, (reachNumber) => { - console.info(`In this period, the renderer reached frame: ${reachNumber} `); - }); - - this.audioRenderer.off('periodReach'); // 取消periodReach事件的订阅,后续将无法监听到“区间标记到达”事件 - } - ``` - -12. (可选)使用on('stateChange')方法订阅渲染器音频状态变化事件。 - - 注册stateChange监听后,当渲染器的状态发生改变时,会触发回调并返回当前渲染器的状态。 - - ```js - async stateChange(){ - this.audioRenderer.on('stateChange', (audioState) => { - console.info('State change event Received'); - console.info(`Current renderer state is: ${audioState}`); - }); - } - ``` - -13. (可选)对on()方法的异常处理。 - - 在使用on()方法时,如果传入的字符串错误或传入的参数类型错误,程序会抛出异常,需要用try catch来捕获。 - - ```js - async errorCall(){ - try { - this.audioRenderer.on('invalidInput', () => { // 字符串不匹配 - }) - } catch (err) { - console.info(`Call on function error, ${err}`); // 程序抛出401异常 - } - try { - this.audioRenderer.on(1, () => { // 入参类型错误 - }) - } catch (err) { - console.info(`Call on function error, ${err}`); // 程序抛出6800101异常 - } - } - ``` - -14. (可选)on('audioInterrupt')方法完整示例。 - 请注意:在调用前声明audioRenderer1与audioRenderer2对象,具体请参考步骤1。 - 同一个应用中的AudioRender1和AudioRender2在创建时均设置了焦点模式为独立,并且调用on('audioInterrupt')监听焦点变化。刚开始AudioRender1拥有焦点,当AudioRender2获取到焦点时,audioRenderer1将收到焦点转移的通知,打印相关日志。如果AudioRender1和AudioRender2不将焦点模式设置为独立,则监听处理中的日志在应用运行过程中永远不会被打印。 - ```js - async runningAudioRender1(){ - let audioStreamInfo = { - samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, - channels: audio.AudioChannel.CHANNEL_1, - sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S32LE, - encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW - } - let audioRendererInfo = { - content: audio.ContentType.CONTENT_TYPE_MUSIC, - usage: audio.StreamUsage.STREAM_USAGE_MEDIA, - rendererFlags: 0 // 0是音频渲染器的扩展标志位,默认为0 - } - let audioRendererOptions = { - streamInfo: audioStreamInfo, - rendererInfo: audioRendererInfo - } - - //1.1 创建对象 - this.audioRenderer1 = await audio.createAudioRenderer(audioRendererOptions); - console.info("Create audio renderer 1 success."); - - //1.2 设置焦点模式为独立模式 :1 - this.audioRenderer1.setInterruptMode(1).then( data => { - console.info('audioRenderer1 setInterruptMode Success!'); - }).catch((err) => { - console.error(`audioRenderer1 setInterruptMode Fail: ${err}`); - }); - - //1.3 设置监听 - this.audioRenderer1.on('audioInterrupt', async(interruptEvent) => { - console.info(`audioRenderer1 on audioInterrupt : ${JSON.stringify(interruptEvent)}`) - }); - - //1.4 启动渲染 - await this.audioRenderer1.start(); - console.info('startAudioRender1 success'); - - //1.5 获取缓存区大小,此处是渲染器的合理的最小缓冲区大小(也可以选择其它大小的缓冲区) - const bufferSize = await this.audioRenderer1.getBufferSize(); - console.info(`audio bufferSize: ${bufferSize}`); - - //1.6 获取原始音频数据文件 - let dir = globalThis.fileDir; //不可直接访问,没权限,切记!!!一定要使用沙箱路径 - const path1 = dir + '/music001_48000_32_1.wav'; // 需要渲染的音乐文件 实际路径为:/data/storage/el2/base/haps/entry/files/music001_48000_32_1.wav - console.info(`audioRender1 file path: ${ path1}`); - let file1 = fs.openSync(path1, fs.OpenMode.READ_ONLY); - let stat = await fs.stat(path1); //音乐文件信息 - let buf = new ArrayBuffer(bufferSize); - let len = stat.size % this.bufferSize == 0 ? Math.floor(stat.size / this.bufferSize) : Math.floor(stat.size / this.bufferSize + 1); - - //1.7 通过audioRender对缓存区的原始音频数据进行渲染 - for (let i = 0;i < len; i++) { - let options = { - offset: i * this.bufferSize, - length: this.bufferSize - } - let readsize = await fs.read(file1.fd, buf, options) - let writeSize = await new Promise((resolve,reject)=>{ - this.audioRenderer1.write(buf,(err,writeSize)=>{ - if(err){ - reject(err) - }else{ - resolve(writeSize) - } - }) - }) - } - fs.close(file1) - await this.audioRenderer1.stop(); //停止渲染 - await this.audioRenderer1.release(); //释放资源 - } - - async runningAudioRender2(){ - let audioStreamInfo = { - samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, - channels: audio.AudioChannel.CHANNEL_1, - sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S32LE, - encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW - } - let audioRendererInfo = { - content: audio.ContentType.CONTENT_TYPE_MUSIC, - usage: audio.StreamUsage.STREAM_USAGE_MEDIA, - rendererFlags: 0 // 0是音频渲染器的扩展标志位,默认为0 - } - let audioRendererOptions = { - streamInfo: audioStreamInfo, - rendererInfo: audioRendererInfo - } - - //2.1 创建对象 - this.audioRenderer2 = await audio.createAudioRenderer(audioRendererOptions); - console.info("Create audio renderer 2 success."); - - //2.2 设置焦点模式为独立模式 :1 - this.audioRenderer2.setInterruptMode(1).then( data => { - console.info('audioRenderer2 setInterruptMode Success!'); - }).catch((err) => { - console.error(`audioRenderer2 setInterruptMode Fail: ${err}`); - }); - - //2.3 设置监听 - this.audioRenderer2.on('audioInterrupt', async(interruptEvent) => { - console.info(`audioRenderer2 on audioInterrupt : ${JSON.stringify(interruptEvent)}`) - }); - - //2.4 启动渲染 - await this.audioRenderer2.start(); - console.info('startAudioRender2 success'); - - //2.5 获取缓存区大小 - const bufferSize = await this.audioRenderer2.getBufferSize(); - console.info(`audio bufferSize: ${bufferSize}`); - - //2.6 获取原始音频数据文件 - let dir = globalThis.fileDir; //不可直接访问,没权限,切记!!!一定要使用沙箱路径 - const path2 = dir + '/music002_48000_32_1.wav'; // 需要渲染的音乐文件 实际路径为:/data/storage/el2/base/haps/entry/files/music002_48000_32_1.wav - console.info(`audioRender2 file path: ${ path2}`); - let file2 = fs.openSync(path2, fs.OpenMode.READ_ONLY); - let stat = await fs.stat(path2); //音乐文件信息 - let buf = new ArrayBuffer(bufferSize); - let len = stat.size % this.bufferSize == 0 ? Math.floor(stat.size / this.bufferSize) : Math.floor(stat.size / this.bufferSize + 1); - - //2.7 通过audioRender对缓存区的原始音频数据进行渲染 - for (let i = 0;i < len; i++) { - let options = { - offset: i * this.bufferSize, - length: this.bufferSize - } - let readsize = await fs.read(file2.fd, buf, options) - let writeSize = await new Promise((resolve,reject)=>{ - this.audioRenderer2.write(buf,(err,writeSize)=>{ - if(err){ - reject(err) - }else{ - resolve(writeSize) - } - }) - }) - } - fs.close(file2) - await this.audioRenderer2.stop(); //停止渲染 - await this.audioRenderer2.release(); //释放资源 - } - - //综合调用入口 - async test(){ - await this.runningAudioRender1(); - await this.runningAudioRender2(); - } - - ``` \ No newline at end of file diff --git a/zh-cn/application-dev/media/audio-routing-manager.md b/zh-cn/application-dev/media/audio-routing-manager.md deleted file mode 100644 index d91249e398ae6a903edfae903cfcf510b6bac4ec..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/media/audio-routing-manager.md +++ /dev/null @@ -1,112 +0,0 @@ -# 路由、设备管理开发指导 - -## 简介 - -AudioRoutingManager提供了音频路由、设备管理的方法。开发者可以通过本指导了解应用如何通过AudioRoutingManager获取当前工作的输入、输出音频设备,监听音频设备的连接状态变化,激活通信设备等。 - -## 运作机制 - -该模块提供了路由、设备管理模块常用接口 - -**图1** 路由、设备管理常用接口 - -![zh-ch_image_audio_routing_manager](figures/zh-ch_image_audio_routing_manager.png) - -**说明:** AudioRoutingManager主要接口有:获取设备列表信息、监听与取消监听设备连接状态、激活通信设备、查询通信设备激活状态。更多介绍请参考[API参考文档](../reference/apis/js-apis-audio.md)。 - - -## 开发指导 - -详细API含义可参考:[音频路由、设备管理API文档AudioRoutingManager](../reference/apis/js-apis-audio.md#audioroutingmanager9) - -1. 创建AudioRoutingManager实例。 - - 在使用AudioRoutingManager的API前,需要使用getRoutingManager创建一个AudioRoutingManager实例。 - - ```js - import audio from '@ohos.multimedia.audio'; - async loadAudioRoutingManager() { - var audioRoutingManager = await audio.getAudioManager().getRoutingManager(); - console.info('audioRoutingManager------create-------success.'); - } - - ``` - -2. (可选)获取设备列表信息、监听设备链接状态变化。 - - 如果开发者需要获取设备列表信息(输入、输出、分布式输入、分布式输出等),或者监听音频设备的链接状态变化时,可参考并调用以下接口。 - - ```js - import audio from '@ohos.multimedia.audio'; - //创建AudioRoutingManager实例 - async loadAudioRoutingManager() { - var audioRoutingManager = await audio.getAudioManager().getRoutingManager(); - console.info('audioRoutingManager------create-------success.'); - } - //获取全部音频设备信息(开发者可以根据自身需要填入适当的DeviceFlag) - async getDevices() { - await loadAudioRoutingManager(); - await audioRoutingManager.getDevices(audio.DeviceFlag.ALL_DEVICES_FLAG).then((data) => { - console.info(`getDevices success and data is: ${JSON.stringify(data)}.`); - }); - } - //监听音频设备状态变化 - async onDeviceChange() { - await loadAudioRoutingManager(); - await audioRoutingManager.on('deviceChange', audio.DeviceFlag.ALL_DEVICES_FLAG, (deviceChanged) => { - console.info('on device change type : ' + deviceChanged.type); - console.info('on device descriptor size : ' + deviceChanged.deviceDescriptors.length); - console.info('on device change descriptor : ' + deviceChanged.deviceDescriptors[0].deviceRole); - console.info('on device change descriptor : ' + deviceChanged.deviceDescriptors[0].deviceType); - }); - } - //取消监听音频设备状态变化 - async offDeviceChange() { - await loadAudioRoutingManager(); - await audioRoutingManager.off('deviceChange', (deviceChanged) => { - console.info('off device change type : ' + deviceChanged.type); - console.info('off device descriptor size : ' + deviceChanged.deviceDescriptors.length); - console.info('off device change descriptor : ' + deviceChanged.deviceDescriptors[0].deviceRole); - console.info('off device change descriptor : ' + deviceChanged.deviceDescriptors[0].deviceType); - }); - } - //综合调用:先查询所有设备,设置监听,然后开发者手动变更设备连接(例如有线耳机),再次查询所有设备,最后取消设备状态变化的监听。 - async test(){ - await getDevices(); - await onDeviceChange()(); - //开发者手动断开/连接设备 - await getDevices(); - await offDeviceChange(); - } - ``` - -3. (可选)设置通信设备激活并查询激活状态。 - - ```js - import audio from '@ohos.multimedia.audio'; - //创建AudioRoutingManager实例 - async loadAudioRoutingManager() { - var audioRoutingManager = await audio.getAudioManager().getRoutingManager(); - console.info('audioRoutingManager------create-------success.'); - } - //设置通信设备激活状态 - async setCommunicationDevice() { - await loadAudioRoutingManager(); - await audioRoutingManager.setCommunicationDevice(audio.CommunicationDeviceType.SPEAKER, true).then(() => { - console.info('setCommunicationDevice true is success.'); - }); - } - //查询通信设备激活状态 - async isCommunicationDeviceActive() { - await loadAudioRoutingManager(); - await audioRoutingManager.isCommunicationDeviceActive(audio.CommunicationDeviceType.SPEAKER).then((value) => { - console.info(`CommunicationDevice state is: ${value}.`); - }); - } - //综合调用:先设置设备激活,然后查询设备状态。 - async test(){ - await setCommunicationDevice(); - await isCommunicationDeviceActive(); - } - ``` - diff --git a/zh-cn/application-dev/media/audio-stream-manager.md b/zh-cn/application-dev/media/audio-stream-manager.md deleted file mode 100644 index eae42fd248eba633fa4ad771bbd92529b93bd59e..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/media/audio-stream-manager.md +++ /dev/null @@ -1,161 +0,0 @@ -# 音频流管理开发指导 - -## 简介 - -AudioStreamManager提供了音频流管理的方法。开发者可以通过本指导了解应用如何通过AudioStreamManager管理音频流。 - -## 运作机制 - -该模块提供了音频流管理调用关系图 - -**图1** 音频流管理调用关系图 - -![zh-ch_image_audio_stream_manager](figures/zh-ch_image_audio_stream_manager.png) - -**说明**:在进行应用开发的过程中,开发者需要使用getStreamManager()创建一个AudioStreamManager实例,进而通过该实例管理音频流。开发者可通过调用on('audioRendererChange')、on('audioCapturerChange')监听音频播放应用和音频录制应用,在应用状态变化、设备变化、音频属性变化时获得通知。同时可通过off('audioRendererChange')、off('audioCapturerChange')取消相关事件的监听。与此同时,开发者可以通过调用(可选)使用getCurrentAudioRendererInfoArray()获取当前音频播放应用的音频流唯一ID、音频播放客户端的UID、音频状态等信息,同理可调用getCurrentAudioCapturerInfoArray()获取音频录制应用的信息。 - -## 开发指导 - -详细API含义可参考:[音频管理API文档AudioStreamManager](../reference/apis/js-apis-audio.md#audiostreammanager9) - -1. 创建AudioStreamManager实例。 - - 在使用AudioStreamManager的API前,需要使用getStreamManager()创建一个AudioStreamManager实例。 - - ```js - var audioManager = audio.getAudioManager(); - var audioStreamManager = audioManager.getStreamManager(); - ``` - -2. (可选)使用on('audioRendererChange')监听音频渲染器更改事件。 -如果音频流监听应用需要在音频播放应用状态变化、设备变化、音频属性变化时获取通知,可以订阅该事件。更多事件请参考[API参考文档](../reference/apis/js-apis-audio.md)。 - - ```js - audioStreamManager.on('audioRendererChange', (AudioRendererChangeInfoArray) => { - for (let i = 0; i < AudioRendererChangeInfoArray.length; i++) { - AudioRendererChangeInfo = AudioRendererChangeInfoArray[i]; - console.info('## RendererChange on is called for ' + i + ' ##'); - console.info('StreamId for ' + i + ' is:' + AudioRendererChangeInfo.streamId); - console.info('ClientUid for ' + i + ' is:' + AudioRendererChangeInfo.clientUid); - console.info('Content for ' + i + ' is:' + AudioRendererChangeInfo.rendererInfo.content); - console.info('Stream for ' + i + ' is:' + AudioRendererChangeInfo.rendererInfo.usage); - console.info('Flag ' + i + ' is:' + AudioRendererChangeInfo.rendererInfo.rendererFlags); - console.info('State for ' + i + ' is:' + AudioRendererChangeInfo.rendererState); - var devDescriptor = AudioRendererChangeInfo.deviceDescriptors; - for (let j = 0; j < AudioRendererChangeInfo.deviceDescriptors.length; j++) { - console.info('Id:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].id); - console.info('Type:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].deviceType); - console.info('Role:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].deviceRole); - console.info('Name:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].name); - console.info('Address:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].address); - console.info('SampleRates:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].sampleRates[0]); - console.info('ChannelCounts' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].channelCounts[0]); - console.info('ChannelMask:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].channelMasks); - } - } - }); - ``` - -3. (可选)使用off('audioRendererChange')取消监听音频渲染器更改事件。 - - ```js - audioStreamManager.off('audioRendererChange'); - console.info('######### RendererChange Off is called #########'); - ``` - -4. (可选)使用on('audioCapturerChange')监听音频捕获器更改事件。 -如果音频流监听应用需要在音频录制应用状态变化、设备变化、音频属性变化时获取通知,可以订阅该事件。更多事件请参考[API参考文档](../reference/apis/js-apis-audio.md)。 - - ```js - audioStreamManager.on('audioCapturerChange', (AudioCapturerChangeInfoArray) => { - for (let i = 0; i < AudioCapturerChangeInfoArray.length; i++) { - console.info(' ## audioCapturerChange on is called for element ' + i + ' ##'); - console.info('StreamId for ' + i + 'is:' + AudioCapturerChangeInfoArray[i].streamId); - console.info('ClientUid for ' + i + 'is:' + AudioCapturerChangeInfoArray[i].clientUid); - console.info('Source for ' + i + 'is:' + AudioCapturerChangeInfoArray[i].capturerInfo.source); - console.info('Flag ' + i + 'is:' + AudioCapturerChangeInfoArray[i].capturerInfo.capturerFlags); - console.info('State for ' + i + 'is:' + AudioCapturerChangeInfoArray[i].capturerState); - for (let j = 0; j < AudioCapturerChangeInfoArray[i].deviceDescriptors.length; j++) { - console.info('Id:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].id); - console.info('Type:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].deviceType); - console.info('Role:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].deviceRole); - console.info('Name:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].name); - console.info('Address:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].address); - console.info('SampleRates:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].sampleRates[0]); - console.info('ChannelCounts' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].channelCounts[0]); - console.info('ChannelMask:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].channelMasks); - } - } - }); - ``` - -5. (可选)使用off('audioCapturerChange')取消监听音频捕获器更改事件。 - - ```js - audioStreamManager.off('audioCapturerChange'); - console.info('######### CapturerChange Off is called #########'); - ``` - -6. (可选)使用getCurrentAudioRendererInfoArray()获取当前音频渲染器的信息。 -该接口可获取音频流唯一ID,音频播放客户端的UID,音频状态以及音频播放器的其他信息。需注意的是若对第三方音频流监听应用未配置ohos.permission.USE_BLUETOOTH权限,则查询到的设备名称和设备地址为空字符串,若正确配置权限,则显示的实际的设备名称和设备地址信息。 - - ```js - await audioStreamManager.getCurrentAudioRendererInfoArray().then( function (AudioRendererChangeInfoArray) { - console.info('######### Get Promise is called ##########'); - if (AudioRendererChangeInfoArray != null) { - for (let i = 0; i < AudioRendererChangeInfoArray.length; i++) { - AudioRendererChangeInfo = AudioRendererChangeInfoArray[i]; - console.info('StreamId for ' + i +' is:' + AudioRendererChangeInfo.streamId); - console.info('ClientUid for ' + i + ' is:' + AudioRendererChangeInfo.clientUid); - console.info('Content ' + i + ' is:' + AudioRendererChangeInfo.rendererInfo.content); - console.info('Stream' + i +' is:' + AudioRendererChangeInfo.rendererInfo.usage); - console.info('Flag' + i + ' is:' + AudioRendererChangeInfo.rendererInfo.rendererFlags); - console.info('State for ' + i + ' is:' + AudioRendererChangeInfo.rendererState); - var devDescriptor = AudioRendererChangeInfo.deviceDescriptors; - for (let j = 0; j < AudioRendererChangeInfo.deviceDescriptors.length; j++) { - console.info('Id:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].id); - console.info('Type:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].deviceType); - console.info('Role:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].deviceRole); - console.info('Name:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].name); - console.info('Address:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].address); - console.info('SampleRates:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].sampleRates[0]); - console.info('ChannelCounts' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].channelCounts[0]); - console.info('ChannelMask:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].channelMasks); - } - } - } - }).catch((err) => { - console.log('getCurrentAudioRendererInfoArray :ERROR: ' + err.message); - }); - ``` - -7. (可选)使用getCurrentAudioCapturerInfoArray()获取当前音频捕获器的信息。 -该接口可获取音频流唯一ID,音频录制客户端的UID,音频状态以及音频捕获器的其他信息。需注意的是若对第三方音频流监听应用未配置ohos.permission.USE_BLUETOOTH权限,则查询到的设备名称和设备地址为空字符串,若正确配置权限,则显示的实际的设备名称和设备地址信息。 - - ```js - await audioStreamManager.getCurrentAudioCapturerInfoArray().then( function (AudioCapturerChangeInfoArray) { - console.info('getCurrentAudioCapturerInfoArray: **** Get Promise Called ****'); - if (AudioCapturerChangeInfoArray != null) { - for (let i = 0; i < AudioCapturerChangeInfoArray.length; i++) { - console.info('StreamId for ' + i + 'is:' + AudioCapturerChangeInfoArray[i].streamId); - console.info('ClientUid for ' + i + 'is:' + AudioCapturerChangeInfoArray[i].clientUid); - console.info('Source for ' + i + 'is:' + AudioCapturerChangeInfoArray[i].capturerInfo.source); - console.info('Flag ' + i + 'is:' + AudioCapturerChangeInfoArray[i].capturerInfo.capturerFlags); - console.info('State for ' + i + 'is:' + AudioCapturerChangeInfoArray[i].capturerState); - var devDescriptor = AudioCapturerChangeInfoArray[i].deviceDescriptors; - for (let j = 0; j < AudioCapturerChangeInfoArray[i].deviceDescriptors.length; j++) { - console.info('Id:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].id); - console.info('Type:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].deviceType); - console.info('Role:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].deviceRole); - console.info('Name:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].name) - console.info('Address:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].address); - console.info('SampleRates:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].sampleRates[0]); - console.info('ChannelCounts' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].channelCounts[0]); - console.info('ChannelMask:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].channelMasks); - } - } - } - }).catch((err) => { - console.log('getCurrentAudioCapturerInfoArray :ERROR: ' + err.message); - }); - ``` \ No newline at end of file diff --git a/zh-cn/application-dev/media/audio-volume-manager.md b/zh-cn/application-dev/media/audio-volume-manager.md deleted file mode 100644 index 3876c86e751018f67cccf75ac75fd0580a6a6ed0..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/media/audio-volume-manager.md +++ /dev/null @@ -1,127 +0,0 @@ -# 音量管理开发指导 - -## 简介 - -AudioVolumeManager提供了音量管理的方法。开发者可以通过本指导了解应用如何通过AudioVolumeManager获取指定流音量信息、监听铃声模式变化、设置麦克风静音等。 - -## 运作机制 - -该模块提供了音量管理模块常用接口 - -**图1** 音量管理常用接口 - -![zh-ch_image_audio_volume_manager](figures/zh-ch_image_audio_volume_manager.png) - -**说明:** AudioVolumeManager包含音量变化监听处理和音频音量组管理相关(AudioVolumeGroupManager),开发者调用AudioVolumeGroupManager的相关方法,需要先调用getVolumeGroupManager方法创建AudioVolumeGroupManager实例,从而调用对应的接口实现相应的功能,主要接口有:获取指定流的音量、设置麦克风静音、监听麦克风状态变化等。更多介绍请参考[API参考文档](../reference/apis/js-apis-audio.md)。 - -## 约束与限制 - -开发者在进行麦克风管理开发前,需要先对所开发的应用配置麦克风权限(ohos.permission.MICROPHONE),如果要设置麦克风状态,则需要配置音频管理配置权限(ohos.permission.MANAGE_AUDIO_CONFIG),需注意该权限为系统级别权限。配置方式请参见[访问控制授权申请](../security/accesstoken-guidelines.md#配置文件权限声明)。 - -## 开发指导 - -详细API含义可参考:[音量管理API文档AudioVolumeManager](../reference/apis/js-apis-audio.md#audiovolumemanager9) - -1. 创建AudioVolumeGroupManager实例。 - - 在使用AudioVolumeGroupManager的API前,需要使用getVolumeGroupManager创建一个AudioStreamManager实例。 - - ```js - import audio from '@ohos.multimedia.audio'; - async loadVolumeGroupManager() { - const groupid = audio.DEFAULT_VOLUME_GROUP_ID; - var audioVolumeGroupManager = await audio.getAudioManager().getVolumeManager().getVolumeGroupManager(groupid); - console.error('audioVolumeGroupManager create success.'); - } - - ``` - -2. (可选)获取音量信息、铃声模式。 - - 如果开发者需要获取指定音频流的音量信息(铃声、语音电话、媒体、语音助手等),或者获取当前设备是静音、震动、响铃模式,可参考并调用以下接口。更多事件请参考[API参考文档](../reference/apis/js-apis-audio.md)。 - - ```js - import audio from '@ohos.multimedia.audio'; - async loadVolumeGroupManager() { - const groupid = audio.DEFAULT_VOLUME_GROUP_ID; - var audioVolumeGroupManager = await audio.getAudioManager().getVolumeManager().getVolumeGroupManager(groupid); - console.info('audioVolumeGroupManager create success.'); - } - - //获取指定流的当前音量(范围为0 ~ 15) - async getVolume() { - await loadVolumeGroupManager(); - await audioVolumeGroupManager.getVolume(audio.AudioVolumeType.MEDIA).then((value) => { - console.info(`getVolume success and volume is: ${value}.`); - }); - } - //获取指定流的最小音量 - async getMinVolume() { - await loadVolumeGroupManager(); - await audioVolumeGroupManager.getMinVolume(audio.AudioVolumeType.MEDIA).then((value) => { - console.info(`getMinVolume success and volume is: ${value}.`); - }); - } - //获取指定流的最大音量 - async getMaxVolume() { - await loadVolumeGroupManager(); - await audioVolumeGroupManager.getMaxVolume(audio.AudioVolumeType.MEDIA).then((value) => { - console.info(`getMaxVolume success and volume is: ${value}.`); - }); - } - //获取当前铃声模式: 静音(0)| 震动(1) | 响铃(2) - async getRingerMode() { - await loadVolumeGroupManager(); - await audioVolumeGroupManager.getRingerMode().then((value) => { - console.info(`getRingerMode success and RingerMode is: ${value}.`); - }); - } - ``` - -3. (可选)查询、设置、监听麦克风状态。 - - 如果开发者需要获取、设置麦克风状态,或者监听麦克风状态变化等信息,可参考并调用以下接口。 - - ```js - import audio from '@ohos.multimedia.audio'; - async loadVolumeGroupManager() { - const groupid = audio.DEFAULT_VOLUME_GROUP_ID; - var audioVolumeGroupManager = await audio.getAudioManager().getVolumeManager().getVolumeGroupManager(groupid); - console.info('audioVolumeGroupManager create success.'); - } - - async on() { //监听麦克风状态变化 - await loadVolumeGroupManager(); - await audioVolumeGroupManager.audioVolumeGroupManager.on('micStateChange', (micStateChange) => { - console.info(`Current microphone status is: ${micStateChange.mute} `); - }); - } - - async isMicrophoneMute() { //查询麦克风是否静音 - await audioVolumeGroupManager.audioVolumeGroupManager.isMicrophoneMute().then((value) => { - console.info(`isMicrophoneMute is: ${value}.`); - }); - } - - async setMicrophoneMuteTrue() { //设置麦克风静音 - await loadVolumeGroupManager(); - await audioVolumeGroupManager.audioVolumeGroupManager.setMicrophoneMute(true).then(() => { - console.info('setMicrophoneMute to mute.'); - }); - } - - async setMicrophoneMuteFalse() { //取消麦克风静音 - await loadVolumeGroupManager(); - await audioVolumeGroupManager.audioVolumeGroupManager.setMicrophoneMute(false).then(() => { - console.info('setMicrophoneMute to not mute.'); - }); - } - async test(){ //综合调用:先设置监听,然后查询麦克风状态,设置麦克风静音后再查询状态,最后取消麦克风静音。 - await on(); - await isMicrophoneMute(); - await setMicrophoneMuteTrue(); - await isMicrophoneMute(); - await setMicrophoneMuteFalse(); - } - ``` - diff --git a/zh-cn/application-dev/media/av-overview.md b/zh-cn/application-dev/media/av-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..e69516b5c71aa873b4be350a65d5e0a78f434092 --- /dev/null +++ b/zh-cn/application-dev/media/av-overview.md @@ -0,0 +1,65 @@ +# 音视频概述 + +在音视频开发指导中,将介绍各种涉及音频、视频播放或录制功能场景的开发方式,指导开发者如何使用系统提供的音视频API实现对应功能。比如使用TonePlayer实现简单的提示音,当设备接收到新消息时,会发出短促的“滴滴”声;使用AVPlayer实现音乐播放器,循环播放一首音乐。 + +在每个功能中,会介绍多种实现方式以应对不同的使用场景,以及该场景相关的子功能点。比如在音频播放功能内,会同时介绍音频的并发策略、音量管理和输出设备等在操作系统中的处理方式,帮助开发者能够开发出功能覆盖更全面的应用。 + +本开发指导仅针对音视频播放或录制本身,由[@ohos.multimedia.audio](../reference/apis/js-apis-audio.md)(下文简称audio模块)和[@ohos.multimedia.media](../reference/apis/js-apis-media.md)(下文简称media模块)提供相关能力,不涉及UI界面、图形处理、媒体存储或其他相关领域功能。 + +## 开发说明 + +在开发音频功能之前,尤其是要实现处理音频数据的功能前,建议开发者先了解声学相关的知识,帮助理解操作系统提供的API是如何控制音频系统,从而开发出更易用、体验更好的音视频类应用。建议了解的相关概念包括但不限于: + +- 音频量化的过程:采样 > 量化 > 编码 + +- 音频量化过程的相关概念:模拟信号和数字信号、采样率、声道、采样格式、位宽、码率、常见编码格式(如AAC、MP3、PCM、WMA等)、常见封装格式(如WAV、MPA、FLAC、AAC、OGG等) + +在开发音乐、视频播放功能之前,建议了解流媒体播放的相关概念包括但不限于: + +- 播放过程:网络协议 > 容器格式 > 音视频编解码 > 图形/音频渲染 + +- 网络协议:比如HLS、HTTP/HTTPS;容器格式:比如mp4,mkv,mpeg-ts,webm。 + +- 编码格式:比如h263/h264/h265,MPEG4/MPEG2。 + +## 音频流介绍 + +在开发音频应用之前,还需要了解什么是音频流,它是OpenHarmony音频系统中的关键概念,在之后的章节中会多次提及。 + +音频流,是音频系统中对一个具备音频格式和音频使用场景信息的独立音频数据处理单元的定义,可以表示播放,也可以表示录制,并且具备独立音量调节和音频设备路由切换能力。 + +音频流基础信息通过[AudioStreamInfo](../reference/apis/js-apis-audio.md#audiostreaminfo8)表示,包含采样、声道、位宽、编码信息,是创建音频播放或录制流的必要参数,描述了音频数据的基本属性。在配置时开发者需要保证基础信息与传输的音频数据是相匹配的,音频系统才能正确处理数据。 + +### 音频流使用场景信息 + +除了基本属性,音频流还需要具备使用场景信息。基础信息只能对音频数据进行描述,但在实际的使用过程中,不同的音频流,在音量大小,设备路由,并发策略上是有区别的。系统就是通过音频流所附带的使用场景信息,为不同的音频流制定合适的处理策略,以达到最佳的音频用户体验。 + +- 播放场景 + 音频播放场景的信息,通过[StreamUsage](../reference/apis/js-apis-audio.md#streamusage)和[ContentType](../reference/apis/js-apis-audio.md#contenttype)进行描述。 + + StreamUsage指音频流本身的用途类型,包括媒体、语音通信、语音播报、通知、铃声等。 + + ContentType指音频流中数据的内容类型,包括语音、音乐、影视、通知、铃声等。 + +- 录制场景 + 音频流录制场景的信息,通过[SourceType](../reference/apis/js-apis-audio.md#sourcetype8)进行描述。 + + SourceType指音频流中录音源的类型,包括麦克风音频源、语音识别音频源、语音通话音频源等。 + +## 支持的音频格式 + +audio模块下的接口支持PCM编码,包括AudioRenderer、AudioCapturer、TonePlayer、OpenSL ES等。 + +音频格式说明: + +- 支持的常用的音频采样率(Hz):8000、11025、12000、16000、22050、24000、32000、44100、48000、64000、96000,具体参考枚举[AudioSamplingRate](../reference/apis/js-apis-audio.md#audiosamplingrate8)。 + 不同设备支持的采样率规格会存在差异。 + +- 支持单声道、双声道,具体参考[AudioChannel](../reference/apis/js-apis-audio.md#audiochannel8)。 + +- 支持的采样格式:U8(无符号8位整数)、S16LE(带符号的16位整数,小尾数)、S24LE(带符号的24位整数,小尾数)、S32LE(带符号的32位整数,小尾数)、F32LE(带符号的32位浮点数,小尾数),具体参考[AudioSampleFormat](../reference/apis/js-apis-audio.md#audiosampleformat8)。 + 由于系统限制,S24LE、S32LE、F32LE仅部分设备支持,请根据实际情况使用。 + + 小尾数指的是小端模式,即数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。这种存储模式将地址的高低和数据的位权有效结合起来,高地址部分权值高,低地址部分权值低。 + +media模块下的接口支持的音视频格式将在[AVPlayer和AVRecorder](avplayer-avrecorder-overview.md)的介绍中承载。 diff --git a/zh-cn/application-dev/media/avplayer-avrecorder-overview.md b/zh-cn/application-dev/media/avplayer-avrecorder-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..87d03fadb46d2c3d03d7fa10ad0cd5fe4ac7183f --- /dev/null +++ b/zh-cn/application-dev/media/avplayer-avrecorder-overview.md @@ -0,0 +1,148 @@ +# AVPlayer和AVRecorder + +media模块提供了[AVPlayer](#avplayer)和[AVRecorder](#avrecorder)用于播放、录制音视频。 + +## AVPlayer + +AVPlayer主要工作是将Audio/Video媒体资源(比如mp4/mp3/mkv/mpeg-ts等)转码为可供渲染的图像和可听见的音频模拟信号,并通过输出设备进行播放。 + +AVPlayer提供功能完善一体化播放能力,应用只需要提供流媒体来源,不负责数据解析和解码就可达成播放效果。 + + +### 音频播放 + +当使用AVPlayer开发音乐应用播放音频时,其交互关系如图所示。 + +**图1** 音频播放外部模块交互图   + +![Audio Playback Interaction Diagram](figures/audio-playback-interaction-diagram.png) + +音乐类应用通过调用JS接口层提供的AVPlayer接口实现相应功能时,框架层会通过播放服务(Player Framework)将资源解析成音频数据流(PCM),音频数据流经过软件解码后输出至音频服务(Audio Framework),由音频服务输出至音频驱动渲染,实现音频播放功能。完整的音频播放需要应用、Player Framework、Audio Framework、音频HDI共同实现。 + +图1中,数字标注表示需要数据与外部模块的传递。 + +1. 音乐应用将媒体资源传递给AVPlayer接口。 + +2. Player Framework将音频PCM数据流输出给Audio Framework,再由Audio Framework输出给音频HDI。 + +### 视频播放 + +当使用AVPlayer开发视频应用播放视频时,其交互关系如图所示。 + +**图2** 视频播放外部模块交互图   + +![Video playback interaction diagram](figures/video-playback-interaction-diagram.png) + +应用通过调用JS接口层提供的AVPlayer接口实现相应功能时,框架层会通过播放服务(Player Framework)解析成单独的音频数据流和视频数据流,音频数据流经过软件解码后输出至音频服务(Audio Framework),再至硬件接口层的音频HDI,实现音频播放功能。视频数据流经过硬件(推荐)/软件解码后输出至图形渲染服务(Graphic Framework),再输出至硬件接口层的显示HDI,完成图形渲染。 + +完整的视频播放需要:应用、XCompomemt、Player Framework、Graphic Framework、Audio Framework、显示HDI和音频HDI共同实现。 + +图2中,数字标注表示需要数据与外部模块的传递。 + +1. 应用从Xcomponent组件获取窗口SurfaceID,获取方式参考[XComponent](../reference/arkui-ts/ts-basic-components-xcomponent.md)。 + +2. 应用把媒体资源、SurfaceID传递给AVPlayer接口。 + +3. Player Framework把视频ES数据流输出给解码HDI,解码获得视频帧(NV12/NV21/RGBA)。 + +4. Player Framework把音频PCM数据流输出给Audio Framework,Audio Framework输出给音频HDI。 + +5. Player Framework把视频帧(NV12/NV21/RGBA)输出给Graphic Framework,Graphic Framework输出给显示HDI。 + +### 支持的格式与协议 + +推荐使用以下主流的播放格式,音视频容器、音视频编码属于内容创作者所掌握的专业领域,不建议应用开发者自制码流进行测试,以免产生无法播放、卡顿、花屏等兼容性问题。若发生此类问题不会影响系统,退出播放即可。 + +支持的协议如下: + +| 协议类型 | 协议描述 | +| -------- | -------- | +| 本地点播 | 协议格式:支持file descriptor,禁止file path | +| 网络点播 | 协议格式:支持http/https/hls | + +支持的音频播放格式如下: + +| 音频容器规格 | 规格描述 | +| -------- | -------- | +| m4a | 音频格式:AAC | +| aac | 音频格式:AAC | +| mp3 | 音频格式:MP3 | +| ogg | 音频格式:VORBIS | +| wav | 音频格式:PCM | + +> **说明:** +> +> 视频播放支持的视频格式分为必选规格和可选规格。必选规格为所有厂商均支持的视频格式。对于可选规格,厂商将基于实际情况决定是否实现。建议开发者做对应的兼容处理,保证应用功能全平台兼容。 + +| 视频格式 | 是否必选规格 | +| -------- | -------- | +| H264 | 是 | +| MPEG2 | 否 | +| MPEG4 | 否 | +| H263 | 否 | +| VP8 | 否 | + +支持的视频播放格式和主流分辨率如下: + +| 视频容器规格 | 规格描述 | 分辨率 | +| -------- | -------- | -------- | +| mp4 | 视频格式:H264/MPEG2/MPEG4/H263
音频格式:AAC/MP3 | 主流分辨率,如4K/1080P/720P/480P/270P | +| mkv | 视频格式:H264/MPEG2/MPEG4/H263
音频格式:AAC/MP3 | 主流分辨率,如4K/1080P/720P/480P/270P | +| ts | 视频格式:H264/MPEG2/MPEG4
音频格式:AAC/MP3 | 主流分辨率,如4K/1080P/720P/480P/270P | +| webm | 视频格式:VP8
音频格式:VORBIS | 主流分辨率,如4K/1080P/720P/480P/270P | + +## AVRecorder + +AVRecorder主要工作是捕获音频信号,接收视频信号,完成音视频编码并保存到文件中,帮助开发者轻松实现音视频录制功能,包括开始录制、暂停录制、恢复录制、停止录制、释放资源等功能控制。它允许调用者指定录制的编码格式、封装格式、文件路径等参数。 + +**图3** 视频录制外部模块交互图   + +![Video recording interaction diagram](figures/video-recording-interaction-diagram.png) + +- 音频录制:应用通过调用JS接口层提供的AVRecorder接口实现音频录制时,框架层会通过录制服务(Player Framework),调用音频服务(Audio Framework)通过音频HDI捕获音频数据,通过软件编码封装后保存至文件中,实现音频录制功能。 + +- 视频录制:应用通过调用JS接口层提供的AVRecorder接口实现视频录制时,先通过Camera接口调用相机服务(Camera Framework)通过视频HDI捕获图像数据送至框架层的录制服务,录制服务将图像数据通过视频编码HDI编码,再将编码后的图像数据封装至文件中,实现视频录制功能。 + +通过音视频录制组合,可分别实现纯音频录制、纯视频录制,音视频录制。 + +图3中,数字标注表示需要数据与外部模块的传递。 + +1. 应用通过AVRecorder接口从录制服务获取SurfaceID。 + +2. 应用将SurfaceID设置给相机服务,相机服务可以通过SurfaceID获取到Surface。相机服务通过视频HDI捕获图像数据送至框架层的媒体服务。 + +3. 相机服务通过Surface将视频数据传递给录制服务。 + +4. 录制服务通过视频编码HDI模块将视频数据编码。 + +5. 录制服务将音频参数设置给音频服务,并从音频服务获取到音频数据。 + +### 支持的格式 + +支持的音频源如下: + +| 音频源类型 | 说明 | +| -------- | -------- | +| mic | 系统麦克风作为音频源输入。 | + +支持的视频源如下: + +| 视频源类型 | 说明 | +| -------- | -------- | +| surface_yuv | 输入surface中携带的是raw data。 | +| surface_es | 输入surface中携带的是ES data。 | + +支持的音视频编码格式如下: + +| 音视频编码格式 | 说明 | +| -------- | -------- | +| audio/mp4a-latm | 音频/mp4a-latm类型 | +| video/mp4v-es | 视频/mpeg4类型 | +| video/avc | 视频/avc类型 | + +支持的输出文件格式如下: + +| 输出文件格式 | 说明 | +| -------- | -------- | +| mp4 | 视频的容器格式,MP4。 | +| m4a | 音频的容器格式,M4A。 | diff --git a/zh-cn/application-dev/media/avplayer-playback.md b/zh-cn/application-dev/media/avplayer-playback.md deleted file mode 100644 index 22891308f9bb0af7eb47c94da7f06833557e510c..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/media/avplayer-playback.md +++ /dev/null @@ -1,493 +0,0 @@ -# AVPlayer播放器开发指导 - -## 简介 - -AVPlayer主要工作是将Audio/Video媒体资源转码为可供渲染的图像和可听见的音频模拟信号,并通过输出设备进行播放,同时对播放任务进行管理,包括开始播放、暂停播放、停止播放、释放资源、设置音量、跳转播放位置、获取轨道信息等功能控制。 - -## 运作机制 - -该模块提供了播放状态变化示意图[AVPlayerState](../reference/apis/js-apis-media.md#avplayerstate9)、音频播放外部模块交互图和视频播放外部模块交互图。 - -**图1** 播放状态变化示意图 - -![zh-ch_image_avplayer_state_machine](figures/zh-ch_image_avplayer_state_machine.png) - - -**图2** 音频播放外部模块交互图 - -![zh-ch_image_avplayer_audio](figures/zh-ch_image_avplayer_audio.png) - -**说明**:应用通过调用JS接口层提供的AVPlayer js接口实现相应功能时,框架层会通过Player Framework的播放服务解析成音频数据流,音频数据流经过软件解码后输出至Audio Framework的音频服务,由音频子系统输出至硬件接口层的音频HDI,实现音频播放功能。完整的音乐播放器工作需要:应用(应用适配)、Player Framework、Audio Framework、Audio HDI(驱动适配)共同实现。 - -*注意:音频播放需要音频子系统配合* - -1. 应用把url传递给AVPlayer JS。 -2. 播放服务把音频PCM数据流输出给音频服务,音频服务输出给Audio HDI。 - - -**图3** 视频播放外部模块交互图 - -![zh-ch_image_avplayer_video](figures/zh-ch_image_avplayer_video.png) - -**说明**:应用通过调用JS接口层提供的AVPlayer js接口实现相应功能时,框架层会通过Player Framework的播放服务解析成单独的音频数据流和视频数据流,音频数据流经过软件解码后输出至Audio Framework的音频服务,由音频子系统输出至硬件接口层的音频HDI,实现音频播放功能。视频数据流经过硬件(推荐)/软件解码后输出至Graphic Framework的渲染服务(Renderer Service),由RS子系统输出至硬件接口层的显示HDI。完整的视频播放器工作需要:应用(应用适配)、XCompomemt组件、Player Framework、Graphic Framework、Audio Framework、Display HDI(驱动适配)和Audio HDI(驱动适配)共同实现。 - -*注意:视频播放需要显示、音频、解码等多个子系统配合。* - -1. 应用从Xcomponent组件获取surfaceID,[获取方式](../reference/arkui-ts/ts-basic-components-xcomponent.md)。 -2. 应用把url、surfaceID传递给AVPlayer JS。 -3. 播放服务把视频ES数据流输出给Codec HDI,解码获得视频帧(NV12/NV21/RGBA)。 -4. 播放服务把音频PCM数据流输出给音频服务,音频服务输出给Audio HDI。 -5. 播放服务把视频帧(NV12/NV21/RGBA)输出给RS服务,RS服务输出给Display HDI。 - -## 兼容性说明 - -视频播放支持的视频格式分必选规格和可选规格。必选规格为所有厂商均支持的视频格式。对于可选规格,厂商将基于实际情况决定是否实现。建议开发者做兼容处理,保证全平台兼容。 -推荐使用主流的播放格式和主流分辨率,不建议开发者自制非常或者异常码流,以免产生无法播放、卡住、花屏等兼容性问题。若发生此类问题不会影响系统,退出码流播放即可。 - -| 视频格式 | 是否必选规格 | -|:--------:|:-----:| -| H264 | 是 | -| MPEG2 | 否 | -| MPEG4 | 否 | -| H263 | 否 | -| VP8 | 否 | - -主流的播放格式和主流分辨率如下: - -| 视频容器规格 | 规格描述 | 分辨率 | -| :----------: | :-----------------------------------------------: | :--------------------------------: | -| mp4 | 视频格式:H264/MPEG2/MPEG4/H263 音频格式:AAC/MP3 | 主流分辨率,如1080P/720P/480P/270P | -| mkv | 视频格式:H264/MPEG2/MPEG4/H263 音频格式:AAC/MP3 | 主流分辨率,如1080P/720P/480P/270P | -| ts | 视频格式:H264/MPEG2/MPEG4 音频格式:AAC/MP3 | 主流分辨率,如1080P/720P/480P/270P | -| webm | 视频格式:VP8 音频格式:VORBIS | 主流分辨率,如1080P/720P/480P/270P | - -| 音频容器规格 | 规格描述 | -| :----------: | :----------: | -| m4a | 音频格式:AAC | -| aac | 音频格式:AAC | -| mp3 | 音频格式:MP3 | -| ogg | 音频格式:VORBIS | -| wav | 音频格式:PCM | - -## 开发指导 - -详细API含义可参考:[媒体服务API文档AVPlayer](../reference/apis/js-apis-media.md#avplayer9) - -### 播放流程说明 - -播放的全流程场景包含:创建实例,设置资源,设置窗口(视频),准备播放(获取轨道信息/音量/倍速/焦点模式/缩放模式/设置bitrates),播控(播放/暂停/Seek/音量/停止),重置资源,销毁播放 - -1:创建实例[createAVPlayer()](../reference/apis/js-apis-media.md#mediacreateavplayer9),AVPlayer初始化[idle](#avplayer_state)状态 - -2:设置业务需要的监听事件,搭配全流程场景使用 - -3:设置资源 [url](../reference/apis/js-apis-media.md#avplayer_属性),AVPlayer进入[initialized](#avplayer_state)状态,此时可以设置视频窗口 [surfaceId](../reference/apis/js-apis-media.md#avplayer_属性),支持的规格可参考:[AVPlayer属性说明](../reference/apis/js-apis-media.md#avplayer_属性) - -4:准备播放 [prepare()](../reference/apis/js-apis-media.md#avplayer_prepare),AVPlayer进入[prepared](#avplayer_state)状态 - -5:视频播控:播放 [play()](../reference/apis/js-apis-media.md#avplayer_play),暂停 [pause()](../reference/apis/js-apis-media.md#avplayer_pause),跳转 [seek()](../reference/apis/js-apis-media.md#avplayer_seek),停止 [stop()](../reference/apis/js-apis-media.md#avplayer_stop) 等操作 - -6:重置资源 [reset()](../reference/apis/js-apis-media.md#avplayer_reset),AVPlayer重新进入[idle](#avplayer_state)状态,允许更换资源 [url](../reference/apis/js-apis-media.md#avplayer_属性) - -7:销毁播放 [release()](../reference/apis/js-apis-media.md#avplayer_release),AVPlayer进入[released](#avplayer_state)状态,退出播放 - -> **说明:** -> -> prepared/playing/paused/compeled 状态时,播放引擎处于工作状态,这需要占用系统较多的运行内存,当客户端暂时不使用播放器时,要求调用 reset() 或 release() 回收。 - -### 监听事件 - -| 事件类型 | 说明 | -| ------------------------------------------------- | ------------------------------------------------------------ | -| stateChange | 必要事件,监听播放器的状态机 | -| error | 必要事件,监听播放器的错误信息 | -| durationUpdate | 用于进度条,监听进度条长度,刷新资源时长 | -| timeUpdate | 用于进度条,监听进度条当前位置,刷新当前时间 | -| seekDone | 响应api调用,监听seek()请求完成情况 | -| speedDone | 响应api调用,监听setSpeed()请求完成情况 | -| volumeChange | 响应api调用,监听setVolume()请求完成情况 | -| bitrateDone | 响应api调用,用于HLS协议流,监听setBitrate()请求完成情况 | -| availableBitrates | 用于HLS协议流,监听HLS资源的可选bitrates,用于setBitrate() | -| bufferingUpdate | 用于网络播放,监听网络播放缓冲信息 | -| startRenderFrame | 用于视频播放,监听视频播放首帧渲染时间 | -| videoSizeChange | 用于视频播放,监听视频播放的宽高信息,可用于调整窗口大小、比例 | -| audioInterrupt | 用于视频播放,监听音频焦点切换信息,搭配属性audioInterruptMode使用 | - -### - -### 全量接口示例 - -```js -import media from '@ohos.multimedia.media' -import audio from '@ohos.multimedia.audio'; -import fs from '@ohos.file.fs' - -const TAG = 'AVPlayerDemo:' -export class AVPlayerDemo { - private count:number = 0 - private avPlayer - private surfaceID:string // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接见上面Xcomponent创建方法 - - // 注册avplayer回调函数 - setAVPlayerCallback() { - // 状态机变化回调函数 - this.avPlayer.on('stateChange', async (state, reason) => { - switch (state) { - case 'idle': // 成功调用reset接口后触发该状态机上报 - console.info(TAG + 'state idle called') - this.avPlayer.release() // 释放avplayer对象 - break; - case 'initialized': // avplayer 设置播放源后触发该状态上报 - console.info(TAG + 'state initialized called ') - this.avPlayer.surfaceId = this.surfaceID // 设置显示画面,当播放的资源为纯音频时无需设置 - this.avPlayer.prepare().then(() => { - console.info(TAG+ 'prepare success'); - }, (err) => { - console.error(TAG + 'prepare filed,error message is :' + err.message) - }) - break; - case 'prepared': // prepare调用成功后上报该状态机 - console.info(TAG + 'state prepared called') - this.avPlayer.play() // 调用播放接口开始播放 - break; - case 'playing': // play成功调用后触发该状态机上报 - console.info(TAG + 'state playing called') - if (this.count == 0) { - this.avPlayer.pause() // 调用暂停播放接口 - } else { - this.avPlayer.seek(10000, media.SeekMode.SEEK_PREV_SYNC) // 前向seek置10秒处,触发seekDone回调函数 - } - break; - case 'paused': // pause成功调用后触发该状态机上报 - console.info(TAG + 'state paused called') - if (this.count == 0) { - this.count++ - this.avPlayer.play() // 继续调用播放接口开始播放 - } - break; - case 'completed': // 播放结束后触发该状态机上报 - console.info(TAG + 'state completed called') - this.avPlayer.stop() //调用播放结束接口 - break; - case 'stopped': // stop接口成功调用后触发该状态机上报 - console.info(TAG + 'state stopped called') - this.avPlayer.reset() // 调用reset接口初始化avplayer状态 - break; - case 'released': - console.info(TAG + 'state released called') - break; - case 'error': - console.info(TAG + 'state error called') - break; - default: - console.info(TAG + 'unkown state :' + state) - break; - } - }) - // 时间上报监听函数 - this.avPlayer.on('timeUpdate', (time:number) => { - console.info(TAG + 'timeUpdate success,and new time is :' + time) - }) - // 音量变化回调函数 - this.avPlayer.on('volumeChange', (vol:number) => { - console.info(TAG + 'volumeChange success,and new volume is :' + vol) - this.avPlayer.setSpeed(media.AVPlayerSpeed.SPEED_FORWARD_2_00_X) // 设置两倍速播放,并触发speedDone回调 - }) - // 视频播放结束触发回调 - this.avPlayer.on('endOfStream', () => { - console.info(TAG + 'endOfStream success') - }) - // seek操作回调函数 - this.avPlayer.on('seekDone', (seekDoneTime:number) => { - console.info(TAG + 'seekDone success,and seek time is:' + seekDoneTime) - this.avPlayer.setVolume(0.5) // 设置音量为0.5,并触发volumeChange回调函数 - }) - // 设置倍速播放回调函数 - this.avPlayer.on('speedDone', (speed:number) => { - console.info(TAG + 'speedDone success,and speed value is:' + speed) - }) - // bitrate设置成功回调函数 - this.avPlayer.on('bitrateDone', (bitrate:number) => { - console.info(TAG + 'bitrateDone success,and bitrate value is:' + bitrate) - }) - // 缓冲上报回调函数 - this.avPlayer.on('bufferingUpdate', (infoType: media.BufferingInfoType, value: number) => { - console.info(TAG + 'bufferingUpdate success,and infoType value is:' + infoType + ', value is :' + value) - }) - // 首帧上报回调函数 - this.avPlayer.on('startRenderFrame', () => { - console.info(TAG + 'startRenderFrame success') - }) - // 视频宽高上报回调函数 - this.avPlayer.on('videoSizeChange', (width: number, height: number) => { - console.info(TAG + 'videoSizeChange success,and width is:' + width + ', height is :' + height) - }) - // 焦点上报回调函数 - this.avPlayer.on('audioInterrupt', (info: audio.InterruptEvent) => { - console.info(TAG + 'audioInterrupt success,and InterruptEvent info is:' + info) - }) - // HLS上报所有支持的比特率 - this.avPlayer.on('availableBitrates', (bitrates: Array) => { - console.info(TAG + 'availableBitrates success,and availableBitrates length is:' + bitrates.length) - }) - } - - async avPlayerDemo() { - // 创建avPlayer实例对象 - this.avPlayer = await media.createAVPlayer() - let fdPath = 'fd://' - let pathDir = "/data/storage/el2/base/haps/entry/files" // pathDir在FA模型和Stage模型的获取方式不同,请参考开发步骤首行的说明,根据实际情况自行获取。 - // path路径的码流可通过"hdc file send D:\xxx\H264_AAC.mp4 /data/app/el2/100/base/ohos.acts.multimedia.media.avplayer/haps/entry/files" 命令,将其推送到设备上 - let path = pathDir + '/H264_AAC.mp4' - let file = await fs.open(path) - fdPath = fdPath + '' + file.fd - this.avPlayer.url = fdPath - } -} -``` - -### 正常播放场景 - -```js -import media from '@ohos.multimedia.media' -import fs from '@ohos.file.fs' - -const TAG = 'AVPlayerDemo:' -export class AVPlayerDemo { - private avPlayer - private surfaceID:string // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接见上面Xcomponent创建方法 - - // 注册avplayer回调函数 - setAVPlayerCallback() { - // 状态机变化回调函数 - this.avPlayer.on('stateChange', async (state, reason) => { - switch (state) { - case 'idle': // 成功调用reset接口后触发该状态机上报 - console.info(TAG + 'state idle called') - break; - case 'initialized': // avplayer 设置播放源后触发该状态上报 - console.info(TAG + 'state initialized called ') - this.avPlayer.surfaceId = this.surfaceID // 设置显示画面,当播放的资源为纯音频时无需设置 - this.avPlayer.prepare().then(() => { - console.info(TAG+ 'prepare success'); - }, (err) => { - console.error(TAG + 'prepare filed,error message is :' + err.message) - }) - break; - case 'prepared': // prepare调用成功后上报该状态机 - console.info(TAG + 'state prepared called') - this.avPlayer.play() // 调用播放接口开始播放 - break; - case 'playing': // play成功调用后触发该状态机上报 - console.info(TAG + 'state playing called') - break; - case 'paused': // pause成功调用后触发该状态机上报 - console.info(TAG + 'state paused called') - break; - case 'completed': // 播放结束后触发该状态机上报 - console.info(TAG + 'state completed called') - this.avPlayer.stop() //调用播放结束接口 - break; - case 'stopped': // stop接口成功调用后触发该状态机上报 - console.info(TAG + 'state stopped called') - this.avPlayer.release() // 调用reset接口初始化avplayer状态 - break; - case 'released': - console.info(TAG + 'state released called') - break; - case 'error': - console.info(TAG + 'state error called') - break; - default: - console.info(TAG + 'unkown state :' + state) - break; - } - }) - } - - async avPlayerDemo() { - // 创建avPlayer实例对象 - this.avPlayer = await media.createAVPlayer() - let fileDescriptor = undefined - // 使用资源管理模块的getRawFileDescriptor获取集成在应用中的媒体资源,并使用AVPlayer的fdSrc属性完成媒体资源初始化 - // 其中的参数fd/offset/length定义请查看媒体API文档,globalThis.abilityContext参数为系统环境变量,在系统启动时在主界面保存为全局变量 - await globalThis.abilityContext.resourceManager.getRawFileDescriptor('H264_AAC.mp4').then((value) => { - fileDescriptor = {fd: value.fd, offset: value.offset, length: value.length} - }) - this.avPlayer.fdSrc = fileDescriptor - } -} -``` - -### 单曲循环场景 - -```js -import media from '@ohos.multimedia.media' -import fs from '@ohos.file.fs' - -const TAG = 'AVPlayerDemo:' -export class AVPlayerDemo { - private count:number = 0 - private avPlayer - private surfaceID:string // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接见上面Xcomponent创建方法 - - // 注册avplayer回调函数 - setAVPlayerCallback() { - // 状态机变化回调函数 - this.avPlayer.on('stateChange', async (state, reason) => { - switch (state) { - case 'idle': // 成功调用reset接口后触发该状态机上报 - console.info(TAG + 'state idle called') - break; - case 'initialized': // avplayer 设置播放源后触发该状态上报 - console.info(TAG + 'state initialized called ') - this.avPlayer.surfaceId = this.surfaceID // 设置显示画面,当播放的资源为纯音频时无需设置 - this.avPlayer.prepare().then(() => { - console.info(TAG+ 'prepare success'); - }, (err) => { - console.error(TAG + 'prepare filed,error message is :' + err.message) - }) - break; - case 'prepared': // prepare调用成功后上报该状态机 - console.info(TAG + 'state prepared called') - this.avPlayer.loop = true // 设置单曲循环播放,单曲循环播放至结尾后会触发endOfStream回调 - this.avPlayer.play() // 调用播放接口开始播放 - break; - case 'playing': // play成功调用后触发该状态机上报 - console.info(TAG + 'state playing called') - break; - case 'paused': // pause成功调用后触发该状态机上报 - console.info(TAG + 'state paused called') - break; - case 'completed': // 播放结束后触发该状态机上报 - console.info(TAG + 'state completed called') - // 当第二次触发endOfStream回调后取消循环播放,再次播放到结尾后触发completed状态机上报 - this.avPlayer.stop() //调用播放结束接口 - break; - case 'stopped': // stop接口成功调用后触发该状态机上报 - console.info(TAG + 'state stopped called') - this.avPlayer.release() // 调用reset接口初始化avplayer状态 - break; - case 'released': - console.info(TAG + 'state released called') - break; - case 'error': - console.info(TAG + 'state error called') - break; - default: - console.info(TAG + 'unkown state :' + state) - break; - } - }) - // 视频播放结束触发回调 - this.avPlayer.on('endOfStream', () => { - console.info(TAG + 'endOfStream success') - if (this.count == 1) { - this.avPlayer.loop = false // 取消循环播放 - } else { - this.count++ - } - }) - } - - async avPlayerDemo() { - // 创建avPlayer实例对象 - this.avPlayer = await media.createAVPlayer() - let fdPath = 'fd://' - let pathDir = "/data/storage/el2/base/haps/entry/files" // pathDir在FA模型和Stage模型的获取方式不同,请参考开发步骤首行的说明,根据实际情况自行获取。 - // path路径的码流可通过"hdc file send D:\xxx\H264_AAC.mp4 /data/app/el2/100/base/ohos.acts.multimedia.media.avplayer/haps/entry/files" 命令,将其推送到设备上 - let path = pathDir + '/H264_AAC.mp4' - let file = await fs.open(path) - fdPath = fdPath + '' + file.fd - this.avPlayer.url = fdPath - } -} -``` -### 视频切换场景 - -```js -import media from '@ohos.multimedia.media' -import fs from '@ohos.file.fs' - -const TAG = 'AVPlayerDemo:' -export class AVPlayerDemo { - private count:number = 0 - private avPlayer - private surfaceID:string // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接见上面Xcomponent创建方法 - - async nextVideo() { - let fdPath = 'fd://' - let pathDir = "/data/storage/el2/base/haps/entry/files" // pathDir在FA模型和Stage模型的获取方式不同,请参考开发步骤首行的说明,根据实际情况自行获取。 - // path路径的码流可通过"hdc file send D:\xxx\H264_MP3.mp4 /data/app/el2/100/base/ohos.acts.multimedia.media.avplayer/haps/entry/files" 命令,将其推送到设备上 - let path = pathDir + '/H264_MP3.mp4' - let file = await fs.open(path) - fdPath = fdPath + '' + file.fd - this.avPlayer.url = fdPath // 再次触发initialized状态机上报 - } - - // 注册avplayer回调函数 - setAVPlayerCallback() { - // 状态机变化回调函数 - this.avPlayer.on('stateChange', async (state, reason) => { - switch (state) { - case 'idle': // 成功调用reset接口后触发该状态机上报 - console.info(TAG + 'state idle called') - await this.nextVideo() // 切换下一个视频播放 - break; - case 'initialized': // avplayer 设置播放源后触发该状态上报 - console.info(TAG + 'state initialized called ') - this.avPlayer.surfaceId = this.surfaceID // 设置显示画面,当播放的资源为纯音频时无需设置 - this.avPlayer.prepare().then(() => { - console.info(TAG+ 'prepare success'); - }, (err) => { - console.error(TAG + 'prepare filed,error message is :' + err.message) - }) - break; - case 'prepared': // prepare调用成功后上报该状态机 - console.info(TAG + 'state prepared called') - this.avPlayer.play() // 调用播放接口开始播放 - break; - case 'playing': // play成功调用后触发该状态机上报 - console.info(TAG + 'state playing called') - break; - case 'paused': // pause成功调用后触发该状态机上报 - console.info(TAG + 'state paused called') - break; - case 'completed': // 播放结束后触发该状态机上报 - console.info(TAG + 'state completed called') - if (this.count == 0) { - this.count++ - this.avPlayer.reset() //调用重置接口准备切换下一个视频 - } else { - this.avPlayer.release() //切换视频后播放至结尾释放avplayer对象 - } - break; - case 'stopped': // stop接口成功调用后触发该状态机上报 - console.info(TAG + 'state stopped called') - break; - case 'released': - console.info(TAG + 'state released called') - break; - case 'error': - console.info(TAG + 'state error called') - break; - default: - console.info(TAG + 'unkown state :' + state) - break; - } - }) - } - - async avPlayerDemo() { - // 创建avPlayer实例对象 - this.avPlayer = await media.createAVPlayer() - let fdPath = 'fd://' - let pathDir = "/data/storage/el2/base/haps/entry/files" // pathDir在FA模型和Stage模型的获取方式不同,请参考开发步骤首行的说明,根据实际情况自行获取。 - // path路径的码流可通过"hdc file send D:\xxx\H264_AAC.mp4 /data/app/el2/100/base/ohos.acts.multimedia.media.avplayer/haps/entry/files" 命令,将其推送到设备上 - let path = pathDir + '/H264_AAC.mp4' - let file = await fs.open(path) - fdPath = fdPath + '' + file.fd - this.avPlayer.url = fdPath - } -} -``` \ No newline at end of file diff --git a/zh-cn/application-dev/media/avrecorder.md b/zh-cn/application-dev/media/avrecorder.md deleted file mode 100644 index 83597f5e940d50ffc6ed7e5974fbe1bf6c5bae93..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/media/avrecorder.md +++ /dev/null @@ -1,490 +0,0 @@ -# 音视频录制开发指导 - -## 简介 - -音视频录制的主要工作是捕获音频信号,接收视频信号,完成音视频编码并保存到文件中,帮助开发者轻松实现音视频录制功能,包括开始录制、暂停录制、恢复录制、停止录制、释放资源等功能控制。它允许调用者指定录制的编码格式、封装格式、文件路径等参数。 - -## 运作机制 - -该模块提供了音视频录制状态变化示意图和音视频录制外部模块交互图。 - -**图1** 音视频录制状态变化示意图 - -![zh-ch_image_video_recorder_state_machine](figures/zh-ch_image_avrecorder_state_machine.png) - -**图2** 视频录制外部模块交互图--待修改 - -![zh-ch_image_video_recorder_zero](figures/zh-ch_image_avrecorder_module_interaction.png) - -**说明**:音频录制时,框架层会通过Native Framework的媒体服务,调用音频子系统通过音频HDI捕获音频数据,通过软件编码封装后保存至文件中,实现音频录制功能。视频录制时,由相机子系统通过视频HDI捕获图像数据,媒体服务将图像数据通过视频编码HDI编码,再将编码后的图像数据封装至文件中,实现视频录制功能。通过音视频录制组合,可分别实现纯音频录制、纯视频录制,音视频录制。 - -## 约束与限制 - -开发者在进行录制功能开发前,需要先对所开发的应用配置相应权限。涉及音频录制,需要获取麦克风权限(ohos.permission.MICROPHONE),权限配置相关内容可参考:[访问控制权限申请指导](../security/accesstoken-guidelines.md)。 - -使用相机进行视频录制还需要与相机模块配合,接口使用以及权限获取详见[相机管理](../reference/apis/js-apis-camera.md)。 - -## 开发指导 - -详细API含义可参考:[媒体服务API文档AVRecorder](../reference/apis/js-apis-media.md#avrecorder9) - -媒体库相关流程含义可参考:[媒体库管理](../reference/apis/js-apis-medialibrary.md) - -相机相关流程含义可参考:[相机管理](../reference/apis/js-apis-camera.md) - -### 音视频录制全流程场景 - -音视频录制全流程场景包含:创建实例、设置录制参数、获取输入surface、开始录制、暂停录制、恢复录制、停止录制、释放资源等流程。 - -音频录制相关配置参数范围,受到设备编解码性能,音频子系统性能等综合限制。 - -视频录制相关配置参数范围,受到设备编解码性能,相机子系统性能等综合限制。 - -``` -import media from '@ohos.multimedia.media' -import camera from '@ohos.multimedia.camera' -import mediaLibrary from '@ohos.multimedia.mediaLibrary' - -export class AVRecorderDemo { - private testFdNumber; // 用于保存fd地址 - - // 获取录制的音频文件fileName对应的fd,需借助媒体库能力。使用mediaLibrary需要添加以下权限, ohos.permission.MEDIA_LOCATION、ohos.permission.WRITE_MEDIA、ohos.permission.READ_MEDIA. - async getFd(fileName) { - // 实现方式参考媒体库资料文档。 - this.testFdNumber = "fd://" + fdNumber.toString(); // e.g. fd://54 - } - - // 当promise接口发生错误上上报的错误回调接口 - failureCallback(error) { - console.info('error happened, error message is ' + error.message); - } - - // 当promise接口发生异常时,系统调用的错误回调接口 - catchCallback(error) { - console.info('catch error happened, error message is ' + error.message); - } - - async AVRecorderDemo() { - let AVRecorder; // AVRecorder空对象在createAVRecorder成功后赋值 - let surfaceID; // 从getInputSurface获取surfaceID,传递给相机的videoOutput - await this.getFd('01.mp4'); - - // 音视频录制相关参数配置,配置参数以实际硬件设备支持的范围为准 - let avProfile = { - audioBitrate : 48000, - audioChannels : 2, - audioCodec : media.CodecMimeType.AUDIO_AAC, - audioSampleRate : 48000, - fileFormat : media.ContainerFormatType.CFT_MPEG_4, - videoBitrate : 2000000, - videoCodec : media.CodecMimeType.VIDEO_AVC, - videoFrameWidth : 640, - videoFrameHeight : 480, - videoFrameRate : 30 - } - let avConfig = { - audioSourceType : media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, - videoSourceType : media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV, - profile : avProfile, - url : 'fd://', - rotation : 0, - location : { latitude : 30, longitude : 130 } - } - - // 创建AVRecorder对象 - await media.createAVRecorder().then((recorder) => { - console.info('case createAVRecorder called'); - if (typeof (recorder) != 'undefined') { - AVRecorder = recorder; - console.info('createAVRecorder success'); - } else { - console.info('createAVRecorder failed'); - } - }, this.failureCallback).catch(this.catchCallback); - - // 对象创建成功后创建on('stateChange')和on('error')监听回调用于监听状态机变化和错误上报 - AVRecorder.on('stateChange', async (state, reason) => { - console.info('case state has changed, new state is :' + state); - switch (state) { - // 用户可以根据需求在不同状态设置自己想要进行的行为 - case 'idle': - // 调用rest接口后触发idle状态;create后也在idle状态 - break; - case 'prepared': - // 调用prepare接口后触发prepared状态; - break; - case 'started': - // 调用start接口后触发started状态; - break; - case 'paused': - // 调用pause接口后触发paused状态; - break; - case 'stopped': - // 调用stop接口后触发stopped状态; - break; - case 'released': - // 调用release接口后触发released状态; - break; - case 'error': - // error状态说明底层出错,用户需排查错误,重新创建avRecorder; - break; - default: - console.info('case state is unknown'); - } - }); - AVRecorder.on('error', (err) => { - // 监听非接口类错误上报 - console.info('case avRecorder.on(error) called, errMessage is ' + err.message); - }); - - // 调用prepare完成音频录制前的准备工作;底层实际是根据prepare的入参来判断是音频录制、视频录制还是音视频录制 - await AVRecorder.prepare(avConfig).then(() => { - console.info('prepare success'); - }, this.failureCallback).catch(this.catchCallback); - - // 包含视频的录制需要调用getInputSurface接口,并将返回值surfaceID传递给camera相关接口 - await AVRecorder.getInputSurface().then((surface) => { - console.info('getInputSurface success'); - surfaceID = surface; // surfaceID给camera的createVideoOutput()作为其中的一个入参 - }, this.failureCallback).catch(this.catchCallback); - - // 视频录制依赖相机相关接口,以下需要先调用相机起流接口后才能继续执行,具体的相机接口调用请参考sample用例 - // 视频录制启动接口 - await AVRecorder.start().then(() => { - console.info('start success'); - }, this.failureCallback).catch(this.catchCallback); - - // 调用pause接口时需要暂停camera出流 - await AVRecorder.pause().then(() => { - console.info('pause success'); - }, this.failureCallback).catch(this.catchCallback); - - // 调用resume接口时需要恢复camera出流 - await AVRecorder.resume().then(() => { - console.info('resume success'); - }, this.failureCallback).catch(this.catchCallback); - - // 停止camera出流后,停止视频录制 - await AVRecorder.stop().then(() => { - console.info('stop success'); - }, this.failureCallback).catch(this.catchCallback); - - // 重置录制相关配置 - await AVRecorder.reset().then(() => { - console.info('reset success'); - }, this.failureCallback).catch(this.catchCallback); - - // 关闭监听回调,如果用户不自行调用off接口,在调用release后,设置的回调接口也会无效 - AVRecorder.off('stateChange'); - AVRecorder.off('error'); - - // 释放视频录制相关资源并释放camera对象相关资源 - await AVRecorder.release().then(() => { - console.info('release success'); - }, this.failureCallback).catch(this.catchCallback); - - // 相关对象置null - AVRecorder = undefined; - surfaceID = undefined; - } -} -``` - -### 纯音频录制全流程场景 - -纯音频录制全流程场景包含:创建实例、设置录制参数、开始录制、暂停录制、恢复录制、停止录制、释放资源等流程。 - -音频录制相关配置参数范围,受到设备编解码性能,音频子系统性能等综合限制。 - -``` -import media from '@ohos.multimedia.media' -import mediaLibrary from '@ohos.multimedia.mediaLibrary' - -export class AudioRecorderDemo { - private testFdNumber; // 用于保存fd地址 - - // 获取录制的音频文件fileName对应的fd,需借助媒体库能力。使用mediaLibrary需要添加以下权限, ohos.permission.MEDIA_LOCATION、ohos.permission.WRITE_MEDIA、ohos.permission.READ_MEDIA - async getFd(fileName) { - // 实现方式参考媒体库资料文档。 - this.testFdNumber = "fd://" + fdNumber.toString(); // e.g. fd://54 - } - - // 当promise接口发生错误上报的错误回调接口 - failureCallback(error) { - console.info('error happened, error message is ' + error.message); - } - - // 当promise接口发生异常时,系统调用的错误回调接口 - catchCallback(error) { - console.info('catch error happened, error message is ' + error.message); - } - - async audioRecorderDemo() { - let audioRecorder; // audioRecorder空对象在createAVRecorder成功后赋值 - await this.getFd('01.m4a'); - // 音频录制相关参数配置 - let audioProfile = { - audioBitrate : 48000, - audioChannels : 2, - audioCodec : media.CodecMimeType.AUDIO_AAC, - audioSampleRate : 48000, - fileFormat : media.ContainerFormatType.CFT_MPEG_4, - } - let audioConfig = { - audioSourceType : media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, - profile : audioProfile, - url : this.testFdNumber, - rotation : 0, - location : { latitude : 30, longitude : 130 } - } - - // 创建audioRecorder对象 - await media.createAVRecorder().then((recorder) => { - console.info('case createAVRecorder called'); - if (typeof (recorder) != 'undefined') { - audioRecorder = recorder; - console.info('createAudioRecorder success'); - } else { - console.info('createAudioRecorder failed'); - } - }, this.failureCallback).catch(this.catchCallback); - - // 对象创建成功后创建on('stateChange')和on('error')监听回调用于监听状态机变化和错误上报 - audioRecorder.on('stateChange', async (state, reason) => { - console.info('case state has changed, new state is :' + state); - switch (state) { - // 用户可以根据需求在不同状态设置自己想要进行的行为 - case 'idle': - // 调用rest接口后触发idle状态;create后也在idle状态 - break; - case 'prepared': - // 调用prepare接口后触发prepared状态; - break; - case 'started': - // 调用start接口后触发started状态; - break; - case 'paused': - // 调用pause接口后触发paused状态; - break; - case 'stopped': - // 调用stop接口后触发stopped状态; - break; - case 'released': - // 调用release接口后触发released状态; - break; - case 'error': - // error状态说明底层出错,用户需排查错误,重新创建avRecorder; - break; - default: - console.info('case state is unknown'); - } - }); - audioRecorder.on('error', (err) => { - // 监听非接口类错误上报 - console.info('case avRecorder.on(error) called, errMessage is ' + err.message); - }); - - // 调用prepare完成音频录制前的准备工作;底层实际是根据prepare的入参来判断是音频录制、视频录制还是音视频录制 - await audioRecorder.prepare(audioConfig).then(() => { - console.info('prepare success'); - }, this.failureCallback).catch(this.catchCallback); - - // 调用start接口启动音频录制 - await audioRecorder.start().then(() => { - console.info('start success'); - }, this.failureCallback).catch(this.catchCallback); - - // 调用pause接口暂停音频录制 - await audioRecorder.pause().then(() => { - console.info('pause success'); - }, this.failureCallback).catch(this.catchCallback); - - // 调用resume接口恢复音频录制 - await audioRecorder.resume().then(() => { - console.info('resume success'); - }, this.failureCallback).catch(this.catchCallback); - - // 调用stop接口停止音频录制 - await audioRecorder.stop().then(() => { - console.info('stop success'); - }, this.failureCallback).catch(this.catchCallback); - - // 调用reset接口重置录制相关配置 - await audioRecorder.reset().then(() => { - console.info('reset success'); - }, this.failureCallback).catch(this.catchCallback); - - // 关闭监听回调,如果用户不自行调用off接口,在调用release后,设置的回调接口也会无效 - avRecorder.off('stateChange'); - avRecorder.off('error'); - - // 调用release接口释放音频录制相关资源 - await audioRecorder.release().then(() => { - console.info('release success'); - }, this.failureCallback).catch(this.catchCallback); - - // 相关对象置null - audioRecorder = undefined; - } -} - -``` - -### 纯视频录制全流程场景 - -纯视频录制全流程场景包含:创建实例、设置录制参数、获取输入surface、开始录制、暂停录制、恢复录制、停止录制、释放资源等流程。 - -视频录制相关配置参数范围,受到设备编解码性能,相机子系统性能等综合限制。 - -``` -import media from '@ohos.multimedia.media' -import camera from '@ohos.multimedia.camera' -import mediaLibrary from '@ohos.multimedia.mediaLibrary' - -export class VideoRecorderDemo { - private testFdNumber; // 用于保存fd地址 - - // 获取录制的音频文件fileName对应的fd,需借助媒体库能力。使用mediaLibrary需要添加以下权限, ohos.permission.MEDIA_LOCATION、ohos.permission.WRITE_MEDIA、ohos.permission.READ_MEDIA. - async getFd(fileName) { - // 实现方式参考媒体库资料文档。 - this.testFdNumber = "fd://" + fdNumber.toString(); // e.g. fd://54 - } - - // 当promise接口发生错误上上报的错误回调接口 - failureCallback(error) { - console.info('error happened, error message is ' + error.message); - } - - // 当promise接口发生异常时,系统调用的错误回调接口 - catchCallback(error) { - console.info('catch error happened, error message is ' + error.message); - } - - async videoRecorderDemo() { - let videoRecorder; // videoRecorder空对象在createAVRecorder成功后赋值 - let surfaceID; // 从getInputSurface获取surfaceID,传递给相机的videoOutput - await this.getFd('01.mp4'); - - // 纯视频录制相关参数配置,配置参数以实际硬件设备支持的范围为准 - let videoProfile = { - fileFormat : media.ContainerFormatType.CFT_MPEG_4, - videoBitrate : 2000000, - videoCodec : media.CodecMimeType.VIDEO_AVC, - videoFrameWidth : 640, - videoFrameHeight : 480, - videoFrameRate : 30 - } - let videoConfig = { - videoSourceType : media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV, - profile : videoProfile, - url : 'fd://', - rotation : 0, - location : { latitude : 30, longitude : 130 } - } - - // 创建videoRecorder对象 - await media.createAVRecorder().then((recorder) => { - console.info('case createVideoRecorder called'); - if (typeof (recorder) != 'undefined') { - videoRecorder = recorder; - console.info('createVideoRecorder success'); - } else { - console.info('createVideoRecorder failed'); - } - }, this.failureCallback).catch(this.catchCallback); - - // 对象创建成功后创建on('stateChange')和on('error')监听回调用于监听状态机变化和错误上报 - videoRecorder.on('stateChange', async (state, reason) => { - console.info('case state has changed, new state is :' + state); - switch (state) { - // 用户可以根据需求在不同状态设置自己想要进行的行为 - case 'idle': - // 调用rest接口后触发idle状态;create后也在idle状态 - break; - case 'prepared': - // 调用prepare接口后触发prepared状态; - break; - case 'started': - // 调用start接口后触发started状态; - break; - case 'paused': - // 调用pause接口后触发paused状态; - break; - case 'stopped': - // 调用stop接口后触发stopped状态; - break; - case 'released': - // 调用release接口后触发released状态; - break; - case 'error': - // error状态说明底层出错,用户需排查错误,重新创建avRecorder; - break; - default: - console.info('case state is unknown'); - } - }); - videoRecorder.on('error', (err) => { - // 监听非接口类错误上报 - console.info('case avRecorder.on(error) called, errMessage is ' + err.message); - }); - - // 调用prepare完成音频录制前的准备工作;底层实际是根据prepare的入参来判断是音频录制、视频录制还是音视频录制 - await videoRecorder.prepare(videoConfig).then(() => { - console.info('prepare success'); - }, this.failureCallback).catch(this.catchCallback); - - // 包含视频的录制需要调用getInputSurface接口,并将返回值surfaceID传递给camera相关接口 - await videoRecorder.getInputSurface().then((surface) => { - console.info('getInputSurface success'); - surfaceID = surface; // surfaceID给camera的createVideoOutput()作为其中的一个入参 - }, this.failureCallback).catch(this.catchCallback); - - // 视频录制依赖相机相关接口,以下需要先调用相机起流接口后才能继续执行,具体的相机接口调用请参考sample用例 - // 视频录制启动接口 - await videoRecorder.start().then(() => { - console.info('start success'); - }, this.failureCallback).catch(this.catchCallback); - - // 调用pause接口时需要暂停camera出流 - await videoRecorder.pause().then(() => { - console.info('pause success'); - }, this.failureCallback).catch(this.catchCallback); - - // 调用resume接口时需要恢复camera出流 - await videoRecorder.resume().then(() => { - console.info('resume success'); - }, this.failureCallback).catch(this.catchCallback); - - // 停止camera出流后,停止视频录制 - await videoRecorder.stop().then(() => { - console.info('stop success'); - }, this.failureCallback).catch(this.catchCallback); - - // 重置录制相关配置 - await videoRecorder.reset().then(() => { - console.info('reset success'); - }, this.failureCallback).catch(this.catchCallback); - - // 关闭监听回调,如果用户不自行调用off接口,在调用release后,设置的回调接口也会无效 - videoRecorder.off('stateChange'); - videoRecorder.off('error'); - - // 释放视频录制相关资源并释放camera对象相关资源 - await videoRecorder.release().then(() => { - console.info('release success'); - }, this.failureCallback).catch(this.catchCallback); - - // 相关对象置null - videoRecorder = undefined; - surfaceID = undefined; - } -} -``` - - - -### 音视频录制APP - -音视频录制APP案例包含:创建实例、设置录制参数、获取输入surface、开始录制、暂停录制、恢复录制、停止录制、释放资源等流程。 - -详细代码可参考:[AVRecorderDemo]([multimedia_player_framework: Implementation of media playback and recording | 媒体播放和录制功能实现 - Gitee.com](https://gitee.com/openharmony/multimedia_player_framework/tree/master/test/sample/AVRecorder)) \ No newline at end of file diff --git a/zh-cn/application-dev/media/avsession-guidelines.md b/zh-cn/application-dev/media/avsession-guidelines.md deleted file mode 100644 index 761506bfbfb3bdf3e7fcd892447c99195797e9f4..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/media/avsession-guidelines.md +++ /dev/null @@ -1,639 +0,0 @@ -# AVSession开发指导 - -> **说明:** -> -> AVSession的所有接口均为系统接口,其功能仅提供给系统应用使用。 - -## 会话接入端开发指导 - -### 基本概念 -- 会话元数据`'AVMetadata'`: 媒体数据相关属性,包含标识当前媒体的ID(assetId),上一首媒体的ID(previousAssetId),下一首媒体的ID(nextAssetId),标题(title),专辑作者(author),专辑名称(album),词作者(writer),媒体时长(duration)等属性。 -- 会话描述符`'AVSessionDescriptor'`: 描述媒体会话的相关信息。包含标识会话的ID(sessionId),会话的类型type(音频Audio/视频Video),会话自定义名称(sessionTag),会话所属应用的信息(elementName)等。 -- 媒体播放状态`'AVPlaybackState'`:用于描述媒体播放状态的相关属性。包含当前媒体的播放状态(state)、位置(position)、速度(speed)、缓冲时间(bufferedTime)、循环模式(loopMode)、是否收藏(isFavorite)等属性。 - -### 接口说明 -会话接入端常用接口如下表所示。接口返回值有两种返回形式:callback和promise,下表中为callback形式接口,promise和callback只是返回值方式不一样,功能相同。更多API说明请参见[API文档](../reference/apis/js-apis-avsession.md)。 - -表1:会话接入端常用接口 - -| 接口名 | 描述 | -|----------------------------------------------------------------------------------|-------------| -| createAVSession(context: Context, tag: string, type: AVSessionType, callback: AsyncCallback\): void | 创建会话 | -| setAVMetadata(data: AVMetadata, callback: AsyncCallback\): void | 设置会话元数据 | -| setAVPlaybackState(state: AVPlaybackState, callback: AsyncCallback\): void | 设置会话播放状态信息 | -| setLaunchAbility(ability: WantAgent, callback: AsyncCallback\): void | 设置启动ability | -| getController(callback: AsyncCallback\): void | 获取当前会话自身控制器 | -| getOutputDevice(callback: AsyncCallback\): void | 获取音频输出设备信息 | -| activate(callback: AsyncCallback\): void | 激活会话 | -| destroy(callback: AsyncCallback\): void | 销毁会话 | - -### 开发步骤 -1.导入模块接口 - -```js -import avSession from '@ohos.multimedia.avsession'; -import wantAgent from '@ohos.app.ability.wantAgent'; -import featureAbility from '@ohos.ability.featureAbility'; -``` - -2.创建会话并激活会话 -```js -// 全局变量定义 -let mediaFavorite = false; -let currentSession = null; -let context = featureAbility.getContext(); - -// 创建音频类型会话 -avSession.createAVSession(context, "AudioAppSample", 'audio').then((session) => { - currentSession = session; - currentSession.activate(); // 激活会话 -}).catch((err) => { - console.info(`createAVSession : ERROR : ${err.message}`); -}); -``` - -3.设置AVSession会话信息,包括: -- 设置会话元数据,除了媒体ID必选外,可选设置媒体标题、专辑信息、媒体作者、媒体时长、上一首/下一首媒体ID等。详细的会话元数据信息可参考API文档中的`AVMetadata`。 -- 设置启动Ability,通过[WantAgent](../reference/apis/js-apis-app-ability-wantAgent.md)的接口实现。WantAgent一般用于封装行为意图信息。 -- 设置播放状态。 -```js -// 设置会话元数据 -let metadata = { - assetId: "121278", - title: "lose yourself", - artist: "Eminem", - author: "ST", - album: "Slim shady", - writer: "ST", - composer: "ST", - duration: 2222, - mediaImage: "https://www.example.com/example.jpg", // 请开发者根据实际情况使用 - subtitle: "8 Mile", - description: "Rap", - lyric: "https://www.example.com/example.lrc", // 请开发者根据实际情况使用 - previousAssetId: "121277", - nextAssetId: "121279", -}; -currentSession.setAVMetadata(metadata).then(() => { - console.info('setAVMetadata successfully'); -}).catch((err) => { - console.info(`setAVMetadata : ERROR : ${err.message}`); -}); -``` - -```js -// 设置启动ability -let wantAgentInfo = { - wants: [ - { - bundleName: "com.neu.setResultOnAbilityResultTest1", - abilityName: "com.example.test.EntryAbility", - } - ], - operationType: wantAgent.OperationType.START_ABILITIES, - requestCode: 0, - wantAgentFlags:[wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG] -} - -wantAgent.getWantAgent(wantAgentInfo).then((agent) => { - currentSession.setLaunchAbility(agent).then(() => { - console.info('setLaunchAbility successfully'); - }).catch((err) => { - console.info(`setLaunchAbility : ERROR : ${err.message}`); - }); -}); -``` - -```js -// 设置播放状态 -let PlaybackState = { - state: avSession.PlaybackState.PLAYBACK_STATE_STOP, - speed: 1.0, - position:{elapsedTime: 0, updateTime: (new Date()).getTime()}, - bufferedTime: 1000, - loopMode: avSession.LoopMode.LOOP_MODE_SEQUENCE, - isFavorite: false, -}; -currentSession.setAVPlaybackState(PlaybackState).then(() => { - console.info('setAVPlaybackState successfully'); -}).catch((err) => { - console.info(`setAVPlaybackState : ERROR : ${err.message}`); -}); -``` - -```js -// 获取当前session会话对象自身的控制器 -currentSession.getController().then((selfController) => { - console.info('getController successfully'); -}).catch((err) => { - console.info(`getController : ERROR : ${err.message}`); -}); -``` - -```js -// 获取音频输出设备信息 -currentSession.getOutputDevice().then((outputInfo) => { - console.info(`getOutputDevice successfully, deviceName : ${outputInfo.deviceName}`); -}).catch((err) => { - console.info(`getOutputDevice : ERROR : ${err.message}`); -}); -``` - -4.注册控制命令监听 -```js -// 注册播放命令监听 -currentSession.on('play', () => { - console.log("调用AudioPlayer.play方法"); - // 设置播放状态 - currentSession.setAVPlaybackState({state: avSession.PlaybackState.PLAYBACK_STATE_PLAY}).then(() => { - console.info('setAVPlaybackState successfully'); - }).catch((err) => { - console.info(`setAVPlaybackState : ERROR : ${err.message}`); - }); -}); - - -// 注册暂停命令监听 -currentSession.on('pause', () => { - console.log("调用AudioPlayer.pause方法"); - // 设置播放状态 - currentSession.setAVPlaybackState({state: avSession.PlaybackState.PLAYBACK_STATE_PAUSE}).then(() => { - console.info('setAVPlaybackState successfully'); - }).catch((err) => { - console.info(`setAVPlaybackState : ERROR : ${err.message}`); - }); -}); - -// 注册停止命令监听 -currentSession.on('stop', () => { - console.log("调用AudioPlayer.stop方法"); - // 设置播放状态 - currentSession.setAVPlaybackState({state: avSession.PlaybackState.PLAYBACK_STATE_STOP}).then(() => { - console.info('setAVPlaybackState successfully'); - }).catch((err) => { - console.info(`setAVPlaybackState : ERROR : ${err.message}`); - }); -}); - -// 注册下一首命令监听 -currentSession.on('playNext', () => { - // 如果媒体文件未准备好,则下载并缓存媒体文件,设置准备状态 - currentSession.setAVPlaybackState({state: avSession.PlaybackState.PLAYBACK_STATE_PREPARE}).then(() => { - console.info('setAVPlaybackState successfully'); - }).catch((err) => { - console.info(`setAVPlaybackState : ERROR : ${err.message}`); - }); - // 成功获取媒体文件 - currentSession.setAVMetadata({assetId: '58970105', title: '不如我们明天见'}).then(() => { - console.info('setAVMetadata successfully'); - }).catch((err) => { - console.info(`setAVMetadata : ERROR : ${err.message}`); - }); - console.log("调用AudioPlayer.play方法"); - // 设置播放状态 - let time = (new Date()).getTime(); - currentSession.setAVPlaybackState({state: avSession.PlaybackState.PLAYBACK_STATE_PLAY, position: {elapsedTime: 0, updateTime: time}, bufferedTime:2000}).then(() => { - console.info('setAVPlaybackState successfully'); - }).catch((err) => { - console.info(`setAVPlaybackState : ERROR : ${err.message}`); - }); -}); - -// 注册播放快进命令监听 -currentSession.on('fastForward', () => { - console.log("调用AudioPlayer的倍速播放"); - // 设置播放状态 - currentSession.setAVPlaybackState({speed: 2.0}).then(() => { - console.info('setAVPlaybackState successfully'); - }).catch((err) => { - console.info(`setAVPlaybackState : ERROR : ${err.message}`); - }); -}); - -// 注册跳播命令监听 -currentSession.on('seek', (time) => { - console.log("调用AudioPlayer的seek方法"); - // 设置播放状态 - currentSession.setAVPlaybackState({position: {elapsedTime: time, updateTime: (new Data()).getTime()}}).then(() => { - console.info('setAVPlaybackState successfully'); - }).catch((err) => { - console.info(`setAVPlaybackState : ERROR : ${err.message}`); - }); -}); - -// 注册设置播放速度命令监听 -currentSession.on('setSpeed', (speed) => { - console.log(`调用AudioPlayer的倍速播放 ${speed}`); - // 设置播放状态 - currentSession.setAVPlaybackState({speed: speed}).then(() => { - console.info('setAVPlaybackState successfully'); - }).catch((err) => { - console.info(`setAVPlaybackState : ERROR : ${err.message}`); - }); -}); - -// 注册设置播放循环模式命令监听 -currentSession.on('setLoopMode', (mode) => { - console.log(`应用自身切换循环模式 ${mode}`); - // 设置播放状态 - currentSession.setAVPlaybackState({loopMode: mode}).then(() => { - console.info('setAVPlaybackState successfully'); - }).catch((err) => { - console.info(`setAVPlaybackState : ERROR : ${err.message}`); - }); -}); - -// 注册设置歌曲收藏命令监听 -currentSession.on('toggleFavorite', (assetId) => { - console.log(`应用保存当前assetId为喜爱 ${assetId}`); - // 根据上一次的状态进行切换 - let favorite = mediaFavorite == false ? true : false; - currentSession.setAVPlaybackState({isFavorite: favorite}).then(() => { - console.info('setAVPlaybackState successfully'); - }).catch((err) => { - console.info(`setAVPlaybackState : ERROR : ${err.message}`); - }); - mediaFavorite = favorite; -}); - -// 注册媒体按键命令监听 -currentSession.on('handleKeyEvent', (event) => { - console.log(`用户按键 ${event.keyCode}`); -}); - -// 注册播放设备变化命令监听 -currentSession.on('outputDeviceChange', (device) => { - console.log(`输出设备变更,更新显示 ${device.deviceName}`); -}); -``` - -5.释放资源 -```js -// 取消注册回调 -currentSession.off('play'); -currentSession.off('pause'); -currentSession.off('stop'); -currentSession.off('playNext'); -currentSession.off('playPrevious'); -currentSession.off('fastForward'); -currentSession.off('rewind'); -currentSession.off('seek'); -currentSession.off('setSpeed'); -currentSession.off('setLoopMode'); -currentSession.off('toggleFavorite'); -currentSession.off('handleKeyEvent'); -currentSession.off('outputDeviceChange'); - -// 去激活session并销毁对象 -currentSession.deactivate().then(() => { - currentSession.destroy(); -}); -``` - -### 调测验证 -在媒体应用上点击播放、暂停、下一首等按键,媒体播放状态出现相应变化。 - -### 常见问题 - -1.会话服务端异常 -- 现象描述: - - 会话服务端异常,应用端无法获取服务端的消息响应。如会话服务未运行或者会话服务通信失败。返回错误信息: Session service exception。 - -- 可能原因: - - 会话重启过程中服务被杀。 - -- 解决办法 - - (1)定时重试,超过3s仍失败时,停止对该会话或者控制器进行操作。 - - (2)销毁当前会话或者会话控制器,并重新创建,如果重新创建失败,则停止会话相关操作。 - -2.会话不存在 -- 现象描述: - - 会话对象不存在时,向该会话设置参数或者发送命令。返回错误信息: The session does not exist。 - -- 可能原因: - - 会话已被销毁,服务端无会话记录。 - -- 解决办法 - - (1)如果在会话被控端产生该错误,请重新创建会话;如果是会话控制端,请停止向该会话发送查询或者控制命令。 - - (2)如果在会话管理端产生该错误,请重新查询系统当前会话记录,在创建控制器时传入正确的会话ID。 - -3.会话未激活 -- 现象描述: - - 会话没有激活时,向会话发送控制命令或者事件。。返回错误信息: The session not active。 - -- 可能原因: - - 会话处于未激活状态。 - -- 解决办法 - - 停止发送该命令或事件,监听会话的激活状态,会话激活后恢复发送该命令或事件。 - -### 相关实例 -提供[音乐Demo](https://gitee.com/openharmony/multimedia_av_session/blob/master/test/resource/player_index_js.md)的代码实例 - -## 会话控制端开发指导(播控中心) - -### 基本概念 -- 远端投播:将本地媒体投播到远端设备,通过本地控制器发送命令,可控制远端播放行为。 -- 发送按键命令:控制器通过发送按键事件的方式控制媒体。 -- 发送控制命令:控制器通过发送控制命令的方式控制媒体。 -- 发送系统按键命令:应用拥有调用该接口的系统权限,通过发送按键事件的方式控制媒体,仅系统应用可用。 -- 发送系统控制命令:应用拥有调用该接口的系统权限,通过发送控制命令的方式控制媒体,仅系统应用可用。 - -### 接口说明 - -会话控制端涉及的常用接口如下表所示。接口返回值有两种返回形式:callback和promise,下表中为callback形式接口,promise和callback只是返回值方式不一样,功能相同。更多API说明请参见[API文档](../reference/apis/js-apis-avsession.md)。 - -表2:会话控制端常用接口 - -| 接口名 | 描述 | -| ------------------------------------------------------------------------------------------------ | ----------------- | -| getAllSessionDescriptors(callback: AsyncCallback\>>): void | 获取所有会话的描述符 | -| createController(sessionId: string, callback: AsyncCallback\): void | 创建控制器 | -| sendAVKeyEvent(event: KeyEvent, callback: AsyncCallback\): void | 发送按键命令 | -| getLaunchAbility(callback: AsyncCallback\): void | 拉起应用 | -| sendControlCommand(command: AVControlCommand, callback: AsyncCallback\): void | 发送控制命令 | -| sendSystemAVKeyEvent(event: KeyEvent, callback: AsyncCallback\): void | 发送系统按键命令 | -| sendSystemControlCommand(command: AVControlCommand, callback: AsyncCallback\): void | 发送系统控制命令 | -| castAudio(session: SessionToken \| 'all', audioDevices: Array\, callback: AsyncCallback\): void | 远端投播 | - -### 开发步骤 -1.导入模块接口 -```js -import avSession from '@ohos.multimedia.avsession'; -import {Action, KeyEvent} from '@ohos.multimodalInput.KeyEvent'; -import wantAgent from '@ohos.app.ability.wantAgent'; -import audio from '@ohos.multimedia.audio'; -``` - -2.获取会话描述符,创建控制器 -```js -// 全局变量定义 -let g_controller = new Array(); -let g_centerSupportCmd:Set = new Set(['play', 'pause', 'playNext', 'playPrevious', 'fastForward', 'rewind', 'seek','setSpeed', 'setLoopMode', 'toggleFavorite']); -let g_validCmd:Set; - -// 获取会话描述符,创建控制器 -avSession.getAllSessionDescriptors().then((descriptors) => { - descriptors.forEach((descriptor) => { - avSession.createController(descriptor.sessionId).then((controller) => { - g_controller.push(controller); - }).catch((err) => { - console.error('createController error'); - }); - }); -}).catch((err) => { - console.error('getAllSessionDescriptors error'); -}); - -// 注册会话创建监听,创建控制器 -avSession.on('sessionCreate', (session) => { - // 新增会话,需要创建控制器 - avSession.createController(session.sessionId).then((controller) => { - g_controller.push(controller); - }).catch((err) => { - console.info(`createController : ERROR : ${err.message}`); - }); -}); -``` - -3.监听AVSession会话状态以及AVSession服务变化 -```js -// 注册会话激活状态变更监听 -controller.on('activeStateChange', (isActive) => { - if (isActive) { - console.log("控制器卡片按键高亮"); - } else { - console.log("控制器卡片按键变更为无效"); - } -}); - -// 注册会话销毁监听 -controller.on('sessionDestroy', () => { - console.info('on sessionDestroy : SUCCESS '); - controller.destroy().then(() => { - console.info('destroy : SUCCESS '); - }).catch((err) => { - console.info(`destroy : ERROR :${err.message}`); - }); -}); - -// 注册系统会话销毁监听 -avSession.on('sessionDestroy', (session) => { - let index = g_controller.findIndex((controller) => { - return controller.sessionId == session.sessionId; - }); - if (index != 0) { - g_controller[index].destroy(); - g_controller.splice(index, 1); - } -}); - -// 注册系统最高优先级会话变更监听 -avSession.on('topSessionChange', (session) => { - let index = g_controller.findIndex((controller) => { - return controller.sessionId == session.sessionId; - }); - // 将该会话显示排到第一个 - if (index != 0) { - g_controller.sort((a, b) => { - return a.sessionId == session.sessionId ? -1 : 0; - }); - } -}); - -// 注册服务异常监听 -avSession.on('sessionServiceDie', () => { - // 服务端异常,应用清理资源 - console.log("服务端异常"); -}) -``` - -4.监听AVSession会话信息变化 -```js -// 注册元数据更新监听 -let metaFilter = ['assetId', 'title', 'description']; -controller.on('metadataChange', metaFilter, (metadata) => { - console.info(`on metadataChange assetId : ${metadata.assetId}`); -}); - -// 注册播放状态更新监听 -let playbackFilter = ['state', 'speed', 'loopMode']; -controller.on('playbackStateChange', playbackFilter, (playbackState) => { - console.info(`on playbackStateChange state : ${playbackState.state}`); -}); - -// 注册会话支持的命令变更监听 -controller.on('validCommandChange', (cmds) => { - console.info(`validCommandChange : SUCCESS : size : ${cmds.size}`); - console.info(`validCommandChange : SUCCESS : cmds : ${cmds.values()}`); - g_validCmd.clear(); - for (let c of g_centerSupportCmd) { - if (cmds.has(c)) { - g_validCmd.add(c); - } - } -}); - -// 注册输出设备变更监听 -controller.on('outputDeviceChange', (device) => { - console.info(`on outputDeviceChange device isRemote : ${device.isRemote}`); -}); -``` - -5.控制AVSession会话行为 -```js -// 用户点击播放按键:发送控制命令--播放 -if (g_validCmd.has('play')) { - controller.sendControlCommand({command:'play'}).then(() => { - console.info('sendControlCommand successfully'); - }).catch((err) => { - console.info(`sendControlCommand : ERROR : ${err.message}`); - }); -} - -// 用户点击循环模式:发送控制命令--单曲循环 -if (g_validCmd.has('setLoopMode')) { - controller.sendControlCommand({command: 'setLoopMode', parameter: avSession.LoopMode.LOOP_MODE_SINGLE}).then(() => { - console.info('sendControlCommand successfully'); - }).catch((err) => { - console.info(`sendControlCommand : ERROR : ${err.message}`); - }); -} - -// 发送按键事件 -let keyItem = {code: 0x49, pressedTime: 123456789, deviceId: 0}; -let event = {action: 2, key: keyItem, keys: [keyItem]}; -controller.sendAVKeyEvent(event).then(() => { - console.info('sendAVKeyEvent Successfully'); -}).catch((err) => { - console.info(`sendAVKeyEvent : ERROR : ${err.message}`); -}); - -// 用户点击卡片空白位置拉起应用 -controller.getLaunchAbility().then((want) => { - console.log("前台拉起应用"); -}).catch((err) => { - console.info(`getLaunchAbility : ERROR : ${err.message}`); -}); - -// 发送系统媒体按键事件 -let keyItem = {code: 0x49, pressedTime: 123456789, deviceId: 0}; -let event = {action: 2, key: keyItem, keys: [keyItem]}; -avSession.sendSystemAVKeyEvent(event).then(() => { - console.info('sendSystemAVKeyEvent Successfully'); -}).catch((err) => { - console.info(`sendSystemAVKeyEvent : ERROR : ${err.message}`); -}); - -// 发送系统控制命令,系统会把控制命令发送到Top会话中 -let avcommand = {command: 'toggleFavorite', parameter: "false"}; -avSession.sendSystemControlCommand(avcommand).then(() => { - console.info('sendSystemControlCommand successfully'); -}).catch((err) => { - console.info(`sendSystemControlCommand : ERROR : ${err.message}`); -}); - -// 投播到其他设备 -let audioManager = audio.getAudioManager(); -let audioDevices; -await audioManager.getDevices(audio.DeviceFlag.OUTPUT_DEVICES_FLAG).then((data) => { - audioDevices = data; - console.info('Promise returned to indicate that the device list is obtained.'); -}).catch((err) => { - console.info(`getDevices : ERROR : ${err.message}`); -}); - -avSession.castAudio('all', audioDevices).then(() => { - console.info('createController : SUCCESS'); -}).catch((err) => { - console.info(`createController : ERROR : ${err.message}`); -}); -``` - -6.释放资源 -```js -// 取消注册回调 - controller.off('metadataChange'); - controller.off('playbackStateChange'); - controller.off('sessionDestroy'); - controller.off('activeStateChange'); - controller.off('validCommandChange'); - controller.off('outputDeviceChange'); - - // 销毁controller对象 - controller.destroy().then(() => { - console.info('destroy : SUCCESS '); - }).catch((err) => { - console.info(`destroy : ERROR : ${err.message}`); - }); -``` - -### 调测验证 -在播控中心点击播放、暂停、下一首等按键,应用播放状态随即发生相应变化。 - -### 常见问题 -1.控制器不存在 -- 现象描述: - - 会话控制器不存在时,向该控制器发送控制命令或者事件。返回错误信息: The session controller does not exist。 - -- 可能原因: - - 控制器已被销毁。 - -- 解决办法 - - 请重新查询系统当前会话记录,并创建对应的会话控制器。 - -2.远端会话连接失败 -- 现象描述: - - 本端会话与远端会话通信失败。返回错误信息: The remote session connection failed。 - -- 可能原因: - - 设备间通信断开。 - -- 解决办法 - - 停止对该会话发送控制命令,并监听输出设备变化,当输出设备发送变化后恢复发送。 - -3.无效会话命令 -- 现象描述: - - 会话被控端不支持该被控命令或事件。返回错误信息: Invalid session command。 - -- 可能原因: - - 被控端不支持该命令。 - -- 解决办法 - - 停止发送该命令或事件,并查询被控会话支持的命令集,发送被控端支持的命令。 - -4.消息过载 -- 现象描述: - - 会话客户端在一段时间内向服务端发送了过多的消息或者命令,引起服务端消息过载。返回错误信息: Command or event overload。 - -- 可能原因: - - 服务端消息过载。 - -- 解决办法 - - 检查自身命令发送是否过于频繁,控制自身查询和控制命令的发送频度。 - -### 相关实例 -提供[播控中心Demo](https://gitee.com/openharmony/multimedia_av_session/blob/master/test/resource/controller_index_js.md)的代码实例 \ No newline at end of file diff --git a/zh-cn/application-dev/media/avsession-overview.md b/zh-cn/application-dev/media/avsession-overview.md index 7c15a9681f97a6a56ba3081192fb8a31aded257b..77736a5f9e2779f5f5097f0d74c5be37fe26763c 100644 --- a/zh-cn/application-dev/media/avsession-overview.md +++ b/zh-cn/application-dev/media/avsession-overview.md @@ -1,56 +1,41 @@ -# AVSession开发概述 +# 媒体会话概述 -> **说明:** -> -> AVSession的所有接口均为系统接口,其功能仅提供给系统应用使用。 +媒体会话(AVSession,Audio & Video Session)服务是OpenHarmony提供的音视频管控服务,用于对系统中所有音视频行为进行统一的管理,例如只允许当前存在一个音频应用处于正在播放状态。 -## 简介 +音视频类应用接入媒体会话后,可以发送应用的数据(比如正在播放的歌曲、歌曲的播放状态等),用户可以通过系统播控中心、语音助手等应用切换多个应用、多个设备播放。音视频类应用如果不接入媒体会话,将无法在后台播放,在应用进入后台时,会被强制停止播放。 - AVSession(Audio & Video Session),音视频会话,即媒体会话。 - - 对应用开发者而言,媒体会话提供了将应用内音视频接入系统播控中心的能力。 - - 对系统开发者而言,媒体会话提供了对系统音视频应用的媒体信息进行展示和统一的媒体播放控制的能力。 +## 基础概念 - 通过AVSession,可以实现: +在开发前,需要先了解以下基础概念: - 1.统一播控入口,提升用户体验。 +- 媒体会话(AVSession) + 媒体会话的一端连接被控的音视频应用,另一端连接音视频应用的控制端(如播控中心、语音助手等)。媒体会话提供了音视频应用和音视频应用控制端之间进行信息交换的通道。 - 当设备上安装了多个音视频类应用时,用户需要切换、进入不同的应用来控制播放。通过接入AVSession,音视频应用可以通过系统的统一播控入口(如播控中心)来控制设备上的媒体播放,无需切换不同应用,提升使用体验。 +- 媒体会话提供方 + 媒体会话提供方指接入媒体会话的音视频应用。音视频应用接入媒体会话后,需要向媒体会话提供播放的媒体信息,例如播放曲目名称、播放状态等。同时,音视频应用需要通过媒体会话接收控制端发出的控制命令并进行正确响应。 - 2.完善音视频后台管控。 - - 当应用在后台自行启动音频播放时,用户难以快速定位对应的应用,造成体验问题。接入AVSession后,允许应用在后台进行音乐播放,便于用户在播控中心快速找到播放应用。 +- 媒体会话控制方 + 媒体会话控制方指接入媒体会话并具有全局管控音视频行为功能的应用,例如系统播控中心、语音助手等。为便于开发者理解,下文将多处使用OpenHarmony系统应用播放中心,作为媒体会话控制方举例。播控中心等系统应用接入媒体会话后,可以通过监听媒体会话获取最新的媒体信息,也可以通过媒体会话向音视频应用发出控制命令。 -## 基本概念 +- 媒体会话控制器(AVSessionController) + 媒体会话控制器的持有者,一般指媒体会话控制方,可以控制媒体会话提供方的应用播放行为,也可以获取应用的播放信息,还可以监听音视频应用播放状态的变化,用于确保媒体会话信息在音视频应用和播控中间之间的同步。 -- 媒体会话 +- 媒体会话管理器(AVSessionManager) + 媒体会话管理器提供了管理媒体会话的能力,可以创建媒体会话、创建媒体会话控制器、发送系统控制事件,也支持对媒体会话的状态进行监听。 - 用于应用和播控中心之间进行信息交换的通道。会话的一端连接被控的媒体应用,另一端连接媒体应用的控制端(如播控中心)。应用接入了媒体会话后,可以通过媒体会话将媒体播放信息传递给控制端,并能够接收到控制端发出的控制命令。 - -- 媒体会话控制器 - 媒体会话控制器的持有者可以控制接入了AVSession应用的播放行为。通过会话控制器,应用可以对应用进程的播放行为进行控制,支持获取应用的播放信息,发送播放控制命令,也支持监听应用的播放状态等的变化,确保媒体会话信息的同步。 +## 媒体会话交互过程 -- 播控中心 - - 系统统一的媒体控制中心,会话控制器的持有者。通过控制器发送命令来控制媒体的播放、暂停等。 +媒体会话分为本地和分布式两种场景。 -## 实现原理 +![AVSession Interaction Process](figures/avsession-interaction-process.png) -该模块提供了`AVSession`会话类和`AVSessionController`控制器类。 +- 本地媒体会话 + 本地媒体会话在本地设备中的媒体会话提供方和媒体会话控制方之间建立连接,实现系统中音视频应用统一的媒体播放控制和媒体信息显示。 -**图1** AVSession交互图 - -![zh-ch_image_avsession](figures/zh-cn_image_avsession.png) - -- 应用与播控中心交互:首先,音频应用创建`AVSession`对象,并设置会话信息(包括媒体元数据、对应拉起的Ability、播放状态等)。然后,播控中心创建`AVSessionController`,可获取会话相关信息,向音频应用发送播放命令。最后,音频应用响应播控中心的命令并更新播放状态。 - -- 支持分布式投播:当组网内的设备创建本地会话之后,播控中心或者音频应用可以根据设备列表选择想要投播的其他设备,将本地会话同步到远端,生成远端会话,并支持远端控制。需要控制远端会话时,通过远端控制器将控制命令发送到远端会话控制中心。 +- 分布式媒体会话 + 分布式媒体会话在跨设备场景中的媒体会话提供方和媒体会话控制方之间建立连接,实现音视频应用跨设备的媒体播放控制和媒体信息显示。例如,将设备A中播放的内容投播到设备B,并在设备B中进行播放控制。 ## 约束和限制 -- 播控中心展示的播放信息,依赖媒体应用主动将媒体信息写入到AVSession。 -- 播控中心控制媒体应用播放,依赖于媒体应用侧对控制命令的响应。 -- AVSession只能够传递媒体播放信息和播放控制指令,不进行信息的展示和控制命令的执行。 -- 普通应用不支持开发播控中心端。当普通音视频应用运行在OpenHarmony上时,默认控制端为系统应用播控中心,开发者无需做额外的操作。 -- 播控中心为系统应用,当开发者需要基于OpenHarmony开发自己的系统时,可以开发本系统的播控中心应用。 -- 为了解决音频在后台播放时用户无法获取到停止音频的入口,影响用户体验,AVSession服务增加了应用后台管控策略,只有应用接入了AVSession,才可以后台播放,否则当应用切后台时系统会强制暂停其音频播放。 +媒体会话服务会对系统中的所有音视频应用进行管控,只有接入了媒体会话的音视频应用才可以在后台播放,未接入的应用在退到后台时,将会被强制暂停音频播放。 diff --git a/zh-cn/application-dev/media/camera-device-input.md b/zh-cn/application-dev/media/camera-device-input.md new file mode 100644 index 0000000000000000000000000000000000000000..6aa0dfc4328e3fbfb7da000627e4b9ac4bf32224 --- /dev/null +++ b/zh-cn/application-dev/media/camera-device-input.md @@ -0,0 +1,82 @@ +# 设备输入 + +在开发一个相机应用前,需要先创建一个独立的相机设备,应用通过调用和控制相机设备,完成预览、拍照和录像等基础操作。 + +## 开发步骤 + +详细的API说明请参考[Camera API参考](../reference/apis/js-apis-camera.md)。 + +1. 导入camera接口,接口中提供了相机相关的属性和方法,导入方法如下。 + + ```ts + import camera from '@ohos.multimedia.camera'; + ``` + +2. 通过getCameraManager()方法,获取cameraManager对象。 + + ```ts + let cameraManager; + let context: any = getContext(this); + cameraManager = camera.getCameraManager(context) + ``` + + > **说明:** + > + > 如果获取对象失败,说明相机可能被占用或无法使用。如果被占用,须等到相机被释放后才能重新获取。 + +3. 通过cameraManager类中的getSupportedCameras()方法,获取当前设备支持的相机列表,列表中存储了设备支持的所有相机ID。若列表不为空,则说明列表中的每个ID都支持独立创建相机对象;否则,说明当前设备无可用相机,不可继续后续操作。 + + ```ts + let cameraArray = cameraManager.getSupportedCameras(); + if (cameraArray.length <= 0) { + console.error("cameraManager.getSupportedCameras error"); + return; + } + + for (let index = 0; index < cameraArray.length; index++) { + console.info('cameraId : ' + cameraArray[index].cameraId); // 获取相机ID + console.info('cameraPosition : ' + cameraArray[index].cameraPosition); // 获取相机位置 + console.info('cameraType : ' + cameraArray[index].cameraType); // 获取相机类型 + console.info('connectionType : ' + cameraArray[index].connectionType); // 获取相机连接类型 + } + ``` + +4. 通过getSupportedOutputCapability()方法,获取当前设备支持的所有输出流,如预览流、拍照流等。输出流在CameraOutputCapability中的各个profile字段中。 + + ```ts + // 创建相机输入流 + let cameraInput; + try { + cameraInput = cameraManager.createCameraInput(cameraArray[0]); + } catch (error) { + console.error('Failed to createCameraInput errorCode = ' + error.code); + } + // 监听cameraInput错误信息 + let cameraDevice = cameraArray[0]; + cameraInput.on('error', cameraDevice, (error) => { + console.info(`Camera input error code: ${error.code}`); + }) + // 打开相机 + await cameraInput.open(); + // 获取相机设备支持的输出流能力 + let cameraOutputCapability = cameraManager.getSupportedOutputCapability(cameraArray[0]); + if (!cameraOutputCapability) { + console.error("cameraManager.getSupportedOutputCapability error"); + return; + } + console.info("outputCapability: " + JSON.stringify(cameraOutputCapability)); + ``` + + +## 状态监听 + +在相机应用开发过程中,可以随时监听相机状态,包括新相机的出现、相机的移除、相机的可用状态。在回调函数中,通过相机ID、相机状态这两个参数进行监听,如当有新相机出现时,可以将新相机加入到应用的备用相机中。 + + 通过注册cameraStatus事件,通过回调返回监听结果,callback返回CameraStatusInfo参数,参数的具体内容可参考相机管理器回调接口实例[CameraStatusInfo](../reference/apis/js-apis-camera.md#camerastatusinfo)。 + +```ts +cameraManager.on('cameraStatus', (cameraStatusInfo) => { + console.info(`camera: ${cameraStatusInfo.camera.cameraId}`); + console.info(`status: ${cameraStatusInfo.status}`); +}) +``` diff --git a/zh-cn/application-dev/media/camera-metadata.md b/zh-cn/application-dev/media/camera-metadata.md new file mode 100644 index 0000000000000000000000000000000000000000..a24fb072d52589a246e774991c2e826774841ffa --- /dev/null +++ b/zh-cn/application-dev/media/camera-metadata.md @@ -0,0 +1,66 @@ +# 元数据 + +元数据(Metadata)是对相机返回的图像信息数据的描述和上下文,针对图像信息,提供的更详细的数据,如照片或视频中,识别人像的取景框坐标等信息。 + +Metadata主要是通过一个TAG(Key),去找对应的Data,用于传递参数和配置信息,减少内存拷贝操作。 + +## 开发步骤 + +详细的API说明请参考[Camera API参考](../reference/apis/js-apis-camera.md)。 + +1. 调用CameraOutputCapability类中的supportedMetadataObjectTypes()方法,获取当前设备支持的元数据类型,并通过createMetadataOutput()方法创建元数据输出流。 + + ```ts + let metadataObjectTypes = cameraOutputCapability.supportedMetadataObjectTypes; + let metadataOutput; + try { + metadataOutput = cameraManager.createMetadataOutput(metadataObjectTypes); + } catch (error) { + // 失败返回错误码error.code并处理 + console.info(error.code); + } + ``` + +2. 调用start()方法输出metadata数据,接口调用失败时,会返回相应错误码,错误码类型参见CameraErrorCode。 + + ```ts + metadataOutput.start().then(() => { + console.info('Callback returned with metadataOutput started.'); + }).catch((err) => { + console.info('Failed to metadataOutput start '+ err.code); + }); + ``` + +3. 调用stop方法停止输出metadata数据,接口调用失败会返回相应错误码,错误码类型参见CameraErrorCode。 + + ```ts + metadataOutput.stop().then(() => { + console.info('Callback returned with metadataOutput stopped.'); + }).catch((err) => { + console.info('Failed to metadataOutput stop '+ err.code); + }); + ``` + +## 状态监听 + +在相机应用开发过程中,可以随时监听metadata数据以及输出流的状态。 + +- 通过注册监听获取metadata对象,监听事件固定为metadataObjectsAvailable。检测到有效metadata数据时,callback返回相应的metadata数据信息,metadataOutput创建成功时可监听。 + + ```ts + metadataOutput.on('metadataObjectsAvailable', (metadataObjectArr) => { + console.info(`metadata output metadataObjectsAvailable`); + }) + ``` + + > **说明:** + > + > 当前的元数据类型仅支持人脸检测(FACE_DETECTION)功能。元数据信息对象为识别到的人脸区域的矩形信息(Rect),包含矩形区域的左上角x坐标、y坐标和矩形的宽高数据。 + +- 通过注册回调函数,获取监听metadata流的错误结果,callback返回metadata输出接口。使用错误时返回对应错误码,错误码类型参见CameraErrorCode。 + + ```ts + metadataOutput.on('error', (metadataOutputError) => { + console.info(`Metadata output error code: ${metadataOutputError.code}`); + }) + ``` diff --git a/zh-cn/application-dev/media/camera-overview.md b/zh-cn/application-dev/media/camera-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..c70c86aa89aa2d4bb5bbaf070da49aeb3dbaa17b --- /dev/null +++ b/zh-cn/application-dev/media/camera-overview.md @@ -0,0 +1,27 @@ +# 相机开发概述 + +开发者通过调用OpenHarmony相机服务提供的接口可以开发相机应用,应用通过访问和操作相机硬件,实现基础操作,如预览、拍照和录像;还可以通过接口组合完成更多操作,如控制闪光灯和曝光时间、对焦或调焦等。 + +## 开发模型 + +相机调用摄像头采集、加工图像视频数据,精确控制对应的硬件,灵活输出图像、视频内容,满足多镜头硬件适配(如广角、长焦、TOF)、多业务场景适配(如不同分辨率、不同格式、不同效果)的要求。 + +相机的工作流程如图所示,可概括为相机输入设备管理、会话管理和相机输出管理三部分。 + +- 相机设备调用摄像头采集数据,作为相机输入流。 + +- 会话管理可配置输入流,即选择哪些镜头进行拍摄。另外还可以配置闪光灯、曝光时间、对焦和调焦等参数,实现不同效果的拍摄,从而适配不同的业务场景。应用可以通过切换会话满足不同场景的拍摄需求。 + +- 配置相机的输出流,即将内容以预览流、拍照流或视频流输出。 + +**图1** 相机工作流程   +![Camera Workflow](figures/camera-workflow.png) + +了解相机工作流程后,建议开发者了解相机的开发模型,便于更好地开发相机应用。 + +**图2** 相机开发模型   +![Camera Development Model](figures/camera-development-model.png) + +相机应用通过控制相机,实现图像显示(预览)、照片保存(拍照)、视频录制(录像)等基础操作。在实现基本操作过程中,相机服务会控制相机设备采集和输出数据,采集的图像数据在相机底层的设备硬件接口(HDI,Hardware Device Interfaces),直接通过BufferQueue传递到具体的功能模块进行处理。BufferQueue在应用开发中无需关注,用于将底层处理的数据及时送到上层进行图像显示。 + +以视频录制为例进行说明,相机应用在录制视频过程中,媒体录制服务先创建一个视频Surface用于传递数据,并提供给相机服务,相机服务可控制相机设备采集视频数据,生成视频流。采集的数据通过底层相机HDI处理后,通过Surface将视频流传递给媒体录制服务,媒体录制服务对视频数据进行处理后,保存为视频文件,完成视频录制。 diff --git a/zh-cn/application-dev/media/camera-preparation.md b/zh-cn/application-dev/media/camera-preparation.md new file mode 100644 index 0000000000000000000000000000000000000000..3fb2ca72f6f81d248bcc31f58598551c1eb2383d --- /dev/null +++ b/zh-cn/application-dev/media/camera-preparation.md @@ -0,0 +1,24 @@ +# 开发准备 + +相机应用开发的主要流程包含开发准备、设备输入、会话管理、预览、拍照和录像等。 + +在开发相机应用时,需要先申请相机相关权限,确保应用拥有访问相机硬件及其他功能的权限,需要的权限如下表。在申请权限前,请保证符合[权限使用的基本原则](../security/accesstoken-overview.md#权限使用的基本原则)。 + + +| 权限名 | 说明 | 授权方式 | +| -------- | -------- | -------- | +| ohos.permission.CAMERA | 允许应用使用相机拍摄照片和录制视频。 | user_grant | +| ohos.permission.MICROPHONE | 允许应用使用麦克风(可选)。
如需同时录制音频,需要申请该权限。 | user_grant | +| ohos.permission.WRITE_MEDIA | 允许应用读写用户外部存储中的媒体文件信息(可选)。 | user_grant | +| ohos.permission.READ_MEDIA | 允许应用读取用户外部存储中的媒体文件信息(可选)。 | user_grant | +| ohos.permission.MEDIA_LOCATION | 允许应用访问用户媒体文件中的地理位置信息(可选)。 | user_grant | + + +以上权限的授权方式均为user_grant(用户授权),即开发者在module.json5文件中配置对应的权限后,需要使用接口[abilityAccessCtrl.requestPermissionsFromUser](../reference/apis/js-apis-abilityAccessCtrl.md#requestpermissionsfromuser9)去校验当前用户是否已授权。如果是,应用可以直接访问/操作目标对象;否则需要弹框向用户申请授权。 + + +具体申请方式及校验方式,请参考[访问控制授权申请指导](../security/accesstoken-guidelines.md)。 + + +> **说明:** +> 即使用户曾被授予过权限,应用在调用此权限保护的接口前,也应该先检查是否有权限。不能把之前授予的状态持久化,因为用户在动态授予后可能通过“设置”取消应用权限。 diff --git a/zh-cn/application-dev/media/camera-preview.md b/zh-cn/application-dev/media/camera-preview.md new file mode 100644 index 0000000000000000000000000000000000000000..ba0cf1d030451fea3337cc8e97e4bb9b263b835e --- /dev/null +++ b/zh-cn/application-dev/media/camera-preview.md @@ -0,0 +1,86 @@ +# 预览 + +预览是启动相机后看见的画面,通常在拍照和录像前执行。 + +## 开发步骤 + +详细的API说明请参考[Camera API参考](../reference/apis/js-apis-camera.md)。 + +1. 创建Surface。 + XComponent组件为预览流提供的Surface,而XComponent的能力由UI提供,相关介绍可参考[XComponent组件参考](../reference/arkui-ts/ts-basic-components-xcomponent.md)。 + + ```ts + // 创建XComponentController + mXComponentController: XComponentController = new XComponentController; + build() { + Flex() { + // 创建XComponent + XComponent({ + id: '', + type: 'surface', + libraryname: '', + controller: this.mXComponentController + }) + .onLoad(() => { + // 设置Surface宽高(1920*1080),预览尺寸设置参考前面 previewProfilesArray 获取的当前设备所支持的预览分辨率大小去设置 + this.mXComponentController.setXComponentSurfaceSize({surfaceWidth:1920,surfaceHeight:1080}); + // 获取Surface ID + globalThis.surfaceId = this.mXComponentController.getXComponentSurfaceId(); + }) + .width('1920px') + .height('1080px') + } + } + ``` + +2. 通过CameraOutputCapability类中的previewProfiles()方法获取当前设备支持的预览能力,返回previewProfilesArray数组 。通过createPreviewOutput()方法创建预览输出流,其中,createPreviewOutput()方法中的而两个参数分别是previewProfilesArray数组中的第一项和步骤一中获取的surfaceId。 + + ```ts + let previewProfilesArray = cameraOutputCapability.previewProfiles; + let previewOutput; + try { + previewOutput = cameraManager.createPreviewOutput(previewProfilesArray[0], surfaceId); + } + catch (error) { + console.error("Failed to create the PreviewOutput instance." + error); + } + ``` + +3. 使能。通过start()方法输出预览流,接口调用失败会返回相应错误码,错误码类型参见[CameraErrorCode](../reference/apis/js-apis-camera.md#cameraerrorcode)。 + + ```ts + previewOutput.start().then(() => { + console.info('Callback returned with previewOutput started.'); + }).catch((err) => { + console.info('Failed to previewOutput start '+ err.code); + }); + ``` + + +## 状态监听 + +在相机应用开发过程中,可以随时监听预览输出流状态,包括预览流启动、预览流结束、预览流输出错误。 + +- 通过注册固定的frameStart回调函数获取监听预览启动结果,previewOutput创建成功时即可监听,预览第一次曝光时触发,有该事件返回结果则认为预览流已启动。 + + ```ts + previewOutput.on('frameStart', () => { + console.info('Preview frame started'); + }) + ``` + +- 通过注册固定的frameEnd回调函数获取监听预览启动结果,previewOutput创建成功时即可监听,预览完成最后一帧时触发,有该事件返回结果则认为预览流已结束。 + + ```ts + previewOutput.on('frameEnd', () => { + console.info('Preview frame ended'); + }) + ``` + +- 通过注册固定的cameraStatus回调函数获取监听预览输出错误结果,callback返回预览输出接口使用错误时对应的错误码,错误码类型参见[CameraErrorCode](../reference/apis/js-apis-camera.md#cameraerrorcode)。 + + ```ts + previewOutput.on('error', (previewOutputError) => { + console.info(`Preview output error code: ${previewOutputError.code}`); + }) + ``` diff --git a/zh-cn/application-dev/media/camera-recording-case.md b/zh-cn/application-dev/media/camera-recording-case.md new file mode 100644 index 0000000000000000000000000000000000000000..ce31512966b3af7305f777f67af36c53a383944e --- /dev/null +++ b/zh-cn/application-dev/media/camera-recording-case.md @@ -0,0 +1,247 @@ +# 录像实现方案 + +## 开发流程 + +在获取到相机支持的输出流能力后,开始创建录像流,开发流程如下。 + +![Recording Development Process](figures/recording-development-process.png) + + +## 完整示例 + +```ts +import camera from '@ohos.multimedia.camera' +import media from '@ohos.multimedia.media' + +// 创建CameraManager对象 +context: any = getContext(this) +let cameraManager = camera.getCameraManager(this.context) +if (!cameraManager) { + console.error("camera.getCameraManager error") + return; +} + +// 监听相机状态变化 +cameraManager.on('cameraStatus', (cameraStatusInfo) => { + console.log(`camera : ${cameraStatusInfo.camera.cameraId}`); + console.log(`status: ${cameraStatusInfo.status}`); +}) + +// 获取相机设备支持的输出流能力 +let cameraOutputCap = cameraManager.getSupportedOutputCapability(cameraArray[0]); +if (!cameraOutputCap) { + console.error("cameraManager.getSupportedOutputCapability error") + return; +} +console.log("outputCapability: " + JSON.stringify(cameraOutputCap)); + +let previewProfilesArray = cameraOutputCap.previewProfiles; +if (!previewProfilesArray) { + console.error("createOutput previewProfilesArray == null || undefined") +} + +let photoProfilesArray = cameraOutputCap.photoProfiles; +if (!photoProfilesArray) { + console.error("createOutput photoProfilesArray == null || undefined") +} + +let videoProfilesArray = cameraOutputCap.videoProfiles; +if (!videoProfilesArray) { + console.error("createOutput videoProfilesArray == null || undefined") +} + +let metadataObjectTypesArray = cameraOutputCap.supportedMetadataObjectTypes; +if (!metadataObjectTypesArray) { + console.error("createOutput metadataObjectTypesArray == null || undefined") +} + +// 配置参数以实际硬件设备支持的范围为准 +let AVRecorderProfile = { + audioBitrate : 48000, + audioChannels : 2, + audioCodec : media.CodecMimeType.AUDIO_AAC, + audioSampleRate : 48000, + fileFormat : media.ContainerFormatType.CFT_MPEG_4, + videoBitrate : 2000000, + videoCodec : media.CodecMimeType.VIDEO_MPEG4, + videoFrameWidth : 640, + videoFrameHeight : 480, + videoFrameRate : 30 +} +let AVRecorderConfig = { + audioSourceType : media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, + videoSourceType : media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV, + profile : AVRecorderProfile, + url : 'fd://', // 文件需先由调用者创建,赋予读写权限,将文件fd传给此参数,eg.fd://45--file:///data/media/01.mp4 + rotation : 0, // 合理值0、90、180、270,非合理值prepare接口将报错 + location : { latitude : 30, longitude : 130 } +} + +let avRecorder +media.createAVRecorder((error, recorder) => { + if (recorder != null) { + avRecorder = recorder; + console.log('createAVRecorder success'); + } else { + console.log(`createAVRecorder fail, error:${error}`); + } +}); + +avRecorder.prepare(AVRecorderConfig, (err) => { + if (err == null) { + console.log('prepare success'); + } else { + console.log('prepare failed and error is ' + err.message); + } +}) + +let videoSurfaceId = null; // 该surfaceID用于传递给相机接口创造videoOutput +avRecorder.getInputSurface((err, surfaceId) => { + if (err == null) { + console.log('getInputSurface success'); + videoSurfaceId = surfaceId; + } else { + console.log('getInputSurface failed and error is ' + err.message); + } +}); + +// 创建VideoOutput对象 +let videoOutput +try { + videoOutput = cameraManager.createVideoOutput(videoProfilesArray[0], videoSurfaceId) +} catch (error) { + console.error('Failed to create the videoOutput instance. errorCode = ' + error.code); +} + +// 监听视频输出错误信息 +videoOutput.on('error', (error) => { + console.log(`Preview output error code: ${error.code}`); +}) + +//创建会话 +let captureSession +try { + captureSession = cameraManager.createCaptureSession() +} catch (error) { + console.error('Failed to create the CaptureSession instance. errorCode = ' + error.code); +} + +// 监听session错误信息 +captureSession.on('error', (error) => { + console.log(`Capture session error code: ${error.code}`); +}) + +// 开始配置会话 +try { + captureSession.beginConfig() +} catch (error) { + console.error('Failed to beginConfig. errorCode = ' + error.code); +} + +// 获取相机列表 +let cameraArray = cameraManager.getSupportedCameras(); +if (cameraArray.length <= 0) { + console.error("cameraManager.getSupportedCameras error") + return; +} + +// 创建相机输入流 +let cameraInput +try { + cameraInput = cameraManager.createCameraInput(cameraArray[0]); +} catch (error) { + console.error('Failed to createCameraInput errorCode = ' + error.code); +} + +// 监听cameraInput错误信息 +let cameraDevice = cameraArray[0]; +cameraInput.on('error', cameraDevice, (error) => { + console.log(`Camera input error code: ${error.code}`); +}) + +// 打开相机 +await cameraInput.open(); + +// 向会话中添加相机输入流 +try { + captureSession.addInput(cameraInput) +} catch (error) { + console.error('Failed to addInput. errorCode = ' + error.code); +} + +// 创建预览输出流,其中参数 surfaceId 参考下面 XComponent 组件,预览流为XComponent组件提供的surface +let previewOutput +try { + previewOutput = cameraManager.createPreviewOutput(previewProfilesArray[0], surfaceId) +} catch (error) { + console.error("Failed to create the PreviewOutput instance.") +} + +// 向会话中添加预览输入流 +try { + captureSession.addOutput(previewOutput) +} catch (error) { + console.error('Failed to addOutput(previewOutput). errorCode = ' + error.code); +} + +// 向会话中添加录像输出流 +try { + captureSession.addOutput(videoOutput) +} catch (error) { + console.error('Failed to addOutput(videoOutput). errorCode = ' + error.code); +} + +// 提交会话配置 +await captureSession.commitConfig() + +// 启动会话 +await captureSession.start().then(() => { + console.log('Promise returned to indicate the session start success.'); +}) + +// 启动录像输出流 +videoOutput.start(async (err) => { + if (err) { + console.error('Failed to start the video output ${err.message}'); + return; + } + console.log('Callback invoked to indicate the video output start success.'); +}); + +// 开始录像 +avRecorder.start().then(() => { + console.log('videoRecorder start success'); +}) + +// 停止录像输出流 +videoOutput.stop((err) => { + if (err) { + console.error('Failed to stop the video output ${err.message}'); + return; + } + console.log('Callback invoked to indicate the video output stop success.'); +}); + +// 停止录像 +avRecorder.stop().then(() => { + console.log('stop success'); +}) + +// 停止当前会话 +captureSession.stop() + +// 释放相机输入流 +cameraInput.close() + +// 释放预览输出流 +previewOutput.release() + +// 释放录像输出流 +videoOutput.release() + +// 释放会话 +captureSession.release() + +// 会话置空 +captureSession = null +``` diff --git a/zh-cn/application-dev/media/camera-recording.md b/zh-cn/application-dev/media/camera-recording.md new file mode 100644 index 0000000000000000000000000000000000000000..0ea40fba30eacc9c3027ac27844cc448fcea7f6c --- /dev/null +++ b/zh-cn/application-dev/media/camera-recording.md @@ -0,0 +1,147 @@ +# 录像 + +录像也是相机应用的最重要功能之一,录像是循环帧的捕获。对于录像的流畅度,开发者可以参考[拍照](camera-shooting.md)中的步骤4,设置分辨率、闪光灯、焦距、照片质量及旋转角度等信息。 + +## 开发步骤 + +详细的API说明请参考[Camera API参考](../reference/apis/js-apis-camera.md)。 + +1. 创建拍照输出流的SurfaceId以及拍照输出的数据,都需要用到系统提供的[media接口](../reference/apis/js-apis-media.md)能力,导入media接口的方法如下。 + + ```ts + import media from '@ohos.multimedia.media'; + ``` + +2. 创建Surface。 + + 系统提供的media接口可以创建一个录像AVRecorder实例,通过该实例的getInputSurface方法获取SurfaceId,与录像输出流做关联,处理录像输出流输出的数据。 + + ```ts + let AVRecorder; + media.createAVRecorder((error, recorder) => { + if (recorder != null) { + AVRecorder = recorder; + console.info('createAVRecorder success'); + } else { + console.info(`createAVRecorder fail, error:${error}`); + } + }); + + let videoSurfaceId = null; + AVRecorder.getInputSurface().then((surfaceId) => { + console.info('getInputSurface success'); + videoSurfaceId = surfaceId; + }).catch((err) => { + console.info('getInputSurface failed and catch error is ' + err.message); + }); + ``` + +3. 创建录像输出流。 + + 通过CameraOutputCapability类中的videoProfiles,可获取当前设备支持的录像输出流。然后,定义创建录像的参数,通过createVideoOutput方法创建录像输出流。 + + ```ts + let videoProfilesArray = cameraOutputCapability.videoProfiles; + if (!videoProfilesArray) { + console.error("createOutput videoProfilesArray == null || undefined"); + } + + // 创建视频录制的参数 + let videoConfig = { + videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV, + profile: { + fileFormat : media.ContainerFormatType.CFT_MPEG_4, // 视频文件封装格式,只支持MP4 + videoBitrate : 100000, // 视频比特率 + videoCodec : media.CodecMimeType.VIDEO_MPEG4, // 视频文件编码格式,支持mpeg4和avc两种格式 + videoFrameWidth : 640, // 视频分辨率的宽 + videoFrameHeight : 480, // 视频分辨率的高 + videoFrameRate : 30 // 视频帧率 + }, + url: 'fd://35', + rotation: 0 + } + // 创建avRecorder + let avRecorder; + media.createAVRecorder((error, recorder) => { + if (recorder != null) { + avRecorder = recorder; + console.info('createAVRecorder success'); + } else { + console.info(`createAVRecorder fail, error:${error}`); + } + }); + // 设置视频录制的参数 + avRecorder.prepare(videoConfig); + // 创建VideoOutput对象 + let videoOutput; + try { + videoOutput = cameraManager.createVideoOutput(videoProfilesArray[0], videoSurfaceId); + } catch (error) { + console.error('Failed to create the videoOutput instance. errorCode = ' + error.code); + } + ``` + +4. 开始录像。 + + 先通过videoOutput的start方法启动录像输出流,再通过avRecorder的start方法开始录像。 + + ``` + videoOutput.start(async (err) => { + if (err) { + console.error('Failed to start the video output ${err.message}'); + return; + } + console.info('Callback invoked to indicate the video output start success.'); + }); + + avRecorder.start().then(() => { + console.info('avRecorder start success'); + } + ``` + +5. 停止录像。 + + 先通过avRecorder的stop方法停止录像,再通过videoOutput的stop方法停止录像输出流。 + + ```ts + videoRecorder.stop().then(() => { + console.info('stop success'); + } + + videoOutput.stop((err) => { + if (err) { + console.error('Failed to stop the video output ${err.message}'); + return; + } + console.info('Callback invoked to indicate the video output stop success.'); + }); + ``` + + +## 状态监听 + +在相机应用开发过程中,可以随时监听录像输出流状态,包括录像开始、录像结束、录像流输出的错误。 + +- 通过注册固定的frameStart回调函数获取监听录像开始结果,videoOutput创建成功时即可监听,录像第一次曝光时触发,有该事件返回结果则认为录像开始。 + + ```ts + videoOutput.on('frameStart', () => { + console.info('Video frame started'); + }) + ``` + +- 通过注册固定的frameEnd回调函数获取监听预览启动结果,videoOutput创建成功时即可监听,录像完成最后一帧时触发,有该事件返回结果则认为录像流已结束。 + + ```ts + videoOutput.on('frameEnd', () => { + console.info('Video frame ended'); + }) + ``` + +- 通过注册固定的error回调函数获取监听录像输出错误结果,callback返回预览输出接口使用错误时对应的错误码,错误码类型参见[CameraErrorCode](../reference/apis/js-apis-camera.md#cameraerrorcode)。 + + ```ts + videoOutput.on('error', (error) => { + console.info(`Video output error code: ${error.code}`); + }) + ``` diff --git a/zh-cn/application-dev/media/camera-session-management.md b/zh-cn/application-dev/media/camera-session-management.md new file mode 100644 index 0000000000000000000000000000000000000000..fdef64d33ff8226ab459f7e3c5f0967b7ee30ad6 --- /dev/null +++ b/zh-cn/application-dev/media/camera-session-management.md @@ -0,0 +1,86 @@ +# 会话管理 + +相机使用预览、拍照、录像、元数据功能前,均需要创建相机会话。 + +在会话中,可以完成以下功能: + +- 配置相机的输入流和输出流。相机在拍摄前,必须完成输入输出流的配置。 + 配置输入流即添加设备输入,对用户而言,相当于选择设备的某一摄像头拍摄;配置输出流,即选择数据将以什么形式输出。当应用需要实现拍照时,输出流应配置为预览流和拍照流,预览流的数据将显示在XComponent组件上,拍照流的数据将通过ImageReceiver接口的能力保存到相册中。 + +- 添加闪光灯、调整焦距等配置。具体支持的配置及接口说明请参考[Camera API参考](../reference/apis/js-apis-camera.md)。 + +- 会话切换控制。应用可以通过移除和添加输出流的方式,切换相机模式。如当前会话的输出流为拍照流,应用可以将拍照流移除,然后添加视频流作为输出流,即完成了拍照到录像的切换。 + +完成会话配置后,应用提交和开启会话,可以开始调用相机相关功能。 + +## 开发步骤 + +1. 调用cameraManager类中的createCaptureSession()方法创建一个会话。 + + ```ts + let captureSession; + try { + captureSession = cameraManager.createCaptureSession(); + } catch (error) { + console.error('Failed to create the CaptureSession instance. errorCode = ' + error.code); + } + ``` + +2. 调用captureSession类中的beginConfig()方法配置会话。 + + ```ts + try { + captureSession.beginConfig(); + } catch (error) { + console.error('Failed to beginConfig. errorCode = ' + error.code); + } + ``` + +3. 使能。向会话中添加相机的输入流和输出流,调用captureSession.addInput()添加相机的输入流;调用captureSession.addOutput()添加相机的输出流。以下示例代码以添加预览流previewOutput和拍照流photoOutput为例,即当前模式支持拍照和预览。 + + 调用captureSession类中的commitConfig()和start()方法提交相关配置,并启动会话。 + + ```ts + try { + captureSession.addInput(cameraInput); + } catch (error) { + console.error('Failed to addInput. errorCode = ' + error.code); + } + try { + captureSession.addOutput(previewOutput); + } catch (error) { + console.error('Failed to addOutput(previewOutput). errorCode = ' + error.code); + } + try { + captureSession.addOutput(photoOutput); + } catch (error) { + console.error('Failed to addOutput(photoOutput). errorCode = ' + error.code); + } + await captureSession.commitConfig() ; + await captureSession.start().then(() => { + console.info('Promise returned to indicate the session start success.'); + }) + ``` + +4. 会话控制。调用captureSession类中的stop()方法可以停止当前会话。调用removeOutput()和addOutput()方法可以完成会话切换控制。以下示例代码以移除拍照流photoOutput,添加视频流videoOutput为例,完成了拍照到录像的切换。 + + ```ts + await captureSession.stop(); + try { + captureSession.beginConfig(); + } catch (error) { + console.error('Failed to beginConfig. errorCode = ' + error.code); + } + // 从会话中移除拍照输出流 + try { + captureSession.removeOutput(photoOutput); + } catch (error) { + console.error('Failed to removeOutput(photoOutput). errorCode = ' + error.code); + } + // 向会话中添加视频输出流 + try { + captureSession.addOutput(videoOutput); + } catch (error) { + console.error('Failed to addOutput(videoOutput). errorCode = ' + error.code); + } + ``` diff --git a/zh-cn/application-dev/media/camera-shooting-case.md b/zh-cn/application-dev/media/camera-shooting-case.md new file mode 100644 index 0000000000000000000000000000000000000000..aa85b7136740b20d84a955e5fccdb4301113914d --- /dev/null +++ b/zh-cn/application-dev/media/camera-shooting-case.md @@ -0,0 +1,240 @@ +# 拍照实现方案 + +## 开发流程 + +在获取到相机支持的输出流能力后,开始创建拍照流,开发流程如下。 + +![Photographing Development Process](figures/photographing-development-process.png) + +## 完整示例 + +```ts +import camera from '@ohos.multimedia.camera' +import image from '@ohos.multimedia.image' +import media from '@ohos.multimedia.media' + +// 创建CameraManager对象 +context: any = getContext(this) +let cameraManager = camera.getCameraManager(this.context) +if (!cameraManager) { + console.error("camera.getCameraManager error") + return; +} +// 监听相机状态变化 +cameraManager.on('cameraStatus', (cameraStatusInfo) => { + console.info(`camera : ${cameraStatusInfo.camera.cameraId}`); + console.info(`status: ${cameraStatusInfo.status}`); +}) + +// 获取相机列表 +let cameraArray = cameraManager.getSupportedCameras(); +if (cameraArray.length <= 0) { + console.error("cameraManager.getSupportedCameras error") + return; +} + +for (let index = 0; index < cameraArray.length; index++) { + console.info('cameraId : ' + cameraArray[index].cameraId); // 获取相机ID + console.info('cameraPosition : ' + cameraArray[index].cameraPosition); // 获取相机位置 + console.info('cameraType : ' + cameraArray[index].cameraType); // 获取相机类型 + console.info('connectionType : ' + cameraArray[index].connectionType); // 获取相机连接类型 +} + +// 创建相机输入流 +let cameraInput +try { + cameraInput = cameraManager.createCameraInput(cameraArray[0]); +} catch (error) { + console.error('Failed to createCameraInput errorCode = ' + error.code); +} + +// 监听cameraInput错误信息 +let cameraDevice = cameraArray[0]; +cameraInput.on('error', cameraDevice, (error) => { + console.info(`Camera input error code: ${error.code}`); +}) + +// 打开相机 +await cameraInput.open(); + +// 获取相机设备支持的输出流能力 +let cameraOutputCap = cameraManager.getSupportedOutputCapability(cameraArray[0]); +if (!cameraOutputCap) { + console.error("cameraManager.getSupportedOutputCapability error") + return; +} +console.info("outputCapability: " + JSON.stringify(cameraOutputCap)); + +let previewProfilesArray = cameraOutputCap.previewProfiles; +if (!previewProfilesArray) { + console.error("createOutput previewProfilesArray == null || undefined") +} + +let photoProfilesArray = cameraOutputCap.photoProfiles; +if (!photoProfilesArray) { + console.error("createOutput photoProfilesArray == null || undefined") +} + +// 创建预览输出流,其中参数 surfaceId 参考上文 XComponent 组件,预览流为XComponent组件提供的surface +let previewOutput +try { + previewOutput = cameraManager.createPreviewOutput(previewProfilesArray[0], surfaceId) +} catch (error) { + console.error("Failed to create the PreviewOutput instance.") +} + +// 监听预览输出错误信息 +previewOutput.on('error', (error) => { + console.info(`Preview output error code: ${error.code}`); +}) + +// 创建ImageReceiver对象,并设置照片参数:分辨率大小是根据前面 photoProfilesArray 获取的当前设备所支持的拍照分辨率大小去设置 +let imageReceiver = await image.createImageReceiver(1920, 1080, 4, 8) +// 获取照片显示SurfaceId +let photoSurfaceId = await imageReceiver.getReceivingSurfaceId() +// 创建拍照输出流 +let photoOutput +try { + photoOutput = cameraManager.createPhotoOutput(photoProfilesArray[0], photoSurfaceId) +} catch (error) { + console.error('Failed to createPhotoOutput errorCode = ' + error.code); +} +//创建会话 +let captureSession +try { + captureSession = cameraManager.createCaptureSession() +} catch (error) { + console.error('Failed to create the CaptureSession instance. errorCode = ' + error.code); +} + +// 监听session错误信息 +captureSession.on('error', (error) => { + console.info(`Capture session error code: ${error.code}`); +}) + +// 开始配置会话 +try { + captureSession.beginConfig() +} catch (error) { + console.error('Failed to beginConfig. errorCode = ' + error.code); +} + +// 向会话中添加相机输入流 +try { + captureSession.addInput(cameraInput) +} catch (error) { + console.error('Failed to addInput. errorCode = ' + error.code); +} + +// 向会话中添加预览输出流 +try { + captureSession.addOutput(previewOutput) +} catch (error) { + console.error('Failed to addOutput(previewOutput). errorCode = ' + error.code); +} + +// 向会话中添加拍照输出流 +try { + captureSession.addOutput(photoOutput) +} catch (error) { + console.error('Failed to addOutput(photoOutput). errorCode = ' + error.code); +} + +// 提交会话配置 +await captureSession.commitConfig() + +// 启动会话 +await captureSession.start().then(() => { + console.info('Promise returned to indicate the session start success.'); +}) +// 判断设备是否支持闪光灯 +let flashStatus +try { + flashStatus = captureSession.hasFlash() +} catch (error) { + console.error('Failed to hasFlash. errorCode = ' + error.code); +} +console.info('Promise returned with the flash light support status:' + flashStatus); + +if (flashStatus) { + // 判断是否支持自动闪光灯模式 + let flashModeStatus + try { + let status = captureSession.isFlashModeSupported(camera.FlashMode.FLASH_MODE_AUTO) + flashModeStatus = status + } catch (error) { + console.error('Failed to check whether the flash mode is supported. errorCode = ' + error.code); + } + if(flashModeStatus) { + // 设置自动闪光灯模式 + try { + captureSession.setFlashMode(camera.FlashMode.FLASH_MODE_AUTO) + } catch (error) { + console.error('Failed to set the flash mode. errorCode = ' + error.code); + } + } +} + +// 判断是否支持连续自动变焦模式 +let focusModeStatus +try { + let status = captureSession.isFocusModeSupported(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO) + focusModeStatus = status +} catch (error) { + console.error('Failed to check whether the focus mode is supported. errorCode = ' + error.code); +} + +if (focusModeStatus) { + // 设置连续自动变焦模式 + try { + captureSession.setFocusMode(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO) + } catch (error) { + console.error('Failed to set the focus mode. errorCode = ' + error.code); + } +} + +// 获取相机支持的可变焦距比范围 +let zoomRatioRange +try { + zoomRatioRange = captureSession.getZoomRatioRange() +} catch (error) { + console.error('Failed to get the zoom ratio range. errorCode = ' + error.code); +} + +// 设置可变焦距比 +try { + captureSession.setZoomRatio(zoomRatioRange[0]) +} catch (error) { + console.error('Failed to set the zoom ratio value. errorCode = ' + error.code); +} +let settings = { + quality: camera.QualityLevel.QUALITY_LEVEL_HIGH, // 设置图片质量高 + rotation: camera.ImageRotation.ROTATION_0 // 设置图片旋转角度0 +} +// 使用当前拍照设置进行拍照 +photoOutput.capture(settings, async (err) => { + if (err) { + console.error('Failed to capture the photo ${err.message}'); + return; + } + console.info('Callback invoked to indicate the photo capture request success.'); +}); +// 停止当前会话 +captureSession.stop() + +// 释放相机输入流 +cameraInput.close() + +// 释放预览输出流 +previewOutput.release() + +// 释放拍照输出流 +photoOutput.release() + +// 释放会话 +captureSession.release() + +// 会话置空 +captureSession = null +``` + diff --git a/zh-cn/application-dev/media/camera-shooting.md b/zh-cn/application-dev/media/camera-shooting.md new file mode 100644 index 0000000000000000000000000000000000000000..13dda3f3ff954bbf774db60d24468e5d9f110f16 --- /dev/null +++ b/zh-cn/application-dev/media/camera-shooting.md @@ -0,0 +1,155 @@ +# 拍照 + +拍照是相机的最重要功能之一,拍照模块基于相机复杂的逻辑,为了保证用户拍出的照片质量,在中间步骤可以设置分辨率、闪光灯、焦距、照片质量及旋转角度等信息。 + +## 开发步骤 + +详细的API说明请参考[Camera API参考](../reference/apis/js-apis-camera.md)。 + +1. 创建拍照输出流的SurfaceId以及拍照输出的数据,都需要用到系统提供的image接口能力,导入image接口的方法如下。 + + ```ts + import image from '@ohos.multimedia.image'; + ``` + +2. 获取SurfaceId。 + 通过image的createImageReceiver方法创建ImageReceiver实例,再通过实例的getReceivingSurfaceId方法获取SurfaceId,与拍照输出流相关联,获取拍照输出流的数据。 + + ```ts + function getImageReceiverSurfaceId() { + let receiver = image.createImageReceiver(640, 480, 4, 8); + console.info('before ImageReceiver check'); + if (receiver !== undefined) { + console.info('ImageReceiver is ok'); + let photoSurfaceId = receiver.getReceivingSurfaceId(); + console.info('ImageReceived id: ' + JSON.stringify(photoSurfaceId)); + } else { + console.info('ImageReceiver is not ok'); + } + } + ``` + +3. 创建拍照输出流。 + 通过CameraOutputCapability类中的photoProfiles()方法,可获取当前设备支持的拍照输出流,通过createPhotoOutput()方法传入支持的某一个输出流及步骤一获取的SurfaceId创建拍照输出流。 + + ```ts + let photoProfilesArray = cameraOutputCapability.photoProfiles; + if (!photoProfilesArray) { + console.error("createOutput photoProfilesArray == null || undefined"); + } + let photoOutput; + try { + photoOutput = cameraManager.createPhotoOutput(photoProfilesArray[0], photoSurfaceId); + } catch (error) { + console.error('Failed to createPhotoOutput errorCode = ' + error.code); + } + ``` + +4. 参数配置。 + 配置相机的参数可以调整拍照的一些功能,包括闪光灯、变焦、焦距等。 + + ```ts + // 判断设备是否支持闪光灯 + let flashStatus; + try { + flashStatus = captureSession.hasFlash(); + } catch (error) { + console.error('Failed to hasFlash. errorCode = ' + error.code); + } + console.info('Promise returned with the flash light support status:' + flashStatus); + if (flashStatus) { + // 判断是否支持自动闪光灯模式 + let flashModeStatus; + try { + let status = captureSession.isFlashModeSupported(camera.FlashMode.FLASH_MODE_AUTO); + flashModeStatus = status; + } catch (error) { + console.error('Failed to check whether the flash mode is supported. errorCode = ' + error.code); + } + if(flashModeStatus) { + // 设置自动闪光灯模式 + try { + captureSession.setFlashMode(camera.FlashMode.FLASH_MODE_AUTO); + } catch (error) { + console.error('Failed to set the flash mode. errorCode = ' + error.code); + } + } + } + // 判断是否支持连续自动变焦模式 + let focusModeStatus; + try { + let status = captureSession.isFocusModeSupported(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO); + focusModeStatus = status; + } catch (error) { + console.error('Failed to check whether the focus mode is supported. errorCode = ' + error.code); + } + if (focusModeStatus) { + // 设置连续自动变焦模式 + try { + captureSession.setFocusMode(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO); + } catch (error) { + console.error('Failed to set the focus mode. errorCode = ' + error.code); + } + } + // 获取相机支持的可变焦距比范围 + let zoomRatioRange; + try { + zoomRatioRange = captureSession.getZoomRatioRange(); + } catch (error) { + console.error('Failed to get the zoom ratio range. errorCode = ' + error.code); + } + // 设置可变焦距比 + try { + captureSession.setZoomRatio(zoomRatioRange[0]); + } catch (error) { + console.error('Failed to set the zoom ratio value. errorCode = ' + error.code); + } + ``` + +5. 触发拍照。 + 通过photoOutput类的capture()方法,执行拍照任务。该方法有两个参数,第一个参数为拍照设置参数的setting,setting中可以设置照片的质量和旋转角度,第二参数为回调函数。 + + ```ts + let settings = { + quality: camera.QualityLevel.QUALITY_LEVEL_HIGH, // 设置图片质量高 + rotation: camera.ImageRotation.ROTATION_0, // 设置图片旋转角度0 + location: captureLocation, // 设置图片地理位置 + mirror: false // 设置镜像使能开关(默认关) + }; + photoOutput.capture(settings, async (err) => { + if (err) { + console.error('Failed to capture the photo ${err.message}'); + return; + } + console.info('Callback invoked to indicate the photo capture request success.'); + }); + ``` + +## 状态监听 + +在相机应用开发过程中,可以随时监听拍照输出流状态,包括拍照流开始、拍照帧的开始与结束、拍照输出流的错误。 + +- 通过注册固定的captureStart回调函数获取监听拍照开始结果,photoOutput时即可监听,拍照第一次曝光时触发,该事件返回此次拍照的captureId。 + + ```ts + photoOutput.on('captureStart', (captureId) => { + console.info(`photo capture stated, captureId : ${captureId}`); + }) + ``` + +- 通过注册固定的frameShutter回调函数获取监听拍照结束结果,photoOutput时即可监听,该事件返回结果为拍照完全结束后的相关信息[CaptureEndInfo](../reference/apis/js-apis-camera.md#captureendinfo)。 + + ```ts + photoOutput.on('captureEnd', (captureEndInfo) => { + console.info(`photo capture end, captureId : ${captureEndInfo.captureId}`); + console.info(`frameCount : ${captureEndInfo.frameCount}`); + }) + ``` + +- 通过注册固定的error回调函数获取监听拍照输出流的错误结果。callback返回拍照输出接口使用错误时的对应错误码,错误码类型参见[CameraErrorCode](../reference/apis/js-apis-camera.md#cameraerrorcode)。 + + ```ts + photoOutput.on('error', (error) => { + console.info(`Photo output error code: ${error.code}`); + }) + ``` diff --git a/zh-cn/application-dev/media/camera.md b/zh-cn/application-dev/media/camera.md deleted file mode 100644 index 8ca2a6e6484b717359c0399f05ebe901daaf85c4..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/media/camera.md +++ /dev/null @@ -1,511 +0,0 @@ -# 相机开发指导 - -## 场景介绍 - -OpenHarmony相机模块支持相机业务的开发,开发者可以通过已开放的接口实现相机硬件的访问、操作和新功能开发,最常见的操作如:预览、拍照和录像等。开发者也可以通过合适的接口或者接口组合实现闪光灯控制、曝光时间控制、手动对焦和自动对焦控制、变焦控制以及更多的功能。 - -开发者在调用Camera能力时,需要了解Camera的一些基本概念: - -- **相机静态能力**:用于描述相机的固有能力的一系列参数,比如朝向、支持的分辨率等信息。 -- **物理相机**:物理相机就是独立的实体摄像头设备。物理相机ID是用于标志每个物理摄像头的唯一字串。 -- **异步操作**:为保证UI线程不被阻塞,部分Camera接口采用异步调用方式。异步方式API均提供了callback函数和Promise函数。 - -## 开发步骤 - -### 接口说明 - -详细API含义请参考:[相机管理API文档](../reference/apis/js-apis-camera.md) - -### 全流程场景 - -包含流程:权限申请、创建实例、参数设置、会话管理、拍照、录像、释放资源等。 - -#### 权限申请 - -在使用相机之前,需要申请相机的相关权限,保证应用拥有相机硬件及其他功能权限,应用权限的介绍请参考权限章节,相机涉及权限如下表。 - -| 权限名称 | 权限属性值 | -| -------- | ------------------------------ | -| 相机权限 | ohos.permission.CAMERA | -| 录音权限 | ohos.permission.MICROPHONE | -| 存储权限 | ohos.permission.WRITE_MEDIA | -| 读取权限 | ohos.permission.READ_MEDIA | -| 位置权限 | ohos.permission.MEDIA_LOCATION | - -参考代码如下: - -```typescript -const PERMISSIONS: Array = [ - 'ohos.permission.CAMERA', - 'ohos.permission.MICROPHONE', - 'ohos.permission.MEDIA_LOCATION', - 'ohos.permission.READ_MEDIA', - 'ohos.permission.WRITE_MEDIA' -] - -function applyPermission() { - console.info('[permission] get permission'); - globalThis.abilityContext.requestPermissionFromUser(PERMISSIONS) - } -``` - -#### 创建实例 - -在实现一个相机应用之前必须先创建一个独立的相机设备,然后才能继续相机的其他操作。如果此步骤操作失败,相机可能被占用或无法使用。如果被占用,必须等到相机释放后才能重新获取CameraManager对象。通过getSupportedCameras() 方法,获取当前使用的设备支持的相机列表。相机列表中存储了当前设备拥有的所有相机ID,如果列表不为空,则列表中的每个ID都支持独立创建相机对象;否则,说明正在使用的设备无可用的相机,不能继续后续的操作。相机设备具备预览、拍照、录像、Metadata等输出流,需要通过getSupportedOutputCapability()接口获取各个输出流的具体能力,通过该接口,可以获取当前设备支持的所有输出流能力,分别在CameraOutputCapability中的各个profile字段中,相机设备创建的建议步骤如下: - -```typescript -import camera from '@ohos.multimedia.camera' -import image from '@ohos.multimedia.image' -import media from '@ohos.multimedia.media' - -// 创建CameraManager对象 -context: any = getContext(this) -let cameraManager = camera.getCameraManager(this.context) -if (!cameraManager) { - console.error("camera.getCameraManager error") - return; -} -// 监听相机状态变化 -cameraManager.on('cameraStatus', (cameraStatusInfo) => { - console.log(`camera : ${cameraStatusInfo.camera.cameraId}`); - console.log(`status: ${cameraStatusInfo.status}`); -}) - -// 获取相机列表 -let cameraArray = cameraManager.getSupportedCameras(); -if (cameraArray.length <= 0) { - console.error("cameraManager.getSupportedCameras error") - return; -} - -for (let index = 0; index < cameraArray.length; index++) { - console.log('cameraId : ' + cameraArray[index].cameraId); // 获取相机ID - console.log('cameraPosition : ' + cameraArray[index].cameraPosition); // 获取相机位置 - console.log('cameraType : ' + cameraArray[index].cameraType); // 获取相机类型 - console.log('connectionType : ' + cameraArray[index].connectionType); // 获取相机连接类型 -} - -// 创建相机输入流 -let cameraInput -try { - cameraInput = cameraManager.createCameraInput(cameraArray[0]); -} catch () { - console.error('Failed to createCameraInput errorCode = ' + error.code); -} - -// 监听cameraInput错误信息 -let cameraDevice = cameraArray[0]; -cameraInput.on('error', cameraDevice, (error) => { - console.log(`Camera input error code: ${error.code}`); -}) - -// 打开相机 -await cameraInput.open(); - -// 获取相机设备支持的输出流能力 -let cameraOutputCap = cameraManager.getSupportedOutputCapability(cameraArray[0]); -if (!cameraOutputCap) { - console.error("cameraManager.getSupportedOutputCapability error") - return; -} -console.info("outputCapability: " + JSON.stringify(cameraOutputCap)); - -let previewProfilesArray = cameraOutputCap.previewProfiles; -if (!previewProfilesArray) { - console.error("createOutput previewProfilesArray == null || undefined") -} - -let photoProfilesArray = cameraOutputCap.photoProfiles; -if (!photoProfilesArray) { - console.error("createOutput photoProfilesArray == null || undefined") -} - -let videoProfilesArray = cameraOutputCap.videoProfiles; -if (!videoProfilesArray) { - console.error("createOutput videoProfilesArray == null || undefined") -} - -let metadataObjectTypesArray = cameraOutputCap.supportedMetadataObjectTypes; -if (!metadataObjectTypesArray) { - console.error("createOutput metadataObjectTypesArray == null || undefined") -} - -// 创建预览输出流,其中参数 surfaceId 参考下面 XComponent 组件,预览流为XComponent组件提供的surface -let previewOutput -try { - previewOutput = cameraManager.createPreviewOutput(previewProfilesArray[0], surfaceId) -} catch (error) { - console.error("Failed to create the PreviewOutput instance.") -} - -// 监听预览输出错误信息 -previewOutput.on('error', (error) => { - console.log(`Preview output error code: ${error.code}`); -}) - -// 创建ImageReceiver对象,并设置照片参数:分辨率大小是根据前面 photoProfilesArray 获取的当前设备所支持的拍照分辨率大小去设置 -let imageReceiver = await image.createImageReceiver(1920, 1080, 4, 8) -// 获取照片显示SurfaceId -let photoSurfaceId = await imageReceiver.getReceivingSurfaceId() -// 创建拍照输出流 -let photoOutput -try { - photoOutput = cameraManager.createPhotoOutput(photoProfilesArray[0], photoSurfaceId) -} catch (error) { - console.error('Failed to createPhotoOutput errorCode = ' + error.code); -} - -// 创建视频录制的参数 -let videoConfig = { - audioSourceType: 1, - videoSourceType: 1, - profile: { - audioBitrate: 48000, - audioChannels: 2, - audioCodec: 'audio/mp4v-es', - audioSampleRate: 48000, - durationTime: 1000, - fileFormat: 'mp4', - videoBitrate: 48000, - videoCodec: 'video/mp4v-es', - videoFrameWidth: 640, - videoFrameHeight: 480, - videoFrameRate: 30 - }, - url: 'file:///data/media/01.mp4', - orientationHint: 0, - maxSize: 100, - maxDuration: 500, - rotation: 0 -} - -// 创建录像输出流 -let videoRecorder -media.createVideoRecorder().then((recorder) => { - console.log('createVideoRecorder called') - videoRecorder = recorder -}) -// 设置视频录制的参数 -videoRecorder.prepare(videoConfig) -//获取录像SurfaceId -let videoSurfaceId -videoRecorder.getInputSurface().then((id) => { - console.log('getInputSurface called') - videoSurfaceId = id -}) - -// 创建VideoOutput对象 -let videoOutput -try { - videoOutput = cameraManager.createVideoOutput(videoProfilesArray[0], videoSurfaceId) -} catch (error) { - console.error('Failed to create the videoOutput instance. errorCode = ' + error.code); -} - -// 监听视频输出错误信息 -videoOutput.on('error', (error) => { - console.log(`Preview output error code: ${error.code}`); -}) -``` -预览流、拍照流和录像流的输入均需要提前创建surface,其中预览流为XComponent组件提供的surface,拍照流为ImageReceiver提供的surface,录像流为VideoRecorder的surface。 - -**XComponent** - -```typescript -mXComponentController: XComponentController = new XComponentController // 创建XComponentController - -build() { - Flex() { - XComponent({ // 创建XComponent - id: '', - type: 'surface', - libraryname: '', - controller: this.mXComponentController - }) - .onload(() => { // 设置onload回调 - // 设置Surface宽高(1920*1080),预览尺寸设置参考前面 previewProfilesArray 获取的当前设备所支持的预览分辨率大小去设置 - this.mXComponentController.setXComponentSurfaceSize({surfaceWidth:1920,surfaceHeight:1080}) - // 获取Surface ID - globalThis.surfaceId = mXComponentController.getXComponentSurfaceId() - }) - .width('1920px') // 设置XComponent宽度 - .height('1080px') // 设置XComponent高度 - } -} -``` - -**ImageReceiver** - -```typescript -function getImageReceiverSurfaceId() { - let receiver = image.createImageReceiver(640, 480, 4, 8) - console.log(TAG + 'before ImageReceiver check') - if (receiver !== undefined) { - console.log('ImageReceiver is ok') - surfaceId1 = receiver.getReceivingSurfaceId() - console.log('ImageReceived id: ' + JSON.stringify(surfaceId1)) - } else { - console.log('ImageReceiver is not ok') - } - } -``` - -**VideoRecorder** - -```typescript -function getVideoRecorderSurface() { - await getFd('CameraManager.mp4'); - mVideoConfig.url = mFdPath; - media.createVideoRecorder((err, recorder) => { - console.info('Entering create video receiver') - mVideoRecorder = recorder - console.info('videoRecorder is :' + JSON.stringify(mVideoRecorder)) - console.info('videoRecorder.prepare called.') - mVideoRecorder.prepare(mVideoConfig, (err) => { - console.info('videoRecorder.prepare success.') - mVideoRecorder.getInputSurface((err, id) => { - console.info('getInputSurface called') - mVideoSurface = id - console.info('getInputSurface surfaceId: ' + JSON.stringify(mVideoSurface)) - }) - }) - }) - } -``` - -#### 会话管理 - -##### 创建会话 - -```typescript -//创建会话 -let captureSession -try { - captureSession = cameraManager.createCaptureSession() -} catch (error) { - console.error('Failed to create the CaptureSession instance. errorCode = ' + error.code); -} - -// 监听session错误信息 -captureSession.on('error', (error) => { - console.log(`Capture session error code: ${error.code}`); -}) - -// 开始配置会话 -try { - captureSession.beginConfig() -} catch (error) { - console.error('Failed to beginConfig. errorCode = ' + error.code); -} - -// 向会话中添加相机输入流 -try { - captureSession.addInput(cameraInput) -} catch (error) { - console.error('Failed to addInput. errorCode = ' + error.code); -} - -// 向会话中添加预览输入流 -try { - captureSession.addOutput(previewOutput) -} catch (error) { - console.error('Failed to addOutput(previewOutput). errorCode = ' + error.code); -} - -// 向会话中添加拍照输出流 -try { - captureSession.addOutput(photoOutput) -} catch (error) { - console.error('Failed to addOutput(photoOutput). errorCode = ' + error.code); -} - -// 提交会话配置 -await captureSession.commitConfig() - -// 启动会话 -await captureSession.start().then(() => { - console.log('Promise returned to indicate the session start success.'); -}) -``` - -##### 切换会话 - -```typescript -// 停止当前会话 -await captureSession.stop() - -// 开始配置会话 -try { - captureSession.beginConfig() -} catch (error) { - console.error('Failed to beginConfig. errorCode = ' + error.code); -} - -// 从会话中移除拍照输出流 -try { - captureSession.removeOutput(photoOutput) -} catch (error) { - console.error('Failed to removeOutput(photoOutput). errorCode = ' + error.code); -} - -// 向会话中添加录像输出流 -try { - captureSession.addOutput(videoOutput) -} catch (error) { - console.error('Failed to addOutput(videoOutput). errorCode = ' + error.code); -} - -// 提交会话配置 -await captureSession.commitConfig() - -// 启动会话 -await captureSession.start().then(() => { - console.log('Promise returned to indicate the session start success.'); -}) -``` - -#### 参数设置 - -```typescript -// 判断设备是否支持闪光灯 -let flashStatus -try { - flashStatus = captureSession.hasFlash() -} catch (error) { - console.error('Failed to hasFlash. errorCode = ' + error.code); -} -console.log('Promise returned with the flash light support status:' + flashStatus); - -if (flashStatus) { - // 判断是否支持自动闪光灯模式 - let flashModeStatus - try { - let status = captureSession.isFlashModeSupported(camera.FlashMode.FLASH_MODE_AUTO) - flashModeStatus = status - } catch (error) { - console.error('Failed to check whether the flash mode is supported. errorCode = ' + error.code); - } - if(flashModeStatus) { - // 设置自动闪光灯模式 - try { - captureSession.setFlashMode(camera.FlashMode.FLASH_MODE_AUTO) - } catch (error) { - console.error('Failed to set the flash mode. errorCode = ' + error.code); - } - } -} - -// 判断是否支持连续自动变焦模式 -let focusModeStatus -try { - let status = captureSession.isFocusModeSupported(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO) - focusModeStatus = status -} catch (error) { - console.error('Failed to check whether the focus mode is supported. errorCode = ' + error.code); -} - -if (focusModeStatus) { - // 设置连续自动变焦模式 - try { - captureSession.setFocusMode(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO) - } catch (error) { - console.error('Failed to set the focus mode. errorCode = ' + error.code); - } -} - -// 获取相机支持的可变焦距比范围 -let zoomRatioRange -try { - zoomRatioRange = captureSession.getZoomRatioRange() -} catch (error) { - console.error('Failed to get the zoom ratio range. errorCode = ' + error.code); -} - -// 设置可变焦距比 -try { - captureSession.setZoomRatio(zoomRatioRange[0]) -} catch (error) { - console.error('Failed to set the zoom ratio value. errorCode = ' + error.code); -} -``` - -#### 拍照 - -```typescript -let settings = { - quality: camera.QualityLevel.QUALITY_LEVEL_HIGH, // 设置图片质量高 - rotation: camera.ImageRotation.ROTATION_0 // 设置图片旋转角度0 -} -// 使用当前拍照设置进行拍照 -photoOutput.capture(settings, async (err) => { - if (err) { - console.error('Failed to capture the photo ${err.message}'); - return; - } - console.log('Callback invoked to indicate the photo capture request success.'); -}); -``` - -#### 录像 - -```typescript -// 启动录像输出流 -videoOutput.start(async (err) => { - if (err) { - console.error('Failed to start the video output ${err.message}'); - return; - } - console.log('Callback invoked to indicate the video output start success.'); -}); - -// 开始录像 -videoRecorder.start().then(() => { - console.info('videoRecorder start success'); -} - -// 停止录像 -videoRecorder.stop().then(() => { - console.info('stop success'); -} - -// 停止录像输出流 -videoOutput.stop((err) => { - if (err) { - console.error('Failed to stop the video output ${err.message}'); - return; - } - console.log('Callback invoked to indicate the video output stop success.'); -}); -``` - -拍照保存接口可参考:[图片处理API文档](image.md#imagereceiver的使用) - -#### 释放资源 - -```typescript -// 停止当前会话 -captureSession.stop() - -// 释放相机输入流 -cameraInput.close() - -// 释放预览输出流 -previewOutput.release() - -// 释放拍照输出流 -photoOutput.release() - -// 释放录像输出流 -videoOutput.release() - -// 释放会话 -captureSession.release() - -// 会话置空 -captureSession = null -``` - -## 流程图 - -应用使用相机的流程示意图如下 -![camera_framework process](figures/camera_framework_process.png) \ No newline at end of file diff --git a/zh-cn/application-dev/media/distributed-audio-playback.md b/zh-cn/application-dev/media/distributed-audio-playback.md new file mode 100644 index 0000000000000000000000000000000000000000..8b6473a6f48fa8f625efc3c5f35259992f4c0cf2 --- /dev/null +++ b/zh-cn/application-dev/media/distributed-audio-playback.md @@ -0,0 +1,101 @@ +# 分布式音频播放(仅对系统应用开放) + +通过分布式音频播放的能力,用户可以将音频投播远端设备播放,实现音频在组网中不同设备之间流转。 + +开发者可以通过分布式音频播放,将当前设备播放的所有音频投放到指定的远端设备播放,或将设备播放的某个音频流投放到指定的远端设备播放。 + +## 开发步骤及示例 + +在将音频投播到组网内其他设备前,需要先获取组网内的设备列表,并监听设备连接状态的变化,具体开发步骤请参考[音频输出设备管理](audio-output-device-management.md)。 + +在获取组网内的设备列表时,可以通过指定DeviceFlag,筛选出需要的设备。 + +| 名称 | 说明 | +| -------- | -------- | +| NONE_DEVICES_FLAG9+ | 无。此接口为系统接口。 | +| OUTPUT_DEVICES_FLAG | 本地输出设备。 | +| INPUT_DEVICES_FLAG | 本地输入设备。 | +| ALL_DEVICES_FLAG | 本地输入输出设备。 | +| DISTRIBUTED_OUTPUT_DEVICES_FLAG9+ | 分布式输出设备。 此接口为系统接口。 | +| DISTRIBUTED_INPUT_DEVICES_FLAG9+ | 分布式输入设备。 此接口为系统接口。 | +| ALL_DISTRIBUTED_DEVICES_FLAG9+ | 分布式输入输出设备。 此接口为系统接口。 | + +具体接口说明请参考[AudioRoutingManager API文档](../reference/apis/js-apis-audio.md#audioroutingmanager9)。 + +### 投播所有音频 + +1. [获取输出设备信息](audio-output-device-management.md#获取输出设备信息)。 + +2. 创建AudioDeviceDescriptor对象,用于指定音频输出设备。 + +3. 调用selectOutputDevice,将当前设备播放的所有音频投放到指定的远端设备播放。 + +```ts +let outputAudioDeviceDescriptor = [{ + deviceRole: audio.DeviceRole.OUTPUT_DEVICE, + deviceType: audio.DeviceType.SPEAKER, + id: 1, + name: "", + address: "", + sampleRates: [44100], + channelCounts: [2], + channelMasks: [0], + networkId: audio.LOCAL_NETWORK_ID, + interruptGroupId: 1, + volumeGroupId: 1, +}]; + +async function selectOutputDevice() { + audioRoutingManager.selectOutputDevice(outputAudioDeviceDescriptor, (err) => { + if (err) { + console.error(`Invoke selectOutputDevice failed, code is ${err.code}, message is ${err.message}`); + } else { + console.info('Invoke selectOutputDevice succeeded.'); + } + }); +} +``` + +### 投播指定音频流 + +1. [获取输出设备信息](audio-output-device-management.md#获取输出设备信息)。 + +2. 创建AudioRendererFilter对象,通过uid指定应用,通过rendererId指定音频流。 + +3. 创建AudioDeviceDescriptor对象,用于指定音频输出设备。 + +4. 调用selectOutputDeviceByFilter,将当前设备播放的指定音频流投放到指定的远端设备播放。 + +```ts +let outputAudioRendererFilter = { + uid: 20010041, + rendererInfo: { + content: audio.ContentType.CONTENT_TYPE_MUSIC, + usage: audio.StreamUsage.STREAM_USAGE_MEDIA, + rendererFlags: 0 }, + rendererId: 0 }; + +let outputAudioDeviceDescriptor = [{ + deviceRole: audio.DeviceRole.OUTPUT_DEVICE, + deviceType: audio.DeviceType.SPEAKER, + id: 1, + name: "", + address: "", + sampleRates: [44100], + channelCounts: [2], + channelMasks: [0], + networkId: audio.LOCAL_NETWORK_ID, + interruptGroupId: 1, + volumeGroupId: 1, +}]; + +async function selectOutputDeviceByFilter() { + audioRoutingManager.selectOutputDeviceByFilter(outputAudioRendererFilter, outputAudioDeviceDescriptor, (err) => { + if (err) { + console.error(`Invoke selectOutputDeviceByFilter failed, code is ${err.code}, message is ${err.message}`); + } else { + console.info('Invoke selectOutputDeviceByFilter succeeded.'); + } + }); +} +``` diff --git a/zh-cn/application-dev/media/distributed-avsession-overview.md b/zh-cn/application-dev/media/distributed-avsession-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..77b70df46cf0ff9a0748d393b9c8f92681835014 --- /dev/null +++ b/zh-cn/application-dev/media/distributed-avsession-overview.md @@ -0,0 +1,52 @@ +# 分布式媒体会话概述 + +OpenHarmony提供的媒体会话允许用户把本地播放的媒体投播到远端分布式设备上,从而实现更好的播放效果,例如将平板上播放的音频投播到智能音箱。 + +当用户将音频媒体投播到远端设备后,可以实时将媒体信息同步到远端设备,同时远端被投播的设备也可以反向控制源端音源,进行上一首/下一首/播放/暂停等控制,用户层面上整个播放控制流程和本地播放一致。 + + +## 交互过程 + +当本地投播主控端设备与远端被控端设备成功配对之后,主控端设备中的媒体会话控制方,可以通过媒体会话管理器将媒体投播到被控设备上,实现分布式媒体会话,交互过程如图所示。 + +![Distributed AVSession Interaction Process](figures/distributed-avsession-interaction-process.png) + +媒体投播时,被控端设备上的AVSession服务会自动创建一个与主控设备保持同步的媒体会话,主控设备的会话信息、主控设备的命令事件与被控设备上的命令事件时刻保持同步。 + +## 分布式媒体会话流程 + +用户进行了分布式投播以后,在投播被控端自动创建出对应的媒体会话,两侧的媒体会话可以相互联动,具体体现在: + +1. 主控端本地媒体会话服务接收到音频跨设备切换命令后将本端的媒体会话信息同步到远端。 + +2. 远端播控中心监听到新增会话后,创建对应会话的控制器。 + +3. 远端播控中心可以通过远端的控制器将控制命令发送到主控端本地媒体会话。 + +4. 主控端本地媒体会话接收远端控制命令后,回调到本端的音频类应用。 + +5. 主控端本地的媒体会话信息变更实时同步到远端播控中心显示。 + +6. 远端设备连接断开后,音频切回主控端本地并暂停播放(音频部件完成回切,媒体会话通知应用暂停)。 + +## 分布式媒体会话场景 + +应用通过分布式媒体会话进行投播时,存在两种场景: + +- 系统投播:通过媒体会话控制方,例如播控中心,发起分布式投播。 + 系统投播对所有应用生效。系统投播后,主控端设备中的所有音频默认从被控端设备播放。 + +- 应用投播:音视频应用在自身应用内通过集成投播组件发起分布式投播(当前暂未支持)。 + 应用投播对单个应用生效。应用投播后,主控端设备中发起投播的应用音频从被控端设备播放,其它应用的音频依旧从主控端设备播放。 + +另外,投播支持抢占,后投播应用可以抢占之前投播的应用,在远端设备进行音频播放。 + +## 分布式媒体会话与分布式音频播放之间的关系 + +媒体会话服务在实现分布式媒体会话进行跨设备投播时,内部的实现逻辑可以粗略描述为: + +- 调用了[分布式音频播放](distributed-audio-playback.md)能力的相关接口,将音频流投播到了远端设备。 + +- 通过分布式能力将媒体会话元数据投播到了远端设备,供远端设备获取并显示。 + +所以,通过分布式媒体会话进行投播,不仅可以实现在远端设备播放音频,而且可以让远端设备显示播放信息,同时借助媒体会话的机制,可以实现在远端设备上对播放的音频进行控制。 diff --git a/zh-cn/application-dev/media/figures/audio-capturer-state.png b/zh-cn/application-dev/media/figures/audio-capturer-state.png deleted file mode 100644 index 52b5556260dbf78c5e816b37013248a07e8dbbc6..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/media/figures/audio-capturer-state.png and /dev/null differ diff --git a/zh-cn/application-dev/media/figures/audio-playback-interaction-diagram.png b/zh-cn/application-dev/media/figures/audio-playback-interaction-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..6977b8a4b7f56fbb585b6e590aa7d80a4b674756 Binary files /dev/null and b/zh-cn/application-dev/media/figures/audio-playback-interaction-diagram.png differ diff --git a/zh-cn/application-dev/media/figures/audio-renderer-state.png b/zh-cn/application-dev/media/figures/audio-renderer-state.png deleted file mode 100644 index 9ae30c2a9306dc85662405c36da9e11d07ed9a2a..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/media/figures/audio-renderer-state.png and /dev/null differ diff --git a/zh-cn/application-dev/media/figures/audio-stream-mgmt-invoking-relationship.png b/zh-cn/application-dev/media/figures/audio-stream-mgmt-invoking-relationship.png new file mode 100644 index 0000000000000000000000000000000000000000..2179b94304f0f6321a25bdf2d5e1cab9d6814757 Binary files /dev/null and b/zh-cn/application-dev/media/figures/audio-stream-mgmt-invoking-relationship.png differ diff --git a/zh-cn/application-dev/media/figures/audiocapturer-status-change.png b/zh-cn/application-dev/media/figures/audiocapturer-status-change.png new file mode 100644 index 0000000000000000000000000000000000000000..aadbc4fb6470b7cdc0f399ee5954a96c01a7f7c3 Binary files /dev/null and b/zh-cn/application-dev/media/figures/audiocapturer-status-change.png differ diff --git a/zh-cn/application-dev/media/figures/audiorenderer-status-change.png b/zh-cn/application-dev/media/figures/audiorenderer-status-change.png new file mode 100644 index 0000000000000000000000000000000000000000..a721044f7aeccfed0260176963d192cac40dd8a6 Binary files /dev/null and b/zh-cn/application-dev/media/figures/audiorenderer-status-change.png differ diff --git a/zh-cn/application-dev/media/figures/avsession-interaction-process.png b/zh-cn/application-dev/media/figures/avsession-interaction-process.png new file mode 100644 index 0000000000000000000000000000000000000000..8c40fb6070e01fee9e0b9325f34ea6db1a862b71 Binary files /dev/null and b/zh-cn/application-dev/media/figures/avsession-interaction-process.png differ diff --git a/zh-cn/application-dev/media/figures/bitmap-operation.png b/zh-cn/application-dev/media/figures/bitmap-operation.png new file mode 100644 index 0000000000000000000000000000000000000000..8c4c585072bd5f173d32c3e6c22b6193c576419a Binary files /dev/null and b/zh-cn/application-dev/media/figures/bitmap-operation.png differ diff --git a/zh-cn/application-dev/media/figures/camera-development-model.png b/zh-cn/application-dev/media/figures/camera-development-model.png new file mode 100644 index 0000000000000000000000000000000000000000..ab7d2d9317b782701686caff871f418ba66621f8 Binary files /dev/null and b/zh-cn/application-dev/media/figures/camera-development-model.png differ diff --git a/zh-cn/application-dev/media/figures/camera-workflow.png b/zh-cn/application-dev/media/figures/camera-workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..ff41ca019502bea5e54e94b7318a017efaf22861 Binary files /dev/null and b/zh-cn/application-dev/media/figures/camera-workflow.png differ diff --git a/zh-cn/application-dev/media/figures/camera_framework_process.png b/zh-cn/application-dev/media/figures/camera_framework_process.png deleted file mode 100644 index 8dfd6c0d05c643fbd715fe6cf1b7431e01fff3e2..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/media/figures/camera_framework_process.png and /dev/null differ diff --git a/zh-cn/application-dev/media/figures/cropping.jpeg b/zh-cn/application-dev/media/figures/cropping.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..a564818815eb3fde13a40ef02d0811bd56803fb9 Binary files /dev/null and b/zh-cn/application-dev/media/figures/cropping.jpeg differ diff --git a/zh-cn/application-dev/media/figures/distributed-avsession-interaction-process.png b/zh-cn/application-dev/media/figures/distributed-avsession-interaction-process.png new file mode 100644 index 0000000000000000000000000000000000000000..250c3aa0d38f815e768a34407581dfb46a0eba31 Binary files /dev/null and b/zh-cn/application-dev/media/figures/distributed-avsession-interaction-process.png differ diff --git a/zh-cn/application-dev/media/figures/horizontal-flip.jpeg b/zh-cn/application-dev/media/figures/horizontal-flip.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..f43e4f6ab2adc68bf0f90eaf8177d36ee91f32ac Binary files /dev/null and b/zh-cn/application-dev/media/figures/horizontal-flip.jpeg differ diff --git a/zh-cn/application-dev/media/figures/image-development-process.png b/zh-cn/application-dev/media/figures/image-development-process.png new file mode 100644 index 0000000000000000000000000000000000000000..2ef40b6049507edfccb75feef71e99ac5bdb6f77 Binary files /dev/null and b/zh-cn/application-dev/media/figures/image-development-process.png differ diff --git a/zh-cn/application-dev/media/figures/invoking-relationship-recording-stream-mgmt.png b/zh-cn/application-dev/media/figures/invoking-relationship-recording-stream-mgmt.png new file mode 100644 index 0000000000000000000000000000000000000000..2179b94304f0f6321a25bdf2d5e1cab9d6814757 Binary files /dev/null and b/zh-cn/application-dev/media/figures/invoking-relationship-recording-stream-mgmt.png differ diff --git a/zh-cn/application-dev/media/figures/local-avsession-interaction-process.png b/zh-cn/application-dev/media/figures/local-avsession-interaction-process.png new file mode 100644 index 0000000000000000000000000000000000000000..f6c8772afc6c11d10617fe041c462a8a0a1f2918 Binary files /dev/null and b/zh-cn/application-dev/media/figures/local-avsession-interaction-process.png differ diff --git a/zh-cn/application-dev/media/figures/media-system-framework.png b/zh-cn/application-dev/media/figures/media-system-framework.png new file mode 100644 index 0000000000000000000000000000000000000000..a2e8cd0862c9f61509b1a6603db04a6adda40f90 Binary files /dev/null and b/zh-cn/application-dev/media/figures/media-system-framework.png differ diff --git a/zh-cn/application-dev/media/figures/offsets.jpeg b/zh-cn/application-dev/media/figures/offsets.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..ab4c87a69bae55a62feddc0ca61a0ef1081bf199 Binary files /dev/null and b/zh-cn/application-dev/media/figures/offsets.jpeg differ diff --git a/zh-cn/application-dev/media/figures/original-drawing.jpeg b/zh-cn/application-dev/media/figures/original-drawing.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..01a0b0d7022dfc0130029154fec7321bc62dfe36 Binary files /dev/null and b/zh-cn/application-dev/media/figures/original-drawing.jpeg differ diff --git a/zh-cn/application-dev/media/figures/photographing-development-process.png b/zh-cn/application-dev/media/figures/photographing-development-process.png new file mode 100644 index 0000000000000000000000000000000000000000..c4e765c855692df379e6a866f45624279986dd8e Binary files /dev/null and b/zh-cn/application-dev/media/figures/photographing-development-process.png differ diff --git a/zh-cn/application-dev/media/figures/playback-status-change.png b/zh-cn/application-dev/media/figures/playback-status-change.png new file mode 100644 index 0000000000000000000000000000000000000000..860764d3d15b93e544a6f27316584963acba2f0f Binary files /dev/null and b/zh-cn/application-dev/media/figures/playback-status-change.png differ diff --git a/zh-cn/application-dev/media/figures/recording-development-process.png b/zh-cn/application-dev/media/figures/recording-development-process.png new file mode 100644 index 0000000000000000000000000000000000000000..e983bc5c35f7aa9489798292ff0b8897a64ab6af Binary files /dev/null and b/zh-cn/application-dev/media/figures/recording-development-process.png differ diff --git a/zh-cn/application-dev/media/figures/recording-status-change.png b/zh-cn/application-dev/media/figures/recording-status-change.png new file mode 100644 index 0000000000000000000000000000000000000000..9f15af9c1992e34fa7d750d08fd0245b6cb3ba67 Binary files /dev/null and b/zh-cn/application-dev/media/figures/recording-status-change.png differ diff --git a/zh-cn/application-dev/media/figures/rotate.jpeg b/zh-cn/application-dev/media/figures/rotate.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..5965abb46dc9648a3dfd9136e7cc0b5c5203e6a7 Binary files /dev/null and b/zh-cn/application-dev/media/figures/rotate.jpeg differ diff --git a/zh-cn/application-dev/media/figures/transparency.png b/zh-cn/application-dev/media/figures/transparency.png new file mode 100644 index 0000000000000000000000000000000000000000..b9b43939f0dad8ee40bf0b6b7e40ddf49d141c66 Binary files /dev/null and b/zh-cn/application-dev/media/figures/transparency.png differ diff --git a/zh-cn/application-dev/media/figures/vertical-flip.jpeg b/zh-cn/application-dev/media/figures/vertical-flip.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..8ef368d6bb914815a90c8d82352cbd6fd9ab505c Binary files /dev/null and b/zh-cn/application-dev/media/figures/vertical-flip.jpeg differ diff --git a/zh-cn/application-dev/media/figures/video-playback-interaction-diagram.png b/zh-cn/application-dev/media/figures/video-playback-interaction-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..cc45511de12c37a147e69133fca519630c2b002d Binary files /dev/null and b/zh-cn/application-dev/media/figures/video-playback-interaction-diagram.png differ diff --git a/zh-cn/application-dev/media/figures/video-playback-status-change.png b/zh-cn/application-dev/media/figures/video-playback-status-change.png new file mode 100644 index 0000000000000000000000000000000000000000..860764d3d15b93e544a6f27316584963acba2f0f Binary files /dev/null and b/zh-cn/application-dev/media/figures/video-playback-status-change.png differ diff --git a/zh-cn/application-dev/media/figures/video-recording-interaction-diagram.png b/zh-cn/application-dev/media/figures/video-recording-interaction-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..5af22c4510a630198ba54125d220db1248e9a09d Binary files /dev/null and b/zh-cn/application-dev/media/figures/video-recording-interaction-diagram.png differ diff --git a/zh-cn/application-dev/media/figures/video-recording-status-change.png b/zh-cn/application-dev/media/figures/video-recording-status-change.png new file mode 100644 index 0000000000000000000000000000000000000000..9f15af9c1992e34fa7d750d08fd0245b6cb3ba67 Binary files /dev/null and b/zh-cn/application-dev/media/figures/video-recording-status-change.png differ diff --git a/zh-cn/application-dev/media/figures/zh-ch_image_audio_player.png b/zh-cn/application-dev/media/figures/zh-ch_image_audio_player.png deleted file mode 100644 index a95dbe631b9187136e25357cb1fd996402513c09..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/media/figures/zh-ch_image_audio_player.png and /dev/null differ diff --git a/zh-cn/application-dev/media/figures/zh-ch_image_audio_recorder_state_machine.png b/zh-cn/application-dev/media/figures/zh-ch_image_audio_recorder_state_machine.png deleted file mode 100644 index 1154a72db714494f912023517c62396ea6f66fd3..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/media/figures/zh-ch_image_audio_recorder_state_machine.png and /dev/null differ diff --git a/zh-cn/application-dev/media/figures/zh-ch_image_audio_recorder_zero.png b/zh-cn/application-dev/media/figures/zh-ch_image_audio_recorder_zero.png deleted file mode 100644 index 90f032d88e6ae0d2a843555c569aa345f19d266c..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/media/figures/zh-ch_image_audio_recorder_zero.png and /dev/null differ diff --git a/zh-cn/application-dev/media/figures/zh-ch_image_audio_routing_manager.png b/zh-cn/application-dev/media/figures/zh-ch_image_audio_routing_manager.png deleted file mode 100644 index 710679f6cac0c30d06dffa97b0e80b3cebe80f79..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/media/figures/zh-ch_image_audio_routing_manager.png and /dev/null differ diff --git a/zh-cn/application-dev/media/figures/zh-ch_image_audio_state_machine.png b/zh-cn/application-dev/media/figures/zh-ch_image_audio_state_machine.png deleted file mode 100644 index 22b7aeaa1db5b369d3daf44854d7f7f9a00f775b..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/media/figures/zh-ch_image_audio_state_machine.png and /dev/null differ diff --git a/zh-cn/application-dev/media/figures/zh-ch_image_audio_stream_manager.png b/zh-cn/application-dev/media/figures/zh-ch_image_audio_stream_manager.png deleted file mode 100644 index b5c72f9d662abb75e177534a888eef14d48d62ab..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/media/figures/zh-ch_image_audio_stream_manager.png and /dev/null differ diff --git a/zh-cn/application-dev/media/figures/zh-ch_image_audio_volume_manager.png b/zh-cn/application-dev/media/figures/zh-ch_image_audio_volume_manager.png deleted file mode 100644 index 0d47fbfacce9c1ff48811e1cf5d764231bdb596b..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/media/figures/zh-ch_image_audio_volume_manager.png and /dev/null differ diff --git a/zh-cn/application-dev/media/figures/zh-ch_image_avplayer_audio.png b/zh-cn/application-dev/media/figures/zh-ch_image_avplayer_audio.png deleted file mode 100644 index 23b05d2a51adb734eab3099c36ff438aba895df8..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/media/figures/zh-ch_image_avplayer_audio.png and /dev/null differ diff --git a/zh-cn/application-dev/media/figures/zh-ch_image_avplayer_state_machine.png b/zh-cn/application-dev/media/figures/zh-ch_image_avplayer_state_machine.png deleted file mode 100644 index aa8afdbcbf142fd745cee03fc422caec51cfe41b..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/media/figures/zh-ch_image_avplayer_state_machine.png and /dev/null differ diff --git a/zh-cn/application-dev/media/figures/zh-ch_image_avplayer_video.png b/zh-cn/application-dev/media/figures/zh-ch_image_avplayer_video.png deleted file mode 100644 index 7a3c6795de742ad057b07223b8051fd2ae576974..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/media/figures/zh-ch_image_avplayer_video.png and /dev/null differ diff --git a/zh-cn/application-dev/media/figures/zh-ch_image_avrecorder_module_interaction.png b/zh-cn/application-dev/media/figures/zh-ch_image_avrecorder_module_interaction.png deleted file mode 100644 index 99f521f7f9b020212db9c19ee64a38649dbe604b..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/media/figures/zh-ch_image_avrecorder_module_interaction.png and /dev/null differ diff --git a/zh-cn/application-dev/media/figures/zh-ch_image_avrecorder_state_machine.png b/zh-cn/application-dev/media/figures/zh-ch_image_avrecorder_state_machine.png deleted file mode 100644 index 7ffcb21f09365e9b072bdaf48f8b98d7d45a8aaa..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/media/figures/zh-ch_image_avrecorder_state_machine.png and /dev/null differ diff --git a/zh-cn/application-dev/media/figures/zh-ch_image_video_player.png b/zh-cn/application-dev/media/figures/zh-ch_image_video_player.png deleted file mode 100644 index df6ee97f4941f854884282f59400fe0bdf675bb0..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/media/figures/zh-ch_image_video_player.png and /dev/null differ diff --git a/zh-cn/application-dev/media/figures/zh-ch_image_video_recorder_state_machine.png b/zh-cn/application-dev/media/figures/zh-ch_image_video_recorder_state_machine.png deleted file mode 100644 index 6fbcfaaa11b645143534b0a76308f6586eda1124..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/media/figures/zh-ch_image_video_recorder_state_machine.png and /dev/null differ diff --git a/zh-cn/application-dev/media/figures/zh-ch_image_video_recorder_zero.png b/zh-cn/application-dev/media/figures/zh-ch_image_video_recorder_zero.png deleted file mode 100644 index 8bfb453f935bd8342be989eba0393541b35ba582..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/media/figures/zh-ch_image_video_recorder_zero.png and /dev/null differ diff --git a/zh-cn/application-dev/media/figures/zh-ch_image_video_state_machine.png b/zh-cn/application-dev/media/figures/zh-ch_image_video_state_machine.png deleted file mode 100644 index 65387d45f1c3a0393643ef06cf6296dd77ad004a..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/media/figures/zh-ch_image_video_state_machine.png and /dev/null differ diff --git a/zh-cn/application-dev/media/figures/zh-cn_image_avsession.png b/zh-cn/application-dev/media/figures/zh-cn_image_avsession.png deleted file mode 100755 index 9abc37206f8a1084796f838ac0c392c175cf93a1..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/media/figures/zh-cn_image_avsession.png and /dev/null differ diff --git a/zh-cn/application-dev/media/figures/zoom.jpeg b/zh-cn/application-dev/media/figures/zoom.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..977db6cfbc5b81f5396e4d81f8954a9f7d4168e4 Binary files /dev/null and b/zh-cn/application-dev/media/figures/zoom.jpeg differ diff --git a/zh-cn/application-dev/media/image-decoding.md b/zh-cn/application-dev/media/image-decoding.md new file mode 100644 index 0000000000000000000000000000000000000000..64aa8ea69ca935ee971484898bc79e7d00c8e7a2 --- /dev/null +++ b/zh-cn/application-dev/media/image-decoding.md @@ -0,0 +1,141 @@ +# 图片解码 + +图片解码指将所支持格式的存档图片解码成统一的[PixelMap](image-overview.md),以便在应用或系统中进行图片显示或[图片处理](image-transformation.md)。当前支持的存档图片格式包括JPEG、PNG、GIF、RAW、WebP、BMP、SVG。 + +## 开发步骤 + +图片解码相关API的详细介绍请参见:[图片解码接口说明](../reference/apis/js-apis-image.md#imagesource)。 + +1. 全局导入Image模块。 + + ```ts + import image from '@ohos.multimedia.image'; + ``` + +2. 获取图片。 + - 方法一:获取沙箱路径。具体请参考[获取应用文件路径](../application-models/application-context-stage.md#获取应用开发路径)。应用沙箱的介绍及如何向应用沙箱推送文件,请参考[文件管理](../file-management/app-sandbox-directory.md)。 + + ```ts + // Stage模型参考如下代码 + const context = getContext(this); + const filePath = context.cacheDir + '/test.jpg'; + ``` + + ```ts + // FA模型参考如下代码 + import featureAbility from '@ohos.ability.featureAbility'; + + const context = featureAbility.getContext(); + const filePath = context.getCacheDir() + "/test.jpg"; + ``` + - 方法二:通过沙箱路径获取图片的文件描述符。具体请参考[file.fs API参考文档](../reference/apis/js-apis-file-fs.md)。 + 该方法需要先导入\@ohos.file.fs模块。 + + ```ts + import fs from '@ohos.file.fs'; + ``` + + 然后调用fs.openSync()获取文件描述符。 + + ```ts + // Stage模型参考如下代码 + const context = getContext(this); + const filePath = context.cacheDir + '/test.jpg'; + const file = fs.openSync(filePath, fs.OpenMode.READ_WRITE); + const fd = file?.fd; + ``` + + ```ts + // FA模型参考如下代码 + import featureAbility from '@ohos.ability.featureAbility'; + + const context = featureAbility.getContext(); + const filePath = context.getCacheDir() + "/test.jpg"; + const file = fs.openSync(filePath, fs.OpenMode.READ_WRITE); + const fd = file?.fd; + ``` + - 方法三:通过资源管理器获取资源文件的ArrayBuffer。具体请参考[ResourceManager API参考文档](../reference/apis/js-apis-resource-manager.md#getrawfilecontent9-1)。 + + ```ts + // Stage模型 + const context = getContext(this); + // 获取resourceManager资源管理器 + const resourceMgr = context.resourceManager; + ``` + + ```ts + // FA模型 + // 导入resourceManager资源管理器 + import resourceManager from '@ohos.resourceManager'; + const resourceMgr = await resourceManager.getResourceManager(); + ``` + + 不同模型获取资源管理器的方式不同,获取资源管理器后,再调用resourceMgr.getRawFileContent()获取资源文件的ArrayBuffer。 + + ```ts + const fileData = await resourceMgr.getRawFileContent('test.jpg'); + // 获取图片的ArrayBuffer + const buffer = fileData.buffer; + ``` + +3. 创建ImageSource实例。 + - 方法一:通过沙箱路径创建ImageSource。沙箱路径可以通过步骤2的方法一获取。 + + ```ts + // path为已获得的沙箱路径 + const imageSource = image.createImageSource(filePath); + ``` + - 方法二:通过文件描述符fd创建ImageSource。文件描述符可以通过步骤2的方法二获取。 + + ```ts + // fd为已获得的文件描述符 + const imageSource = image.createImageSource(fd); + ``` + - 方法三:通过缓冲区数组创建ImageSource。缓冲区数组可以通过步骤2的方法三获取。 + + ```ts + const imageSource = image.createImageSource(buffer); + ``` + +4. 设置解码参数DecodingOptions,解码获取PixelMap图片对象。 + + ```ts + let decodingOptions = { + editable: true, + desiredPixelFormat: 3, + } + // 创建pixelMap并进行简单的旋转和缩放 + const pixelMap = await imageSource.createPixelMap(decodingOptions); + ``` + + 解码完成,获取到PixelMap对象后,可以进行后续[图片处理](image-transformation.md)。 + +## 开发示例-对资源文件中的图片进行解码 + +1. 获取resourceManager资源管理。 + + ```ts + const context = getContext(this); + // 获取resourceManager资源管理 + const resourceMgr = context.resourceManager; + ``` + +2. 获取rawfile文件夹下test.jpg的ArrayBuffer。 + + ```ts + const fileData = await resourceMgr.getRawFileContent('test.jpg'); + // 获取图片的ArrayBuffer + const buffer = fileData.buffer; + ``` + +3. 创建imageSource。 + + ```ts + const imageSource = image.createImageSource(buffer); + ``` + +4. 创建PixelMap。 + + ```ts + const pixelMap = await imageSource.createPixelMap(); + ``` diff --git a/zh-cn/application-dev/media/image-encoding.md b/zh-cn/application-dev/media/image-encoding.md new file mode 100644 index 0000000000000000000000000000000000000000..325a5f94271cf092681d61354f323f146503641a --- /dev/null +++ b/zh-cn/application-dev/media/image-encoding.md @@ -0,0 +1,48 @@ +# 图片编码 + +图片编码指将PixelMap编码成不同格式的存档图片(当前仅支持打包为JPEG和WebP格式),用于后续处理,如保存、传输等。 + +## 开发步骤 + +图片编码相关API的详细介绍请参见:[图片编码接口说明](../reference/apis/js-apis-image.md#imagepacker)。 + +1. 创建图像编码ImagePacker对象。 + + ```ts + // 导入相关模块包 + import image from '@ohos.multimedia.image'; + + const imagePackerApi = image.createImagePacker(); + ``` + +2. 设置编码输出流和编码参数。 + + format为图像的编码格式;quality为图像质量,范围从0-100,100为最佳质量。 + + ```ts + let packOpts = { format:"image/jpeg", quality:98 }; + ``` + +3. [创建PixelMap对象或创建ImageSource](image-decoding.md)对象。 + +4. 进行图片编码,并保存编码后的图片。 + + 方法一:通过PixelMap进行编码。 + + ```ts + imagePackerApi.packing(pixelMap, packOpts).then( data => { + // data 为打包获取到的文件流,写入文件保存即可得到一张图片 + }).catch(error => { + console.error('Failed to pack the image. And the error is: ' + error); + }) + ``` + + 方法二:通过imageSource进行编码。 + + ```ts + imagePackerApi.packing(imageSource, packOpts).then( data => { + // data 为打包获取到的文件流,写入文件保存即可得到一张图片 + }).catch(error => { + console.error('Failed to pack the image. And the error is: ' + error); + }) + ``` diff --git a/zh-cn/application-dev/media/image-overview.md b/zh-cn/application-dev/media/image-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..29747a33d28834532e7250c39b14a8c8d5caa8a0 --- /dev/null +++ b/zh-cn/application-dev/media/image-overview.md @@ -0,0 +1,34 @@ +# 图片开发概述 + +应用开发中的图片开发是对图片像素数据进行解析、处理、构造的过程,达到目标图片效果,主要涉及图片解码、图片处理、图片编码等。 + +在学习图片开发前,需要熟悉以下基本概念。 + +- 图片解码 + 指将所支持格式的存档图片解码成统一的PixelMap,以便在应用或系统中进行图片显示或图片处理。当前支持的存档图片格式包括JPEG、PNG、GIF、RAW、WebP、BMP、SVG。 + +- PixelMap + 指图片解码后无压缩的位图,用于图片显示或图片处理。 + +- 图片处理 + 指对PixelMap进行相关的操作,如旋转、缩放、设置透明度、获取图片信息、读写像素数据等。 + +- 图片编码 + 指将PixelMap编码成不同格式的存档图片(当前仅支持JPEG和WebP),用于后续处理,如保存、传输等。 + +图片开发的主要流程如下图所示。 + +**图1** 图片开发流程示意图   +![Image development process](figures/image-development-process.png) + +1. 获取图片:通过应用沙箱等方式获取原始图片。 + +2. 创建ImageSource实例:ImageSource是图片解码出来的图片源类,用于获取或修改图片相关信息。 + +3. [图片解码](image-decoding.md):通过ImageSource解码生成PixelMap。 + +4. [图片处理](image-transformation.md):对PixelMap进行处理,更改图片属性实现图片的旋转、缩放、裁剪等效果。然后通过[Image组件](../ui/arkts-graphics-display.md)显示图片。 + +5. [图片编码](image-encoding.md):使用图片打包器类ImagePacker,将PixelMap或ImageSource进行压缩编码,生成一张新的图片。 + +除上述基本图片开发能力外,OpenHarmony还提供常用[图片工具](image-tool.md),供开发者选择使用。 diff --git a/zh-cn/application-dev/media/image-pixelmap-operation.md b/zh-cn/application-dev/media/image-pixelmap-operation.md new file mode 100644 index 0000000000000000000000000000000000000000..c3aa0fd21e0b57355b7dcb0277cd758e4b6943d6 --- /dev/null +++ b/zh-cn/application-dev/media/image-pixelmap-operation.md @@ -0,0 +1,60 @@ +# 位图操作 + +当需要对目标图片中的部分区域进行处理时,可以使用位图操作功能。此功能常用于图片美化等操作。 + +如下图所示,一张图片中,将指定的矩形区域像素数据读取出来,进行修改后,再写回原图片对应区域。 + +**图1** 位图操作示意图   +![Bitmap operation](figures/bitmap-operation.png) + +## 开发步骤 + +位图操作相关API的详细介绍请参见[API参考](../reference/apis/js-apis-image.md#pixelmap7)。 + +1. 完成[图片解码](image-decoding.md#开发步骤),获取PixelMap位图对象。 + +2. 从PixelMap位图对象中获取信息。 + + ```ts + // 获取图像像素的总字节数 + let pixelBytesNumber = pixelMap.getPixelBytesNumber(); + // 获取图像像素每行字节数 + let rowCount = pixelMap.getBytesNumberPerRow(); + // 获取当前图像像素密度。像素密度是指每英寸图片所拥有的像素数量。像素密度越大,图片越精细。 + let getDensity = pixelMap.getDensity(); + ``` + +3. 读取并修改目标区域像素数据,写回原图。 + + ```ts + // 场景一:将读取的整张图像像素数据结果写入ArrayBuffer中 + const readBuffer = new ArrayBuffer(pixelBytesNumber); + pixelMap.readPixelsToBuffer(readBuffer).then(() => { + console.info('Succeeded in reading image pixel data.'); + }).catch(error => { + console.error('Failed to read image pixel data. And the error is: ' + error); + }) + + // 场景二:读取指定区域内的图片数据,结果写入area.pixels中 + const area = { + pixels: new ArrayBuffer(8), + offset: 0, + stride: 8, + region: { size: { height: 1, width: 2 }, x: 0, y: 0 } + } + pixelMap.readPixels(area).then(() => { + console.info('Succeeded in reading the image data in the area.'); + }).catch(error => { + console.error('Failed to read the image data in the area. And the error is: ' + error); + }) + + // 对于读取的图片数据,可以独立使用(创建新的pixelMap),也可以对area.pixels进行所需修改 + // 将图片数据area.pixels写入指定区域内 + pixelMap.writePixels(area).then(() => { + console.info('Succeeded to write pixelMap into the specified area.'); + }) + + // 将图片数据结果写入pixelMap中 + const writeColor = new ArrayBuffer(96); + pixelMap.writeBufferToPixels(writeColor, () => {}); + ``` diff --git a/zh-cn/application-dev/media/image-tool.md b/zh-cn/application-dev/media/image-tool.md new file mode 100644 index 0000000000000000000000000000000000000000..2d7f8c3220d66f5c567814df04e710a6e482c7e2 --- /dev/null +++ b/zh-cn/application-dev/media/image-tool.md @@ -0,0 +1,43 @@ +# 图片工具 + +图片工具当前主要提供图片EXIF信息的读取与编辑能力。 + +EXIF(Exchangeable image file format)是专门为数码相机的照片设定的文件格式,可以记录数码照片的属性信息和拍摄数据。当前仅支持JPEG格式图片。 + +在图库等应用中,需要查看或修改数码照片的EXIF信息。由于摄像机的手动镜头的参数无法自动写入到EXIF信息中或者因为相机断电等原因经常会导致拍摄时间出错,这时候就需要手动修改错误的EXIF数据,即可使用本功能。 + +OpenHarmony目前仅支持对部分EXIF信息的查看和修改,具体支持的范围请参见:[EIXF信息](../reference/apis/js-apis-image.md#propertykey7)。 + +## 开发步骤 + +EXIF信息的读取与编辑相关API的详细介绍请参见[API参考](../reference/apis/js-apis-image.md#getimageproperty7)。 + +1. 获取图片,创建图片源ImageSource。 + + ```ts + // 导入相关模块包 + import image from '@ohos.multimedia.image'; + + // 获取沙箱路径创建ImageSource + const fd = ...; // 获取需要被处理的图片的fd + const imageSource = image.createImageSource(fd); + ``` + +2. 读取、编辑EXIF信息。 + + ```ts + // 读取EXIF信息,BitsPerSample为每个像素比特数 + imageSource.getImageProperty('BitsPerSample', (error, data) => { + if (error) { + console.error('Failed to get the value of the specified attribute key of the image.And the error is: ' + error); + } else { + console.info('Succeeded in getting the value of the specified attribute key of the image ' + data); + } + }) + + // 编辑EXIF信息 + imageSource.modifyImageProperty('ImageWidth', '120').then(() => { + const width = imageSource.getImageProperty("ImageWidth"); + console.info('The new imageWidth is ' + width); + }) + ``` diff --git a/zh-cn/application-dev/media/image-transformation.md b/zh-cn/application-dev/media/image-transformation.md new file mode 100644 index 0000000000000000000000000000000000000000..bab88068a6979b882066b271be6f3b1a7d304fe7 --- /dev/null +++ b/zh-cn/application-dev/media/image-transformation.md @@ -0,0 +1,93 @@ +# 图像变换 + +图片处理指对PixelMap进行相关的操作,如获取图片信息、裁剪、缩放、偏移、旋转、翻转、设置透明度、读写像素数据等。图片处理主要包括图像变换、[位图操作](image-pixelmap-operation.md),本文介绍图像变换。 + +## 开发步骤 + +图像变换相关API的详细介绍请参见[API参考](../reference/apis/js-apis-image.md#pixelmap7)。 + +1. 完成[图片解码](image-decoding.md#开发步骤),获取Pixelmap对象。 + +2. 获取图片信息。 + + ``` + // 获取图片大小 + pixelMap.getImageInfo().then( info => { + console.info('info.width = ' + info.size.width); + console.info('info.height = ' + info.size.height); + }).catch((err) => { + console.error("Failed to obtain the image pixel map information.And the error is: " + err); + }); + ``` + +3. 进行图像变换操作。 + + 原图: + + ![Original drawing](figures/original-drawing.jpeg) + - 裁剪 + + ``` + // x:裁剪起始点横坐标0 + // y:裁剪起始点纵坐标0 + // height:裁剪高度400,方向为从上往下 + // width:裁剪宽度400,方向为从左到右 + pixelMap.crop({x: 0, y: 0, size: { height: 400, width: 400 } }); + ``` + + ![cropping](figures/cropping.jpeg) + + - 缩放 + + ``` + // 宽为原来的0.5 + // 高为原来的0.5 + pixelMap.scale(0.5, 0.5); + ``` + + ![zoom](figures/zoom.jpeg) + + - 偏移 + + ``` + // 向下偏移100 + // 向右偏移100 + pixelMap.translate(100, 100); + ``` + + ![offsets](figures/offsets.jpeg) + + - 旋转 + + ``` + // 顺时针旋转90° + pixelMap.rotate(90); + ``` + + ![rotate](figures/rotate.jpeg) + + - 翻转 + + ``` + // 垂直翻转 + pixelMap.flip(false, true); + ``` + + ![Vertical Flip](figures/vertical-flip.jpeg) + + + ``` + // 水平翻转 + pixelMap.flip(true, false); + ``` + + ![Horizontal Flip](figures/horizontal-flip.jpeg) + + - 透明度 + + ``` + // 透明度0.5 + pixelMap.opacity(0.5); + ``` + + ![Transparency](figures/transparency.png) diff --git a/zh-cn/application-dev/media/image.md b/zh-cn/application-dev/media/image.md deleted file mode 100644 index 7a07a7d579c4a0df736dbde4215e0b97b23da362..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/media/image.md +++ /dev/null @@ -1,291 +0,0 @@ -# 图片开发指导 - -## 场景介绍 - -图片开发的主要工作是将获取到的图片进行解码,将解码后的pixelmap编码成支持的格式,本文将对图片的解码、编码等场景开发进行介绍说明。 - -## 接口说明 - -详细API含义请参考:[图片处理API文档](../reference/apis/js-apis-image.md) - -## 开发步骤 - -### 全流程场景 - -包含流程:创建实例、读取图片信息、读写pixelmap、更新数据、打包像素、释放资源等流程。 - -```js -const color = new ArrayBuffer(96); // 用于存放图像像素数据 -let opts = { alphaType: 0, editable: true, pixelFormat: 4, scaleMode: 1, size: { height: 2, width: 3 } } // 图像像素数据 - -// 创建pixelmap对象 -image.createPixelMap(color, opts, (err, pixelmap) => { - console.log('Succeeded in creating pixelmap.'); - // 创建pixelmap对象失败 - if (err) { - console.info('create pixelmap failed, err' + err); - return - } - - // 用于读像素 - const area = { - pixels: new ArrayBuffer(8), - offset: 0, - stride: 8, - region: { size: { height: 1, width: 2 }, x: 0, y: 0 } - } - pixelmap.readPixels(area,() => { - let bufferArr = new Uint8Array(area.pixels); - let res = true; - for (let i = 0; i < bufferArr.length; i++) { - console.info(' buffer ' + bufferArr[i]); - if(res) { - if(bufferArr[i] == 0) { - res = false; - console.log('readPixels end.'); - break; - } - } - } - }) - - // 用于存像素 - const readBuffer = new ArrayBuffer(96); - pixelmap.readPixelsToBuffer(readBuffer,() => { - let bufferArr = new Uint8Array(readBuffer); - let res = true; - for (let i = 0; i < bufferArr.length; i++) { - if(res) { - if (bufferArr[i] !== 0) { - res = false; - console.log('readPixelsToBuffer end.'); - break; - } - } - } - }) - - // 用于写像素 - pixelmap.writePixels(area,() => { - const readArea = { pixels: new ArrayBuffer(20), offset: 0, stride: 8, region: { size: { height: 1, width: 2 }, x: 0, y: 0 }} - pixelmap.readPixels(readArea,() => { - let readArr = new Uint8Array(readArea.pixels); - let res = true; - for (let i = 0; i < readArr.length; i++) { - if(res) { - if (readArr[i] !== 0) { - res = false; - console.log('readPixels end.please check buffer'); - break; - } - } - } - }) - }) - - const writeColor = new ArrayBuffer(96); //图像像素数据 - // 用于写像素到缓冲区 - pixelmap.writeBufferToPixels(writeColor).then(() => { - const readBuffer = new ArrayBuffer(96); - pixelmap.readPixelsToBuffer(readBuffer).then (() => { - let bufferArr = new Uint8Array(readBuffer); - let res = true; - for (let i = 0; i < bufferArr.length; i++) { - if(res) { - if (bufferArr[i] !== i) { - res = false; - console.log('readPixels end.please check buffer'); - break; - } - } - } - }) - }) - - // 用于获取图片信息 - pixelmap.getImageInfo((err, imageInfo) => { - // 获取图片信息失败 - if (err || imageInfo == null) { - console.info('getImageInfo failed, err' + err); - return - } - if (imageInfo !== null) { - console.log('Succeeded in getting imageInfo'); - } - }) - - // 用于释放pixelmap - pixelmap.release(()=>{ - console.log('Succeeded in releasing pixelmap'); - }) -}) - -// 用于创建imagesource(uri) -let path = '/data/local/tmp/test.jpg'; -const imageSourceApi1 = image.createImageSource(path); - -// 用于创建imagesource(fd) -let fd = 29; -const imageSourceApi2 = image.createImageSource(fd); - -// 用于创建imagesource(data) -const data = new ArrayBuffer(96); -const imageSourceApi3 = image.createImageSource(data); - -// 用于释放imagesource -imageSourceApi3.release(() => { - console.log('Succeeded in releasing imagesource'); -}) - -// 用于编码 -const imagePackerApi = image.createImagePacker(); -const imageSourceApi = image.createImageSource(0); -let packOpts = { format:"image/jpeg", quality:98 }; -imagePackerApi.packing(imageSourceApi, packOpts, (err, data) => { - if (err) { - console.info('packing from imagePackerApi failed, err' + err); - return - } - console.log('Succeeded in packing'); -}) - -// 用于释放imagepacker -imagePackerApi.release(); -``` - -### 解码场景 - -```js -let path = '/data/local/tmp/test.jpg'; // 设置创建imagesource的路径 - -// 用路径创建imagesource -const imageSourceApi = image.createImageSource(path); // '/data/local/tmp/test.jpg' - -// 设置参数 -let decodingOptions = { - sampleSize:1, // 缩略图采样大小 - editable: true, // 是否可编辑 - desiredSize:{ width:1, height:2}, // 期望输出大小 - rotateDegrees:10, // 旋转角度 - desiredPixelFormat:2, // 解码的像素格式 - desiredRegion: { size: { height: 1, width: 2 }, x: 0, y: 0 }, // 解码的区域 - index:0 // 图片序号 - }; - -// 用于回调方式创建pixelmap -imageSourceApi.createPixelMap(decodingOptions, (err, pixelmap) => { - // 创建pixelmap对象失败 - if (err) { - console.info('create pixelmap failed, err' + err); - return - } - console.log('Succeeded in creating pixelmap.'); -}) - -// 用于promise创建pixelmap -imageSourceApi.createPixelMap().then(pixelmap => { - console.log('Succeeded in creating pixelmap.'); - - // 用于获取像素每行字节数 - let num = pixelmap.getBytesNumberPerRow(); - - // 用于获取像素总字节数 - let pixelSize = pixelmap.getPixelBytesNumber(); - - // 用于获取pixelmap信息 - pixelmap.getImageInfo().then( imageInfo => {}); - - // 用于释放pixelmap - pixelmap.release(()=>{ - console.log('Succeeded in releasing pixelmap'); - }) -}).catch(error => { - console.log('Failed in creating pixelmap.' + error); -}) -``` - -### 编码场景 - -```js -let path = '/data/local/tmp/test.png'; // 设置创建imagesource的路径 - -// 用于设置imagesource -const imageSourceApi = image.createImageSource(path); // '/data/local/tmp/test.png' - -// 如果创建imagesource失败,打印错误信息 -if (imageSourceApi == null) { - console.log('Failed in creating imageSource.'); -} - -// 如果创建imagesource成功,则创建imagepacker -const imagePackerApi = image.createImagePacker(); - -// 如果创建失败,打印错误信息 -if (imagePackerApi == null) { - console.log('Failed in creating imagePacker.'); -} - -// 如果创建imagepacker成功,则设置编码参数 -let packOpts = { format:"image/jpeg", // 支持编码的格式为jpg - quality:98 } // 图片质量0-100 - -// 用于编码 -imagePackerApi.packing(imageSourceApi, packOpts) -.then( data => { - console.log('Succeeded in packing'); -}) - -// 编码完成,释放imagepacker -imagePackerApi.release(); - -// 用于获取imagesource信息 -imageSourceApi.getImageInfo((err, imageInfo) => { - console.log('Succeeded in getting imageInfo'); -}) - -const array = new ArrayBuffer(100); //增量数据 -// 用于更新增量数据 -imageSourceApi.updateData(array, false, 0, 10,(error, data)=> {}) - -``` - -### ImageReceiver的使用 - -示例场景:camera作为客户端将拍照数据传给服务端 - -```js -public async init(surfaceId: any) { - - // 服务端代码,创建ImageReceiver - let receiver = image.createImageReceiver(8 * 1024, 8, image.ImageFormat.JPEG, 1); - - // 获取Surface ID - receiver.getReceivingSurfaceId((err, surfaceId) => { - // 获取Surface ID失败 - if (err) { - console.info('getReceivingSurfaceId failed, err' + err); - return - } - console.info("receiver getReceivingSurfaceId success"); - }); - // 注册Surface的监听,在surface的buffer准备好后触发 - receiver.on('imageArrival', () => { - // 去获取Surface中最新的buffer - receiver.readNextImage((err, img) => { - img.getComponent(4, (err, component) => { - // 消费component.byteBuffer,例如:将buffer内容保存成图片。 - }) - }) - }) - - // 调用Camera方法将surfaceId传递给Camera。camera会通过surfaceId获取surface,并生产出surface buffer。 -} -``` - -## 相关实例 - -针对图片开发,有以下相关实例可供参考: - -- [`Image`:图片处理(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/BasicFeature/Media/Image) - -- [`GamePuzzle`:拼图(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/BasicFeature/Media/GamePuzzle) \ No newline at end of file diff --git a/zh-cn/application-dev/media/local-avsession-overview.md b/zh-cn/application-dev/media/local-avsession-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..e71021292ba600d5e413aed307edcd28cdb18c03 --- /dev/null +++ b/zh-cn/application-dev/media/local-avsession-overview.md @@ -0,0 +1,63 @@ +# 本地媒体会话概述 + +## 交互过程 + +本地媒体会话的数据源均在设备本地,交互过程如图所示。 + +![Local AVSession Interaction Process](figures/local-avsession-interaction-process.png) + +此过程中涉及两大角色,媒体会话提供方和媒体会话控制方。 + +本地媒体会话中,媒体会话提供方通过媒体会话管理器和媒体会话控制方进行信息交互: + +1. 媒体会话提供方通过AVSessionManager创建AVSession对象。 + +2. 媒体会话提供方通过AVSession对象,设置会话元数据(媒体ID、标题、媒体时长等)、会话播放属性(播放状态、播放倍速、播放位置等)等。 + +3. 媒体会话控制方通过AVSessionManager创建AVSessionController对象。 + +4. 媒体会话控制方通过AVSessionController对象可以监听对应会话元数据变化、播放属性变化等。 + +5. 媒体会话控制方通过AVSessionController对象还可以向媒体会话发送控制命令。 + +6. 媒体会话提供方通过AVSession对象可以监听来自媒体会话控制方的控制命令,例如:“play”播放、“playNext”播放下一首、“fastForward”快进、 “setSpeed”设置播放倍数等。 + +## 媒体会话管理器 + +媒体会话管理器(AVSessionManager),提供了管理AVSession的能力,可以创建AVSession、创建AVSessionController、发送系统控制事件,也支持对AVSession的状态进行监听。 + +实际上,AVSessionManager与AVSession、AVSessionController对象不同,并不是一个具体的对象,它是媒体会话的根命名域。在实际编程过程中,可以通过如下方式引入: + +```ts +import AVSessionManager from '@ohos.multimedia.avsession'; +``` + +根命名域中的所有方法都可以作为AVSessionManager的方法。 + +例如,媒体会话提供方通过AVSessionManager创建媒体会话的示例如下所示: + +```ts +// 创建session +async createSession() { + let session: AVSessionManager.AVSession = await AVSessionManager.createAVSession(this.context, 'SESSION_NAME', 'audio'); + console.info(`session create done : sessionId : ${session.sessionId}`); +} +``` + +例如,媒体会话控制方通过AVSessionManager创建媒体会话控制器的示例如下所示: + +```ts +// 创建controller +async createController() { + // 获取到所有存活session的描述符列表 + let descriptorsArray: Array> = await AVSessionManager.getAllSessionDescriptors(); + if (descriptorsArray.length > 0) { + // 为了演示,我们简单取第一个描述符的sessionId用来创建对应的controller + let sessionId: string = descriptorsArray[0].sessionId; + let avSessionController: AVSessionManager.AVSessionController = await AVSessionManager.createController(sessionId); + console.info(`controller create done : sessionId : ${avSessionController.sessionId}`); + } +} +``` + +更多关于AVSessionManager的方法,可以参考[API文档](../reference/apis/js-apis-avsession.md)。 diff --git a/zh-cn/application-dev/media/media-application-overview.md b/zh-cn/application-dev/media/media-application-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..ff8f726b7d58514f5025f2ebfa50239e2142a313 --- /dev/null +++ b/zh-cn/application-dev/media/media-application-overview.md @@ -0,0 +1,19 @@ +# 媒体应用开发概述 + +## 媒体系统架构 + +媒体系统提供用户视觉、听觉信息的处理能力,如音视频信息的采集、压缩存储、解压播放等。 在操作系统实现中,通常基于不同的媒体信息处理内容,将媒体划分为音频子系统、视频子系统(也称播放录制子系统)、相机子系统、图片子系统或服务等。 + +如下图所示,媒体系统面向应用开发提供音视频应用、相机应用、图库应用的编程框架接口;面向设备开发提供对接不同硬件芯片适配加速功能;中间以服务形态提供媒体核心功能和管理机制。 + +**图1** 媒体系统整体框架   + +![Media system framework](figures/media-system-framework.png) + +- 音频子系统(audio):提供音量管理、音频路由管理、混音管理接口与服务。 + +- 视频子系统(media):提供音视频解压播放、压缩录制接口与服务。 + +- 相机子系统(camera):提供精确控制相机镜头,采集视觉信息的接口与服务。 + +- 图片子系统(image):提供图片编解码、图片处理接口与服务。 diff --git a/zh-cn/application-dev/media/mic-management.md b/zh-cn/application-dev/media/mic-management.md new file mode 100644 index 0000000000000000000000000000000000000000..f8fe096ee24b6484066135bf9fada876d60c839c --- /dev/null +++ b/zh-cn/application-dev/media/mic-management.md @@ -0,0 +1,114 @@ +# 管理麦克风 + +因为在录制过程中需要使用麦克风录制相关音频数据,所以建议开发者在调用录制接口前查询麦克风状态,并在录制过程中监听麦克风的状态变化,避免影响录制效果。 + +在音频录制过程中,用户可以将麦克风静音,此时录音过程正常进行,录制生成的数据文件的大小随录制时长递增,但写入文件的数据均为0,即无声数据(空白数据)。 + +## 开发步骤及注意事项 + +在AudioVolumeGroupManager中提供了管理麦克风状态的方法,接口的详细说明请参考[API文档](../reference/apis/js-apis-audio.md#audiovolumegroupmanager9)。 + +1. 创建audioVolumeGroupManager对象。 + + ```ts + import audio from '@ohos.multimedia.audio'; + + let audioVolumeGroupManager; + async function loadVolumeGroupManager() { //创建audioVolumeGroupManager对象 + const groupid = audio.DEFAULT_VOLUME_GROUP_ID; + audioVolumeGroupManager = await audio.getAudioManager().getVolumeManager().getVolumeGroupManager(groupid); + console.info('audioVolumeGroupManager create success.'); + } + ``` + +2. 调用on('micStateChange')监听麦克风状态变化,当麦克风静音状态发生变化时将通知应用。 + + 目前此订阅接口在单进程多AudioManager实例的使用场景下,仅最后一个实例的订阅生效,其他实例的订阅会被覆盖(即使最后一个实例没有进行订阅),因此推荐使用单一AudioManager实例进行开发。 + + + ```ts + async function on() { //监听麦克风状态变化 + audioVolumeGroupManager.on('micStateChange', (micStateChange) => { + console.info(`Current microphone status is: ${micStateChange.mute} `); + }); + } + ``` + +3. 调用isMicrophoneMute查询麦克风当前静音状态,返回true为静音,false为非静音。 + + ```ts + async function isMicrophoneMute() { //查询麦克风是否静音 + await audioVolumeGroupManager.isMicrophoneMute().then((value) => { + console.info(`isMicrophoneMute is: ${value}.`); + }); + } + ``` + +4. 根据查询结果的实际情况,调用setMicrophoneMute设置麦克风静音状态,入参输入true为静音,false为非静音。设置成功返回true,否则返回false。 + + ```ts + async function setMicrophoneMuteTrue() { //设置麦克风静音,入参为true + await audioVolumeGroupManager.setMicrophoneMute(true).then(() => { + console.info('setMicrophoneMute to mute.'); + }); + } + async function setMicrophoneMuteFalse() { //取消麦克风静音,入参为false + await audioVolumeGroupManager.setMicrophoneMute(false).then(() => { + console.info('setMicrophoneMute to not mute.'); + }); + } + ``` + +## 完整示例 + +参考以下示例,完成从设置麦克风静音到取消麦克风静音的过程。 + +```ts +import audio from '@ohos.multimedia.audio'; + +@Entry +@Component +struct AudioVolumeGroup { + private audioVolumeGroupManager: audio.AudioVolumeGroupManager; + + async loadVolumeGroupManager() { + const groupid = audio.DEFAULT_VOLUME_GROUP_ID; + this.audioVolumeGroupManager = await audio.getAudioManager().getVolumeManager().getVolumeGroupManager(groupid); + console.info('audioVolumeGroupManager------create-------success.'); + } + + async on() { //监听麦克风状态变化 + await this.loadVolumeGroupManager(); + this.audioVolumeGroupManager.on('micStateChange', (micStateChange) => { + console.info(`Current microphone status is: ${micStateChange.mute} `); + }); + } + async isMicrophoneMute() { //查询麦克风是否静音 + await this.audioVolumeGroupManager.isMicrophoneMute().then((value) => { + console.info(`isMicrophoneMute is: ${value}.`); + }); + } + async setMicrophoneMuteTrue() { //设置麦克风静音 + await this.loadVolumeGroupManager(); + await this.audioVolumeGroupManager.setMicrophoneMute(true).then(() => { + console.info('setMicrophoneMute to mute.'); + }); + } + async setMicrophoneMuteFalse() { //取消麦克风静音 + await this.loadVolumeGroupManager(); + await this.audioVolumeGroupManager.setMicrophoneMute(false).then(() => { + console.info('setMicrophoneMute to not mute.'); + }); + } + async test(){ + await this.on(); + await this.isMicrophoneMute(); + await this.setMicrophoneMuteTrue(); + await this.isMicrophoneMute(); + await this.setMicrophoneMuteFalse(); + await this.isMicrophoneMute(); + await this.setMicrophoneMuteTrue(); + await this.isMicrophoneMute(); + } +} +``` diff --git a/zh-cn/application-dev/media/opensles-capture.md b/zh-cn/application-dev/media/opensles-capture.md deleted file mode 100644 index bf7120054a8431a0ccbe877a312d44dbd80f33e6..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/media/opensles-capture.md +++ /dev/null @@ -1,151 +0,0 @@ -# OpenSL ES音频录制开发指导 - -## 简介 - -开发者可以通过本文档了解在**OpenHarmony**中如何使用**OpenSL ES**进行录音相关操作;当前仅实现了部分[**OpenSL ES**接口](https://gitee.com/openharmony/third_party_opensles/blob/master/api/1.0.1/OpenSLES.h),因此调用未实现接口后会返回**SL_RESULT_FEATURE_UNSUPPORTED**。 - -## 开发指导 - -以下步骤描述了在**OpenHarmony**如何使用 **OpenSL ES** 开发音频录音功能: - -1. 添加头文件 - - ```c++ - #include - #include - #include - ``` - -2. 使用 **slCreateEngine** 接口创建引擎对象和实例化引擎对象 **engine** - - ```c++ - SLObjectItf engineObject = nullptr; - slCreateEngine(&engineObject, 0, nullptr, 0, nullptr, nullptr); - (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE); - ``` - -3. 获取接口 **SL_IID_ENGINE** 的引擎接口 **engineEngine** 实例 - - ```c++ - SLEngineItf engineItf = nullptr; - result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineItf); - ``` - -4. 配置录音器信息(配置输入源audiosource、输出源audiosink),创建录音对象**pcmCapturerObject** - - ```c++ - SLDataLocator_IODevice io_device = { - SL_DATALOCATOR_IODEVICE, - SL_IODEVICE_AUDIOINPUT, - SL_DEFAULTDEVICEID_AUDIOINPUT, - NULL - }; - - SLDataSource audioSource = { - &io_device, - NULL - }; - - SLDataLocator_BufferQueue buffer_queue = { - SL_DATALOCATOR_BUFFERQUEUE, - 3 - }; - - // 具体参数需要根据音频文件格式进行适配 - SLDataFormat_PCM format_pcm = { - SL_DATAFORMAT_PCM, // 输入的音频格式 - 1, // 单声道 - SL_SAMPLINGRATE_44_1, // 采样率,44100HZ - SL_PCMSAMPLEFORMAT_FIXED_16, // 音频采样格式,小尾数,带符号的16位整数 - 0, - 0, - 0 - }; - - SLDataSink audioSink = { - &buffer_queue, - &format_pcm - }; - - SLObjectItf pcmCapturerObject = nullptr; - result = (*engineItf)->CreateAudioRecorder(engineItf, &pcmCapturerObject, - &audioSource, &audioSink, 0, nullptr, nullptr); - (*pcmCapturerObject)->Realize(pcmCapturerObject, SL_BOOLEAN_FALSE); - ``` - -5. 获取录音接口**SL_IID_RECORD** 的 **recordItf** 接口实例 - - ```c++ - SLRecordItf recordItf; - (*pcmCapturerObject)->GetInterface(pcmCapturerObject, SL_IID_RECORD, &recordItf); - ``` - -6. 获取接口 **SL_IID_OH_BUFFERQUEUE** 的 **bufferQueueItf** 实例 - - ```c++ - SLOHBufferQueueItf bufferQueueItf; - (*pcmCapturerObject)->GetInterface(pcmCapturerObject, SL_IID_OH_BUFFERQUEUE, &bufferQueueItf); - ``` - -7. 注册 **BufferQueueCallback** 回调 - - ```c++ - static void BufferQueueCallback(SLOHBufferQueueItf bufferQueueItf, void *pContext, SLuint32 size) - { - AUDIO_INFO_LOG("BufferQueueCallback"); - FILE *wavFile = (FILE *)pContext; - if (wavFile != nullptr) { - SLuint8 *buffer = nullptr; - SLuint32 pSize = 0; - (*bufferQueueItf)->GetBuffer(bufferQueueItf, &buffer, pSize); - if (buffer != nullptr) { - fwrite(buffer, 1, pSize, wavFile); - (*bufferQueueItf)->Enqueue(bufferQueueItf, buffer, size); - } - } - - return; - } - - // wavFile_ 需要设置为用户想要录音的文件描述符 - (*bufferQueueItf)->RegisterCallback(bufferQueueItf, BufferQueueCallback, wavFile_); - ``` - -8. 开始录音 - - ```c++ - static void CaptureStart(SLRecordItf recordItf, SLOHBufferQueueItf bufferQueueItf, FILE *wavFile) - { - AUDIO_INFO_LOG("CaptureStart"); - (*recordItf)->SetRecordState(recordItf, SL_RECORDSTATE_RECORDING); - if (wavFile != nullptr) { - SLuint8* buffer = nullptr; - SLuint32 pSize = 0; - (*bufferQueueItf)->GetBuffer(bufferQueueItf, &buffer, pSize); - if (buffer != nullptr) { - AUDIO_INFO_LOG("CaptureStart, enqueue buffer length: %{public}lu.", pSize); - fwrite(buffer, 1, pSize, wavFile); - (*bufferQueueItf)->Enqueue(bufferQueueItf, buffer, pSize); - } else { - AUDIO_INFO_LOG("CaptureStart, buffer is null or pSize: %{public}lu.", pSize); - } - } - - return; - } - ``` - -9. 结束录音 - - ```c++ - static void CaptureStop(SLRecordItf recordItf) - { - AUDIO_INFO_LOG("Enter CaptureStop"); - fflush(wavFile_); - (*recordItf)->SetRecordState(recordItf, SL_RECORDSTATE_STOPPED); - (*pcmCapturerObject)->Destroy(pcmCapturerObject); - fclose(wavFile_); - wavFile_ = nullptr; - return; - } - ``` \ No newline at end of file diff --git a/zh-cn/application-dev/media/opensles-playback.md b/zh-cn/application-dev/media/opensles-playback.md deleted file mode 100644 index 51108a83dc6567b4c21e69c21a0424cb1b0b951a..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/media/opensles-playback.md +++ /dev/null @@ -1,104 +0,0 @@ -# OpenSL ES音频播放开发指导 - -## 简介 - -开发者可以通过本文档了解在**OpenHarmony**中如何使用**OpenSL ES**进行音频播放相关操作;当前仅实现了部分[**OpenSL ES**接口](https://gitee.com/openharmony/third_party_opensles/blob/master/api/1.0.1/OpenSLES.h),因此调用未实现接口后会返回**SL_RESULT_FEATURE_UNSUPPORTED** - -## 开发指导 - -以下步骤描述了在**OpenHarmony**如何使用**OpenSL ES**开发音频播放功能: - -1. 添加头文件 - - ```c++ - #include - #include - #include - ``` - -2. 使用 **slCreateEngine** 接口和获取 **engine** 实例 - - ```c++ - SLObjectItf engineObject = nullptr; - slCreateEngine(&engineObject, 0, nullptr, 0, nullptr, nullptr); - (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE); - ``` - -3. 获取接口 **SL_IID_ENGINE** 的 **engineEngine** 实例 - - ```c++ - SLEngineItf engineEngine = nullptr; - (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine); - ``` - -4. 配置播放器信息,创建 **AudioPlayer** - - ```c++ - SLDataLocator_BufferQueue slBufferQueue = { - SL_DATALOCATOR_BUFFERQUEUE, - 0 - }; - - // 具体参数需要根据音频文件格式进行适配 - SLDataFormat_PCM pcmFormat = { - SL_DATAFORMAT_PCM, - 2, - 48000, - 16, - 0, - 0, - 0 - }; - SLDataSource slSource = {&slBufferQueue, &pcmFormat}; - - SLObjectItf pcmPlayerObject = nullptr; - (*engineEngine)->CreateAudioPlayer(engineEngine, &pcmPlayerObject, &slSource, null, 0, nullptr, nullptr); - (*pcmPlayerObject)->Realize(pcmPlayerObject, SL_BOOLEAN_FALSE); - ``` - -5. 获取接口 **SL_IID_OH_BUFFERQUEUE** 的 **bufferQueueItf** 实例 - - ```c++ - SLOHBufferQueueItf bufferQueueItf; - (*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_OH_BUFFERQUEUE, &bufferQueueItf); - ``` - -6. 打开音频文件,注册 **BufferQueueCallback** 回调 - - ```c++ - FILE *wavFile_ = nullptr; - - static void BufferQueueCallback (SLOHBufferQueueItf bufferQueueItf, void *pContext, SLuint32 size) - { - FILE *wavFile = (FILE *)pContext; - if (!feof(wavFile)) { - SLuint8 *buffer = nullptr; - SLuint32 pSize = 0; - (*bufferQueueItf)->GetBuffer(bufferQueueItf, &buffer, pSize); - //从文件读取数据 - fread(buffer, 1, size, wavFile); - (*bufferQueueItf)->Enqueue(bufferQueueItf, buffer, size); - } - return; - } - - // wavFile_ 需要设置为用户想要播放的文件描述符 - wavFile_ = fopen(path, "rb"); - (*bufferQueueItf)->RegisterCallback(bufferQueueItf, BufferQueueCallback, wavFile_); - ``` - -7. 获取接口 **SL_PLAYSTATE_PLAYING** 的 **playItf** 实例,开始播放 - - ```c++ - SLPlayItf playItf = nullptr; - (*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_PLAY, &playItf); - (*playItf)->SetPlayState(playItf, SL_PLAYSTATE_PLAYING); - ``` - -8. 结束音频播放 - - ```c++ - (*playItf)->SetPlayState(playItf, SL_PLAYSTATE_STOPPED); - (*pcmPlayerObject)->Destroy(pcmPlayerObject); - (*engineObject)->Destroy(engineObject); - ``` \ No newline at end of file diff --git a/zh-cn/application-dev/media/remote-camera.md b/zh-cn/application-dev/media/remote-camera.md deleted file mode 100644 index 91bfd4a6912e972b32b3bf6427b7654c9fca020e..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/media/remote-camera.md +++ /dev/null @@ -1,65 +0,0 @@ -# 分布式相机开发指导 - -## 场景介绍 - -分布式相机模块支持相机相关基础功能介绍说明。 - -## 开发步骤 -在计算器中连接分布式设备,在获取相机列表getSupportedCameras(),遍历返回的列表,判断对应Camera对象中的ConnectionType是否等于CAMERA_CONNECTION_REMOTE,若等于则使用此对象创建cameraInput,之后调用与本地相机使用一样。参考:[相机开发指导](./camera.md) - -详细API含义请参考:[相机管理API文档](../reference/apis/js-apis-camera.md) - -### 连接分布式相机 - -打开设置->WLAN,将需要连接分布式相机的两台设备连入同一局域网。 - -打开计算器,点击右上角小图标,出现新的窗口,按提示输入验证码,连接成功。 - -### 创建实例 - -```js -import camera from '@ohos.multimedia.camera' -import image from '@ohos.multimedia.image' -import media from '@ohos.multimedia.media' -import featureAbility from '@ohos.ability.featureAbility' - -// 创建CameraManager对象 -let cameraManager = camera.getCameraManager(globalThis.Context) -if (!cameraManager) { - console.error("camera.getCameraManager error") - return; -} - -// 注册回调函数监听相机状态变化,获取状态变化的相机信息 -cameraManager.on('cameraStatus', (cameraStatusInfo) => { - console.log('camera : ' + cameraStatusInfo.camera.cameraId); - console.log('status: ' + cameraStatusInfo.status); -}) - -// 获取相机列表 -let remoteCamera -let cameraArray = cameraManager.getSupportedCameras(); -if (cameraArray.length <= 0) { - console.error("cameraManager.getSupportedCameras error") - return; -} - -for(let cameraIndex = 0; cameraIndex < cameraArray.length; cameraIndex++) { - console.log('cameraId : ' + cameraArray[cameraIndex].cameraId) // 获取相机ID - console.log('cameraPosition : ' + cameraArray[cameraIndex].cameraPosition) // 获取相机位置 - console.log('cameraType : ' + cameraArray[cameraIndex].cameraType) // 获取相机类型 - console.log('connectionType : ' + cameraArray[cameraIndex].connectionType) // 获取相机连接类型 - if (cameraArray[cameraIndex].connectionType == CAMERA_CONNECTION_REMOTE) { - remoteCamera = cameraArray[cameraIndex] - } -} - -// 创建相机输入流 -let cameraInput -try { - cameraInput = cameraManager.createCameraInput(remoteCamera); -} catch () { - console.error('Failed to createCameraInput errorCode = ' + error.code); -} -``` -剩余步骤参照[相机开发指导](./camera.md) \ No newline at end of file diff --git a/zh-cn/application-dev/media/using-audiocapturer-for-recording.md b/zh-cn/application-dev/media/using-audiocapturer-for-recording.md new file mode 100644 index 0000000000000000000000000000000000000000..189093159072f5ad7b987ba493ed8d2e5cc1d990 --- /dev/null +++ b/zh-cn/application-dev/media/using-audiocapturer-for-recording.md @@ -0,0 +1,211 @@ +# 使用AudioCapturer开发音频录制功能 + +AudioCapturer是音频采集器,用于录制PCM(Pulse Code Modulation)音频数据,适合有音频开发经验的开发者实现更灵活的录制功能。 + +## 开发指导 + +使用AudioCapturer录制音频涉及到AudioCapturer实例的创建、音频采集参数的配置、采集的开始与停止、资源的释放等。本开发指导将以一次录制音频数据的过程为例,向开发者讲解如何使用AudioCapturer进行音频录制,建议搭配[AudioCapturer的API说明](../reference/apis/js-apis-audio.md#audiocapturer8)阅读。 + +下图展示了AudioCapturer的状态变化,在创建实例后,调用对应的方法可以进入指定的状态实现对应的行为。需要注意的是在确定的状态执行不合适的方法可能导致AudioCapturer发生错误,建议开发者在调用状态转换的方法前进行状态检查,避免程序运行产生预期以外的结果。 + +**图1** AudioCapturer状态变化示意图   +![AudioCapturer status change](figures/audiocapturer-status-change.png) + +使用on('stateChange')方法可以监听AudioCapturer的状态变化,每个状态对应值与说明见[AudioState](../reference/apis/js-apis-audio.md#audiostate8)。 + +### 开发步骤及注意事项 + +1. 配置音频采集参数并创建AudioCapturer实例,音频采集参数的详细信息可以查看[AudioCapturerOptions](../reference/apis/js-apis-audio.md#audiocaptureroptions8)。 + + ```ts + import audio from '@ohos.multimedia.audio'; + + let audioStreamInfo = { + samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100, + channels: audio.AudioChannel.CHANNEL_2, + sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, + encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW + }; + + let audioCapturerInfo = { + source: audio.SourceType.SOURCE_TYPE_MIC, + capturerFlags: 0 + }; + + let audioCapturerOptions = { + streamInfo: audioStreamInfo, + capturerInfo: audioCapturerInfo + }; + + audio.createAudioCapturer(audioCapturerOptions, (err, data) => { + if (err) { + console.error(`Invoke createAudioCapturer failed, code is ${err.code}, message is ${err.message}`); + } else { + console.info('Invoke createAudioCapturer succeeded.'); + let audioCapturer = data; + } + }); + ``` + +2. 调用start()方法进入running状态,开始录制音频。 + + ```ts + audioCapturer.start((err) => { + if (err) { + console.error(`Capturer start failed, code is ${err.code}, message is ${err.message}`); + } else { + console.info('Capturer start success.'); + } + }); + ``` + +3. 指定录制文件地址,调用read()方法读取缓冲区的数据。 + + ```ts + let file = fs.openSync(path, 0o2 | 0o100); + let bufferSize = await audioCapturer.getBufferSize(); + let buffer = await audioCapturer.read(bufferSize, true); + fs.writeSync(file.fd, buffer); + ``` + +4. 调用stop()方法停止录制。 + + ```ts + audioCapturer.stop((err) => { + if (err) { + console.error(`Capturer stop failed, code is ${err.code}, message is ${err.message}`); + } else { + console.info('Capturer stopped.'); + } + }); + ``` + +5. 调用release()方法销毁实例,释放资源。 + + ```ts + audioCapturer.release((err) => { + if (err) { + console.error(`capturer release failed, code is ${err.code}, message is ${err.message}`); + } else { + console.info('capturer released.'); + } + }); + ``` + + +### 完整示例 + +下面展示了使用AudioCapturer录制音频的完整示例代码。 + +```ts +import audio from '@ohos.multimedia.audio'; +import fs from '@ohos.file.fs'; + +const TAG = 'AudioCapturerDemo'; + +export default class AudioCapturerDemo { + private audioCapturer = undefined; + private audioStreamInfo = { + samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100, + channels: audio.AudioChannel.CHANNEL_1, + sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, + encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW + } + private audioCapturerInfo = { + source: audio.SourceType.SOURCE_TYPE_MIC, // 音源类型 + capturerFlags: 0 // 音频采集器标志 + } + private audioCapturerOptions = { + streamInfo: this.audioStreamInfo, + capturerInfo: this.audioCapturerInfo + } + + // 初始化,创建实例,设置监听事件 + init() { + audio.createAudioCapturer(this.audioCapturerOptions, (err, capturer) => { // 创建AudioCapturer实例 + if (err) { + console.error(`Invoke createAudioCapturer failed, code is ${err.code}, message is ${err.message}`); + return; + } + + console.info(`${TAG}: create AudioCapturer success`); + this.audioCapturer = capturer; + this.audioCapturer.on('markReach', 1000, (position) => { // 订阅markReach事件,当采集的帧数达到1000时触发回调 + if (position === 1000) { + console.info('ON Triggered successfully'); + } + }); + this.audioCapturer.on('periodReach', 2000, (position) => { // 订阅periodReach事件,当采集的帧数达到2000时触发回调 + if (position === 2000) { + console.info('ON Triggered successfully'); + } + }); + + }); + } + + // 开始一次音频采集 + async start() { + let stateGroup = [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED]; + if (stateGroup.indexOf(this.audioCapturer.state) === -1) { // 当且仅当状态为prepared、paused和stopped之一时才能启动采集 + console.error(`${TAG}: start failed`); + return; + } + await this.audioCapturer.start(); // 启动采集 + + let context = getContext(this); + const path = context.filesDir + '/test.wav'; // 采集到的音频文件存储路径 + + let file = fs.openSync(path, 0o2 | 0o100); // 如果文件不存在则创建文件 + let fd = file.fd; + let numBuffersToCapture = 150; // 循环写入150次 + let count = 0; + while (numBuffersToCapture) { + let bufferSize = await this.audioCapturer.getBufferSize(); + let buffer = await this.audioCapturer.read(bufferSize, true); + let options = { + offset: count * bufferSize, + length: bufferSize + }; + if (buffer === undefined) { + console.error(`${TAG}: read buffer failed`); + } else { + let number = fs.writeSync(fd, buffer, options); + console.info(`${TAG}: write date: ${number}`); + } + numBuffersToCapture--; + count++; + } + } + + // 停止采集 + async stop() { + // 只有采集器状态为STATE_RUNNING或STATE_PAUSED的时候才可以停止 + if (this.audioCapturer.state !== audio.AudioState.STATE_RUNNING && this.audioCapturer.state !== audio.AudioState.STATE_PAUSED) { + console.info('Capturer is not running or paused'); + return; + } + await this.audioCapturer.stop(); // 停止采集 + if (this.audioCapturer.state === audio.AudioState.STATE_STOPPED) { + console.info('Capturer stopped'); + } else { + console.error('Capturer stop failed'); + } + } + + // 销毁实例,释放资源 + async release() { + // 采集器状态不是STATE_RELEASED或STATE_NEW状态,才能release + if (this.audioCapturer.state === audio.AudioState.STATE_RELEASED || this.audioCapturer.state === audio.AudioState.STATE_NEW) { + console.info('Capturer already released'); + return; + } + await this.audioCapturer.release(); // 释放资源 + if (this.audioCapturer.state == audio.AudioState.STATE_RELEASED) { + console.info('Capturer released'); + } else { + console.error('Capturer release failed'); + } + } +} +``` diff --git a/zh-cn/application-dev/media/using-audiorenderer-for-playback.md b/zh-cn/application-dev/media/using-audiorenderer-for-playback.md new file mode 100644 index 0000000000000000000000000000000000000000..4ec5d646b73ab3653ba80509faa92943f0f06955 --- /dev/null +++ b/zh-cn/application-dev/media/using-audiorenderer-for-playback.md @@ -0,0 +1,268 @@ +# 使用AudioRenderer开发音频播放功能 + +AudioRenderer是音频渲染器,用于播放PCM(Pulse Code Modulation)音频数据,相比AVPlayer而言,可以在输入前添加数据预处理,更适合有音频开发经验的开发者,以实现更灵活的播放功能。 + +## 开发指导 + +使用AudioRenderer播放音频涉及到AudioRenderer实例的创建、音频渲染参数的配置、渲染的开始与停止、资源的释放等。本开发指导将以一次渲染音频数据的过程为例,向开发者讲解如何使用AudioRenderer进行音频渲染,建议搭配[AudioRenderer的API说明](../reference/apis/js-apis-audio.md#audiorenderer8)阅读。 + +下图展示了AudioRenderer的状态变化,在创建实例后,调用对应的方法可以进入指定的状态实现对应的行为。需要注意的是在确定的状态执行不合适的方法可能导致AudioRenderer发生错误,建议开发者在调用状态转换的方法前进行状态检查,避免程序运行产生预期以外的结果。 + +为保证UI线程不被阻塞,大部分AudioRenderer调用都是异步的。对于每个API均提供了callback函数和Promise函数,以下示例均采用callback函数。 + +**图1** AudioRenderer状态变化示意图   + +![AudioRenderer status change](figures/audiorenderer-status-change.png) + +在进行应用开发的过程中,建议开发者通过on('stateChange')方法订阅AudioRenderer的状态变更。因为针对AudioRenderer的某些操作,仅在音频播放器在固定状态时才能执行。如果应用在音频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。 + +- prepared状态: 通过调用createAudioRenderer()方法进入到该状态。 + +- running状态: 正在进行音频数据播放,可以在prepared状态通过调用start()方法进入此状态,也可以在pause状态和stopped状态通过调用start()方法进入此状态。 + +- paused状态: 在running状态可以通过调用pause()方法暂停音频数据的播放并进入paused状态,暂停播放之后可以通过调用start()方法继续音频数据播放。 + +- stopped状态: 在paused/running状态可以通过stop()方法停止音频数据的播放。 + +- released状态: 在prepared、paused、stopped等状态,用户均可通过release()方法释放掉所有占用的硬件和软件资源,并且不会再进入到其他的任何一种状态了。 + +### 开发步骤及注意事项 + +1. 配置音频渲染参数并创建AudioRenderer实例,音频渲染参数的详细信息可以查看[AudioRendererOptions](../reference/apis/js-apis-audio.md#audiorendereroptions8)。 + + ```ts + import audio from '@ohos.multimedia.audio'; + + let audioStreamInfo = { + samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100, + channels: audio.AudioChannel.CHANNEL_1, + sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, + encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW + }; + + let audioRendererInfo = { + content: audio.ContentType.CONTENT_TYPE_SPEECH, + usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION, + rendererFlags: 0 + }; + + let audioRendererOptions = { + streamInfo: audioStreamInfo, + rendererInfo: audioRendererInfo + }; + + audio.createAudioRenderer(audioRendererOptions, (err, data) => { + if (err) { + console.error(`Invoke createAudioRenderer failed, code is ${err.code}, message is ${err.message}`); + return; + } else { + console.info('Invoke createAudioRenderer succeeded.'); + let audioRenderer = data; + } + }); + ``` + +2. 调用start()方法进入running状态,开始渲染音频。 + + ```ts + audioRenderer.start((err) => { + if (err) { + console.error(`Renderer start failed, code is ${err.code}, message is ${err.message}`); + } else { + console.info('Renderer start success.'); + } + }); + ``` + +3. 指定待渲染文件地址,打开文件调用write()方法向缓冲区持续写入音频数据进行渲染播放。如果需要对音频数据进行处理以实现个性化的播放,在写入之前操作即可。 + + ```ts + const bufferSize = await audioRenderer.getBufferSize(); + let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY); + let buf = new ArrayBuffer(bufferSize); + let readsize = await fs.read(file.fd, buf); + let writeSize = await new Promise((resolve, reject) => { + audioRenderer.write(buf, (err, writeSize) => { + if (err) { + reject(err); + } else { + resolve(writeSize); + } + }); + }); + ``` + +4. 调用stop()方法停止渲染。 + + ```ts + audioRenderer.stop((err) => { + if (err) { + console.error(`Renderer stop failed, code is ${err.code}, message is ${err.message}`); + } else { + console.info('Renderer stopped.'); + } + }); + ``` + +5. 调用release()方法销毁实例,释放资源。 + + ```ts + audioRenderer.release((err) => { + if (err) { + console.error(`Renderer release failed, code is ${err.code}, message is ${err.message}`); + } else { + console.info('Renderer released.'); + } + }); + ``` + +### 完整示例 + +下面展示了使用AudioRenderer渲染音频文件的示例代码。 + +```ts +import audio from '@ohos.multimedia.audio'; +import fs from '@ohos.file.fs'; + +const TAG = 'AudioRendererDemo'; + +export default class AudioRendererDemo { + private renderModel = undefined; + private audioStreamInfo = { + samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, // 采样率 + channels: audio.AudioChannel.CHANNEL_2, // 通道数 + sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式 + encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式 + } + private audioRendererInfo = { + content: audio.ContentType.CONTENT_TYPE_MUSIC, // 媒体类型 + usage: audio.StreamUsage.STREAM_USAGE_MEDIA, // 音频流使用类型 + rendererFlags: 0 // 音频渲染器标志 + } + private audioRendererOptions = { + streamInfo: this.audioStreamInfo, + rendererInfo: this.audioRendererInfo + } + + // 初始化,创建实例,设置监听事件 + init() { + audio.createAudioRenderer(this.audioRendererOptions, (err, renderer) => { // 创建AudioRenderer实例 + if (!err) { + console.info(`${TAG}: creating AudioRenderer success`); + this.renderModel = renderer; + this.renderModel.on('stateChange', (state) => { // 设置监听事件,当转换到指定的状态时触发回调 + if (state == 1) { + console.info('audio renderer state is: STATE_PREPARED'); + } + if (state == 2) { + console.info('audio renderer state is: STATE_RUNNING'); + } + }); + this.renderModel.on('markReach', 1000, (position) => { // 订阅markReach事件,当渲染的帧数达到1000帧时触发回调 + if (position == 1000) { + console.info('ON Triggered successfully'); + } + }); + } else { + console.info(`${TAG}: creating AudioRenderer failed, error: ${err.message}`); + } + }); + } + + // 开始一次音频渲染 + async start() { + let stateGroup = [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED]; + if (stateGroup.indexOf(this.renderModel.state) === -1) { // 当且仅当状态为prepared、paused和stopped之一时才能启动渲染 + console.error(TAG + 'start failed'); + return; + } + await this.renderModel.start(); // 启动渲染 + + const bufferSize = await this.renderModel.getBufferSize(); + let context = getContext(this); + let path = context.filesDir; + const filePath = path + '/test.wav'; // 使用沙箱路径获取文件,实际路径为/data/storage/el2/base/haps/entry/files/test.wav + + let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY); + let stat = await fs.stat(filePath); + let buf = new ArrayBuffer(bufferSize); + let len = stat.size % bufferSize === 0 ? Math.floor(stat.size / bufferSize) : Math.floor(stat.size / bufferSize + 1); + for (let i = 0; i < len; i++) { + let options = { + offset: i * bufferSize, + length: bufferSize + }; + let readsize = await fs.read(file.fd, buf, options); + + // buf是要写入缓冲区的音频数据,在调用AudioRenderer.write()方法前可以进行音频数据的预处理,实现个性化的音频播放功能,AudioRenderer会读出写入缓冲区的音频数据进行渲染 + + let writeSize = await new Promise((resolve, reject) => { + this.renderModel.write(buf, (err, writeSize) => { + if (err) { + reject(err); + } else { + resolve(writeSize); + } + }); + }); + if (this.renderModel.state === audio.AudioState.STATE_RELEASED) { // 如果渲染器状态为released,停止渲染 + fs.close(file); + await this.renderModel.stop(); + } + if (this.renderModel.state === audio.AudioState.STATE_RUNNING) { + if (i === len - 1) { // 如果音频文件已经被读取完,停止渲染 + fs.close(file); + await this.renderModel.stop(); + } + } + } + } + + // 暂停渲染 + async pause() { + // 只有渲染器状态为running的时候才能暂停 + if (this.renderModel.state !== audio.AudioState.STATE_RUNNING) { + console.info('Renderer is not running'); + return; + } + await this.renderModel.pause(); // 暂停渲染 + if (this.renderModel.state === audio.AudioState.STATE_PAUSED) { + console.info('Renderer is paused.'); + } else { + console.error('Pausing renderer failed.'); + } + } + + // 停止渲染 + async stop() { + // 只有渲染器状态为running或paused的时候才可以停止 + if (this.renderModel.state !== audio.AudioState.STATE_RUNNING && this.renderModel.state !== audio.AudioState.STATE_PAUSED) { + console.info('Renderer is not running or paused.'); + return; + } + await this.renderModel.stop(); // 停止渲染 + if (this.renderModel.state === audio.AudioState.STATE_STOPPED) { + console.info('Renderer stopped.'); + } else { + console.error('Stopping renderer failed.'); + } + } + + // 销毁实例,释放资源 + async release() { + // 渲染器状态不是released状态,才能release + if (this.renderModel.state === audio.AudioState.STATE_RELEASED) { + console.info('Renderer already released'); + return; + } + await this.renderModel.release(); // 释放资源 + if (this.renderModel.state === audio.AudioState.STATE_RELEASED) { + console.info('Renderer released'); + } else { + console.error('Renderer release failed.'); + } + } +} +``` + +当同优先级或高优先级音频流要使用输出设备时,当前音频流会被中断,应用可以自行响应中断事件并做出处理。具体的音频并发处理方式可参考[多音频播放的并发策略](audio-playback-concurrency.md)。 diff --git a/zh-cn/application-dev/media/using-avplayer-for-playback.md b/zh-cn/application-dev/media/using-avplayer-for-playback.md new file mode 100644 index 0000000000000000000000000000000000000000..99dc462d4eb110b162b89b9d9e646c4250f8c6b0 --- /dev/null +++ b/zh-cn/application-dev/media/using-avplayer-for-playback.md @@ -0,0 +1,165 @@ +# 使用AVPlayer开发音频播放功能 + +使用AVPlayer可以实现端到端播放原始媒体资源,本开发指导将以完整地播放一首音乐作为示例,向开发者讲解AVPlayer音频播放相关功能。 + +以下指导仅介绍如何实现媒体资源播放,如果要实现后台播放或熄屏播放,需要使用[AVSession(媒体会话)](avsession-overview.md)和[申请长时任务](../task-management/continuous-task-dev-guide.md),避免播放被系统强制中断。 + + +播放的全流程包含:创建AVPlayer,设置播放资源,设置播放参数(音量/倍速/焦点模式),播放控制(播放/暂停/跳转/停止),重置,销毁资源。 + + +在进行应用开发的过程中,开发者可以通过AVPlayer的state属性主动获取当前状态或使用on('stateChange')方法监听状态变化。如果应用在音频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。 + + +**图1** 播放状态变化示意图   +![Playback status change](figures/playback-status-change.png) + +状态的详细说明请参考[AVPlayerState](../reference/apis/js-apis-media.md#avplayerstate9)。当播放处于prepared / playing / paused / completed状态时,播放引擎处于工作状态,这需要占用系统较多的运行内存。当客户端暂时不使用播放器时,调用reset()或release()回收内存资源,做好资源利用。 + +## 开发步骤及注意事项 + +详细的API说明请参考[AVPlayer API参考](../reference/apis/js-apis-media.md#avplayer9)。 + +1. 创建实例createAVPlayer(),AVPlayer初始化idle状态。 + +2. 设置业务需要的监听事件,搭配全流程场景使用。支持的监听事件包括: + | 事件类型 | 说明 | + | -------- | -------- | + | stateChange | 必要事件,监听播放器的state属性改变。 | + | error | 必要事件,监听播放器的错误信息。 | + | durationUpdate | 用于进度条,监听进度条长度,刷新资源时长。 | + | timeUpdate | 用于进度条,监听进度条当前位置,刷新当前时间。 | + | seekDone | 响应API调用,监听seek()请求完成情况。
当使用seek()跳转到指定播放位置后,如果seek操作成功,将上报该事件。 | + | speedDone | 响应API调用,监听setSpeed()请求完成情况。
当使用setSpeed()设置播放倍速后,如果setSpeed操作成功,将上报该事件。 | + | volumeChange | 响应API调用,监听setVolume()请求完成情况。
当使用setVolume()调节播放音量后,如果setVolume操作成功,将上报该事件。 | + | bufferingUpdate | 用于网络播放,监听网络播放缓冲信息,用于上报缓冲百分比以及缓存播放进度。 | + | audioInterrupt | 监听音频焦点切换信息,搭配属性audioInterruptMode使用。
如果当前设备存在多个音频正在播放,音频焦点被切换(即播放其他媒体如通话等)时将上报该事件,应用可以及时处理。 | + +3. 设置资源:设置属性url,AVPlayer进入initialized状态。 + > **说明:** + > + > 下面代码示例中的url仅作示意使用,开发者需根据实际情况,确认资源有效性并设置: + > + > - 如果使用本地资源播放,必须确认资源文件可用,并使用应用沙箱路径访问对应资源,参考[获取应用文件路径](../application-models/application-context-stage.md#获取应用开发路径)。应用沙箱的介绍及如何向应用沙箱推送文件,请参考[文件管理](../file-management/app-sandbox-directory.md)。 + > + > - 如果使用网络播放路径,需[申请相关权限](../security/accesstoken-guidelines.md):ohos.permission.INTERNET。 + > + > - 如果使用ResourceManager.getRawFd打开HAP资源文件描述符,使用方法可参考[ResourceManager API参考](../reference/apis/js-apis-resource-manager.md#getrawfd9)。 + > + > - 需要使用[支持的播放格式与协议](avplayer-avrecorder-overview.md#支持的格式与协议)。 + +4. 准备播放:调用prepare(),AVPlayer进入prepared状态,此时可以获取duration,设置音量。 + +5. 音频播控:播放play(),暂停pause(),跳转seek(),停止stop() 等操作。 + +6. (可选)更换资源:调用reset()重置资源,AVPlayer重新进入idle状态,允许更换资源url。 + +7. 退出播放:调用release()销毁实例,AVPlayer进入released状态,退出播放。 + +## 完整示例 + +参考以下示例,完整地播放一首音乐。 + +```ts +import media from '@ohos.multimedia.media'; +import fs from '@ohos.file.fs'; +import common from '@ohos.app.ability.common'; + +export class AVPlayerDemo { + private avPlayer; + private count: number = 0; + + // 注册avplayer回调函数 + setAVPlayerCallback() { + // seek操作结果回调函数 + this.avPlayer.on('seekDone', (seekDoneTime) => { + console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`); + }) + // error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程 + this.avPlayer.on('error', (err) => { + console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`); + this.avPlayer.reset(); // 调用reset重置资源,触发idle状态 + }) + // 状态机变化回调函数 + this.avPlayer.on('stateChange', async (state, reason) => { + switch (state) { + case 'idle': // 成功调用reset接口后触发该状态机上报 + console.info('AVPlayer state idle called.'); + this.avPlayer.release(); // 调用release接口销毁实例对象 + break; + case 'initialized': // avplayer 设置播放源后触发该状态上报 + console.info('AVPlayerstate initialized called.'); + this.avPlayer.prepare().then(() => { + console.info('AVPlayer prepare succeeded.'); + }, (err) => { + console.error(`Invoke prepare failed, code is ${err.code}, message is ${err.message}`); + }); + break; + case 'prepared': // prepare调用成功后上报该状态机 + console.info('AVPlayer state prepared called.'); + this.avPlayer.play(); // 调用播放接口开始播放 + break; + case 'playing': // play成功调用后触发该状态机上报 + console.info('AVPlayer state playing called.'); + if (this.count !== 0) { + console.info('AVPlayer start to seek.'); + this.avPlayer.seek(this.avPlayer.duration); //seek到音频末尾 + } else { + this.avPlayer.pause(); // 调用暂停接口暂停播放 + } + this.count++; + break; + case 'paused': // pause成功调用后触发该状态机上报 + console.info('AVPlayer state paused called.'); + this.avPlayer.play(); // 再次播放接口开始播放 + break; + case 'completed': // 播放结束后触发该状态机上报 + console.info('AVPlayer state completed called.'); + this.avPlayer.stop(); //调用播放结束接口 + break; + case 'stopped': // stop接口成功调用后触发该状态机上报 + console.info('AVPlayer state stopped called.'); + this.avPlayer.reset(); // 调用reset接口初始化avplayer状态 + break; + case 'released': + console.info('AVPlayer state released called.'); + break; + default: + console.info('AVPlayer state unknown called.'); + break; + } + }) + } + + // 以下demo为使用fs文件系统打开沙箱地址获取媒体文件地址并通过url属性进行播放示例 + async avPlayerUrlDemo() { + // 创建avPlayer实例对象 + this.avPlayer = await media.createAVPlayer(); + // 创建状态机变化回调函数 + this.setAVPlayerCallback(); + let fdPath = 'fd://'; + // 通过UIAbilityContext获取沙箱地址filesDir,以Stage模型为例 + let context = getContext(this) as common.UIAbilityContext; + let pathDir = context.filesDir; + let path = pathDir + '/01.mp3'; + // 打开相应的资源文件地址获取fd,并为url赋值触发initialized状态机上报 + let file = await fs.open(path); + fdPath = fdPath + '' + file.fd; + this.avPlayer.url = fdPath; + } + + // 以下demo为使用资源管理接口获取打包在HAP内的媒体资源文件并通过fdSrc属性进行播放示例 + async avPlayerFdSrcDemo() { + // 创建avPlayer实例对象 + this.avPlayer = await media.createAVPlayer(); + // 创建状态机变化回调函数 + this.setAVPlayerCallback(); + // 通过UIAbilityContext的resourceManager成员的getRawFd接口获取媒体资源播放地址 + // 返回类型为{fd,offset,length},fd为HAP包fd地址,offset为媒体资源偏移量,length为播放长度 + let context = getContext(this) as common.UIAbilityContext; + let fileDescriptor = await context.resourceManager.getRawFd('01.mp3'); + // 为fdSrc赋值触发initialized状态机上报 + this.avPlayer.fdSrc = fileDescriptor; + } +} +``` diff --git a/zh-cn/application-dev/media/using-avrecorder-for-recording.md b/zh-cn/application-dev/media/using-avrecorder-for-recording.md new file mode 100644 index 0000000000000000000000000000000000000000..f3c62923a7fa5ed418c1c18a8d316e893d9fa25d --- /dev/null +++ b/zh-cn/application-dev/media/using-avrecorder-for-recording.md @@ -0,0 +1,179 @@ +# 使用AVRecorder开发音频录制功能 + +使用AVRecorder可以实现音频录制功能,本开发指导将以“开始录制-暂停录制-恢复录制-停止录制”的一次流程为示例,向开发者讲解AVRecorder音频录制相关功能。 + +在进行应用开发的过程中,开发者可以通过AVRecorder的state属性,主动获取当前状态或使用on('stateChange')方法监听状态变化。开发过程中应该严格遵循状态机要求,例如只能在started状态下调用pause()接口,只能在paused状态下调用resume()接口。 + +**图1** 录制状态变化示意图   + +![Recording status change](figures/recording-status-change.png) + +状态的详细说明请参考[AVRecorderState](../reference/apis/js-apis-media.md#avrecorderstate9)。 + + +## 开发步骤及注意事项 + +详细的API说明请参考[AVRecorder API参考](../reference/apis/js-apis-media.md#avrecorder9)。 + +1. 创建AVRecorder实例,实例创建完成进入idle状态。 + + ```ts + import media from '@ohos.multimedia.media'; + + let avRecorder = undefined; + media.createAVRecorder().then((recorder) => { + avRecorder = recorder; + }, (err) => { + console.error(`Invoke createAVRecorder failed, code is ${err.code}, message is ${err.message}`); + }) + ``` + +2. 设置业务需要的监听事件,监听状态变化及错误上报。 + | 事件类型 | 说明 | + | -------- | -------- | + | stateChange | 必要事件,监听播放器的state属性改变 | + | error | 必要事件,监听播放器的错误信息 | + + + ```ts + // 状态上报回调函数 + avRecorder.on('stateChange', (state, reason) => { + console.log(`current state is ${state}`); + // 用户可以在此补充状态发生切换后想要进行的动作 + }) + + // 错误上报回调函数 + avRecorder.on('error', (err) => { + console.error(`avRecorder failed, code is ${err.code}, message is ${err.message}`); + }) + ``` + +3. 配置音频录制参数,调用prepare()接口,此时进入prepared状态。 + > **说明:** + > 配置参数需要注意: + > + > - prepare接口的入参avConfig中仅设置音频相关的配置参数,如示例代码所示。 + > 如果只需要录制音频,请不要设置视频相关配置参数;如果需要录制视频,可以参考[视频录制开发指导](video-recording.md)进行开发。直接设置视频相关参数会导致后续步骤报错。 + > + > - 需要使用支持的[录制规格](avplayer-avrecorder-overview.md#支持的格式)。 + > + > - 录制输出的url地址(即示例里avConfig中的url),形式为fd://xx (fd number)。需要基础文件操作接口([ohos.file.fs](../reference/apis/js-apis-file-fs.md))实现应用文件访问能力,获取方式参考[应用文件访问与管理](../file-management/app-file-access.md)。 + + + ```ts + let avProfile = { + audioBitrate: 100000, // 音频比特率 + audioChannels: 2, // 音频声道数 + audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式,当前只支持aac + audioSampleRate: 48000, // 音频采样率 + fileFormat: media.ContainerFormatType.CFT_MPEG_4A, // 封装格式,当前只支持m4a + } + let avConfig = { + audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, // 音频输入源,这里设置为麦克风 + profile: avProfile, + url: 'fd://35', // 参考应用文件访问与管理中的开发示例获取创建的音频文件fd填入此处 + } + avRecorder.prepare(avConfig).then(() => { + console.log('Invoke prepare succeeded.'); + }, (err) => { + console.error(`Invoke prepare failed, code is ${err.code}, message is ${err.message}`); + }) + ``` + +4. 开始录制,调用start()接口,此时进入started状态。 + +5. 暂停录制,调用pause()接口,此时进入paused状态。 + +6. 恢复录制,调用resume()接口,此时再次进入started状态。 + +7. 停止录制,调用stop()接口,此时进入stopped状态。 + +8. 重置资源,调用reset()重新进入idle状态,允许重新配置录制参数。 + +9. 销毁实例,调用release()进入released状态,退出录制。 + + +## 完整示例 + + 参考以下示例,完成“开始录制-暂停录制-恢复录制-停止录制”的完整流程。 + +```ts +import media from '@ohos.multimedia.media'; + +export class AudioRecorderDemo { + private avRecorder; + private avProfile = { + audioBitrate: 100000, // 音频比特率 + audioChannels: 2, // 音频声道数 + audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式,当前只支持aac + audioSampleRate: 48000, // 音频采样率 + fileFormat: media.ContainerFormatType.CFT_MPEG_4A, // 封装格式,当前只支持m4a + }; + private avConfig = { + audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, // 音频输入源,这里设置为麦克风 + profile: this.avProfile, + url: 'fd://35', // 参考应用文件访问与管理开发示例新建并读写一个文件 + }; + + // 注册audioRecorder回调函数 + setAudioRecorderCallback() { + // 状态机变化回调函数 + this.avRecorder.on('stateChange', (state, reason) => { + console.log(`AudioRecorder current state is ${state}`); + }) + // 错误上报回调函数 + this.avRecorder.on('error', (err) => { + console.error(`AudioRecorder failed, code is ${err.code}, message is ${err.message}`); + }) + } + + // 开始录制对应的流程 + async startRecordingProcess() { + // 1.创建录制实例 + this.avRecorder = await media.createAVRecorder(); + this.setAudioRecorderCallback(); + // 2.获取录制文件fd赋予avConfig里的url;参考FilePicker文档 + // 3.配置录制参数完成准备工作 + await this.avRecorder.prepare(this.avConfig); + // 4.开始录制 + await this.avRecorder.start(); + } + + // 暂停录制对应的流程 + async pauseRecordingProcess() { + if (this.avRecorder.state === 'started') { // 仅在started状态下调用pause为合理状态切换 + await this.avRecorder.pause(); + } + } + + // 恢复录制对应的流程 + async resumeRecordingProcess() { + if (this.avRecorder.state === 'paused') { // 仅在paused状态下调用resume为合理状态切换 + await this.avRecorder.resume(); + } + } + + // 停止录制对应的流程 + async stopRecordingProcess() { + // 1. 停止录制 + if (this.avRecorder.state === 'started' + || this.avRecorder.state === 'paused') { // 仅在started或者paused状态下调用stop为合理状态切换 + await this.avRecorder.stop(); + } + // 2.重置 + await this.avRecorder.reset(); + // 3.释放录制实例 + await this.avRecorder.release(); + // 4.关闭录制文件fd + } + + // 一个完整的【开始录制-暂停录制-恢复录制-停止录制】示例 + async audioRecorderDemo() { + await this.startRecordingProcess(); // 开始录制 + // 用户此处可以自行设置录制时长,例如通过设置休眠阻止代码执行 + await this.pauseRecordingProcess(); //暂停录制 + await this.resumeRecordingProcess(); // 恢复录制 + await this.stopRecordingProcess(); // 停止录制 + } +} +``` diff --git a/zh-cn/application-dev/media/using-avsession-controller.md b/zh-cn/application-dev/media/using-avsession-controller.md new file mode 100644 index 0000000000000000000000000000000000000000..26ae18f57f822a297e6f3b2ef43c2957b74f67c6 --- /dev/null +++ b/zh-cn/application-dev/media/using-avsession-controller.md @@ -0,0 +1,244 @@ +# 媒体会话控制方 + +OpenHarmony系统预置的播控中心,作为媒体会话控制方与音视频应用进行交互,包括获取媒体信息进行展示以及下发播控命令等。 + +系统应用开发者也可以根据需要,按照本章节的内容自行开发一款新的系统应用(例如新开发一款播控中心或语音助手),作为媒体会话控制方的角色,与系统中的音视频应用进行交互。 + +## 基本概念 + +- 媒体会话描述符(AVSessionDescriptor):描述媒体会话的相关信息,包含标识媒体会话的ID(sessionId),媒体会话的类型type(音频Audio/视频Video),媒体会话自定义名称(sessionTag),媒体会话所属应用的信息(elementName)、是否为置顶会话(isTopSession)等。 + +- 置顶会话(TopSession):系统中优先级最高的媒体会话,例如当前处于正在播放状态的会话。一般来说,如果想与媒体会话通信,需要获取会话对应的控制器,而媒体会话控制方可以在不用获取对应控制器的情况下,直接与置顶会话通信,例如直接向置顶会话发送播控命令和按键事件。 + +## 接口说明 + +媒体会话控制方使用的关键接口如下表所示。接口返回值有两种返回形式:callback和promise,下表中为callback形式接口,promise和callback只是返回值方式不一样,功能相同。 + +更多API说明请参见[API文档](../reference/apis/js-apis-avsession.md)。 + +| 接口名 | 说明 | +| -------- | -------- | +| getAllSessionDescriptors(callback: AsyncCallback<Array<Readonly<AVSessionDescriptor>>>): void | 获取系统中所有媒体会话的描述符。 | +| createController(sessionId: string, callback: AsyncCallback<AVSessionController>): void | 创建媒体会话控制器。 | +| getValidCommands(callback: AsyncCallback<Array<AVControlCommandType>>): void | 获取媒体会话支持的有效命令。
音视频应用在接入媒体会话时监听的播控命令,即为媒体会话支持的有效命令,相关信息请参见[媒体会话提供方监听播控命令事件](using-avsession-developer.md)。 | +| getLaunchAbility(callback: AsyncCallback<WantAgent>): void | 获取媒体会话中配置的可被拉起的UIAbility。
当用户在媒体会话控制方应用进行界面操作,例如点击了播控中心卡片后,可以拉起对应的应用。 | +| sendAVKeyEvent(event: KeyEvent, callback: AsyncCallback<void>): void | 通过会话对应的AVSessionController向会话发送按键命令。 | +| sendSystemAVKeyEvent(event: KeyEvent, callback: AsyncCallback<void>): void | 向置顶会话发送按键命令。 | +| sendControlCommand(command: AVControlCommand, callback: AsyncCallback<void>): void | 通过会话对应的AVSessionController向会话发送播控命令。 | +| sendSystemControlCommand(command: AVControlCommand, callback: AsyncCallback<void>): void | 向置顶会话发送播控命令。 | + +## 开发步骤 + +系统应用作为媒体会话控制方接入媒体会话的基本步骤如下所示: + +1. 通过AVSessionManager获取媒体会话描述符AVSessionDescriptor,创建媒体会话控制器AVSessionController。 + 媒体会话控制方可以获取当前系统中所有的AVSessionDescriptor,并创建每个会话对应的AVSessionController,从而对系统中的音视频应用进行统一的播放控制。 + + ```ts + //导入AVSession模块 + import AVSessionManager from '@ohos.multimedia.avsession'; + + // 全局变量定义 + let g_controller = new Array(); + let g_centerSupportCmd:Set = new Set(['play', 'pause', 'playNext', 'playPrevious', 'fastForward', 'rewind', 'seek','setSpeed', 'setLoopMode', 'toggleFavorite']); + let g_validCmd:Set; + // 获取会话描述符,创建控制器 + AVSessionManager.getAllSessionDescriptors().then((descriptors) => { + descriptors.forEach((descriptor) => { + AVSessionManager.createController(descriptor.sessionId).then((controller) => { + g_controller.push(controller); + }).catch((err) => { + console.error(`createController : ERROR : ${err.message}`); + }); + }); + }).catch((err) => { + console.error(`getAllSessionDescriptors : ERROR : ${err.message}`); + }); + + ``` + +2. 监听AVSession会话状态及AVSession服务状态事件。 + + AVSession会话状态事件包括: + + - sessionCreate:媒体会话创建事件。 + - sessionDestroy:媒体会话销毁事件。 + - topSessionChange:置顶会话发生变化事件。 + + AVSession服务状态事件指sessionServiceDie,在AVSession服务异常时产生该事件。 + + ```ts + // 注册会话创建监听,创建控制器 + AVSessionManager.on('sessionCreate', (session) => { + // 新增会话,需要创建控制器 + AVSessionManager.createController(session.sessionId).then((controller) => { + g_controller.push(controller); + }).catch((err) => { + console.info(`createController : ERROR : ${err.message}`); + }); + }); + + // 注册系统会话销毁监听 + AVSessionManager.on('sessionDestroy', (session) => { + let index = g_controller.findIndex((controller) => { + return controller.sessionId === session.sessionId; + }); + if (index !== 0) { + g_controller[index].destroy(); + g_controller.splice(index, 1); + } + }); + // 注册系统最高优先级会话变更监听 + AVSessionManager.on('topSessionChange', (session) => { + let index = g_controller.findIndex((controller) => { + return controller.sessionId === session.sessionId; + }); + // 将该会话显示排到第一个 + if (index !== 0) { + g_controller.sort((a, b) => { + return a.sessionId === session.sessionId ? -1 : 0; + }); + } + }); + // 注册服务异常监听 + AVSessionManager.on('sessionServiceDie', () => { + // 服务端异常,应用清理资源 + console.info("服务端异常"); + }) + ``` + +3. 监听媒体信息变化及会话其他事件。 + + AVSession媒体信息变化事件主要包括: + + - metadataChange:媒体会话元数据变化事件。 + - playbackStateChange:媒体播放状态变化事件。 + - activeStateChange:媒体会话激活状态变化事件。 + - validCommandChange:媒体会话支持的有效命令变化事件。 + - outputDeviceChange:播放设备变化事件。 + - sessionDestroy:媒体会话销毁事件。 + + 媒体会话控制方可以根据实际需要监听对应的事件。 + + ```ts + // 注册会话激活状态变更监听 + controller.on('activeStateChange', (isActive) => { + if (isActive) { + console.info("控制器卡片按键高亮"); + } else { + console.info("控制器卡片按键变更为无效"); + } + }); + // 注册会话销毁监听 + controller.on('sessionDestroy', () => { + console.info('on sessionDestroy : SUCCESS '); + controller.destroy().then(() => { + console.info('destroy : SUCCESS '); + }).catch((err) => { + console.info(`destroy : ERROR :${err.message}`); + }); + }); + + // 注册元数据更新监听 + let metaFilter = ['assetId', 'title', 'description']; + controller.on('metadataChange', metaFilter, (metadata) => { + console.info(`on metadataChange assetId : ${metadata.assetId}`); + }); + // 注册播放状态更新监听 + let playbackFilter = ['state', 'speed', 'loopMode']; + controller.on('playbackStateChange', playbackFilter, (playbackState) => { + console.info(`on playbackStateChange state : ${playbackState.state}`); + }); + // 注册会话支持的命令变更监听 + controller.on('validCommandChange', (cmds) => { + console.info(`validCommandChange : SUCCESS : size : ${cmds.size}`); + console.info(`validCommandChange : SUCCESS : cmds : ${cmds.values()}`); + g_validCmd.clear(); + for (let c of g_centerSupportCmd) { + if (cmds.has(c)) { + g_validCmd.add(c); + } + } + }); + // 注册输出设备变更监听 + controller.on('outputDeviceChange', (device) => { + console.info(`on outputDeviceChange device isRemote : ${device.isRemote}`); + }); + ``` + +4. 获取媒体会话提供方传递的媒体信息,可以用于界面展示,例如在播控中心展示当前播放的曲目及对应的播放状态。 + + ```ts + async getInfoFromSessionByController() { + // 假设我们已经有了一个对应session的controller,如何创建controller可以参考之前的案例 + let controller: AVSessionManager.AVSessionController = ALLREADY_HAVE_A_CONTROLLER; + // 获取sessionId + let sessionId: string = controller.sessionId; + console.info(`get sessionId by controller : isActive : ${sessionId}`); + // 获取session激活状态 + let isActive: boolean = await controller.isActive(); + console.info(`get activeState by controller : ${isActive}`); + // 获取session的媒体信息 + let metadata: AVSessionManager.AVMetadata = await controller.getAVMetadata(); + console.info(`get media title by controller : ${metadata.title}`); + console.info(`get media artist by controller : ${metadata.artist}`); + // 获取session的播放信息 + let avPlaybackState: AVSessionManager.AVPlaybackState = await controller.getAVPlaybackState(); + console.info(`get playbackState by controller : ${avPlaybackState.state}`); + console.info(`get favoriteState by controller : ${avPlaybackState.isFavorite}`); + } + ``` + +5. 控制媒体会话行为,例如发送用户在播控中心对当前曲目的操作(播放/暂停/上一首/下一首等)命令。 + + 作为媒体会话提供方的音视频应用在监听到相关的播控命令事件后,在相应的逻辑中可以完成对应的操作动作。 + + + ```ts + async sendCommandToSessionByController() { + // 假设我们已经有了一个对应session的controller,如何创建controller可以参考之前的案例 + let controller: AVSessionManager.AVSessionController = ALLREADY_HAVE_A_CONTROLLER; + // 获取这个session支持的命令种类 + let validCommandTypeArray: Array = await controller.getValidCommands(); + console.info(`get validCommandArray by controller : length : ${validCommandTypeArray.length}`); + // 下发播放命令 + // 如果可用命令包含播放,则下发播放命令,当然正常session都应该提供并实现播放功能吧 + if (validCommandTypeArray.indexOf('play') >= 0) { + let avCommand: AVSessionManager.AVControlCommand = {command:'play'}; + controller.sendControlCommand(avCommand); + } + // 下发暂停命令 + if (validCommandTypeArray.indexOf('pause') >= 0) { + let avCommand: AVSessionManager.AVControlCommand = {command:'pause'}; + controller.sendControlCommand(avCommand); + } + // 下发上一首命令 + if (validCommandTypeArray.indexOf('playPrevious') >= 0) { + let avCommand: AVSessionManager.AVControlCommand = {command:'playPrevious'}; + controller.sendControlCommand(avCommand); + } + // 下发下一首命令 + if (validCommandTypeArray.indexOf('playNext') >= 0) { + let avCommand: AVSessionManager.AVControlCommand = {command:'playNext'}; + controller.sendControlCommand(avCommand); + } + } + ``` + +6. 在媒体会话控制方应用退出时及时取消事件监听,并释放资源。 + + ```ts + async destroyController() { + // 假设我们已经有了一个对应session的controller,如何创建controller可以参考之前的案例 + let controller: AVSessionManager.AVSessionController = ALLREADY_HAVE_A_CONTROLLER; + + // 销毁当前的controller,销毁后这个controller将不在可用 + controller.destroy(function (err) { + if (err) { + console.info(`Destroy controller ERROR : code: ${err.code}, message: ${err.message}`); + } else { + console.info('Destroy controller SUCCESS'); + } + }); + } + ``` diff --git a/zh-cn/application-dev/media/using-avsession-developer.md b/zh-cn/application-dev/media/using-avsession-developer.md new file mode 100644 index 0000000000000000000000000000000000000000..9c5486ed6392978749775ed0368775219157209e --- /dev/null +++ b/zh-cn/application-dev/media/using-avsession-developer.md @@ -0,0 +1,198 @@ +# 媒体会话提供方 + +音视频应用在实现音视频功能的同时,需要作为媒体会话提供方接入媒体会话,在媒体会话控制方(例如播控中心)中展示媒体相关信息,及响应媒体会话控制方下发的播控命令。 + +## 基本概念 + +- 媒体会话元数据(AVMetadata): 用于描述媒体数据相关属性,包含标识当前媒体的ID(assetId),上一首媒体的ID(previousAssetId),下一首媒体的ID(nextAssetId),标题(title),专辑作者(author),专辑名称(album),词作者(writer),媒体时长(duration)等属性。 + +- 媒体播放状态(AVPlaybackState):用于描述媒体播放状态的相关属性,包含当前媒体的播放状态(state)、播放位置(position)、播放倍速(speed)、缓冲时间(bufferedTime)、循环模式(loopMode)、是否收藏(isFavorite)等属性。 + +## 接口说明 + +媒体会话提供方使用的关键接口如下表所示。接口返回值有两种返回形式:callback和promise,下表中为callback形式接口,promise和callback只是返回值方式不一样,功能相同。 + +更多API说明请参见[API文档](../reference/apis/js-apis-avsession.md)。 + +| 接口名 | 说明 | +| -------- | -------- | +| createAVSession(context: Context, tag: string, type: AVSessionType, callback: AsyncCallback<AVSession>): void | 创建媒体会话。
一个UIAbility只能存在一个媒体会话,重复创建会失败。 | +| setAVMetadata(data: AVMetadata, callback: AsyncCallback<void>): void | 设置媒体会话元数据。 | +| setAVPlaybackState(state: AVPlaybackState, callback: AsyncCallback<void>): void | 设置媒体会话播放状态。 | +| setLaunchAbility(ability: WantAgent, callback: AsyncCallback<void>): void | 设置启动UIAbility。 | +| getController(callback: AsyncCallback<AVSessionController>): void | 获取当前会话自身控制器。 | +| activate(callback: AsyncCallback<void>): void | 激活媒体会话。 | +| destroy(callback: AsyncCallback<void>): void | 销毁媒体会话。 | + +## 开发步骤 + +音视频应用作为媒体会话提供方接入媒体会话的基本步骤如下所示: + +1. 通过AVSessionManager的方法创建并激活媒体会话。 + + ```ts + import AVSessionManager from '@ohos.multimedia.avsession'; //导入AVSession模块 + + // 创建session + async createSession() { + let session: AVSessionManager.AVSession = await AVSessionManager.createAVSession(this.context, 'SESSION_NAME', 'audio'); + session.activate(); + console.info(`session create done : sessionId : ${session.sessionId}`); + } + ``` + +2. 跟随媒体信息的变化,及时设置媒体会话信息。需要设置的媒体会话信息主要包括: + - 媒体会话元数据AVMetadata。 + - 媒体播放状态AVPlaybackState。 + + 音视频应用设置的媒体会话信息,会被媒体会话控制方通过AVSessionController相关方法获取后进行显示或处理。 + + ```ts + async setSessionInfo() { + // 假设已经创建了一个session,如何创建session可以参考之前的案例 + let session: AVSessionManager.AVSession = ALLREADY_CREATE_A_SESSION; + // 播放器逻辑··· 引发媒体信息与播放状态的变更 + // 设置必要的媒体信息 + let metadata: AVSessionManager.AVMetadata = { + assetId: "0", + title: "TITLE", + artist: "ARTIST" + }; + session.setAVMetadata(metadata).then(() => { + console.info('SetAVMetadata successfully'); + }).catch((err) => { + console.info(`SetAVMetadata BusinessError: code: ${err.code}, message: ${err.message}`); + }); + // 简单设置一个播放状态 - 暂停 未收藏 + let playbackState: AVSessionManager.AVPlaybackState = { + state:AVSessionManager.PlaybackState.PLAYBACK_STATE_PAUSE, + isFavorite:false + }; + session.setAVPlaybackState(playbackState, function (err) { + if (err) { + console.info(`SetAVPlaybackState BusinessError: code: ${err.code}, message: ${err.message}`); + } else { + console.info('SetAVPlaybackState successfully'); + } + }); + } + ``` + +3. 设置用于被媒体会话控制方拉起的UIAbility。当用户操作媒体会话控制方的界面时,例如点击播控中心的卡片,可以拉起此处配置的UIAbility。 + 设置UIAbility时通过WantAgent接口实现,更多关于WantAgent的信息请参考[WantAgent](../reference/apis/js-apis-wantAgent.md)。 + + ```ts + import WantAgent from "@ohos.wantAgent"; + ``` + + ```ts + // 假设已经创建了一个session,如何创建session可以参考之前的案例 + let session: AVSessionManager.AVSession = ALLREADY_CREATE_A_SESSION; + let wantAgentInfo: { + wants: [ + { + bundleName: "com.example.musicdemo", + abilityName: "com.example.musicdemo.MainAbility" + } + ], + operationType: WantAgent.OperationType.START_ABILITIES, + requestCode: 0, + wantAgentFlags: [WantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG] + } + WantAgent.getWantAgent(wantAgentInfo).then((agent) => { + session.setLaunchAbility(agent) + }) + ``` + +4. 注册播控命令事件监听,便于响应用户通过媒体会话控制方,例如播控中心,下发的播控命令。 + > **说明:** + > + > 媒体会话提供方在注册相关播控命令事件监听时,监听的事件会在媒体会话控制方的getValidCommands()方法中体现,即媒体会话控制方会认为对应的方法有效,进而根据需要触发相应的事件。为了保证媒体会话控制方下发的播控命令可以被正常执行,媒体会话提供方请勿进行无逻辑的空实现监听。 + + ```ts + async setListenerForMesFromController() { + // 假设已经创建了一个session,如何创建session可以参考之前的案例 + let session: AVSessionManager.AVSession = ALLREADY_CREATE_A_SESSION; + // 一般在监听器中会对播放器做相应逻辑处理 + // 不要忘记处理完后需要通过set接口同步播放相关信息,参考上面的用例 + session.on('play', () => { + console.info('on play , do play task'); + + // do some tasks ··· + }); + session.on('pause', () => { + console.info('on pause , do pause task'); + // do some tasks ··· + }); + session.on('stop', () => { + console.info('on stop , do stop task'); + // do some tasks ··· + }); + session.on('playNext', () => { + console.info('on playNext , do playNext task'); + // do some tasks ··· + }); + session.on('playPrevious', () => { + console.info('on playPrevious , do playPrevious task'); + // do some tasks ··· + }); + } + ``` + +5. 获取当前媒体会话自身的控制器,与媒体会话对应进行通信交互。 + + ```ts + async createControllerFromSession() { + // 假设已经创建了一个session,如何创建session可以参考之前的案例 + let session: AVSessionManager.AVSession = ALLREADY_CREATE_A_SESSION; + + // 通过已有session获取一个controller对象 + let controller: AVSessionManager.AVSessionController = await session.getController(); + + // controller可以与原session对象进行基本的通信交互,比如下发播放命令 + let avCommand: AVSessionManager.AVControlCommand = {command:'play'}; + controller.sendControlCommand(avCommand); + + // 或者做状态变更监听 + controller.on('playbackStateChange', 'all', (state: AVSessionManager.AVPlaybackState) => { + + // do some things + }); + + // controller可以做的操作还有很多,具体可以参考媒体会话控制方相关的说明 + } + ``` + +6. 音视频应用在退出,并且不需要继续播放时,及时取消监听以及销毁媒体会话释放资源。 + 取消播控命令监听的示例代码如下所示 : + + ```ts + async unregisterSessionListener() { + // 假设已经创建了一个session,如何创建session可以参考之前的案例 + let session: AVSessionManager.AVSession = ALLREADY_CREATE_A_SESSION; + + // 取消指定session下的相关监听 + session.off('play'); + session.off('pause'); + session.off('stop'); + session.off('playNext'); + session.off('playPrevious'); + } + ``` + + 销毁媒体会话示例代码如下所示: + + ```ts + async destroySession() { + // 假设已经创建了一个session,如何创建session可以参考之前的案例 + let session: AVSessionManager.AVSession = ALLREADY_CREATE_A_SESSION; + // 主动销毁已创建的session + session.destroy(function (err) { + if (err) { + console.info(`Destroy BusinessError: code: ${err.code}, message: ${err.message}`); + } else { + console.info('Destroy : SUCCESS '); + } + }); + } + ``` diff --git a/zh-cn/application-dev/media/using-distributed-avsession.md b/zh-cn/application-dev/media/using-distributed-avsession.md new file mode 100644 index 0000000000000000000000000000000000000000..8f6793b1ee4c1d734fdea5b33e89e73f1868f565 --- /dev/null +++ b/zh-cn/application-dev/media/using-distributed-avsession.md @@ -0,0 +1,55 @@ +# 使用分布式媒体会话 + +## 基本概念 + +- 远端媒体会话:可信组网内的设备创建本地媒体会话后,媒体会话服务会将本地会话自动同步到远端,生成远端媒体会话。本地媒体会话与远端媒体会话时刻保持同步。 + +- 远端媒体会话控制器:远端投播后,远端设备上的媒体会话控制器。 + +## 接口说明 + +使用分布式媒体会话进行远端投播时使用的关键接口如下表所示。接口返回值有两种返回形式:callback和promise,下表中为callback形式接口,promise和callback只是返回值方式不一样,功能相同。 + +更多API说明请参见[API文档](../reference/apis/js-apis-avsession.md)。 + +| 接口名 | 说明 | +| -------- | -------- | +| castAudio(session: SessionToken \| ‘all’, audioDevices: Array<audio.AudioDeviceDescriptor>, callback: AsyncCallback<void>): void | 投播会话到指定设备列表。 | + +## 开发步骤 + +系统应用作为媒体会话控制方接入媒体会话时,根据需要使用分布式媒体会话进行投播的步骤如下所示: + +1. 导入模块接口。由于在进行投播之前,需要从audio模块获取音频设备描述符AudioDeviceDescriptor,所以除了导入avsession模块外,还需要导入audio模块。 + + ```ts + import AVSessionManager from '@ohos.multimedia.avsession'; + import audio from '@ohos.multimedia.audio'; + ``` + +2. 通过AVSessionManager的castAudio接口将当前设备所有会话投播到其他设备。 + + ```ts + // 投播到其他设备 + let audioManager = audio.getAudioManager(); + let audioRoutingManager = audioManager.getRoutingManager(); + let audioDevices; + await audioRoutingManager.getDevices(audio.DeviceFlag.OUTPUT_DEVICES_FLAG).then((data) => { + audioDevices = data; + console.info('Promise returned to indicate that the device list is obtained.'); + }).catch((err) => { + console.info(`getDevices : ERROR : ${err.message}`); + }); + + AVSessionManager.castAudio('all', audioDevices).then(() => { + console.info('createController : SUCCESS'); + }).catch((err) => { + console.info(`createController : ERROR : ${err.message}`); + }); + ``` + + 系统应用在投播主控端发起投播后,媒体会话框架会通知远端设备的AVSession服务创建远端媒体会话。投播主控端的媒体会话变化时(例如媒体信息变化、播放状态变化等),媒体会话框架会自动同步变化到远端设备。 + + 远端设备上的媒体会话处理机制,与本地设备上的机制保持一致,即远端设备上的媒体会话控制方,例如播控中心,会监听媒体会话创建事件,并创建相应的远端媒体会话控制器对远端媒体会话进行管理,而管理过程中的控制命令会由媒体会话框架自动同步到投播主控端设备。 + + 本地投播主控端中的媒体会话提供方,例如各音视频应用,会监听各类播控命令事件,从而可以及时响应来自远端设备的各类播控命令。 diff --git a/zh-cn/application-dev/media/using-opensl-es-for-playback.md b/zh-cn/application-dev/media/using-opensl-es-for-playback.md new file mode 100644 index 0000000000000000000000000000000000000000..72072a6077bb1e54bdd9914066b64482c3d66eb4 --- /dev/null +++ b/zh-cn/application-dev/media/using-opensl-es-for-playback.md @@ -0,0 +1,131 @@ +# 使用OpenSL ES开发音频播放功能 + +OpenSL ES全称为Open Sound Library for Embedded Systems,是一个嵌入式、跨平台、免费的音频处理库。为嵌入式移动多媒体设备上的应用开发者提供标准化、高性能、低延迟的API。OpenHarmony的Native API基于[Khronos Group](https://www.khronos.org/)开发的[OpenSL ES](https://www.khronos.org/opensles/) 1.0.1 API 规范实现,开发者可以通过<OpenSLES.h>和<OpenSLES_OpenHarmony.h>在OpenHarmony上使用相关API。 + +## OpenHarmony上的OpenSL ES + +OpenSL ES中提供了以下的接口,OpenHarmony当前仅实现了部分[接口](https://gitee.com/openharmony/third_party_opensles/blob/master/api/1.0.1/OpenSLES.h),可以实现音频播放的基础功能。 + +调用未实现接口后会返回**SL_RESULT_FEATURE_UNSUPPORTED,**当前没有相关扩展可以使用。 + +以下列表列举了OpenHarmony上已实现的OpenSL ES的接口,具体说明请参考[OpenSL ES](https://www.khronos.org/opensles/)规范: + +- **OpenHarmony上支持的Engine接口:** + - SLresult (\*CreateAudioPlayer) (SLEngineItf self, SLObjectItf \* pPlayer, SLDataSource \*pAudioSrc, SLDataSink \*pAudioSnk, SLuint32 numInterfaces, const SLInterfaceID \* pInterfaceIds, const SLboolean \* pInterfaceRequired) + - SLresult (\*CreateAudioRecorder) (SLEngineItf self, SLObjectItf \* pRecorder, SLDataSource \*pAudioSrc, SLDataSink \*pAudioSnk, SLuint32 numInterfaces, const SLInterfaceID \* pInterfaceIds, const SLboolean \* pInterfaceRequired) + - SLresult (\*CreateOutputMix) (SLEngineItf self, SLObjectItf \* pMix, SLuint32 numInterfaces, const SLInterfaceID \* pInterfaceIds, const SLboolean \* pInterfaceRequired) + +- **OpenHarmony上支持的Object接口:** + - SLresult (\*Realize) (SLObjectItf self, SLboolean async) + - SLresult (\*GetState) (SLObjectItf self, SLuint32 \* pState) + - SLresult (\*GetInterface) (SLObjectItf self, const SLInterfaceID iid, void \* pInterface) + - void (\*Destroy) (SLObjectItf self) + +- **OpenHarmony上支持的Playback接口:** + - SLresult (\*SetPlayState) (SLPlayItf self, SLuint32 state) + - SLresult (\*GetPlayState) (SLPlayItf self, SLuint32 \*pState) + +- **OpenHarmony上支持的Volume控制接口**: + - SLresult (\*SetVolumeLevel) (SLVolumeItf self, SLmillibel level) + - SLresult (\*GetVolumeLevel) (SLVolumeItf self, SLmillibel \*pLevel) + - SLresult (\*GetMaxVolumeLevel) (SLVolumeItf self, SLmillibel \*pMaxLevel) + +- **OpenHarmony上支持的BufferQueue接口**: + + 以下接口需引入<OpenSLES_OpenHarmony.h>使用。 + | 接口 | 说明 | + | -------- | -------- | + | SLresult (\*Enqueue) (SLOHBufferQueueItf self, const void \*buffer, SLuint32 size) | 根据情况将buffer加到相应队列中。
如果是播放操作,则将带有音频数据的buffer插入到filledBufferQ_队列中;如果是录音操作,则将录音使用后的空闲buffer插入到freeBufferQ_队列中。
self:表示调用该函数的BufferQueue接口对象。
buffer:播放时表示带有音频数据的buffer,录音时表示已存储完录音数据后的空闲buffer。
size:表示buffer的大小。 | + | SLresult (\*Clear) (SLOHBufferQueueItf self) | 释放BufferQueue接口对象。
self:表示调用该函数的BufferQueue接口对象将被释放。 | + | SLresult (\*GetState) (SLOHBufferQueueItf self, SLOHBufferQueueState \*state) | 获取BufferQueue接口对象状态。
self:表示调用该函数的BufferQueue接口对象。
state:BufferQueue的当前状态。 | + | SLresult (\*RegisterCallback) (SLOHBufferQueueItf self, SlOHBufferQueueCallback callback, void\* pContext) | 注册回调函数。
self:表示调用该函数的BufferQueue接口对象。
callback:播放/录音时注册的回调函数。
pContext:播放时传入待播放音频文件,录音时传入将要录制的音频文件。 | + | SLresult (\*GetBuffer) (SLOHBufferQueueItf self, SLuint8\*\* buffer, SLuint32\* size) | 根据情况获取相应的buffer。
如果是播放操作,则从freeBufferQ_队列中获取空闲buffer;如果是录音操作,则从filledBufferQ_队列中获取携带录音数据的buffer。
self:表示调用该函数的BufferQueue接口对象。
buffer:播放时表示空闲的buffer,录音时表示携带录音数据的buffer。
size:表示buffer的大小。 | + +## 完整示例 + +参考以下示例代码,播放一个音频文件。 + +1. 添加头文件。 + + ```c++ + #include + #include + #include + ``` + +2. 使用slCreateEngine接口和获取engine实例。 + + ```c++ + SLObjectItf engineObject = nullptr; + slCreateEngine(&engineObject, 0, nullptr, 0, nullptr, nullptr); + (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE); + ``` + +3. 获取接口SL_IID_ENGINE的engineEngine实例。 + + ```c++ + SLEngineItf engineEngine = nullptr; + (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine); + ``` + +4. 配置播放器信息,创建AudioPlayer。 + + ```c++ + SLDataLocator_BufferQueue slBufferQueue = { + SL_DATALOCATOR_BUFFERQUEUE, + 0 + }; + + // 具体参数需要根据音频文件格式进行适配 + SLDataFormat_PCM pcmFormat = { + SL_DATAFORMAT_PCM, + 2, // 通道数 + SL_SAMPLINGRATE_48, // 采样率 + SL_PCMSAMPLEFORMAT_FIXED_16, // 音频采样格式 + 0, + 0, + 0 + }; + SLDataSource slSource = {&slBufferQueue, &pcmFormat}; + SLObjectItf pcmPlayerObject = nullptr; + (*engineEngine)->CreateAudioPlayer(engineEngine, &pcmPlayerObject, &slSource, null, 0, nullptr, nullptr); + (*pcmPlayerObject)->Realize(pcmPlayerObject, SL_BOOLEAN_FALSE); + ``` + +5. 获取接口SL_IID_OH_BUFFERQUEUE的bufferQueueItf实例。 + + ```c++ + SLOHBufferQueueItf bufferQueueItf; + (*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_OH_BUFFERQUEUE, &bufferQueueItf); + ``` + +6. 打开音频文件,注册BufferQueueCallback回调。 + + ```c++ + static void BufferQueueCallback (SLOHBufferQueueItf bufferQueueItf, void *pContext, SLuint32 size) + { + SLuint8 *buffer = nullptr; + SLuint32 pSize; + (*bufferQueueItf)->GetBuffer(bufferQueueItf, &buffer, &pSize); + // 将待播放音频数据写入buffer + (*bufferQueueItf)->Enqueue(bufferQueueItf, buffer, size); + } + void *pContext; // 可传入自定义的上下文信息,会在Callback内收到 + (*bufferQueueItf)->RegisterCallback(bufferQueueItf, BufferQueueCallback, pContext); + ``` + +7. 获取接口SL_PLAYSTATE_PLAYING的playItf实例,开始播放。 + + ```c++ + SLPlayItf playItf = nullptr; + (*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_PLAY, &playItf); + (*playItf)->SetPlayState(playItf, SL_PLAYSTATE_PLAYING); + ``` + +8. 结束音频播放。 + + ```c++ + (*playItf)->SetPlayState(playItf, SL_PLAYSTATE_STOPPED); + (*pcmPlayerObject)->Destroy(pcmPlayerObject); + (*engineObject)->Destroy(engineObject); + ``` diff --git a/zh-cn/application-dev/media/using-opensl-es-for-recording.md b/zh-cn/application-dev/media/using-opensl-es-for-recording.md new file mode 100644 index 0000000000000000000000000000000000000000..5dc7772809f8e552ecb7e139d3ef5e1eaada06fb --- /dev/null +++ b/zh-cn/application-dev/media/using-opensl-es-for-recording.md @@ -0,0 +1,148 @@ +# 使用OpenSLES开发音频录制功能 + +OpenSL ES全称为Open Sound Library for Embedded Systems,是一个嵌入式、跨平台、免费的音频处理库。为嵌入式移动多媒体设备上的应用开发者提供标准化、高性能、低延迟的API。OpenHarmony的Native API基于[Khronos Group](https://www.khronos.org/)开发的[OpenSL ES](https://www.khronos.org/opensles/) 1.0.1 API 规范实现,开发者可以通过<OpenSLES.h>和<OpenSLES_OpenHarmony.h>在OpenHarmony上使用相关API。 + +## OpenHarmony上的OpenSL ES + +OpenSL ES中提供了以下的接口,OpenHarmony当前仅实现了部分[接口](https://gitee.com/openharmony/third_party_opensles/blob/master/api/1.0.1/OpenSLES.h),可以实现音频播放的基础功能。 + +调用未实现接口后会返回**SL_RESULT_FEATURE_UNSUPPORTED,**当前没有相关扩展可以使用。 + +以下列表列举了OpenHarmony上已实现的OpenSL ES的接口,具体说明请参考[OpenSL ES](https://www.khronos.org/opensles/)规范: + +- **OpenHarmony上支持的Engine接口:** + - SLresult (\*CreateAudioPlayer) (SLEngineItf self, SLObjectItf \* pPlayer, SLDataSource \*pAudioSrc, SLDataSink \*pAudioSnk, SLuint32 numInterfaces, const SLInterfaceID \* pInterfaceIds, const SLboolean \* pInterfaceRequired) + - SLresult (\*CreateAudioRecorder) (SLEngineItf self, SLObjectItf \* pRecorder, SLDataSource \*pAudioSrc, SLDataSink \*pAudioSnk, SLuint32 numInterfaces, const SLInterfaceID \* pInterfaceIds, const SLboolean \* pInterfaceRequired) + - SLresult (\*CreateOutputMix) (SLEngineItf self, SLObjectItf \* pMix, SLuint32 numInterfaces, const SLInterfaceID \* pInterfaceIds, const SLboolean \* pInterfaceRequired) + +- **OpenHarmony上支持的Object接口:** + - SLresult (\*Realize) (SLObjectItf self, SLboolean async) + - SLresult (\*GetState) (SLObjectItf self, SLuint32 \* pState) + - SLresult (\*GetInterface) (SLObjectItf self, const SLInterfaceID iid, void \* pInterface) + - void (\*Destroy) (SLObjectItf self) + +- **OpenHarmony上支持的Recorder接口:** + - SLresult (\*SetRecordState) (SLRecordItf self, SLuint32 state) + - SLresult (\*GetRecordState) (SLRecordItf self,SLuint32 \*pState) + +- **OpenHarmony上支持的BufferQueue接口**: + + 以下接口需引入<OpenSLES_OpenHarmony.h>使用。 + | 接口 | 说明 | + | -------- | -------- | + | SLresult (\*Enqueue) (SLOHBufferQueueItf self, const void \*buffer, SLuint32 size) | 根据情况将buffer加到相应队列中。
如果是播放操作,则将带有音频数据的buffer插入到filledBufferQ_队列中;如果是录音操作,则将录音使用后的空闲buffer插入到freeBufferQ_队列中。
self:表示调用该函数的BufferQueue接口对象。
buffer:播放时表示带有音频数据的buffer,录音时表示已存储完录音数据后的空闲buffer。
size:表示buffer的大小。 | + | SLresult (\*Clear) (SLOHBufferQueueItf self) | 释放BufferQueue接口对象。
self:表示调用该函数的BufferQueue接口对象将被释放。 | + | SLresult (\*GetState) (SLOHBufferQueueItf self, SLOHBufferQueueState \*state) | 获取BufferQueue接口对象状态。
self:表示调用该函数的BufferQueue接口对象。
state:BufferQueue的当前状态。 | + | SLresult (\*RegisterCallback) (SLOHBufferQueueItf self, SlOHBufferQueueCallback callback, void\* pContext) | 注册回调函数。
self:表示调用该函数的BufferQueue接口对象。
callback:播放/录音时注册的回调函数。
pContext:播放时传入待播放音频文件,录音时传入将要录制的音频文件。 | + | SLresult (\*GetBuffer) (SLOHBufferQueueItf self, SLuint8\*\* buffer, SLuint32\* size) | 根据情况获取相应的buffer。
如果是播放操作,则从freeBufferQ_队列中获取空闲buffer;如果是录音操作,则从filledBufferQ_队列中获取携带录音数据的buffer。
self:表示调用该函数的BufferQueue接口对象。
buffer:播放时表示空闲的buffer,录音时表示携带录音数据的buffer。
size:表示buffer的大小。 | + +## 完整示例 + +参考下列示例代码,完成音频录制。 + +1. 添加头文件 + + ```c++ + #include + #include + #include + ``` + +2. 使用slCreateEngine接口创建引擎对象和实例化引擎对象engine。 + + ```c++ + SLObjectItf engineObject = nullptr; + slCreateEngine(&engineObject, 0, nullptr, 0, nullptr, nullptr); + (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE); + ``` + +3. 获取接口SL_IID_ENGINE的引擎接口engineEngine实例。 + + ```c++ + SLEngineItf engineItf = nullptr; + (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineItf); + ``` + +4. 配置录音器信息(配置输入源audiosource、输出源audiosink),创建录音对象pcmCapturerObject。 + + ```c++ + SLDataLocator_IODevice io_device = { + SL_DATALOCATOR_IODEVICE, + SL_IODEVICE_AUDIOINPUT, + SL_DEFAULTDEVICEID_AUDIOINPUT, + NULL + }; + SLDataSource audioSource = { + &io_device, + NULL + }; + SLDataLocator_BufferQueue buffer_queue = { + SL_DATALOCATOR_BUFFERQUEUE, + 3 + }; + // 具体参数需要根据音频文件格式进行适配 + SLDataFormat_PCM format_pcm = { + SL_DATAFORMAT_PCM, // 输入的音频格式 + 1, // 单声道 + SL_SAMPLINGRATE_44_1, // 采样率: 44100HZ + SL_PCMSAMPLEFORMAT_FIXED_16, // 音频采样格式,小尾数,带符号的16位整数 + 0, + 0, + 0 + }; + SLDataSink audioSink = { + &buffer_queue, + &format_pcm + }; + + SLObjectItf pcmCapturerObject = nullptr; + (*engineItf)->CreateAudioRecorder(engineItf, &pcmCapturerObject, + &audioSource, &audioSink, 0, nullptr, nullptr); + (*pcmCapturerObject)->Realize(pcmCapturerObject, SL_BOOLEAN_FALSE); + + ``` + +5. 获取录音接口SL_IID_RECORD的recordItf接口实例。 + + ```c++ + SLRecordItf recordItf; + (*pcmCapturerObject)->GetInterface(pcmCapturerObject, SL_IID_RECORD, &recordItf); + ``` + +6. 获取接口 SL_IID_OH_BUFFERQUEUE 的 bufferQueueItf 实例 + + ```c++ + SLOHBufferQueueItf bufferQueueItf; + (*pcmCapturerObject)->GetInterface(pcmCapturerObject, SL_IID_OH_BUFFERQUEUE, &bufferQueueItf); + ``` + +7. 注册BufferQueueCallback回调。 + + ```c++ + static void BufferQueueCallback(SLOHBufferQueueItf bufferQueueItf, void *pContext, SLuint32 size) + { + // 可从pContext获取注册时传入的使用者信息 + SLuint8 *buffer = nullptr; + SLuint32 pSize = 0; + (*bufferQueueItf)->GetBuffer(bufferQueueItf, &buffer, &pSize); + if (buffer != nullptr) { + // 可从buffer内读取录音数据进行后续处理 + (*bufferQueueItf)->Enqueue(bufferQueueItf, buffer, size); + } + } + void *pContext; // 可传入自定义的上下文信息,会在Callback内收到 + (*bufferQueueItf)->RegisterCallback(bufferQueueItf, BufferQueueCallback, pContext); + ``` + +8. 开始录音。 + + ```c++ + (*recordItf)->SetRecordState(recordItf, SL_RECORDSTATE_RECORDING); + ``` + +9. 结束音频录制。 + + ```c++ + (*recordItf)->SetRecordState(recordItf, SL_RECORDSTATE_STOPPED); + (*pcmCapturerObject)->Destroy(pcmCapturerObject); + ``` diff --git a/zh-cn/application-dev/media/using-toneplayer-for-playback.md b/zh-cn/application-dev/media/using-toneplayer-for-playback.md new file mode 100644 index 0000000000000000000000000000000000000000..ebe5d84a2ac7a5fc958f03bbd9c56062b3b7a485 --- /dev/null +++ b/zh-cn/application-dev/media/using-toneplayer-for-playback.md @@ -0,0 +1,140 @@ +# 使用TonePlayer开发音频播放功能(仅对系统应用开放) + +TonePlayer9+提供播放和管理DTMF(Dual Tone Multi Frequency,双音多频)音调的方法,包括各种系统监听音调、专有音调,如拨号音、通话回铃音等。主要工作是将需要生成音调的[ToneType](../reference/apis/js-apis-audio.md#tonetype9)类型,通过自带算法生成多个不同频率的正弦波叠加形成声音数据,通过[AudioRenderer](../reference/apis/js-apis-audio.md#audiorenderer8)进行播放,同时对播放任务进行管理。包含加载DTMF音调配置、启动DTMF音调播放、停止当前正在播放的音调、释放与此TonePlayer对象关联的资源等流程。详细API说明请参考[TonePlayer API文档](../reference/apis/js-apis-audio.md#toneplayer9)。 + + +## 支持的播放音调类型 + +播放音调类型[ToneType](../reference/apis/js-apis-audio.md#tonetype9)信息(如下表所示),可通过"audio.ToneType.指定类型" 作为参数调用load()方法加载指定类型的音调资源。 + +| 播放音调类型 | 值 | 说明 | +| -------- | -------- | -------- | +| TONE_TYPE_DIAL_0 | 0 | 键0的DTMF音。 | +| TONE_TYPE_DIAL_1 | 1 | 键1的DTMF音。 | +| TONE_TYPE_DIAL_2 | 2 | 键2的DTMF音。 | +| TONE_TYPE_DIAL_3 | 3 | 键3的DTMF音。 | +| TONE_TYPE_DIAL_4 | 4 | 键4的DTMF音。 | +| TONE_TYPE_DIAL_5 | 5 | 键5的DTMF音。 | +| TONE_TYPE_DIAL_6 | 6 | 键6的DTMF音。 | +| TONE_TYPE_DIAL_7 | 7 | 键7的DTMF音。 | +| TONE_TYPE_DIAL_8 | 8 | 键8的DTMF音。 | +| TONE_TYPE_DIAL_9 | 9 | 键9的DTMF音。 | +| TONE_TYPE_DIAL_S | 10 | 键\*的DTMF音。 | +| TONE_TYPE_DIAL_P | 11 | 键\#的DTMF音。 | +| TONE_TYPE_DIAL_A | 12 | 键A的DTMF音。 | +| TONE_TYPE_DIAL_B | 13 | 键B的DTMF音。 | +| TONE_TYPE_DIAL_C | 14 | 键C的DTMF音。 | +| TONE_TYPE_DIAL_D | 15 | 键D的DTMF音。 | +| TONE_TYPE_COMMON_SUPERVISORY_DIAL | 100 | 呼叫监管音调,拨号音。 | +| TONE_TYPE_COMMON_SUPERVISORY_BUSY | 101 | 呼叫监管音调,忙。 | +| TONE_TYPE_COMMON_SUPERVISORY_CONGESTION | 102 | 呼叫监管音调,拥塞。 | +| TONE_TYPE_COMMON_SUPERVISORY_RADIO_ACK | 103 | 呼叫监管音调,无线电 ACK。 | +| TONE_TYPE_COMMON_SUPERVISORY_RADIO_NOT_AVAILABLE | 104 | 呼叫监管音调,无线电不可用。 | +| TONE_TYPE_COMMON_SUPERVISORY_CALL_WAITING | 106 | 呼叫监管音调,呼叫等待。 | +| TONE_TYPE_COMMON_SUPERVISORY_RINGTONE | 107 | 呼叫监管音调,铃声。 | +| TONE_TYPE_COMMON_PROPRIETARY_BEEP | 200 | 专有声调,一般蜂鸣声。 | +| TONE_TYPE_COMMON_PROPRIETARY_ACK | 201 | 专有声调,ACK。 | +| TONE_TYPE_COMMON_PROPRIETARY_PROMPT | 203 | 专有声调,PROMPT。 | +| TONE_TYPE_COMMON_PROPRIETARY_DOUBLE_BEEP | 204 | 专有声调,双重蜂鸣声。 | + + +## 开发步骤及注意事项 + +以下步骤描述了TonePlayer接口实现播放功能流程: + +1. 创建DTMF播放器 ,获取tonePlayer实例。 + + ```ts + import audio from '@ohos.multimedia.audio'; + let audioRendererInfo = { + content : audio.ContentType.CONTENT_TYPE_SONIFICATION, + usage : audio.StreamUsage.STREAM_USAGE_MEDIA, + rendererFlags : 0 + }; + tonePlayerPromise = audio.createTonePlayer(audioRendererInfo); + ``` + +2. 加载指定类型DTMF音调配置。 + + ```ts + tonePlayerPromise.load(audio.ToneType.TONE_TYPE_DIAL_0); + ``` + +3. 启动DTMF音调播放。 + + ```ts + tonePlayerPromise.start(); + ``` + +4. 停止当前正在播放的音调。 + + ```ts + tonePlayerPromise.stop(); + ``` + +5. 释放与此TonePlayer对象关联的资源。 + + ```ts + tonePlayerPromise.release(); + ``` + +在接口未按此正常调用时序调用时,接口会返回错误码6800301 NAPI_ERR_SYSTEM。 + + +## 完整示例 + +参考以下示例,点击键盘拨号按键,并启动对应的DTMF音调播放。 + +为保证UI线程不被阻塞,大部分TonePlayer调用都是异步的。对于每个API均提供了callback函数和Promise函数,以下示例均采用Promise函数,更多方式可参考API文档[TonePlayer](../reference/apis/js-apis-audio.md#toneplayer9)。 + + +```ts +import audio from '@ohos.multimedia.audio'; + +export class TonelayerDemo { + private timer : number; + private timerPro : number; + // promise调用方式 + async testTonePlayerPromise(type) { + console.info('testTonePlayerPromise start'); + if (this.timerPro) clearTimeout(this.timerPro); + let tonePlayerPromise; + let audioRendererInfo = { + content : audio.ContentType.CONTENT_TYPE_SONIFICATION, + usage : audio.StreamUsage.STREAM_USAGE_MEDIA, + rendererFlags : 0 + }; + this.timerPro = setTimeout(async () => { + try { + console.info('testTonePlayerPromise: createTonePlayer'); + // 创建DTMF播放器 + tonePlayerPromise = await audio.createTonePlayer(audioRendererInfo); + console.info('testTonePlayerPromise: createTonePlayer-success'); + console.info(`testTonePlayerPromise: load type: ${type}`); + // 加载type类型音调 + await tonePlayerPromise.load(type); + console.info('testTonePlayerPromise: load-success'); + console.info(`testTonePlayerPromise: start type: ${type}`); + // 启动DTMF音调播放 + await tonePlayerPromise.start(); + console.info('testTonePlayerPromise: start-success'); + console.info(`testTonePlayerPromise: stop type: ${type}`); + setTimeout(async()=>{ + // 停止当前正在播放的音调 + await tonePlayerPromise.stop(); + console.info('testTonePlayerPromise: stop-success'); + console.info(`testTonePlayerPromise: release type: ${type}`); + // 释放与此TonePlayer对象关联的资源 + await tonePlayerPromise.release(); + console.info('testTonePlayerPromise: release-success'); + }, 30) + } catch(err) { + console.error(`testTonePlayerPromise err : ${err}`); + } + }, 200) + }; + async testTonePlayer() { + this.testTonePlayerPromise(audio.ToneType.TONE_TYPE_DIAL_0); + } +} +``` diff --git a/zh-cn/application-dev/media/video-playback.md b/zh-cn/application-dev/media/video-playback.md index ae650343e6f553dc08bbac682b3803de491577cc..7bbf8400ed151eaa94a9acfd5833c0b318fe4ac4 100644 --- a/zh-cn/application-dev/media/video-playback.md +++ b/zh-cn/application-dev/media/video-playback.md @@ -1,433 +1,176 @@ -# 视频播放开发指导 +# 视频播放 -## 简介 +在OpenHarmony系统中,提供两种视频播放开发的方案: -视频播放的主要工作是将视频数据转码并输出到设备进行播放,同时管理播放任务,包括开始播放、暂停播放、停止播放、资源释放、音量设置、跳转播放位置、设置倍数、获取轨道信息等功能控制。本文将对视频播放全流程、视频切换、视频循环播放等场景开发进行介绍说明。 +- [AVPlayer](using-avplayer-for-playback.md):功能较完善的音视频播放ArkTS/JS API,集成了流媒体和本地资源解析,媒体资源解封装,视频解码和渲染功能,适用于对媒体资源进行端到端播放的场景,可直接播放mp4、mkv等格式的视频文件。 -## 运作机制 +- Video组件:封装了视频播放的基础能力,需要设置数据源以及基础信息即可播放视频,但相对扩展能力较弱。Video组件由ArkUI提供能力,相关指导请参考UI开发文档-[Video组件](../ui/arkts-common-components-video-player.md)。 -该模块提供了视频播放状态变化示意图和视频播放外部模块交互图。 - -**图1** 视频播放状态变化示意图 - -![zh-ch_image_video_state_machine](figures/zh-ch_image_video_state_machine.png) - -**图2** 视频播放外部模块交互图 - -![zh-ch_image_video_player](figures/zh-ch_image_video_player.png) - -**说明**:三方应用通过调用JS接口层提供的js接口实现相应功能时,框架层会通过Native Framework的媒体服务,调用音频部件将软件解码后的音频数据,输出至音频HDI,和图形子系统将硬件接口层的解码HDI部件的解码后的图像数据,输出至显示HDI,实现视频播放功能。 - -*注意:视频播放需要显示、音频、编解码等硬件能力。* - -1. 三方应用从Xcomponent组件获取surfaceID。 -2. 三方应用把surfaceID传递给VideoPlayer JS。 -3. 媒体服务把帧数据flush给surface buffer。 - -## 兼容性说明 -视频播放支持的视频格式分必选规格和可选规格。必选规格为所有厂商均支持的视频格式。对于可选规格,厂商将基于实际情况决定是否实现。建议开发者做兼容处理,保证全平台兼容。 -推荐使用主流的播放格式和主流分辨率,不建议开发者自制非常或者异常码流,以免产生无法播放、卡住、花屏等兼容性问题。若发生此类问题不会影响系统,退出码流播放即可。 - -| 视频格式 | 是否必选规格 | -|:--------:|:-----:| -| H264 | 是 | -| MPEG2 | 否 | -| MPEG4 | 否 | -| H263 | 否 | -| VP8 | 否 | - -主流的播放格式和主流分辨率如下: - -| 视频容器规格 | 规格描述 | 分辨率 | -| :----------: | :-----------------------------------------------: | :--------------------------------: | -| mp4 | 视频格式:H264/MPEG2/MPEG4/H263 音频格式:AAC/MP3 | 主流分辨率,如1080P/720P/480P/270P | -| mkv | 视频格式:H264/MPEG2/MPEG4/H263 音频格式:AAC/MP3 | 主流分辨率,如1080P/720P/480P/270P | -| ts | 视频格式:H264/MPEG2/MPEG4 音频格式:AAC/MP3 | 主流分辨率,如1080P/720P/480P/270P | -| webm | 视频格式:VP8 音频格式:VORBIS | 主流分辨率,如1080P/720P/480P/270P | +本开发指导将介绍如何使用AVPlayer开发视频播放功能,以完整地播放一个视频作为示例,实现端到端播放原始媒体资源。如果要实现后台播放或熄屏播放,需要使用[AVSession(媒体会话)](avsession-overview.md)和[申请长时任务](../task-management/continuous-task-dev-guide.md),避免播放过程中音频模块被系统强制中断。 ## 开发指导 -详细API含义可参考:[媒体服务API文档VideoPlayer](../reference/apis/js-apis-media.md#videoplayer8) - -### 全流程场景 - -视频播放的全流程场景包含:创建实例,设置url,设置SurfaceId,准备播放视频,播放视频,暂停播放,获取轨道信息,跳转播放位置,设置音量,设置倍速,结束播放,重置,释放资源等流程。 - -VideoPlayer支持的url媒体源输入类型可参考:[url属性说明](../reference/apis/js-apis-media.md#videoplayer_属性) - -Xcomponent创建方法可参考:[Xcomponent创建方法](../reference/arkui-ts/ts-basic-components-xcomponent.md) - -```js -import media from '@ohos.multimedia.media' -import fs from '@ohos.file.fs' -export class VideoPlayerDemo { - // 函数调用发生错误时用于上报错误信息 - failureCallback(error) { - console.info(`error happened,error Name is ${error.name}`); - console.info(`error happened,error Code is ${error.code}`); - console.info(`error happened,error Message is ${error.message}`); - } - - // 当函数调用发生异常时用于上报错误信息 - catchCallback(error) { - console.info(`catch error happened,error Name is ${error.name}`); - console.info(`catch error happened,error Code is ${error.code}`); - console.info(`catch error happened,error Message is ${error.message}`); - } - - // 用于打印视频轨道信息 - printfDescription(obj) { - for (let item in obj) { - let property = obj[item]; - console.info('key is ' + item); - console.info('value is ' + property); - } - } - - async videoPlayerDemo() { - let videoPlayer = undefined; - let surfaceID = 'test' // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接见上面Xcomponent创建方法 - let fdPath = 'fd://' - // path路径的码流可通过"hdc file send D:\xxx\H264_AAC.mp4 /data/app/el1/bundle/public/ohos.acts.multimedia.video.videoplayer/ohos.acts.multimedia.video.videoplayer/assets/entry/resources/rawfile" 命令,将其推送到设备上 - let path = '/data/app/el1/bundle/public/ohos.acts.multimedia.video.videoplayer/ohos.acts.multimedia.video.videoplayer/assets/entry/resources/rawfile/H264_AAC.mp4'; - let file = await fs.open(path); - fdPath = fdPath + '' + file.fd; - // 调用createVideoPlayer接口返回videoPlayer实例对象 - await media.createVideoPlayer().then((video) => { - if (typeof (video) != 'undefined') { - console.info('createVideoPlayer success!'); - videoPlayer = video; - } else { - console.info('createVideoPlayer fail!'); +播放的全流程包含:创建AVPlayer,设置播放资源和窗口,设置播放参数(音量/倍速/缩放模式),播放控制(播放/暂停/跳转/停止),重置,销毁资源。在进行应用开发的过程中,开发者可以通过AVPlayer的state属性主动获取当前状态或使用on('stateChange')方法监听状态变化。如果应用在视频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。 + +**图1** 播放状态变化示意图   + +![Playback status change](figures/video-playback-status-change.png) + +状态的详细说明请参考[AVPlayerState](../reference/apis/js-apis-media.md#avplayerstate9)。当播放处于prepared / playing / paused / compeled状态时,播放引擎处于工作状态,这需要占用系统较多的运行内存。当客户端暂时不使用播放器时,调用reset()或release()回收内存资源,做好资源利用。 + +### 开发步骤及注意事项 + +详细的API说明请参考[AVPlayer API参考](../reference/apis/js-apis-media.md#avplayer9)。 + +1. 创建实例createAVPlayer(),AVPlayer初始化idle状态。 + +2. 设置业务需要的监听事件,搭配全流程场景使用。支持的监听事件包括: + | 事件类型 | 说明 | + | -------- | -------- | + | stateChange | 必要事件,监听播放器的state属性改变。 | + | error | 必要事件,监听播放器的错误信息。 | + | durationUpdate | 用于进度条,监听进度条长度,刷新资源时长。 | + | timeUpdate | 用于进度条,监听进度条当前位置,刷新当前时间。 | + | seekDone | 响应API调用,监听seek()请求完成情况。
当使用seek()跳转到指定播放位置后,如果seek操作成功,将上报该事件。 | + | speedDone | 响应API调用,监听setSpeed()请求完成情况。
当使用setSpeed()跳转到指定播放位置后,如果setSpeed操作成功,将上报该事件。 | + | volumeChange | 响应API调用,监听setVolume()请求完成情况。
当使用setVolume()跳转到指定播放位置后,如果setVolume操作成功,将上报该事件。 | + | bitrateDone | 响应API调用,用于HLS协议流,监听setBitrate()请求完成情况。
当使用setBitrate()指定播放比特率后,如果setBitrate操作成功,将上报该事件。 | + | availableBitrates | 用于HLS协议流,监听HLS资源的可选bitrates,用于setBitrate()。 | + | bufferingUpdate | 用于网络播放,监听网络播放缓冲信息。 | + | startRenderFrame | 用于视频播放,监听视频播放首帧渲染时间。 | + | videoSizeChange | 用于视频播放,监听视频播放的宽高信息,可用于调整窗口大小、比例。 | + | audioInterrupt | 监听音频焦点切换信息,搭配属性audioInterruptMode使用。
如果当前设备存在多个媒体正在播放,音频焦点被切换(即播放其他媒体如通话等)时将上报该事件,应用可以及时处理。 | + +3. 设置资源:设置属性url,AVPlayer进入initialized状态。 + > **说明:** + > + > 下面代码示例中的url仅作示意使用,开发者需根据实际情况,确认资源有效性并设置: + > + > - 确认相应的资源文件可用,并使用应用沙箱路径访问对应资源,参考[获取应用文件路径](../application-models/application-context-stage.md#获取应用开发路径)。应用沙箱的介绍及如何向应用沙箱推送文件,请参考[文件管理](../file-management/app-sandbox-directory.md)。 + > + > - 如果使用网络播放路径,需[申请相关权限](../security/accesstoken-guidelines.md):ohos.permission.INTERNET。 + > + > - 如果使用ResourceManager.getRawFd打开HAP资源文件描述符,使用方法可参考[ResourceManager API参考](../reference/apis/js-apis-resource-manager.md#getrawfd9)。 + > + > - 需要使用[支持的播放格式与协议](avplayer-avrecorder-overview.md#支持的格式与协议)。 + +4. 设置窗口:获取并设置属性SurfaceID,用于设置显示画面。 + 应用需要从XComponent组件获取surfaceID,获取方式请参考[XComponent](../reference/arkui-ts/ts-basic-components-xcomponent.md)。 + +5. 准备播放:调用prepare(),AVPlayer进入prepared状态,此时可以获取duration,设置缩放模式、音量等。 + +6. 视频播控:播放play(),暂停pause(),跳转seek(),停止stop() 等操作。 + +7. 调用reset()重置资源,AVPlayer重新进入idle状态,允许更换资源url。 + +8. 调用release()销毁实例,AVPlayer进入released状态,退出播放。 + + +### 完整示例 + + +```ts +import media from '@ohos.multimedia.media'; +import fs from '@ohos.file.fs'; +import common from '@ohos.app.ability.common'; + +export class AVPlayerDemo { + private avPlayer; + private count: number = 0; + private surfaceID: string; // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接见上面Xcomponent创建方法 + + // 注册avplayer回调函数 + setAVPlayerCallback() { + // seek操作结果回调函数 + this.avPlayer.on('seekDone', (seekDoneTime) => { + console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`); + }) + // error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程 + this.avPlayer.on('error', (err) => { + console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`); + this.avPlayer.reset(); // 调用reset重置资源,触发idle状态 + }) + // 状态机变化回调函数 + this.avPlayer.on('stateChange', async (state, reason) => { + switch (state) { + case 'idle': // 成功调用reset接口后触发该状态机上报 + console.info('AVPlayer state idle called.'); + this.avPlayer.release(); // 调用release接口销毁实例对象 + break; + case 'initialized': // avplayer 设置播放源后触发该状态上报 + console.info('AVPlayerstate initialized called.'); + this.avPlayer.surfaceId = this.surfaceID; // 设置显示画面,当播放的资源为纯音频时无需设置 + this.avPlayer.prepare().then(() => { + console.info('AVPlayer prepare succeeded.'); + }, (err) => { + console.error(`Invoke prepare failed, code is ${err.code}, message is ${err.message}`); + }); + break; + case 'prepared': // prepare调用成功后上报该状态机 + console.info('AVPlayer state prepared called.'); + this.avPlayer.play(); // 调用播放接口开始播放 + break; + case 'playing': // play成功调用后触发该状态机上报 + console.info('AVPlayer state playing called.'); + if (this.count !== 0) { + console.info('AVPlayer start to seek.'); + this.avPlayer.seek(this.avPlayer.duration); //seek到视频末尾 + } else { + this.avPlayer.pause(); // 调用暂停接口暂停播放 + } + this.count++; + break; + case 'paused': // pause成功调用后触发该状态机上报 + console.info('AVPlayer state paused called.'); + this.avPlayer.play(); // 再次播放接口开始播放 + break; + case 'completed': // 播放结束后触发该状态机上报 + console.info('AVPlayer state completed called.'); + this.avPlayer.stop(); //调用播放结束接口 + break; + case 'stopped': // stop接口成功调用后触发该状态机上报 + console.info('AVPlayer state stopped called.'); + this.avPlayer.reset(); // 调用reset接口初始化avplayer状态 + break; + case 'released': + console.info('AVPlayer state released called.'); + break; + default: + console.info('AVPlayer state unknown called.'); + break; } - }, this.failureCallback).catch(this.catchCallback); - // 设置播放源 - videoPlayer.url = fdPath; - - // 设置surfaceID用于显示视频画面 - await videoPlayer.setDisplaySurface(surfaceID).then(() => { - console.info('setDisplaySurface success'); - }, this.failureCallback).catch(this.catchCallback); - - // 调用prepare完成播放前准备工作 - await videoPlayer.prepare().then(() => { - console.info('prepare success'); - }, this.failureCallback).catch(this.catchCallback); - - // 调用play接口正式开始播放 - await videoPlayer.play().then(() => { - console.info('play success'); - }, this.failureCallback).catch(this.catchCallback); - - // 暂停播放 - await videoPlayer.pause().then(() => { - console.info('pause success'); - }, this.failureCallback).catch(this.catchCallback); - - // 通过promise回调方式获取视频轨道信息communication_dsoftbus - let arrayDescription; - await videoPlayer.getTrackDescription().then((arrlist) => { - if (typeof (arrlist) != 'undefined') { - arrayDescription = arrlist; - } else { - console.log('video getTrackDescription fail'); - } - }, this.failureCallback).catch(this.catchCallback); - - for (let i = 0; i < arrayDescription.length; i++) { - this.printfDescription(arrayDescription[i]); - } - - // 跳转播放时间到50s位置,具体入参意义请参考接口文档 - let seekTime = 50000; - await videoPlayer.seek(seekTime, media.SeekMode.SEEK_NEXT_SYNC).then((seekDoneTime) => { - console.info('seek success'); - }, this.failureCallback).catch(this.catchCallback); - - // 音量设置接口,具体入参意义请参考接口文档 - let volume = 0.5; - await videoPlayer.setVolume(volume).then(() => { - console.info('setVolume success'); - }, this.failureCallback).catch(this.catchCallback); - - // 倍速设置接口,具体入参意义请参考接口文档 - let speed = media.PlaybackSpeed.SPEED_FORWARD_2_00_X; - await videoPlayer.setSpeed(speed).then(() => { - console.info('setSpeed success'); - }, this.failureCallback).catch(this.catchCallback); - - // 结束播放 - await videoPlayer.stop().then(() => { - console.info('stop success'); - }, this.failureCallback).catch(this.catchCallback); - - // 重置播放配置 - await videoPlayer.reset().then(() => { - console.info('reset success'); - }, this.failureCallback).catch(this.catchCallback); - - // 释放播放资源 - await videoPlayer.release().then(() => { - console.info('release success'); - }, this.failureCallback).catch(this.catchCallback); - - // 相关对象置undefined - videoPlayer = undefined; - surfaceID = undefined; - } -} -``` - -### 正常播放场景 - -```js -import media from '@ohos.multimedia.media' -import fs from '@ohos.file.fs' -export class VideoPlayerDemo { - // 函数调用发生错误时用于上报错误信息 - failureCallback(error) { - console.info(`error happened,error Name is ${error.name}`); - console.info(`error happened,error Code is ${error.code}`); - console.info(`error happened,error Message is ${error.message}`); - } - - // 当函数调用发生异常时用于上报错误信息 - catchCallback(error) { - console.info(`catch error happened,error Name is ${error.name}`); - console.info(`catch error happened,error Code is ${error.code}`); - console.info(`catch error happened,error Message is ${error.message}`); - } - - // 用于打印视频轨道信息 - printfDescription(obj) { - for (let item in obj) { - let property = obj[item]; - console.info('key is ' + item); - console.info('value is ' + property); - } - } - - async videoPlayerDemo() { - let videoPlayer = undefined; - let surfaceID = 'test' // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接: - let fdPath = 'fd://' - // path路径的码流可通过"hdc file send D:\xxx\H264_AAC.mp4 /data/app/el1/bundle/public/ohos.acts.multimedia.video.videoplayer/ohos.acts.multimedia.video.videoplayer/assets/entry/resources/rawfile" 命令,将其推送到设备上 - let path = '/data/app/el1/bundle/public/ohos.acts.multimedia.video.videoplayer/ohos.acts.multimedia.video.videoplayer/assets/entry/resources/rawfile/H264_AAC.mp4'; + }) + } + + // 以下demo为使用fs文件系统打开沙箱地址获取媒体文件地址并通过url属性进行播放示例 + async avPlayerUrlDemo() { + // 创建avPlayer实例对象 + this.avPlayer = await media.createAVPlayer(); + // 创建状态机变化回调函数 + this.setAVPlayerCallback(); + let fdPath = 'fd://'; + let context = getContext(this) as common.UIAbilityContext; + // 通过UIAbilityContext获取沙箱地址filesDir,以Stage模型为例 + let pathDir = context.filesDir; + let path = pathDir + '/H264_AAC.mp4'; + // 打开相应的资源文件地址获取fd,并为url赋值触发initialized状态机上报 let file = await fs.open(path); fdPath = fdPath + '' + file.fd; - // 调用createVideoPlayer接口返回videoPlayer实例对象 - await media.createVideoPlayer().then((video) => { - if (typeof (video) != 'undefined') { - console.info('createVideoPlayer success!'); - videoPlayer = video; - } else { - console.info('createVideoPlayer fail!'); - } - }, this.failureCallback).catch(this.catchCallback); - // 设置播放源 - videoPlayer.url = fdPath; - - // 设置surfaceID用于显示视频画面 - await videoPlayer.setDisplaySurface(surfaceID).then(() => { - console.info('setDisplaySurface success'); - }, this.failureCallback).catch(this.catchCallback); - - // 调用prepare完成播放前准备工作 - await videoPlayer.prepare().then(() => { - console.info('prepare success'); - }, this.failureCallback).catch(this.catchCallback); - - // 调用play接口正式开始播放 - await videoPlayer.play().then(() => { - console.info('play success'); - }, this.failureCallback).catch(this.catchCallback); - - // 结束播放 - await videoPlayer.stop().then(() => { - console.info('stop success'); - }, this.failureCallback).catch(this.catchCallback); - - // 释放播放资源 - await videoPlayer.release().then(() => { - console.info('release success'); - }, this.failureCallback).catch(this.catchCallback); - - // 相关对象置undefined - videoPlayer = undefined; - surfaceID = undefined; - } -} -``` - -### 切视频场景 - -```js -import media from '@ohos.multimedia.media' -import fs from '@ohos.file.fs' -export class VideoPlayerDemo { - // 函数调用发生错误时用于上报错误信息 - failureCallback(error) { - console.info(`error happened,error Name is ${error.name}`); - console.info(`error happened,error Code is ${error.code}`); - console.info(`error happened,error Message is ${error.message}`); - } - - // 当函数调用发生异常时用于上报错误信息 - catchCallback(error) { - console.info(`catch error happened,error Name is ${error.name}`); - console.info(`catch error happened,error Code is ${error.code}`); - console.info(`catch error happened,error Message is ${error.message}`); - } - - // 用于打印视频轨道信息 - printfDescription(obj) { - for (let item in obj) { - let property = obj[item]; - console.info('key is ' + item); - console.info('value is ' + property); - } - } - - async videoPlayerDemo() { - let videoPlayer = undefined; - let surfaceID = 'test' // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接: - let fdPath = 'fd://' - // path路径的码流可通过"hdc file send D:\xxx\H264_AAC.mp4 /data/app/el1/bundle/public/ohos.acts.multimedia.video.videoplayer/ohos.acts.multimedia.video.videoplayer/assets/entry/resources/rawfile" 命令,将其推送到设备上 - let path = '/data/app/el1/bundle/public/ohos.acts.multimedia.video.videoplayer/ohos.acts.multimedia.video.videoplayer/assets/entry/resources/rawfile/H264_AAC.mp4'; - let nextPath = '/data/app/el1/bundle/public/ohos.acts.multimedia.video.videoplayer/ohos.acts.multimedia.video.videoplayer/assets/entry/resources/rawfile/MP4_AAC.mp4'; - let file = await fs.open(path); - fdPath = fdPath + '' + file.fd; - // 调用createVideoPlayer接口返回videoPlayer实例对象 - await media.createVideoPlayer().then((video) => { - if (typeof (video) != 'undefined') { - console.info('createVideoPlayer success!'); - videoPlayer = video; - } else { - console.info('createVideoPlayer fail!'); - } - }, this.failureCallback).catch(this.catchCallback); - // 设置播放源 - videoPlayer.url = fdPath; - - // 设置surfaceID用于显示视频画面 - await videoPlayer.setDisplaySurface(surfaceID).then(() => { - console.info('setDisplaySurface success'); - }, this.failureCallback).catch(this.catchCallback); - - // 调用prepare完成播放前准备工作 - await videoPlayer.prepare().then(() => { - console.info('prepare success'); - }, this.failureCallback).catch(this.catchCallback); - - // 调用play接口正式开始播放 - await videoPlayer.play().then(() => { - console.info('play success'); - }, this.failureCallback).catch(this.catchCallback); - - // 重置播放配置 - await videoPlayer.reset().then(() => { - console.info('reset success'); - }, this.failureCallback).catch(this.catchCallback); - - // 获取下一个视频fd地址 - fdPath = 'fd://' - let nextFile = await fs.open(nextPath); - fdPath = fdPath + '' + nextFile.fd; - // 设置第二个视频播放源 - videoPlayer.url = fdPath; - - // 调用prepare完成播放前准备工作 - await videoPlayer.prepare().then(() => { - console.info('prepare success'); - }, this.failureCallback).catch(this.catchCallback); - - // 调用play接口正式开始播放 - await videoPlayer.play().then(() => { - console.info('play success'); - }, this.failureCallback).catch(this.catchCallback); - - // 释放播放资源 - await videoPlayer.release().then(() => { - console.info('release success'); - }, this.failureCallback).catch(this.catchCallback); - - // 相关对象置undefined - videoPlayer = undefined; - surfaceID = undefined; - } -} -``` - -### 单个视频循环场景 - -```js -import media from '@ohos.multimedia.media' -import fs from '@ohos.file.fs' -export class VideoPlayerDemo { - // 函数调用发生错误时用于上报错误信息 - failureCallback(error) { - console.info(`error happened,error Name is ${error.name}`); - console.info(`error happened,error Code is ${error.code}`); - console.info(`error happened,error Message is ${error.message}`); - } - - // 当函数调用发生异常时用于上报错误信息 - catchCallback(error) { - console.info(`catch error happened,error Name is ${error.name}`); - console.info(`catch error happened,error Code is ${error.code}`); - console.info(`catch error happened,error Message is ${error.message}`); - } - - // 用于打印视频轨道信息 - printfDescription(obj) { - for (let item in obj) { - let property = obj[item]; - console.info('key is ' + item); - console.info('value is ' + property); - } - } - - async videoPlayerDemo() { - let videoPlayer = undefined; - let surfaceID = 'test' // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接: - let fdPath = 'fd://' - // path路径的码流可通过"hdc file send D:\xxx\H264_AAC.mp4 /data/app/el1/bundle/public/ohos.acts.multimedia.video.videoplayer/ohos.acts.multimedia.video.videoplayer/assets/entry/resources/rawfile" 命令,将其推送到设备上 - let path = '/data/app/el1/bundle/public/ohos.acts.multimedia.video.videoplayer/ohos.acts.multimedia.video.videoplayer/assets/entry/resources/rawfile/H264_AAC.mp4'; - let file = await fs.open(path); - fdPath = fdPath + '' + file.fd; - // 调用createVideoPlayer接口返回videoPlayer实例对象 - await media.createVideoPlayer().then((video) => { - if (typeof (video) != 'undefined') { - console.info('createVideoPlayer success!'); - videoPlayer = video; - } else { - console.info('createVideoPlayer fail!'); - } - }, this.failureCallback).catch(this.catchCallback); - // 设置播放源 - videoPlayer.url = fdPath; - - // 设置surfaceID用于显示视频画面 - await videoPlayer.setDisplaySurface(surfaceID).then(() => { - console.info('setDisplaySurface success'); - }, this.failureCallback).catch(this.catchCallback); - - // 调用prepare完成播放前准备工作 - await videoPlayer.prepare().then(() => { - console.info('prepare success'); - }, this.failureCallback).catch(this.catchCallback); - // 设置循环播放属性 - videoPlayer.loop = true; - // 调用play接口正式开始循环播放 - await videoPlayer.play().then(() => { - console.info('play success, loop value is ' + videoPlayer.loop); - }, this.failureCallback).catch(this.catchCallback); + this.avPlayer.url = fdPath; + } + + // 以下demo为使用资源管理接口获取打包在HAP内的媒体资源文件并通过fdSrc属性进行播放示例 + async avPlayerFdSrcDemo() { + // 创建avPlayer实例对象 + this.avPlayer = await media.createAVPlayer(); + // 创建状态机变化回调函数 + this.setAVPlayerCallback(); + // 通过UIAbilityContext的resourceManager成员的getRawFd接口获取媒体资源播放地址 + // 返回类型为{fd,offset,length},fd为HAP包fd地址,offset为媒体资源偏移量,length为播放长度 + let context = getContext(this) as common.UIAbilityContext; + let fileDescriptor = await context.resourceManager.getRawFd('H264_AAC.mp4'); + // 为fdSrc赋值触发initialized状态机上报 + this.avPlayer.fdSrc = fileDescriptor; } } ``` - -## 相关实例 -针对视频播放开发,有以下相关实例可供参考: - -- [`MediaCollections:`媒体管理合集(ArkTS)(API9)(Full SDK)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/BasicFeature/FileManagement/MediaCollections) -- [视频播放器(ArkTS)(Full SDK)(API9)](https://gitee.com/openharmony/codelabs/tree/master/Media/VideoPlayerStage) \ No newline at end of file diff --git a/zh-cn/application-dev/media/video-recorder.md b/zh-cn/application-dev/media/video-recorder.md deleted file mode 100644 index a1ea9d11fa885806eb5510bb09bef69f942e6beb..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/media/video-recorder.md +++ /dev/null @@ -1,159 +0,0 @@ -# 视频录制开发指导 - -## 简介 - -视频录制的主要工作是捕获音视频信号,完成音视频编码并保存到文件中,帮助开发者轻松实现音视频录制功能,包括开始录制、暂停录制、恢复录制、停止录制、释放资源等功能控制。它允许调用者指定录制的编码格式、封装格式、文件路径等参数。 - -## 运作机制 - -该模块提供了视频录制状态变化示意图和视频录制外部模块交互图。 - -**图1** 视频录制状态变化示意图 - -![zh-ch_image_video_recorder_state_machine](figures/zh-ch_image_video_recorder_state_machine.png) - -**图2** 视频录制外部模块交互图 - -![zh-ch_image_video_recorder_zero](figures/zh-ch_image_video_recorder_zero.png) - -**说明**:三方相机应用或系统相机通过调用JS接口层提供的js接口实现相应功能时,框架层会通过Native Framework的媒体服务,调用音频部件通过音频HDI捕获的音频数据,再通过软件编码输出编码封装后的音频数据保存至文件中,和图形子系统通过视频HDI捕获的图像数据,再通过视频编码HDI编码,将编码后的图像数据保存至文件中,实现视频录制功能。 - -## 约束与限制 - -开发者在进行录制功能开发前,需要先对所开发的应用配置麦克风权限(ohos.permission.MICROPHONE)和相机权限(ohos.permission.CAMERA),权限配置相关内容可参考:[访问控制权限申请指导](../security/accesstoken-guidelines.md) - -## 开发指导 - -详细API含义可参考:[媒体服务API文档VideoRecorder](../reference/apis/js-apis-media.md#videorecorder9) - -### 全流程场景 - -视频录制全流程场景包含:创建实例、设置录制参数、开始录制、暂停录制、恢复录制、停止录制、释放资源等流程。 - -```js -import media from '@ohos.multimedia.media' -import mediaLibrary from '@ohos.multimedia.mediaLibrary' -export class VideoRecorderDemo { - private testFdNumber; // 用于保存fd地址 - // pathName是传入的录制文件名,例如:01.mp4,生成后的文件地址:/storage/media/100/local/files/Video/01.mp4 - // 使用mediaLibrary需要添加以下权限, ohos.permission.MEDIA_LOCATION、ohos.permission.WRITE_MEDIA、ohos.permission.READ_MEDIA - async getFd(pathName) { - let displayName = pathName; - const mediaTest = mediaLibrary.getMediaLibrary(); - let fileKeyObj = mediaLibrary.FileKey; - let mediaType = mediaLibrary.MediaType.VIDEO; - let publicPath = await mediaTest.getPublicDirectory(mediaLibrary.DirectoryType.DIR_VIDEO); - let dataUri = await mediaTest.createAsset(mediaType, displayName, publicPath); - if (dataUri != undefined) { - let args = dataUri.id.toString(); - let fetchOp = { - selections : fileKeyObj.ID + "=?", - selectionArgs : [args], - } - let fetchFileResult = await mediaTest.getFileAssets(fetchOp); - let fileAsset = await fetchFileResult.getAllObject(); - let fdNumber = await fileAsset[0].open('Rw'); - this.testFdNumber = "fd://" + fdNumber.toString(); - } - } - - // 当发生错误上上报的错误回调接口 - failureCallback(error) { - console.info('error happened, error name is ' + error.name); - console.info('error happened, error code is ' + error.code); - console.info('error happened, error message is ' + error.message); - } - - // 当发生异常时,系统调用的错误回调接口 - catchCallback(error) { - console.info('catch error happened, error name is ' + error.name); - console.info('catch error happened, error code is ' + error.code); - console.info('catch error happened, error message is ' + error.message); - } - - async videoRecorderDemo() { - let videoRecorder = null; // videoRecorder空对象在createVideoRecorder成功后赋值 - let surfaceID = null; // 用于保存getInputSurface返回的surfaceID - // 获取需要录制的视频的fd地址 - await this.getFd('01.mp4'); - // 录制相关参数配置,配置参数以实际硬件设备支持的范围为准 - let videoProfile = { - audioBitrate : 48000, - audioChannels : 2, - audioCodec : 'audio/mp4a-latm', - audioSampleRate : 48000, - fileFormat : 'mp4', - videoBitrate : 2000000, - videoCodec : 'video/avc', - videoFrameWidth : 640, - videoFrameHeight : 480, - videoFrameRate : 30 - } - - let videoConfig = { - audioSourceType : 1, - videoSourceType : 0, - profile : videoProfile, - url : this.testFdNumber, // testFdNumber由getFd生成 - orientationHint : 0, - location : { latitude : 30, longitude : 130 } - } - // 创建videoRecorder对象 - await media.createVideoRecorder().then((recorder) => { - console.info('case createVideoRecorder called'); - if (typeof (recorder) != 'undefined') { - videoRecorder = recorder; - console.info('createVideoRecorder success'); - } else { - console.info('createVideoRecorder failed'); - } - }, this.failureCallback).catch(this.catchCallback); - - // 调用prepare完成视频录制前的准备工作 - await videoRecorder.prepare(videoConfig).then(() => { - console.info('prepare success'); - }, this.failureCallback).catch(this.catchCallback); - - // 获取surfaceID并保存下来传递给camera相关接口 - await videoRecorder.getInputSurface().then((surface) => { - console.info('getInputSurface success'); - surfaceID = surface; - }, this.failureCallback).catch(this.catchCallback); - - // 视频录制依赖相机相关接口,以下需要先调用相机起流接口后才能继续执行,具体的相机接口调用请参考sample用例 - // 视频录制启动接口 - await videoRecorder.start().then(() => { - console.info('start success'); - }, this.failureCallback).catch(this.catchCallback); - - // 调用pause接口时需要暂停camera出流 - await videoRecorder.pause().then(() => { - console.info('pause success'); - }, this.failureCallback).catch(this.catchCallback); - - // 调用resume接口时需要恢复camera出流 - await videoRecorder.resume().then(() => { - console.info('resume success'); - }, this.failureCallback).catch(this.catchCallback); - - // 停止camera出流后,停止视频录制 - await videoRecorder.stop().then(() => { - console.info('stop success'); - }, this.failureCallback).catch(this.catchCallback); - - // 重置录制相关配置 - await videoRecorder.reset().then(() => { - console.info('reset success'); - }, this.failureCallback).catch(this.catchCallback); - - // 释放视频录制相关资源并释放camera对象相关资源 - await videoRecorder.release().then(() => { - console.info('release success'); - }, this.failureCallback).catch(this.catchCallback); - - // 相关对象置null - videoRecorder = undefined; - surfaceID = undefined; - } -} -``` \ No newline at end of file diff --git a/zh-cn/application-dev/media/video-recording.md b/zh-cn/application-dev/media/video-recording.md new file mode 100644 index 0000000000000000000000000000000000000000..7cdf749e64ad558962d06fe7d15de88ab0f77b36 --- /dev/null +++ b/zh-cn/application-dev/media/video-recording.md @@ -0,0 +1,230 @@ +# 视频录制 + +在OpenHarmony系统中,当前仅支持AVRecorder开发视频录制,集成了音频捕获,音频编码,视频编码,音视频封装功能,适用于实现简单视频录制并直接得到视频本地文件的场景。 + +本开发指导将以“开始录制-暂停录制-恢复录制-停止录制”的一次流程为示例,向开发者讲解如何使用AVRecorder进行视频录制。 + +在进行应用开发的过程中,开发者可以通过AVRecorder的state属性主动获取当前状态,或使用on('stateChange')方法监听状态变化。开发过程中应该严格遵循状态机要求,例如只能在started状态下调用pause()接口,只能在paused状态下调用resume()接口。 + +**图1** 录制状态变化示意图   + +![Recording status change](figures/video-recording-status-change.png) + +状态的详细说明请参考[AVRecorderState](../reference/apis/js-apis-media.md#avrecorderstate9)。 + +## 开发步骤及注意事项 + +> **说明:** +> +> AVRecorder只负责视频数据的处理,需要与视频数据采集模块配合才能完成视频录制。视频数据采集模块需要通过Surface将视频数据传递给AVRecorder进行数据处理。当前常用的数据采集模块为相机模块,相机模块目前仅对系统应用开放,具体请参考[相机模块](../reference/apis/js-apis-camera.md)。 + +AVRecorder详细的API说明请参考[AVRecorder API参考](../reference/apis/js-apis-media.md#avrecorder9)。 + +1. 创建AVRecorder实例,实例创建完成进入idle状态。 + + ```ts + import media from '@ohos.multimedia.media' + let avRecorder + media.createAVRecorder().then((recorder) => { + avRecorder = recorder + }, (error) => { + console.error('createAVRecorder failed') + }) + ``` + +2. 设置业务需要的监听事件,监听状态变化及错误上报。 + | 事件类型 | 说明 | + | -------- | -------- | + | stateChange | 必要事件,监听播放器的state属性改变 | + | error | 必要事件,监听播放器的错误信息 | + + ```ts + // 状态上报回调函数 + avRecorder.on('stateChange', (state, reason) => { + console.info('current state is: ' + state); + }) + // 错误上报回调函数 + avRecorder.on('error', (err) => { + console.error('error happened, error message is ' + err); + }) + ``` + +3. 配置视频录制参数,调用prepare()接口,此时进入prepared状态。 + > **说明:** + > + > 配置参数需要注意: + > + > - prepare接口的入参avConfig中仅设置视频相关的配置参数,如示例代码所示。 + > 如果添加了音频参数,系统将认为是“音频+视频录制”。 + > + > - 需要使用支持的[录制规格](avplayer-avrecorder-overview.md#支持的格式),视频比特率、分辨率、帧率以实际硬件设备支持的范围为准。 + > + > - 录制输出的url地址(即示例里avConfig中的url),形式为fd://xx (fd number)。需要调用基础文件操作接口([ohos.file.fs](../reference/apis/js-apis-file-fs.md))实现应用文件访问能力,获取方式参考[应用文件访问与管理](../file-management/app-file-access.md)。 + + ```ts + let avProfile = { + fileFormat : media.ContainerFormatType.CFT_MPEG_4, // 视频文件封装格式,只支持MP4 + videoBitrate : 200000, // 视频比特率 + videoCodec : media.CodecMimeType.VIDEO_AVC, // 视频文件编码格式,支持mpeg4和avc两种格式 + videoFrameWidth : 640, // 视频分辨率的宽 + videoFrameHeight : 480, // 视频分辨率的高 + videoFrameRate : 30 // 视频帧率 + } + let avConfig = { + videoSourceType : media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV, // 视频源类型,支持YUV和ES两种格式 + profile : this.avProfile, + url : 'fd://35', // 参考应用文件访问与管理开发示例新建并读写一个文件 + rotation : 0, // 视频旋转角度,默认为0不旋转,支持的值为0、90、180、270 + } + avRecorder.prepare(avConfig).then(() => { + console.info('avRecorder prepare success') + }, (error) => { + console.error('avRecorder prepare failed') + }) + ``` + +4. 获取视频录制需要的SurfaceID。 + 调用getInputSurface()接口,接口的返回值SurfaceID用于传递给视频数据输入源模块。常用的输入源模块为相机,以下示例代码中,采用相机作为视频输入源为例。 + + 输入源模块通过SurfaceID可以获取到Surface,通过Surface可以将视频数据流传递给AVRecorder,由AVRecorder再进行视频数据的处理。 + + ```ts + avRecorder.getInputSurface().then((surfaceId) => { + console.info('avRecorder getInputSurface success') + }, (error) => { + console.error('avRecorder getInputSurface failed') + }) + ``` + +5. 初始化视频数据输入源。该步骤需要在输入源模块完成,以相机为例,需要创建录像输出流,包括创建Camera对象、获取相机列表、创建相机输入流等,相机详细步骤请参考[相机-录像实现方案](camera-recording-case.md)。 + +6. 开始录制,启动输入源输入视频数据,例如相机模块调用camera.VideoOutput.start接口启动相机录制。然后调用AVRecorder.start()接口,此时AVRecorder进入started状态。 + +7. 暂停录制,调用pause()接口,此时AVRecorder进入paused状态,同时暂停输入源输入数据。例如相机模块调用camera.VideoOutput.stop停止相机视频数据输入。 + +8. 恢复录制,调用resume()接口,此时再次进入started状态。 + +9. 停止录制,调用stop()接口,此时进入stopped状态,同时停止相机录制。 + +10. 重置资源,调用reset()重新进入idle状态,允许重新配置录制参数。 + +11. 销毁实例,调用release()进入released状态,退出录制,释放视频数据输入源相关资源,例如相机资源。 + + +## 完整示例 + +参考以下示例,完成“开始录制-暂停录制-恢复录制-停止录制”的完整流程。 + + +```ts +import media from '@ohos.multimedia.media' +const TAG = 'VideoRecorderDemo:' +export class VideoRecorderDemo { + private avRecorder; + private videoOutSurfaceId; + private avProfile = { + fileFormat : media.ContainerFormatType.CFT_MPEG_4, // 视频文件封装格式,只支持MP4 + videoBitrate : 100000, // 视频比特率 + videoCodec : media.CodecMimeType.VIDEO_AVC, // 视频文件编码格式,支持mpeg4和avc两种格式 + videoFrameWidth : 640, // 视频分辨率的宽 + videoFrameHeight : 480, // 视频分辨率的高 + videoFrameRate : 30 // 视频帧率 + } + private avConfig = { + videoSourceType : media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV, // 视频源类型,支持YUV和ES两种格式 + profile : this.avProfile, + url : 'fd://35', // 参考应用文件访问与管理开发示例新建并读写一个文件 + rotation : 0, // 视频旋转角度,默认为0不旋转,支持的值为0、90、180、270 + } + + // 注册avRecorder回调函数 + setAvRecorderCallback() { + // 状态机变化回调函数 + this.avRecorder.on('stateChange', (state, reason) => { + console.info(TAG + 'current state is: ' + state); + }) + // 错误上报回调函数 + this.avRecorder.on('error', (err) => { + console.error(TAG + 'error ocConstantSourceNode, error message is ' + err); + }) + } + + // 相机相关准备工作 + async prepareCamera() { + // 具体实现查看相机资料 + } + + // 启动相机出流 + async startCameraOutput() { + // 调用VideoOutput的start接口开始录像输出 + } + + // 停止相机出流 + async stopCameraOutput() { + // 调用VideoOutput的stop接口停止录像输出 + } + + // 释放相机实例 + async releaseCamera() { + // 释放相机准备阶段创建出的实例 + } + + // 开始录制对应的流程 + async startRecordingProcess() { + // 1.创建录制实例; + this.avRecorder = await media.createAVRecorder(); + this.setAvRecorderCallback(); + // 2. 获取录制文件fd;获取到的值传递给avConfig里的url,实现略 + // 3.配置录制参数完成准备工作 + await this.avRecorder.prepare(this.avConfig); + this.videoOutSurfaceId = await this.avRecorder.getInputSurface(); + // 4.完成相机相关准备工作 + await this.prepareCamera(); + // 5.启动相机出流 + await this.startCameraOutput(); + // 6. 启动录制 + await this.videoRecorder.start(); + } + + // 暂停录制对应的流程 + async pauseRecordingProcess() { + if (this.avRecorder.state === 'started') { // 仅在started状态下调用pause为合理状态切换 + await this.avRecorder.pause(); + await this.stopCameraOutput(); // 停止相机出流 + } + } + + // 恢复录制对应的流程 + async resumeRecordingProcess() { + if (this.avRecorder.state === 'paused') { // 仅在paused状态下调用resume为合理状态切换 + await this.startCameraOutput(); // 启动相机出流 + await this.avRecorder.resume(); + } + } + + async stopRecordingProcess() { + // 1. 停止录制 + if (this.avRecorder.state === 'started' + || this.avRecorder.state === 'paused' ) { // 仅在started或者paused状态下调用stop为合理状态切换 + await this.avRecorder.stop(); + await this.stopCameraOutput(); + } + // 2.重置 + await this.avRecorder.reset(); + // 3.释放录制实例 + await this.avRecorder.release(); + // 4.文件录制完成后,关闭fd,实现略 + // 5.释放相机相关实例 + await this.releaseCamera(); + } + + // 一个完整的【开始录制-暂停录制-恢复录制-停止录制】示例 + async videoRecorderDemo() { + await this.startRecordingProcess(); // 开始录制 + // 用户此处可以自行设置录制时长,例如通过设置休眠阻止代码执行 + await this.pauseRecordingProcess(); //暂停录制 + await this.resumeRecordingProcess(); // 恢复录制 + await this.stopRecordingProcess(); // 停止录制 + } +} +``` diff --git a/zh-cn/application-dev/media/volume-management.md b/zh-cn/application-dev/media/volume-management.md new file mode 100644 index 0000000000000000000000000000000000000000..354ad9954fb6aa66d5a05705b2aa1c5011ae9878 --- /dev/null +++ b/zh-cn/application-dev/media/volume-management.md @@ -0,0 +1,48 @@ +# 播放音量管理 + +播放音量的管理主要包括对系统音量的管理和对音频流音量的管理。系统音量与音频流音量分别是指OpenHarmony系统的总音量和指定音频流的音量,其中音频流音量的大小受制于系统音量,管理两者的接口不同。 + +## 系统音量 + +管理系统音量的接口是AudioVolumeManager,在使用之前,需要使用getVolumeManager()获取AudioVolumeManager实例。目前该接口只能获取音量信息及监听音量变化,不能主动调节系统音量。 + +```ts +import audio from '@ohos.multimedia.audio'; +let audioManager = audio.getAudioManager(); +let audioVolumeManager = audioManager.getVolumeManager(); +``` + +### 监听系统音量变化 + +通过设置监听事件,可以监听系统音量的变化: + +```ts +audioVolumeManager.on('volumeChange', (volumeEvent) => { + console.info(`VolumeType of stream: ${volumeEvent.volumeType} `); + console.info(`Volume level: ${volumeEvent.volume} `); + console.info(`Whether to updateUI: ${volumeEvent.updateUi} `); +}); +``` + +### 调节系统音量(仅对系统应用开放) + +目前调节系统音量主要是靠SystemAPI,具体服务于物理音量按键和设置。通过音量按键可以调节系统音量的大小,根据按下的具体按键调用系统接口,实现系统音量的大小调节。调节的音量类型包括媒体、铃声和通知。 + +## 音频流音量 + +管理音频流音量的接口是AVPlayer或AudioRenderer的setVolume()方法,使用AVPlayer设置音频流音量的示例代码如下: + +```ts +let volume = 1.0 // 指定的音量大小,取值范围为[0.00-1.00],1表示最大音量 +avPlayer.setVolume(volume) +``` + +使用AudioRenderer设置音频流音量的示例代码如下: + +```ts +audioRenderer.setVolume(0.5).then(data=>{ // 音量范围为[0.0-1.0] + console.info('Invoke setVolume succeeded.'); +}).catch((err) => { + console.error(`Invoke setVolume failed, code is ${err.code}, message is ${err.message}`); +}); +``` diff --git a/zh-cn/application-dev/quick-start/Readme-CN.md b/zh-cn/application-dev/quick-start/Readme-CN.md index 7447243c41ecf4b6f0c3a73c18f51941802d0ece..da18cd93dd0b2882229adbfd0bfc84d91b0e4945 100755 --- a/zh-cn/application-dev/quick-start/Readme-CN.md +++ b/zh-cn/application-dev/quick-start/Readme-CN.md @@ -43,12 +43,37 @@ - [资源分类与访问](resource-categories-and-access.md) - 学习ArkTS语言 - [初识ArkTS语言](arkts-get-started.md) - - ArkTS语法(声明式UI) - - [基本UI描述](arkts-basic-ui-description.md) - - 状态管理 - - [基本概念](arkts-state-mgmt-concepts.md) - - [页面级变量的状态管理](arkts-state-mgmt-page-level.md) - - [应用级变量的状态管理](arkts-state-mgmt-application-level.md) - - [动态构建UI元素](arkts-dynamic-ui-elememt-building.md) - - [渲染控制](arkts-rendering-control.md) - - [使用限制与扩展](arkts-restrictions-and-extensions.md) \ No newline at end of file + - 基本语法 + - [基本语法概述](arkts-basic-syntax-overview.md) + - [声明式UI描述](arkts-declarative-ui-description.md) + - 自定义组件 + - [创建自定义组件](arkts-create-custom-components.md) + - [页面和自定义组件生命周期](arkts-page-custom-components-lifecycle.md) + - [\@Builder:自定义构建函数](arkts-builder.md) + - [\@BuilderParam:引用\@Builder函数](arkts-builderparam.md) + - [\@Styles:定义组件重用样式](arkts-style.md) + - [\@Extend:定义扩展组件样式](arkts-extend.md) + - [stateStyles:多态样式](arkts-statestyles.md) + - 状态管理 + - [状态管理概述](arkts-state-management-overview.md) + - 管理组件拥有的状态 + - [\@State:组件内状态](arkts-state.md) + - [\@Prop:父子单向同步](arkts-prop.md) + - [\@Link:父子双向同步](arkts-link.md) + - [\@Provide和\@Consume:与后代组件双向同步](arkts-provide-and-consume.md) + - [\@Observed和\@ObjectLink:嵌套类对象属性变化](arkts-observed-and-objectlink.md) + - 管理应用拥有的状态 + - [管理应用拥有的状态概述](arkts-application-state-management-overview.md) + - [LocalStorage:页面级UI状态存储](arkts-localstorage.md) + - [AppStorage:应用全局的UI状态存储](arkts-appstorage.md) + - [PersistentStorage:持久化存储UI状态](arkts-persiststorage.md) + - [Environment:设备环境查询](arkts-environment.md) + - 其他状态管理 + - [其他状态管理概述](arkts-other-state-mgmt-functions-overview.md) + - [\@Watch:状态变量更改通知](arkts-watch.md) + - [$$语法:内置组件双向同步](arkts-two-way-sync.md) + - 渲染控制 + - [渲染控制概述](arkts-rendering-control-overview.md) + - [if/else:条件渲染](arkts-rendering-control-ifelse.md) + - [ForEach:循环渲染](arkts-rendering-control-foreach.md) + - [LazyForEach:数据懒加载](arkts-rendering-control-lazyforeach.md) diff --git a/zh-cn/application-dev/quick-start/arkts-application-state-management-overview.md b/zh-cn/application-dev/quick-start/arkts-application-state-management-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..f3bb0e45598c3aa59ae20b521eec25ba5cff8375 --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-application-state-management-overview.md @@ -0,0 +1,13 @@ +# 管理应用拥有的状态概述 + + +上一个章节中介绍的装饰器仅能在页面内,即一个组件树上共享状态变量。如果开发者要实现应用级的,或者多个页面的状态数据共享,就需要用到应用级别的状态管理的概念。ArkTS根据不同特性,提供了多种应用状态管理的能力: + + +- [LocalStorage](arkts-localstorage.md):页面级UI状态存储,通常用于[UIAbility](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/apis/js-apis-app-ability-uiAbility.md)内、页面间的状态共享。 + +- [AppStorage](arkts-appstorage.md):特殊的单例LocalStorage对象,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储; + +- [PersistentStorage](arkts-persiststorage.md):持久化存储UI状态,通常和AppStorage配合使用,选择AppStorage存储的数据写入磁盘,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同; + +- [Environment](arkts-environment.md):应用程序运行的设备的环境参数,环境参数会同步到AppStorage中,可以和AppStorage搭配使用。 diff --git a/zh-cn/application-dev/quick-start/arkts-appstorage.md b/zh-cn/application-dev/quick-start/arkts-appstorage.md new file mode 100644 index 0000000000000000000000000000000000000000..74f7f100bf2749ba34e789c16e443f53db61e587 --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-appstorage.md @@ -0,0 +1,204 @@ +# AppStorage:应用全局的UI状态存储 + + +AppStorage是应用全局的UI状态存储,是和应用的进程绑定的,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储。 + + +和LocalStorage不同的是,LocalStorage是页面级的,通常应用于页面内的数据共享。而对于AppStorage,是应用级的全局状态共享。AppStorage还相当于整个应用的“中枢”,[持久化数据PersistentStorage](arkts-persiststorage.md)和[环境变量Environment](arkts-environment.md)都是通过和AppStorage中转,才可以和UI回交互。 + + +本文仅介绍AppStorage使用场景和相关的装饰器:\@StorageProp和\@StorageLink。 + + +## 概述 + +AppStorage是LocalStorage是在应用启动的时候会被创建的单例。它的目的是为了提供应用状态数据的中心存储,这些状态数据在应用级别都是可访问的。AppStorage将在应用运行过程保留其属性。属性通过唯一的键字符串值访问。 + +AppStorage可以和UI组件同步,且可以在应用业务逻辑中被访问。 + +AppStorage中的属性可以被双向同步,数据可以是存在于本地或远程设备上,并具有不同的功能,比如数据持久化(详见[PersistentStorage](arkts-persiststorage.md))。这些数据是通过业务逻辑中实现,与UI解耦,如果希望这些数据在UI中使用,需要用到[@StorageProp](#storageprop)和[@StorageLink](#storagelink)。 + + +## \@StorageProp + +在上文中已经提到,如果要建立AppStorage和自定义组件的联系,需要使用\@StorageProp和\@StorageLink装饰器。使用\@StorageProp(key)/\@StorageLink(key)装饰组件内的变量,key标识了AppStorage的属性。 + +当自定义组件初始化的时候,\@StorageProp(key)/\@StorageLink(key)装饰的变量会通过给定的key,绑定在AppStorage对应是属性,完成初始化。本地初始化是必要的,因为无法保证AppStorage一定存在给定的key,这取决于应用逻辑,是否在组件初始化之前在AppStorage实例中存入对应的属性。 + + +\@StorageProp(key)是和AppStorage中key对应的属性建立单向数据同步,我们允许本地改变的发生,但是对于\@StorageProp,本地的修改永远不会同步回AppStorage中,相反,如果AppStorage给定key的属性发生改变,改变会被同步给\@StorageProp,并覆盖掉本地的修改。 + + +### 装饰器使用规则说明 + +| \@StorageProp变量装饰器 | 说明 | +| ------------------ | ---------------------------------------- | +| 装饰器参数 | key:常量字符串,必填(字符串需要有引号)。 | +| 允许装饰的变量类型 | Object class、string、number、boolean、enum类型,以及这些类型的数组。嵌套类型的场景请参考[观察变化和行为表现](#观察变化和行为表现)。
类型必须被指定,且必须和LocalStorage中对应属性相同。不支持any,不允许使用undefined和null。 | +| 同步类型 | 单向同步:从AppStorage的对应属性到组件的状态变量。
组件本地的修改是允许的,但是AppStorage中给定的属性一旦发生变化,将覆盖本地的修改。 | +| 被装饰变量的初始值 | 必须指定,如果AppStorage实例中不存在属性,则作为初始化默认值,并存入AppStorage中。 | + + +### 变量的传递/访问规则说明 + +| 传递/访问 | 说明 | +| ---------- | ---------------------------------------- | +| 从父节点初始化和更新 | 禁止,\@StorageProp不支持从父节点初始化,只能AppStorage中key对应的属性初始化,如果没有对应key的话,将使用本地默认值初始化 | +| 初始化子节点 | 支持,可用于初始化\@State、\@Link、\@Prop、\@Provide。 | +| 是否支持组件外访问 | 否。 | + + + **图1** \@StorageProp初始化规则图示   + + +![zh-cn_image_0000001552978157](figures/zh-cn_image_0000001552978157.png) + + +### 观察变化和行为表现 + +**观察变化** + + +- 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。 + +- 当装饰的数据类型为class或者Object时,可以观察到赋值和属性赋值的变化,即Object.keys(observedObject)返回的所有属性。 + +- 当装饰的对象是array时,可以观察到数组添加、删除、更新数组单元的变化。 + + +**框架行为** + + +- 当\@StorageProp(key)装饰的数值改变被观察到时,修改不会被同步回AppStorage对应属性键值key的属性中。 + +- 当前\@StorageProp(key)单向绑定的数据会被修改,即仅限于当前组件的私有成员变量改变,其他的绑定该key的数据不会同步改变。 + +- 当\@StorageProp(key)装饰的数据本身是状态变量,它的改变虽然不会同步回AppStorage中,但是会引起所属的自定义组件的重新渲染。 + +- 当AppStorage中key对应的属性发生改变时,会同步给所有\@StorageProp(key)装饰的数据,\@StorageProp(key)本地的修改将被覆盖。 + + +## \@StorageLink + +\@StorageLink(key)是和AppStorage中key对应的属性建立双向数据同步: + +1. 本地修改发生,该修改会被回AppStorage中; + +2. AppStorage中的修改发生后,该修改会被同步到所有绑定AppStorage对应key的属性上,包括单向(\@StorageProp和通过Prop创建的单向绑定变量)、双向(\@StorageLink和通过Link创建的双向绑定变量)变量和其他实例(比如PersistentStorage)。 + + +### 装饰器使用规则说明 + +| \@StorageLink变量装饰器 | 说明 | +| ------------------ | ---------------------------------------- | +| 装饰器参数 | key:常量字符串,必填(字符串需要有引号)。 | +| 允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。嵌套类型的场景请参考[观察变化和行为表现](#观察变化和行为表现)。
类型必须被指定,且必须和AppStorage中对应属性相同。不支持any,不允许使用undefined和null。 | +| 同步类型 | 双向同步:从AppStorage的对应属性到自定义组件,从自定义组件到AppStorage对应属性。 | +| 被装饰变量的初始值 | 必须指定,如果AppStorage实例中不存在属性,则作为初始化默认值,并存入AppStorage中。 | + + +### 变量的传递/访问规则说明 + +| 传递/访问 | 说明 | +| ---------- | ---------------------------------------- | +| 从父节点初始化和更新 | 禁止。 | +| 初始化子节点 | 支持,可用于初始化常规变量、\@State、\@Link、\@Prop、\@Provide。 | +| 是否支持组件外访问 | 否。 | + + + **图2** \@StorageLink初始化规则图示   + + +![zh-cn_image_0000001501938718](figures/zh-cn_image_0000001501938718.png) + + +### 观察变化和行为表现 + +**观察变化** + + +- 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。 + +- 当装饰的数据类型为class或者Object时,可以观察到赋值和属性赋值的变化,即Object.keys(observedObject)返回的所有属性。 + +- 当装饰的对象是array时,可以观察到数组添加、删除、更新数组单元的变化。 + + +**框架行为** + + +1. 当\@StorageLink(key)装饰的数值改变被观察到时,修改将被同步回AppStorage对应属性键值key的属性中。 + +2. AppStorage中属性键值key对应的数据一旦改变,属性键值key绑定的所有的数据(包括双向\@StorageLink和单向\@StorageProp)都将同步修改; + +3. 当\@StorageLink(key)装饰的数据本身是状态变量,它的改变不仅仅会同步回AppStorage中,还会引起所属的自定义组件的重新渲染。 + + +## 使用场景 + + +### 从应用逻辑使用AppStorage和LocalStorage + +AppStorage是单例,它的所有API都是静态的,使用方法类似于中LocalStorage对应的非静态方法。 + + +```ts +AppStorage.SetOrCreate('PropA', 47); + +let storage: LocalStorage = new LocalStorage({ 'PropA': 17 }); +let propA: number = AppStorage.Get('PropA') // propA in AppStorage == 47, propA in LocalStorage == 17 +var link1: SubscribedAbstractProperty = AppStorage.Link('PropA'); // link1.get() == 47 +var link2: SubscribedAbstractProperty = AppStorage.Link('PropA'); // link2.get() == 47 +var prop: SubscribedAbstractProperty = AppStorage.Prop('PropA'); // prop.get() = 47 + +link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48 +prop.set(1); // one-way sync: prop.get()=1; but link1.get() == link2.get() == 48 +link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49 + +storage.get('PropA') // == 17 +storage.set('PropA', 101); +storage.get('PropA') // == 101 + +AppStorage.Get('PropA') // == 49 +link1.get() // == 49 +link2.get() // == 49 +prop.get() // == 49 +``` + + +### 从UI内部使用AppStorage和LocalStorage + +\@StorageLink变量装饰器与AppStorage配合使用,正如\@LocalStorageLink与LocalStorage配合使用一样。此装饰器使用AppStorage中的属性创建双向数据同步。 + + +```ts +AppStorage.SetOrCreate('PropA', 47); +let storage = new LocalStorage({ 'PropA': 48 }); + +@Entry(storage) +@Component +struct CompA { + @StorageLink('PropA') storLink: number = 1; + @LocalStorageLink('PropA') localStorLink: number = 1; + + build() { + Column({ space: 20 }) { + Text(`From AppStorage ${this.storLink}`) + .onClick(() => this.storLink += 1) + + Text(`From LocalStorage ${this.localStorLink}`) + .onClick(() => this.localStorLink += 1) + } + } +} +``` + + +## 限制条件 + +AppStorage与[PersistentStorage](arkts-persiststorage.md)以及[Environment](arkts-environment.md)配合使用时,需要注意以下几点: + +- 在AppStorage中创建属性后,调用PersistentStorage.PersistProp()接口时,会使用在AppStorage中已经存在的值,并覆盖PersistentStorage中的同名属性,所以建议要使用相反的调用顺序,反例可见[在PersistentStorage之前访问AppStorage中的属性](arkts-persiststorage.md#在persistentstorage之前访问appstorage中的属性); + +- 如果在AppStorage中已经创建属性后,再调用Environment.EnvProp()创建同名的属性,会调用失败。因为AppStorage已经有同名属性,Environment环境变量不会再写入AppStorage中,所以建议AppStorage中属性不要使用Environment预置环境变量名。 + diff --git a/zh-cn/application-dev/quick-start/arkts-basic-syntax-overview.md b/zh-cn/application-dev/quick-start/arkts-basic-syntax-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..7a7404987fb97f7e471576b801b266be96688815 --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-basic-syntax-overview.md @@ -0,0 +1,40 @@ +# 基本语法概述 + + +在初步了解了ArkTS语言之后,我们以一个具体的示例来说明ArkTS的基本组成。如下图所示,当开发者点击按钮时,文本内容从“Hello World”变为“Hello ArkUI”。 + + + **图1** 示例效果图   + +![Video_2023-03-06_152548](figures/Video_2023-03-06_152548.gif) + + +本示例中,ArkTS的基本组成如下所示。 + + + **图2** ArkTS的基本组成   + +![arkts-basic-grammar](figures/arkts-basic-grammar.png) + + +- 装饰器: 用于装饰类、结构、方法以及变量,并赋予其特殊的含义。如上述示例中\@Entry、\@Component和\@State都是装饰器,\@Component表示自定义组件,\@Entry表示该自定义组件为入口组件,\@State表示组件中的状态变量,状态变量变化会触发UI刷新。 + +- [UI描述](arkts-declarative-ui-description.md):以声明式的方式来描述UI的结构,例如build()方法中的代码块。 + +- [自定义组件](arkts-create-custom-components.md):可复用的UI单元,可组合其他组件,如上述被\@Component装饰的struct Hello。 + +- 系统组件:ArkUI框架中默认内置的基础和容器组件,可直接被开发者调用,比如示例中的Column 、 Text 、 Divider 、 Button。 + +- 属性方法:组件可以通过链式调用配置多项属性,如fontSize()、width()、height()、color()等。 + +- 事件方法:组件可以通过链式调用设置多个事件的响应逻辑,如跟随在Button后面的onClick()。 + + +除此之外,ArkTS扩展了多种语法范式来使开发更加便捷: + + +- [@Builder](arkts-builder.md)/[@BuilderParam](arkts-builderparam.md):特殊的封装UI描述的方法,细粒度的封装和复用UI描述。 + +- [@Extend](arkts-extend.md)/[@Style](arkts-style.md):扩展内置组件和封装属性样式,更灵活地组合内置组件。 + +- [stateStyles](arkts-statestyles.md):多态样式,可以依据组件的内部状态的不同,设置不同样式。 diff --git a/zh-cn/application-dev/quick-start/arkts-basic-ui-description.md b/zh-cn/application-dev/quick-start/arkts-basic-ui-description.md deleted file mode 100644 index bf975b96765f0b4b0ab94ac3c80dda0aa320eb73..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/quick-start/arkts-basic-ui-description.md +++ /dev/null @@ -1,203 +0,0 @@ -# 基本UI描述 - -ArkTS通过装饰器@Component和@Entry装饰struct关键字声明的数据结构,构成一个自定义组件。自定义组件中提供了一个build函数,开发者需在该函数内以链式调用的方式进行基本的UI描述,UI描述的方法请参考[UI描述规范](#ui描述规范)。 - -## 基本概念 - -- struct:自定义组件可以基于struct实现,不能有继承关系,对于struct的实例化,可以省略new。 - -- 装饰器:装饰器给被装饰的对象赋予某一种能力,其不仅可以装饰类或结构体,还可以装饰类的属性。多个装饰器可以叠加到目标元素上,定义在同一行中或者分开多行,推荐分开多行定义。 - - ```ts - @Entry - @Component - struct MyComponent { - } - ``` - -- build函数:自定义组件必须定义build函数,并且禁止自定义构造函数。build函数满足Builder构造器接口定义,用于定义组件的声明式UI描述。 - - ```ts - interface Builder { - build: () => void - } - ``` - -- @Component:装饰struct,结构体在装饰后具有基于组件的能力,需要实现build方法来创建UI。 - -- @Entry: 装饰struct,组件被装饰后作为页面的入口,页面加载时将被渲染显示。 - -- @Preview:装饰struct, 用@Preview装饰的自定义组件可以在DevEco Studio的预览器上进行实时预览,加载页面时,将创建并显示@Preview装饰的自定义组件。 - - > **说明:** 在单个源文件中,最多可以使用10个@Preview装饰自定义组件,更多说明请参考[查看ArkTS组件预览效果](https://developer.harmonyos.com/cn/docs/documentation/doc-guides/ohos-previewing-app-service-0000001218760596#section146052489820)。 - -- 链式调用:以 "." 链式调用的方式配置UI组件的属性方法、事件方法等。 - -## UI描述规范 - -### 无参数构造配置 - -如果组件的接口定义中不包含必选构造参数,组件后面的“()”中不需要配置任何内容。例如,Divider组件不包含构造参数: - -```ts -Column() { - Text('item 1') - Divider() - Text('item 2') -} -``` - -### 有参数构造配置 - -如果组件的接口定义中包含构造参数,则在组件后面的“()”中可配置相应参数,参数可以使用常量进行赋值。 - -例如: - -- Image组件的必选参数src: - - ```ts - Image('https://xyz/test.jpg') - ``` - -- Text组件的参数content,该参数非必选,即配置或不配置均可: - - ```ts - Text('test') - ``` - -变量或表达式也可以用于参数赋值,其中表达式返回的结果类型必须满足参数类型要求,变量的定义详见[页面级变量的状态管理](arkts-state-mgmt-page-level.md)与[应用级变量的状态管理](arkts-state-mgmt-application-level.md)。例如,设置变量或表达式来构造Image和Text组件的参数: - -```ts -Image(this.imagePath) -Image('https://' + this.imageUrl) -Text(`count: ${this.count}`) -``` - -### 属性配置 - -使用属性方法配置组件的属性,属性方法紧随组件,并用"."运算符连接。 - -- 配置Text组件的字体大小属性: - - ```ts - Text('test') - .fontSize(12) - ``` - -- 使用"."运算符进行链式调用并同时配置组件的多个属性,如下所示: - - ```ts - Image('test.jpg') - .alt('error.jpg') - .width(100) - .height(100) - ``` - -- 除了直接传递常量参数外,还可以传递变量或表达式,如下所示: - - ```ts - Text('hello') - .fontSize(this.size) - Image('test.jpg') - .width(this.count % 2 === 0 ? 100 : 200) - .height(this.offset + 100) - ``` - -- 对于系统内置组件,框架还为其属性预定义了一些[枚举类型](../reference/arkui-ts/ts-appendix-enums.md)供开发人员调用,枚举类型可以作为参数传递,且必须满足参数类型要求。例如,可以按以下方式配置Text组件的颜色和字体属性: - - ```ts - Text('hello') - .fontSize(20) - .fontColor(Color.Red) - .fontWeight(FontWeight.Bold) - ``` - -### 事件配置 - -通过事件方法可以配置组件支持的事件,事件方法紧随组件,并用"."运算符连接。 - -- 使用lambda表达式配置组件的事件方法: - - ```ts - Button('add counter') - .onClick(() => { - this.counter += 2; - }) - ``` - -- 使用匿名函数表达式配置组件的事件方法,要求使用bind,以确保函数体中的this引用包含的组件: - - ```ts - Button('add counter') - .onClick(function () { - this.counter += 2; - }.bind(this)) - ``` - -- 使用组件的成员函数配置组件的事件方法: - - ```ts - myClickHandler(): void { - this.counter += 2; - } - - ... - - Button('add counter') - .onClick(this.myClickHandler.bind(this)) - ``` - -### 子组件配置 - -对于支持子组件配置的组件,例如容器组件,在"{ ... }"里为组件添加子组件的UI描述。Column、Row、Stack、Grid、List等组件都是容器组件。 - -- 以下是简单的Column示例: - - ```ts - Column() { - Text('Hello') - .fontSize(100) - Divider() - Text(this.myText) - .fontSize(100) - .fontColor(Color.Red) - } - ``` - -- 容器组件之间也可以互相嵌套,实现相对复杂的多级嵌套效果: - - ```ts - Column() { - Row() { - Image('test1.jpg') - .width(100) - .height(100) - Button('click +1') - .onClick(() => { - console.info('+1 clicked!'); - }) - } - - Divider() - Row() { - Image('test2.jpg') - .width(100) - .height(100) - Button('click +2') - .onClick(() => { - console.info('+2 clicked!'); - }) - } - - Divider() - Row() { - Image('test3.jpg') - .width(100) - .height(100) - Button('click +3') - .onClick(() => { - console.info('+3 clicked!'); - }) - } - } - ``` \ No newline at end of file diff --git a/zh-cn/application-dev/quick-start/arkts-builder.md b/zh-cn/application-dev/quick-start/arkts-builder.md new file mode 100644 index 0000000000000000000000000000000000000000..0de195ec6e80014163e1a76ca973e6563aea79ce --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-builder.md @@ -0,0 +1,130 @@ +# \@Builder:自定义构建函数 + + +前面章节介绍了如何创建一个自定义组件。该自定义组件内部UI结构固定,仅与使用方进行数据传递。ArkUI还提供了一种更轻量的UI元素复用机制\@Builder,\@Builder所装饰的函数遵循build()函数语法规则,开发者可以将重复使用的UI元素抽象成一个方法,在build方法里调用。 + + +为了简化语言,我们将\@Builder装饰的函数也称为“自定义构建函数”。 + + +> **说明:** +> +> 从API version 9开始,该装饰器支持在ArkTS卡片中使用。 + + +## 装饰器使用说明 + + +### 自定义组件内自定义构建函数 + +定义的语法: + + +```ts +@Builder myBuilderFunction({ ... }) +``` + +使用方法: + + +```ts +this.myBuilderFunction({ ... }) +``` + +- 允许在自定义组件内定义一个或多个自定义构建函数,该函数被认为是该组件的私有、特殊类型的成员函数。 + +- 自定义构建函数可以在所属组件的build方法和其他自定义构建函数中调用,但不允许在组件外调用。 + +- 在自定义函数体中,this指代当前所属组件,组件的状态变量可以在自定义构建函数内访问。建议通过this访问自定义组件的状态变量而不是参数传递。 + + +### 全局自定义构建函数 + +定义的语法: + + +```ts +@Builder function MyGlobalBuilderFunction({ ... }) +``` + +使用方法: + + +```ts +MyGlobalBuilderFunction() +``` + + +- 全局的自定义构建函数可以被整个应用获取,不允许使用this和bind方法。 + +- 如果不涉及组件状态变化,建议使用全局的自定义构建方法。 + + +## 参数传递规则 + +自定义构建函数的参数传递有[按值传递](#按值传递参数)和[按引用传递](#按引用传递参数)两种,均需遵守以下规则: + +- 参数的类型必须与参数声明的类型一致,不允许undefined、null和返回undefined、null的表达式。 + +- 在自定义构建函数内部,不允许改变参数值。如果需要改变参数值,且同步回调用点,建议使用[@Link](arkts-link.md)。 + +- \@Builder内UI语法遵循[UI语法规则](arkts-create-custom-components.md#build函数)。 + + +### 按引用传递参数 + +按引用传递参数时,传递的参数可为状态变量,且状态变量的改变会引起\@Builder方法内的UI刷新。ArkUI提供$$作为按引用传递参数的范式。 + + +```ts +ABuilder( $$ : { paramA1: string, paramB1 : string } ); +``` + + + +```ts +@Builder function ABuilder($$: { paramA1: string }) { + Row() { + Text(`UseStateVarByReference: ${$$.paramA1} `) + } +} +@Entry +@Component +struct Parent { + @State label: string = 'Hello'; + build() { + Column() { + // 在Parent组件中调用ABuilder的时候,将this.label引用传递给ABuilder + ABuilder({ paramA1: this.label }) + Button('Click me').onClick(() => { + // 点击“Click me”后,UI从“Hello”刷新为“ArkUI” + this.label = 'ArkUI'; + }) + } + } +} +``` + + +### 按值传递参数 + +调用\@Builder装饰的函数默认按值传递。当传递的参数为状态变量时,状态变量的改变不会引起\@Builder方法内的UI刷新。所以当使用状态变量的时候,推荐使用[按引用传递](#按引用传递参数)。 + + +```ts +@Builder function ABuilder(paramA1: string) { + Row() { + Text(`UseStateVarByValue: ${paramA1} `) + } +} +@Entry +@Component +struct Parent { + label: string = 'Hello'; + build() { + Column() { + ABuilder(this.label) + } + } +} +``` diff --git a/zh-cn/application-dev/quick-start/arkts-builderparam.md b/zh-cn/application-dev/quick-start/arkts-builderparam.md new file mode 100644 index 0000000000000000000000000000000000000000..cb4b6e274ab20da9fe3493a0d1d167883648986a --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-builderparam.md @@ -0,0 +1,210 @@ +# \@BuilderParam:引用\@Builder函数 + + +当开发者创建了自定义组件,并想对该组件添加特定功能时,例如在自定义组件中添加一个点击跳转操作。若直接在组件内嵌入事件方法,将会导致所有引入该自定义组件的地方均增加了该功能。为解决此问题,ArkUI引入了\@BuilderParam装饰器,\@BuilderParam用来装饰指向\@Builder方法的变量,开发者可在初始化自定义组件时对此属性进行赋值,为自定义组件增加特定的功能。该装饰器用于声明任意UI描述的一个元素,类似slot占位符。 + + +> **说明:** +> +> 从API version 9开始,该装饰器支持在ArkTS卡片中使用。 + + +## 装饰器使用说明 + + +### 初始化\@BuilderParam装饰的方法 + +\@BuildParam装饰的方法只能被自定义构建函数(\@Builder装饰的方法)初始化。 + +- 使用所属自定义组件的自定义构建函数或者全局的自定义构建函数,在本地初始化\@BuilderParam。 + + ```ts + @Builder function GlobalBuilder0() {} + + @Component + struct Child { + @Builder doNothingBuilder() {}; + + @BuilderParam aBuilder0: () => void = this.doNothingBuilder; + @BuilderParam aBuilder1: () => void = GlobalBuilder0; + build(){} + } + ``` + +- 用父组件自定义构建函数初始化子组件\@BuildParam装饰的方法。 + + ```ts + @Component + struct Child { + @BuilderParam aBuilder0: () => void; + + build() { + Column() { + this.aBuilder0() + } + } + } + + @Entry + @Component + struct Parent { + @Builder componentBuilder() { + Text(`Parent builder `) + } + + build() { + Column() { + Child({ aBuilder0: this.componentBuilder }) + } + } + } + ``` + + +- 需注意this指向正确。 + + 以下示例中,Parent组件在调用this.componentBuilder()时,this.label指向其所属组件,即“Parent”。\@Builder componentBuilder()传给子组件\@BuilderParam aBuilder0,在Child组件中调用this.aBuilder0()时,this.label指向在Child的label,即“Child”。 + + > **说明:** + > + > 开发者谨慎使用bind改变函数调用的上下文,可能会使this指向混乱。 + + ```ts + @Component + struct Child { + label: string = `Child` + @BuilderParam aBuilder0: () => void; + + build() { + Column() { + this.aBuilder0() + } + } + } + + @Entry + @Component + struct Parent { + label: string = `Parent` + + @Builder componentBuilder() { + Text(`${this.label}`) + } + + build() { + Column() { + this.componentBuilder() + Child({ aBuilder0: this.componentBuilder }) + } + } + } + ``` + + +## 使用场景 + + +### 参数初始化组件 + +\@BuilderParam装饰的方法可以是有参数和无参数的两种形式,需与指向的\@Builder方法类型匹配。\@BuilderParam装饰的方法类型需要和\@Builder方法类型一致。 + + +```ts +@Builder function GlobalBuilder1($$ : {label: string }) { + Text($$.label) + .width(400) + .height(50) + .backgroundColor(Color.Blue) +} + +@Component +struct Child { + label: string = 'Child' + // 无参数类,指向的componentBuilder也是无参数类型 + @BuilderParam aBuilder0: () => void; + // 有参数类型,指向的GlobalBuilder1也是有参数类型的方法 + @BuilderParam aBuilder1: ($$ : { label : string}) => void; + + build() { + Column() { + this.aBuilder0() + this.aBuilder1({label: 'global Builder label' } ) + } + } +} + +@Entry +@Component +struct Parent { + label: string = 'Parent' + + @Builder componentBuilder() { + Text(`${this.label}`) + } + + build() { + Column() { + this.componentBuilder() + Child({ aBuilder0: this.componentBuilder, aBuilder1: GlobalBuilder1 }) + } + } +} +``` + + +### 尾随闭包初始化组件示例 + +在自定义组件中使用\@BuilderParam装饰的属性时也可通过尾随闭包进行初始化。在初始化自定义组件时,组件后紧跟一个大括号“{}”形成尾随闭包场景。 + +> **说明:** +> 此场景下自定义组件内有且仅有一个使用\@BuilderParam装饰的属性。 + +开发者可以将尾随闭包内的内容看做\@Builder装饰的函数传给\@BuilderParam。示例如下: + + +```ts +// xxx.ets +@Component +struct CustomContainer { + @Prop header: string; + @BuilderParam closer: () => void + + build() { + Column() { + Text(this.header) + .fontSize(30) + this.closer() + } + } +} + +@Builder function specificParam(label1: string, label2: string) { + Column() { + Text(label1) + .fontSize(30) + Text(label2) + .fontSize(30) + } +} + +@Entry +@Component +struct CustomContainerUser { + @State text: string = 'header'; + + build() { + Column() { + // 创建CustomContainer,在创建CustomContainer时,通过其后紧跟一个大括号“{}”形成尾随闭包 + // 作为传递给子组件CustomContainer @BuilderParam closer: () => void的参数 + CustomContainer({ header: this.text }) { + Column() { + specificParam('testA', 'testB') + }.backgroundColor(Color.Yellow) + .onClick(() => { + this.text = 'changeHeader'; + }) + } + } + } +} +``` diff --git a/zh-cn/application-dev/quick-start/arkts-create-custom-components.md b/zh-cn/application-dev/quick-start/arkts-create-custom-components.md new file mode 100644 index 0000000000000000000000000000000000000000..c0aad400230fb95dfdf13b920f14cd308b6050fc --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-create-custom-components.md @@ -0,0 +1,382 @@ +# 创建自定义组件 + + +在ArkUI中,UI显示的内容均为组件,由框架直接提供的称为系统组件,由开发者定义的称为自定义组件。在进行 UI 界面开发时,通常不是简单的将系统组件进行组合使用,而是需要考虑代码可复用性、业务逻辑与UI分离,后续版本演进等因素。因此,将UI和部分业务逻辑封装成自定义组件是不可或缺的能力。 + + +自定义组件具有以下特点: + + +- 可组合:允许开发者组合使用系统组件、及其属性和方法。 + +- 可重用:自定义组件可以被其他组件重用,并作为不同的实例在不同的父组件或容器中使用。 + +- 数据驱动UI更新:通过状态变量的改变,来驱动UI的刷新。 + + +以下示例展示了自定义组件的基本用法。 + + + +```ts +@Component +struct HelloComponent { + @State message: string = 'Hello, World!'; + + build() { + // HelloComponent自定义组件组合系统组件Row和Text + Row() { + Text(this.message) + .onClick(() => { + // 状态变量message的改变驱动UI刷新,UI从'Hello, World!'刷新为'Hello, ArkUI!' + this.message = 'Hello, ArkUI!'; + }) + } + } +} +``` + + +HelloComponent可以在其他自定义组件中的build()函数中多次创建,实现自定义组件的重用。 + + + +```ts +@Entry +@Component +struct ParentComponent { + build() { + Column() { + Text('ArkUI message') + HelloComponent({ message: 'Hello, World!' }); + Divider() + HelloComponent({ message: '你好!' }); + } + } +} +``` + + +要完全理解上面的示例,需要了解自定义组件的以下概念定义,本文将在后面的小节中介绍: + + +- [自定义组件的基本结构](#自定义组件的基本结构) + +- [成员函数/变量](#成员函数变量) + +- [自定义组件的参数规定](#自定义组件的参数规定) + +- [build()函数](#build函数) + +- [自定义组件通用样式](#自定义组件通用样式) + +- [自定义属性方法](#自定义属性方法) + + +## 自定义组件的基本结构 + +- struct:自定义组件基于struct实现,struct + 自定义组件名 + {...}的组合构成自定义组件,不能有继承关系。对于struct的实例化,可以省略new。 + > **说明:** + > + > 自定义组件名、类名、函数名不能和系统组件名相同。 + +- \@Component:\@Component装饰器仅能装饰struct关键字声明的数据结构。struct被\@Component装饰后具备组件化的能力,需要实现build方法描述UI,一个struct只能被一个\@Component装饰。 + > **说明:** + > + > 从API version 9开始,该装饰器支持在ArkTS卡片中使用。 + + ```ts + @Component + struct MyComponent { + } + ``` + +- build()函数:build()函数用于定义自定义组件的声明式UI描述,自定义组件必须定义build()函数。 + + ```ts + @Component + struct MyComponent { + build() { + } + } + ``` + +- \@Entry:\@Entry装饰的自定义组件将作为UI页面的入口。在单个UI页面中,最多可以使用\@Entry装饰一个自定义组件。\@Entry可以接受一个可选的[LocalStorage](arkts-localstorage.md)的参数。 + + > **说明:** + > + > 从API version 9开始,该装饰器支持在ArkTS卡片中使用。 + + ```ts + @Entry + @Component + struct MyComponent { + } + ``` + + +## 成员函数/变量 + +自定义组件除了必须要实现build()函数外,还可以实现其他成员函数,成员函数具有以下约束: + + +- 不支持静态函数。 + +- 成员函数的访问始终是私有的,仅能定义private。定义访问权限是可选的,其他访问权限会带来语法错误。 + + +自定义组件可以包含成员变量,成员变量具有以下约束: + + +- 不支持静态成员变量。 + +- 所有成员变量都是私有的,变量的访问规则与成员函数的访问规则相同。 + +- 自定义组件的成员变量本地初始化有些是可选的,有些是必选的。具体是否需要本地初始化,是否需要从父组件通过参数传递初始化子组件的成员变量,请参考[状态管理](arkts-state-management-overview.md)。 + + +## 自定义组件的参数规定 + +从上文的示例中,我们已经了解到,可以在build方法或者[@Builder](arkts-builder.md)装饰的函数里创建自定义组件,在创建的过程中,参数可以被提供给组件。 + + +```ts +@Component +struct MyComponent { + private countDownFrom: number = 0; + private color: Color = Color.Blue; + + build() { + } +} + +@Entry +@Component +struct ParentComponent { + private someColor: Color = Color.Pink; + + build() { + Column() { + // 创建MyComponent实例,并将创建MyComponent成员变量countDownFrom初始化为10,将成员变量color初始化为this.someColor + MyComponent({ countDownFrom: 10, color: this.someColor }) + } + } +} +``` + + +## build()函数 + +所有声明在build()函数的语言,我们统称为UI描述语言,UI描述语言需要遵循以下规则: + +- \@Entry装饰的自定义组件,其build()函数下的根节点唯一且必要,且必须为容器组件,其中ForEach禁止作为根节点。 + \@Component装饰的自定义组件,其build()函数下的根节点唯一且必要,可以为非容器组件,其中ForEach禁止作为根节点。 + + ```ts + @Entry + @Component + struct MyComponent { + build() { + // 根节点唯一且必要,必须为容器组件 + Row() { + ChildComponent() + } + } + } + + @Component + struct ChildComponent { + build() { + // 根节点唯一且必要,可为非容器组件 + Image('test.jpg') + } + } + ``` + +- 不允许声明本地变量,反例如下。 + + ```ts + build() { + // 反例:不允许声明本地变量 + let a: number = 1; + } + ``` + +- 不允许在UI描述里直接使用console.info,但允许在方法或者函数里使用,反例如下。 + + ```ts + build() { + // 反例:不允许console.info + console.info('print debug log'); + } + ``` + +- 不允许创建本地的作用域,反例如下。 + + ```ts + build() { + // 反例:不允许本地作用域 + { + ... + } + } + ``` + +- 不允许调用除了被\@Builder装饰以外的方法,允许系统组件的参数是TS方法的返回值。 + + ```ts + @Component + struct ParentComponent { + doSomeCalculations() { + } + + calcTextValue(): string { + return 'Hello World'; + } + + @Builder doSomeRender() { + Text(`Hello World`) + } + + build() { + Column() { + // 反例:不能调用没有用@Builder装饰的方法 + this.doSomeCalculations(); + // 正例:可以调用 + this.doSomeRender(); + // 正例:参数可以为调用TS方法的返回值 + Text(this.calcTextValue()) + } + } + } + ``` + +- 不允许switch语法,如果需要使用条件判断,请使用if。反例如下。 + + ```ts + build() { + Column() { + // 反例:不允许使用switch语法 + switch (expression) { + case 1: + Text('...') + break; + case 2: + Image('...') + break; + default: + Text('...') + break; + } + } + } + ``` + +- 不允许使用表达式,反例如下。 + + ```ts + build() { + Column() { + // 反例:不允许使用表达式 + (this.aVar > 10) ? Text('...') : Image('...') + } + } + ``` + + +## 自定义组件通用样式 + +自定义组件通过“.”链式调用的形式设置通用样式。 + + +```ts +@Component +struct MyComponent2 { + build() { + Button(`Hello World`) + } +} + +@Entry +@Component +struct MyComponent { + build() { + Row() { + MyComponent2() + .width(200) + .height(300) + .backgroundColor(Color.Red) + } + } +} +``` + +> **说明:** +> +> ArkUI给自定义组件设置样式时,相当于给MyComponent2套了一个不可见的容器组件,而这些样式是设置在容器组件上的,而非直接设置给MyComponent2的Button组件。通过渲染结果我们可以很清楚的看到,背景颜色红色并没有直接生效在Button上,而是生效在Button所处的开发者不可见的容器组件上。 + + +## 自定义属性方法 + +自定义组件不支持提供自定义属性方法,可以借助类似Controller控制器能力,提供自定义接口。 + + +```ts +// 自定义controller +export class MyComponentController { + item: MyComponent = null; + + setItem(item: MyComponent) { + this.item = item; + } + + changeText(value: string) { + this.item.value = value; + } +} + +// 自定义组件 +@Component +export default struct MyComponent { + public controller: MyComponentController = null; + @State value: string = 'Hello World'; + + build() { + Column() { + Text(this.value) + .fontSize(50) + } + } + + aboutToAppear() { + if (this.controller) + this.controller.setItem(this); // 绑定controller + } +} + +// 使用处逻辑 +@Entry +@Component +struct StyleExample { + controller = new MyComponentController(); + + build() { + Column() { + MyComponent({ controller: this.controller }) + } + .onClick(() => { + this.controller.changeText('Text'); + }) + } +} +``` + +在上面的示例中: + +1. 通过子组件MyComponent的aboutToAppear方法,把当前的this指针传递给MyComponentController的item成员变量。 + +2. 在StyleExample父组件中持有controller实例,调用controller的changeText方法,即相当于通过controller持有的MyComponent子组件的this指针,改变MyComponent的状态变量value的值。 + +通过controller的封装,MyComponent对外暴露了changeText的接口,所有持有controller的实例都可以通过调用changeText接口,改变MyComponent的状态变量value的值。 + + diff --git a/zh-cn/application-dev/quick-start/arkts-declarative-ui-description.md b/zh-cn/application-dev/quick-start/arkts-declarative-ui-description.md new file mode 100644 index 0000000000000000000000000000000000000000..d4ca2ea18be32aa8b24892593b097b53a21d65c3 --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-declarative-ui-description.md @@ -0,0 +1,172 @@ +# 声明式UI描述 + + +ArkTS以声明方式组合和扩展组件来描述应用程序的UI,同时还提供了基本的属性、事件和子组件配置方法,帮助开发者实现应用交互逻辑。 + + +## 创建组件 + +根据组件构造方法的不同,创建组件包含有参数和无参数两种方式。 + +> **说明:** +> +> 创建组件时不需要new运算符。 + + +### 无参数 + +如果组件的接口定义没有包含必选构造参数,则组件后面的“()”不需要配置任何内容。例如,Divider组件不包含构造参数: + + +```ts +Column() { + Text('item 1') + Divider() + Text('item 2') +} +``` + + +### 有参数 + +如果组件的接口定义包含构造参数,则在组件后面的“()”配置相应参数。 + +- Image组件的必选参数src。 + + ```ts + Image('https://xyz/test.jpg') + ``` + + +- Text组件的非必选参数content。 + +- ```ts + // string类型的参数 + Text('test') + // $r形式引入应用资源,可应用于多语言场景 + Text($r('app.string.title_value')) + // 无参数形式 + Text() + ``` + + +- 变量或表达式也可以用于参数赋值,其中表达式返回的结果类型必须满足参数类型要求。 + 例如,设置变量或表达式来构造Image和Text组件的参数。 + + ```ts + Image(this.imagePath) + Image('https://' + this.imageUrl) + Text(`count: ${this.count}`) + ``` + + +## 配置属性 + +属性方法以“.”链式调用的方式配置系统组件的样式和其他属性,建议每个属性方法单独写一行。 + + +- 配置Text组件的字体大小。 + + ```ts + Text('test') + .fontSize(12) + ``` + +- 配置组件的多个属性。 + + ```ts + Image('test.jpg') + .alt('error.jpg') + .width(100) + .height(100) + ``` + +- 除了直接传递常量参数外,还可以传递变量或表达式。 + + ```ts + Text('hello') + .fontSize(this.size) + Image('test.jpg') + .width(this.count % 2 === 0 ? 100 : 200) + .height(this.offset + 100) + ``` + +- 对于系统组件,ArkUI还为其属性预定义了一些枚举类型供开发者调用,枚举类型可以作为参数传递,但必须满足参数类型要求。 + 例如,可以按以下方式配置Text组件的颜色和字体样式。 + + ```ts + Text('hello') + .fontSize(20) + .fontColor(Color.Red) + .fontWeight(FontWeight.Bold) + ``` + + +## 配置事件 + +事件方法以“.”链式调用的方式配置系统组件支持的事件,建议每个属性方法单独写一行。 + + +- 使用lambda表达式配置组件的事件方法。 + + ```ts + Button('Click me') + .onClick(() => { + this.myText = 'ArkUI'; + }) + ``` + +- 使用匿名函数表达式配置组件的事件方法,要求使用bind,以确保函数体中的this指向当前组件。 + + ```ts + Button('add counter') + .onClick(function(){ + this.counter += 2; + }.bind(this)) + ``` + +- 使用组件的成员函数配置组件的事件方法。 + + ```ts + myClickHandler(): void { + this.counter += 2; + } + ... + Button('add counter') + .onClick(this.myClickHandler.bind(this)) + ``` + + +## 配置子组件 + +如果组件支持子组件配置,则需在尾随闭包"{...}"中为组件添加子组件的UI描述。Column、Row、Stack、Grid、List等组件都是容器组件。 + + +- 以下是简单的Column组件配置子组件的示例。 + + ```ts + Column() { + Text('Hello') + .fontSize(100) + Divider() + Text(this.myText) + .fontSize(100) + .fontColor(Color.Red) + } + ``` + +- 容器组件均支持子组件配置,可以实现相对复杂的多级嵌套。 + + ```ts + Column() { + Row() { + Image('test1.jpg') + .width(100) + .height(100) + Button('click +1') + .onClick(() => { + console.info('+1 clicked!'); + }) + } + } + ``` diff --git a/zh-cn/application-dev/quick-start/arkts-dynamic-ui-elememt-building.md b/zh-cn/application-dev/quick-start/arkts-dynamic-ui-elememt-building.md deleted file mode 100644 index 3de63bc1291d343afe73d9117f0ec0157348c657..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/quick-start/arkts-dynamic-ui-elememt-building.md +++ /dev/null @@ -1,385 +0,0 @@ -# 动态构建UI元素 - -[基本UI描述](arkts-basic-ui-description.md)介绍的是如何创建一个内部UI结构固定的自定义组件,为了满足开发者自定义组件内部UI结构的需求,ArkTS同时提供了动态构建UI元素的能力。 - -## @Builder - -可通过@Builder装饰器进行描述,该装饰器可以修饰一个函数,此函数可以在build函数之外声明,并在build函数中或其他@Builder修饰的函数中使用,从而实现在一个自定义组件内快速生成多个布局内容。使用方式如下面示例所示。 - -```ts -// xxx.ets -@Component -struct CompB { - @State CompValue: string = '' - - aboutToAppear() { - console.info('CompB aboutToAppear.') - } - - aboutToDisappear() { - console.info('CompB aboutToDisappear.') - } - - build() { - Column() { - Button(this.CompValue) - .margin(5) - } - } -} - -@Entry -@Component -struct CompA { - size1: number = 100 - @State CompValue1: string = "Hello,CompValue1" - @State CompValue2: string = "Hello,CompValue2" - @State CompValue3: string = "Hello,CompValue3" - - // @Builder装饰的函数CompC内使用自定义组件CompB - @Builder CompC(value: string) { - CompB({ CompValue: value }) - } - - @Builder SquareText(label: string) { - Text(label) - .fontSize(18) - .width(1 * this.size1) - .height(1 * this.size1) - } - - // @Builder装饰的函数RowOfSquareTexts内使用@Builder装饰的函数SquareText - @Builder RowOfSquareTexts(label1: string, label2: string) { - Row() { - this.SquareText(label1) - this.SquareText(label2) - } - .width(2 * this.size1) - .height(1 * this.size1) - } - - build() { - Column() { - Row() { - this.SquareText("A") - this.SquareText("B") - } - .width(2 * this.size1) - .height(1 * this.size1) - - this.RowOfSquareTexts("C", "D") - Column() { - // 使用三次@Builder装饰的自定义组件 - this.CompC(this.CompValue1) - this.CompC(this.CompValue2) - this.CompC(this.CompValue3) - } - .width(2 * this.size1) - .height(2 * this.size1) - } - .width(2 * this.size1) - .height(2 * this.size1) - } -} -``` -![builder](figures/builder.PNG) - -## @BuilderParam8+ - -@BuilderParam装饰器用于修饰自定义组件内函数类型的属性(例如:`@BuilderParam noParam: () => void`),并且在初始化自定义组件时被@BuilderParam修饰的属性必须赋值。 - -### 引入动机 - -当开发者创建自定义组件,并想对该组件添加特定功能时(例如在自定义组件中添加一个点击跳转操作)。若直接在组件内嵌入事件方法,将会导致所有引入该自定义组件的地方均增加了该功能。为解决此问题,引入了@BuilderParam装饰器,此装饰器修饰的属性值可为@Builder装饰的函数,开发者可在初始化自定义组件时对此属性进行赋值,为自定义组件增加特定的功能。 - -### 参数初始化组件 - -通过参数初始化组件时,将@Builder装饰的函数赋值给@BuilderParam修饰的属性,并在自定义组件内调用该属性值。若@BuilderParam修饰的属性在进行赋值时不带参数(如:`noParam: this.specificNoParam`),则此属性的类型需定义为无返回值的函数(如:`@BuilderParam noParam: () => void`);若带参数(如:`withParam: this.SpecificWithParam('WithParamA')`),则此属性的类型需定义成any(如:`@BuilderParam withParam: any`)。 - -```ts -// xxx.ets -@Component -struct CustomContainer { - header: string = '' - @BuilderParam noParam: () => void - @BuilderParam withParam: any - footer: string = '' - - build() { - Column() { - Text(this.header) - .fontSize(30) - this.noParam() - this.withParam() - Text(this.footer) - .fontSize(30) - } - } -} - -@Entry -@Component -struct CustomContainerUser { - @Builder specificNoParam() { - Column() { - Text('noParam').fontSize(30) - } - } - - @Builder SpecificWithParam(label: string) { - Column() { - Text(label).fontSize(30) - } - } - - build() { - Column() { - CustomContainer({ - header: 'HeaderA', - noParam: this.specificNoParam, - withParam: this.SpecificWithParam('WithParamA'), - footer: 'FooterA' - }) - Divider() - .strokeWidth(3) - .margin(10) - CustomContainer({ - header: 'HeaderB', - noParam: this.specificNoParam, - withParam: this.SpecificWithParam('WithParamB'), - footer: 'FooterB' - }) - } - } -} -``` - -![builder1](figures/builder1.PNG) - -### 尾随闭包初始化组件 - -在自定义组件中使用@BuilderParam修饰的属性时也可通过尾随闭包进行初始化(在初始化自定义组件时,组件后紧跟一个大括号“{}”形成尾随闭包场景(`CustomContainer(){}`)。开发者可把尾随闭包看做一个容器,向其中填充内容,如在闭包内增加组件(`{Column(){...}`),闭包内语法规范与build函数一致。此场景下自定义组件内有且仅有一个使用@BuilderParam修饰的属性。 - -示例:在闭包内添加Column组件并设置点击事件,在Column组件内调用@Builder修饰的specificParam函数,点击Column组件后将自定义组件CustomContainer中header的属性值由“header”改变为“changeHeader”。在初始化自定义组件CustomContainer时,尾随闭包的内容会被赋值给@BuilderParam修饰的closer属性。 - -```ts -// xxx.ets -@Component -struct CustomContainer { - header: string = '' - @BuilderParam closer: () => void - - build() { - Column() { - Text(this.header) - .fontSize(30) - this.closer() - } - } -} - -@Builder function specificParam(label1: string, label2: string) { - Column() { - Text(label1) - .fontSize(30) - Text(label2) - .fontSize(30) - } -} - -@Entry -@Component -struct CustomContainerUser { - @State text: string = 'header' - - build() { - Column() { - CustomContainer({ - header: this.text, - }) { - Column() { - specificParam('testA', 'testB') - }.backgroundColor(Color.Yellow) - .onClick(() => { - this.text = 'changeHeader' - }) - } - } - } -} -``` - -![builder2](figures/builder2.gif) - -## @Styles - -ArkTS为了避免开发者对重复样式的设置,通过@Styles装饰器可以将多个样式设置提炼成一个方法,直接在组件声明时调用,通过@Styles装饰器可以快速定义并复用自定义样式。当前@Styles仅支持通用属性。 - -@Styles可以定义在组件内或组件外,在组件外定义时需在方法名前面添加function关键字,组件内定义时则不需要添加function关键字。 - -```ts -// xxx.ets -@Styles function globalFancy () { - .width(150) - .height(100) - .backgroundColor(Color.Pink) -} - -@Entry -@Component -struct FancyUse { - @Styles componentFancy() { - .width(100) - .height(200) - .backgroundColor(Color.Yellow) - } - - build() { - Column({ space: 10 }) { - Text('FancyA') - .globalFancy() - .fontSize(30) - Text('FancyB') - .globalFancy() - .fontSize(20) - Text('FancyC') - .componentFancy() - .fontSize(30) - Text('FancyD') - .componentFancy() - .fontSize(20) - } - } -} -``` - -![styles](figures/styles.PNG) - -@Styles还可以在[StateStyles](../reference/arkui-ts/ts-universal-attributes-polymorphic-style.md)属性内部使用,在组件处于不同的状态时赋予相应的属性。 - -在StateStyles内可以直接调用组件外定义的@Styles方法,但需要通过this关键字调用组件内定义的@Styles方法。 - -```ts -// xxx.ets -@Styles function globalFancy () { - .width(120) - .height(120) - .backgroundColor(Color.Green) -} - -@Entry -@Component -struct FancyUse { - @Styles componentFancy() { - .width(80) - .height(80) - .backgroundColor(Color.Red) - } - - build() { - Row({ space: 10 }) { - Button('Fancy') - .stateStyles({ - normal: { - .width(100) - .height(100) - .backgroundColor(Color.Blue) - }, - disabled: this.componentFancy, - pressed: globalFancy - }) - } - } -} -``` - -![styles1](figures/styles1.gif) - -## @Extend - -@Extend装饰器将新的属性方法添加到Text、Column、Button等内置组件上,通过@Extend装饰器可以快速地扩展原生组件。@Extend不能定义在自定义组件struct内。 - -```ts -// xxx.ets -@Extend(Text) function fancy (fontSize: number) { - .fontColor(Color.Red) - .fontSize(fontSize) - .fontStyle(FontStyle.Italic) - .fontWeight(600) -} - -@Entry -@Component -struct FancyUse { - build() { - Row({ space: 10 }) { - Text("Fancy") - .fancy(16) - Text("Fancy") - .fancy(24) - Text("Fancy") - .fancy(32) - } - } -} - -``` - -> **说明:** -> -> - @Extend装饰器不能定义在自定义组件struct内。 -> - @Extend装饰器内仅支持属性方法设置。 - -![extend](figures/extend.PNG) - -## @CustomDialog - -@CustomDialog装饰器用于装饰自定义弹窗组件,使得弹窗可以动态设置内容及样式。 - -```ts -// xxx.ets -@CustomDialog -struct DialogExample { - controller: CustomDialogController - action: () => void - - build() { - Row() { - Button('Close CustomDialog') - .onClick(() => { - this.controller.close() - this.action() - }) - }.padding(20) - } -} - -@Entry -@Component -struct CustomDialogUser { - dialogController: CustomDialogController = new CustomDialogController({ - builder: DialogExample({ action: this.onAccept }), - cancel: this.existApp, - autoCancel: true - }); - - onAccept() { - console.info('onAccept'); - } - - existApp() { - console.info('Cancel dialog!'); - } - - build() { - Column() { - Button('Click to open Dialog') - .onClick(() => { - this.dialogController.open() - }) - } - } -} -``` - -![customdialog](figures/customDialog.gif) \ No newline at end of file diff --git a/zh-cn/application-dev/quick-start/arkts-environment.md b/zh-cn/application-dev/quick-start/arkts-environment.md new file mode 100644 index 0000000000000000000000000000000000000000..240e4358842abcd0beb42e21fc819af60032abcb --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-environment.md @@ -0,0 +1,71 @@ +# Environment:设备环境查询 + + +开发者如果需要应用程序运行的设备的环境参数,以此来作出不同的场景判断,比如多语言,暗黑模式等,需要用到Environment设备环境查询。 + + +Environment是ArkUI框架在应用程序启动时创建的单例对象。它为AppStorage提供了一系列描述应用程序运行状态的属性。Environment的所有属性都是不可变的(即应用不可写入),所有的属性都是简单类型。 + + +## 使用场景 + + +### 从UI中访问Environment参数 + +- 使用Environment.EnvProp将设备运行的环境变量存入AppStorage中: + + ```ts + // 将设备的语言code存入AppStorage,默认值为en + // 后续设备的预览设置切换,都将同步到AppStorage中 + Environment.EnvProp('languageCode', 'en'); + ``` + +- 可以使用\@StorageProp链接到Component中。Component会根据设备运行环境的变化而更新: + + ```ts + @StorageProp('languageCode') lang : string = 'en'; + ``` + +设备环境到Component的更新链:Environment --> AppStorage -->Component。 + +> **说明:** +> \@StorageProp关联的环境参数可以在本地更改,但不能同步回AppStorage中,因为应用对环境变量参数是不可写的,只能在Environment中查询。 + + +```ts +// 将设备languageCode存入AppStorage中 +Environment.EnvProp('languageCode', 'en'); +let enable = AppStorage.Get('languageCode'); + +@Entry +@Component +struct Index { + @StorageProp('languageCode') languageCode: string = 'en'; + + build() { + Row() { + Column() { + // 输出当前设备的languageCode + Text(this.languageCode) + } + } + } +} +``` + + +### 应用逻辑使用Environment + + +```ts +// 使用Environment.EnvProp将设备运行languageCode存入AppStorage中; +Environment.EnvProp('languageCode', 'en'); +// 从AppStorage获取单向绑定的languageCode的变量 +const lang: SubscribedAbstractProperty = AppStorage.Prop('languageCode'); + +if (lang.get() === 'zh') { + console.info('你好'); +} else { + console.info('Hello!'); +} +``` diff --git a/zh-cn/application-dev/quick-start/arkts-extend.md b/zh-cn/application-dev/quick-start/arkts-extend.md new file mode 100644 index 0000000000000000000000000000000000000000..450ec1b2450138023f7abf4b55ee0a857bbf72ed --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-extend.md @@ -0,0 +1,176 @@ +# \@Extend:定义扩展组件样式 + + +在前文的示例中,可以使用\@Styles用于样式的扩展,在\@Styles的基础上,我们提供了\@Extend,用于扩展原生组件样式。 + + +> **说明:** +> 从API version 9开始,该装饰器支持在ArkTS卡片中使用。 + + +## 装饰器使用说明 + + +### 语法 + + +```ts +@Extend(UIComponentName) function functionName { ... } +``` + + +### 使用规则 + +- 和\@Styles不同,\@Extend仅支持定义在全局,不支持在组件内部定义。 + +- 和\@Styles不同,\@Extend支持封装指定的组件的私有属性和私有事件和预定义相同组件的\@Extend的方法。 + + ```ts + // @Extend(Text)可以支持Text的私有属性fontColor + @Extend(Text) function fancy () { + .fontColor(Color.Red) + } + // superFancyText可以调用预定义的fancy + @Extend(Text) function superFancyText(size:number) { + .fontSize(size) + .fancy() + } + ``` + + +- 和\@Styles不同,\@Extend装饰的方法支持参数,开发者可以在调用时传递参数,调用遵循TS方法传值调用。 + + ```ts + // xxx.ets + @Extend(Text) function fancy (fontSize: number) { + .fontColor(Color.Red) + .fontSize(fontSize) + } + + @Entry + @Component + struct FancyUse { + build() { + Row({ space: 10 }) { + Text('Fancy') + .fancy(16) + Text('Fancy') + .fancy(24) + } + } + } + ``` + +- \@Extend装饰的方法的参数可以为function,作为Event事件的句柄。 + + ```ts + @Extend(Text) function makeMeClick(onClick: () => void) { + .backgroundColor(Color.Blue) + .onClick(onClick) + } + + @Entry + @Component + struct FancyUse { + @State label: string = 'Hello World'; + + onClickHandler() { + this.label = 'Hello ArkUI'; + } + + build() { + Row({ space: 10 }) { + Text(`${this.label}`) + .makeMeClick(this.onClickHandler.bind(this)) + } + } + } + ``` + +- \@Extend的参数可以为[状态变量](arkts-state-management-overview.md),当状态变量改变时,UI可以正常的被刷新渲染。 + + ```ts + @Extend(Text) function fancy (fontSize: number) { + .fontColor(Color.Red) + .fontSize(fontSize) + } + + @Entry + @Component + struct FancyUse { + @State fontSizeValue: number = 20 + build() { + Row({ space: 10 }) { + Text('Fancy') + .fancy(this.fontSizeValue) + .onClick(() => { + this.fontSizeValue = 30 + }) + } + } + } + ``` + + +## 使用场景 + +以下示例声明了3个Text组件,每个Text组件均设置了fontStyle、fontWeight和backgroundColor样式。 + + +```ts +@Entry +@Component +struct FancyUse { + @State label: string = 'Hello World' + + build() { + Row({ space: 10 }) { + Text(`${this.label}`) + .fontStyle(FontStyle.Italic) + .fontWeight(100) + .backgroundColor(Color.Blue) + Text(`${this.label}`) + .fontStyle(FontStyle.Italic) + .fontWeight(200) + .backgroundColor(Color.Pink) + Text(`${this.label}`) + .fontStyle(FontStyle.Italic) + .fontWeight(300) + .backgroundColor(Color.Orange) + }.margin('20%') + } +} +``` + +\@Extend将样式组合复用,示例如下。 + + +```ts +@Extend(Text) function fancyText(weightValue: number, color: Color) { + .fontStyle(FontStyle.Italic) + .fontWeight(weightValue) + .backgroundColor(color) +} +``` + +通过\@Extend组合样式后,使得代码更加简洁,增强可读性。 + + +```ts +@Entry +@Component +struct FancyUse { + @State label: string = 'Hello World' + + build() { + Row({ space: 10 }) { + Text(`${this.label}`) + .fancyText(100, Color.Blue) + Text(`${this.label}`) + .fancyText(200, Color.Pink) + Text(`${this.label}`) + .fancyText(200, Color.Orange) + }.margin('20%') + } +} +``` diff --git a/zh-cn/application-dev/quick-start/arkts-get-started.md b/zh-cn/application-dev/quick-start/arkts-get-started.md index af97f571b9c0fa336d501b3cd322f65e61fb2cd4..378d07d84d7608023257b25cdb4dab983b9cc674 100644 --- a/zh-cn/application-dev/quick-start/arkts-get-started.md +++ b/zh-cn/application-dev/quick-start/arkts-get-started.md @@ -1,30 +1,17 @@ # 初识ArkTS语言 -ArkTS是OpenHarmony优选的主力应用开发语言。ArkTS基于TypeScript(简称TS)语言扩展而来,是TS的超集。 -- ArkTS继承了TS的所有特性。 +ArkTS是OpenHarmony优选的主力应用开发语言。ArkTS围绕应用开发在[TypeScript](https://www.typescriptlang.org/)(简称TS)生态基础上做了进一步扩展,继承了TS的所有特性,是TS的超集。因此,在学习ArkTS语言之前,建议开发者具备TS语言开发能力。 -- 当前,ArkTS在TS的基础上主要扩展了[声明式UI](arkts-basic-ui-description.md)能力,让开发者能够以更简洁、更自然的方式开发高性能应用。 - 当前扩展的声明式UI能力包括如下特性。 +当前,ArkTS在TS的基础上主要扩展了如下能力: - - [基本UI描述](arkts-basic-ui-description.md):ArkTS定义了各种装饰器、自定义组件、UI描述机制,再配合UI开发框架中的内置组件及其相关的事件方法、属性方法等共同构成了UI开发的主体。 - - [状态管理](arkts-state-mgmt-page-level.md):ArkTS提供了多维度的状态管理机制,在UI开发框架中,和UI相关联的数据,不仅可以在组件内使用,还可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,也可以是应用全局范围内的传递,还可以是跨设备传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活的利用这些能力来实现数据和UI的联动。 - - [动态构建UI元素](arkts-dynamic-ui-elememt-building.md):ArkTS提供了动态构建UI元素的能力,不仅可以自定义组件内部的UI结构,还可以复用组件样式,扩展原生组件。 - - [渲染控制](arkts-rendering-control.md):ArkTS提供了渲染控制的能力。条件渲染可根据应用的不同状态,渲染对应状态下的UI内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件。 - - [使用限制与扩展](arkts-restrictions-and-extensions.md):ArkTS在使用过程中存在限制与约束,同时也扩展了双向绑定等能力。 -- 未来,ArkTS会结合应用开发/运行的需求持续演进,逐步提供并行和并发能力增强、系统类型增强、分布式开发范式等更多特性。 +- [基本语法](arkts-basic-syntax-overview.md):ArkTS定义了声明式UI描述、自定义组件和动态扩展UI元素的能力,再配合ArkUI开发框架中的系统组件及其相关的事件方法、属性方法等共同构成了UI开发的主体。 -下面我们以一个具体的示例来说明ArkTS的基本组成。如下图所示的代码示例,UI界面包含两段文本、一条分割线和一个按钮,当开发者点击按钮时,文本内容会从'Hello World'变为 'Hello ArkUI'。 +- [状态管理](arkts-state-management-overview.md):ArkTS提供了多维度的状态管理机制。在UI开发框架中,与UI相关联的数据可以在组件内使用,也可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,还可以在应用全局范围内传递或跨设备传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活的利用这些能力来实现数据和UI的联动。 -![arkts-get-started](figures/arkts-get-started.png) +- [渲染控制](arkts-rendering-control-overview.md):ArkTS提供了渲染控制的能力。条件渲染可根据应用的不同状态,渲染对应状态下的UI内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件。数据懒加载从数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。 -这个示例中所包含的ArkTS声明式开发范式的基本组成说明如下: -- 装饰器: 用于装饰类、结构、方法以及变量,赋予其特殊的含义,如上述示例中@Entry、@Component和@State都是装饰器。 具体而言,@Component表示这是个自定义组件;@Entry则表示这是个入口组件;@State表示这是组件中的状态变量,这个变量变化会触发UI刷新。 -- 自定义组件:可复用的UI单元,可组合其他组件,如上述被@Component装饰的struct Hello。 -- UI描述:声明式的方法来描述UI的结构,例如build()方法中的代码块。 -- 内置组件:ArkTS中默认内置的基础组件、容器组件、媒体组件、绘制组件、画布组件等各种组件,开发者可以直接调用,如示例中的Column、Text、Divider、Button等。 -- 属性方法:用于组件属性的配置,如fontSize()、width()、height()、color()等,可通过链式调用的方式设置多项属性。 -- 事件方法:用于添加组件对事件的响应逻辑,如跟随在Button后面的onClick(),同样可以通过链式调用的方式设置多个事件响应逻辑。 +未来,ArkTS会结合应用开发/运行的需求持续演进,逐步提供并行和并发能力增强、系统类型增强、分布式开发范式等更多特性。 diff --git a/zh-cn/application-dev/quick-start/arkts-link.md b/zh-cn/application-dev/quick-start/arkts-link.md new file mode 100644 index 0000000000000000000000000000000000000000..2f66fd1692799339701b53f20baaa14af803b7a6 --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-link.md @@ -0,0 +1,187 @@ +# \@Link:父子双向同步 + + +子组件中被\@Link装饰的变量与其父组件中对应的数据源建立双向数据绑定。 + + +> **说明:** +> 从API version 9开始,该装饰器支持在ArkTS卡片中使用。 + + +## 概述 + +\@Link装饰的变量与其父组件中的数据源共享相同的值。 + + +## 装饰器使用规则说明 + +| \@Link变量装饰器 | 说明 | +| ----------- | ---------------------------------------- | +| 装饰器参数 | 无 | +| 同步类型 | 双向同步。
父组件中\@State, \@StorageLink和\@Link 和子组件\@Link可以建立双向数据同步,反之亦然。 | +| 允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。嵌套类型的场景请参考[观察变化](#观察变化)。
类型必须被指定,且和双向绑定状态变量的类型相同。
不支持any,不支持简单类型和复杂类型的联合类型,不允许使用undefined和null。
**说明:**
不支持Length、ResourceStr、ResourceColor类型,Length、ResourceStr、ResourceColor为简单类型和复杂类型的联合类型。 | +| 被装饰变量的初始值 | 无,禁止本地初始化。 | + + +## 变量的传递/访问规则说明 + +| 传递/访问 | 说明 | +| ---------- | ---------------------------------------- | +| 从父组件初始化和更新 | 必选。与父组件\@State, \@StorageLink和\@Link 建立双向绑定。允许父组件中\@State、\@Link、\@Prop、\@Provide、\@Consume、\@ObjectLink、\@StorageLink、\@StorageProp、\@LocalStorageLink和\@LocalStorageProp装饰变量初始化子组件\@Link。
从API version 9开始,\@Link子组件从父组件初始化\@State的语法为Comp({ aLink: this.aState })。同样Comp({aLink: $aState})也支持。 | +| 用于初始化子组件 | 允许,可用于初始化常规变量、\@State、\@Link、\@Prop、\@Provide。 | +| 是否支持组件外访问 | 私有,只能在所属组件内访问。 | + + **图1** 初始化规则图示   + +![zh-cn_image_0000001502092556](figures/zh-cn_image_0000001502092556.png) + + +## 观察变化和行为表现 + + +### 观察变化 + +- 当装饰的数据类型为boolean、string、number类型时,可以同步观察到数值的变化,示例请参考[简单类型和类对象类型的@Link](#简单类型和类对象类型的link)。 + +- 当装饰的数据类型为class或者Object时,可以观察到赋值和属性赋值的变化,即Object.keys(observedObject)返回的所有属性,示例请参考[简单类型和类对象类型的@Link](#简单类型和类对象类型的link)。 + +- 当装饰的对象是array时,可以观察到数组添加、删除、更新数组单元的变化,示例请参考[数组类型的@Link](#数组类型的link)。 + + +### 框架行为 + +\@Link装饰的变量和其所述的自定义组件共享生命周期。 + +为了了解\@Link变量初始化和更新机制,有必要先了解父组件和和拥有\@Link变量的子组件的关系,初始渲染和双向更新的流程(以父组件为\@State为例)。 + +1. 初始渲染:执行父组件的build()函数后将创建子组件的新实例。初始化过程如下: + 1. 必须指定父组件中的\@State变量,用于初始化子组件的\@Link变量。子组件的\@Link变量值与其父组件的数据源变量保持同步(双向数据同步)。 + 2. 父组件的\@State状态变量包装类通过构造函数传给子组件,子组件的\@Link包装类拿到父组件的\@State的状态变量后,将当前\@Link包装类this指针注册给父组件的\@State变量。 + +2. \@Link的数据源的更新:即父组件中状态变量更新,引起相关子组件的\@Link的更新。处理步骤: + 1. 通过初始渲染的步骤可知,子组件\@Link包装类把当前this指针注册给父组件。父组件\@State变量变更后,会遍历更新所有依赖它的系统组件(elementid)和状态变量(比如\@Link包装类)。 + 2. 通知\@Link包装类更新后,子组件中所有依赖\@Link状态变量的系统组件(elementId)都会被通知更新。以此实现父组件对子组件的状态数据同步。 + +3. \@Link的更新:当子组件中\@Link更新后,处理步骤如下(以父组件为\@State为例): + 1. \@Link更新后,调用父组件的\@State包装类的set方法,将更新后的数值同步回父组件。 + 2. 子组件\@Link和父组件\@State分别遍历依赖的系统组件,进行对应的UI的更新。以此实现子组件\@Link同步回父组件\@State。 + + +## 使用场景 + + +### 简单类型和类对象类型的\@Link + +以下示例中,点击父组件ShufflingContainer中的“Parent View: Set yellowButton”和“Parent View: Set GreenButton”,可以从父组件将变化同步给子组件,子组件GreenButton和YellowButton中\@Link装饰变量的变化也会同步给其父组件。 + + +```ts +class GreenButtonState { + width: number = 0; + constructor(width: number) { + this.width = width; + } +} +@Component +struct GreenButton { + @Link greenButtonState: GreenButtonState; + build() { + Button('Green Button') + .width(this.greenButtonState.width) + .height(150.0) + .backgroundColor('#00ff00') + .onClick(() => { + if (this.greenButtonState.width < 700) { + // 更新class的属性,变化可以被观察到同步回父组件 + this.greenButtonState.width += 125; + } else { + // 更新class,变化可以被观察到同步回父组件 + this.greenButtonState = new GreenButtonState(100); + } + }) + } +} +@Component +struct YellowButton { + @Link yellowButtonState: number; + build() { + Button('Yellow Button') + .width(this.yellowButtonState) + .height(150.0) + .backgroundColor('#ffff00') + .onClick(() => { + // 子组件的简单类型可以同步回父组件 + this.yellowButtonState += 50.0; + }) + } +} +@Entry +@Component +struct ShufflingContainer { + @State greenButtonState: GreenButtonState = new GreenButtonState(300); + @State yellowButtonProp: number = 100; + build() { + Column() { + // 简单类型从父组件@State向子组件@Link数据同步 + Button('Parent View: Set yellowButton') + .onClick(() => { + this.yellowButtonProp = (this.yellowButtonProp < 700) ? this.yellowButtonProp + 100 : 100; + }) + // class类型从父组件@State向子组件@Link数据同步 + Button('Parent View: Set GreenButton') + .onClick(() => { + this.greenButtonState.width = (this.greenButtonState.width < 700) ? this.greenButtonState.width + 100 : 100; + }) + // class类型初始化@Link + GreenButton({ greenButtonState: $greenButtonState }) + // 简单类型初始化@Link + YellowButton({ yellowButtonState: $yellowButtonProp }) + } + } +} +``` + + +### 数组类型的\@Link + + +```ts +@Component +struct Child { + @Link items: number[]; + + build() { + Column() { + Button(`Button1: push`).onClick(() => { + this.items.push(this.items.length + 1); + }) + Button(`Button2: replace whole item`).onClick(() => { + this.items = [100, 200, 300]; + }) + } + } +} + +@Entry +@Component +struct Parent { + @State arr: number[] = [1, 2, 3]; + + build() { + Column() { + Child({ items: $arr }) + ForEach(this.arr, + item => { + Text(`${item}`) + }, + item => item.toString() + ) + } + } +} +``` + + +上文所述,ArkUI框架可以观察到数组元素的添加,删除和替换。在该示例中\@State和\@Link的类型是相同的number[],不允许将\@Link定义成number类型(\@Link item : number),并在父组件中用\@State数组中每个数据项创建子组件。如果要使用这个场景,可以参考[\@Prop](arkts-prop.md)和\@Observed。 + + diff --git a/zh-cn/application-dev/quick-start/arkts-localstorage.md b/zh-cn/application-dev/quick-start/arkts-localstorage.md new file mode 100644 index 0000000000000000000000000000000000000000..e7d0df5b2ce5a7ffb1973b18cfdec3254a897c32 --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-localstorage.md @@ -0,0 +1,427 @@ +# LocalStorage:页面级UI状态存储 + + +LocalStorage是页面级的UI状态存储,通过\@Entry装饰器接受的参数可以在页面内共享同一个LocalStorage实例。LocalStorage也可以在UIAbility内,页面间共享状态。 + + +本文仅介绍LocalStorage使用场景和相关的装饰器:\@LocalStorageProp和\@LocalStorageLink。 + + +> **说明:** +> +> 本模块从API version 9开始支持。 + + +## 概述 + +LocalStorage是ArkTS为构建页面级别状态变量提供存储的内存内“数据库”。 + +- 应用程序可以创建多个LocalStorage实例,LocalStorage实例可以在页面内共享,也可以通过GetShared接口,获取在UIAbility里创建的GetShared,实现跨页面、UIAbility内共享。 + +- 组件树的根节点,即被\@Entry装饰的\@Component,可以被分配一个LocalStorage实例,此组件的所有子组件实例将自动获得对该LocalStorage实例的访问权限; + +- 被\@Component装饰的组件最多可以访问一个LocalStorage实例和[AppStorage](arkts-appstorage.md),未被\@Entry装饰的组件不可被独立分配LocalStorage实例,只能接受父组件通过\@Entry传递来的LocalStorage实例。一个LocalStorage实例在组件树上可以被分配给多个组件。 + +- LocalStorage中的所有属性都是可变的。 + +应用程序决定LocalStorage对象的生命周期。当应用释放最后一个指向LocalStorage的引用时,比如销毁最后一个自定义组件,LocalStorage将被JS Engine垃圾回收。 + +LocalStorage根据与\@Component装饰的组件的的同步类型不同,提供了两个装饰器: + +- [@LocalStorageProp](#localstorageprop):\@LocalStorageProp装饰的变量和与LocalStorage中给定属性建立单行同步关系。 + +- [@LocalStorageLink](#localstoragelink):\@LocalStorageLink装饰的变量和在\@Component中创建与LocalStorage中给定属性建立双向同步关系。 + + +## 限制条件 + +LocalStorage创建后,命名属性的类型不可更改。后续调用Set时必须使用相同类型的值。 + + +## \@LocalStorageProp + +在上文中已经提到,如果要建立LocalStorage和自定义组件的联系,需要使用\@LocalStorageProp和\@LocalStorageLink装饰器。使用\@LocalStorageProp(key)/\@LocalStorageLink(key)装饰组件内的变量,key标识了LocalStorage的属性。 + + +当自定义组件初始化的时候,\@LocalStorageProp(key)/\@LocalStorageLink(key)装饰的变量会通过给定的key,绑定在LocalStorage对应是属性,完成初始化。本地初始化是必要的,因为无法保证LocalStorage一定存在给定的key(这取决于应用逻辑,是否在组件初始化之前在LocalStorage实例中存入对应的属性)。 + + +> **说明:** +> +> 从API version 9开始,该装饰器支持在ArkTS卡片中使用。 + + +\@LocalStorageProp(key)是和LocalStorage中key对应的属性建立单向数据同步,我们允许本地改变的发生,但是对于\@LocalStorageProp,本地的修改永远不会同步回LocalStorage中,相反,如果LocalStorage给定key的属性发生改变,改变会被同步给\@LocalStorageProp,并覆盖掉本地的修改。 + + +### 装饰器使用规则说明 + +| \@LocalStorageProp变量装饰器 | 说明 | +| ----------------------- | ---------------------------------------- | +| 装饰器参数 | key:常量字符串,必填(字符串需要有引号)。 | +| 允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。嵌套类型的场景请参考[观察变化和行为表现](#观察变化和行为表现)。
类型必须被指定,且必须和LocalStorage中对应属性相同。不支持any,不允许使用undefined和null。 | +| 同步类型 | 单向同步:从LocalStorage的对应属性到组件的状态变量。组件本地的修改是允许的,但是LocalStorage中给定的属性一旦发生变化,将覆盖本地的修改。 | +| 被装饰变量的初始值 | 必须指定,如果LocalStorage实例中不存在属性,则作为初始化默认值,并存入LocalStorage中。 | + + +### 变量的传递/访问规则说明 + +| 传递/访问 | 说明 | +| ---------- | ---------------------------------------- | +| 从父节点初始化和更新 | 禁止,\@LocalStorageProp不支持从父节点初始化,只能从LocalStorage中key对应的属性初始化,如果没有对应key的话,将使用本地默认值初始化。 | +| 初始化子节点 | 支持,可用于初始化\@State、\@Link、\@Prop、\@Provide。 | +| 是否支持组件外访问 | 否。 | + + **图1** \@LocalStorageProp初始化规则图示   + +![zh-cn_image_0000001501936014](figures/zh-cn_image_0000001501936014.png) + + +### 观察变化和行为表现 + +**观察变化** + + +- 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。 + +- 当装饰的数据类型为class或者Object时,可以观察到赋值和属性赋值的变化,即Object.keys(observedObject)返回的所有属性。 + +- 当装饰的对象是array时,可以观察到数组添加、删除、更新数组单元的变化。 + + +**框架行为** + + +- 当\@LocalStorageProp(key)装饰的数值改变被观察到时,修改不会被同步回LocalStorage对应属性键值key的属性中。 + +- 当前\@LocalStorageProp(key)单向绑定的数据会被修改,即仅限于当前组件的私有成员变量改变,其他的绑定该key的数据不会同步改变。 + +- 当\@LocalStorageProp(key)装饰的数据本身是状态变量,它的改变虽然不会同步回LocalStorage中,但是会引起所属的自定义组件的重新渲染。 + +- 当LocalStorage中key对应的属性发生改变时,会同步给所有\@LocalStorageProp(key)装饰的数据,\@LocalStorageProp(key)本地的修改将被覆盖。 + + +## \@LocalStorageLink + +如果我们需要将自定义组件的状态变量的更新同步回LocalStorage,就需要用到\@LocalStorageLink。 + +\@LocalStorageLink(key)是和LocalStorage中key对应的属性建立双向数据同步: + +1. 本地修改发生,该修改会被回LocalStorage中; + +2. LocalStorage中的修改发生后,该修改会被同步到所有绑定LocalStorage对应key的属性上,包括单向(\@LocalStorageProp和通过prop创建的单向绑定变量)、双向(\@LocalStorageLink和通过link创建的双向绑定变量)变量。 + + +### 装饰器使用规则说明 + +| \@LocalStorageLink变量装饰器 | 说明 | +| ----------------------- | ---------------------------------------- | +| 装饰器参数 | key:常量字符串,必填(字符串需要有引号)。 | +| 允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。嵌套类型的场景请参考[观察变化和行为表现](#观察变化和行为表现)。
类型必须被指定,且必须和LocalStorage中对应属性相同。不支持any,不允许使用undefined和null。 | +| 同步类型 | 双向同步:从LocalStorage的对应属性到自定义组件,从自定义组件到LocalStorage对应属性。 | +| 被装饰变量的初始值 | 必须指定,如果LocalStorage实例中不存在属性,则作为初始化默认值,并存入LocalStorage中。 | + + +### 变量的传递/访问规则说明 + +| 传递/访问 | 说明 | +| ---------- | ---------------------------------------- | +| 从父节点初始化和更新 | 禁止,\@LocalStorageLink不支持从父节点初始化,只能从LocalStorage中key对应的属性初始化,如果没有对应key的话,将使用本地默认值初始化。 | +| 初始化子节点 | 支持,可用于初始化\@State、\@Link、\@Prop、\@Provide。 | +| 是否支持组件外访问 | 否。 | + + + **图2** \@LocalStorageLink初始化规则图示   + + +![zh-cn_image_0000001552855957](figures/zh-cn_image_0000001552855957.png) + + +### 观察变化和行为表现 + +**观察变化** + + +- 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。 + +- 当装饰的数据类型为class或者Object时,可以观察到赋值和属性赋值的变化,即Object.keys(observedObject)返回的所有属性。 + +- 当装饰的对象是array时,可以观察到数组添加、删除、更新数组单元的变化。 + + +**框架行为** + + +1. 当\@LocalStorageLink(key)装饰的数值改变被观察到时,修改将被同步回LocalStorage对应属性键值key的属性中。 + +2. LocalStorage中属性键值key对应的数据一旦改变,属性键值key绑定的所有的数据(包括双向\@LocalStorageLink和单向\@LocalStorageProp)都将同步修改; + +3. 当\@LocalStorageLink(key)装饰的数据本身是状态变量,它的改变不仅仅会同步回LocalStorage中,还会引起所属的自定义组件的重新渲染。 + + +## 使用场景 + + +### 应用逻辑使用LocalStorage + + +```ts +let storage = new LocalStorage({ 'PropA': 47 }); // 创建新实例并使用给定对象初始化 +let propA = storage.get('PropA') // propA == 47 +let link1 = storage.link('PropA'); // link1.get() == 47 +let link2 = storage.link('PropA'); // link2.get() == 47 +let prop = storage.prop('PropA'); // prop.get() = 47 +link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48 +prop.set(1); // one-way sync: prop.get()=1; but link1.get() == link2.get() == 48 +link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49 +``` + + +### 从UI内部使用LocalStorage + +除了应用程序逻辑使用LocalStorage,还可以借助LocalStorage相关的两个装饰器\@LocalStorageProp和\@LocalStorageLink,在UI组件内部获取到LocalStorage实例中存储的状态变量。 + +本示例以\@LocalStorage为例,展示了: + +- 使用构造函数创建LocalStorage实例storage; + +- 使用\@Entry装饰器将storage添加到CompA顶层组件中; + +- \@LocalStorageLink绑定LocalStorage对给定的属性,建立双向数据同步。 + + ```ts + // 创建新实例并使用给定对象初始化 + let storage = new LocalStorage({ 'PropA': 47 }); + + @Component + struct Child { + // @LocalStorageLink变量装饰器与LocalStorage中的'ProA'属性建立双向绑定 + @LocalStorageLink('PropA') storLink2: number = 1; + + build() { + Button(`Child from LocalStorage ${this.storLink2}`) + // 更改将同步至LocalStorage中的'ProA'以及Parent.storLink1 + .onClick(() => this.storLink2 += 1) + } + } + // 使LocalStorage可从@Component组件访问 + @Entry(storage) + @Component + struct CompA { + // @LocalStorageLink变量装饰器与LocalStorage中的'ProA'属性建立双向绑定 + @LocalStorageLink('PropA') storLink1: number = 1; + + build() { + Column({ space: 15 }) { + Button(`Parent from LocalStorage ${this.storLink1}`) // initial value from LocalStorage will be 47, because 'PropA' initialized already + .onClick(() => this.storLink1 += 1) + // @Component子组件自动获得对CompA LocalStorage实例的访问权限。 + Child() + } + } + } + ``` + + +### \@LocalStorageProp和LocalStorage单向同步的简单场景 + +在下面的示例中,CompA 组件和Child组件分别在本地创建了与storage的'PropA'对应属性的单向同步的数据,我们可以看到: + +- CompA中对this.storProp1的修改,只会在CompA中生效,并没有同步回storage; + +- Child组件中,Text绑定的storProp2 依旧显示47。 + + ```ts + // 创建新实例并使用给定对象初始化 + let storage = new LocalStorage({ 'PropA': 47 }); + // 使LocalStorage可从@Component组件访问 + @Entry(storage) + @Component + struct CompA { + // @LocalStorageProp变量装饰器与LocalStorage中的'ProA'属性建立单向绑定 + @LocalStorageProp('PropA') storProp1: number = 1; + + build() { + Column({ space: 15 }) { + // 点击后从47开始加1,只改变当前组件显示的storProp1,不会同步到LocalStorage中 + Button(`Parent from LocalStorage ${this.storProp1}`) + .onClick(() => this.storProp1 += 1) + Child() + } + } + } + + @Component + struct Child { + // @LocalStorageProp变量装饰器与LocalStorage中的'ProA'属性建立单向绑定 + @LocalStorageProp('PropA') storProp2: number = 2; + + build() { + Column({ space: 15 }) { + // 当CompA改变时,当前storProp2不会改变,显示47 + Text(`Parent from LocalStorage ${this.storProp2}`) + } + } + } + ``` + + +### \@LocalStorageLink和LocalStorage双向同步的简单场景 + +下面的示例展示了\@LocalStorageLink装饰的数据和LocalStorage双向同步的场景 + + +```ts +// 构造LocalStorage实例 +let storage = new LocalStorage({ 'PropA': 47 }); +// 调用link9+接口构造'PropA'的双向同步数据,linkToPropA 是全部变量 +let linkToPropA = storage.link('PropA'); + +@Entry(storage) +@Component +struct CompA { + + // @LocalStorageLink('PropA')在CompA自定义组件中创建'PropA'的双向同步数据,初始值为47,因为在构造LocalStorage已经给“PropA”设置47 + @LocalStorageLink('PropA') storLink: number = 1; + + build() { + Column() { + Text(`incr @LocalStorageLink variable`) + // 点击“incr @LocalStorageLink variable”,this.storLink加1,改变同步回storage,全局变量linkToPropA也会同步改变 + + .onClick(() => this.storLink += 1) + + // 并不建议在组件内使用全局变量linkToPropA.get(),因为可能会有生命周期不同引起的错误。 + Text(`@LocalStorageLink: ${this.storLink} - linkToPropA: ${linkToPropA.get()}`) + } + } +} +``` + + +### 兄弟节点之间同步状态变量 + +下面的示例展示了通过\@LocalStorageLink双向同步兄弟节点之间的状态。 + +先看Parent自定义组件中发生的变化: + +1. 点击“countStorage ${this.playCount} incr by 1”,this.playCount减1,修改同步回LocalStorage中,Child组件中的playCountLink绑定的组件会同步刷新; + +2. 点击“countStorage ${this.playCount} incr by 1”,调用LocalStorage的set接口,更新LocalStorage中“countStorage”对应的属性,Child组件中的playCountLink绑定的组件会同步刷新; + +3. Text组件“playCount in LocalStorage for debug ${storage.get<number>('countStorage')}”没有同步刷新,原因是因为storage.get<number>('countStorage')返回的是常规变量,常规变量的更新并不会引起Text组件的重新渲染。 + +Child自定义组件中的变化: + +1. playCountLink的刷新会同步回LocalStorage,并且引起兄弟组件和父组件相应的刷新。 + + ```ts + let storage = new LocalStorage({ countStorage: 1 }); + + @Component + struct Child { + // 子组件实例的名字 + label: string = 'no name'; + // 和LocalStorage中“countStorage”的双向绑定数据 + @LocalStorageLink('countStorage') playCountLink: number = 0; + + build() { + Row() { + Text(this.label) + .width(50).height(60).fontSize(12) + Text(`playCountLink ${this.playCountLink}: inc by 1`) + .onClick(() => { + this.playCountLink += 1; + }) + .width(200).height(60).fontSize(12) + }.width(300).height(60) + } + } + + @Entry(storage) + @Component + struct Parent { + @LocalStorageLink('countStorage') playCount: number = 0; + + build() { + Column() { + Row() { + Text('Parent') + .width(50).height(60).fontSize(12) + Text(`playCount ${this.playCount} dec by 1`) + .onClick(() => { + this.playCount -= 1; + }) + .width(250).height(60).fontSize(12) + }.width(300).height(60) + + Row() { + Text('LocalStorage') + .width(50).height(60).fontSize(12) + Text(`countStorage ${this.playCount} incr by 1`) + .onClick(() => { + storage.set('countStorage', 1 + storage.get('countStorage')); + }) + .width(250).height(60).fontSize(12) + }.width(300).height(60) + + Child({ label: 'ChildA' }) + Child({ label: 'ChildB' }) + + Text(`playCount in LocalStorage for debug ${storage.get('countStorage')}`) + .width(300).height(60).fontSize(12) + } + } + } + ``` + + +### 将LocalStorage实例从UIAbility共享到一个或多个视图 + +上面的实例中,LocalStorage的实例仅仅在一个\@Entry装饰的组件和其所属的子组件(一个页面)中共享,如果希望其在多个视图中共享,可以在所属UIAbility中创建LocalStorage实例,并调用windowStage.[loadContent](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/apis/js-apis-window.md#loadcontent9)。 + + +```ts +// EntryAbility.ts +import UIAbility from '@ohos.app.ability.UIAbility'; +import window from '@ohos.window'; + +export default class EntryAbility extends UIAbility { + storage: LocalStorage = new LocalStorage({ + 'PropA': 47 + }); + + onWindowStageCreate(windowStage: window.WindowStage) { + windowStage.loadContent('pages/Index', this.storage); + } +} +``` + +在UI页面通过GetShared接口获取在通过loadContent共享的LocalStorage实例。 + + +```ts +// 通过GetShared接口获取stage共享的Storage实例 +let storage = LocalStorage.GetShared() + +@Entry(storage) +@Component +struct CompA { + // can access LocalStorage instance using + // @LocalStorageLink/Prop decorated variables + @LocalStorageLink('PropA') varA: number = 1; + + build() { + Column() { + Text(`${this.varA}`).fontSize(50) + } + } +} +``` + + +> **说明:** +> +> 对于开发者更建议使用这个方式来构建LocalStorage的实例,并且在创建LocalStorage实例的时候就写入默认值,因为默认值可以作为运行异常的备份,也可以用作页面的单元测试。 + + diff --git a/zh-cn/application-dev/quick-start/arkts-observed-and-objectlink.md b/zh-cn/application-dev/quick-start/arkts-observed-and-objectlink.md new file mode 100644 index 0000000000000000000000000000000000000000..417c45305c862dd54f2fa343cdf8bb69ec020598 --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-observed-and-objectlink.md @@ -0,0 +1,382 @@ +# \@Observed和\@ObjectLink:嵌套类对象属性变化 + + +上文所述的装饰器仅能观察到第一层的变化,但是在实际应用开发中,应用会根据开发需要,封装自己的数据模型。对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这就引出了\@Observed/\@ObjectLink装饰器。 + + +> **说明:** +> 从API version 9开始,这两个装饰器支持在ArkTS卡片中使用。 + + +## 概述 + +\@ObjectLink和\@Observed类装饰器用于在涉及嵌套对象或数组的场景中进行双向数据同步: + +- 被\@Observed装饰的类,可以被观察到属性的变化; + +- 子组件中\@ObjectLink装饰器装饰的状态变量用于接受\@Observed装饰的类的实例,和父组件中对应的状态变量建立双向数据绑定。这个实例可以是数组中的被\@Observed装饰的项,或者是class object中是属性,这个属性同样也需要被\@Observed装饰。 + +- 单独使用\@Observed是没有任何作用的,需要搭配\@ObjectLink或者[\@Prop](arkts-prop.md)使用。 + + +## 装饰器说明 + +| \@Observed类装饰器 | 说明 | +| -------------- | --------------------------------- | +| 装饰器参数 | 无 | +| 类装饰器 | 装饰class。需要放在class的定义前,使用new创建类对象。 | + +| \@ObjectLink变量装饰器 | 说明 | +| ----------------- | ---------------------------------------- | +| 装饰器参数 | 无 | +| 同步类型 | 不与父组件中的任何类型同步变量。 | +| 允许装饰的变量类型 | 必须为被\@Observed装饰的class实例,必须指定类型。
不支持简单类型,可以使用[\@Prop](arkts-prop.md)。
\@ObjectLink的属性是可以改变的,但是变量的分配是不允许的,也就是说这个装饰器装饰变量是只读的,不能被改变。 | +| 被装饰变量的初始值 | 不允许。 | + +\@ObjectLink装饰的数据为可读示例。 + + +```ts +// 允许@ObjectLink装饰的数据属性赋值 +this.objLink.a= ... +// 不允许@ObjectLink装饰的数据自身赋值 +this.objLink= ... +``` + +> **说明:** +> +> \@ObjectLink装饰的变量不能被赋值,如果要使用赋值操作,请使用[@Prop](arkts-prop.md)。 +> +> - \@Prop装饰的变量和数据源的关系是是单向同步,\@Prop装饰的变量在本地拷贝了数据源,所以它允许本地更改,如果父组件中的数据源有更新,\@Prop装饰的变量本地的修改将被覆盖; +> +> - \@ObjectLink装饰的变量和数据源的关系是双向同步,\@ObjectLink装饰的变量相当于指向数据源的指针。如果一旦发生\@ObjectLink装饰的变量的赋值,则同步链将被打断。 + + +## 变量的传递/访问规则说明 + +| \@ObjectLink传递/访问 | 说明 | +| ----------------- | ---------------------------------------- | +| 从父组件初始化 | 必须指定。
初始化\@ObjectLink装饰的变量必须同时满足以下场景:
- 类型必须是\@Observed装饰的class。
- 初始化的数值需要是数组项,或者class的属性。
- 同步源的class或者数组必须是\@State,\@Link,\@Provide,\@Consume或者\@ObjectLink装饰的数据。
同步源是数组项的示例请参考[对象数组](#对象数组)。初始化的class的示例请参考[嵌套对象](#嵌套对象)。 | +| 与源对象同步 | 双向。 | +| 可以初始化子组件 | 允许,可用于初始化常规变量、\@State、\@Link、\@Prop、\@Provide | + + + **图1** 初始化规则图示   + + +![zh-cn_image_0000001502255262](figures/zh-cn_image_0000001502255262.png) + + +## 观察变化和行为表现 + + +### 观察的变化 + +\@Observed装饰的类,如果其属性为非简单类型,比如class、Object或者数组,也需要被\@Observed装饰,否则将观察不到其属性的变化。 + + +```ts +class ClassA { + public c: number; + + constructor(c: number) { + this.c = c; + } +} + +@Observed +class ClassB { + public a: ClassA; + public b: number; + + constructor(a: ClassA, b: number) { + this.a = a; + this.b = b; + } +} +``` + +以上示例中,ClassB被\@Observed装饰,其成员变量的赋值的变化是可以被观察到的,但对于ClassA,没有被\@Observed装饰,其属性的修改不能被观察到。 + + +```ts +@ObjectLink b: ClassB + +// 赋值变化可以被观察到 +this.b.a = new ClassA(5) +this.b.b = 5 + +// ClassA没有被@Observed装饰,其属性的变化观察不到 +this.b.a.c = 5 +``` + +\@ObjectLink:\@ObjectLink只能接受被\@Observed装饰class的实例,可以观察到: + +- 其属性的数值的变化,其中属性是指Object.keys(observedObject)返回的所有属性,示例请参考[嵌套对象](#嵌套对象)。 + +- 如果数据源是数组,则可以观察到数组item的替换,如果数据源是class,可观察到class的属性的变化,示例请参考[对象数组](#对象数组)。 + + +### 框架行为 + +1. 初始渲染: + 1. \@Observed装饰的class的实例会被不透明的代理对象包装,代理了class上的属性的setter和getter方法 + 2. 子组件中\@ObjectLink装饰的从父组件初始化,接受被\@Observed装饰的class的实例,\@ObjectLink的包装类会将自己注册给\@Observed class。 + +2. 属性更新:当\@Observed装饰的class属性改变时,会走到代理的setter和getter,然后遍历依赖它的\@ObjectLink包装类,通知数据更新。 + + +## 使用场景 + + +### 嵌套对象 + +以下是嵌套类对象的数据结构。 + + +```ts +// objectLinkNestedObjects.ets +let NextID: number = 1; + +@Observed +class ClassA { + public id: number; + public c: number; + + constructor(c: number) { + this.id = NextID++; + this.c = c; + } +} + +@Observed +class ClassB { + public a: ClassA; + + constructor(a: ClassA) { + this.a = a; + } +} +``` + + + 以下组件层次结构呈现的是此数据结构 + +```ts +@Component +struct ViewA { + label: string = 'ViewA1'; + @ObjectLink a: ClassA; + + build() { + Row() { + Button(`ViewA [${this.label}] this.a.c=${this.a.c} +1`) + .onClick(() => { + this.a.c += 1; + }) + } + } +} + +@Entry +@Component +struct ViewB { + @State b: ClassB = new ClassB(new ClassA(0)); + + build() { + Column() { + ViewA({ label: 'ViewA #1', a: this.b.a }) + ViewA({ label: 'ViewA #2', a: this.b.a }) + + Button(`ViewB: this.b.a.c+= 1`) + .onClick(() => { + this.b.a.c += 1; + }) + Button(`ViewB: this.b.a = new ClassA(0)`) + .onClick(() => { + this.b.a = new ClassA(0); + }) + Button(`ViewB: this.b = new ClassB(ClassA(0))`) + .onClick(() => { + this.b = new ClassB(new ClassA(0)); + }) + } + } +} +``` + + +ViewB中的事件句柄: + + +- this.b.a = new ClassA(0) 和this.b = new ClassB(new ClassA(0)): 对\@State装饰的变量b和其属性的修改。 + +- this.b.a.c = ... :该变化属于第二次的变化,[@State](arkts-state.md#观察变化)无法观察到第二层的变化,但是ClassA被\@Observed装饰,ClassA的属性c的变化可以被\@ObjectLink观察到。 + + +ViewA中的事件句柄: + + +- this.a.c += 1:对\@ObjectLink变量a的修改,将触发Button组件的刷新。\@ObjectLink和\@Prop不同,\@ObjectLink不拷贝来自父组件的数据源,而是在本地构建了指向其数据源的引用。 + +- \@ObjectLink变量是只读的,this.a = new ClassA(...)是不允许的,因为一旦赋值操作发生,指向数据源的引用将被重置,同步将被打断。 + + +### 对象数组 + +对象数组是一种常用的数据结构。以下示例展示了数组对象的用法。 + + +```ts +@Component +struct ViewA { + // 子组件ViewA的@ObjectLink的类型是ClassA + @ObjectLink a: ClassA; + label: string = 'ViewA1'; + + build() { + Row() { + Button(`ViewA [${this.label}] this.a.c = ${this.a.c} +1`) + .onClick(() => { + this.a.c += 1; + }) + } + } +} + +@Entry +@Component +struct ViewB { + // ViewB中有@State装饰的ClassA[] + @State arrA: ClassA[] = [new ClassA(0), new ClassA(0)]; + + build() { + Column() { + ForEach(this.arrA, + (item) => { + ViewA({ label: `#${item.id}`, a: item }) + }, + (item) => item.id.toString() + ) + // 使用@State装饰的数组的数组项初始化@ObjectLink,其中数组项是被@Observed装饰的ClassA的实例 + ViewA({ label: `ViewA this.arrA[first]`, a: this.arrA[0] }) + ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1] }) + + Button(`ViewB: reset array`) + .onClick(() => { + this.arrA = [new ClassA(0), new ClassA(0)]; + }) + Button(`ViewB: push`) + .onClick(() => { + this.arrA.push(new ClassA(0)) + }) + Button(`ViewB: shift`) + .onClick(() => { + this.arrA.shift() + }) + Button(`ViewB: chg item property in middle`) + .onClick(() => { + this.arrA[Math.floor(this.arrA.length / 2)].c = 10; + }) + Button(`ViewB: chg item property in middle`) + .onClick(() => { + this.arrA[Math.floor(this.arrA.length / 2)] = new ClassA(11); + }) + } + } +} +``` + +- this.arrA[Math.floor(this.arrA.length/2)] = new ClassA(..) :该状态变量的改变触发2次更新: + 1. ForEach:数组项的赋值导致ForEach的[itemGenerator](arkts-rendering-control-foreach.md#接口描述)被修改,因此数组项被识别为有更改,ForEach的item builder将执行,创建新的ViewA组件实例。 + 2. ViewA({ label: `ViewA this.arrA[first]`, a: this.arrA[0] }):上述更改改变了数组中第一个元素,所以绑定this.arrA[0]的ViewA将被更新; + +- this.arrA.push(new ClassA(0)) : 将触发2次不同效果的更新: + 1. ForEach:新添加的ClassA对象对于ForEach是未知的[itemGenerator](arkts-rendering-control-foreach.md#接口描述),ForEach的item builder将执行,创建新的ViewA组件实例。 + 2. ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1] }):数组的最后一项有更改,因此引起第二个ViewA的实例的更改。对于ViewA({ label: `ViewA this.arrA[first]`, a: this.arrA[0] }),数组的更改并没有触发一个数组项更改的改变,所以第一个ViewA不会刷新。 + +- this.arrA[Math.floor(this.arrA.length/2)].c:[@State](arkts-state.md#观察变化)无法观察到第二层的变化,但是ClassA被\@Observed装饰,ClassA的属性的变化将被\@ObjectLink观察到。 + + +### 二维数组 + +使用\@Observed观察二维数组的变化。可以声明一个被\@Observed装饰的继承Array的子类。 + + +```ts +@Observed +class StringArray extends Array { +} +``` + +使用new StringArray()来构造StringArray的实例,new运算符使得\@Observed生效,\@Observed观察到StringArray的属性变化。 + +声明一个从Array扩展的类class StringArray extends Array<String> {},并创建StringArray的实例。\@Observed装饰的类需要使用new运算符来构建class实例。 + + +```ts +@Observed +class StringArray extends Array { +} + +@Component +struct ItemPage { + @ObjectLink itemArr: StringArray; + + build() { + Row() { + Text('ItemPage') + .width(100).height(100) + + ForEach(this.itemArr, + item => { + Text(item) + .width(100).height(100) + }, + item => item + ) + } + } +} + +@Entry +@Component +struct IndexPage { + @State arr: Array = [new StringArray(), new StringArray(), new StringArray()]; + + build() { + Column() { + ItemPage({ itemArr: this.arr[0] }) + ItemPage({ itemArr: this.arr[1] }) + ItemPage({ itemArr: this.arr[2] }) + + Divider() + + ForEach(this.arr, + itemArr => { + ItemPage({ itemArr: itemArr }) + }, + itemArr => itemArr[0] + ) + + Divider() + + Button('update') + .onClick(() => { + console.error('Update all items in arr'); + if (this.arr[0][0] !== undefined) { + // 正常情况下需要有一个真实的ID来与ForEach一起使用,但此处没有 + // 因此需要确保推送的字符串是唯一的。 + this.arr[0].push(`${this.arr[0].slice(-1).pop()}${this.arr[0].slice(-1).pop()}`); + this.arr[1].push(`${this.arr[1].slice(-1).pop()}${this.arr[1].slice(-1).pop()}`); + this.arr[2].push(`${this.arr[2].slice(-1).pop()}${this.arr[2].slice(-1).pop()}`); + } else { + this.arr[0].push('Hello'); + this.arr[1].push('World'); + this.arr[2].push('!'); + } + }) + } + } +} +``` diff --git a/zh-cn/application-dev/quick-start/arkts-other-state-mgmt-functions-overview.md b/zh-cn/application-dev/quick-start/arkts-other-state-mgmt-functions-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..f47b542b64e8f56be9986970fdc55bd02c8b5906 --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-other-state-mgmt-functions-overview.md @@ -0,0 +1,9 @@ +# 其他状态管理概述 + + +除了前面章节提到的组件状态管理和应用状态管理,ArkTS还提供了\@Watch和$$来为开发者提供更多功能: + + +- \@Watch用于监听状态变量的变化。 + +- $$运算符:给内置组件提供TS变量的引用,使得TS变量和内置组件的内部状态保持同步。 diff --git a/zh-cn/application-dev/quick-start/arkts-page-custom-components-lifecycle.md b/zh-cn/application-dev/quick-start/arkts-page-custom-components-lifecycle.md new file mode 100644 index 0000000000000000000000000000000000000000..01432b784c0432abcfa6c994dcdaaeeafd4fb673 --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-page-custom-components-lifecycle.md @@ -0,0 +1,186 @@ +# 页面和自定义组件生命周期 + + +在开始之前,我们先明确自定义组件和页面的关系: + + +- 自定义组件:\@Component装饰的UI单元,可以组合多个系统组件实现UI的复用。 + +- 页面:即应用的UI页面。可以由一个或者多个自定义组件组成,\@Entry装饰的自定义组件为页面的入口组件,即页面的根节点,一个页面有且仅能有一个\@Entry。只有被\@Entry装饰的组件才可以调用页面的生命周期。 + + +页面生命周期,即被\@Entry装饰的组件生命周期,提供以下生命周期接口: + + +- [onPageShow](../reference/arkui-ts/ts-custom-component-lifecycle.md#onpageshow):页面每次显示时触发。 + +- [onPageHide](../reference/arkui-ts/ts-custom-component-lifecycle.md#onpagehide):页面每次隐藏时触发一次。 + +- [onBackPress](../reference/arkui-ts/ts-custom-component-lifecycle.md#onbackpress):当用户点击返回按钮时触发。 + + +组件生命周期,即一般用\@Component装饰的自定义组件,提供以下生命周期接口: + + +- [aboutToAppear](../reference/arkui-ts/ts-custom-component-lifecycle.md#abouttoappear):组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其build()函数之前执行。 + +- [aboutToDisappear](../reference/arkui-ts/ts-custom-component-lifecycle.md#abouttodisappear):在自定义组件即将析构销毁时执行。 + + +生命周期流程如下图所示,下图展示的是被\@Entry装饰的组件(首页)生命周期。 + + +![zh-cn_image_0000001502372786](figures/zh-cn_image_0000001502372786.png) + + +根据上面的流程图,我们从自定义组件的初始创建、重新渲染和删除来详细解释。 + + +## 自定义组件的创建和渲染流程 + +1. 自定义组件的创建:自定义组件的实例由ArkUI框架创建。 + +2. 初始化自定义组件的成员变量:通过本地默认值或者构造方法传递参数来初始化自定义组件的成员变量,初始化顺序为成员变量的定义顺序。 + +3. 如果开发者定义了aboutToAppear,则执行aboutToAppear方法。 + +4. 在首次渲染的时候,执行build方法渲染系统组件,如果有自定义子组件,则创建自定义组件的实例。在执行build()函数的过程中,框架会观察每个状态变量的读取状态,将保存两个map: + 1. 状态变量 -> UI组件(包括ForEach和if)。 + 2. UI组件 -> 此组件的更新函数,即一个lambda方法,作为build()函数的子集,创建对应的UI组件并执行其属性方法,示意如下。 + + + ```ts + build() { + ... + this.observeComponentCreation(() => { + Button.create(); + }) + + this.observeComponentCreation(() => { + Text.create(); + }) + ... + } + ``` + + +当应用在后台启动时,此时应用进程并没有销毁,所以仅需要执行onPageShow。 + + +## 自定义组件重新渲染 + +当事件句柄被触发(比如设置了点击事件,即触发点击事件)改变了状态变量时,或者LocalStorage / AppStorage中的属性更改,并导致绑定的状态变量更改其值时: + + +1. 框架观察到了变化,将启动重新渲染。 + +2. 根据框架持有的两个map(自定义组件的创建和渲染流程中第4步),框架可以知道该状态变量管理了哪些UI组件,以及这些UI组件对应的更新函数。执行这些UI组件的更新函数,实现最小化更新。 + + +## 自定义组件的删除 + +如果if组件的分支改变,或者ForEach循环渲染中数组的个数改变,组件将被删除: + + +1. 在删除组件之前,将调用其aboutToDisappear生命周期函数,标记着该节点将要被销毁。ArkUI的节点删除机制是:后端节点直接从组件树上摘下,后端节点被销毁,对前端节点解引用,当前端节点已经没有引用时,将被JS虚拟机垃圾回收。 + +2. 自定义组件和它的变量将被删除,如果其有同步的变量,比如[@Link](arkts-link.md)、[@Prop](zh-cn_topic_0000001524296665.xml)、[@StorageLink](arkts-appstorage.md#storagelink),将从[同步源](arkts-state-management-overview.md#基本概念)上取消注册。 + + +不建议在生命周期aboutToDisappear内使用async await,如果在生命周期的aboutToDisappear使用异步操作(Promise或者回调方法),自定义组件将被保留在Promise的闭包中,直到回调方法被执行完,这个行为阻止了自定义组件的垃圾回收。 + + +以下示例展示了生命周期的调用时机: + + + +```ts +// Index.ets +import router from '@ohos.router'; + +@Entry +@Component +struct MyComponent { + @State showChild: boolean = true; + + // 只有被@Entry装饰的组件才可以调用页面的生命周期 + onPageShow() { + console.info('Index onPageShow'); + } + // 只有被@Entry装饰的组件才可以调用页面的生命周期 + onPageHide() { + console.info('Index onPageHide'); + } + + // 只有被@Entry装饰的组件才可以调用页面的生命周期 + onBackPress() { + console.info('Index onBackPress'); + } + + // 组件生命周期 + aboutToAppear() { + console.info('MyComponent aboutToAppear'); + } + + // 组件生命周期 + aboutToDisappear() { + console.info('MyComponent aboutToDisappear'); + } + + build() { + Column() { + // this.showChild为true,创建Child子组件,执行Child aboutToAppear + if (this.showChild) { + Child() + } + // this.showChild为false,删除Child子组件,执行Child aboutToDisappear + Button('create or delete Child').onClick(() => { + this.showChild = false; + }) + // push到Page2页面,执行onPageHide + Button('push to next page') + .onClick(() => { + router.pushUrl({ url: 'pages/Page2' }); + }) + } + + } +} + +@Component +struct Child { + @State title: string = 'Hello World'; + // 组件生命周期 + aboutToDisappear() { + console.info('[lifeCycle] Child aboutToDisappear') + } + // 组件生命周期 + aboutToAppear() { + console.info('[lifeCycle] Child aboutToAppear') + } + + build() { + Text(this.title).fontSize(50).onClick(() => { + this.title = 'Hello ArkUI'; + }) + } +} +``` + + +以上示例中,Index页面包含两个自定义组件,一个是被\@Entry装饰的MyComponent,也是页面的入口组件,即页面的根节点;一个是Child,是MyComponent的子组件。只有\@Entry装饰的节点才可以生效页面的生命周期方法,所以MyComponent中声明了当前Index页面的页面生命周期函数。MyComponent和其子组件Child也同时也声明了组件的生命周期函数。 + + +- 应用冷启动的初始化流程为:MyComponent aboutToAppear --> MyComponent build --> Child aboutToAppear --> Child build --> Child build执行完毕 --> MyComponent build执行完毕 --> Index onPageShow。 + +- 点击“delete Child”,if绑定的this.showChild变成false,删除Child组件,会执行Child aboutToDisappear方法。 + + +- 点击“push to next page”,调用router.pushUrl接口,跳转到另外一个页面,当前Index页面隐藏,执行页面生命周期Index onPageHide。此处调用的是router.pushUrl接口,Index页面被隐藏,并没有销毁,所以只调用onPageHide。跳转到新页面后,执行初始化新页面的生命周期的流程。 + +- 如果调用的是router.replaceUrl,则当前Index页面被销毁,执行的生命周期流程将变为:Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear。上文已经提到,组件的销毁是从组件树上直接摘下子树,所以先调用父组件的aboutToDisappear,再调用子组件的aboutToDisAppear,然后执行初始化新页面的生命周期流程。 + +- 点击返回按钮,触发页面生命周期Index onBackPress。最小化应用或者应用进入后台,触发Index onPageHide。这两个状态下应用都没有被销毁,所以并不会执行组件的aboutToDisappear 。应用回到前台,执行Index onPageShow。 + + +- 退出应用,执行Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear。 diff --git a/zh-cn/application-dev/quick-start/arkts-persiststorage.md b/zh-cn/application-dev/quick-start/arkts-persiststorage.md new file mode 100644 index 0000000000000000000000000000000000000000..a50cbe24ed891c59250e206ccfa291da2c974481 --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-persiststorage.md @@ -0,0 +1,114 @@ +# PersistentStorage:持久化存储UI状态 + + +前两个小节介绍的LocalStorage和AppStorage都是运行时的内存,但是在应用退出再次启动后,依然能保存选定的结果,是应用开发中十分常见的现象,这就需要用到PersistentStorage。 + + +PersistentStorage是应用程序中的可选单例对象。此对象的作用是持久化存储选定的AppStorage属性,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同。 + + +## 概述 + +PersistentStorage将选定的AppStorage属性保留在设备磁盘上。应用程序通过API,以决定哪些AppStorage属性应借助PersistentStorage持久化。UI和业务逻辑不直接访问PersistentStorage中的属性,所有属性访问都是对AppStorage的访问,AppStorage中的更改会自动同步到PersistentStorage。 + +PersistentStorage和AppStorage中的属性建立双向同步。应用开发通常通过AppStorage访问PersistentStorage,另外还有一些接口可以用于管理持久化属性,但是业务逻辑始终是通过AppStorage获取和设置属性的。 + + +## 限制条件 + +持久化数据是一个相对缓慢的操作,应用程序应避免以下情况: + +- 持久化大型数据集。 + +- 持久化经常变化的变量。 + +当持久化更改的过程变得太繁重时,PersistentStorage实现可能会限制持久化属性更改的频率。 + + +## 使用场景 + + +### 从AppStorage中访问PersistentStorage初始化的属性 + +1. 初始化PersistentStorage: + + ```ts + PersistentStorage.PersistProp('aProp', 47); + ``` + +2. 在AppStorage获取对应属性: + + ```ts + AppStorage.Get('aProp'); // returns 47 + ``` + + 或在组件内部定义: + + + ```ts + @StorageLink('aProp') aProp: number = 48; + ``` + +完整代码如下: + + +```ts +PersistentStorage.PersistProp('aProp', 47); + +@Entry +@Component +struct Index { + @State message: string = 'Hello World' + @StorageLink('aProp') aProp: number = 48 + + build() { + Row() { + Column() { + Text(this.message) + // 应用退出时会保存当前结果。重新启动后,会显示上一次的保存结果 + Text(`${this.aProp}`) + .onClick(() => { + this.aProp += 1; + }) + } + } + } +} +``` + +- 新应用安装后首次启动运行: + 1. 调用PersistProp初始化PersistentStorage,首先查询在PersistentStorage本地文件中是否存在“aProp”,查询结果为不存在,因为应用是第一次安装。; + 2. 接着查询属性“aProp”在AppStorage中是否存在,依旧不存在; + 3. 在AppStorge中创建名为“aProp”的number类型属性,属性初始值是定义的默认值47; + 4. PersistentStorage将属性“aProp”和值47写入磁盘,AppStorage中“aProp”对应的值和其后续的更改将被持久化; + 5. 在Index组件中创建状态变量\@StorageLink('aProp') aProp,和AppStorage中“aProp”双向绑定,在创建的过程中会在AppStorage中查找,成功找到“aProp”,所以使用其在AppStorage找到的值47。 + + **图1** PersistProp初始化流程   + +![zh-cn_image_0000001553348833](figures/zh-cn_image_0000001553348833.png) + +- 触发点击事件后: + 1. 状态变量\@StorageLink('aProp') aProp改变,触发Text组件重新刷新; + 2. \@StorageLink装饰的变量是和AppStorage中建立双向同步的,所以\@StorageLink('aProp') aProp的变化会被同步回AppStorage中; + 3. AppStorage中“aProp”属性的改变会同步到所有绑定该“aProp”的单向或者双向变量,在本示例中没有其他的绑定“aProp”的变量; + 4. 因为“aProp”对应的属性已经被持久化,所以在AppStorage中“aProp”的改变会触发PersistentStorage将新的改变写会本地磁盘。 + +- 后续启动应用: + 1. 执行PersistentStorage.PersistProp('aProp', 47),在首先查询在PersistentStorage本地文件查询“aProp”属性,成功查询到; + 2. 将在PersistentStorage查询到的值写入AppStorage中; + 3. 在Index组件里,\@StorageLink绑定的“aProp”为PersistentStorage写入AppStorage中的值,即为上一次退出引用存入的值。 + + +### 在PersistentStorage之前访问AppStorage中的属性 + +该示例为反例。在调用PersistentStorage.PersistProp或者PersistProps之前使用接口访问AppStorage中的属性是错误的,因为这样的调用顺序会丢失上一次应用程序运行中的属性值: + + +```ts +let aProp = AppStorage.SetOrCreate('aProp', 47); +PersistentStorage.PersistProp('aProp', 48); +``` + +应用在非首次运行时,先执行AppStorage.SetOrCreate('aProp', 47):属性“aProp”在AppStorage中创建,其类型为number,其值设置为指定的默认值47。'aProp'是持久化的属性,所以会被写回PersistentStorage磁盘中,PersistentStorage存储的上次退出应用的值丢失。 + +PersistentStorage.PersistProp('aProp', 48):在PersistentStorage中查找到“aProp”,找到,值为47。 diff --git a/zh-cn/application-dev/quick-start/arkts-prop.md b/zh-cn/application-dev/quick-start/arkts-prop.md new file mode 100644 index 0000000000000000000000000000000000000000..f85f88c5d57eae8be12660cbbc9ccb0a2c236f5c --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-prop.md @@ -0,0 +1,368 @@ +# \@Prop:父子单向同步 + + +\@Prop装饰的变量可以和父组件建立单向的同步关系。\@Prop装饰的变量是可变的,但是变化不会同步回其父组件。 + + +> **说明:** +> +> 从API version 9开始,该装饰器支持在ArkTS卡片中使用。 + + +## 概述 + +\@Prop装饰的变量和父组件建立单向的同步关系: + +- \@Prop变量允许在本地修改,但修改后的变化不会同步回父组件。 + +- 当父组件中的数据源更改时,与之相关的\@Prop装饰的变量都会自动更新。如果子组件已经在本地修改了\@Prop装饰的相关变量值,而在父组件中对应的\@State装饰的变量被修改后,子组件本地修改的\@Prop装饰的相关变量值将被覆盖。 + + +## 装饰器使用规则说明 + +| \@Prop变量装饰器 | 说明 | +| ----------- | ---------------------------------------- | +| 装饰器参数 | 无 | +| 同步类型 | 单向同步:对父组件状态变量值的修改,将同步给子组件\@Prop装饰的变量,子组件\@Prop变量的修改不会同步到父组件的状态变量上 | +| 允许装饰的变量类型 | string、number、boolean、enum类型。
不支持any,不允许使用undefined和null。
必须指定类型。
在父组件中,传递给\@Prop装饰的值不能为undefined或者null,反例如下所示。
CompA ({ aProp: undefined })
CompA ({ aProp: null })
\@Prop和[数据源](arkts-state-management-overview.md#基本概念)类型需要相同,有以下三种情况(数据源以\@State为例):
- \@Prop装饰的变量和父组件状态变量类型相同,即\@Prop : S和\@State : S,示例请参考[父组件@State到子组件@Prop简单数据类型同步](#父组件state到子组件prop简单数据类型同步)。
- 当父组件的状态变量为数组时,\@Prop装饰的变量和父组件状态变量的数组项类型相同,即\@Prop : S和\@State : Array<S>,示例请参考[父组件@State数组中的项到子组件@Prop简单数据类型同步](#父组件state数组项到子组件prop简单数据类型同步);
- 当父组件状态变量为Object或者class时,\@Prop装饰的变量和父组件状态变量的属性类型相同,即\@Prop : S和\@State : { propA: S },示例请参考[从父组件中的@State类对象属性到@Prop简单类型的同步](#从父组件中的state类对象属性到prop简单类型的同步)。 | +| 被装饰变量的初始值 | 允许本地初始化。 | + + +## 变量的传递/访问规则说明 + +| 传递/访问 | 说明 | +| --------- | ---------------------------------------- | +| 从父组件初始化 | 如果本地有初始化,则是可选的。没有的话,则必选,支持父组件中的常规变量、\@State、\@Link、\@Prop、\@Provide、\@Consume、\@ObjectLink、\@StorageLink、\@StorageProp、\@LocalStorageLink和\@LocalStorageProp去初始化子组件中的\@Prop变量。 | +| 用于初始化子组件 | \@Prop支持去初始化子组件中的常规变量、\@State、\@Link、\@Prop、\@Provide。 | +| 是否支持组件外访问 | \@Prop装饰的变量是私有的,只能在组件内访问。 | + + + **图1** 初始化规则图示   + + +![zh-cn_image_0000001552972029](figures/zh-cn_image_0000001552972029.png) + + +## 观察变化和行为表现 + + +### 观察变化 + +\@Prop装饰的数据可以观察到以下变化。 + +- 当装饰的类型是允许的类型,即string、number、boolean、enum类型都可以观察到的赋值变化; + + ```ts + // 简单类型 + @Prop count: number; + // 赋值的变化可以被观察到 + this.count = 1; + ``` + +对于\@State和\@Prop的同步场景: + +- 使用父组件中\@State变量的值初始化子组件中的\@Prop变量。当\@State变量变化时,该变量值也会同步更新至\@Prop变量。 + +- \@Prop装饰的变量的修改不会影响其数据源\@State装饰变量的值。 + +- 除了\@State,数据源也可以用\@Link或\@Prop装饰,对\@Prop的同步机制是相同的。 + +- 数据源和\@Prop变量的类型需要相同。 + + +### 框架行为 + +要理解\@Prop变量值初始化和更新机制,有必要了解父组件和拥有\@Prop变量的子组件初始渲染和更新流程。 + +1. 初始渲染: + 1. 执行父组件的build()函数将创建子组件的新实例,将数据源传递给子组件; + 2. 初始化子组件\@Prop装饰的变量。 + +2. 更新: + 1. 子组件\@Prop更新时,更新仅停留在当前子组件,不会同步回父组件; + 2. 当父组件的数据源更新时,子组件的\@Prop装饰的变量将被来自父组件的数据源重置,所有\@Prop装饰的本地的修改将被父组件的更新覆盖。 + + +## 使用场景 + + +### 父组件\@State到子组件\@Prop简单数据类型同步 + + +以下示例是\@State到子组件\@Prop简单数据同步,父组件ParentComponent的状态变量countDownStartValue初始化子组件CountDownComponent中\@Prop装饰的count,点击“Try again”,count的修改仅保留在CountDownComponent 不会同步给父组件CountDownComponent。 + + +ParentComponent的状态变量countDownStartValue的变化将重置CountDownComponent的count。 + + + +```ts +@Component +struct CountDownComponent { + @Prop count: number; + costOfOneAttempt: number = 1; + + build() { + Column() { + if (this.count > 0) { + Text(`You have ${this.count} Nuggets left`) + } else { + Text('Game over!') + } + // @Prop装饰的变量不会同步给父组件 + Button(`Try again`).onClick(() => { + this.count -= this.costOfOneAttempt; + }) + } + } +} + +@Entry +@Component +struct ParentComponent { + @State countDownStartValue: number = 10; + + build() { + Column() { + Text(`Grant ${this.countDownStartValue} nuggets to play.`) + // 父组件的数据源的修改会同步给子组件 + Button(`+1 - Nuggets in New Game`).onClick(() => { + this.countDownStartValue += 1; + }) + // 父组件的修改会同步给子组件 + Button(`-1 - Nuggets in New Game`).onClick(() => { + this.countDownStartValue -= 1; + }) + + CountDownComponent({ count: this.countDownStartValue, costOfOneAttempt: 2 }) + } + } +} +``` + + +在上面的示例中: + + +1. CountDownComponent子组件首次创建时其\@Prop装饰的count变量将从父组件\@State装饰的countDownStartValue变量初始化; + +2. 按“+1”或“-1”按钮时,父组件的\@State装饰的countDownStartValue值会变化,这将触发父组件重新渲染,在父组件重新渲染过程中会刷新使用countDownStartValue状态变量的UI组件并单向同步更新CountDownComponent子组件中的count值; + +3. 更新count状态变量值也会触发CountDownComponent的重新渲染,在重新渲染过程中,评估使用count状态变量的if语句条件(this.count > 0),并执行true分支中的使用count状态变量的UI组件相关描述来更新Text组件的UI显示; + +4. 当按下子组件CountDownComponent的“Try again”按钮时,其\@Prop变量count将被更改,但是count值的更改不会影响父组件的countDownStartValue值; + +5. 父组件的countDownStartValue值会变化时,父组件的修改将覆盖掉子组件CountDownComponent中count本地的修改。 + + +### 父组件\@State数组项到子组件\@Prop简单数据类型同步 + + +父组件中\@State如果装饰的数组,其数组项也可以初始化\@Prop。以下示例中父组件Index中\@State装饰的数组arr,将其数组项初始化子组件Child中\@Prop装饰的value。 + + + +```ts +@Component +struct Child { + @Prop value: number; + + build() { + Text(`${this.value}`) + .fontSize(50) + .onClick(()=>{this.value++}) + } +} + +@Entry +@Component +struct Index { + @State arr: number[] = [1,2,3]; + + build() { + Row() { + Column() { + Child({value: this.arr[0]}) + Child({value: this.arr[1]}) + Child({value: this.arr[2]}) + + Divider().height(5) + + ForEach(this.arr, + item => { + Child({value: item}) + }, + item => item.toString() + ) + Text('replace entire arr') + .fontSize(50) + .onClick(()=>{ + // 两个数组都包含项“3”。 + this.arr = this.arr[0] == 1 ? [3,4,5] : [1,2,3]; + }) + } + } + } +} +``` + + +初始渲染创建6个子组件实例,每个\@Prop装饰的变量初始化都在本地拷贝了一份数组项。子组件onclick事件处理程序会更改局部变量值。 + + +假设我们点击了多次,所有变量的本地取值都是“7”。 + + + +``` +7 +7 +7 +---- +7 +7 +7 +``` + + +单击replace entire arr后,屏幕将显示以下信息。 + + + +``` +3 +4 +5 +---- +7 +4 +5 +``` + + +- 在子组件Child中做的所有的修改都不会同步回父组件Index组件,所以即使6个组件显示都为7,但在父组件Index中,this.arr保存的值依旧是[1,2,3]。 + +- 点击replace entire arr,this.arr[0] == 1成立,将this.arr赋值为[3, 4, 5]; + +- 因为this.arr[0]已更改,Child({value: this.arr[0]})组件将this.arr[0]更新同步到实例\@Prop装饰的变量。Child({value: this.arr[1]})和Child({value: this.arr[2]})的情况也类似。 + + +- this.arr的更改触发ForEach更新,this.arr更新的前后都有数值为3的数组项:[3, 4, 5] 和[1, 2, 3]。根据diff机制,数组项“3”将被保留,删除“1”和“2”的数组项,添加为“4”和“5”的数组项。这就意味着,数组项“3”的组件不会重新生成,而是将其移动到第一位。所以“3”对应的组件不会更新,此时“3”对应的组件数值为“7”,ForEach最终的渲染结果是“7”,“4”,“5”。 + + +### 从父组件中的\@State类对象属性到\@Prop简单类型的同步 + +如果图书馆有一本图书和两位用户,每位用户都可以将图书标记为已读,此标记行为不会影响其它读者用户。从代码角度讲,对\@Prop图书对象的本地更改不会同步给图书馆组件中的\@State图书对象。 + + +```ts +class Book { + public title: string; + public pages: number; + public readIt: boolean = false; + + constructor(title: string, pages: number) { + this.title = title; + this.pages = pages; + } +} + +@Component +struct ReaderComp { + @Prop title: string; + @Prop readIt: boolean; + + build() { + Row() { + Text(this.title) + Text(`... ${this.readIt ? 'I have read' : 'I have bot read it'}`) + .onClick(() => this.readIt = true) + } + } +} + +@Entry +@Component +struct Library { + @State book: Book = new Book('100 secrets of C++', 765); + + build() { + Column() { + ReaderComp({ title: this.book.title, readIt: this.book.readIt }) + ReaderComp({ title: this.book.title, readIt: this.book.readIt }) + } + } +} +``` + + +### \@Prop本地初始化不和父组件同步 + +为了支持\@Component装饰的组件复用场景,\@Prop支持本地初始化,这样可以让\@Prop是否与父组件建立同步关系变得可选。当且仅当\@Prop有本地初始化时,从父组件向子组件传递\@Prop的数据源才是可选的。 + +下面的示例中,子组件包含两个\@Prop变量: + +- \@Prop customCounter没有本地初始化,所以需要父组件提供数据源去初始化\@Prop,并当父组件的数据源变化时,\@Prop也将被更新; + +- \@Prop customCounter2有本地初始化,在这种情况下,\@Prop依旧允许但非强制父组件同步数据源给\@Prop。 + + +```ts +@Component +struct MyComponent { + @Prop customCounter: number; + @Prop customCounter2: number = 5; + + build() { + Column() { + Row() { + Text(`From Main: ${this.customCounter}`).width(90).height(40).fontColor('#FF0010') + } + + Row() { + Button('Click to change locally !').width(480).height(60).margin({ top: 10 }) + .onClick(() => { + this.customCounter2++ + }) + }.height(100).width(480) + + Row() { + Text(`Custom Local: ${this.customCounter2}`).width(90).height(40).fontColor('#FF0010') + } + } + } +} + +@Entry +@Component +struct MainProgram { + @State mainCounter: number = 10; + + build() { + Column() { + Row() { + Column() { + Button('Click to change number').width(480).height(60).margin({ top: 10, bottom: 10 }) + .onClick(() => { + this.mainCounter++ + }) + } + } + + Row() { + Column( + // customCounter必须从父组件初始化,因为MyComponent的customCounter成员变量缺少本地初始化;此处,customCounter2可以不做初始化。 + MyComponent({ customCounter: this.mainCounter }) + // customCounter2也可以从父组件初始化,父组件初始化的值会覆盖子组件customCounter2的本地初始化的值 + MyComponent({ customCounter: this.mainCounter, customCounter2: this.mainCounter }) + }.width('40%') + } + + Row() { + Text('').width(480).height(10) + } + } + } +} +``` + \ No newline at end of file diff --git a/zh-cn/application-dev/quick-start/arkts-provide-and-consume.md b/zh-cn/application-dev/quick-start/arkts-provide-and-consume.md new file mode 100644 index 0000000000000000000000000000000000000000..f0b2f76a1b188a93742f26a04c54be00c44199fa --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-provide-and-consume.md @@ -0,0 +1,171 @@ +# \@Provide和\@Consume:与后代组件双向同步 + + +\@Provide和\@Consume,应用于与后代组件的双向数据同步,应用于状态数据在多个层级之间传递的场景。不同于上文提到的父子组件之间通过命名参数机制传递,\@Provide和\@Consume摆脱参数传递机制的束缚,实现跨层级传递。 + + +其中\@Provide装饰的变量是在祖先节点中,可以理解为被“提供”给后代的状态变量。\@Consume装饰的变量是在后代组件中,去“消费(绑定)”祖先节点提供的变量。 + + +> **说明:** +> +> 从API version 9开始,这两个装饰器支持在ArkTS卡片中使用。 + + +## 概述 + +\@Provide/\@Consume装饰的状态变量有以下特性: + +- \@Provide装饰的状态变量自动对其所有后代组件可用,即该变量被“provide”给他的后代组件。由此可见,\@Provide的方便之处在于,开发者不需要多次在组件之间传递变量。 + +- 后代通过使用\@Consume去获取\@Provide提供的变量,建立在\@Provide和\@Consume之间的双向数据同步,与\@State/\@Link不同的是,前者可以在多层级的父子组件之间传递。 + +- \@Provide和\@Consume可以通过相同的变量名或者相同的变量别名绑定,变量类型必须相同。 + + +```ts +// 通过相同的变量名绑定 +@Provide a: number = 0; +@Consume a: number; + +// 通过相同的变量别名绑定 +@Provide('a') b: number = 0; +@Consume('a') c: number; +``` + + +\@Provide和\@Consume通过相同的变量名或者相同的变量别名绑定时,\@Provide修饰的变量和\@Consume修饰的变量是一对多的关系。不允许在同一个自定义组件内,包括其子组件中声明多个同名或者同别名的\@Provide装饰的变量。 + + +## 装饰器说明 + +\@State的规则同样适用于\@Provide,差异为\@Provide还作为多层后代的同步源。 + +| \@Provide变量装饰器 | 说明 | +| -------------- | ---------------------------------------- | +| 装饰器参数 | 别名:常量字符串,可选。
如果指定了别名,则通过别名来绑定变量;如果未指定别名,则通过变量名绑定变量。 | +| 同步类型 | 双向同步。
从\@Provide变量到所有\@Consume变量以及相反的方向的数据同步。双向同步的操作与\@State和\@Link的组合相同。 | +| 允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。嵌套类型的场景请参考[观察变化](#观察变化)。
不支持any,不支持简单类型和复杂类型的联合类型,不允许使用undefined和null。
必须指定类型。\@Provide变量的\@Consume变量的类型必须相同。
**说明:**
不支持Length、ResourceStr、ResourceColor类型,Length、ResourceStr、ResourceColor为简单类型和复杂类型的联合类型。 | +| 被装饰变量的初始值 | 必须指定。 | + +| \@Consume变量装饰器 | 说明 | +| -------------- | ---------------------------------------- | +| 装饰器参数 | 别名:常量字符串,可选。
如果提供了别名,则必须有\@Provide的变量和其有相同的别名才可以匹配成功;否则,则需要变量名相同才能匹配成功。 | +| 同步类型 | 双向:从\@Provide变量(具体请参见\@Provide)到所有\@Consume变量,以及相反的方向。双向同步操作与\@State和\@Link的组合相同。 | +| 允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。嵌套类型的场景请参考[观察变化](#观察变化)。
不支持any,不允许使用undefined和null。
必须指定类型。\@Provide变量的\@Consume变量的类型必须相同。
**说明:**
- \@Consume装饰的变量,在其父节点或者祖先节点上,必须有对应的属性和别名的\@Provide装饰的变量。 | +| 被装饰变量的初始值 | 无,禁止本地初始化。 | + + +## 变量的传递/访问规则说明 + + +| \@Provide传递/访问 | 说明 | +| -------------- | ---------------------------------------- | +| 从父组件初始化和更新 | 可选,允许父组件中常规变量、\@State、\@Link、\@Prop、\@Provide、\@Consume、\@ObjectLink、\@StorageLink、\@StorageProp、\@LocalStorageLink和\@LocalStorageProp装饰的变量装饰变量初始化子组件\@Provide。 | +| 用于初始化子组件 | 允许,可用于初始化\@State、\@Link、\@Prop、\@Provide。 | +| 和父组件同步 | 否。 | +| 和后代组件同步 | 和\@Consume双向同步。 | +| 是否支持组件外访问 | 私有,仅可以在所属组件内访问。 | + + + **图1** \@Provide初始化规则图示   + + +![zh-cn_image_0000001552614217](figures/zh-cn_image_0000001552614217.png) + + +| \@Consume传递/访问 | 说明 | +| -------------- | ---------------------------------------- | +| 从父组件初始化和更新 | 禁止。通过相同的变量名和alias(别名)从\@Provide初始化。 | +| 用于初始化子组件 | 允许,可用于初始化\@State、\@Link、\@Prop、\@Provide。 | +| 和祖先组件同步 | 和\@Provide双向同步。 | +| 是否支持组件外访问 | 私有,仅可以在所属组件内访问 | + + + **图2** \@Consume初始化规则图示   + + +![zh-cn_image_0000001502094666](figures/zh-cn_image_0000001502094666.png) + + +## 观察变化和行为表现 + + +### 观察变化 + +- 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。 + +- 当装饰的数据类型为class或者Object的时候,可以观察到赋值和属性赋值的变化(属性为Object.keys(observedObject)返回的所有属性)。 + +- 当装饰的对象是array的时候,可以观察到数组的添加、删除、更新数组单元。 + + +### 框架行为 + +1. 初始渲染: + 1. \@Provide装饰的变量会以map的形式,传递给当前\@Provide所属组件的所有子组件; + 2. 子组件中如果使用\@Consume变量,则会在map中查找是否有该变量名/alias(别名)对应的\@Provide的变量,如果查找不到,框架会抛出JS ERROR; + 3. 在初始化\@Consume变量时,和\@State/\@Link的流程类似,\@Consume变量会保存在map中查找到的\@Provide变量,并把自己注册给\@Provide。 + +2. 当\@Provide装饰的数据变化时: + 1. 通过初始渲染的步骤可知,子组件\@Consume已把自己注册给父组件。父组件\@Provide变量变更后,会遍历更新所有依赖它的系统组件(elementid)和状态变量(\@Consume); + 2. 通知\@Consume更新后,子组件所有依赖\@Consume的系统组件(elementId)都会被通知更新。以此实现\@Provide对\@Consume状态数据同步。 + +3. 当\@Consume装饰的数据变化时: + 1. 通过初始渲染的步骤可知,子组件\@Consume持有\@Provide的实例。在\@Consume更新后调用\@Provide的更新方法,将更新的数值同步回\@Provide,以此实现\@Consume向\@Provide的同步更新。 + + +## 使用场景 + +在下面的示例是与后代组件双向同步状态\@Provide和\@Consume场景。当分别点击CompA和CompD组件内Button时,reviewVotes 的更改会双向同步在CompA和CompD中。 + + + +```ts +@Component +struct CompD { + // @Consume装饰的变量通过相同的属性名绑定其祖先组件CompA内的@Provide装饰的变量 + @Consume reviewVotes: number; + + build() { + Column() { + Text(`reviewVotes(${this.reviewVotes})`) + Button(`reviewVotes(${this.reviewVotes}), give +1`) + .onClick(() => this.reviewVotes += 1) + } + .width('50%') + } +} + +@Component +struct CompC { + build() { + Row({ space: 5 }) { + CompD() + CompD() + } + } +} + +@Component +struct CompB { + build() { + CompC() + } +} + +@Entry +@Component +struct CompA { + // @Provide装饰的变量reviewVotes由入口组件CompA提供其后代组件 + @Provide reviewVotes: number = 0; + + build() { + Column() { + Button(`reviewVotes(${this.reviewVotes}), give +1`) + .onClick(() => this.reviewVotes += 1) + CompB() + } + } +} +``` diff --git a/zh-cn/application-dev/quick-start/arkts-rendering-control-foreach.md b/zh-cn/application-dev/quick-start/arkts-rendering-control-foreach.md new file mode 100644 index 0000000000000000000000000000000000000000..d9caebec07283074b7a647b53c5cd4b3bc866efe --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-rendering-control-foreach.md @@ -0,0 +1,344 @@ +# ForEach:循环渲染 + + +ForEach基于数组类型数据执行循环渲染。 + + +## 接口描述 + + +```ts +ForEach( + arr: any[], + itemGenerator: (item: any, index?: number) => void, + keyGenerator?: (item: any, index?: number) => string +) +``` + + +| 参数名 | 参数类型 | 必填 | 参数描述 | +| ------------- | ---------------------------------------- | ---- | ---------------------------------------- | +| arr | Array | 是 | 必须是数组,允许设置为空数组,空数组场景下将不会创建子组件。同时允许设置返回值为数组类型的函数,例如arr.slice(1, 3),设置的函数不得改变包括数组本身在内的任何状态变量,如Array.splice、Array.sort或Array.reverse这些改变原数组的函数。 | +| itemGenerator | (item: any, index?: number) => void | 是 | 生成子组件的lambda函数,为数组中的每一个数据项创建一个或多个子组件,单个子组件或子组件列表必须包括在大括号“{...}”中。
**说明:**
- 子组件的类型必须是ForEach的父容器组件所允许的(例如,只有当ForEach父级为List组件时,才允许LitemItem子组件)。
- 允许子类构造函数返回if或另一个ForEach。ForEach可以在if内的任意位置。
- 可选index参数如在函数体中使用,则必须仅在函数签名中指定。 | +| keyGenerator | (item: any, index?: number) => string | 否 | 匿名函数,用于给数组中的每一个数据项生成唯一且固定的键值。键值生成器的功能是可选的,但是,为了使开发框架能够更好地识别数组更改,提高性能,建议提供。如将数组反向时,如果没有提供键值生成器,则ForEach中的所有节点都将重建。
**说明:**
- 同一数组中的不同项绝对不能计算出相同的ID。
- 如果未使用index参数,则项在数组中的位置变动不得改变项的键值。如果使用了index参数,则当项在数组中的位置有变动时,键值必须更改。
- 当某个项目被新项替换(值不同)时,被替换的项键值和新项的键值必须不同。
- 在构造函数中使用index参数时,键值生成函数也必须使用该参数。
- 键值生成函数不允许改变任何组件状态。 | + + +## 使用限制 + +- ForEach必须在容器组件内使用。 + +- 生成的子组件应当是允许包含在ForEach父容器组件中的子组件。 + +- 允许子组件生成器函数中包含if/else条件渲染,同时也允许ForEach包含在if/else条件渲染语句中。 + +- itemGenerator函数的调用顺序不一定和数组中的数据项相同,在开发过程中不要假设itemGenerator和keyGenerator函数是否执行及其执行顺序。例如,以下示例可能无法正确运行: + + ```ts + ForEach(anArray.map((item1, index1) => { return { i: index1 + 1, data: item1 }; }), + item => Text(`${item.i}. item.data.label`), + item => item.data.id.toString()) + ``` + + +## 开发者的建议 + +- 建议开发者不要假设项构造函数的执行顺序。执行顺序可能不能是数组中项的排列顺序。 + +- 不要假设数组项是否是初始渲染。ForEach的初始渲染在\@Component首次渲染时构建所有数组项。后续框架版本中可能会将此行为更改为延迟加载模式。 + +- 使用 index参数对UI更新性能有严重的负面影响,请尽量避免。 + +- 如果项构造函数中使用index参数,则项索引函数中也必须使用该参数。否则,如果项索引函数未使用index参数,ForEach在生成实际的键值时,框架也会把index考虑进来,默认将index拼接在后面。 + + +## 使用场景 + + +### 简单ForEach示例 + +根据arr数据分别创建3个Text和Divide组件。 + + +```ts +@Entry +@Component +struct MyComponent { + @State arr: number[] = [10, 20, 30]; + + build() { + Column({ space: 5 }) { + Button('Reverse Array') + .onClick(() => { + this.arr.reverse(); + }) + ForEach(this.arr, (item: number) => { + Text(`item value: ${item}`).fontSize(18) + Divider().strokeWidth(2) + }, (item: number) => item.toString()) + } + } +} +``` + + +### 复杂ForEach示例 + + +```ts +@Component +struct CounterView { + label: string; + @State count: number = 0; + + build() { + Button(`${this.label}-${this.count} click +1`) + .width(300).height(40) + .backgroundColor('#a0ffa0') + .onClick(() => { + this.count++; + }) + } +} + +@Entry +@Component +struct MainView { + @State arr: number[] = Array.from(Array(10).keys()); // [0.,.9] + nextUnused: number = this.arr.length; + + build() { + Column() { + Button(`push new item`) + .onClick(() => { + this.arr.push(this.nextUnused++) + }) + .width(300).height(40) + Button(`pop last item`) + .onClick(() => { + this.arr.pop() + }) + .width(300).height(40) + Button(`prepend new item (unshift)`) + .onClick(() => { + this.arr.unshift(this.nextUnused++) + }) + .width(300).height(40) + Button(`remove first item (shift)`) + .onClick(() => { + this.arr.shift() + }) + .width(300).height(40) + Button(`insert at pos ${Math.floor(this.arr.length / 2)}`) + .onClick(() => { + this.arr.splice(Math.floor(this.arr.length / 2), 0, this.nextUnused++); + }) + .width(300).height(40) + Button(`remove at pos ${Math.floor(this.arr.length / 2)}`) + .onClick(() => { + this.arr.splice(Math.floor(this.arr.length / 2), 1); + }) + .width(300).height(40) + Button(`set at pos ${Math.floor(this.arr.length / 2)} to ${this.nextUnused}`) + .onClick(() => { + this.arr[Math.floor(this.arr.length / 2)] = this.nextUnused++; + }) + .width(300).height(40) + ForEach(this.arr, + (item) => { + CounterView({ label: item.toString() }) + }, + (item) => item.toString() + ) + } + } +} +``` + +MainView拥有一个\@State装饰的数字数组。添加、删除和替换数组项是可观察到的变化事件,当这些事件发生时,MainView内的ForEach都会更新。 + +项目索引函数为每个数组项创建唯一且持久的键值,ArkUI框架通过此键值确定数组中的项是否有变化,只要键值相同,数组项的值就假定不变,但其索引位置可能会更改。此机制的运行前提是不同的数组项不能有相同的键值。 + +使用计算出的ID,框架可以对添加、删除和保留的数组项加以区分: + +1. 框架将删除已删除数组项的UI组件。 + +2. 框架仅对新添加的数组项执行项构造函数。 + +3. 框架不会为保留的数组项执行项构造函数。如果数组中的项索引已更改,框架将仅根据新顺序移动其UI组件,但不会更新该UI组件。 + +建议使用项目索引函数,但这是可选的。生成的ID必须是唯一的,这意味着不能为数组中的不同项计算出相同的ID。即使两个数组项具有相同的值,其ID也必须不同。 + +如果数组项值更改,则ID必须更改。 +如前所述,id生成函数是可选的。以下是不带项索引函数的ForEach: + + ```ts + ForEach(this.arr, + (item) => { + CounterView({ label: item.toString() }) + } + ) + ``` + +如果没有提供项ID函数,则框架会尝试在更新ForEach时智能检测数组更改。但是,它可能会删除子组件,并为在数组中移动(索引被更改)的数组项重新执行项构造函数。在上面的示例中,这将更改应用程序针对CounterView counter状态的行为。创建新的CounterView实例时,counter的值将初始化为0。 + + +### 使用\@ObjectLink的ForEach示例 + +当需要保留重复子组件的状态时,\@ObjectLink可将状态在组件树中向父组件推送。 + + +```ts +let NextID: number = 0; + +@Observed +class MyCounter { + public id: number; + public c: number; + + constructor(c: number) { + this.id = NextID++; + this.c = c; + } +} + +@Component +struct CounterView { + @ObjectLink counter: MyCounter; + label: string = 'CounterView'; + + build() { + Button(`CounterView [${this.label}] this.counter.c=${this.counter.c} +1`) + .width(200).height(50) + .onClick(() => { + this.counter.c += 1; + }) + } +} + +@Entry +@Component +struct MainView { + @State firstIndex: number = 0; + @State counters: Array = [new MyCounter(0), new MyCounter(0), new MyCounter(0), + new MyCounter(0), new MyCounter(0)]; + + build() { + Column() { + ForEach(this.counters.slice(this.firstIndex, this.firstIndex + 3), + (item) => { + CounterView({ label: `Counter item #${item.id}`, counter: item }) + }, + (item) => item.id.toString() + ) + Button(`Counters: shift up`) + .width(200).height(50) + .onClick(() => { + this.firstIndex = Math.min(this.firstIndex + 1, this.counters.length - 3); + }) + Button(`counters: shift down`) + .width(200).height(50) + .onClick(() => { + this.firstIndex = Math.max(0, this.firstIndex - 1); + }) + } + } +} +``` + +当增加firstIndex的值时,Mainview内的ForEach将更新,并删除与项ID firstIndex-1关联的CounterView子组件。对于ID为firstindex + 3的数组项,将创建新的CounterView子组件实例。由于CounterView子组件的状态变量counter值由父组件Mainview维护,故重建CounterView子组件实例不会重建状态变量counter值。 + +> **说明:** +> +> 违反上述数组项ID规则是最常见的应用开发错误,尤其是在Array<number>场景下,因为执行过程中很容易添加重复的数字。 + + +### ForEach的嵌套使用 + +允许将ForEach嵌套在同一组件中的另一个ForEach中,但更推荐将组件拆分为两个,每个构造函数只包含一个ForEach。下面为ForEach嵌套使用反例。 + + +```ts +class Month { + year: number; + month: number; + days: number[]; + + constructor(year: number, month: number, days: number[]) { + this.year = year; + this.month = month; + this.days = days; + } +} +@Component +struct CalendarExample { + // 模拟6个月 + @State calendar : Month[] = [ + new Month(2020, 1, [...Array(31).keys()]), + new Month(2020, 2, [...Array(28).keys()]), + new Month(2020, 3, [...Array(31).keys()]), + new Month(2020, 4, [...Array(30).keys()]), + new Month(2020, 5, [...Array(31).keys()]), + new Month(2020, 6, [...Array(30).keys()]) + ] + build() { + Column() { + Button() { + Text('next month') + }.onClick(() => { + this.calendar.shift() + this.calendar.push(new Month(year: 2020, month: 7, days: [...Array(31).keys()])) + }) + ForEach(this.calendar, + (item: Month) => { + ForEach(item.days, + (day : number) => { + // 构建日期块 + }, + (day : number) => day.toString() + )// 内部ForEach + }, + (item: Month) => (item.year * 12 + item.month).toString() // 字段与年和月一起使用,作为月份的唯一ID。 + )// 外部ForEach + } + } +} +``` + +以上示例存在两个问题: + +1. 代码可读性差。 + +2. 对于上述的年月份数据的数组结构形式,由于框架无法观察到针对该数组中Month数据结构的改变(比如day数组变化),从而内层的ForEach无法刷新日期显示。 + +建议应用设计时将Calendar拆分为Year、Month和Day子组件。定义一个“Day”模型类,以保存有关day的信息,并用\@Observed装饰此类。DayView组件利用ObjectLink装饰变量以绑定day数据。对MonthView和Month模型类执行同样的操作。 + + +### ForEach中使用可选index参数示例 + +可以在构造函数和ID生成函数中使用可选的index参数。 + + +```ts +@Entry +@Component +struct ForEachWithIndex { + @State arr: number[] = [4, 3, 1, 5]; + + build() { + Column() { + ForEach(this.arr, + (it, indx) => { + Text(`Item: ${indx} - ${it}`) + }, + (it, indx) => { + return `${indx} - ${it}` + } + ) + } + } +} +``` + +必须正确构造ID生成函数。当在项构造函数中使用index参数时,ID生成函数也必须使用index参数,以生成唯一ID和给定源数组项的ID。当数组项在数组中的索引位置发生变化时,其ID会发生变化。 + +此示例还说明了index参数会造成显著性能下降。即使项在源数组中移动而不做修改,因为索引发生改变,依赖该数组项的UI仍然需要重新渲染。例如,使用索引排序时,数组只需要将ForEach未修改的子UI节点移动到正确的位置,这对于框架来说是一个轻量级操作。而使用索引时,所有子UI节点都需要重新构建,这操作负担要重得多。 diff --git a/zh-cn/application-dev/quick-start/arkts-rendering-control-ifelse.md b/zh-cn/application-dev/quick-start/arkts-rendering-control-ifelse.md new file mode 100644 index 0000000000000000000000000000000000000000..5a9c98c2792a021b46311c6ee7c3f163caed3a66 --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-rendering-control-ifelse.md @@ -0,0 +1,225 @@ +# if/else:条件渲染 + + +ArkTS提供了渲染控制的能力。条件渲染可根据应用的不同状态,使用if、else和else if渲染对应状态下的UI内容。 + + +## 使用规则 + +- 支持if、else和else if语句。 + +- if、else if后跟随的条件语句可以使用状态变量。 + +- 允许在容器组件内使用,通过条件渲染语句构建不同的子组件。 + +- 条件渲染语句在涉及到组件的父子关系时是“透明”的,当父组件和子组件之间存在一个或多个if语句时,必须遵守父组件关于子组件使用的规则。 + +- 每个分支内部的构建函数必须遵循构建函数的规则,并创建一个或多个组件。无法创建组件的空构建函数会产生语法错误。 + +- 某些容器组件限制子组件的类型或数量,将条件渲染语句用于这些组件内时,这些限制将同样应用于条件渲染语句内创建的组件。例如,Grid容器组件的子组件仅支持GridItem组件,在Grid内使用条件渲染语句时,条件渲染语句内仅允许使用GridItem组件。 + + +## 更新机制 + +当if、else if后跟随的状态判断中使用的状态变量值变化时,条件渲染语句会进行更新,更新步骤如下: + +1. 评估if和else if的状态判断条件,如果分支没有变化,请无需执行以下步骤。如果分支有变化,则执行2、3步骤: + +2. 删除此前构建的所有子组件。 + +3. 执行新分支的构造函数,将获取到的组件添加到if父容器中。如果缺少适用的else分支,则不构建任何内容。 + +条件可以包括Typescript表达式。对于构造函数中的表达式,此类表达式不得更改应用程序状态。 + + +## 使用场景 + + +### 使用if进行条件渲染 + + +```ts +@Entry +@Component +struct ViewA { + @State count: number = 0; + + build() { + Column() { + Text(`count=${this.count}`) + + if (this.count > 0) { + Text(`count is positive`) + .fontColor(Color.Green) + } + + Button('increase count') + .onClick(() => { + this.count++; + }) + + Button('decrease count') + .onClick(() => { + this.count--; + }) + } + } +} +``` + +if语句的每个分支都包含一个构建函数。此类构建函数必须创建一个或多个子组件。在初始渲染时,if语句会执行构建函数,并将生成的子组件添加到其父组件中。 + +每当if或else if条件语句中使用的状态变量发生变化时,条件语句都会更新并重新评估新的条件值。如果条件值评估发生了变化,这意味着需要构建另一个条件分支。此时ArkUI框架将: + +1. 删除所有以前渲染的(早期分支的)组件。 + +2. 执行新分支的构造函数,将生成的子组件添加到其父组件中。 + +在以上示例中,如果count从0增加到1,那么if语句更新,条件count > 0将重新评估,评估结果将从false更改为true。因此,将执行条件为真分支的构造函数,创建一个Text组件,并将它添加到父组件Column中。如果后续count更改为0,则Text组件将从Column组件中删除。由于没有else分支,因此不会执行新的构造函数。 + + +### if ... else ...语句和子组件状态 + +以下示例包含if ... else ...语句与拥有\@State装饰变量的子组件。 + + +```ts +@Component +struct CounterView { + @State counter: number = 0; + label: string = 'unknown'; + + build() { + Row() { + Text(`${this.label}`) + Button(`counter ${this.counter} +1`) + .onClick(() => { + this.counter += 1; + }) + } + } +} + +@Entry +@Component +struct MainView { + @State toggle: boolean = true; + + build() { + Column() { + if (this.toggle) { + CounterView({ label: 'CounterView #positive' }) + } else { + CounterView({ label: 'CounterView #negative' }) + } + Button(`toggle ${this.toggle}`) + .onClick(() => { + this.toggle = !this.toggle; + }) + } + } +} +``` + +CounterView(label为 'CounterView \#positive')子组件在初次渲染时创建。此子组件携带名为counter的状态变量。当修改CounterView.counter状态变量时,CounterView(label为 'CounterView \#positive')子组件重新渲染时并保留状态变量值。当MainView.toggle状态变量的值更改为false时,MainView父组件内的if语句将更新,随后将删除CounterView(label为 'CounterView \#positive')子组件。与此同时,将创建新的CounterView(label为 'CounterView \#negative')实例。而它自己的counter状态变量设置为初始值0。 + +> ![icon-note.gif](public_sys-resources/icon-note.gif) **说明:** +> CounterView(label为 'CounterView \#positive')和CounterView(label为 'CounterView \#negative')是同一自定义组件的两个不同实例。if分支的更改,不会更新现有子组件,也不会保留状态。 + +以下示例展示了条件更改时,若需要保留counter值所做的修改。 + + +``` +@Component +struct CounterView { + @Link counter: number; + label: string = 'unknown'; + + build() { + Row() { + Text(`${this.label}`) + Button(`counter ${this.counter} +1`) + .onClick(() => { + this.counter += 1; + }) + } + } +} + +@Entry +@Component +struct MainView { + @State toggle: boolean = true; + @State counter: number = 0; + + build() { + Column() { + if (this.toggle) { + CounterView({ counter: $counter, label: 'CounterView #positive' }) + } else { + CounterView({ counter: $counter, label: 'CounterView #negative' }) + } + Button(`toggle ${this.toggle}`) + .onClick(() => { + this.toggle = !this.toggle; + }) + } + } +} +``` + +此处,\@State counter变量归父组件所有。因此,当CounterView组件实例被删除时,该变量不会被销毁。CounterView组件通过\@Link装饰器引用状态。状态必须从子级移动到其父级(或父级的父级),以避免在条件内容或重复内容被销毁时丢失状态。 + + +### 嵌套if语句 + +条件语句的嵌套对父组件的相关规则没有影响。 + + +```ts +@Entry +@Component +struct CompA { + @State toggle: boolean = false; + @State toggleColor: boolean = false; + + build() { + Column() { + Text('Before') + .fontSize(15) + if (this.toggle) { + Text('Top True, positive 1 top') + .backgroundColor('#aaffaa').fontSize(20) + // 内部if语句 + if (this.toggleColor) { + Text('Top True, Nested True, positive COLOR Nested ') + .backgroundColor('#00aaaa').fontSize(15) + } else { + Text('Top True, Nested False, Negative COLOR Nested ') + .backgroundColor('#aaaaff').fontSize(15) + } + } else { + Text('Top false, negative top level').fontSize(20) + .backgroundColor('#ffaaaa') + if (this.toggleColor) { + Text('positive COLOR Nested ') + .backgroundColor('#00aaaa').fontSize(15) + } else { + Text('Negative COLOR Nested ') + .backgroundColor('#aaaaff').fontSize(15) + } + } + Text('After') + .fontSize(15) + Button('Toggle Outer') + .onClick(() => { + this.toggle = !this.toggle; + }) + Button('Toggle Inner') + .onClick(() => { + this.toggleColor = !this.toggleColor; + }) + } + } +} +``` diff --git a/zh-cn/application-dev/quick-start/arkts-rendering-control-lazyforeach.md b/zh-cn/application-dev/quick-start/arkts-rendering-control-lazyforeach.md new file mode 100644 index 0000000000000000000000000000000000000000..876ae9dd328440fd5816983e3321b61dd92c6b16 --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-rendering-control-lazyforeach.md @@ -0,0 +1,220 @@ +# LazyForEach:数据懒加载 + + +LazyForEach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当LazyForEach在滚动容器中使用了,框架会根据滚动容器可视区域按需创建组件,当组件划出可视区域外时,框架会进行组件销毁回收以降低内存占用。 + + +## 接口描述 + + +```ts +LazyForEach( + dataSource: IDataSource, // 需要进行数据迭代的数据源 + itemGenerator: (item: any) => void, // 子组件生成函数 + keyGenerator?: (item: any) => string // (可选) .键值生成函数 +): void +interface IDataSource { + totalCount(): number; // Get total count of data + getData(index: number): any; // Get single data by index + registerDataChangeListener(listener: DataChangeListener): void; // Register listener to listening data changes + unregisterDataChangeListener(listener: DataChangeListener): void; // Unregister listener +} +interface DataChangeListener { + onDataReloaded(): void; // Called while data reloaded + onDataAdd(index: number): void; // Called while single data added + onDataMove(from: number, to: number): void; // Called while single data moved + onDataDelete(index: number): void; // Called while single data deleted + onDataChange(index: number): void; // Called while single data changed +} +``` + +**参数:** + + +| 参数名 | 参数类型 | 必填 | 参数描述 | +| ------------- | --------------------------------------- | ---- | ---------------------------------------- | +| dataSource | IDataSource | 是 | LazyForEach数据源,需要开发者实现相关接口。 | +| itemGenerator | (item: any) => void | 是 | 子组件生成函数,为数组中的每一个数据项创建一个子组件。
**说明:**
itemGenerator的函数体必须使用大括号{...}。itemGenerator每次迭代只能并且必须生成一个子组件。itemGenerator中可以使用if语句,但是必须保证if语句每个分支都会创建一个相同类型的子组件。itemGenerator中不允许使用ForEach和LazyForEach语句。 | +| keyGenerator | (item: any) => string | 否 | 键值生成函数,用于给数据源中的每一个数据项生成唯一且固定的键值。当数据项在数组中的位置更改时,其键值不得更改,当数组中的数据项被新项替换时,被替换项的键值和新项的键值必须不同。键值生成器的功能是可选的,但是,为了使开发框架能够更好地识别数组更改,提高性能,建议提供。如将数组反向时,如果没有提供键值生成器,则LazyForEach中的所有节点都将重建。
**说明:**
数据源中的每一个数据项生成的键值不能重复。 | + + +## IDataSource类型说明 + + +```ts +interface IDataSource { + totalCount(): number; + getData(index: number): any; + registerDataChangeListener(listener: DataChangeListener): void; + unregisterDataChangeListener(listener: DataChangeListener): void; +} +``` + +| 接口声明 | 参数类型 | 说明 | +| ---------------------------------------- | ------------------ | ------------------------------------- | +| totalCount(): number | - | 获得数据总数。 | +| getData(index: number): any | number | 获取索引值index对应的数据。
index:获取数据对应的索引值 | +| registerDataChangeListener(listener:DataChangeListener): void | DataChangeListener | 注册数据改变的监听器。
listener:数据变化监听器 | +| unregisterDataChangeListener(listener:DataChangeListener): void | DataChangeListener | 注销数据改变的监听器。
listener:数据变化监听器 | + + +## DataChangeListener类型说明 + + +```ts +interface DataChangeListener { + onDataReloaded(): void; + onDataAdded(index: number): void; + onDataMoved(from: number, to: number): void; + onDataDeleted(index: number): void; + onDataChanged(index: number): void; + onDataAdd(index: number): void; + onDataMove(from: number, to: number): void; + onDataDelete(index: number): void; + onDataChange(index: number): void; +} +``` + +| 接口声明 | 参数类型 | 说明 | +| ---------------------------------------- | -------------------------------------- | ---------------------------------------- | +| onDataReloaded(): void | - | 通知组件重新加载所有数据。 | +| onDataAdded(index: number):void | number | 通知组件index的位置有数据添加。
index:数据添加位置的索引值 | +| onDataMoved(from: number, to: number): void | from: number,
to: number | 通知组件数据有移动。
from: 数据移动起始位置,to: 数据移动目标位置。
**说明:**
数据移动前后键值要保持不变,如果键值有变化,应使用删除数据和新增数据接口。 | +| onDataChanged(index: number): void | number | 通知组件index的位置有数据有变化。
index:数据变化监听器。 | +| onDataAdd(index: number): void | number | 通知组件index的位置有数据添加。
index:数据添加位置的索引值 | +| onDataMove(from: number, to: number): void | from: number,
to: number | 通知组件数据有移动。
from: 数据移动起始位置,to: 数据移动目标位置。
**说明:**
数据移动前后键值要保持不变,如果键值有变化,应使用删除数据和新增数据接口。 | +| onDataChanged(index: number): void | number | 通知组件index的位置有数据有变化。
index:数据变化位置的索引值 | + + +## 使用限制 + +- LazyForEach必须在容器组件内使用,仅有List、Grid以及Swiper组件支持数据懒加载(即只加载可视部分以及其前后少量数据用于缓冲),其他组件仍然是一次性加载所有的数据。 + +- LazyForEach在每次迭代中,必须创建且只允许创建一个子组件。 + +- 生成的子组件必须是允许包含在LazyForEach父容器组件中的子组件。 + +- 允许LazyForEach包含在if/else条件渲染语句中,也允许LazyForEach中出现if/else条件渲染语句。 + +- 键值生成器必须针对每个数据生成唯一的值,如果键值相同,将导致键值相同的UI组件被框架忽略,从而无法在父容器内显示。 + +- LazyForEach必须使用DataChangeListener对象来进行更新,第一个参数dataSource使用状态变量时,状态变量改变不会触发LazyForEach的UI刷新。 + +- 为了高性能渲染,通过DataChangeListener对象的onDataChange方法来更新UI时,需要生成不同于原来的键值来触发组件刷新。 + +- itemGenerator函数的调用顺序不一定和数据源中的数据项相同,在开发过程中不要假设itemGenerator和keyGenerator函数是否执行及其执行顺序。例如,以下示例可能无法正常运行: + + + ```ts + LazyForEach(dataSource, + item => Text(`${item.i}. item.data.label`), + item => item.data.id.toString()) + ``` + + +## 示例 + + +```ts +// Basic implementation of IDataSource to handle data listener +class BasicDataSource implements IDataSource { + private listeners: DataChangeListener[] = []; + + public totalCount(): number { + return 0; + } + + public getData(index: number): any { + return undefined; + } + + registerDataChangeListener(listener: DataChangeListener): void { + if (this.listeners.indexOf(listener) < 0) { + console.info('add listener'); + this.listeners.push(listener); + } + } + + unregisterDataChangeListener(listener: DataChangeListener): void { + const pos = this.listeners.indexOf(listener); + if (pos >= 0) { + console.info('remove listener'); + this.listeners.splice(pos, 1); + } + } + + notifyDataReload(): void { + this.listeners.forEach(listener => { + listener.onDataReloaded(); + }) + } + + notifyDataAdd(index: number): void { + this.listeners.forEach(listener => { + listener.onDataAdd(index); + }) + } + + notifyDataChange(index: number): void { + this.listeners.forEach(listener => { + listener.onDataChange(index); + }) + } + + notifyDataDelete(index: number): void { + this.listeners.forEach(listener => { + listener.onDataDelete(index); + }) + } + + notifyDataMove(from: number, to: number): void { + this.listeners.forEach(listener => { + listener.onDataMove(from, to); + }) + } +} + +class MyDataSource extends BasicDataSource { + private dataArray: string[] = ['/path/image0', '/path/image1', '/path/image2', '/path/image3']; + + public totalCount(): number { + return this.dataArray.length; + } + + public getData(index: number): any { + return this.dataArray[index]; + } + + public addData(index: number, data: string): void { + this.dataArray.splice(index, 0, data); + this.notifyDataAdd(index); + } + + public pushData(data: string): void { + this.dataArray.push(data); + this.notifyDataAdd(this.dataArray.length - 1); + } +} + +@Entry +@Component +struct MyComponent { + private data: MyDataSource = new MyDataSource(); + + build() { + List({ space: 3 }) { + LazyForEach(this.data, (item: string) => { + ListItem() { + Row() { + Image(item).width('30%').height(50) + Text(item).fontSize(20).margin({ left: 10 }) + }.margin({ left: 10, right: 10 }) + } + .onClick(() => { + this.data.pushData('/path/image' + this.data.totalCount()); + }) + }, item => item) + } + } +} +``` diff --git a/zh-cn/application-dev/quick-start/arkts-rendering-control-overview.md b/zh-cn/application-dev/quick-start/arkts-rendering-control-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..4ee0e53737ad6000377d5ab1e745ba5a1ca1f4f5 --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-rendering-control-overview.md @@ -0,0 +1,4 @@ +# 渲染控制概述 + + +ArkUI通过[自定义组件](arkts-create-custom-components.md)的build()函数和[@builder装饰器](arkts-builder.md)中的声明式UI描述语句构建相应的UI。在声明式描述语句中开发者除了使用系统组件外,还可以使用渲染控制语句来辅助UI的构建,这些渲染控制语句包括控制组件是否显示的条件渲染语句,基于数组数据快速生成组件的循环渲染语句以及针对大数据量场景的数据懒加载语句。 diff --git a/zh-cn/application-dev/quick-start/arkts-rendering-control.md b/zh-cn/application-dev/quick-start/arkts-rendering-control.md deleted file mode 100644 index 1d1635674d5e913a1a4ac33c84e2a165e092df5d..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/quick-start/arkts-rendering-control.md +++ /dev/null @@ -1,284 +0,0 @@ -# 渲染控制 - -ArkTS也提供了渲染控制的能力。条件渲染可根据应用的不同状态,渲染对应状态下的UI内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件。 - -## 条件渲染 - -使用if/else进行条件渲染。 - - -> **说明:** -> -> - if/else条件语句可以使用状态变量。 -> -> - 使用if/else可以使子组件的渲染依赖条件语句。 -> -> - 必须在容器组件内使用。 -> -> - 某些容器组件限制子组件的类型或数量,将if/else用于这些组件内时,这些限制将同样应用于if/else语句内创建的组件。例如,Grid容器组件的子组件仅支持GridItem组件,在Grid内使用if/else时,则if/else语句内也仅允许使用GridItem组件。 - - -```ts -Column() { - if (this.count < 0) { - Text('count is negative').fontSize(14) - } else if (this.count % 2 === 0) { - Text('count is even').fontSize(14) - } else { - Text('count is odd').fontSize(14) - } -} -``` - -## 循环渲染 - -通过循环渲染(ForEach)从数组中获取数据,并为每个数据项创建相应的组件,可减少代码复杂度。 - -```ts -ForEach( - arr: any[], - itemGenerator: (item: any, index?: number) => void, - keyGenerator?: (item: any, index?: number) => string -) -``` - -从API version 9开始,该接口支持在ArkTS卡片中使用。 - -**参数:** - -| 参数名 | 参数类型 | 必填 | 参数描述 | -| ------------- | ------------------------------------- | ---- | ------------------------------------------------------------ | -| arr | any[] | 是 | 必须是数组,允许设置为空数组,空数组场景下将不会创建子组件。同时允许设置返回值为数组类型的函数,例如arr.slice(1, 3),设置的函数不得改变包括数组本身在内的任何状态变量,如Array.splice、Array.sort或Array.reverse这些改变原数组的函数。 | -| itemGenerator | (item: any, index?: number) => void | 是 | 生成子组件的lambda函数,为数组中的每一个数据项创建一个或多个子组件,单个子组件或子组件列表必须包括在大括号“{...}”中。 | -| keyGenerator | (item: any, index?: number) => string | 否 | 匿名函数,用于给数组中的每一个数据项生成唯一且固定的键值。当数据项在数组中的位置更改时,其键值不得更改,当数组中的数据项被新项替换时,被替换项的键值和新项的键值必须不同。键值生成器的功能是可选的,但是,为了使开发框架能够更好地识别数组更改,提高性能,建议提供。如将数组反向时,如果没有提供键值生成器,则ForEach中的所有节点都将重建。 | - -> **说明:** -> -> - ForEach必须在容器组件内使用。 -> -> - 生成的子组件应当是允许包含在ForEach父容器组件中的子组件。 -> -> - 允许子组件生成器函数中包含if/else条件渲染,同时也允许ForEach包含在if/else条件渲染语句中。 -> -> - itemGenerator函数的调用顺序不一定和数组中的数据项相同,在开发过程中不要假设itemGenerator和keyGenerator函数是否执行及其执行顺序。例如,以下示例可能无法正常工作: -> -> ```ts -> ForEach(anArray.map((item1, index1) => { return { i: index1 + 1, data: item1 }; }), -> item => Text(`${item.i}. item.data.label`), -> item => item.data.id.toString()) -> ``` - -## 示例 - -```ts -// xxx.ets -@Entry -@Component -struct MyComponent { - @State arr: number[] = [10, 20, 30] - - build() { - Column({ space: 5 }) { - Button('Reverse Array') - .onClick(() => { - this.arr.reverse() - }) - - ForEach(this.arr, (item: number) => { - Text(`item value: ${item}`).fontSize(18) - Divider().strokeWidth(2) - }, (item: number) => item.toString()) - } - } -} -``` - -![forEach1](figures/forEach1.gif) - -## 数据懒加载 - -通过数据懒加载(LazyForEach)从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。 - -```ts -LazyForEach( - dataSource: IDataSource, - itemGenerator: (item: any) => void, - keyGenerator?: (item: any) => string -): void - -interface IDataSource { - totalCount(): number; - getData(index: number): any; - registerDataChangeListener(listener: DataChangeListener): void; - unregisterDataChangeListener(listener: DataChangeListener): void; -} - -interface DataChangeListener { - onDataReloaded(): void; - onDataAdd(index: number): void; - onDataMove(from: number, to: number): void; - onDataDelete(index: number): void; - onDataChange(index: number): void; -} -``` - -**参数:** - -| 参数名 | 参数类型 | 必填 | 参数描述 | -| ------------- | --------------------- | ---- | ------------------------------------------------------------ | -| dataSource | IDataSource | 是 | 实现IDataSource接口的对象,需要开发者实现相关接口。 | -| itemGenerator | (item: any, index?: number) => void | 是 | 生成子组件的lambda函数,为数组中的每一个数据项创建一个或多个子组件,单个子组件或子组件列表必须包括在大括号“{...}”中。 | -| keyGenerator | (item: any, index?: number) => string | 否 | 匿名函数,用于给数组中的每一个数据项生成唯一且固定的键值。当数据项在数组中的位置更改时,其键值不得更改,当数组中的数据项被新项替换时,被替换项的键值和新项的键值必须不同。键值生成器的功能是可选的,但是,为了使开发框架能够更好地识别数组更改,提高性能,建议提供。如将数组反向时,如果没有提供键值生成器,则LazyForEach中的所有节点都将重建。 | - -### IDataSource类型说明 - -| 名称 | 描述 | -| ------------------------------------------------------------ | ---------------------- | -| totalCount(): number | 获取数据总数。 | -| getData(index: number): any | 获取索引值index对应的数据。 | -| registerDataChangeListener(listener:DataChangeListener): void | 注册数据改变的监听器。 | -| unregisterDataChangeListener(listener:DataChangeListener): void | 注销数据改变的监听器。 | - -### DataChangeListener类型说明 - -| 名称 | 描述 | -| -------------------------------------------------------- | -------------------------------------- | -| onDataReloaded(): void | 重新加载所有数据。 | -| onDataAdded(index: number): voiddeprecated | 通知组件index的位置有数据添加。从API Version 8开始废弃,建议使用onDataAdd。 | -| onDataMoved(from: number, to: number): voiddeprecated | 通知组件数据从from的位置移到to的位置。从API Version 8开始废弃,建议使用onDataMove。 | -| onDataDeleted(index: number): voiddeprecated | 通知组件index的位置有数据删除。从API Version 8开始废弃,建议使用onDataDelete。 | -| onDataChanged(index: number): voiddeprecated | 通知组件index的位置有数据变化。 从API Version 8开始废弃,建议使用onDataChange。 | -| onDataAdd(index: number): void8+ | 通知组件index的位置有数据添加。 | -| onDataMove(from: number, to: number): void8+ | 通知组件数据从from的位置移到to的位置。 | -| onDataDelete(index: number): void8+ | 通知组件index的位置有数据删除。 | -| onDataChange(index: number): void8+ | 通知组件index的位置有数据变化。 | - -## 示例 - -```ts -// xxx.ets -class BasicDataSource implements IDataSource { - private listeners: DataChangeListener[] = [] - - public totalCount(): number { - return 0 - } - - public getData(index: number): any { - return undefined - } - - registerDataChangeListener(listener: DataChangeListener): void { - if (this.listeners.indexOf(listener) < 0) { - console.info('add listener') - this.listeners.push(listener) - } - } - - unregisterDataChangeListener(listener: DataChangeListener): void { - const pos = this.listeners.indexOf(listener); - if (pos >= 0) { - console.info('remove listener') - this.listeners.splice(pos, 1) - } - } - - notifyDataReload(): void { - this.listeners.forEach(listener => { - listener.onDataReloaded() - }) - } - - notifyDataAdd(index: number): void { - this.listeners.forEach(listener => { - listener.onDataAdd(index) - }) - } - - notifyDataChange(index: number): void { - this.listeners.forEach(listener => { - listener.onDataChange(index) - }) - } - - notifyDataDelete(index: number): void { - this.listeners.forEach(listener => { - listener.onDataDelete(index) - }) - } - - notifyDataMove(from: number, to: number): void { - this.listeners.forEach(listener => { - listener.onDataMove(from, to) - }) - } -} - -class MyDataSource extends BasicDataSource { - // 初始化数据列表 - private dataArray: string[] = ['/path/image0.png', '/path/image1.png', '/path/image2.png', '/path/image3.png'] - - public totalCount(): number { - return this.dataArray.length - } - - public getData(index: number): any { - return this.dataArray[index] - } - - public addData(index: number, data: string): void { - this.dataArray.splice(index, 0, data) - this.notifyDataAdd(index) - } - - public pushData(data: string): void { - this.dataArray.push(data) - this.notifyDataAdd(this.dataArray.length - 1) - } -} - -@Entry -@Component -struct MyComponent { - private data: MyDataSource = new MyDataSource() - - build() { - List({ space: 3 }) { - LazyForEach(this.data, (item: string) => { - ListItem() { - Row() { - Image(item).width(50).height(50) - Text(item).fontSize(20).margin({ left: 10 }) - }.margin({ left: 10, right: 10 }) - } - .onClick(() => { - // 每点击一次列表项,数据增加一项 - this.data.pushData('/path/image' + this.data.totalCount() + '.png') - }) - }, item => item) - }.height('100%').width('100%') - } -} -``` - -> **说明:** -> -> - LazyForEach必须在容器组件内使用,目前仅有List、Grid以及Swiper组件支持数据懒加载(即只加载可视部分以及其前后少量数据用于缓冲),其他组件仍然是一次性加载所有的数据。 -> -> - LazyForEach在每次迭代中,必须创建且只允许创建一个子组件。 -> -> - 生成的子组件必须是允许包含在LazyForEach父容器组件中的子组件。 -> -> - 允许LazyForEach包含在if/else条件渲染语句中。 -> -> - 为了高性能渲染,通过DataChangeListener对象的onDataChange方法来更新UI时,仅当itemGenerator中创建的子组件内使用了状态变量时,才会触发组件刷新。 -> -> - itemGenerator函数的调用顺序不一定和数据源中的数据项相同,在开发过程中不要假设itemGenerator和keyGenerator函数是否执行及其执行顺序。例如,以下示例可能无法正常工作: -> -> ```ts -> LazyForEach(dataSource, -> item => Text(`${item.i}. item.data.label`), -> item => item.data.id.toString()) -> ``` - -![lazyForEach](figures/lazyForEach.gif) \ No newline at end of file diff --git a/zh-cn/application-dev/quick-start/arkts-restrictions-and-extensions.md b/zh-cn/application-dev/quick-start/arkts-restrictions-and-extensions.md deleted file mode 100644 index ae8318ee1472177ba59b6eaf8b21d8f2e5623bb4..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/quick-start/arkts-restrictions-and-extensions.md +++ /dev/null @@ -1,257 +0,0 @@ -# 使用限制与扩展 - -## 在生成器函数中的使用限制 - -ArkTS语言的使用在生成器函数中存在一定的限制: - -- 表达式仅允许在字符串(${expression})、if/else条件语句、ForEach的参数以及组件的参数中使用。 - -- 任何表达式都不能导致应用程序中状态变量(@State、@Link、@Prop)的改变,否则会造成未定义和潜在不稳定的框架行为。 - -- 生成器函数内部不能有局部变量。 - -上述限制都不适用于事件方法(如onClick)的匿名函数实现。 - -## 变量的双向绑定 - -ArkTS支持通过$$双向绑定变量,通常应用于状态值频繁改变的变量。 - -- 当前$$支持基础类型变量,以及@State、@Link和@Prop装饰的变量。 -- 当前$$仅支持[bindPopup](../reference/arkui-ts/ts-universal-attributes-popup.md)属性方法的show参数,[Radio](../reference/arkui-ts/ts-basic-components-radio.md)组件的checked属性,[Refresh](../reference/arkui-ts/ts-container-refresh.md)组件的refreshing参数。 -- $$绑定的变量变化时,仅渲染刷新当前组件,提高渲染速度。 - -```ts -// xxx.ets -@Entry -@Component -struct bindPopupPage { - @State customPopup: boolean = false - - build() { - Column() { - Button('Popup') - .margin(20) - .onClick(() => { - this.customPopup = !this.customPopup - }) - .bindPopup($$this.customPopup, { - message: "showPopup" - }) - } - } -} -``` - -![popup](figures/popup.gif) - -## 状态变量数据类型声明使用限制 - -1. 所有的状态装饰器变量需要显式声明变量类型,不允许声明any,不支持Date数据类型。 - - 示例: - - ```ts - // xxx.ets - @Entry - @Component - struct DatePickerExample { - //错误写法: @State isLunar: any = false - @State isLunar: boolean = false - //错误写法: @State selectedDate: Date = new Date('2021-08-08') - private selectedDate: Date = new Date('2021-08-08') - - build() { - Column() { - Button('切换公历农历') - .margin({ top: 30 }) - .onClick(() => { - this.isLunar = !this.isLunar - }) - DatePicker({ - start: new Date('1970-1-1'), - end: new Date('2100-1-1'), - selected: this.selectedDate - }) - .lunar(this.isLunar) - .onChange((value: DatePickerResult) => { - this.selectedDate.setFullYear(value.year, value.month, value.day) - console.info('select current date is: ' + JSON.stringify(value)) - }) - - }.width('100%') - } - } - ``` - - ![datePicker](../../application-dev/reference/arkui-ts/figures/datePicker.gif) - -2. @State、@Provide、 @Link和@Consume四种状态变量的数据类型声明只能由简单数据类型或引用数据类型的其中一种构成。 - - 类型定义中的Length、ResourceStr、ResourceColor三个类型是简单数据类型或引用数据类型的组合,所以不能被以上四种状态装饰器变量使用。 - Length、ResourceStr、ResourceColor的定义请看文档[arkui-ts类型定义](../../application-dev/reference/arkui-ts/ts-types.md)。 - - 示例: - - ```ts - // xxx.ets - @Entry - @Component - struct IndexPage { - //错误写法: @State message: string | Resource = 'Hello World' - @State message: string = 'Hello World' - //错误写法: @State message: ResourceStr = $r('app.string.hello') - @State resourceStr: Resource = $r('app.string.hello') - - build() { - Row() { - Column() { - Text(`${this.message}`) - .fontSize(50) - .fontWeight(FontWeight.Bold) - } - .width('100%') - } - .height('100%') - } - } - ``` - - ![hello](figures/hello.PNG) - -## 自定义组件成员变量初始化的方式与约束 - -组件的成员变量可以通过两种方式初始化: - -- 本地初始化: - - ```ts - @State counter: Counter = new Counter() - ``` -- 在构造组件时通过构造参数初始化: - - ```ts - MyComponent({counter: $myCounter}) - ``` - -具体允许哪种方式取决于状态变量的装饰器: - -| 装饰器类型 | 本地初始化 | 通过构造函数参数初始化 | -| ------------ | ----- | ----------- | -| @State | 必须 | 可选 | -| @Prop | 禁止 | 必须 | -| @Link | 禁止 | 必须 | -| @StorageLink | 必须 | 禁止 | -| @StorageProp | 必须 | 禁止 | -| @LocalStorageLink | 必须 | 禁止 | -| @LocalStorageProp | 必须 | 禁止 | -| @Provide | 必须 | 可选 | -| @Consume | 禁止 | 禁止 | -| @ObjectLink | 禁止 | 必须 | -| 常规成员变量 | 推荐 | 可选 | - -从上表中可以看出: - -- @State变量需要本地初始化,初始化的值可以被构造参数覆盖。 - -- @Prop和@Link变量必须且仅通过构造函数参数进行初始化。 - -通过构造函数方法初始化成员变量,需要遵循如下规则: - -| **从父组件中的变量(右)到子组件中的变量(下)** | **regular** | **@State** | **@Link** | **@Prop** | **@Provide** | **@Consume** | **@ObjectLink** | -|---------------------------------|----------------------------|------------|-----------|-----------|--------------|--------------|------------------| -| **regular** | 支持 | 支持 | 支持 | 支持 | 不支持 | 不支持 | 支持 | -| **@State** | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | -| **@Link** | 不支持 | 支持(1) | 支持(1) | 支持(1) | 支持(1) | 支持(1) | 支持(1) | -| **@Prop** | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | -| **@Provide** | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | -| **@Consume** | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | -| **@ObjectLink** | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | - -| **从父组件中的变量(右)到子组件中的变量(下)** | **@StorageLink** | **@StorageProp** | **@LocalStorageLink** | **@LocalStorageProp** | -|------------------|------------------|------------------|-----------------------|------------------------| -| **regular** | 支持 | 不支持 | 不支持 | 不支持 | -| **@State** | 支持 | 支持 | 支持 | 支持 | -| **@Link** | 支持(1) | 支持(1) | 支持(1) | 支持(1) | -| **@Prop** | 支持 | 支持 | 支持 | 支持 | -| **@Provide** | 支持 | 支持 | 支持 | 支持 | -| **@Consume** | 不支持 | 不支持 | 不支持 | 不支持 | -| **@ObjectLink** | 不支持 | 不支持 | 不支持 | 不支持 | - -> **说明** -> -> **支持(1)**:必须使用`$`, 例如 `this.$varA`。 -> **regular**:未加修饰的常规变量。 - -从上表中可以看出: - -- 子组件的@ObjectLink变量不支持父组件装饰器变量的直接赋值,其父组件的源必须是数组的项或对象的属性,该数组或对象必现用`@State`、`@Link`、`@Provide`、`@Consume`或`@ObjectLink`装饰器修饰。 - -- 父组件的常规变量可以用于初始化子组件的`@State`变量,但不能用于初始化`@Link`、`@Consume`和`@ObjectLink`变量。 - -- 父组件的@State变量可以初始化子组件的`@Prop`、`@Link`(通过$)或常规变量,但不能初始化子组件的@Consume变量。 - -- 父组件的@Link变量不可以初始化子组件的`@Consume`和`@ObjectLink`变量。 - -- 父组件的@Prop变量不可以初始化子组件的`@Consume`和`@ObjectLink`变量。 - -- 不允许从父组件初始化`@StorageLink`, `@StorageProp`, `@LocalStorageLink`, `@LocalStorageProp`修饰的变量。 - -- 除了上述规则外,还需要遵循TS的强类型规则。 - -示例: -```ts -@Entry -@Component -struct Parent { - message: string = "Hello World" - build() { - Column() { - Child({ - stateMessage: this.message, - /* ArkTS:ERROR The regular property 'message' cannot be assigned - to the @Link property 'linkMessage'.*/ - linkMessage: this.$message - }) - } - .width('100%') - } -} - -@Component -struct Child { - @State stateMessage: string = "Hello World" - @Link linkMessage: string - build() { - Column() { - Text(this.stateMessage) - .fontSize(50) - .fontWeight(FontWeight.Bold) - } - .width('100%') - } -} -``` - -## 自定义组件名,类名,函数名和系统组件名相同约束。 - -自定义组件名,类名,函数名不能和系统组件名相同。 - -示例: - -``` -// Rect.ets -export class Rect { - constructor(){} -} -// Index.ets -// ERROR: The module name 'Rect' can not be the same as the inner component name. -import { Rect } from './Rect'; -@Entry -@Component -struct Index { - build() { - - } -} -``` - diff --git a/zh-cn/application-dev/quick-start/arkts-state-management-overview.md b/zh-cn/application-dev/quick-start/arkts-state-management-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..f9cef2129955f17534cb1efd1be00c17cad7b120 --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-state-management-overview.md @@ -0,0 +1,125 @@ +# 状态管理概述 + + +在前文的描述中,我们构建的页面多为静态界面。如果希望构建一个动态的、有交互的界面,就需要引入“状态”的概念。 + + + **图1** 效果图   + +![Video_2023-03-06_152548](figures/Video_2023-03-06_152548.gif) + + +上面的示例中,用户与应用程序的交互触发了文本状态变更,状态变更引起了UI渲染,UI从“Hello World”变更为“Hello ArkUI”。 + + +在声明式UI编程框架中,UI是程序状态的运行结果,用户构建了一个UI模型,其中应用的运行时的状态是参数。当参数改变时,UI作为返回结果,也将进行对应的改变。这些运行时的状态变化所带来的UI的重新渲染,在ArkUI中统称为状态管理机制。 + + +自定义组件拥有变量,变量必须被装饰器装饰才可以成为状态变量,状态变量的改变会引起UI的渲染刷新。如果不使用状态变量,UI只能在初始化时渲染,后续将不会再刷新。 下图展示了State和View(UI)之间的关系。 + + +![zh-cn_image_0000001562352677](figures/zh-cn_image_0000001562352677.png) + + +- View(UI):UI渲染,一般指自定义组件的build方法和\@Builder装饰的方法内的UI描述。 + +- State:状态,一般指的是装饰器装饰的数据。用户通过触发组件的事件方法,改变状态数据。状态数据的改变,引起UI的重新渲染。 + + +## 基本概念 + +- 状态变量:被状态装饰器装饰的变量,改变会引起UI的渲染更新。 + +- 常规变量:没有状态的变量,通常应用于辅助计算。它的改变永远不会引起UI的刷新。 + +- 数据源/同步源:状态变量的原始来源,可以同步给不同的状态数据。通常意义为父组件传给子组件的数据。 + +- 命名参数机制:父组件通过指定参数传递给子组件的状态变量,为父子传递同步参数的主要手段。示例:CompA: ({ aProp: this.aProp })。 + +- 从父组件初始化:父组件传使用命名参数机制,将指定参数传递给子组件。本地初始化的默认值在有父组件传值的情况下,会被覆盖。示例: + + ```ts + @Component + struct MyComponent { + @State count: number = 0; + private increaseBy: number = 1; + + build() { + } + } + + @Component + struct Parent { + build() { + Column() { + // 从父组件初始化,覆盖本地定义的默认值 + MyComponent({ count: 1, increaseBy: 2 }) + } + } + } + ``` + +- 初始化子节点:组件中状态变量可以传递给子组件,初始化子组件对应的状态变量。示例同上。 + +- 本地初始化:变量声明的时候赋值,作为初始化的默认值。示例:\@State count: number = 0。 + + +## 装饰器总览 + +ArkUI提供了多种装饰器,通过使用这些装饰器,状态变量不仅可以观察在组件内的改变,还可以在不同组件层级间传递,比如父子组件、跨组件层级,也可以观察全局范围内的变化。根据状态变量的影响范围,将所有的装饰器可以大致分为: + + +- 管理组件拥有状态的装饰器:组件级别的状态管理,可以观察组件内变化,和不同组件层级的变化,但需要唯一观察同一个组件树上,即同一个页面内。 + +- 管理应用拥有状态的装饰器:应用级别的状态管理,可以观察不同页面,甚至不同UIAbility的状态变化,是应用内全局的状态管理。 + + +从数据的传递形式和同步类型层面看,装饰器也可分为: + + +- 只读的单向传递; + +- 可变更的双向传递。 + + +图示如下,具体装饰器的介绍,可详见[管理组件拥有的状态](arkts-state.md)和[管理应用拥有的状态](arkts-application-state-management-overview.md)。开发者可以灵活地利用这些能力来实现数据和UI的联动。 + + +![zh-cn_image_0000001502704640](figures/zh-cn_image_0000001502704640.png) + + +上图中,Components部分的装饰器为组件级别的状态管理,Application部分为应用的状态管理。开发者可以通过\@StorageLink/\@LocalStorageLink和\@StorageProp/\@LocalStorageProp实现应用和组件状态的双向和单向同步。图中箭头方向为数据同步方向,单箭头为单向同步,双箭头为双向同步。 + + +[管理组件拥有的状态](arkts-state.md),即图中Components级别的状态管理: + + +- \@State:\@State装饰的变量拥有其所属组件的状态,可以作为其子组件单向和双向同步的数据源。当其数值改变时,会引起相关组件的渲染刷新。 + +- \@Prop:\@Prop装饰的变量可以和父组件建立单向同步关系,\@Prop装饰的变量是可变的,但修改不会同步回父组件。 + +- \@Link:\@Link装饰的变量和父组件构建双向同步关系的状态变量,父组件会接受来自\@Link装饰的变量的修改的同步,父组件的更新也会同步给\@Link装饰的变量。 + +- \@Provide/\@Consume:\@Provide/\@Consume装饰的变量用于跨组件层级(多层组件)同步状态变量,可以不需要通过参数命名机制传递,通过alias(别名)或者属性名绑定。 + +- \@Observed:\@Observed装饰class,需要观察多层嵌套场景的class需要被\@Observed装饰。单独使用\@Observed没有任何作用,需要和\@ObjectLink、\@Prop连用。 + +- \@ObjectLink:\@ObjectLink装饰的变量接收\@Observed装饰的class的实例,应用于观察多层嵌套场景,和父组件的数据源构建双向同步。 + + +[管理应用拥有的状态](arkts-application-state-management-overview.md),即图中Application级别的状态管理: + + +- AppStorage是应用程序中的一个特殊的单例LocalStorage对象,是应用级的数据库,和进程绑定,通过[@StorageProp](arkts-appstorage.md#storageprop)和[@StorageLink](arkts-appstorage.md#storagelink)装饰器可以和组件联动。 + +- AppStorage是应用状态的“中枢”,需要和组件(UI)交互的数据存入AppStorage,比如持久化数据PersistentStorage和环境变量Environment。UI再通过AppStorage提供的装饰器或者API接口,访问这些数据; + +- 框架还提供了LocalStorage,AppStorage是LocalStorage特殊的单例。LocalStorage是应用程序声明的应用状态的内存“数据库”,通常用于页面级的状态共享,通过[@LocalStorageProp](arkts-localstorage.md#localstorageprop)和[@LocalStorageLink](arkts-localstorage.md#localstoragelink)装饰器可以和UI联动。 + + +### 其他状态管理功能 + +\@Watch用于监听状态变量的变化。 + + +$$运算符:给内置组件提供TS变量的引用,使得TS变量和内置组件的内部状态保持同步。 diff --git a/zh-cn/application-dev/quick-start/arkts-state-mgmt-application-level.md b/zh-cn/application-dev/quick-start/arkts-state-mgmt-application-level.md deleted file mode 100644 index 820d443c1ebedd22e9ad0617ff5c79906f0b1f23..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/quick-start/arkts-state-mgmt-application-level.md +++ /dev/null @@ -1,269 +0,0 @@ -# 应用级变量的状态管理 - -在前面的章节中,已经讲述了如何管理页面级变量的状态,本章将说明如何管理应用级变量的状态,具体接口说明请参考[应用级变量的状态管理接口](../reference/arkui-ts/ts-state-management.md)。 - -## AppStorage - -[AppStorage](../reference/arkui-ts/ts-state-management.md#appstorage)是应用程序中的单例对象,由UI框架在应用程序启动时创建,在应用程序退出时销毁,为应用程序范围内的可变状态属性提供中央存储。 - -AppStorage包含整个应用程序中需要访问的所有状态属性,只要应用程序保持运行,AppStorage就会保存所有属性及属性值,属性值可以通过唯一的键值进行访问。 - -组件可以通过装饰器将应用程序状态数据与AppStorage进行同步,应用业务逻辑的实现也可以通过接口访问AppStorage。 - -AppStorage的选择状态属性可以与不同的数据源或数据接收器同步,这些数据源和接收器可以是设备上的本地或远程,并具有不同的功能,如数据持久性。这样的数据源和接收器可以独立于UI在业务逻辑中实现。 - -默认情况下,AppStorage中的属性是可变的,AppStorage还可使用不可变(只读)属性。 - -> **说明**:[Worker](../reference/apis/js-apis-worker.md)和主线程只能通过[postMessage](../reference/apis/js-apis-worker.md#postmessage)交互,不能使用AppStorage进行交互。 - -### @StorageLink装饰器 - -组件通过使用@StorageLink(key)装饰的状态变量,与AppStorage建立双向数据绑定,key为AppStorage中的属性键值。当创建包含@StorageLink的状态变量的组件时,该状态变量的值将使用AppStorage中的值进行初始化。在UI组件中对@StorageLink的状态变量所做的更改将同步到AppStorage,并从AppStorage同步到任何其他绑定实例中,如PersistentStorage或其他绑定的UI组件。 - -### @StorageProp装饰器 - -组件通过使用@StorageProp(key)装饰的状态变量,与AppStorage建立单向数据绑定,key标识AppStorage中的属性键值。当创建包含@StorageProp的状态变量的组件时,该状态变量的值将使用AppStorage中的值进行初始化。AppStorage中属性值的更改会导致绑定该状态变量的UI组件进行状态更新。 - -### 示例 - -每次用户单击Count按钮时,this.varA变量值都会增加1,此变量与AppStorage中的varA同步。每次用户单击language按钮时,修改AppStorage中的languageCode,此修改会同步给this.languageCode变量。 - -```ts -// xxx.ets -@Entry -@Component -struct ComponentA { - @StorageLink('varA') varA: number = 2 - @StorageProp('languageCode') languageCode: string = 'en' - private label: string = 'count' - - aboutToAppear() { - this.label = (this.languageCode === 'zh') ? '数量' : 'Count' - } - - build() { - Column() { - Row({ space: 20 }) { - Button(`${this.label}: ${this.varA}`) - .onClick(() => { - AppStorage.Set('varA', AppStorage.Get('varA') + 1) - }) - Button(`language: ${this.languageCode}`) - .onClick(() => { - if (AppStorage.Get('languageCode') === 'zh') { - AppStorage.Set('languageCode', 'en') - } else { - AppStorage.Set('languageCode', 'zh') - } - this.label = (this.languageCode === 'zh') ? '数量' : 'Count' - }) - } - .margin({ top: 50, bottom: 50 }) - - Row() { - Button(`更改@StorageLink修饰的变量:${this.varA}`).height(40).fontSize(14) - .onClick(() => { - this.varA++ - }) - } - }.width('100%') - } -} -``` - -![appstorage](figures/appstorage.gif) - -## LocalStorage - -> **说明:** -> -> 该接口从API version 9开始支持。后续版本如有新增内容,则采用上角标单独标记该内容的起始版本。 - -LocalStorage是应用程序中的存储单元,生命周期跟随其关联的Ability。在Stage模型下,LocalStorage解决AppStorage共享范围过大的问题,提供Ability之间全局数据的隔离。同时,LocalStorage为应用程序范围内的可变状态属性和非可变状态属性提供存储,可变状态属性和非可变状态属性是构建应用程序UI的一部分,如一个Ability的UI。解决App与Ability之间数据互相干扰问题,多实例场景下同一个Ability类的不同Ability实例之间的数据互相干扰问题。在分布式迁移的场景下,Ability是系统调度的最小单元,配合LocalStorage更方便实现组件的数据迁移。 - -应用层:一个应用程序可以创建多个LocalStorage实例,应用程序的每一个Ability对应一个LocalStorage实例。 - -Ability:一个应用程序可以拥有多个Ability,一个Ability中的所有子组件最多可以分配一个LocalStorage实例。并且,Ability中的所有子组件都将继承对此LocalStorage实例存储对象的访问权。 - -一个组件最多可以访问一个LocalStorage实例,一个LocalStorage对象可以分配给多个组件。 - -### @LocalStorageLink装饰器 - -组件通过使用@LocalStorageLink(key)装饰的状态变量,key值为LocalStorage中的属性键值,与LocalStorage建立双向数据绑定。当创建包含@LocalStorageLink的状态变量的组件时,该状态变量的值将会使用LocalStorage中的值进行初始化。如果LocalStorage中未定义初始值,将使用@LocalStorageLink定义的初始值。在UI组件中对@LocalStorageLink的状态变量所做的更改将同步到LocalStorage中,并从LocalStorage同步到Ability下的组件中。 - -### @LocalStorageProp装饰器 - -组件通过使用LocalStorageProp(key)装饰的状态变量,key值为LocalStorage中的属性键值,与LocalStorage建立单向数据绑定。当创建包含@LocalStorageProp的状态变量的组件时,该状态变量的值将使用LocalStorage中的值进行初始化。LocalStorage中的属性值的更改会导致当前Ability下的所有UI组件进行状态更新。 - -> **说明:** 创建LocalStorage实例时如未定义初始值,可以使用组件内@LocalStorageLink和@LocalStorageProp的初始值。如果定义时给定了初始值,那么不会再使用@LocalStorageLink和@LocalStorageProp的初始值。 - -### 示例1(在一个Ability中创建LocalStorage) - -LocalStorage通过loadContent接口加载,接口说明详见[loadContent](../reference/apis/js-apis-window.md#loadcontent9-1)。 - -```ts -import UIAbility from '@ohos.app.ability.UIAbility'; - -export default class EntryAbility extends UIAbility { - storage: LocalStorage - - onCreate() { - this.storage = new LocalStorage() - this.storage.setOrCreate('storageSimpleProp', 121) - console.info('[Demo EntryAbility onCreate]') - } - - onDestroy() { - console.info('[Demo EntryAbility onDestroy]') - } - - onWindowStageCreate(windowStage) { - // storage作为参数传递给loadContent接口 - windowStage.loadContent('pages/Index', this.storage) - } - - onWindowStageDestroy() { - console.info('[Demo] EntryAbility onWindowStageDestroy') - } - - onForeground() { - console.info('[Demo] EntryAbility onForeground') - } - - onBackground() { - console.info('[Demo] EntryAbility onBackground') - } -} -``` - -@Component组件获取数据 - -```ts -// Index.ets -let storage = LocalStorage.GetShared() - -@Entry(storage) -@Component -struct LocalStorageComponent { - @LocalStorageLink('storageSimpleProp') simpleVarName: number = 0 - - build() { - Column() { - Button(`LocalStorageLink: ${this.simpleVarName.toString()}`) - .margin(20) - .onClick(() => { - this.simpleVarName += 1 - }) - Text(JSON.stringify(this.simpleVarName)) - .fontSize(50) - LocalStorageComponentProp() - }.width('100%') - } -} - -@Component -struct LocalStorageComponentProp { - @LocalStorageProp('storageSimpleProp') simpleVarName: number = 0 - - build() { - Column() { - Button(`LocalStorageProp: ${this.simpleVarName.toString()}`) - .margin(20) - .onClick(() => { - this.simpleVarName += 1 - }) - Text(JSON.stringify(this.simpleVarName)) - .fontSize(50) - }.width('100%') - } -} -``` - -![appstorage1](figures/appstorage1.gif) - -### 示例2(在Entry页面定义LocalStorage) - -```ts -// xxx.ets -let storage = new LocalStorage({ "PropA": 47 }) - -@Entry(storage) -@Component -struct ComA { - @LocalStorageLink("PropA") storageLink: number = 1 - - build() { - Column() { - Text(`Parent from LocalStorage ${this.storageLink}`) - .fontSize(18) - .margin(20) - .onClick(() => this.storageLink += 1) - Child() - } - } -} - -@Component -struct Child { - @LocalStorageLink("PropA") storageLink: number = 1 - - build() { - Text(`Child from LocalStorage ${this.storageLink}`) - .fontSize(18) - .margin(20) - .onClick(() => this.storageLink += 1) - } -} -``` - -![appstorage2](figures/appstorage2.gif) - -## PersistentStorage - -[PersistentStorage](../reference/arkui-ts/ts-state-management.md#persistentstorage)提供了一些静态方法用来管理应用持久化数据,可以将特定标记的持久化数据链接到AppStorage中,并由AppStorage接口访问对应持久化数据,或者通过@StorageLink装饰器来访问对应key的变量。 - -> **说明:** -> -> - PersistentStorage的PersistProp接口使用时,需要保证输入对应的key在AppStorage中存在。 -> - PersistentStorage的DeleteProp接口使用时,只能对本次应用启动时已经link过的数据生效。 - -```ts -// xxx.ets -PersistentStorage.PersistProp('highScore', '0') - -@Entry -@Component -struct PersistentComponent { - @StorageLink('highScore') highScore: string = '0' - @State currentScore: number = 0 - - build() { - Column() { - if (this.currentScore === Number(this.highScore)) { - Text(`new highScore : ${this.highScore}`).fontSize(18) - } - Button(`goal!, currentScore : ${this.currentScore}`) - .margin(20) - .onClick(() => { - this.currentScore++ - if (this.currentScore > Number(this.highScore)) { - this.highScore = this.currentScore.toString() - } - }) - }.width('100%') - } -} -``` - -![appstorage3](figures/appstorage3.gif) - -## Environment - -[Environment](../reference/arkui-ts/ts-state-management.md#environment)是框架在应用程序启动时创建的单例对象,它为AppStorage提供了一系列应用程序需要的环境状态数据,这些数据描述了应用程序运行的设备环境,包括系统语言、深浅色模式等等。Environment及其属性是不可变的,所有数据类型均为简单类型。如下示例展示了从Environment获取系统是否开启无障碍屏幕朗读: - -```ts -Environment.EnvProp('accessibilityEnabled', 'default') -var enable = AppStorage.Get('accessibilityEnabled') -``` - -accessibilityEnabled是Environment提供的系统默认变量识别符。首先需要将对应系统属性绑定到AppStorage上,再通过AppStorage中的方法或者装饰器访问对应的系统属性数据。 \ No newline at end of file diff --git a/zh-cn/application-dev/quick-start/arkts-state-mgmt-concepts.md b/zh-cn/application-dev/quick-start/arkts-state-mgmt-concepts.md deleted file mode 100644 index 1e446d1ebd7dc34fbfacf8917bfbfb88cc1384d7..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/quick-start/arkts-state-mgmt-concepts.md +++ /dev/null @@ -1,34 +0,0 @@ -# 基本概念 - -ArkTS提供了多维度的状态管理机制,在ArkUI开发框架中,和UI相关联的数据,不仅可以在组件内使用,还可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,也可以是应用全局范围内的传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活地利用这些能力来实现数据和UI的联动。 - - -![](figures/CoreSpec_figures_state-mgmt-overview.png) - - -## 页面级变量的状态管理 - -| 装饰器 | 装饰内容 | 说明 | -| ----------- | ------------------------- | ------------------------------------------------------------ | -| @State | 基本数据类型,类,数组 | 修饰的状态数据被修改时会触发组件的build方法进行UI界面更新。 | -| @Prop | 基本数据类型 | 修改后的状态数据用于在父组件和子组件之间建立单向数据依赖关系。修改父组件关联数据时,当前组件会重新渲染。 | -| @Link | 基本数据类型,类,数组 | 父子组件之间的双向数据绑定,父组件的内部状态数据作为数据源,任何一方所做的修改都会反映给另一方。 | -| @Observed | 类 | @Observed应用于类,表示该类中的数据变更被UI页面管理。 | -| @ObjectLink | 被@Observed所装饰类的对象 | @ObjectLink装饰的状态数据被修改时,在父组件或者其他兄弟组件内与它关联的状态数据所在的组件都会重新渲染。 | -| @Provide | 基本数据类型,类,数组 | @Provide作为数据的提供方,可以更新其子孙节点的数据,并触发页面重新渲染。 | -| @Consume | 基本数据类型,类,数组 | @Consume装饰的变量在感知到@Provide装饰的变量更新后,会触发当前自定义组件的重新渲染。 | - -## 应用级变量的状态管理 - -AppStorage是整个应用程序状态的中心“数据库”,UI框架会针对应用程序创建单例AppStorage对象,并提供相应的装饰器和接口供应用程序使用。 - -- @StorageLink:@StorageLink(name)的原理类似于@Consume(name),不同的是,该给定名称的链接对象是从AppStorage中获得的,在UI组件和AppStorage之间建立双向绑定同步数据。 -- @StorageProp:@StorageProp(name)将UI组件数据与AppStorage进行单向同步,AppStorage中值的更改会更新UI组件中的数据,但UI组件无法更改AppStorage中的数据。 -- AppStorage还提供了用于业务逻辑实现的API,用于添加、读取、修改和删除应用程序的状态数据,此API所做的更改会导致修改的状态数据同步到UI组件上进行UI更新。 -- LocalStorage是应用程序中每一个Ability的存储器。 -- @LocalStorageLink:组件通过使用@LocalStorageLink(key)装饰的状态变量,key值为LocalStorage中的属性键值,与LocalStorage建立双向数据绑定。 -- @LocalStorageProp:组件通过使用@LocalStorageProp(key)装饰的状态变量,key值为LocalStorage中的属性键值,与LocalStorage建立单向数据绑定。 -- PersistentStorage提供了一些静态方法用来管理应用持久化数据,可以将特定标记的持久化数据链接到AppStorage中,并由AppStorage接口访问对应持久化数据,或者通过@StorageLink装饰器来访问对应key的变量。 -- Environment是框架在应用程序启动时创建的单例对象,它为AppStorage提供了一系列应用程序需要的环境状态数据,这些数据描述了应用程序运行的设备环境。 - -请参考[状态变量数据类型声明的使用限制](arkts-restrictions-and-extensions.md)了解状态变量使用规范。 diff --git a/zh-cn/application-dev/quick-start/arkts-state-mgmt-page-level.md b/zh-cn/application-dev/quick-start/arkts-state-mgmt-page-level.md deleted file mode 100644 index c8ac889e835c71d0f017286bb8c639e4e5b8c1bb..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/quick-start/arkts-state-mgmt-page-level.md +++ /dev/null @@ -1,525 +0,0 @@ -# 页面级变量的状态管理 - -@State、@Prop、@Link、@Provide、@Consume、@ObjectLink、@Observed和@Watch用于管理页面级变量的状态。 - -请参考[状态变量多种数据类型声明的使用限制](./arkts-restrictions-and-extensions.md)了解@State、@Provide、 @Link和@Consume四种状态变量的约束条件。 - -## @State - -@State装饰的变量是组件内部的状态数据,当这些状态数据被修改时,将会调用所在组件的build方法进行UI刷新。 - -@State状态数据具有以下特征: - -- 支持多种类型数据:支持class、number、boolean、string强类型数据的值类型和引用类型,以及这些强类型构成的数组,即Array<class>、Array<string>、Array<boolean>、Array<number>。不支持object和any。 -- 支持多实例:组件不同实例的内部状态数据独立。 -- 内部私有:标记为@State的属性是私有变量,只能在组件内访问。 -- 需要本地初始化:必须为所有@State变量分配初始值,变量未初始化可能导致未定义的框架异常行为。 -- 创建自定义组件时支持通过状态变量名设置初始值:在创建组件实例时,可以通过变量名显式指定@State状态变量的初始值。 - -**示例:** - -在下面的示例中: - -- 用户定义的组件MyComponent定义了@State状态变量count和title。如果count或title的值发生变化,则执行MyComponent的build方法来重新渲染组件; - -- EntryComponent中有多个MyComponent组件实例,第一个MyComponent内部状态的更改不会影响第二个MyComponent; - -- 创建MyComponent实例时通过变量名给组件内的变量进行初始化,如: - - ```ts - MyComponent({ title: { value: 'Hello World 2' }, count: 7 }) - ``` - -```ts -// xxx.ets -class Model { - value: string - - constructor(value: string) { - this.value = value - } -} - -@Entry -@Component -struct EntryComponent { - build() { - Column() { - MyComponent({ count: 1, increaseBy: 2 }) // 第1个MyComponent实例 - MyComponent({ title: { value: 'Hello World 2' }, count: 7 }) // 第2个MyComponent实例 - } - } -} - -@Component -struct MyComponent { - @State title: Model = { value: 'Hello World' } - @State count: number = 0 - private toggle: string = 'Hello World' - private increaseBy: number = 1 - - build() { - Column() { - Text(`${this.title.value}`).fontSize(30) - Button('Click to change title') - .margin(20) - .onClick(() => { - // 修改内部状态变量title - this.title.value = (this.toggle == this.title.value) ? 'Hello World' : 'Hello ArkUI' - }) - - Button(`Click to increase count=${this.count}`) - .margin(20) - .onClick(() => { - // 修改内部状态变量count - this.count += this.increaseBy - }) - } - } -} -``` - - -## @Prop - -@Prop与@State有相同的语义,但初始化方式不同。@Prop装饰的变量必须使用其父组件提供的@State变量进行初始化,允许组件内部修改@Prop变量,但变量的更改不会通知给父组件,父组件变量的更改会同步到@prop装饰的变量,即@Prop属于单向数据绑定。 - -@Prop状态数据具有以下特征: - -- 支持简单类型:仅支持number、string、boolean等简单数据类型; -- 私有:仅支持组件内访问; -- 支持多个实例:一个组件中可以定义多个标有@Prop的属性; -- 创建自定义组件时将值传递给@Prop变量进行初始化:在创建组件的新实例时,必须初始化所有@Prop变量,不支持在组件内部进行初始化。 - -> **说明:** -> -> @Prop修饰的变量不能在组件内部进行初始化。 - -**示例:** - -在下面的示例中,当按“+1”或“-1”按钮时,父组件状态发生变化,重新执行build方法,此时将创建一个新的CountDownComponent组件实例。父组件的countDownStartValue状态变量被用于初始化子组件的@Prop变量,当按下子组件的“count - costOfOneAttempt”按钮时,其@Prop变量count将被更改,CountDownComponent重新渲染,但是count值的更改不会影响父组件的countDownStartValue值。 - -```ts -// xxx.ets -@Entry -@Component -struct ParentComponent { - @State countDownStartValue: number = 10 // 初始化countDownStartValue - - build() { - Column() { - Text(`Grant ${this.countDownStartValue} nuggets to play.`).fontSize(18) - Button('+1 - Nuggets in New Game') - .margin(15) - .onClick(() => { - this.countDownStartValue += 1 - }) - - Button('-1 - Nuggets in New Game') - .margin(15) - .onClick(() => { - this.countDownStartValue -= 1 - }) - // 创建子组件时,必须在构造函数参数中提供其@Prop变量count的初始值,同时初始化常规变量costOfOneAttempt(非Prop变量) - CountDownComponent({ count: this.countDownStartValue, costOfOneAttempt: 2 }) - } - } -} - -@Component -struct CountDownComponent { - @Prop count: number - private costOfOneAttempt: number - - build() { - Column() { - if (this.count > 0) { - Text(`You have ${this.count} Nuggets left`).fontSize(18) - } else { - Text('Game over!').fontSize(18) - } - - Button('count - costOfOneAttempt') - .margin(15) - .onClick(() => { - this.count -= this.costOfOneAttempt - }) - } - } -} -``` - - -## @Link - -@Link装饰的变量可以和父组件的@State变量建立双向数据绑定: - -- 支持多种类型:@Link支持的数据类型与@State相同,即class、number、string、boolean或这些类型的数组; -- 私有:仅支持组件内访问; -- 单个数据源:父组件中用于初始化子组件@Link变量的必须是父组件定义的状态变量; -- 双向通信:子组件对@Link变量的更改将同步修改父组件中的@State变量; -- 创建自定义组件时需要将变量的引用传递给@Link变量,在创建组件的新实例时,必须使用命名参数初始化所有@Link变量。@Link变量可以使用@State变量或@Link变量的引用进行初始化,@State变量可以通过`'$'`操作符创建引用。 - -> **说明:** -> -> @Link修饰的变量不能在组件内部进行初始化。 - -**简单类型示例:** - -@Link语义是从`'$'`操作符引出,即`$isPlaying`是`this.isPlaying`内部状态的双向数据绑定。当单击子组件PlayButton中的按钮时,@Link变量更改,PlayButton与父组件中的Text和Button将同时进行刷新,同样地,当点击父组件中的Button修改`this.isPlaying`时,子组件PlayButton与父组件中的Text和Button也将同时刷新。 - -```ts -// xxx.ets -@Entry -@Component -struct Player { - @State isPlaying: boolean = false - - build() { - Column() { - PlayButton({ buttonPlaying: $isPlaying }) - Text(`Player is ${this.isPlaying ? '' : 'not'} playing`).fontSize(18) - Button('Parent:' + this.isPlaying) - .margin(15) - .onClick(() => { - this.isPlaying = !this.isPlaying - }) - } - } -} - -@Component -struct PlayButton { - @Link buttonPlaying: boolean - - build() { - Column() { - Button(this.buttonPlaying ? 'pause' : 'play') - .margin(20) - .onClick(() => { - this.buttonPlaying = !this.buttonPlaying - }) - } - } -} -``` - -**复杂类型示例:** - -```ts -// xxx.ets -@Entry -@Component -struct Parent { - @State arr: number[] = [1, 2, 3] - - build() { - Column() { - Child({ items: $arr }) - Button('Parent Button: splice') - .margin(10) - .onClick(() => { - this.arr.splice(0, 1, 60) - }) - ForEach(this.arr, item => { - Text(item.toString()).fontSize(18).margin(10) - }, item => item.toString()) - } - } -} - - -@Component -struct Child { - @Link items: number[] - - build() { - Column() { - Button('Child Button1: push') - .margin(15) - .onClick(() => { - this.items.push(100) - }) - Button('Child Button2: replace whole item') - .margin(15) - .onClick(() => { - this.items = [100, 200, 300] - }) - } - } -} -``` - -**@Link、@State和@Prop结合使用示例:** - -下面示例中,ParentView包含ChildA和ChildB两个子组件,ParentView的状态变量counter分别用于初始化ChildA的@Prop变量和ChildB的@Link变量。 - -- ChildB使用@Link建立双向数据绑定,当ChildB修改counterRef状态变量值时,该更改将同步到ParentView和ChildA共享; -- ChildA使用@Prop建立从ParentView到自身的单向数据绑定,当ChildA修改counterVal状态变量值时,ChildA将重新渲染,但该更改不会传达给ParentView和ChildB。 - -```ts -// xxx.ets -@Entry -@Component -struct ParentView { - @State counter: number = 0 - - build() { - Column() { - ChildA({ counterVal: this.counter }) - ChildB({ counterRef: $counter }) - } - } -} - -@Component -struct ChildA { - @Prop counterVal: number - - build() { - Button(`ChildA: (${this.counterVal}) + 1`) - .margin(15) - .onClick(() => { - this.counterVal += 1 - }) - } -} - -@Component -struct ChildB { - @Link counterRef: number - - build() { - Button(`ChildB: (${this.counterRef}) + 1`) - .margin(15) - .onClick(() => { - this.counterRef += 1 - }) - } -} -``` - -## @Observed和ObjectLink数据管理 - -当开发者需要在子组件中针对父组件的一个变量(parent_a)设置双向同步时,开发者可以在父组件中使用@State装饰变量(parent_a),并在子组件中使用@Link装饰对应的变量(child_a)。这样不仅可以实现父组件与单个子组件之间的数据同步,也可以实现父组件与多个子组件之间的数据同步。如下图所示,可以看到,父子组件针对ClassA类型的变量设置了双向同步,那么当子组件1中变量对应的属性c的值变化时,会通知父组件同步变化,而当父组件中属性c的值变化时,会通知所有子组件同步变化。 - -![zh-cn_image_0000001251090821](figures/zh-cn_image_0000001251090821.png) - -然而,上述例子是针对某个数据对象进行的整体同步,而当开发者只想针对父组件中某个数据对象的部分信息进行同步时,使用@Link就不能满足要求。如果这些部分信息是一个类对象,就可以使用@ObjectLink配合@Observed来实现,如下图所示。 - -![zh-cn_image_0000001206450834](figures/zh-cn_image_0000001206450834.png) - -### 设置要求 - -- @Observed用于类,@ObjectLink用于变量。 - -- @ObjectLink装饰的变量类型必须为类(class type)。 - - 类要被@Observed装饰器所装饰。 - - 不支持简单类型参数,可以使用@Prop进行单向同步。 - -- @ObjectLink装饰的变量是不可变的。 - - 属性的改动是被允许的,当改动发生时,如果同一个对象被多个@ObjectLink变量所引用,那么所有拥有这些变量的自定义组件都会被通知进行重新渲染。 - -- @ObjectLink装饰的变量不可设置默认值。 - - 必须让父组件中有一个由@State、@Link、@StorageLink、@Provide或@Consume装饰的变量所参与的TS表达式进行初始化。 - -- @ObjectLink装饰的变量是私有变量,只能在组件内访问。 - - -### 示例 - -```ts -// xxx.ets -// 父组件ViewB中的类对象ClassA与子组件ViewA保持数据同步时,可以使用@ObjectLink和@Observed,绑定该数据对象的父组件和其他子组件同步更新 -var nextID: number = 0 - -@Observed -class ClassA { - public name: string - public c: number - public id: number - - constructor(c: number, name: string = 'OK') { - this.name = name - this.c = c - this.id = nextID++ - } -} - -@Component -struct ViewA { - label: string = 'ViewA1' - @ObjectLink a: ClassA - - build() { - Row() { - Button(`ViewA [${this.label}] this.a.c= ${this.a.c} +1`) - .onClick(() => { - this.a.c += 1 - }) - }.margin({ top: 10 }) - } -} - -@Entry -@Component -struct ViewB { - @State arrA: ClassA[] = [new ClassA(0), new ClassA(0)] - - build() { - Column() { - ForEach(this.arrA, (item) => { - ViewA({ label: `#${item.id}`, a: item }) - }, (item) => item.id.toString()) - ViewA({ label: `this.arrA[first]`, a: this.arrA[0] }) - ViewA({ label: `this.arrA[last]`, a: this.arrA[this.arrA.length - 1] }) - - Button(`ViewB: reset array`) - .margin({ top: 10 }) - .onClick(() => { - this.arrA = [new ClassA(0), new ClassA(0)] - }) - Button(`ViewB: push`) - .margin({ top: 10 }) - .onClick(() => { - this.arrA.push(new ClassA(0)) - }) - Button(`ViewB: shift`) - .margin({ top: 10 }) - .onClick(() => { - this.arrA.shift() - }) - }.width('100%') - } -} -``` - - -## @Provide和@Consume - -@Provide作为数据的提供方,可以更新其子孙节点的数据,并触发页面渲染。@Consume在感知到@Provide数据的更新后,会触发当前自定义组件的重新渲染。 - -> **说明:** -> -> 使用@Provide和@Consume时应避免循环引用导致死循环。 - -### @Provide - -| 名称 | 说明 | -| -------------- | ------------------------------------------------------------ | -| 装饰器参数 | 是一个string类型的常量,用于给装饰的变量起别名。如果规定别名,则提供对应别名的数据更新。如果没有,则使用变量名作为别名。推荐使用@Provide('alias')这种形式。 | -| 同步机制 | @Provide的变量类似@State,可以修改对应变量进行页面重新渲染。也可以修改@Consume装饰的变量,反向修改@State变量。 | -| 初始值 | 必须设置初始值。 | -| 页面重渲染场景 | 触发页面渲染的修改:
- 基础类型(boolean,string,number)变量的改变;
- @Observed class类型变量及其属性的修改;
- 添加,删除,更新数组中的元素。 | - -### @Consume - -| 类型 | 说明 | -| ------ | ---------------- | -| 初始值 | 不可设置默认初始值。 | - -### 示例 - -```ts -// xxx.ets -@Entry -@Component -struct CompA { - @Provide("reviewVote") reviewVotes: number = 0; - - build() { - Column() { - CompB() - Button(`CompA: ${this.reviewVotes}`) - .margin(10) - .onClick(() => { - this.reviewVotes += 1; - }) - } - } -} - -@Component -struct CompB { - build() { - Column() { - CompC() - } - } -} - -@Component -struct CompC { - @Consume("reviewVote") reviewVotes: number - - build() { - Column() { - Button(`CompC: ${this.reviewVotes}`) - .margin(10) - .onClick(() => { - this.reviewVotes += 1 - }) - }.width('100%') - } -} -``` - -## @Watch - -@Watch用于监听状态变量的变化,语法结构为: - -```ts -@State @Watch("onChanged") count : number = 0 -``` - -如上所示,给状态变量增加一个@Watch装饰器,通过@Watch注册一个回调方法onChanged, 当状态变量count被改变时, 触发onChanged回调。 - -装饰器@State、@Prop、@Link、@ObjectLink、@Provide、@Consume、@StorageProp以及@StorageLink所装饰的变量均可以通过@Watch监听其变化。 - - -> **说明:** -> -> 深层次数据修改不会触发@Watch回调,例如无法监听数组中对象值的改变。 - -```ts -// xxx.ets -@Entry -@Component -struct CompA { - @State @Watch('onBasketUpdated') shopBasket: Array = [7, 12, 47, 3] - @State totalPurchase: number = 0 - @State addPurchase: number = 0 - - aboutToAppear() { - this.updateTotal() - } - - updateTotal(): void { - let sum = 0; - this.shopBasket.forEach((i) => { - sum += i - }) - // 计算新的购物篮总价值,如果超过100,则适用折扣 - this.totalPurchase = (sum < 100) ? sum : 0.9 * sum - return this.totalPurchase - } - - // shopBasket更改时触发该方法 - onBasketUpdated(propName: string): void { - this.updateTotal() - } - - build() { - Column() { - Button('add to basket ' + this.addPurchase) - .margin(15) - .onClick(() => { - this.addPurchase = Math.round(100 * Math.random()) - this.shopBasket.push(this.addPurchase) - }) - Text(`${this.totalPurchase}`) - .fontSize(30) - } - } -} -``` \ No newline at end of file diff --git a/zh-cn/application-dev/quick-start/arkts-state.md b/zh-cn/application-dev/quick-start/arkts-state.md new file mode 100644 index 0000000000000000000000000000000000000000..50cc33b3bf26e654dbb3db5ef408533e8a85fc8d --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-state.md @@ -0,0 +1,261 @@ +# \@State:组件内状态 + + +\@State装饰的变量,或称为状态变量,一旦变量拥有了状态属性,就和自定义组件的渲染绑定起来。当状态改变时,UI会发生对应的渲染改变。 + + +在状态变量相关装饰器中,\@State是最基础的,使变量拥有状态属性的装饰器,它也是大部分状态变量的数据源。 + + +> **说明:** +> +> 从API version 9开始,该装饰器支持在ArkTS卡片中使用。 + + +## 概述 + +\@State装饰的变量,与声明式范式中的其他被装饰变量一样,是私有的,只能从组件内部访问,在声明时必须指定其类型和本地初始化。初始化也可选择使用命名参数机制从父组件完成初始化。 + +\@State装饰的变量拥有以下特点: + +- \@State装饰的变量与子组件中的\@Prop、\@Link或\@ObjectLink装饰变量之间建立单向或双向数据同步。 + +- \@State装饰的变量生命周期与其所属自定义组件的生命周期相同。 + + +## 装饰器使用规则说明 + +| \@State变量装饰器 | 说明 | +| ------------ | ---------------------------------------- | +| 装饰器参数 | 无 | +| 同步类型 | 不与父组件中任何类型的变量同步。 | +| 允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。嵌套类型的场景请参考[观察变化](#观察变化)。
类型必须被指定。
不支持any,不支持简单类型和复杂类型的联合类型,不允许使用undefined和null。
**说明:**
建议不要装饰Date类型,应用可能会产生异常行为。
不支持Length、ResourceStr、ResourceColor类型,Length、ResourceStr、ResourceColor为简单类型和复杂类型的联合类型。 | +| 被装饰变量的初始值 | 必须指定。 | + + +## 变量的传递/访问规则说明 + +| 传递/访问 | 说明 | +| --------- | ---------------------------------------- | +| 从父组件初始化 | 可选,从父组件初始化或者本地初始化。
支持父组件中常规变量、\@State、\@Link、\@Prop、\@Provide、\@Consume、\@ObjectLink、\@StorageLink、\@StorageProp、\@LocalStorageLink和\@LocalStorageProp装饰的变量,初始化子组件的\@State。 | +| 用于初始化子组件 | \@State装饰的变量支持初始化子组件的常规变量、\@State、\@Link、\@Prop、\@Provide。 | +| 是否支持组件外访问 | 不支持,只能在组件内访问。 | + + **图1** 初始化规则图示   + +![zh-cn_image_0000001502091796](figures/zh-cn_image_0000001502091796.png) + + +## 观察变化和行为表现 + +并不是状态变量的所有更改都会引起UI的刷新,只有可以被框架观察到的修改才会引起UI刷新。该小节去介绍什么样的修改才能被观察到,以及观察到变化后,框架的是怎么引起UI刷新的,即框架的行为表现是什么。 + + +### 观察变化 + +- 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。 + + ```ts + // for simple type + @State count: number = 0; + // value changing can be observed + this.count = 1; + ``` + +- 当装饰的数据类型为class或者Object时,可以观察到自身的赋值的变化,和其属性赋值的变化,即Object.keys(observedObject)返回的所有属性。例子如下。 + 声明ClassA和Model类。 + + ```ts + class ClassA { + public value: string; + + constructor(value: string) { + this.value = value; + } + } + + class Model { + public value: string; + public name: ClassA; + constructor(value: string, a: ClassA) { + this.value = value; + this.name = a; + } + } + ``` + + \@State装饰的类型是Model + + ```ts + // class类型 + @State title: Model = new Model('Hello', new ClassA('World')); + ``` + + 对\@State装饰变量的赋值。 + + ```ts + // class类型赋值 + this.title = new Model('Hi', new ClassA('ArkUI')); + ``` + + 对\@State装饰变量的属性赋值。 + + ```ts + // class属性的赋值 + this.title.value = 'Hi' + ``` + + 嵌套属性的赋值观察不到。 + + ```ts + // 嵌套的属性赋值观察不到 + this.title.name.value = 'ArkUI' + ``` +- 当装饰的对象是array时,可以观察到数组本身的赋值和添加、删除、更新数组的变化。例子如下。 + 声明ClassA和Model类。 + + ```ts + class Model { + public value: number; + constructor(value: number) { + this.value = value; + } + } + ``` + + \@State装饰的对象为Model类型数组时。 + + ```ts + @State title: Model[] = [new Model(11), new Model(1)] + ``` + + 数组自身的赋值可以观察到。 + + ```ts + this.title = [new Model(2)] + ``` + + 数组项的赋值可以观察到。 + + ```ts + this.title[0] = new Model(2) + ``` + + 删除数组项可以观察到。 + + ```ts + this.title.pop() + ``` + + 新增数组项可以观察到。 + + ```ts + this.title.push(new Model(12)) + ``` + + +### 框架行为 + +- 当状态变量被改变时,查询依赖该状态变量的组件; + +- 执行依赖该状态变量的组件的更新方法,组件更新渲染; + +- 和该状态变量不相关的组件或者UI描述不会发生重新渲染,从而实现页面渲染的按需更新。 + + +## 使用场景 + + +### 装饰简单类型的变量 + +以下示例为\@State装饰的简单类型,count被\@State装饰成为状态变量,count的改变引起Button组件的刷新: + +- 当状态变量count改变时,查询到只有Buttont组件关联了它; + +- 执行Button组件的更新方法,实现按需刷新。 + + +```ts +@Entry +@Component +struct MyComponent { + @State count: number = 0; + + build() { + Button(`click times: ${this.count}`) + .onClick(() => { + this.count += 1; + }) + } +} +``` + + +### 装饰class对象类型的变量 + +- 自定义组件MyComponent定义了被\@State装饰的状态变量count和title,其中title的类型为自定义类Model。如果count或title的值发生变化,则查询MyComponent中使用该状态变量的UI组件,并进行重新渲染。 + +- EntryComponent中有多个MyComponent组件实例,第一个MyComponent内部状态的更改不会影响第二个MyComponent。 + + + +```ts +class Model { + public value: string; + + constructor(value: string) { + this.value = value; + } +} + +@Entry +@Component +struct EntryComponent { + build() { + Column() { + // 此处指定的参数都将在初始渲染时覆盖本地定义的默认值,并不是所有的参数都需要从父组件初始化 + MyComponent({ count: 1, increaseBy: 2 }) + MyComponent({ title: new Model('Hello, World 2'), count: 7 }) + } + } +} + +@Component +struct MyComponent { + @State title: Model = new Model('Hello World'); + @State count: number = 0; + private increaseBy: number = 1; + + build() { + Column() { + Text(`${this.title.value}`) + Button(`Click to change title`).onClick(() => { + // @State变量的更新将触发上面的Text组件内容更新 + this.title.value = this.title.value === 'Hello ArkUI' ? 'Hello World' : 'HelloArkUI'; + }) + + Button(`Click to increase count=${this.count}`).onClick(() => { + // @State变量的更新将触发上面的Text组件内容更新 + this.count += this.increaseBy; + }) + } + } +} +``` + + +从该示例中,我们可以了解到\@State变量首次渲染的初始化流程: + + +1. 使用默认的本地初始化: + + ```ts + @State title: Model = new Model('Hello World'); + @State count: number = 0; + ``` + +2. 对于\@State来说,命名参数机制传递的值并不是必选的,如果没有命名参数传值,则使用本地初始化的默认值: + + ```ts + MyComponent({ count: 1, increaseBy: 2 }) + ``` diff --git a/zh-cn/application-dev/quick-start/arkts-statestyles.md b/zh-cn/application-dev/quick-start/arkts-statestyles.md new file mode 100644 index 0000000000000000000000000000000000000000..171d0e0ad3445ed5d8ea4c3c0f65e0a6c8b8f5e6 --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-statestyles.md @@ -0,0 +1,128 @@ +# stateStyles:多态样式 + + +\@Styles和\@Extend仅仅应用于静态页面的样式复用,stateStyles可以依据组件的内部状态的不同,快速设置不同样式。这就是我们本章要介绍的内容stateStyles(又称为:多态样式)。 + + +## 概述 + +stateStyles是属性方法,可以根据UI内部状态来设置样式,类似于css伪类,但语法不同。ArkUI提供以下四种状态: + +- focused:获焦态。 + +- normal:正常态。 + +- pressed:按压态。 + +- disabled:不可用态。 + + +## 使用场景 + + +### 基础场景 + +下面的示例展示了stateStyles最基本的使用场景。Button处于第一个组件,默认获焦,生效focused指定的粉色样式。按压时显示为pressed态指定的黑色。如果在Button前再放一个组件,使其不处于获焦态,就会生效normal态的黄色。 + + +```ts +@Entry +@Component +struct StateStylesSample { + build() { + Column() { + Button('Click me') + .stateStyles({ + focused: { + .backgroundColor(Color.Pink) + }, + pressed: { + .backgroundColor(Color.Black) + }, + normal: { + .backgroundColor(Color.Yellow) + } + }) + }.margin('30%') + } +} +``` + + + **图1** 获焦态和按压态   + +![Video_2023-03-17_120758](figures/Video_2023-03-17_120758.gif) + + +### \@Styles和stateStyles联合使用 + +以下示例通过\@Styles指定stateStyles的不同状态。 + + + +```ts +@Entry +@Component +struct MyComponent { + @Styles normalStyle() { + .backgroundColor(Color.Gray) + } + + @Styles pressedStyle() { + .backgroundColor(Color.Red) + } + + build() { + Column() { + Text('Text1') + .fontSize(50) + .fontColor(Color.White) + .stateStyles({ + normal: this.normalStyle, + pressed: this.pressedStyle, + }) + } + } +} +``` + + **图2** 正常态和按压态   + +![Video_2023-03-17_144824](figures/Video_2023-03-17_144824.gif) + + +### 在stateStyles里使用常规变量和状态变量 + +stateStyles可以通过this绑定组件内的常规变量和状态变量。 + + +```ts +@Entry +@Component +struct CompWithInlineStateStyles { + @State focusedColor: Color = Color.Red; + normalColor: Color = Color.Green + + build() { + Button('clickMe').height(100).width(100) + .stateStyles({ + normal: { + .backgroundColor(this.normalColor) + }, + focused: { + .backgroundColor(this.focusedColor) + } + }) + .onClick(() => { + this.focusedColor = Color.Pink + }) + .margin('30%') + } +} +``` + +Button默认获焦显示红色,点击事件触发后,获焦态变为粉色。 + + **图3** 点击改变获焦态样式   + +![Video_2023-03-17_144605](figures/Video_2023-03-17_144605.gif) diff --git a/zh-cn/application-dev/quick-start/arkts-style.md b/zh-cn/application-dev/quick-start/arkts-style.md new file mode 100644 index 0000000000000000000000000000000000000000..04a862b4a6c13dd6386738971e8630b883707e2b --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-style.md @@ -0,0 +1,104 @@ +# \@Styles:定义组件重用样式 + + +如果每个组件的样式都需要单独设置,在开发过程中会出现大量代码在进行重复样式设置,虽然可以复制粘贴,但为了代码简洁性和后续方便维护,我们推出了可以提炼公共样式进行复用的装饰器\@Styles。 + + +\@Styles装饰器可以将多条样式设置提炼成一个方法,直接在组件声明的位置调用。通过\@Styles装饰器可以快速定义并复用自定义样式。用于快速定义并复用自定义样式。 + + +> **说明:** +> +> 从API version 9开始,该装饰器支持在ArkTS卡片中使用。 + + +## 装饰器使用说明 + +- 当前\@Styles仅支持[通用属性](../reference/arkui-ts/ts-universal-attributes-size.md)和[通用事件](../reference/arkui-ts/ts-universal-events-click.md)。 + +- \@Styles方法不支持参数,反例如下。 + + ```ts + // 反例: @Styles不支持参数 + @Styles function globalFancy (value: number) { + .width(value) + } + ``` + +- \@Styles可以定义在组件内或全局,在全局定义时需在方法名前面添加function关键字,组件内定义时则不需要添加function关键字。 + + ```ts + // 全局 + @Styles function functionName() { ... } + + // 在组件内 + @Component + struct FancyUse { + @Styles fancy() { + .height(100) + } + } + ``` + +- 定义在组件内的\@Styles可以通过this访问组件的常量和状态变量,并可以在\@Styles里通过事件来改变状态变量的值,示例如下: + + ```ts + @Component + struct FancyUse { + @State heightVlaue: number = 100 + @Styles fancy() { + .height(this.heightVlaue) + .backgroundColor(Color.Yellow) + .onClick(() => { + this.heightVlaue = 200 + }) + } + } + ``` + +- 组件内\@Styles的优先级高于全局\@Styles。 + 框架优先找当前组件内的\@Styles,如果找不到,则会全局查找。 + + +## 使用场景 + +以下示例中演示了组件内\@Styles和全局\@Styles的用法。 + + + +```ts +// 定义在全局的@Styles封装的样式 +@Styles function globalFancy () { + .width(150) + .height(100) + .backgroundColor(Color.Pink) +} + +@Entry +@Component +struct FancyUse { + @State heightVlaue: number = 100 + // 定义在组件内的@Styles封装的样式 + @Styles fancy() { + .width(200) + .height(this.heightVlaue) + .backgroundColor(Color.Yellow) + .onClick(() => { + this.heightVlaue = 200 + }) + } + + build() { + Column({ space: 10 }) { + // 使用全局的@Styles封装的样式 + Text('FancyA') + .globalFancy () + .fontSize(30) + // 使用全局的@Styles封装的样式 + Text('FancyB') + .fancy() + .fontSize(30) + } + } +} +``` diff --git a/zh-cn/application-dev/quick-start/arkts-two-way-sync.md b/zh-cn/application-dev/quick-start/arkts-two-way-sync.md new file mode 100644 index 0000000000000000000000000000000000000000..c48a7a148efe5169278de9224faa8da0f84926df --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-two-way-sync.md @@ -0,0 +1,47 @@ +# $语法:内置组件双向同步 + + +$$运算符为系统内置组件提供TS变量的引用,使得TS变量和系统内置组件的内部状态保持同步。 + + +内部状态具体指什么取决于组件。例如,[bindPopup](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/arkui-ts/ts-universal-attributes-popup.md)属性方法的show参数。 + + +## 使用规则 + +- 当前$$支持基础类型变量,以及\@State、\@Link和\@Prop装饰的变量。 + +- 当前$$仅支持[bindPopup](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/arkui-ts/ts-universal-attributes-popup.md)属性方法的show参数,[Radio](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-radio.md)组件的checked属性,[Refresh](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/arkui-ts/ts-container-refresh.md)组件的refreshing参数。 + +- $$绑定的变量变化时,会触发UI的同步刷新。 + + +## 使用示例 + +以[bindPopup](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/arkui-ts/ts-universal-attributes-popup.md)属性方法的show参数为例: + + +```ts +// xxx.ets +@Entry +@Component +struct bindPopupPage { + @State customPopup: boolean = false; + + build() { + Column() { + Button('Popup') + .margin(20) + .onClick(() => { + this.customPopup = !this.customPopup + }) + .bindPopup($$this.customPopup, { + message: 'showPopup' + }) + } + } +} +``` + + +![popup](figures/popup.gif) diff --git a/zh-cn/application-dev/quick-start/arkts-watch.md b/zh-cn/application-dev/quick-start/arkts-watch.md new file mode 100644 index 0000000000000000000000000000000000000000..b1dbbfb0e395c8e7e628783635cb899e0468992a --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-watch.md @@ -0,0 +1,175 @@ +# \@Watch:状态变量更改通知 + + +\@Watch应用于对状态变量的监听。如果开发者需要关注某个状态变量的值是否改变,可以使用\@Watch为状态变量设置回调函数。 + + +> **说明:** +> +> 从API version 9开始,该装饰器支持在ArkTS卡片中使用。 + + +## 概述 + +\@Watch用于监听状态变量的变化,当状态变量变化时,\@Watch的回调方法将被调用。\@Watch在ArkUI框架内部判断数值有无更新使用的是严格相等(===),遵循严格相等规范。当在严格相等为false的情况下,就会触发\@Watch的回调。 + + +## 装饰器说明 + +| \@Watch补充变量装饰器 | 说明 | +| -------------- | ---------------------------------------- | +| 装饰器参数 | 必填。常量字符串,字符串需要有引号。是(string) => void自定义成员函数的方法的引用。 | +| 可装饰的自定义组件变量 | 可监听所有装饰器装饰的状态变量。不允许监听常规变量。 | +| 装饰器的顺序 | 建议\@State、\@Prop、\@Link等装饰器在\@Watch装饰器之前。 | + + +## 语法说明 + +| 类型 | 说明 | +| ---------------------------------------- | ---------------------------------------- | +| (changedPropertyName? : string) => void | 该函数是自定义组件的成员函数,changedPropertyName是被watch的属性名。
在多个状态变量绑定同一个\@Watch的回调方法的时候,可以通过changedPropertyName进行不同的逻辑处理
将属性名作为字符串输入参数,不返回任何内容。 | + + +## 观察变化和行为表现 + +1. 当观察到状态变量的变化(包括双向绑定的AppStorage和LocalStorage中对应的key发生的变化)的时候,对应的\@Watch的回调方法将被触发; + +2. \@Watch方法在自定义组件的属性变更之后同步执行; + +3. 如果在\@Watch的方法里改变了其他的状态变量,也会引起的状态变更和\@Watch的执行; + +4. 在第一次初始化的时候,\@Watch装饰的方法不会被调用,即认为初始化不是状态变量的改变。只有在后续状态改变时,才会调用\@Watch回调方法。 + + +## 限制条件 + +- 建议开发者避免无限循环。循环可能是因为在\@Watch的回调方法里直接或者间接地修改了同一个状态变量引起的。为了避免循环的产生,建议不要在\@Watch的回调方法里修改当前装饰的状态变量; + +- 开发者应关注性能,属性值更新函数会延迟组件的重新渲染(具体请见上面的行为表现),因此,回调函数应仅执行快速运算; + +- 不建议在\@Watch函数中调用async await,因为\@Watch设计的用途是为了快速的计算,异步行为可能会导致重新渲染速度的性能问题。 + + +## 使用场景 + + +### \@Watch与\@Link组合使用 + +以下示例说明了如何在子组件中观察\@Link变量。 + + +```ts +class PurchaseItem { + static NextId: number = 0; + public id: number; + public price: number; + + constructor(price: number) { + this.id = PurchaseItem.NextId++; + this.price = price; + } +} + +@Component +struct BasketViewer { + @Link @Watch('onBasketUpdated') shopBasket: PurchaseItem[]; + @State totalPurchase: number = 0; + + updateTotal(): number { + let total = this.shopBasket.reduce((sum, i) => sum + i.price, 0); + // 超过100欧元可享受折扣 + if (total >= 100) { + total = 0.9 * total; + } + return total; + } + // @Watch 回调 + onBasketUpdated(propName: string): void { + this.totalPurchase = this.updateTotal(); + } + + build() { + Column() { + ForEach(this.shopBasket, + (item) => { + Text(`Price: ${item.price.toFixed(2)} €`) + }, + item => item.id.toString() + ) + Text(`Total: ${this.totalPurchase.toFixed(2)} €`) + } + } +} + +@Entry +@Component +struct BasketModifier { + @State shopBasket: PurchaseItem[] = []; + + build() { + Column() { + Button('Add to basket') + .onClick(() => { + this.shopBasket.push(new PurchaseItem(Math.round(100 * Math.random()))) + }) + BasketViewer({ shopBasket: $shopBasket }) + } + } +} +``` + +处理步骤如下: + +1. BasketModifier组件的Button.onClick向BasketModifier shopBasket中添加条目; + +2. \@Link装饰的BasketViewer shopBasket值发生变化; + +3. 状态管理框架调用\@Watch函数BasketViewer onBasketUpdated 更新BaketViewer TotalPurchase的值; + +4. \@Link shopBasket的改变,新增了数组项,ForEach组件会执行item Builder,渲染构建新的Item项;\@State totalPurchase改变,对应的Text组件也重新渲染;重新渲染是异步发生的。 + + +### \@Watch和自定义组件更新 + +以下示例展示组件更新和\@Watch的处理步骤。count在两个组件中均由\@State装饰。 + + +```ts +@Component +struct TotalView { + @Prop @Watch('onCountUpdated') count: number; + @State total: number = 0; + // @Watch cb + onCountUpdated(propName: string): void { + this.total += this.count; + } + + build() { + Text(`Total: ${this.total}`) + } +} + +@Entry +@Component +struct CountModifier { + @State count: number = 0; + + build() { + Column() { + Button('add to basket') + .onClick(() => { + this.count++ + }) + TotalView({ count: this.count }) + } + } +} +``` + +处理步骤: + +1. CountModifier自定义组件的Button.onClick点击事件自增count。 + +2. 由于\@State count变量更改,子组件TotalView中的\@Prop被更新,其\@Watch('onCountUpdated')方法被调用,更新了子组件TotalView 中的total变量。 + +3. 子组件TotalView中的Text重新渲染。 diff --git a/zh-cn/application-dev/quick-start/figures/CoreSpec_figures_state-mgmt-overview.png b/zh-cn/application-dev/quick-start/figures/CoreSpec_figures_state-mgmt-overview.png deleted file mode 100644 index 37ae5324808a0ab50f210907ca65a09e4456a371..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/quick-start/figures/CoreSpec_figures_state-mgmt-overview.png and /dev/null differ diff --git a/zh-cn/application-dev/quick-start/figures/Video_2023-03-06_152548.gif b/zh-cn/application-dev/quick-start/figures/Video_2023-03-06_152548.gif new file mode 100644 index 0000000000000000000000000000000000000000..c5cd5839d7eebfea2b10d06f197e7caddc30f61c Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/Video_2023-03-06_152548.gif differ diff --git a/zh-cn/application-dev/quick-start/figures/Video_2023-03-17_120758.gif b/zh-cn/application-dev/quick-start/figures/Video_2023-03-17_120758.gif new file mode 100644 index 0000000000000000000000000000000000000000..500d179cd0a05e73a65047711a03c25fac596ba2 Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/Video_2023-03-17_120758.gif differ diff --git a/zh-cn/application-dev/quick-start/figures/Video_2023-03-17_144605.gif b/zh-cn/application-dev/quick-start/figures/Video_2023-03-17_144605.gif new file mode 100644 index 0000000000000000000000000000000000000000..d48d7a7d7e0370acaf85fdc9164b526c69397a5d Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/Video_2023-03-17_144605.gif differ diff --git a/zh-cn/application-dev/quick-start/figures/Video_2023-03-17_144824.gif b/zh-cn/application-dev/quick-start/figures/Video_2023-03-17_144824.gif new file mode 100644 index 0000000000000000000000000000000000000000..f4437b3e2c92eaaa6ed7cd6daf508939b8d8ec99 Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/Video_2023-03-17_144824.gif differ diff --git a/zh-cn/application-dev/quick-start/figures/arkts-basic-grammar.png b/zh-cn/application-dev/quick-start/figures/arkts-basic-grammar.png new file mode 100644 index 0000000000000000000000000000000000000000..3f1c355759a60d5d2bca9166776ff3000b947796 Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/arkts-basic-grammar.png differ diff --git a/zh-cn/application-dev/quick-start/figures/arkts-get-started.png b/zh-cn/application-dev/quick-start/figures/arkts-get-started.png deleted file mode 100644 index 21de3f9615767dfb3e50fc5a0e1707eb87dfb73d..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/quick-start/figures/arkts-get-started.png and /dev/null differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001206450834.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001206450834.png deleted file mode 100644 index 35a5db40879212c9d90e5a02bba02e49e1158c8f..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001206450834.png and /dev/null differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001251090821.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001251090821.png deleted file mode 100644 index 69aec480939a34e310d8fbeead6a8e33644bb11b..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001251090821.png and /dev/null differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001501936014.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001501936014.png new file mode 100644 index 0000000000000000000000000000000000000000..7f6f1aa654cee34259ae3699a4eb82a5fa4bedf6 Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001501936014.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001501938718.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001501938718.png new file mode 100644 index 0000000000000000000000000000000000000000..2b133e6c4b8fa5ddd176dde3566ac2fa1aae635f Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001501938718.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001502091796.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001502091796.png new file mode 100644 index 0000000000000000000000000000000000000000..1f66ecd249d5ec2570288f0a4c2d44848d3fae44 Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001502091796.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001502092556.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001502092556.png new file mode 100644 index 0000000000000000000000000000000000000000..e7b56752fed8e17502a7685d172cf796f213cbb7 Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001502092556.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001502094666.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001502094666.png new file mode 100644 index 0000000000000000000000000000000000000000..c0503a81e37bb673026af6da01c06853531c11bd Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001502094666.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001502255262.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001502255262.png new file mode 100644 index 0000000000000000000000000000000000000000..c4715b0d8616e163f5bed207cf5732ba360f307f Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001502255262.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001502372786.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001502372786.png new file mode 100644 index 0000000000000000000000000000000000000000..b67f19d236bc4684e8eb1802b249e4096398b9ba Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001502372786.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001502704640.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001502704640.png new file mode 100644 index 0000000000000000000000000000000000000000..3bc469d94558ec8c6637d01423e33b053eeaa557 Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001502704640.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001552614217.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001552614217.png new file mode 100644 index 0000000000000000000000000000000000000000..e011bf227710e2796bbc6cc075066e608c0d83f5 Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001552614217.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001552855957.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001552855957.png new file mode 100644 index 0000000000000000000000000000000000000000..0d81f1ac85bf11bc8d593e677cb284309e39ae8b Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001552855957.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001552972029.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001552972029.png new file mode 100644 index 0000000000000000000000000000000000000000..38c0cfa6f39685d47eb47da8b6a522bf68c2a602 Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001552972029.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001552978157.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001552978157.png new file mode 100644 index 0000000000000000000000000000000000000000..13c73e874103d7bc798154d8bdcfde1a8b298e5c Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001552978157.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001553348833.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001553348833.png new file mode 100644 index 0000000000000000000000000000000000000000..3f1eb07e78b2cb1234c50fb9eda495dd3f5f2001 Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001553348833.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001562352677.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001562352677.png new file mode 100644 index 0000000000000000000000000000000000000000..64e710ea9ea0fe8e484d262c9260600b01ffd00a Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001562352677.png differ diff --git a/zh-cn/application-dev/reference/apis/js-apis-media.md b/zh-cn/application-dev/reference/apis/js-apis-media.md index 99703db51cdf6183a2d20767901dd3f5190877c9..ab85032419529f0e7c737cc2c0b5e7a13b9c7c51 100644 --- a/zh-cn/application-dev/reference/apis/js-apis-media.md +++ b/zh-cn/application-dev/reference/apis/js-apis-media.md @@ -350,7 +350,7 @@ Codec MIME类型枚举。 播放管理类,用于管理和播放媒体资源。在调用AVPlayer的方法前,需要先通过[createAVPlayer()](#mediacreateavplayer9)构建一个AVPlayer实例。 -Audio/Video播放demo可参考:[AVPlayer开发指导](../../media/avplayer-playback.md)。 +Audio/Video播放demo可参考:[音频播放开发指导](../../media/using-avplayer-for-playback.md)、[视频播放开发指导](../../media/video-playback.md)。 ### 属性 @@ -1602,7 +1602,7 @@ avPlayer.off('audioInterrupt') ## AVPlayerState9+ -[AVPlayer](#avplayer9)的状态机,可通过state属性主动获取当前状态,也可通过监听[stateChange](#stateChange_on)事件上报当前状态,状态机之间的切换规则,可参考[AVPlayer播放器开发指导](../../media/avplayer-playback.md)。 +[AVPlayer](#avplayer9)的状态机,可通过state属性主动获取当前状态,也可通过监听[stateChange](#stateChange_on)事件上报当前状态,状态机之间的切换规则,可参考[音频播放开发指导](../../media/using-avplayer-for-playback.md)。 **系统能力:** SystemCapability.Multimedia.Media.AVPlayer @@ -1709,7 +1709,11 @@ audioPlayer.getTrackDescription((error, arrList) => { 音视频录制管理类,用于音视频媒体录制。在调用AVRecorder的方法前,需要先通过createAVRecorder()构建一个AVRecorder实例。 -音视频录制demo可参考:[音视频录制开发指导](../../media/avrecorder.md) +音视频录制demo可参考:[音频录制开发指导](../../media/using-avrecorder-for-recording.md)、[视频录制开发指导](../../media/video-recording.md)。 + +> **说明:** +> +> 使用相机进行视频录制时,需要与相机模块配合,相机模块接口开放状态以及使用详情见[相机管理](js-apis-camera.md)。 ### 属性 @@ -2591,8 +2595,6 @@ avRecorder.off('error'); 视频录制管理类,用于录制视频媒体。在调用VideoRecorder的方法前,需要先通过[createVideoRecorder()](#mediacreatevideorecorder9)构建一个[VideoRecorder](#videorecorder9)实例。 -视频录制demo可参考:[视频录制开发指导](../../media/video-recorder.md) - ### 属性 **系统能力:** SystemCapability.Multimedia.Media.VideoRecorder @@ -3918,8 +3920,6 @@ audioPlayer.setVolume(3); //设置volume为无效值,触发'error'事件 视频播放管理类,用于管理和播放视频媒体。在调用VideoPlayer的方法前,需要先通过[createVideoPlayer()](#createvideoplayer)构建一个VideoPlayer实例。 -视频播放demo可参考:[视频播放开发指导](../../media/video-playback.md) - ### 属性 **系统能力:** SystemCapability.Multimedia.Media.VideoPlayer @@ -4738,8 +4738,6 @@ videoPlayer.url = 'fd://error'; //设置错误的播放地址,触发'error' 音频录制管理类,用于录制音频媒体。在调用AudioRecorder的方法前,需要先通过[createAudioRecorder()](#mediacreateaudiorecorder) 构建一个AudioRecorder实例。 -音频录制demo可参考:[音频录制开发指导](../../media/audio-recorder.md) - ### prepare prepare(config: AudioRecorderConfig): void diff --git a/zh-cn/application-dev/reference/arkui-ts/Readme-CN.md b/zh-cn/application-dev/reference/arkui-ts/Readme-CN.md index 81aa95dac6af8492a5c2a0e6375b2beb384873e1..62c15d1718469b8b857c585a4dd1b0b9f4bc3385 100644 --- a/zh-cn/application-dev/reference/arkui-ts/Readme-CN.md +++ b/zh-cn/application-dev/reference/arkui-ts/Readme-CN.md @@ -167,6 +167,7 @@ - [时间选择弹窗](ts-methods-timepicker-dialog.md) - [文本选择弹窗](ts-methods-textpicker-dialog.md) - [菜单](ts-methods-menu.md) +- [自定义组件的生命周期](ts-custom-component-lifecycle.md) - [应用级变量的状态管理](ts-state-management.md) - [像素单位](ts-pixel-units.md) - [枚举说明](ts-appendix-enums.md) diff --git a/zh-cn/application-dev/reference/arkui-ts/ts-custom-component-lifecycle.md b/zh-cn/application-dev/reference/arkui-ts/ts-custom-component-lifecycle.md new file mode 100644 index 0000000000000000000000000000000000000000..3f1da9b0dcd14ea60b7d1a91ea9171d0cde0e647 --- /dev/null +++ b/zh-cn/application-dev/reference/arkui-ts/ts-custom-component-lifecycle.md @@ -0,0 +1,192 @@ +# 自定义组件的生命周期 + + +自定义组件的生命周期回调函数用于通知用户该自定义组件的生命周期,这些回调函数是私有的,在运行时由开发框架在特定的时间进行调用,不能从应用程序中手动调用这些回调函数。 + + +>**说明:** +> +>允许在生命周期函数中使用Promise和异步回调函数,比如网络资源获取,定时器设置等; + + +## aboutToAppear + +aboutToAppear?(): void + +aboutToAppear函数在创建自定义组件的新实例后,在执行其build()函数之前执行。允许在aboutToAppear函数中改变状态变量,更改将在后续执行build()函数中生效。 + + +## aboutToDisappear + +aboutToDisappear?(): void + +aboutToDisappear函数在自定义组件析构销毁之前执行。不允许在aboutToDisappear函数中改变状态变量,特别是\@Link变量的修改可能会导致应用程序行为不稳定。 + + +## onPageShow + +onPageShow?(): void + +页面每次显示时触发一次,包括路由过程、应用进入前台等场景,仅\@Entry装饰的自定义组件生效。 + + +## onPageHide + +onPageHide?(): void + +页面每次隐藏时触发一次,包括路由过程、应用进入前后台等场景,仅\@Entry装饰的自定义组件生效。 + + +## onBackPress + +onBackPress?(): void + +当用户点击返回按钮时触发,仅\@Entry装饰的自定义组件生效。返回true表示页面自己处理返回逻辑,不进行页面路由,返回false表示使用默认的路由返回逻辑。不设置返回值按照false处理。 + + +```ts +// xxx.ets +@Entry +@Component +struct IndexComponent { + @State textColor: Color = Color.Black; + + onPageShow() { + this.textColor = Color.Blue; + console.info('IndexComponent onPageShow'); + } + + onPageHide() { + this.textColor = Color.Transparent; + console.info('IndexComponent onPageHide'); + } + + onBackPress() { + this.textColor = Color.Red; + console.info('IndexComponent onBackPress'); + } + + build() { + Column() { + Text('Hello World') + .fontColor(this.textColor) + .fontSize(30) + .margin(30) + }.width('100%') + } +} +``` + +![zh-cn_image_0000001563060749](figures/zh-cn_image_0000001563060749.png) + + +## onLayout9+ + +onLayout?(children: Array<LayoutChild>, constraint: ConstraintSizeOptions): void + +框架会在自定义组件布局时,将该自定义组件的子节点信息和自身的尺寸范围通过onLayout传递给该自定义组件。不允许在onLayout函数中改变状态变量。 + +**参数:** + +| 参数名 | 类型 | 说明 | +| ---------- | ---------------------------------------- | ---------------- | +| children | Array<[LayoutChild](#layoutchild9)> | 子组件布局信息。 | +| constraint | [ConstraintSizeOptions](ts-types.md#constraintsizeoptions) | 父组件constraint信息。 | + + +## onMeasure9+ + +onMeasure?(children: Array<LayoutChild>, constraint: ConstraintSizeOptions): void + +框架会在自定义组件确定尺寸时,将该自定义组件的子节点信息和自身的尺寸范围通过onMeasure传递给该自定义组件。不允许在onMeasure函数中改变状态变量。 + +**参数:** + +| 参数名 | 类型 | 说明 | +| ---------- | ---------------------------------------- | ---------------- | +| children | Array<[LayoutChild](#layoutchild9)> | 子组件布局信息。 | +| constraint | [ConstraintSizeOptions](ts-types.md#constraintsizeoptions) | 父组件constraint信息。 | + + +## LayoutChild9+ + +子组件布局信息。 + +| 参数 | 参数类型 | 描述 | +| ---------- | ---------------------------------------- | ------------------- | +| name | string | 子组件名称。 | +| id | string | 子组件id。 | +| constraint | [ConstraintSizeOptions](ts-types.md#constraintsizeoptions) | 子组件约束尺寸。 | +| borderInfo | [LayoutBorderInfo](#layoutborderinfo9) | 子组件border信息。 | +| position | [Position](ts-types.md#position) | 子组件位置坐标。 | +| measure | (childConstraint:) => void | 调用此方法对子组件的尺寸范围进行限制。 | +| layout | (LayoutInfo: [LayoutInfo](#layoutinfo9)) => void | 调用此方法对子组件的位置信息进行限制。 | + + +## LayoutBorderInfo9+ + +子组件border信息。 + +| 参数 | 参数类型 | 描述 | +| ----------- | ------------------------------------ | ----------------------- | +| borderWidth | [EdgeWidths](ts-types.md#edgewidths) | 边框宽度类型,用于描述组件边框不同方向的宽度。 | +| margin | [Margin](ts-types.md#margin) | 外边距类型,用于描述组件不同方向的外边距。 | +| padding | [Padding](ts-types.md#padding) | 内边距类型,用于描述组件不同方向的内边距。 | + + +## LayoutInfo9+ + +子组件layout信息。 + +| 参数 | 参数类型 | 描述 | +| ---------- | ---------------------------------------- | -------- | +| position | [Position](ts-types.md#position) | 子组件位置坐标。 | +| constraint | [ConstraintSizeOptions](ts-types.md#constraintsizeoptions) | 子组件约束尺寸。 | + + +```ts +// xxx.ets +@Entry +@Component +struct Index { + build() { + Column() { + CustomLayout() { + ForEach([1, 2, 3], (index) => { + Text('Sub' + index) + .fontSize(30) + .borderWidth(2) + }) + } + } + } +} + + +@Component +struct CustomLayout { + @BuilderParam builder: () => {}; + + onLayout(children: Array, constraint: ConstraintSizeOptions) { + let pos = 0; + children.forEach((child) => { + child.layout({ position: { x: pos, y: pos }, constraint: constraint }) + pos += 100; + }) + } + + onMeasure(children: Array, constraint: ConstraintSizeOptions) { + let size = 100; + children.forEach((child) => { + child.measure({ minHeight: size, minWidth: size, maxWidth: size, maxHeight: size }) + size += 50; + }) + } + + build() { + this.builder() + } +} +``` + +![zh-cn_image_0000001511900496](figures/zh-cn_image_0000001511900496.png) diff --git a/zh-cn/application-dev/reference/arkui-ts/ts-state-management.md b/zh-cn/application-dev/reference/arkui-ts/ts-state-management.md index d83c8cffbdd9cc1f05d62075d0a0b10be4481dcd..9e777fcbe54b3fb0f98a1952f8d2202939effa03 100644 --- a/zh-cn/application-dev/reference/arkui-ts/ts-state-management.md +++ b/zh-cn/application-dev/reference/arkui-ts/ts-state-management.md @@ -1,238 +1,294 @@ # 应用级变量的状态管理 -状态管理模块提供了应用程序的数据存储能力、持久化数据管理能力、Ability数据存储能力和应用程序需要的环境状态。 -> **说明:** +状态管理模块提供了应用程序的数据存储能力、持久化数据管理能力、UIAbility数据存储能力和应用程序需要的环境状态。 + + +>**说明:** > -> 本模块首批接口从API version 7开始支持,后续版本的新增接口,采用上角标单独标记接口的起始版本。 +>本模块首批接口从API version 7开始支持,后续版本的新增接口,采用上角标单独标记接口的起始版本。 + + +本文中T和S的含义如下: + + +| 类型 | 描述 | +| ---- | -------------------------------------- | +| T | Class,number,boolean,string和这些类型的数组形式。 | +| S | number,boolean,string。 | + ## AppStorage + ### Link -Link(propName: string): any +static Link(propName: string): any + +与AppStorage中对应的propName建立双向数据绑定。如果给定的propName在AppStorage中存在,返回与AppStorage中propName对应属性的双向绑定数据。 -与localStorage双向数据绑定。 +双向绑定数据的修改会同步回AppStorage中,AppStorage会将变化同步到所有绑定该propName的数据和自定义组件中。 + +如果AppStorage中不存在propName,则返回undefined。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| -------- | ------ | ---- | ----------- | -| propName | string | 是 | 要双向绑定的属性名称。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| -------- | ------ | ---- | ---------------- | +| propName | string | 是 | AppStorage中的属性名。 | **返回值:** -| 类型 | 描述 | -| ----- | ---------------------------------------- | -| @Link | 在具有给定键的数据,则返回到此属性的双向数据绑定,该双向绑定意味着变量或者组件对数据的更改将同步到AppStorage,通过AppStorage对数据的修改将同步到变量或者组件。 | +| 类型 | 描述 | +| ---- | ---------------------------------------- | +| any | 返回双向绑定的数据,如果AppStorage不存在对应的propName,在返回undefined。 | + ```ts -let simple = AppStorage.Link('simpleProp') +AppStorage.SetOrCreate('PropA', 47); +let linkToPropA1 = AppStorage.Link('PropA'); +let linkToPropA2 = AppStorage.Link('PropA'); // linkToPropA2.get() == 47 +linkToPropA1.set(48); // 双向同步: linkToPropA1.get() == linkToPropA2.get() == 48 ``` + ### SetAndLink -SetAndLink\(propName: string, defaultValue: T): SubscribedAbstractProperty\ +static SetAndLink<T>(propName: string, defaultValue: T): SubscribedAbstractProperty<T> -与Link接口类似,如果当前的key保存于AppStorage,则返回该key对应的value。如果该key未被创建,则创建一个对应default值的Link返回。 +与Link接口类似,如果给定的propName在AppStorage中存在,则返回该propName对应的属性的双向绑定数据。如果不存在,则使用defaultValue在AppStorage创建和初始化propName,返回其双向绑定数据。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| ------------ | ------ | ---- | ----------- | -| propName | string | 是 | 要进行创建的key值。 | -| defaultValue | T | 是 | 要进行设置的默认值。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| ------------ | ------ | ---- | ---------------------------------------- | +| propName | string | 是 | AppStorage中的属性名。 | +| defaultValue | T | 是 | 当propName在AppStorage中不存在,使用defaultValue在AppStorage中初始化对应的propName。 | **返回值:** -| 类型 | 描述 | -| ----- | ---------------------------------------- | -| @Link | 与Link接口类似,如果当前的key保存于AppStorage,返回该key值对应的value值。如果该key值未被创建,则创建一个对应的defaultValue的Link返回。 | +| 类型 | 描述 | +| ----------------------------------- | ---------------------------------------- | +| SubscribedAbstractProperty<T> | SubscribedAbstractProperty<T>的实例,和AppStorage中propName对应属性的双向绑定的数据。 | + ```ts -let simple = AppStorage.SetAndLink('simpleProp', 121) +AppStorage.SetOrCreate('PropA', 47); +let link1: SubscribedAbstractProperty = AppStorage.SetAndLink('PropB', 49); // Create PropB 49 +let link2: SubscribedAbstractProperty = AppStorage.SetAndLink('PropA', 50); // PropA exists, remains 47 ``` + ### Prop -Prop(propName: string): any +static Prop(propName: string): any + +与AppStorage中对应的propName建立单向属性绑定。如果给定的propName在AppStorage中存在,则返回与AppStorage中propName对应属性的单向绑定数据。如果AppStorage中不存在propName,则返回undefined。单向绑定数据的修改不会被同步回AppStorage中。 -单向属性绑定的一种。更新组件的状态。。 +>**说明:** +> Prop仅支持简单类型。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| -------- | ------ | ---- | ----------- | -| propName | string | 是 | 要进行创建的key值。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| -------- | ------ | ---- | ---------------- | +| propName | string | 是 | AppStorage中的属性名。 | **返回值:** -| 类型 | 描述 | -| ----- | ---------------------------------------- | -| @Prop | 如果存在具有给定键的属性,则返回此属性的单向数据绑定。该单向绑定意味着只能通过AppStorage将属性的更改同步到变量或者组件。该方法返回的变量为不可变变量,适用于可变和不可变的状态属性,如果具有此键的属性不存在则返回undefined。 | +| 类型 | 描述 | +| ---- | ---------------------------------------- | +| any | 返回单向绑定的数据,如果AppStorage不存在对应的propName,在返回undefined。 | + ```ts -let simple = AppStorage.Prop('simpleProp') +AppStorage.SetOrCreate('PropA', 47); +let prop1 = AppStorage.Prop('PropA'); +let prop2 = AppStorage.Prop('PropA'); +prop1.set(1); // one-way sync: prop1.get()=1; but prop2.get() == 47 ``` + ### SetAndProp -SetAndProp\(propName: string, defaultValue: S): SubscribedAbstractProperty\ +static SetAndProp<S>(propName: string, defaultValue: S): SubscribedAbstractProperty<S> -与Prop接口类似,如果当前的key保存于AppStorage,则返回该key对应的value。如果该key未被创建,则创建一个对应default值的Prop返回。 +与Prop接口类似。如果给定的propName在AppStorage存在,则返回该propName对应的属性的单向绑定数据。如果不存在,则使用defaultValue在AppStorage创建和初始化propName对应的属性,返回其单向绑定数据。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| ------------ | ------ | ---- | --------------- | -| propName | string | 是 | 要保存的的键值对中的key值。 | -| defaultValue | S | 是 | 创建的默认值。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| ------------ | ------ | ---- | ---------------------------------------- | +| propName | string | 是 | AppStorage中的属性名。 | +| defaultValue | S | 是 | 当propName在AppStorage中不存在时,使用default在AppStorage中初始化对应的propName。 | **返回值:** -| 类型 | 描述 | -| ----- | ---------------------------------------- | -| @Prop | 如果当前的key保存与AppStorage,返回该key值对应的value值。如果该key值未被创建,则创建一个对应的defaultValue的Prop返回。 | +| 类型 | 描述 | +| ----------------------------------- | --------------------------------------- | +| SubscribedAbstractProperty<S> | SubscribedAbstractProperty<S>的实例。 | + ```ts -let simple = AppStorage.SetAndProp('simpleProp', 121) +AppStorage.SetOrCreate('PropA', 47); +let prop: SubscribedAbstractProperty = AppStorage.SetAndProp('PropB', 49); // PropA -> 47, PropB -> 49 ``` + ### Has -Has(propName: string): boolean +static Has(propName: string): boolean -判断对应键值的属性是否存在。 +判断propName对应的属性是否在AppStorage中存在。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| -------- | ------ | ---- | ------- | -| propName | string | 是 | 属性的属性值。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| -------- | ------ | ---- | ---------------- | +| propName | string | 是 | AppStorage中的属性名。 | **返回值:** -| 类型 | 描述 | -| ------- | ------------- | -| boolean | 返回属性的属性值是否存在。 | +| 类型 | 描述 | +| ------- | ---------------------------------------- | +| boolean | 如果propName对应的属性在AppStorage中存在,则返回true。不存在则返回false。 | + ```ts -let simple = AppStorage.Has('simpleProp') +AppStorage.Has('simpleProp'); ``` + ### Get -Get\(propName: string): T | undefined +static Get<T>(propName: string): T | undefined -通过此接口获取对应key值的value。 +获取propName在AppStorage中对应的属性。如果不存在返回undefined。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| -------- | ------ | ---- | ----------- | -| propName | string | 是 | 要获取对应的key值。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| -------- | ------ | ---- | ---------------- | +| propName | string | 是 | AppStorage中的属性名。 | **返回值:** -| 类型 | 描述 | -| ----------------- | ------------- | -| T或undefined | 属性存在返回属性值,属性不存在返回undefined。 | +| 类型 | 描述 | +| ------------------------ | ---------------------------------------- | +| T \| undefined | AppStorage中propName对应的属性,如果不存在返回undefined。 | + ```ts -let simple = AppStorage.Get('simpleProp') +AppStorage.SetOrCreate('PropA', 47); +let value: number = AppStorage.Get('PropA'); // 47 ``` + ### Set -Set\(propName: string, newValue: T): boolean +static Set<T>(propName: string, newValue: T): boolean -对已保存的key值,替换其value值。 +在AppStorage中设置propName对应属性的值。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| -------- | ------ | ---- | ----------- | -| propName | string | 是 | 要设置的key值。 | -| newValue | T | 是 | 要设置的value值。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| -------- | ------ | ---- | ---------------------- | +| propName | string | 是 | AppStorage中的属性名。 | +| newValue | T | 是 | 属性值,不能为undefined或null。 | **返回值:** -| 类型 | 描述 | -| ------- | ----------------------------------- | -| boolean | 如果存在key值,设置value值并返回true,否则返回false。 | +| 类型 | 描述 | +| ------- | ---------------------------------------- | +| boolean | 如果AppStorage不存在propName对应的属性,或者设置的newValue是undefined或者null,返回false。设置成功则返回true。 | + ```ts -let simple = AppStorage.Set('simpleProp', 121) +AppStorage.SetOrCreate('PropA', 48); +let res: boolean = AppStorage.Set('PropA', 47) // true +let res1: boolean = AppStorage.Set('PropB', 47) // false ``` + ### SetOrCreate -SetOrCreate\(propName: string, newValue: T): void +static SetOrCreate<T>(propName: string, newValue: T): void -创建或更新setOrCreate内部的值。 +propName如果已经在AppStorage中存在,则设置propName对应是属性的值为newValue。如果不存在,则创建propName属性,值为newValue。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| -------- | ------ | ---- | --------------- | -| propName | string | 是 | 要更新或者创建的key值。 | -| newValue | T | 是 | 要更新或者创建的value值。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| -------- | ------ | ---- | ---------------------- | +| propName | string | 是 | AppStorage中的属性名。 | +| newValue | T | 是 | 属性值,不能为undefined或null。 | -**返回值:** - -| 类型 | 描述 | -| ------- | ---------------------------------------- | -| boolean | 如果已存在与给定键名字相同的属性,更新其值且返回true。如果不存在具有给定名称的属性,在AppStorage中创建具有给定默认值的新属性,默认值必须是T类型。不允许undefined 或 null 返回true。 | ```ts -let simple = AppStorage.SetOrCreate('simpleProp', 121) +AppStorage.SetOrCreate('simpleProp', 121); ``` + ### Delete -Delete(propName: string): boolean +static Delete(propName: string): boolean -删除key指定的键值对。 +在AppStorage中删除propName对应的属性。 + +在AppStorage中删除该属性的前提是必须保证该属性没有订阅者。如果有订阅者,则返回false。删除成功返回true。 + +属性的订阅者为Link、Prop等接口绑定的propName,以及\@StorageLink('propName')和\@StorageProp('propName')。这就意味着如果自定义组件中使用\@StorageLink('propName')和\@StorageProp('propName')或者SubscribedAbstractProperty实例依旧对propName有同步关系,则该属性不能从AppStorage中删除。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| -------- | ------ | ---- | ------------ | -| propName | string | 是 | 要删除的属性的key值。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| -------- | ------ | ---- | ---------------- | +| propName | string | 是 | AppStorage中的属性名。 | **返回值:** | 类型 | 描述 | | ------- | ---------------------------------------- | -| boolean | 删除key指定的键值对,如果存在且删除成功返回true,不存在或删除失败返回false。 | +| boolean | 如果AppStorage中有对应的属性,且该属性已经没有订阅者,则删除成功,返回true。如果属性不存在,或者该属性还存在订阅者,则返回false。 | + ```ts -let simple = AppStorage.Delete('simpleProp') +AppStorage.SetOrCreate('PropA', 47); +AppStorage.Link('PropA'); +let res: boolean = AppStorage.Delete('PropA'); // false, PropA still has a subscriber + +AppStorage.SetOrCreate('PropB', 48); +let res1: boolean = AppStorage.Delete('PropB'); // true, PropB is deleted from AppStorage successfully ``` -### keys -keys(): IterableIterator\ +### Keys + +static Keys(): IterableIterator<string> -查找所有键。 +返回AppStorage中所有的属性名。 **返回值:** -| 类型 | 描述 | -| -------------- | -------------- | -| array\ | 返回包含所有键的字符串数组。 | +| 类型 | 描述 | +| ------------------------------ | ------------------ | +| IterableIterator<string> | AppStorage中所有的属性名。 | + ```ts -let simple = AppStorage.Keys() +AppStorage.SetOrCreate('PropB', 48); +let keys: IterableIterator = AppStorage.Keys(); ``` + ### staticClear -staticClear(): boolean +static staticClear(): boolean 删除所有的属性。 -从API version 9开始废弃,推荐使用[Clear](#clear)。 +从API version 9开始废弃,推荐使用[Clear9+](#clear9)。 **返回值:** @@ -240,333 +296,389 @@ staticClear(): boolean | ------- | --------------------------------- | | boolean | 删除所有的属性,如果当前有状态变量依旧引用此属性,返回false。 | + ```ts -let simple = AppStorage.staticClear() +let simple = AppStorage.staticClear(); ``` + ### Clear9+ -Clear(): boolean +static Clear(): boolean -删除所有的属性。 +清除AppStorage的所有的属性。在AppStorage中清除所有属性的前提是,已经没有任何订阅者。如果有,则什么都不做返回false;删除成功返回true。 + +订阅者的含义和参考[Delete](#delete)。 **返回值:** -| 类型 | 描述 | -| ------- | --------------------------------- | -| boolean | 删除所有的属性,如果当前有状态变量依旧引用此属性,返回false。 | +| 类型 | 描述 | +| ------- | ---------------------------------------- | +| boolean | 如果AppStorage中的属性已经没有订阅者,则清除成功,返回true。否则返回false。 | -```ts -let simple = AppStorage.Clear() + +```typescript +AppStorage.SetOrCreate('PropA', 47); +let res: boolean = AppStorage.Clear(); // true, there are no subscribers ``` + ### IsMutable -IsMutable(propName: string): boolean +static IsMutable(propName: string): boolean -查询属性及状态。 +返回AppStorage中propName对应的属性是否是可变的。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| -------- | ------ | :--- | ------------ | -| propName | string | 是 | 要查询的属性的key值。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| -------- | ------ | ---- | ---------------- | +| propName | string | 是 | AppStorage中的属性名。 | **返回值:** -| 类型 | 描述 | -| ------- | ------------------ | -| boolean | 返回此属性是否存在并且是否可以改变。 | +| 类型 | 描述 | +| ------- | -------------------------------- | +| boolean | 返回AppStorage中propNam对应的属性是否是可变的。 | + ```ts -let simple = AppStorage.IsMutable('simpleProp') +AppStorage.SetOrCreate('PropA', 47); +let res: boolean = AppStorage.IsMutable('simpleProp'); ``` + ### Size -Size(): number +static Size(): number -存在的键值对的个数。 +返回LocalStorage中的属性数量。 **返回值:** -| 类型 | 描述 | -| ------ | --------- | -| number | 返回键值对的数量。 | +| 类型 | 描述 | +| ------ | ------------------- | +| number | 返回AppStorage中属性的数量。 | + ```ts -let simple = AppStorage.Size() +AppStorage.SetOrCreate('PropB', 48); +let res: number = AppStorage.Size(); // 1 ``` + ## LocalStorage9+ + ### constructor9+ constructor(initializingProperties?: Object) -创建一个新的LocalStorage对象,并进行初始化。 +创建一个新的LocalStorage实例。使用Object.keys(initializingProperties)返回的属性和其数值,初始化LocalStorage实例。 **参数:** | 参数名 | 类型 | 必填 | 参数描述 | | ---------------------- | ------ | ---- | ---------------------------------------- | -| initializingProperties | Object | 否 | object.keys(obj)返回的所有对象属性及其值都将添加到LocalStorage。 | +| initializingProperties | Object | 否 | 用initializingProperties包含的属性和数值初始化LocalStorage。initializingProperties不能为undefined。 | + ```ts -let storage = new LocalStorage() +let storage: LocalStorage = new LocalStorage({ 'PropA': 47 }); ``` + ### GetShared9+ static GetShared(): LocalStorage -获取当前的共享的LocalStorage对象。 +获取当前stage共享的LocalStorage实例。 -此接口仅可在Stage模型下使用。 +**模型约束:**此接口仅可在Stage模型下使用。 **返回值:** -| 类型 | 描述 | -| ----------------------------- | ----------------- | -| [LocalStorage](#localstorage) | 返回LocalStorage对象。 | +| 类型 | 描述 | +| ------------------------------ | ----------------- | +| [LocalStorage](#localstorage9) | 返回LocalStorage实例。 | + ```ts -let storage = LocalStorage.GetShared() +let storage: LocalStorage = LocalStorage.GetShared(); ``` + ### has9+ has(propName: string): boolean -判断属性值是否存在。 +判断propName对应的属性是否在LocalStorage中存在。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| -------- | ------ | ---- | ------- | -| propName | string | 是 | 属性的属性值。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| -------- | ------ | ---- | ------------------ | +| propName | string | 是 | LocalStorage中的属性名。 | **返回值:** -| 类型 | 描述 | -| ------- | ------------- | -| boolean | 返回属性的属性值是否存在。 | +| 类型 | 描述 | +| ------- | ---------------------------------------- | +| boolean | 如果propName对应的属性在AppStorage中存在,则返回true。不存在则返回false。 | + -```ts -let storage = new LocalStorage() -storage.has('storageSimpleProp') +``` +let storage: LocalStorage = new LocalStorage({ 'PropA': 47 }); +storage.has('PropA'); // true ``` + ### get9+ -get\(propName: string): T +get<T>(propName: string): T | undefined -获取对应key值的value。 +获取propName在LocalStorage中对应的属性。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| -------- | ------ | ---- | ----------- | -| propName | string | 是 | 要获取对应的key值。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| -------- | ------ | ---- | ------------------ | +| propName | string | 是 | LocalStorage中的属性名。 | **返回值:** -| 类型 | 描述 | -| -------------- | ---------------------------------------- | -| T \| undefined | 当keyvalue存在时,返回keyvalue值。不存在返回undefined。 | +| 类型 | 描述 | +| ------------------------ | ---------------------------------------- | +| T \| undefined | LocalStorage中propName对应的属性,如果不存在返回undefined。 | + ```ts -let storage = new LocalStorage() -let simpleValue = storage.get('storageSimpleProp') +let storage: LocalStorage = new LocalStorage({ 'PropA': 47 }); +let value: number = storage.get('PropA'); // 47 ``` + ### set9+ -set\(propName: string, newValue: T): boolean +set<T>(propName: string, newValue: T): boolean -存储对象值。 +在LocalStorage中设置propName对应属性的值。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| -------- | ------ | ---- | ----------- | -| propName | string | 是 | 要设置的key值。 | -| newValue | T | 是 | 要设置的value值。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| -------- | ------ | ---- | ----------------------- | +| propName | string | 是 | LocalStorage中的属性名。 | +| newValue | T | 是 | 属性值,不能为undefined或者null。 | **返回值:** -| 类型 | 描述 | -| ------- | ----------------------------------- | -| boolean | 如果存在key值,设置value值并返回true,否则返回false。 | +| 类型 | 描述 | +| ------- | ---------------------------------------- | +| boolean | 如果LocalStorage不存在propName对应的属性,或者设置的newValue是undefined或者null,返回false。设置成功返回true。 | + ```ts -let storage = new LocalStorage() -storage.set('storageSimpleProp', 121) +let storage: LocalStorage = new LocalStorage({ 'PropA': 47 }); +let res: boolean = storage.set('PropA', 47); // true +let res1: boolean = storage.set('PropB', 47); // false ``` + ### setOrCreate9+ -setOrCreate\(propName: string, newValue: T): boolean +setOrCreate<T>(propName: string, newValue?: T): boolean -创建或更新setOrCreate内部的值。 +propName如果已经在LocalStorage中存在,则设置propName对应是属性的值为newValue。如果不存在,则创建propName属性,初始化为newValue。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| -------- | ------ | :--- | -------------- | -| propName | string | 是 | 要更新或创建的key值。 | -| newValue | T | 是 | 要更新或创建的value值。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| -------- | ------ | ---- | ----------------------- | +| propName | string | 是 | LocalStorage中的属性名。 | +| newValue | T | 否 | 属性值,不能为undefined或者null。 | **返回值:** | 类型 | 描述 | | ------- | ---------------------------------------- | -| boolean | 如果已存在与给定键名字相同的属性,更新其值且返回true。如果不存在具有给定名称的属性,在LocalStorage中创建具有给定默认值的新属性,默认值必须是T类型,不允许undefined 或 null 。 | +| boolean | 如果设置的newValue是undefined或者null,返回false。
如果LocalStorage存在propName,则更新其值为newValue,返回true。
如果LocalStorage不存在propName,则创建propName,并初始化其值为newValue,返回true。 | + ```ts -let storage = new LocalStorage() -storage.setOrCreate('storageSimpleProp', 121) +let storage: LocalStorage = new LocalStorage({ 'PropA': 47 }); +let res: boolean =storage.setOrCreate('PropA', 121); // true +let res1: boolean =storage.setOrCreate('PropB', 111); // true +let res2: boolean =storage.setOrCreate('PropB', undefined); // false ``` + ### link9+ -link\(propName: string): T +link<T>(propName: string): SubscribedAbstractProperty<T> + +如果给定的propName在LocalStorage实例中存在,则返回与LocalStorage中propName对应属性的双向绑定数据。 + +双向绑定数据的修改会被同步回LocalStorage中,LocalStorage会将变化同步到所有绑定该propName的数据和Component中。 -与localStorage双向数据绑定。 +如果LocalStorage中不存在propName,则返回undefined。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| -------- | ------ | ---- | ----------- | -| propName | string | 是 | 要双向绑定的属性名称。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| -------- | ------ | ---- | ------------------ | +| propName | string | 是 | LocalStorage中的属性名。 | **返回值:** -| 类型 | 描述 | -| ---- | ---------------------------------------- | -| T | 如果存在具有给定键的属性,返回到此属性的双向绑定,该双向绑定意味着变量或者组件对数据的更改将同步到LocalStorage,然后通过LocalStorage实例同步到任何变量或组件。如果不存在给定键的属性,返回undefined。 | +| 类型 | 描述 | +| ----------------------------------- | ---------------------------------------- | +| SubscribedAbstractProperty<T> | SubscribedAbstractProperty<T>的实例,如果AppStorage不存在对应的propName,再返回undefined。 | + ```ts -let storage = new LocalStorage() -let localStorage = storage.link('storageSimpleProp') +let storage: LocalStorage = new LocalStorage({ 'PropA': 47 }); +let linkToPropA1: SubscribedAbstractProperty = storage.link('PropA'); +let linkToPropA2: SubscribedAbstractProperty = storage.link('PropA'); // linkToPropA2.get() == 47 +linkToPropA1.set(48); // 双向同步: linkToPropA1.get() == linkToPropA2.get() == 48 ``` + ### setAndLink9+ -setAndLink\(propName: string, defaultValue: T): T +setAndLink<T>(propName: string, defaultValue: T): SubscribedAbstractProperty<T> -与link接口类似,双向数据绑定localStorage。 +与Link接口类似,如果给定的propName在LocalStorage存在,则返回该propName对应的属性的双向绑定数据。如果不存在,则使用defaultValue在LocalStorage创建和初始化propName,返回其双向绑定数据。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| ------------ | ------ | ---- | ----------- | -| propName | string | 是 | 要进行创建的key值。 | -| defaultValue | T | 是 | 要进行设置的默认值。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| ------------ | ------ | ---- | ---------------------------------------- | +| propName | string | 是 | LocalStorage中的属性名。 | +| defaultValue | T | 是 | 当propName在LocalStorage中不存在,使用default在LocalStorage中初始化对应的propName。 | **返回值:** -| 类型 | 描述 | -| ----- | ---------------------------------------- | -| @Link | 与Link接口类似,如果当前的key保存于LocalStorage,返回该key值对应的value值。如果该key值未被创建,则创建一个对应的defaultValue的Link返回。 | +| 类型 | 描述 | +| ----------------------------------- | ---------------------------------------- | +| SubscribedAbstractProperty<T> | SubscribedAbstractProperty<T>的实例,如果AppStorage不存在对应的propName,再返回undefined。 | + ```ts -let storage = new LocalStorage() -let localStorage = storage.setAndLink('storageSimpleProp', 121) +let storage: LocalStorage = new LocalStorage({ 'PropA': 47 }); +let link1: SubscribedAbstractProperty = storage.setAndLink('PropB', 49); // Create PropB 49 +var link2: SubscribedAbstractProperty = storage.setAndLink('PropA', 50); // PropA exists, remains 47 ``` + ### prop9+ -prop\(propName: string): T +prop<T>(propName: string): SubscribedAbstractProperty<T> -单向属性绑定的一种。更新组件的状态。 +如果给定的propName在LocalStorage存在,则返回与LocalStorage中propName对应属性的单向绑定数据。如果LocalStorage中不存在propName,则返回undefined。单向绑定数据的修改不会被同步回LocalStorage中。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| -------- | ------ | ---- | ------------- | -| propName | string | 是 | 要单向数据绑定的key值。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| -------- | ------ | ---- | ------------------ | +| propName | string | 是 | LocalStorage中的属性名。 | **返回值:** -| 类型 | 描述 | -| ----- | ---------------------------------------- | -| @Prop | 如果存在具有给定键的属性,返回此属性的单向数据绑定。该单向绑定意味着只能通过LocalStorage将属性的更改同步到变量或组件。该方法返回的变量为不可变变量,适用于可变和不可变的状态变量。如果此键的属性不存在则返回undefined。 | +| 类型 | 描述 | +| ----------------------------------- | ---------------------------------------- | +| SubscribedAbstractProperty<T> | SubscribedAbstractProperty<T>的实例,如果AppStorage不存在对应的propName,在返回undefined。 | + ```ts -let storage = new LocalStorage() -let localStorage = storage.prop('storageSimpleProp') +let storage: LocalStorage = new LocalStorage({ 'PropA': 47 }); +let prop1: SubscribedAbstractProperty = storage.prop('PropA'); +let prop2: SubscribedAbstractProperty = storage.prop('PropA'); +prop1.set(1); // one-way sync: prop1.get()=1; but prop2.get() == 47 ``` + ### setAndProp9+ -setAndProp\(propName: string, defaultValue: T): T +setAndProp<T>(propName: string, defaultValue: T): SubscribedAbstractProperty<T> -与Prop接口类似,存在localStorage的单向数据绑定prop值。 +propName在LocalStorage存在,则返回该propName对应的属性的单向绑定数据。如果不存在,则使用defaultValue在LocalStorage创建和初始化propName对应的属性,返回其单向绑定数据。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| ------------ | ------ | ---- | -------------- | -| propName | string | 是 | 要保存的键值对中的key值。 | -| defaultValue | T | 是 | 创建的默认值。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| ------------ | ------ | ---- | ---------------------------------------- | +| propName | string | 是 | LocalStorage中的属性名。 | +| defaultValue | T | 是 | 当propName在AppStorage中不存在,使用default在AppStorage中初始化对应的propName。 | **返回值:** -| 类型 | 描述 | -| ----- | ---------------------------------------- | -| @Prop | 如果当前的key保存与LocalStorage,返回该key值对应的value值。如果该key值未被创建,则创建一个对应的defaultValue的Prop返回。 | +| 类型 | 描述 | +| ----------------------------------- | ---------------------------------------- | +| SubscribedAbstractProperty<T> | SubscribedAbstractProperty<T>的实例,和AppStorage中propName对应属性的单向绑定的数据。 | + ```ts -let storage = new LocalStorage() -let localStorage = storage.setAndProp('storageSimpleProp', 121) +let storage: LocalStorage = new LocalStorage({ 'PropA': 47 }); +let prop: SubscribedAbstractProperty = storage.setAndProp('PropB', 49); // PropA -> 47, PropB -> 49 ``` + ### delete9+ delete(propName: string): boolean -删除key指定的键值对。 +在LocalStorage中删除propName对应的属性。删除属性的前提是该属性已经没有订阅者,如果有则返回false。删除成功则返回true。 + +属性的订阅者是link,prop接口绑定的propName,以及\@LocalStorageLink('propName')和\@LocalStorageProp('propName')。如果自定义组件Component中使用或者SubscribedAbstractProperty(link和prop接口的返回类型)依旧有同步关系,则该属性不能从LocalStorage中删除。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| -------- | ------ | :--- | ------------ | -| propName | string | 是 | 要删除的属性的key值。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| -------- | ------ | ---- | ------------------ | +| propName | string | 是 | LocalStorage中的属性名。 | **返回值:** | 类型 | 描述 | | ------- | ---------------------------------------- | -| boolean | 删除key指定的键值对。存在且删除成功,返回true。不存在、删除失败或有状态变量依旧引用propName,返回false。 | +| boolean | 如果LocalStorage中有对应的属性,且该属性已经没有订阅者,则删除成功返回true。如果属性不存在,或者该属性还存在订阅者,则返回false。 | + ```ts -let storage = new LocalStorage() -storage.delete('storageSimpleProp') +let storage: LocalStorage = new LocalStorage({ 'PropA': 47 }); +storage.link('PropA'); +let res: boolean = storage.delete('PropA'); // false, PropA still has a subscriber +let res1: boolean = storage.delete('PropB'); // false, PropB is not in storage +storage.setOrCreate('PropB', 48); +let res2: boolean = storage.delete('PropB'); // true, PropB is deleted from storage successfully ``` + ### keys9+ -keys(): IterableIterator\ +keys(): IterableIterator<string> -查找所有键。 +返回AppStorage中所有的属性名。 **返回值:** -| 类型 | 描述 | -| -------------- | ------------------- | -| array\ | 返回包含所有键不可序列化的字符串数组。 | +| 类型 | 描述 | +| ------------------------------ | -------------------- | +| IterableIterator<string> | LocalStorage中所有的属性名。 | + ```ts -let storage = new LocalStorage() -let simple = storage.keys() +let storage: LocalStorage = new LocalStorage({ 'PropA': 47 }); +let keys: IterableIterator = storage.keys(); ``` + ### size9+ size(): number -存在的键值对的个数。 +返回LocalStorage中的属性数量。 **返回值:** @@ -574,191 +686,272 @@ size(): number | ------ | --------- | | number | 返回键值对的数量。 | + ```ts -let storage = new LocalStorage() -let simple = storage.size() +let storage: LocalStorage = new LocalStorage({ 'PropA': 47 }); +let res: number = storage.size(); // 1 ``` -### Clear9+ + +### clear9+ clear(): boolean -删除所有的属性。 + +清除LocalStorage的所有的属性。在LocalStorage中清除所有属性的前提是已经没有任何订阅者。如果有则返回false;清除成功返回true。 + **返回值:** -| 类型 | 描述 | -| ------- | --------------------------------- | -| boolean | 删除所有的属性,如果当前有状态变量依旧引用此属性,返回false。 | + +| 类型 | 描述 | +| ------- | ---------------------------------------- | +| boolean | 如果LocalStorage中的属性已经没有任何订阅者,则清除成功,返回true。否则返回false。 | + + ```ts -let storage = new LocalStorage() -let simple = storage.clear() +let storage: LocalStorage = new LocalStorage({ 'PropA': 47 }); +let res: boolean = storage.clear(); // true, there are no subscribers ``` -## PersistentStorage -### constructor +## SubscribedAbstractProperty + + +### get9+ + +abstract get(): T -constructor(appStorage: AppStorage, storage: Storage) +读取从AppStorage/LocalStorage同步属性的数据。 + +**返回值:** + +| 类型 | 描述 | +| ---- | ------------------------------- | +| T | AppStorage/LocalStorage同步属性的数据。 | + + +```ts +AppStorage.SetOrCreate('PropA', 47); +let prop1 = AppStorage.Prop('PropA'); +prop1.get(); // prop1.get()=47 +``` + + +### set9+ + +abstract set(newValue: T): void + + +设置AppStorage/LocalStorage同步属性的数据。 -创建一个新的persistentstorage对象。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| ---------- | ---------- | ---- | ---------------- | -| appStorage | AppStorage | 是 | 保存所有属性及属性值的单例对象。 | -| storage | Storage | 是 | Storage实例对象。 | + +| 参数名 | 类型 | 必填 | 参数描述 | +| -------- | ---- | ---- | ------- | +| newValue | T | 是 | 要设置的数据。 | + + + +``` +AppStorage.SetOrCreate('PropA', 47); +let prop1 = AppStorage.Prop('PropA'); +prop1.set(1); // prop1.get()=1 +``` + +### aboutToBeDeleted10+ + +abstract aboutToBeDeleted(): void + +取消SubscribedAbstractProperty实例对AppStorage/LocalStorage单/双向同步关系。 + ```ts -let persistentstorage = new PersistentStorage(AppStorage,Storage) +AppStorage.SetOrCreate('PropA', 47); +let link = AppStorage.SetAndLink('PropB', 49); // PropA -> 47, PropB -> 49 +link.aboutToBeDeleted(); +link.set(50); // PropB -> 49, link.get() --> undefined ``` + +## PersistentStorage + + ### PersistProp -PersistProp(key:string,defaultValue:T): void +static PersistProp<T>(key: string, defaultValue: T): void + +将AppStorage中key对应的属性持久化到文件中。该接口的调用通常在访问AppStorage之前。 + +确定属性的类型和值的顺序如下: -关联命名的属性再AppStorage变为持久化数据。 +1. 如果PersistentStorage文件中存在key对应的属性,在AppStorage中创建对应的propName,并用在PersistentStorage中找到的key的属性初始化。 + +2. 如果PersistentStorage文件中没有查询到key对应的属性,则在AppStorage中查找key对应的属性。如果找到key对应的属性,则将该属性持久化。 + +3. 如果AppStorage也没查找到key对应的属性,则在AppStorage中创建key对应的属性。用defaultValue初始化其值,并将该属性持久化。 + +根据上述的初始化流程,如果AppStorage中有该属性,则会使用其值,覆盖掉PersistentStorage文件中的值。由于AppStorage是内存内数据,该行为会导致数据丧失持久化能力。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| ------------ | ------ | ---- | -------------- | -| key | string | 是 | 要关联的属性的key值。 | -| defaultValue | T | 是 | 要关联的属性的value值。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| ------------ | ------ | ---- | ---------------------------------------- | +| key | string | 是 | 属性名。 | +| defaultValue | T | 是 | 在PersistentStorage和AppStorage未查询到时,则使用默认值初始化初始化它。不允许为undefined和null。 | + + +**示例:** + + ```ts -PersistentStorage.PersistProp('highScore', '0') +PersistentStorage.PersistProp('highScore', '0'); ``` + ### DeleteProp -DeleteProp(key: string): void +static DeleteProp(key: string): void -取消双向数据绑定,该属性值将从持久存储中删除。 +PersistProp的逆向操作。将key对应的属性从PersistentStorage删除,后续AppStorage的操作,对PersistentStorage不会再有影响。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| ---- | ------ | ---- | ------------ | -| key | string | 是 | 要取消的属性的key值。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| ---- | ------ | ---- | ----------------------- | +| key | string | 是 | PersistentStorage中的属性名。 | + ```ts -PersistentStorage.DeleteProp('highScore') +PersistentStorage.DeleteProp('highScore'); ``` + ### PersistProps -PersistProps(properties: {key: string, defaultValue: any}[]): void +static PersistProps(properties: {key: string, defaultValue: any;}[]): void -关联多个命名的属性绑定。 +行为和PersistProp类似,不同在于可以一次性持久化多个数据,适合在应用启动的时候初始化。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | -| ---- | ---------------------------------- | ---- | --------- | -| key | {key: string, defaultValue: any}[] | 是 | 要关联的属性数组。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| ---------- | ---------------------------------------- | ---- | ---------------------------------------- | +| properties | {key: string, defaultValue: any}[] | 是 | 持久化数组,启动key为属性名,defaultValue为默认值。规则同PersistProp。 | + ```ts -PersistentStorage.PersistProps([{key: 'highScore', defaultValue: '0'},{key: 'wightScore',defaultValue: '1'}]) +PersistentStorage.PersistProps([{ key: 'highScore', defaultValue: '0' }, { key: 'wightScore', defaultValue: '1' }]); ``` + ### Keys -Keys(): Array\ +static Keys(): Array<string> -返回所有持久化属性的标记。 +返回所有持久化属性的key的数组。 **返回值:** -| 类型 | 描述 | -| -------------- | ------------- | -| Array\ | 返回所有持久化属性的标记。 | +| 类型 | 描述 | +| ------------------- | ----------------- | +| Array<string> | 返回所有持久化属性的key的数组。 | + ```ts -let simple = PersistentStorage.Keys() +let keys: Array = PersistentStorage.Keys(); ``` -> **说明:** -> -> - PersistProp接口使用时,需要保证输入对应的key应当在AppStorage存在。 -> -> - DeleteProp接口使用时,只能对本次启动已经link过的数据生效。 ## Environment -### constructor -创建一个environment对象。 +### EnvProp -```ts -let simple = new Environment() -``` +static EnvProp<S>(key: string, value: S): boolean -### EnvProp +将Environment的内置环境变量key存入AppStorage中。如果系统中未查询到Environment环境变量key的值,则使用默认值value,存入成功,返回true。如果AppStorage已经有对应的key,则返回false。 -EnvProp\(key: string, value: S): boolean +所以建议在程序启动的时候调用该接口。 -关联此系统项到AppStorage中,建议在app启动时使用此接口。如果该属性在AppStorage已存在,返回false。请勿使用AppStorage中的变量,在调用此方法关联环境变量。 +在没有调用EnvProp,就使用AppStorage读取环境变量是错误的。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | 参数描述 | -| ----- | ------ | ---- | ---------- | ------------------------- | -| key | string | 是 | 要关联的key值 | 要关联的key值,支持的范围详见内置环境变量说明。 | -| value | S | 是 | 要关联的value值 | 要关联的value值。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| ----- | ------ | ---- | --------------------------------------- | +| key | string | 是 | 环境变量名称,支持的范围详见[内置环境变量说明](#内置环境变量说明)。 | +| value | S | 是 | 查询不到环境变量key,则使用value作为默认值存入AppStorage中。 | **返回值:** -| 类型 | 描述 | -| ------- | ---------------------- | -| boolean | 返回该属性在AppStorage中是否存在。 | +| 类型 | 描述 | +| ------- | ---------------------------------------- | +| boolean | 如果key对应的属性在AppStorage中存在,则返回false。不存在则在AppStorage中创建key对应的属性,返回true。 | + +**示例:** -**内置环境变量说明:** + +```ts +Environment.EnvProp('accessibilityEnabled', 'default'); +``` + + +### 内置环境变量说明 | key | 类型 | 说明 | | -------------------- | --------------- | ---------------------------------------- | | accessibilityEnabled | string | 无障碍屏幕朗读是否启用。 | -| colorMode | ColorMode | 深浅色模式,可选值为:
- ColorMode.LIGHT:浅色模式;
- ColorMode.DARK:深色模式。 | +| colorMode | ColorMode | 深浅色模式,可选值为:
- ColorMode.LIGHT:浅色模式;
- ColorMode.DARK:深色模式。 | | fontScale | number | 字体大小比例。 | | fontWeightScale | number | 字重比例。 | -| layoutDirection | LayoutDirection | 布局方向类型,可选值为:
- LayoutDirection.LTR:从左到右;
- LayoutDirection.RTL:从右到左。 | +| layoutDirection | LayoutDirection | 布局方向类型,可选值为:
- LayoutDirection.LTR:从左到右;
- LayoutDirection.RTL:从右到左。 | | languageCode | string | 当前系统语言,小写字母,例如zh。 | -```ts -Environment.EnvProp('accessibilityEnabled', 'default') -``` ### EnvProps -EnvProps(props: {key: string, defaultValue: any}[]): void +static EnvProps(props: {key: string; defaultValue: any;}[]): void -关联此系统项数组到AppStorage中 +和EnvProp类似,不同点在于参数为数组,可以一次性初始化多个数据。建议在应用启动时调用,将系统环境变量批量存入AppStorage中。 **参数:** -| 参数名 | 类型 | 必填 | 参数描述 | 参数描述 | -| ---- | ---------------------------------- | ---- | --------- | --------- | -| key | {key: string, defaultValue: any}[] | 是 | 要关联的属性数组。 | 要关联的属性数组。 | +| 参数名 | 类型 | 必填 | 参数描述 | +| ----- | ---------------------------------------- | ---- | ------------------ | +| props | {key: string, defaultValue: any}[] | 是 | 系统环境变量和默认值的键值对的数组。 | + ```ts -Environment.EnvProps([{key: 'accessibilityEnabled', defaultValue: 'default'},{key: 'accessibilityUnEnabled', defaultValue: 'undefault'}]) +Environment.EnvProps([{ key: 'accessibilityEnabled', defaultValue: 'default' }, { + key: 'languageCode', + defaultValue: 'en' +}, { key: 'prop', defaultValue: 'hhhh' }]); ``` + ### Keys -Keys(): Array\ +static Keys(): Array<string> -返回关联的系统项。 +返回环境变量的属性key的数组。 **返回值:** -| 类型 | 描述 | -| -------------- | ----------- | -| Array\ | 返回关联的系统项数组。 | +| 类型 | 描述 | +| ------------------- | ----------- | +| Array<string> | 返回关联的系统项数组。 | + ```ts -let simple = Environment.Keys() -``` +Environment.EnvProps([{ key: 'accessibilityEnabled', defaultValue: 'default' }, { + key: 'languageCode', + defaultValue: 'en' +}, { key: 'prop', defaultValue: 'hhhh' }]); +let keys: Array = Environment.Keys(); // accessibilityEnabled, languageCode, prop +``` \ No newline at end of file diff --git a/zh-cn/application-dev/ui/Readme-CN.md b/zh-cn/application-dev/ui/Readme-CN.md index 8d0e6bba9e21fe19ef53e6fa16068f30cfb73f57..44c62182fdf3920df1899bd616e0d08e8069b247 100755 --- a/zh-cn/application-dev/ui/Readme-CN.md +++ b/zh-cn/application-dev/ui/Readme-CN.md @@ -1,34 +1,66 @@ # UI开发 -- [方舟开发框架(ArkUI)概述](arkui-overview.md) -- 基于ArkTS的声明式开发范式 - - [概述](ui-ts-overview.md) - - [声明式UI开发指导](ui-ts-developing-intro.md) - - 声明式UI开发实例 - - [创建简单视图](ui-ts-creating-simple-page.md) - - 构建完整实例 - - [构建食物数据模型](ui-ts-building-data-model.md) - - [构建食物列表List布局](ui-ts-building-category-list-layout.md) - - [构建食物分类Grid布局](ui-ts-building-category-grid-layout.md) - - [页面跳转与数据传递](ui-ts-page-redirection-data-transmission.md) - - 添加闪屏动画 - - [绘制图像](ui-ts-drawing-feature.md) - - [添加动画效果](ui-ts-animation-feature.md) - - [常用组件说明](ui-ts-components-intro.md) - - 常见布局开发指导 - - 自适应布局 - - [线性布局](ui-ts-layout-linear.md) - - [层叠布局](ui-ts-layout-stack.md) - - [弹性布局](ui-ts-layout-flex.md) - - [网格布局](ui-ts-layout-grid.md) - - 响应式布局 - - [栅格布局](ui-ts-layout-grid-container-new.md) - - [媒体查询](ui-ts-layout-mediaquery.md) - - [自定义组件的生命周期](ui-ts-custom-component-lifecycle-callbacks.md) - - [Web组件开发指导](ui-ts-components-web.md) - - [性能提升的推荐方法](ui-ts-performance-improvement-recommendation.md) -- 兼容JS的类Web开发范式 - - [概述](ui-js-overview.md) +- [方舟开发框架概述](arkui-overview.md) +- UI开发(ArkTS声明式开发范式) + - [UI开发(ArkTS声明式开发范式)概述](arkts-ui-development-overview.md) + - 开发布局 + - [布局概述](arkts-layout-development-overview.md) + - 构建布局 + - [线性布局](arkts-layout-development-linear.md) + - [层叠布局](arkts-layout-development-stack-layout.md) + - [弹性布局](arkts-layout-development-flex-layout.md) + - [相对布局](arkts-layout-development-relative-layout.md) + - [栅格布局](arkts-layout-development-grid-layout.md) + - [媒体查询](arkts-layout-development-media-query.md) + - [创建列表](arkts-layout-development-create-list.md) + - [创建网格](arkts-layout-development-create-grid.md) + - [创建轮播](arkts-layout-development-create-looping.md) + - [改善布局性能](arkts-layout-development-performance-boost.md) + - 添加组件 + - 添加常用组件 + - [按钮](arkts-common-components-button.md) + - [单选框](arkts-common-components-radio-button.md) + - [切换按钮](arkts-common-components-switch.md) + - [进度条](arkts-common-components-progress-indicator.md) + - [文本显示](arkts-common-components-text-display.md) + - [文本输入](arkts-common-components-text-input.md) + - [自定义弹窗](arkts-common-components-custom-dialog.md) + - [视频播放](arkts-common-components-video-player.md) + - [XComponent](arkts-common-components-xcomponent.md) + - 添加气泡和菜单 + - [气泡提示](arkts-popup-and-menu-components-popup.md) + - [菜单](arkts-popup-and-menu-components-menu.md) + - 设置页面路由和组件导航 + - [页面路由](arkts-routing.md) + - 组件导航 + - [Navigation](arkts-navigation-navigation.md) + - [Tabs](arkts-navigation-tabs.md) + - 显示图形 + - [显示图片](arkts-graphics-display.md) + - [绘制几何图形](arkts-geometric-shape-drawing.md) + - [使用画布绘制自定义图形](arkts-drawing-customization-on-canvas.md) + - 使用动画 + - [动画概述](arkts-animation-overview.md) + - 页面内的动画 + - [布局更新动画](arkts-layout-update-animation.md) + - [组件内转场动画](arkts-transition-animation-within-component.md) + - [弹簧曲线动画](arkts-spring-animation.md) + - 页面间的动画 + - [放大缩小视图](arkts-zoom-animation.md) + - [页面转场动画](arkts-page-transition-animation.md) + - 支持交互事件 + - [交互事件概述](arkts-event-overview.md) + - 使用通用事件 + - [触屏事件](arkts-common-events-touch-screen-event.md) + - [键鼠事件](arkts-common-events-device-input-event.md) + - [焦点事件(毕雪峰 00579046)](arkts-common-events-focus-event.md) + - 使用手势事件 + - [绑定手势方法](arkts-gesture-events-binding.md) + - [单一手势](arkts-gesture-events-single-gesture.md) + - [组合手势](arkts-gesture-events-combined-gestures.md) + - [性能提升的推荐方法](arkts-performance-improvement-recommendation.md) +- UI开发(兼容JS的类Web开发范式) + - [UI开发(兼容JS的类Web开发范式)概述](ui-js-overview.md) - 框架说明 - [文件组织](js-framework-file.md) - [js标签配置](js-framework-js-tag.md) diff --git a/zh-cn/application-dev/ui/arkts-animation-overview.md b/zh-cn/application-dev/ui/arkts-animation-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..a26ef6acfe2b0241d0c222e940bdacaa629a6016 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-animation-overview.md @@ -0,0 +1,27 @@ +# 动画概述 + + +动画的原理是在一个时间段内,多次改变UI外观,由于人眼会产生视觉暂留,所以最终看到的就是一个“连续”的动画。UI的一次改变称为一个动画帧,对应一次屏幕刷新,而决定动画流畅度的一个重要指标就是帧率FPS(Frame Per Second),即每秒的动画帧数,帧率越高则动画就会越流畅。 + + +ArkUI中,产生动画的方式是改变属性值且指定动画参数。动画参数包含了如动画时长、变化规律(即曲线)等参数。当属性值发生变化后,按照动画参数,从原来的状态过渡到新的状态,即形成一个动画。 + + +ArkUI提供的动画能力按照页面的分类方式,可分为页面内的动画和页面间的动画。如下图所示,页面内的动画指在一个页面内即可发生的动画,页面间的动画指两个页面跳转时才会发生的动画。 + + + **图1** 按照页面分类的动画   + +![zh-cn_image_0000001562700385](figures/zh-cn_image_0000001562700385.png) + + +如果按照基础能力分,可分为属性动画、显式动画、转场动画三部分。如下图所示。 + + + **图2** 按照基础能力分类的动画   + + +![zh-cn_image_0000001562820753](figures/zh-cn_image_0000001562820753.png) + + +本文按照页面的分类方式,从使用场景出发,提供各种动画的使用方法和注意事项,使开发者快速学习动画。 diff --git a/zh-cn/application-dev/ui/arkts-common-components-button.md b/zh-cn/application-dev/ui/arkts-common-components-button.md new file mode 100644 index 0000000000000000000000000000000000000000..08a05ae17b2bbe6319e71977ab261e5a60153a5a --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-common-components-button.md @@ -0,0 +1,261 @@ +# 按钮 + + +Button是按钮组件,通常用于响应用户的点击操作,其类型包括胶囊按钮、圆形按钮、普通按钮。Button当做为容器使用时可以通过添加子组件实现包含文字、图片等元素的按钮。具体用法请参考[Button](../reference/arkui-ts/ts-basic-components-button.md)。 + + +## 创建按钮 + +Button通过调用接口来创建,接口调用有以下两种形式: + + +- 创建不包含子组件的按钮。 + + ```ts + Button(label?: string, options?: { type?: ButtonType, stateEffect?: boolean }) + ``` + + 该接口用于创建不包含子组件的按钮,其中label用来设置按钮文字,type用于设置Button类型,stateEffect属性设置Button是否开启点击效果。 + + ```ts + Button('Ok', { type: ButtonType.Normal, stateEffect: true }) + .borderRadius(8) + .backgroundColor(0x317aff) + .width(90) + .height(40) + ``` + + ![zh-cn_image_0000001562820757](figures/zh-cn_image_0000001562820757.png) + + +- 创建包含子组件的按钮。 + + ```ts + Button(options?: {type?: ButtonType, stateEffect?: boolean}) + ``` + + 该接口用于创建包含子组件的按钮,只支持包含一个子组件,子组件可以是[基础组件](../reference/arkui-ts/ts-basic-components-blank.md)或者[容器组件](../reference/arkui-ts/ts-container-ability-component.md)。 + + ```ts + Button({ type: ButtonType.Normal, stateEffect: true }) { + Row() { + Image($r('app.media.loading')).width(20).height(40).margin({ left: 12 }) + Text('loading').fontSize(12).fontColor(0xffffff).margin({ left: 5, right: 12 }) + }.alignItems(VerticalAlign.Center) + }.borderRadius(8).backgroundColor(0x317aff).width(90).height(40) + ``` + + ![zh-cn_image_0000001511421216](figures/zh-cn_image_0000001511421216.png) + + +## 设置按钮类型 + +Button有三种可选类型,分别为Capsule(胶囊类型)、Circle(圆形按钮)和Normal(普通按钮),通过type进行设置。 + + +- 胶囊按钮(默认类型) + 此类型按钮的圆角自动设置为高度的一半,不支持通过borderRadius属性重新设置圆角。 + + ```ts + Button('Disable', { type: ButtonType.Capsule, stateEffect: false }) + .backgroundColor(0x317aff) + .width(90) + .height(40) + ``` + + ![zh-cn_image_0000001511421208](figures/zh-cn_image_0000001511421208.png) + + +- 圆形按钮 + 此类型按钮为圆形,不支持通过borderRadius属性重新设置圆角。 + + ```ts + Button('Circle', { type: ButtonType.Circle, stateEffect: false }) + .backgroundColor(0x317aff) + .width(90) + .height(90) + ``` + + ![zh-cn_image_0000001511740428](figures/zh-cn_image_0000001511740428.png) + +- 普通按钮 + 此类型的按钮默认圆角为0,支持通过borderRadius属性重新设置圆角。 + + ```ts + Button('Ok', { type: ButtonType.Normal, stateEffect: true }) + .borderRadius(8) + .backgroundColor(0x317aff) + .width(90) + .height(40) + ``` + + ![zh-cn_image_0000001563060641](figures/zh-cn_image_0000001563060641.png) + + +## 自定义样式 + +- 设置边框弧度。 + 一般使用通用属性来自定义按钮样式。例如通过borderRadius属性设置按钮的边框弧度。 + + ```ts + Button('circle border', { type: ButtonType.Normal }) + .borderRadius(20) + .height(40) + ``` + + ![zh-cn_image_0000001511900392](figures/zh-cn_image_0000001511900392.png) + + +- 设置文本样式。 + 通过添加文本样式设置按钮文本的展示样式。 + + ```ts + Button('font style', { type: ButtonType.Normal }) + .fontSize(20) + .fontColor(Color.Pink) + .fontWeight(800) + ``` + + ![zh-cn_image_0000001511580828](figures/zh-cn_image_0000001511580828.png) + + +- 设置背景颜色。 + 添加backgroundColor属性设置按钮的背景颜色。 + + ```ts + Button('background color').backgroundColor(0xF55A42) + ``` + + ![zh-cn_image_0000001562940477](figures/zh-cn_image_0000001562940477.png) + + +- 用作功能型按钮。 + 为删除操作创建一个按钮。 + + ```ts + Button({ type: ButtonType.Circle, stateEffect: true }) { + Image($r('app.media.ic_public_delete_filled')).width(30).height(30) + }.width(55).height(55).margin({ left: 20 }).backgroundColor(0xF55A42) + ``` + + ![zh-cn_image_0000001511740436](figures/zh-cn_image_0000001511740436.png) + + +## 添加事件 + +Button组件通常用于触发某些操作,可以绑定onClick事件来响应点击操作后的自定义行为。 + +```ts +Button('Ok', { type: ButtonType.Normal, stateEffect: true }) + .onClick(()=>{ + console.info('Button onClick') + }) +``` + + +## 场景示例 + +- 用于启动操作。 + 可以用按钮启动任何用户界面元素,按钮会根据用户的操作触发相应的事件。例如,在List容器里通过点击按钮进行页面跳转。 + + ```ts + // xxx.ets + import router from '@ohos.router'; + @Entry + @Component + struct ButtonCase1 { + build() { + List({ space: 4 }) { + ListItem() { + Button("First").onClick(() => { + router.pushUrl({ url: 'pages/first_page' }) + }) + .width('100%') + } + ListItem() { + Button("Second").onClick(() => { + router.pushUrl({ url: 'pages/second_page' }) + }) + .width('100%') + } + ListItem() { + Button("Third").onClick(() => { + router.pushUrl({ url: 'pages/third_page' }) + }) + .width('100%') + } + } + .listDirection(Axis.Vertical) + .backgroundColor(0xDCDCDC).padding(20) + } + } + ``` + + ![zh-cn_image_0000001562700393](figures/zh-cn_image_0000001562700393.png) + + +- 用于表单的提交。 + 在用户登录/注册页面,使用按钮进行登录或注册操作。 + + ```ts + // xxx.ets + @Entry + @Component + struct ButtonCase2 { + build() { + Column() { + TextInput({ placeholder: 'input your username' }).margin({ top: 20 }) + TextInput({ placeholder: 'input your password' }).type(InputType.Password).margin({ top: 20 }) + Button('Register').width(300).margin({ top: 20 }) + .onClick(() => { + // 需要执行的操作 + }) + }.padding(20) + } + } + ``` + + ![zh-cn_image_0000001562940473](figures/zh-cn_image_0000001562940473.png) + +- 悬浮按钮 + 在可以滑动的界面,滑动时按钮始终保持悬浮状态。 + + ```ts + // xxx.ets + @Entry + @Component + struct HoverButtonExample { + private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + build() { + Stack() { + List({ space: 20, initialIndex: 0 }) { + ForEach(this.arr, (item) => { + ListItem() { + Text('' + item) + .width('100%').height(100).fontSize(16) + .textAlign(TextAlign.Center).borderRadius(10).backgroundColor(0xFFFFFF) + } + }, item => item) + }.width('90%') + Button() { + Image($r('app.media.ic_public_add')) + .width(50) + .height(50) + } + .width(60) + .height(60) + .position({x: '80%', y: 600}) + .shadow({radius: 10}) + .onClick(() => { + // 需要执行的操作 + }) + } + .width('100%') + .height('100%') + .backgroundColor(0xDCDCDC) + .padding({ top: 5 }) + } + } + ``` + + ![GIF](figures/GIF.gif) diff --git a/zh-cn/application-dev/ui/arkts-common-components-custom-dialog.md b/zh-cn/application-dev/ui/arkts-common-components-custom-dialog.md new file mode 100644 index 0000000000000000000000000000000000000000..94808dff9a47d5157983b4fbcdf8f1b1abb92cf6 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-common-components-custom-dialog.md @@ -0,0 +1,100 @@ +# 自定义弹窗 + + +自定义弹窗CustomDialog可用于广告、中奖、警告、软件更新等与用户交互响应操作。开发者可以通过CustomDialogController类显示自定义弹窗。具体用法请参考[自定义弹窗](../reference/arkui-ts/ts-methods-custom-dialog-box.md)。 + + +## 创建自定义弹窗 + +1. 使用\@CustomDialog装饰器装饰自定义弹窗。 + +2. \@CustomDialog装饰器用于装饰自定义弹框,此装饰器内进行自定义内容(也就是弹框内容)。 + + ```ts + @CustomDialog + struct CustomDialogExample { + controller: CustomDialogController + build() { + Column() { + Text('我是内容') + .fontSize(20) + .margin({ top: 10, bottom: 10 }) + } + } + } + ``` + +3. 创建构造器,与装饰器呼应相连。 + + ```ts + dialogController: CustomDialogController = new CustomDialogController({ + builder: CustomDialogExample({}), + }) + ``` + +4. 点击与onClick事件绑定的组件使弹窗弹出 + + ```ts + Flex({justifyContent:FlexAlign.Center}){ + Button('click me') + .onClick(() => { + this.dialogController.open() + }) + }.width('100%') + ``` + + ![zh-cn_image_0000001562700493](figures/zh-cn_image_0000001562700493.png) + + +## 弹窗的交互 + +弹窗可用于数据交互,完成用户一系列响应操作。 + + +1. 在\@CustomDialog装饰器内添加按钮操作,同时添加数据函数的创建。 + + ```ts + @CustomDialog + struct CustomDialogExample { + controller: CustomDialogController + cancel: () => void + confirm: () => void + build() { + Column() { + Text('我是内容').fontSize(20).margin({ top: 10, bottom: 10 }) + Flex({ justifyContent: FlexAlign.SpaceAround }) { + Button('cancel') + .onClick(() => { + this.controller.close() + this.cancel() + }).backgroundColor(0xffffff).fontColor(Color.Black) + Button('confirm') + .onClick(() => { + this.controller.close() + this.confirm() + }).backgroundColor(0xffffff).fontColor(Color.Red) + }.margin({ bottom: 10 }) + } + } + } + ``` + +2. 页面内需要在构造器内进行接收,同时创建相应的函数操作。 + + ```ts + dialogController: CustomDialogController = new CustomDialogController({ + builder: CustomDialogExample({ + cancel: this.onCancel, + confirm: this.onAccept, + }), + alignment: DialogAlignment.Default, // 可设置dialog的对齐方式,设定显示在底部或中间等,默认为底部显示 + }) + onCancel() { + console.info('Callback when the first button is clicked') + } + onAccept() { + console.info('Callback when the second button is clicked') + } + ``` + + ![zh-cn_image_0000001511421320](figures/zh-cn_image_0000001511421320.png) diff --git a/zh-cn/application-dev/ui/arkts-common-components-progress-indicator.md b/zh-cn/application-dev/ui/arkts-common-components-progress-indicator.md new file mode 100644 index 0000000000000000000000000000000000000000..70026bd8724058808a26dcbd303ca6a0fdd85367 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-common-components-progress-indicator.md @@ -0,0 +1,135 @@ +# 进度条 + + +Progress是进度条显示组件,显示内容通常为某次目标操作的当前进度。具体用法请参考[Progress](../reference/arkui-ts/ts-basic-components-progress.md)。 + + +## 创建进度条 + +Progress通过调用接口来创建,接口调用形式如下: + + + +```ts +Progress(options: {value: number, total?: number, type?: ProgressType}) +``` + + +该接口用于创建type样式的进度条,其中value用于设置初始进度值,total用于设置进度总长度,type决定Progress样式。 + + + +```ts +Progress({ value: 24, total: 100, type: ProgressType.Linear }) // 创建一个进度总长为100,初始进度值为24的线性进度条 +``` + + +![create](figures/create.png) + + +## 设置进度条样式 + +Progress有5种可选类型,在创建时通过设置ProgressType枚举类型给type可选项指定Progress类型。其分别为:ProgressType.Linear(线性样式)、 ProgressType.Ring(环形无刻度样式)、ProgressType.ScaleRing(环形有刻度样式)、ProgressType.Eclipse(圆形样式)和ProgressType.Capsule(胶囊样式)。 + + +- 线性样式进度条(默认类型) + >**说明:** + > + > 从API version9开始,组件高度大于宽度的时候自适应垂直显示,相等时仍然保持水平显示。 + + + ```ts + Progress({ value: 20, total: 100, type: ProgressType.Linear }).width(200).height(50) + Progress({ value: 20, total: 100, type: ProgressType.Linear }).width(50).height(200) + ``` + + ![zh-cn_image_0000001562700417](figures/zh-cn_image_0000001562700417.png) + +- 环形无刻度样式进度条 + + ```ts + // 从左往右,1号环形进度条,默认前景色为蓝色,默认strokeWidth进度条宽度为2.0vp + Progress({ value: 40, total: 150, type: ProgressType.Ring }).width(100).height(100) + // 从左往右,2号环形进度条 + Progress({ value: 40, total: 150, type: ProgressType.Ring }).width(100).height(100) + .color(Color.Grey) // 进度条前景色为灰色 + .style({ strokeWidth: 15}) // 设置strokeWidth进度条宽度为15.0vp + ``` + + ![progress_ring](figures/progress_ring.png) + +- 环形有刻度样式进度条 + + ```ts + Progress({ value: 20, total: 150, type: ProgressType.ScaleRing }).width(100).height(100) + .backgroundColor(Color.Black) + .style({ scaleCount: 20, scaleWidth: 5 }) // 设置环形有刻度进度条总刻度数为20,刻度宽度为5vp + Progress({ value: 20, total: 150, type: ProgressType.ScaleRing }).width(100).height(100) + .backgroundColor(Color.Black) + .style({ strokeWidth: 15, scaleCount: 20, scaleWidth: 5 }) // 设置环形有刻度进度条宽度15,总刻度数为20,刻度宽度为5vp + Progress({ value: 20, total: 150, type: ProgressType.ScaleRing }).width(100).height(100) + .backgroundColor(Color.Black) + .style({ strokeWidth: 15, scaleCount: 20, scaleWidth: 3 }) // 设置环形有刻度进度条宽度15,总刻度数为20,刻度宽度为3vp + ``` + + ![progress_scalering](figures/progress_scalering.png) + +- 圆形样式进度条 + + ```ts + // 从左往右,1号圆形进度条,默认前景色为蓝色 + Progress({ value: 10, total: 150, type: ProgressType.Eclipse }).width(100).height(100) + // 从左往右,2号圆形进度条,指定前景色为灰色 + Progress({ value: 20, total: 150, type: ProgressType.Eclipse }).color(Color.Grey).width(100).height(100) + ``` + + ![progress_circle](figures/progress_circle.png) + +- 胶囊样式进度条 + >**说明:** + > + >- 头尾两端圆弧处的进度展示效果与ProgressType.Eclipse样式相同; + >- 中段处的进度展示效果为矩形状长条,与ProgressType.Linear线性样式相似; + > + >- 组件高度大于宽度的时候自适应垂直显示。 + + + ```ts + Progress({ value: 10, total: 150, type: ProgressType.Capsule }).width(100).height(50) + Progress({ value: 20, total: 150, type: ProgressType.Capsule }).width(50).height(100).color(Color.Grey) + Progress({ value: 50, total: 150, type: ProgressType.Capsule }).width(50).height(100).backgroundColor(Color.Black) + ``` + + ![progress_captule](figures/progress_captule.png) + + +## 场景示例 + +更新当前进度值,如应用安装进度条。可通过点击Button增加progressValue,.value()属性将progressValue设置给Progress组件,进度条组件即会触发刷新,更新当前进度。 + +```ts +@Entry +@Component +struct ProgressCase1 { + @State progressValue: number = 0 // 设置进度条初始值为0 + build() { + Column() { + Column() { + Progress({value:0, total:100, type:ProgressType.Capsule}).width(200).height(50) + .style({strokeWidth:50}).value(this.progressValue) + Row().width('100%').height(5) + Button("进度条+5") + .onClick(()=>{ + this.progressValue += 5 + if (this.progressValue > 100){ + this.progressValue = 0 + } + }) + } + }.width('100%').height('100%') + } +} +``` + + +![progress](figures/progress.gif) diff --git a/zh-cn/application-dev/ui/arkts-common-components-radio-button.md b/zh-cn/application-dev/ui/arkts-common-components-radio-button.md new file mode 100644 index 0000000000000000000000000000000000000000..e1c4b45dcb936ba274cb6b47ad2c1cb12468e640 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-common-components-radio-button.md @@ -0,0 +1,106 @@ +# 单选框 + + +Radio是单选框组件,通常用于提供相应的用户交互选择项,同一组的Radio中只有一个可以被选中。具体用法请参考[Radio](../reference/arkui-ts/ts-basic-components-radio.md)。 + + +## 创建单选框 + +Radio通过调用接口来创建,接口调用形式如下: + + +```ts +Radio(options: {value: string, group: string}) +``` + + 该接口用于创建一个单选框,其中value是单选框的名称,group是单选框的所属群组名称。checked属性可以设置单选框的状态,状态分别为false和true时,设置为true时表示单选框被选中。Radio仅支持选中和未选中两种样式,不支持自定义颜色和形状。 + +```ts +Radio({ value: 'Radio1', group: 'radioGroup' }) + .checked(false) +Radio({ value: 'Radio2', group: 'radioGroup' }) + .checked(true) +``` + + +![zh-cn_image_0000001562820821](figures/zh-cn_image_0000001562820821.png) + + +## 添加事件 + +除支持[通用事件](../reference/arkui-ts/ts-universal-events-click.md)外,Radio通常用于选中后触发某些操作,可以绑定onChange事件来响应选中操作后的自定义行为。 + + + +```ts + Radio({ value: 'Radio1', group: 'radioGroup' }) + .onChange((isChecked: boolean) => { + if(isChecked) { + //需要执行的操作 + } + }) + Radio({ value: 'Radio2', group: 'radioGroup' }) + .onChange((isChecked: boolean) => { + if(isChecked) { + //需要执行的操作 + } + }) +``` + + +## 场景示例 + +通过点击Radio切换声音模式。 + + +```ts +// xxx.ets +import promptAction from '@ohos.promptAction'; +@Entry +@Component +struct RadioExample { + build() { + Row() { + Column() { + Radio({ value: 'Radio1', group: 'radioGroup' }).checked(true) + .height(50) + .width(50) + .onChange((isChecked: boolean) => { + if(isChecked) { + // 切换为响铃模式 + promptAction.showToast({ message: 'Ringing mode.' }) + } + }) + Text('Ringing') + } + Column() { + Radio({ value: 'Radio2', group: 'radioGroup' }) + .height(50) + .width(50) + .onChange((isChecked: boolean) => { + if(isChecked) { + // 切换为振动模式 + promptAction.showToast({ message: 'Vibration mode.' }) + } + }) + Text('Vibration') + } + Column() { + Radio({ value: 'Radio3', group: 'radioGroup' }) + .height(50) + .width(50) + .onChange((isChecked: boolean) => { + if(isChecked) { + // 切换为静音模式 + promptAction.showToast({ message: 'Silent mode.' }) + } + }) + Text('Silent') + } + }.height('100%').width('100%').justifyContent(FlexAlign.Center) + } +} +``` + + +![zh-cn_image_0000001562700457](figures/zh-cn_image_0000001562700457.png) diff --git a/zh-cn/application-dev/ui/arkts-common-components-switch.md b/zh-cn/application-dev/ui/arkts-common-components-switch.md new file mode 100644 index 0000000000000000000000000000000000000000..1546ba584f4235c062d96ca6f820b986073786bd --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-common-components-switch.md @@ -0,0 +1,153 @@ +# 切换按钮 + + +Toggle组件提供状态按钮样式,勾选框样式及开关样式,一般用于两种状态之间的切换。具体用法请参考[Toggle](../reference/arkui-ts/ts-basic-components-toggle.md)。 + + +## 创建切换按钮 + +Toggle通过调用接口来创建,接口调用形式如下: + + + +```ts +Toggle(options: { type: ToggleType, isOn?: boolean }) +``` + + +该接口用于创建切换按钮,其中ToggleType为开关类型,包括Button、Checkbox和Switch,isOn为切换按钮的状态,接口调用有以下两种形式: + + +- 创建不包含子组件的Toogle。 + 当ToggleType为Checkbox或者Switch时,用于创建不包含子组件的Toggle: + + + ```ts + Toggle({ type: ToggleType.Checkbox, isOn: false }) + Toggle({ type: ToggleType.Checkbox, isOn: true }) + ``` + + ![zh-cn_image_0000001562940485](figures/zh-cn_image_0000001562940485.png) + + + ```ts + Toggle({ type: ToggleType.Switch, isOn: false }) + Toggle({ type: ToggleType.Switch, isOn: true }) + ``` + + ![zh-cn_image_0000001511421228](figures/zh-cn_image_0000001511421228.png) + + +- 创建包含子组件的Toggle。 + 当ToggleType为Button时,如果子组件有文本设置,则相应的文本内容会显示在按钮内部。 + + + ```ts + Toggle({ type: ToggleType.Button, isOn: false }) { + Text('status button') + .fontColor('#182431') + .fontSize(12) + }.width(100) + Toggle({ type: ToggleType.Button, isOn: true }) { + Text('status button') + .fontColor('#182431') + .fontSize(12) + }.width(100) + ``` + + ![zh-cn_image_0000001511900404](figures/zh-cn_image_0000001511900404.png) + + +## 自定义样式 + +- 通过selectedColor属性设置Toggle打开选中后的背景颜色。 + + ```ts + Toggle({ type: ToggleType.Button, isOn: true }) { + Text('status button') + .fontColor('#182431') + .fontSize(12) + }.width(100).selectedColor(Color.Pink) + Toggle({ type: ToggleType.Checkbox, isOn: true }) + .selectedColor(Color.Pink) + Toggle({ type: ToggleType.Switch, isOn: true }) + .selectedColor(Color.Pink) + ``` + + ![zh-cn_image_0000001563060657](figures/zh-cn_image_0000001563060657.png) + +- 通过switchPointColor属性设置Switch类型的圆形滑块颜色,仅对type为ToggleType.Switch生效。 + + ```ts + Toggle({ type: ToggleType.Switch, isOn: false }) + .switchPointColor(Color.Pink) + Toggle({ type: ToggleType.Switch, isOn: true }) + .switchPointColor(Color.Pink) + ``` + + ![zh-cn_image_0000001511421232](figures/zh-cn_image_0000001511421232.png) + + +## 添加事件 + +除支持通用事件外,Toggle通常用于选中和取消选中后触发某些操作,可以绑定onChange事件来响应操作后的自定义行为。 + + +```ts +Toggle({ type: ToggleType.Switch, isOn: false }) + .onChange((isOn: boolean) => { + if(isOn) { + // 需要执行的操作 + } + }) +``` + + +## 场景示例 + +Toggle可用于切换蓝牙开关状态。 + + + +```ts +// xxx.ets +import prompt from '@ohos.promptAction'; +@Entry +@Component +struct ToggleExample { + build() { + Column() { + Row() { + Text("Bluetooth Mode") + .height(50) + .fontSize(16) + } + Row() { + Text("Bluetooth") + .height(50) + .padding({left: 10}) + .fontSize(16) + .textAlign(TextAlign.Start) + .backgroundColor(0xFFFFFF) + Toggle({ type: ToggleType.Switch }) + .margin({left: 200, right: 10}) + .onChange((isOn: boolean) => { + if(isOn) { + promptAction.showToast({ message: 'Bluetooth is on.' }) + } else { + promptAction.showToast({ message: 'Bluetooth is off.' }) + } + }) + } + .backgroundColor(0xFFFFFF) + } + .padding(10) + .backgroundColor(0xDCDCDC) + .width('100%') + .height('100%') + } +} +``` + + +![zh-cn_image_0000001511740448](figures/zh-cn_image_0000001511740448.png) diff --git a/zh-cn/application-dev/ui/arkts-common-components-text-display.md b/zh-cn/application-dev/ui/arkts-common-components-text-display.md new file mode 100644 index 0000000000000000000000000000000000000000..e91fec8627edf96accc4469e5cc796f309d2a8fe --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-common-components-text-display.md @@ -0,0 +1,421 @@ +# 文本显示 + + +Text是文本组件,通常用于展示用户的视图,如显示文章的文字。具体用法可参考[Text](../reference/arkui-ts/ts-basic-components-text.md)。 + + +## 创建文本 + +Text可通过以下两种方式来创建: + + +- string字符串 + + ```ts + Text('我是一段文本') + ``` + + +![zh-cn_image_0000001563060685](figures/zh-cn_image_0000001563060685.png) + + +- 引用Resource资源 + 资源引用类型可以通过$r创建Resource类型对象,文件位置为/resources/base/element/string.json。 + + + ```ts + Text($r('app.string.module_desc')) + .baselineOffset(0) + .fontSize(30) + .border({ width: 1 }) + .padding(10) + .width(300) + ``` + + ![zh-cn_image_0000001511580872](figures/zh-cn_image_0000001511580872.png) + + +## 添加子组件 + +[Span](../reference/arkui-ts/ts-basic-components-span.md)只能作为Text组件的子组件显示文本内容。可以在一个Text内添加多个Span来显示一段信息,例如产品说明书、承诺书等。 + +- 创建Span。 + Span组件需要写到Text组件内,单独写Span组件不会显示信息,Text与Span同时配置文本内容内容时,Span内容覆盖Text内容。 + + + ```ts + Text('我是Text') { + Span('我是Span') + } + .padding(10) + .borderWidth(1) + ``` + + ![zh-cn_image_0000001562700441](figures/zh-cn_image_0000001562700441.png) + +- 设置文本装饰线及颜色。 + 通过decoration设置文本装饰线及颜色。 + + + ```ts + Text() { + Span('我是Span1,').fontSize(16).fontColor(Color.Grey) + .decoration({ type: TextDecorationType.LineThrough, color: Color.Red }) + Span('我是Span2').fontColor(Color.Blue).fontSize(16) + .fontStyle(FontStyle.Italic) + .decoration({ type: TextDecorationType.Underline, color: Color.Black }) + Span(',我是Span3').fontSize(16).fontColor(Color.Grey) + .decoration({ type: TextDecorationType.Overline, color: Color.Green }) + } + .borderWidth(1) + .padding(10) + ``` + + ![zh-cn_image_0000001562700437](figures/zh-cn_image_0000001562700437.png) + +- 通过textCase设置文字一直保持大写或者小写状态。 + + ```ts + Text() { + Span('I am Upper-span').fontSize(12) + .textCase(TextCase.UpperCase) + } + .borderWidth(1) + .padding(10) + ``` + + ![zh-cn_image_0000001562940525](figures/zh-cn_image_0000001562940525.png) + +- 添加事件。 + 由于Span组件无尺寸信息,事件仅支持点击事件onClick。 + + + ```ts + Text() { + Span('I am Upper-span').fontSize(12) + .textCase(TextCase.UpperCase) + .onClick(()=>{ + console.info('我是Span——onClick') + }) + } + ``` + + +## 自定义文本样式 + +- 通过textAlign属性设置文本对齐样式。 + + ```ts + Text('左对齐') + .width(300) + .textAlign(TextAlign.Start) + .border({ width: 1 }) + .padding(10) + Text('中间对齐') + .width(300) + .textAlign(TextAlign.Center) + .border({ width: 1 }) + .padding(10) + Text('右对齐') + .width(300) + .textAlign(TextAlign.End) + .border({ width: 1 }) + .padding(10) + ``` + + ![zh-cn_image_0000001511421260](figures/zh-cn_image_0000001511421260.png) + +- 通过textOverflow属性控制文本超长处理,textOverflow需配合maxLines一起使用(默认情况下文本自动折行)。 + + ```ts + Text('This is the setting of textOverflow to Clip text content This is the setting of textOverflow to None text content. This is the setting of textOverflow to Clip text content This is the setting of textOverflow to None text content.') + .width(250) + .textOverflow({ overflow: TextOverflow.None }) + .maxLines(1) + .fontSize(12) + .border({ width: 1 }).padding(10) + Text('我是超长文本,超出的部分显示省略号。I am an extra long text, with ellipses displayed for any excess。') + .width(250) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .maxLines(1) + .fontSize(12) + .border({ width: 1 }).padding(10) + ``` + + ![zh-cn_image_0000001563060693](figures/zh-cn_image_0000001563060693.png) + + ![zh-cn_image_0000001563060701](figures/zh-cn_image_0000001563060701.png) + +- 通过lineHeight属性设置文本行高。 + + ```ts + Text('This is the text with the line height set. This is the text with the line height set.') + .width(300).fontSize(12).border({ width: 1 }).padding(10) + Text('This is the text with the line height set. This is the text with the line height set.') + .width(300).fontSize(12).border({ width: 1 }).padding(10) + .lineHeight(20) + ``` + + ![zh-cn_image_0000001511740480](figures/zh-cn_image_0000001511740480.png) + +- 通过decoration属性设置文本装饰线样式及其颜色。 + + ```ts + Text('This is the text') + .decoration({ + type: TextDecorationType.LineThrough, + color: Color.Red + }) + .borderWidth(1).padding(10).margin(5) + Text('This is the text') + .decoration({ + type: TextDecorationType.Overline, + color: Color.Red + }) + .borderWidth(1).padding(10).margin(5) + Text('This is the text') + .decoration({ + type: TextDecorationType.Underline, + color: Color.Red + }) + .borderWidth(1).padding(10).margin(5) + ``` + + ![zh-cn_image_0000001511580888](figures/zh-cn_image_0000001511580888.png) + +- 通过baselineOffset属性设置文本基线的偏移量。 + + ```ts + Text('This is the text content with baselineOffset 0.') + .baselineOffset(0) + .fontSize(12) + .border({ width: 1 }) + .padding(10) + .width('100%') + .margin(5) + Text('This is the text content with baselineOffset 30.') + .baselineOffset(30) + .fontSize(12) + .border({ width: 1 }) + .padding(10) + .width('100%') + .margin(5) + + Text('This is the text content with baselineOffset -20.') + .baselineOffset(-20) + .fontSize(12) + .border({ width: 1 }) + .padding(10) + .width('100%') + .margin(5) + ``` + + ![zh-cn_image_0000001562820789](figures/zh-cn_image_0000001562820789.png) + +- 通过letterSpacing属性设置文本字符间距。 + + ```ts + Text('This is the text content with letterSpacing 0.') + .letterSpacing(0) + .fontSize(12) + .border({ width: 1 }) + .padding(10) + .width('100%') + .margin(5) + Text('This is the text content with letterSpacing 3.') + .letterSpacing(3) + .fontSize(12) + .border({ width: 1 }) + .padding(10) + .width('100%') + .margin(5) + Text('This is the text content with letterSpacing -1.') + .letterSpacing(-1) + .fontSize(12) + .border({ width: 1 }) + .padding(10) + .width('100%') + .margin(5) + ``` + + ![zh-cn_image_0000001562940513](figures/zh-cn_image_0000001562940513.png) + +- 通过minFontSize与maxFontSize自适应字体大小,minFontSize设置文本最小显示字号,maxFontSize设置文本最大显示字号,minFontSize与maxFontSize必须搭配同时使用,以及需配合maxline或布局大小限制一起使用,单独设置不生效。 + + ```ts + Text('我的最大字号为30,最小字号为5,宽度为250,maxLines为1') + .width(250) + .maxLines(1) + .maxFontSize(30) + .minFontSize(5) + .border({ width: 1 }) + .padding(10) + .margin(5) + Text('我的最大字号为30,最小字号为5,宽度为250,maxLines为2') + .width(250) + .maxLines(2) + .maxFontSize(30) + .minFontSize(5) + .border({ width: 1 }) + .padding(10) + .margin(5) + Text('我的最大字号为30,最小字号为15,宽度为250,高度为50') + .width(250) + .height(50) + .maxFontSize(30) + .minFontSize(15) + .border({ width: 1 }) + .padding(10) + .margin(5) + Text('我的最大字号为30,最小字号为15,宽度为250,高度为100') + .width(250) + .height(100) + .maxFontSize(30) + .minFontSize(15) + .border({ width: 1 }) + .padding(10) + .margin(5) + ``` + + ![zh-cn_image_0000001511740472](figures/zh-cn_image_0000001511740472.png) + +- 通过textCase属性设置文本大小写。 + + ```ts + Text('This is the text content with textCase set to Normal.') + .textCase(TextCase.Normal) + .padding(10) + .border({ width: 1 }) + .padding(10) + .margin(5) + + // 文本全小写展示 + Text('This is the text content with textCase set to LowerCase.') + .textCase(TextCase.LowerCase) + .border({ width: 1 }) + .padding(10) + .margin(5) + + // 文本全大写展示 + Text('This is the text content with textCase set to UpperCase.') + .textCase(TextCase.UpperCase) + .border({ width: 1 }) + .padding(10) + .margin(5) + ``` + + ![zh-cn_image_0000001562940529](figures/zh-cn_image_0000001562940529.png) + +- 通过copyOption属性设置文本是否可复制粘贴。 + + ```ts + Text("这是一段可复制文本") + .fontSize(30) + .copyOption(CopyOptions.InApp) + ``` + + ![zh-cn_image_0000001511580868](figures/zh-cn_image_0000001511580868.png) + + +## 添加事件 + +Text组件可以添加通用事件,可以绑定onClick、onTouch等事件来响应操作。 + + +```ts +Text('点我') + .onClick(()=>{ + console.info('我是Text的点击响应事件'); + }) +``` + + +## 场景示例 + + +```ts +// xxx.ets +@Entry +@Component +struct TextExample { + build() { + Column() { + Row() { + Text("1").fontSize(14).fontColor(Color.Red).margin({ left: 10, right: 10 }) + Text("我是热搜词条1") + .fontSize(12) + .fontColor(Color.Blue) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .fontWeight(300) + Text("爆") + .margin({ left: 6 }) + .textAlign(TextAlign.Center) + .fontSize(10) + .fontColor(Color.White) + .fontWeight(600) + .backgroundColor(0x770100) + .borderRadius(5) + .width(15) + .height(14) + }.width('100%').margin(5) + + Row() { + Text("2").fontSize(14).fontColor(Color.Red).margin({ left: 10, right: 10 }) + Text("我是热搜词条2 我是热搜词条2 我是热搜词条2 我是热搜词条2 我是热搜词条2") + .fontSize(12) + .fontColor(Color.Blue) + .fontWeight(300) + .constraintSize({ maxWidth: 200 }) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + Text("热") + .margin({ left: 6 }) + .textAlign(TextAlign.Center) + .fontSize(10) + .fontColor(Color.White) + .fontWeight(600) + .backgroundColor(0xCC5500) + .borderRadius(5) + .width(15) + .height(14) + }.width('100%').margin(5) + + Row() { + Text("3").fontSize(14).fontColor(Color.Orange).margin({ left: 10, right: 10 }) + Text("我是热搜词条3") + .fontSize(12) + .fontColor(Color.Blue) + .fontWeight(300) + .maxLines(1) + .constraintSize({ maxWidth: 200 }) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + Text("热") + .margin({ left: 6 }) + .textAlign(TextAlign.Center) + .fontSize(10) + .fontColor(Color.White) + .fontWeight(600) + .backgroundColor(0xCC5500) + .borderRadius(5) + .width(15) + .height(14) + }.width('100%').margin(5) + + Row() { + Text("4").fontSize(14).fontColor(Color.Grey).margin({ left: 10, right: 10 }) + Text("我是热搜词条4 我是热搜词条4 我是热搜词条4 我是热搜词条4 我是热搜词条4") + .fontSize(12) + .fontColor(Color.Blue) + .fontWeight(300) + .constraintSize({ maxWidth: 200 }) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + }.width('100%').margin(5) + }.width('100%') + } +} + +``` + +![zh-cn_image_0000001562820805](figures/zh-cn_image_0000001562820805.png) diff --git a/zh-cn/application-dev/ui/arkts-common-components-text-input.md b/zh-cn/application-dev/ui/arkts-common-components-text-input.md new file mode 100644 index 0000000000000000000000000000000000000000..ce8e4c1a2267b9eec504972353f0ea1c11dd51b6 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-common-components-text-input.md @@ -0,0 +1,151 @@ +# 文本输入 + + +TextInput、TextArea是输入框组件,通常用于响应用户的输入操作,比如评论区的输入、聊天框的输入、表格的输入等,也可以结合其它组件构建功能页面,例如登录注册页面。具体用法参考[TextInput](../reference/arkui-ts/ts-basic-components-textinput.md)、[TextArea](../reference/arkui-ts/ts-basic-components-textarea.md)。 + + +## 创建输入框 + +TextInput为单行输入框、TextArea为多行输入框。通过以下接口来创建。 + + +```ts +TextArea(value?:{placeholder?: ResourceStr, text?: ResourceStr, controller?: TextAreaController}) +``` + + + +```ts +TextInput(value?:{placeholder?: ResourceStr, text?: ResourceStr, controller?: TextInputController}) +``` + + +- 单行输入框 + + ```ts + TextInput() + ``` + + ![zh-cn_image_0000001511580844](figures/zh-cn_image_0000001511580844.png) + + +- 多行输入框 + + ```ts + TextArea() + ``` + + ![zh-cn_image_0000001562940481](figures/zh-cn_image_0000001562940481.png) + + 多行输入框文字超出一行时会自动折行。 + + + ```ts + TextArea({text:"我是TextArea我是TextArea我是TextArea我是TextArea"}).width(300) + ``` + + ![zh-cn_image_0000001511580836](figures/zh-cn_image_0000001511580836.png) + + +## 设置输入框类型 + +TextInput有5种可选类型,分别为Normal基本输入模式、Password密码输入模式、Email邮箱地址输入模式、Number纯数字输入模式、PhoneNumber电话号码输入模式。通过type属性进行设置: + + +- 基本输入模式(默认类型) + + ```ts + TextInput() + .type(InputType.Normal) + ``` + + ![zh-cn_image_0000001562820765](figures/zh-cn_image_0000001562820765.png) + +- 密码输入模式 + + ```ts + TextInput() + .type(InputType.Password) + ``` + + ![zh-cn_image_0000001511580840](figures/zh-cn_image_0000001511580840.png) + + +## 自定义样式 + +- 设置无输入时的提示文本。 + TextInput({placeholder:'我是提示文本'}) + + + ```ts + TextInput({placeholder:'我是提示文本'}) + ``` + + ![zh-cn_image_0000001511900400](figures/zh-cn_image_0000001511900400.png) + + +- 设置输入框当前的文本内容。 + + ```ts + TextInput({placeholder:'我是提示文本',text:'我是当前文本内容'}) + ``` + + ![zh-cn_image_0000001562820761](figures/zh-cn_image_0000001562820761.png) + +- 添加backgroundColor改变输入框的背景颜色。 + + ```ts + TextInput({placeholder:'我是提示文本',text:'我是当前文本内容'}) + .backgroundColor(Color.Pink) + ``` + + ![zh-cn_image_0000001511740444](figures/zh-cn_image_0000001511740444.png) + + 更丰富的样式可以结合[通用属性](../reference/arkui-ts/ts-universal-attributes-size.md)实现。 + + +## 添加事件 + +文本框主要用于获取用户输入的信息,把信息处理成数据进行上传,绑定onChange事件可以获取输入框内改变的内容。用户也可以使用通用事件来进行相应的交互操作。 + + + +```ts +TextInput() + .onChange((value: string) => { + console.info(value); + }) + .onFocus(() => { + console.info('获取焦点'); + }) +``` + + +## 场景示例 + +用于表单的提交,在用户登录/注册页面,用户的登录或注册的输入操作。 + + + +```ts +@Entry +@Component +struct TextInputSample { + build() { + Column() { + TextInput({ placeholder: 'input your username' }).margin({ top: 20 }) + .onSubmit((EnterKeyType)=>{ + console.info(EnterKeyType+'输入法回车键的类型值') + }) + TextInput({ placeholder: 'input your password' }).type(InputType.Password).margin({ top: 20 }) + .onSubmit((EnterKeyType)=>{ + console.info(EnterKeyType+'输入法回车键的类型值') + }) + Button('Sign in').width(150).margin({ top: 20 }) + }.padding(20) + } +} +``` + + +![zh-cn_image_0000001563060653](figures/zh-cn_image_0000001563060653.png) diff --git a/zh-cn/application-dev/ui/arkts-common-components-video-player.md b/zh-cn/application-dev/ui/arkts-common-components-video-player.md new file mode 100644 index 0000000000000000000000000000000000000000..8bc03f4d17169840af1fe703f27954e12afd2457 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-common-components-video-player.md @@ -0,0 +1,244 @@ +# 视频播放 + + +Video组件用于播放视频文件并控制其播放状态,常用于为短视频应用和应用内部视频的列表页面。当视频完整出现时会自动播放,用户点击视频区域则会暂停播放,同时显示播放进度条,通过拖动播放进度条指定视频播放到具体位置。具体用法请参考[Video](../reference/arkui-ts/ts-media-components-video.md)。 + + +## 创建视频组件 + +Video通过调用接口来创建,接口调用形式如下: + + +```ts +Video(value: {src?: string | Resource, currentProgressRate?: number | string | PlaybackSpeed, previewUri?: string | PixelMap | Resource, controller?: VideoController}) +``` + +该接口用于创建视频播放组件。其中,src指定视频播放源的路径,加载方式请参考[加载视频资源](#加载视频资源),currentProgressRate用于设置视频播放倍速,previewUri指定视频未播放时的预览图片路径,controller设置视频控制器,用于自定义控制视频。 + + +## 加载视频资源 + +Video组件支持加载本地视频和网络视频。 + + +### 加载本地视频 + +- 普通本地视频。 + 加载本地视频时,首先在本地rawfile目录指定对应的文件,如下图所示。 + + ![zh-cn_image_0000001562700409](figures/zh-cn_image_0000001562700409.png) + + 再使用资源访问符$rawfile()引用视频资源。 + + ```ts + @Component + export struct VideoPlayer{ + private controller:VideoController; + private previewUris: Resource = $r ('app.media.preview'); + private innerResource: Resource = $rawfile('videoTest.mp4'); + build(){ + Column() { + Video({ + src: this.innerResource, + previewUri: this.previewUris, + controller: this.controller + }) + } + } + } + ``` + + +- [Data Ability](../application-models/dataability-overview.md)提供的视频路径带有dataability://前缀,使用时确保对应视频资源存在即可。 + + ```ts + @Component + export struct VideoPlayer{ + private controller:VideoController; + private previewUris: Resource = $r ('app.media.preview'); + private videosrc: string= 'dataability://device_id/com.domainname.dataability.videodata/video/10' + build(){ + Column() { + Video({ + src: this.videosrc, + previewUri: this.previewUris, + controller: this.controller + }) + } + } + } + ``` + + +### 加载网络视频 + +加载网络视频时,需要申请权限ohos.permission.INTERNET,具体申请方式请参考[权限申请声明](../security/accesstoken-guidelines.md)。此时,Video的src属性为网络视频的链接。 + + +```ts +@Component +export struct VideoPlayer{ + private controller:VideoController; + private previewUris: Resource = $r ('app.media.preview'); + private videosrc: string= 'https://www.example.com/example.mp4' // 使用时请替换为实际视频加载网址 + build(){ + Column() { + Video({ + src: this.videosrc, + previewUri: this.previewUris, + controller: this.controller + }) + } + } +} +``` + + +## 添加属性 + +Video组件[属性](../reference/arkui-ts/ts-media-components-video.md#属性)主要用于设置视频的播放形式。例如设置视频播放是否静音、播放时是否显示控制条等。 + + +```ts +@Component +export struct VideoPlayer { + private controller: VideoController; + + build() { + Column() { + Video({ + controller: this.controller + }) + .muted(false) //设置是否静音 + .controls(false) //设置是否显示默认控制条 + .autoPlay(false) //设置是否自动播放 + .loop(false) //设置是否循环播放 + .objectFit(ImageFit.Contain) //设置视频适配模式 + } + } +} +``` + + +## 事件调用 + + Video组件回调事件主要为播放开始、暂停结束、播放失败、视频准备和操作进度条等事件,除此之外,Video组件也支持通用事件的调用,如点击、触摸等事件的调用。详细的事件请参考[事件说明](../reference/arkui-ts/ts-media-components-video.md#事件)。 + +```ts +@Entry +@Component +struct VideoPlayer{ + private controller:VideoController; + private previewUris: Resource = $r ('app.media.preview'); + private innerResource: Resource = $rawfile('videoTest.mp4'); + build(){ + Column() { + Video({ + src: this.innerResource, + previewUri: this.previewUris, + controller: this.controller + }) + .onUpdate((event) => { //更新事件回调 + console.info("Video update."); + }) + .onPrepared((event) => { //准备事件回调 + console.info("Video prepared."); + }) + .onError(() => { //失败事件回调 + console.info("Video error."); + }) + } + } +} +``` + + +## Video控制器使用 + +Video控制器主要用于控制视频的状态,包括播放、暂停、停止以及设置进度等,详细的使用请参考[VideoController使用说明](../reference/arkui-ts/ts-media-components-video.md#videocontroller)。 + +- 默认控制器 + 默认的控制器支持视频的开始、暂停、进度调整、全屏显示四项基本功能。 + + ```ts + @Entry + @Component + struct VideoGuide { + @State videoSrc: Resource = $rawfile('videoTest.mp4') + @State previewUri: string = 'common/videoIcon.png' + @State curRate: PlaybackSpeed = PlaybackSpeed.Speed_Forward_1_00_X + build() { + Row() { + Column() { + Video({ + src: this.videoSrc, + previewUri: this.previewUri, + currentProgressRate: this.curRate + }) + } + .width('100%') + } + .height('100%') + } + } + ``` + +- 自定义控制器 + 使用自定义的控制器,先将默认控制器关闭掉,之后可以使用button以及slider等组件进行自定义的控制与显示,适合自定义较强的场景下使用。 + + ```ts + @Entry + @Component + struct VideoGuide { + @State videoSrc: Resource = $rawfile('videoTest.mp4') + @State previewUri: string = 'common/videoIcon.png' + @State curRate: PlaybackSpeed = PlaybackSpeed.Speed_Forward_1_00_X + @State isAutoPlay: boolean = false + @State showControls: boolean = true + @State sliderStartTime: string = ''; + @State currentTime: number = 0; + @State durationTime: number = 0; + @State durationStringTime: string =''; + controller: VideoController = new VideoController() + + build() { + Row() { + Column() { + Video({ + src: this.videoSrc, + previewUri: this.previewUri, + currentProgressRate: this.curRate, + controller: this.controller + }).controls(false).autoPlay(true) + .onPrepared((event)=>{ + this.durationTime = event.duration + }) + .onUpdate((event)=>{ + this.currentTime =event.time + }) + Row() { + Text(JSON.stringify(this.currentTime) + 's') + Slider({ + value: this.currentTime, + min: 0, + max: this.durationTime + }) + .onChange((value: number, mode: SliderChangeMode) => { + this.controller.setCurrentTime(value); + }).width("90%") + Text(JSON.stringify(this.durationTime) + 's') + } + .opacity(0.8) + .width("100%") + } + .width('100%') + } + .height('40%') + } + } + ``` + + +## 其他说明 + +Video组件已经封装好了视频播放的基础能力,开发者无需进行视频实例的创建,视频信息的设置获取,只需要设置数据源以及基础信息即可播放视频,相对扩展能力较弱。如果开发者想自定义视频播放,还请参考[媒体系统播放音视频](../media/video-playback.md)。 diff --git a/zh-cn/application-dev/ui/arkts-common-components-xcomponent.md b/zh-cn/application-dev/ui/arkts-common-components-xcomponent.md new file mode 100644 index 0000000000000000000000000000000000000000..b2e72c0310aba5e3c190db64e39645a0ebaef34d --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-common-components-xcomponent.md @@ -0,0 +1,299 @@ +# XComponent + + +[XComponent](../reference/arkui-ts/ts-basic-components-xcomponent.md)组件作为一种绘制组件,通常用于满足开发者较为复杂的自定义绘制需求,例如相机预览流的显示和游戏画面的绘制。 + + +其可通过指定其type字段来实现不同的功能,主要有两个“surface”和“component”字段可供选择。 + + +对于“surface”类型,开发者可将相关数据传入XComponent单独拥有的“surface”来渲染画面。 + + +对于“component”类型则主要用于实现动态加载显示内容的目的。 + + +## surface类型 + +XComponent设置为surface类型时通常用于EGL/OpenGLES和媒体数据写入,并将其显示在XComponent组件上。 + +设置为“surface“类型时XComponent组件可以和其他组件一起进行布局和渲染。 + +同时XComponent又拥有单独的“surface“,可以为开发者在native侧提供native window用来创建EGL/OpenGLES环境,进而使用标准的OpenGL ES开发。 + +除此之外,媒体相关应用(视频、相机等)也可以将相关数据写入XComponent所提供的surface,从而实现呈现相应画面。 + + +## 使用EGL/OpenGLES渲染 + + +### native侧代码开发要点 + +OpenHarmony的应用如果要通过js来桥接native,一般需要使用napi接口来处理js交互,XComponent同样不例外,具体使用请参考[Native API在应用工程中的使用指导](../napi/napi-guidelines.md)。 + +Native侧处理js逻辑的文件类型为so: + +- 每个模块对应一个so + +- so的命名规则为 lib{模块名}.so + + +对于使用XComponent进行标准OpenGL ES开发的场景,CMAKELists.txt文件内容大致如下: + + + +``` +cmake_minimum_required(VERSION 3.4.1) +project(XComponent) # 项目名称 + +set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR}) +# 头文件查找路径 +include_directories(${NATIVERENDER_ROOT_PATH} + ${NATIVERENDER_ROOT_PATH}/include + ) + +# 编译目标so,SHARED表示动态库 +add_library(nativerender SHARED + xxx.cpp + ) + +# 查找相关库 (包括OpenGL ES相关库和XComponent提供的ndk接口) +find_library( EGL-lib + EGL ) + +find_library( GLES-lib + GLESv3 ) + +find_library( libace-lib + ace_ndk.z ) + +# 编译so所需要的依赖 +target_link_libraries(nativerender PUBLIC ${EGL-lib} ${GLES-lib} ${libace-lib} libace_napi.z.so libc++.a) +``` + + +### Napi模块注册 + + +```c++ +static napi_value Init(napi_env env, napi_value exports) +{ + // 定义暴露在模块上的方法 + napi_property_descriptor desc[] ={ + DECLARE_NAPI_FUNCTION("changeColor", PluginRender::NapiChangeColor), + }; + // 通过此接口开发者可在exports上挂载native方法(即上面的PluginRender::NapiChangeColor),exports会通过js引擎绑定到js层的一个js对象 + NAPI_CALL(env, napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc)); + return exports; +} + +static napi_module nativerenderModule = { + .nm_version = 1, + .nm_flags = 0, + .nm_filename = nullptr, + .nm_register_func = Init, // 指定加载对应模块时的回调函数 + .nm_modname = "nativerender", // 指定模块名称,对于XComponent相关开发,这个名称必须和ArkTS侧XComponent中libraryname的值保持一致 + .nm_priv = ((void*)0), + .reserved = { 0 }, +}; + +extern "C" __attribute__((constructor)) void RegisterModule(void) +{ + // 注册so模块 + napi_module_register(&nativerenderModule);c +} +``` + + +### 解析XComponent组件的NativeXComponent实例 + +NativeXComponent为XComponent提供了在native层的实例,可作为js层和native层XComponent绑定的桥梁。XComponent所提供的的NDK接口都依赖于该实例。具体NKD接口可参考[Native XComponent](../reference/native-apis/_o_h___native_x_component.md)。 + + +可以在模块被加载时的回调内(即[Napi模块注册](#napi模块注册)中的Init函数)解析获得NativeXComponent实例 + + + +```c++ +{ + // ... + napi_status status; + napi_value exportInstance = nullptr; + OH_NativeXComponent *nativeXComponent = nullptr; + // 用来解析出被wrap了NativeXComponent指针的属性 + status = napi_get_named_property(env, exports, OH_NATIVE_XCOMPONENT_OBJ, &exportInstance); + if (status != napi_ok) { + return false; + } + // 通过napi_unwrap接口,解析出NativeXComponent的实例指针 + status = napi_unwrap(env, exportInstance, reinterpret_cast(&nativeXComponent)); + // ... +} +``` + + +### 注册XComponent事件回调 + +依赖[解析XComponent组件的NativeXComponent实例](#解析xcomponent组件的nativexcomponent实例)拿到的NativeXComponent指针,通过OH_NativeXComponent_RegisterCallback接口进行回调注册 + + + +```c++ +{ + ... + OH_NativeXComponent *nativeXComponent = nullptr; + // 解析出NativeXComponent实例 + + OH_NativeXComponent_Callback callback; + callback->OnSurfaceCreated = OnSurfaceCreatedCB; // surface创建成功后触发,开发者可以从中获取native window的句柄 + callback->OnSurfaceChanged = OnSurfaceChangedCB; // surface发生变化后触发,开发者可以从中获取native window的句柄以及XComponent的变更信息 + callback->OnSurfaceDestroyed = OnSurfaceDestroyedCB; // surface销毁时触发,开发者可以在此释放资源 + callback->DispatchTouchEvent = DispatchTouchEventCB; // XComponent的touch事件回调接口,开发者可以从中获得此次touch事件的信息 + + OH_NativeXComponent_RegisterCallback(nativeXComponent, callback); + ... +} +``` + + +### 创建EGL/OpenGLES环境 + +在注册的OnSurfaceCreated回调中开发者能拿到native window的句柄(其本质就是XComponent所单独拥有的surface),因此可以在这里创建应用自己的EGL/OpenGLES开发环境,由此开始具体渲染逻辑的开发。 + + +```c++ +EGLCore* eglCore_; // EGLCore为封装了OpenGL相关接口的类 +uint64_t width_; +uint64_t height_; +void OnSurfaceCreatedCB(OH_NativeXComponent* component, void* window) +{ + int32_t ret = OH_NativeXComponent_GetXComponentSize(component, window, &width_, &height_); + if (ret === OH_NATIVEXCOMPONENT_RESULT_SUCCESS) { + eglCore_->GLContextInit(window, width_, height_); // 初始化OpenGL环境 + } +} +``` + + +### ArkTS侧语法介绍 + +开发者在ArkTS侧使用如下代码即可用XComponent组件进行利用EGL/OpenGLES渲染的开发。 + + +```ts +XComponent({ id: 'xcomponentId1', type: 'surface', libraryname: 'nativerender' }) + .onLoad((context) => {}) + .onDestroy(() => {}) +``` + +- id : 与XComponent组件为一一对应关系,不可重复。通常开发者可以在native侧通过OH_NativeXComponent_GetXComponentId接口来获取对应的id从而绑定对应的XComponent。 + +- libraryname:加载模块的名称,必须与在native侧Napi模块注册时nm_modname的名字一致。 + >**说明:** + > + > 应用加载模块实现跨语言调用有两种方式: + > + > 1. 使用NAPI的import方式加载: + > + > ```ts + > import nativerender from "libnativerender.so" + > ``` + > + > 2. 使用XComponent组件加载,本质也是使用了NAPI机制来加载。 + > 该加载方式和import加载方式的区别在于,在加载动态库是会将XComponent的NativeXComponent实例暴露到应用的native层中,从而让开发者可以使用XComponent的NDK接口。 + +- onLoad事件 + - 触发时刻:XComponent准备好surface后触发。 + - 参数context:其上面挂载了暴露在模块上的native方法,使用方法类似于利用 import context2 from "libnativerender.so" 直接加载模块后获得的context2实例。 + - 时序:onLoad事件的触发和Surface相关,其和native侧的OnSurfaceCreated的时序如下图: + + ![图片2](figures/图片2.png) + +- onDestroy事件 + 触发时刻:XComponent组件被销毁时触发与一般ArkUI的组件销毁时机一致,其和native侧的OnSurfaceDestroyed的时序如下图: + + ![图片3](figures/图片3.png) + + +### 媒体数据写入 + +XComponent所持有的Surface符合“生产者-消费者”模型 + +OpenHarmony上Camera、VideoPlayer等符合生产者设计的部件都可以将数据写入XComponent持有的surface并通过XComponent显示。 + +![图片1](figures/图片1.png) + +开发者可通过绑定XComponentController获得对应XComponent的surfaceId(该id可以唯一确定一个surface),从而传给相应的部件接口。 + + +```ts +@State surfaceId:string = ""; +mXComponentController: XComponentController = new XComponentController(); +XComponent({ id: '', type: 'surface', controller: this.mXComponentController }) + .onLoad(() => { + this.surfaceId = this.mXComponentController.getXComponentSurfaceId() + }) +``` + +具体部件接口可参考:[AVPlayer](../reference/apis/js-apis-media.md#avplayer9)、[Camera](../reference/apis/js-apis-camera.md) 等。 + + +### component类型 + +XComponent设置为component类型时通常用于在XComponent内部执行非UI逻辑以实现动态加载显示内容的目的。 + + +>**说明:** +> +> type为"component"时,XComponent作为容器,子组件沿垂直方向布局: +> +> - 垂直方向上对齐格式:[FlexAlign](../reference/arkui-ts/ts-appendix-enums.md#flexalign).Start +> +> - 水平方向上对齐格式:[FlexAlign](../reference/arkui-ts/ts-appendix-enums.md#flexalign).Center +> +> 不支持所有的事件响应。 +> +> 布局方式更改和事件响应均可通过挂载子组件来设置。 +> +> 内部所写的非UI逻辑需要封装在一个或多个函数内。 + + +### 场景示例 + + +```ts +@Builder +function addText(label: string): void { + Text(label) + .fontSize(40) +} + +@Entry +@Component +struct Index { + @State message: string = 'Hello XComponent' + @State messageCommon: string = 'Hello World' + build() { + Row() { + Column() { + XComponent({ id: 'xcomponentId-container', type: 'component' }) { + addText(this.message) + Divider() + .margin(4) + .strokeWidth(2) + .color('#F1F3F5') + .width("80%") + Column() { + Text(this.messageCommon) + .fontSize(30) + } + } + } + .width('100%') + } + .height('100%') + } +} +``` + +![zh-cn_image_0000001511900428](figures/zh-cn_image_0000001511900428.png) diff --git a/zh-cn/application-dev/ui/arkts-common-events-device-input-event.md b/zh-cn/application-dev/ui/arkts-common-events-device-input-event.md new file mode 100644 index 0000000000000000000000000000000000000000..ad0276b9766be22514f96d99b0fcbd6d14309f8f --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-common-events-device-input-event.md @@ -0,0 +1,375 @@ +# 键鼠事件 + + +键鼠事件指键盘,鼠标外接设备的输入事件。 + + +## 鼠标事件 + +支持的鼠标事件包含通过外设鼠标、触控板触发的事件。 + +鼠标事件可触发以下回调: + +| 名称 | 描述 | +| ---------------------------------------- | ---------------------------------------- | +| onHover(event: (isHover: boolean) => void) | 鼠标进入或退出组件时触发该回调。
isHover:表示鼠标是否悬浮在组件上,鼠标进入时为true, 退出时为false。 | +| onMouse(event: (event?: MouseEvent) => void) | 当前组件被鼠标按键点击时或者鼠标在组件上悬浮移动时,触发该回调,event返回值包含触发事件时的时间戳、鼠标按键、动作、鼠标位置在整个屏幕上的坐标和相对于当前组件的坐标。 | + +当组件绑定onHover回调时,可以通过[hoverEffect](../reference/arkui-ts/ts-universal-attributes-hover-effect.md)属性设置该组件的鼠标悬浮态显示效果。 + + + **图1** 鼠标事件数据流   + + +![zh-cn_image_0000001511900504](figures/zh-cn_image_0000001511900504.png) + + +鼠标事件传递到ArkUI之后,会先判断鼠标事件是否是左键的按下/抬起/移动,然后做出不同响应: + + +- 是:鼠标事件先转换成相同位置的触摸事件,执行触摸事件的碰撞测试、手势判断和回调响应。接着去执行鼠标事件的碰撞测试和回调响应。 + +- 否:事件仅用于执行鼠标事件的碰撞测试和回调响应。 + + +>**说明:** +> +>所有单指可响应的触摸事件/手势事件,均可通过鼠标左键来操作和响应。例如当我们需要开发单击Button跳转页面的功能、且需要支持手指点击和鼠标左键点击,那么只绑定一个点击事件(onClick)就可以实现该效果。若需要针对手指和鼠标左键的点击实现不一样的效果,可以在onClick回调中,使用回调参数中的source字段即可判断出当前触发事件的来源是手指还是鼠标。 + + +### onHover + + +```ts +onHover(event: (isHover?: boolean) => void) +``` + + +鼠标悬浮事件回调。参数isHover类型为boolean,表示鼠标进入组件或离开组件。该事件不支持自定义冒泡设置,默认父子冒泡。 + + +若组件绑定了该接口,当鼠标指针从组件外部进入到该组件的瞬间会触发事件回调,参数isHover等于true;鼠标指针离开组件的瞬间也会触发该事件回调,参数isHover等于false。 + + +>**说明:** +> +>事件冒泡:在一个树形结构中,当子节点处理完一个事件后,再将该事件交给它的父节点处理。 + + + + +```ts +// xxx.ets +@Entry +@Component +struct MouseExample { + @State isHovered: boolean = false; + + build() { + Column() { + Button(this.isHovered ? 'Hovered!' : 'Not Hover') + .width(200).height(100) + .backgroundColor(this.isHovered ? Color.Green : Color.Gray) + .onHover((isHover: boolean) => { // 使用onHover接口监听鼠标是否悬浮在Button组件上 + this.isHovered = isHover; + }) + }.width('100%').height('100%').justifyContent(FlexAlign.Center) + } +} +``` + + +该示例创建了一个Button组件,初始背景色为灰色,内容为“Not Hover”。示例中的Button组件绑定了onHover回调,在该回调中将this.isHovered变量置为回调参数:isHover。 + + +当鼠标从Button外移动到Button内的瞬间,回调响应,isHover值等于true,isHovered的值变为true,将组件的背景色改成Color.Green,内容变为“Hovered!”。 + + +当鼠标从Button内移动到Button外的瞬间,回调响应,isHover值等于false,又将组件变成了初始的样式。 + + +![onHover](figures/onHover.gif) + + +### onMouse + + +```ts +onMouse(event: (event?: MouseEvent) => void) +``` + + +鼠标事件回调。绑定该API的组件每当鼠标指针在该组件内产生行为(MouseAction)时,触发事件回调,参数为[MouseEvent](../reference/arkui-ts/ts-universal-mouse-key.md)对象,表示触发此次的鼠标事件。该事件支持自定义冒泡设置,默认父子冒泡。常见用于开发者自定义的鼠标行为逻辑处理。 + + +开发者可以通过回调中的MouseEvent对象获取触发事件的坐标(screenX/screenY/x/y)、按键([MouseButton](../reference/arkui-ts/ts-appendix-enums.md#mousebutton))、行为([MouseAction](../reference/arkui-ts/ts-appendix-enums.md#mouseaction))、时间戳(timestamp)、交互组件的区域([EventTarget](../reference/arkui-ts/ts-universal-events-click.md))、事件来源([SourceType](../reference/arkui-ts/ts-gesture-settings.md))等。MouseEvent的回调函数stopPropagation用于设置当前事件是否阻止冒泡。 + + +>**说明:** +> +>按键(MouseButton)的值:Left/Right/Middle/Back/Forward 均对应鼠标上的实体按键,当这些按键被按下或松开时触发这些按键的事件。None表示无按键,会出现在鼠标没有按键按下或松开的状态下,移动鼠标所触发的事件中。 + + + +```ts +// xxx.ets +@Entry +@Component +struct MouseExample { + @State isHovered: boolean = false; + @State buttonText: string = ''; + @State columnText: string = ''; + + build() { + Column() { + Button(this.isHovered ? 'Hovered!' : 'Not Hover') + .width(200) + .height(100) + .backgroundColor(this.isHovered ? Color.Green : Color.Gray) + .onHover((isHover: boolean) => { + this.isHovered = isHover + }) + .onMouse((event: MouseEvent) => { // 给Button组件设置onMouse回调 + this.buttonText = 'Button onMouse:\n' + '' + + 'button = ' + event.button + '\n' + + 'action = ' + event.action + '\n' + + 'x,y = (' + event.x + ',' + event.y + ')' + '\n' + + 'screenXY=(' + event.screenX + ',' + event.screenY + ')'; + }) + Divider() + Text(this.buttonText).fontColor(Color.Green) + Divider() + Text(this.columnText).fontColor(Color.Red) + } + .width('100%') + .height('100%') + .justifyContent(FlexAlign.Center) + .borderWidth(2) + .borderColor(Color.Red) + .onMouse((event: MouseEvent) => { // 给Column组件设置onMouse回调 + this.columnText = 'Column onMouse:\n' + '' + + 'button = ' + event.button + '\n' + + 'action = ' + event.action + '\n' + + 'x,y = (' + event.x + ',' + event.y + ')' + '\n' + + 'screenXY=(' + event.screenX + ',' + event.screenY + ')'; + }) + } +} +``` + + +在onHover示例的基础上,给Button绑定onMouse接口。在回调中,打印出鼠标事件的button/action等回调参数值。同时,在外层的Column容器上,也做相同的设置。整个过程可以分为以下两个动作: + + +1. 移动鼠标:当鼠标从Button外部移入Button的过程中,仅触发了Column的onMouse回调;当鼠标移入到Button内部后,由于onMouse事件默认是冒泡的,所以此时会同时响应Column的onMouse回调和Button的onMouse回调。此过程中,由于鼠标仅有移动动作没有点击动作,因此打印信息中的button均为0(MouseButton.None的枚举值)、action均为3(MouseAction.Move的枚举值)。 + +2. 点击鼠标:鼠标进入Button后进行了2次点击,分别是左键点击和右键点击。 + 左键点击时:button = 1(MouseButton.Left的枚举值),按下时 action = 1(MouseAction.Press的枚举值),抬起时 action = 2(MouseAction.Release的枚举值)。 + + 右键点击时:button = 2(MouseButton.Right的枚举值),按下时 action = 1(MouseAction.Press的枚举值),抬起时 action = 2(MouseAction.Release的枚举值)。 + + +![onMouse1](figures/onMouse1.gif) + + +如果需要阻止鼠标事件冒泡,可以通过调用stopPropagation()方法进行设置。 + + + +```ts +Button(this.isHovered ? 'Hovered!' : 'Not Hover') + .width(200) + .height(100) + .backgroundColor(this.isHovered ? Color.Green : Color.Gray) + .onHover((isHover: boolean) => { + this.isHovered = isHover; + }) + .onMouse((event: MouseEvent) => { + event.stopPropagation(); // 在Button的onMouse事件中设置阻止冒泡 + this.buttonText = 'Button onMouse:\n' + '' + + 'button = ' + event.button + '\n' + + 'action = ' + event.action + '\n' + + 'x,y = (' + event.x + ',' + event.y + ')' + '\n' + + 'screenXY=(' + event.screenX + ',' + event.screenY + ')'; + }) +``` + + +在子组件(Button)的onMouse中,通过回调参数event调用stopPropagation回调方法(如下)即可阻止Button子组件的鼠标事件冒泡到父组件Column上。 + + + +```ts +event.stopPropagation() +``` + + +效果是:当鼠标在Button组件上操作时,仅Button的onMouse回调会响应,Column的onMouse回调不会响应。 + + +### hoverEffect + + +```ts +hoverEffect(value: HoverEffect) +``` + + +鼠标悬浮态效果设置的通用属性。参数类型为HoverEffect,HoverEffect提供的Auto、Scale、Highlight效果均为固定效果,开发者无法自定义设置效果参数。 + + + **表1** HoverEffect说明 + +| HoverEffect枚举值 | 效果说明 | +| -------------- | ---------------------------------------- | +| Auto | 组件默认提供的悬浮态效果,由各组件定义。 | +| Scale | 动画播放方式,鼠标悬浮时:组件大小从100%放大至105%,鼠标离开时:组件大小从105%缩小至100%。 | +| Highlight | 动画播放方式,鼠标悬浮时:组件背景色叠加一个5%透明度的白色,视觉效果是组件的原有背景色变暗,鼠标离开时:组件背景色恢复至原有样式。 | +| None | 禁用悬浮态效果 | + + + +```ts +// xxx.ets +@Entry +@Component +struct HoverExample { + build() { + Column({ space: 10 }) { + Button('Auto') + .width(170).height(70) + Button('Scale') + .width(170).height(70) + .hoverEffect(HoverEffect.Scale) + Button('Highlight') + .width(170).height(70) + .hoverEffect(HoverEffect.Highlight) + Button('None') + .width(170).height(70) + .hoverEffect(HoverEffect.None) + }.width('100%').height('100%').justifyContent(FlexAlign.Center) + } +} +``` + + +![hoverEffect](figures/hoverEffect.gif) + + +Button默认的悬浮态效果就是缩放效果,因此Auto和Scale的效果一样,Highlight会使背板颜色变暗,None会禁用悬浮态效果。 + + +## 按键事件 + + **图2** 按键事件数据流   + +![zh-cn_image_0000001511580944](figures/zh-cn_image_0000001511580944.png) + + +按键事件由外设键盘等设备触发,经驱动和多模处理转换后发送给当前获焦的窗口。窗口获取到事件后,会先给输入法分发(输入法会消费按键用作输入),若输入法未消费该按键事件,才会将事件发给ArkUI框架。因此,当某输入框组件获焦,且打开了输入法,此时大部分按键事件均会被输入法消费,例如字母键会被输入法用来往输入框中输入对应字母字符、方向键会被输入法用来切换选中备选词。 + + +按键事件到ArkUI框架之后,会先找到完整的父子节点获焦链。从叶子节点到根节点,逐一发送按键事件。 + + +### onKeyEvent + + +```ts +onKeyEvent(event: (event?: KeyEvent) => void) +``` + + +按键事件回调,当绑定该方法的组件处于[获焦状态](arkts-common-events-focus-event.md)下,外设键盘的按键事件会触发该API的回调响应,回调参数为[KeyEvent](../reference/arkui-ts/ts-universal-events-key.md),可由该参数获得当前按键事件的按键行为([KeyType](../reference/arkui-ts/ts-appendix-enums.md#keytype))、键码([keyCode](../reference/apis/js-apis-keycode.md))、按键英文名称(keyText)、事件来源设备类型([KeySource](../reference/arkui-ts/ts-appendix-enums.md#keysource))、事件来源设备id(deviceId)、元键按压状态(metaKey)、时间戳(timestamp)、阻止冒泡设置(stopPropagation)。 + + + +```ts +// xxx.ets +@Entry +@Component +struct KeyEventExample { + @State buttonText: string = ''; + @State buttonType: string = ''; + @State columnText: string = ''; + @State columnType: string = ''; + + build() { + Column() { + Button('onKeyEvent') + .width(140).height(70) + .onKeyEvent((event: KeyEvent) => { // 给Button设置onKeyEvent事件 + if (event.type === KeyType.Down) { + this.buttonType = 'Down'; + } + if (event.type === KeyType.Up) { + this.buttonType = 'Up'; + } + this.buttonText = 'Button: \n' + + 'KeyType:' + this.buttonType + '\n' + + 'KeyCode:' + event.keyCode + '\n' + + 'KeyText:' + event.keyText; + }) + + Divider() + Text(this.buttonText).fontColor(Color.Green) + + Divider() + Text(this.columnText).fontColor(Color.Red) + }.width('100%').height('100%').justifyContent(FlexAlign.Center) + .onKeyEvent((event: KeyEvent) => { // 给父组件Column设置onKeyEvent事件 + if (event.type === KeyType.Down) { + this.columnType = 'Down'; + } + if (event.type === KeyType.Up) { + this.columnType = 'Up'; + } + this.columnText = 'Column: \n' + + 'KeyType:' + this.buttonType + '\n' + + 'KeyCode:' + event.keyCode + '\n' + + 'KeyText:' + event.keyText; + }) + } +} +``` + + +上述示例中给组件Button和其父容器Column绑定onKeyEvent。应用打开页面加载后,组件树上第一个可获焦的非容器组件自动获焦,该应用只有一个Button组件,因此该组件会自动获焦,由于Button是Column的子节点,Button获焦也同时意味着Column获焦。获焦机制见[焦点事件](arkts-common-events-focus-event.md)。 + + +![zh-cn_image_0000001511421324](figures/zh-cn_image_0000001511421324.gif) + + +打开应用后,依次在键盘上按这些按键:“空格、回车、左Ctrl、左Shift、字母A、字母Z”。 + + +1. 由于onKeyEvent事件默认是冒泡的,所以Button和Column的onKeyEvent都可以响应。 + +2. 每个按键都有2次回调,分别对应KeyType.Down和KeyType.Up,表示按键被按下、然后抬起。 + + +如果要阻止冒泡,即仅Button响应键盘事件,Column不响应,在Button的onKeyEvent回调中加入event.stopPropagation()方法即可,如下: + + + +```ts +Button('onKeyEvent') + .width(140).height(70) + .onKeyEvent((event: KeyEvent) => { + // 通过stopPropagation阻止事件冒泡 + event.stopPropagation(); + if (event.type === KeyType.Down) { + this.buttonType = 'Down'; + } + if (event.type === KeyType.Up) { + this.buttonType = 'Up'; + } + this.buttonText = 'Button: \n' + + 'KeyType:' + this.buttonType + '\n' + + 'KeyCode:' + event.keyCode + '\n' + + 'KeyText:' + event.keyText; +}) +``` + + +![zh-cn_image_0000001511900508](figures/zh-cn_image_0000001511900508.gif) diff --git a/zh-cn/application-dev/ui/arkts-common-events-focus-event.md b/zh-cn/application-dev/ui/arkts-common-events-focus-event.md new file mode 100644 index 0000000000000000000000000000000000000000..8ef2f46380c727821a24c0bc4ad508b9657ceda1 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-common-events-focus-event.md @@ -0,0 +1,1055 @@ +# 焦点事件(毕雪峰 00579046) + + +## 基本概念 + +- 焦点 + 指向当前应用界面上唯一的一个可交互元素,当用户使用键盘、电视遥控器、车机摇杆/旋钮等非指向性输入设备与应用程序进行间接交互时,基于焦点的导航和交互是重要的输入手段。 + +- 默认焦点 + 应用打开或切换页面后,若当前页上存在可获焦的组件,则树形结构的组件树中第一个可获焦的组件默认获得焦点。可以使用[自定义默认焦点](#自定义默认焦点)进行自定义指定。 + +- 获焦 + 指组件获得了焦点,同一时刻,应用中最多只有1个末端组件是获焦的,且此时它的所有祖宗组件(整个组件链)均是获焦的。当期望某个组件获焦,须确保该组件及其所有的祖宗节点均是可获焦的([focusable](#设置组件是否获焦)属性为true)。 + +- 失焦 + 指组件从获焦状态变成了非获焦状态,失去了焦点。组件失焦时,它的所有祖宗组件(失焦组件链)与新的获焦组件链不相同的节点都会失焦。 + +- 走焦 + 表示焦点在当前应用中转移的过程,走焦会带来原焦点组件的失焦和新焦点组件的获焦。应用中焦点发生变化的方式按行为可分为两类: + + - 主动走焦:指开发者/用户主观的行为导致焦点移动,包含:外接键盘上按下TAB/方向键、使用[requestFocus](#focuscontrolrequestfocus)主动给指定组件申请焦点、组件[focusOnTouch](#focusontouch)属性为true后点击组件。 + - 被动走焦:指组件焦点因其他操作被动的转移焦点,此特性为焦点系统默认行为,无法由开发者自由设定,例如当使用if-else语句将处于获焦的组件删除/将处于获焦的组件(或其父组件)置成不可获焦时、当页面切换时。 + +- 焦点态 + 获焦组件的样式,不同组件的焦点态样式大同小异,默认情况下焦点态不显示,仅使用外接键盘按下TAB键/方向键时才会触发焦点态样式出现。首次触发焦点态显示的TAB键/方向键不会触发走焦。当应用接收到点击事件时(包括手指触屏的按下事件和鼠标左键的按下事件),自动隐藏焦点态样式。焦点态样式由后端组件定义,开发者无法修改。 + + +## 走焦规则 + +走焦规则是指用户使用“TAB键/SHIFT+TAB键/方向键”主动进行走焦,或焦点系统在执行被动走焦时的顺序规则。组件的走焦规则默认由走焦系统定义,由焦点所在的容器决定。 + +- 线性走焦:常见的容器有Flex、Row、Column、List,这些都是典型的单方向容器,组件在这些容器内的排列都是线性的,那么走焦规则也是线性的。走焦的方向和方向键的方向一致。 + + **图1** 线性走焦示意图   + + ![zh-cn_image_0000001562700537](figures/zh-cn_image_0000001562700537.png) + + 例如Row容器,使用方向键左右(←/→)即可将焦点在相邻的2个可获焦组件之间来回切换。 + +- 十字走焦:使用方向键上(↑)下(↓)左(←)右(→)可以使焦点在相邻的组件上切换。典型的是Grid容器,如下图: + + **图2** Grid组件十字走焦示意图   + + ![zh-cn_image_0000001511740580](figures/zh-cn_image_0000001511740580.png) + + >**说明:** + > - TAB/SHIFT+TAB键在以上两种走焦规则上的功能和方向键一致。TAB键等同于“先执行方向键右,若无法走焦,再执行方向键下”,SHIFT+TAB键等同于“先执行方向键左,若无法走焦,再执行方向键上”。 + > + > - 触发走焦的按键是按下的事件(DOWN事件)。 + > + > - 删除组件、设置组件无法获焦后,会使用线性走焦规则,自动先往被删除/Unfocusable组件的前置兄弟组件上走焦,无法走焦的话,再忘后置兄弟组件上走焦。 + +- tabIndex走焦:给组件设置[tabIndex](../reference/arkui-ts/ts-universal-attributes-focus.md)通用属性,自定义组件的TAB键/SHIFT+TAB键的走焦顺序。 + +- 区域走焦:给容器组件设置tabIndex通用属性,再结合[groupDefaultFocus](#groupdefaultfocus)通用属性,自定义容器区域的TAB键/SHIFT+TAB键的走焦顺序和默认获焦组件。 + +- 走焦至容器组件规则:当焦点走焦到容器(该容器没有配置groupDefaultFocus)上时,若该容器组件为首次获焦,则会先计算目标容器组件的子组件的区域位置,得到距离目标容器中心点最近的子组件,焦点会走到目标容器上的该子组件上。若该容器非首次获焦,焦点会自动走焦到上一次目标容器中获焦的子组件。 + +- 焦点交互:当某组件获焦时,该组件的固有点击任务或开发者绑定的onClick回调任务,会自动挂载到空格/回车按键上,当按下按键时,任务就和手指/鼠标点击一样被执行。 + + +>**说明:** +> +>本文涉及到的焦点均为组件焦点,另外一个焦点的概念是:窗口焦点,指向当前获焦的窗口。当窗口失焦时,该窗口应用中的所有获焦组件全部失焦。 + + +## 监听组件的焦点变化 + + +```ts +onFocus(event: () => void) +``` + + +获焦事件回调,绑定该API的组件获焦时,回调响应。 + + + +```ts +onBlur(event:() => void) +``` + + +失焦事件回调,绑定该API的组件失焦时,回调响应。 + + +onFocus和onBlur两个接口通常成对使用,来监听组件的焦点变化。 + + +以下示例代码展示获焦/失焦回调的使用方法: + + + +```ts +// xxx.ets +@Entry +@Component +struct FocusEventExample { + @State oneButtonColor: Color = Color.Gray; + @State twoButtonColor: Color = Color.Gray; + @State threeButtonColor: Color = Color.Gray; + + build() { + Column({ space: 20 }) { + // 通过外接键盘的上下键可以让焦点在三个按钮间移动,按钮获焦时颜色变化,失焦时变回原背景色 + Button('First Button') + .width(260) + .height(70) + .backgroundColor(this.oneButtonColor) + .fontColor(Color.Black) + // 监听第一个组件的获焦事件,获焦后改变颜色 + .onFocus(() => { + this.oneButtonColor = Color.Green; + }) + // 监听第一个组件的失焦事件,失焦后改变颜色 + .onBlur(() => { + this.oneButtonColor = Color.Gray; + }) + + Button('Second Button') + .width(260) + .height(70) + .backgroundColor(this.twoButtonColor) + .fontColor(Color.Black) + // 监听第二个组件的获焦事件,获焦后改变颜色 + .onFocus(() => { + this.twoButtonColor = Color.Green; + }) + // 监听第二个组件的失焦事件,失焦后改变颜色 + .onBlur(() => { + this.twoButtonColor = Color.Grey; + }) + + Button('Third Button') + .width(260) + .height(70) + .backgroundColor(this.threeButtonColor) + .fontColor(Color.Black) + // 监听第三个组件的获焦事件,获焦后改变颜色 + .onFocus(() => { + this.threeButtonColor = Color.Green; + }) + // 监听第三个组件的失焦事件,失焦后改变颜色 + .onBlur(() => { + this.threeButtonColor = Color.Gray ; + }) + }.width('100%').margin({ top: 20 }) + } +} +``` + + +![zh-cn_image_0000001511740584](figures/zh-cn_image_0000001511740584.gif) + + +上述示例包含以下4步: + + +1. 应用打开时,“First Button”默认获取焦点,onFocus回调响应,背景色变成绿色。 + +2. 按下TAB键(或方向键下↓),“First Button”显示焦点态样式:组件外围有一个蓝色的闭合框。不触发走焦,焦点仍然在“First Button”上。 + +3. 按下TAB键(或方向键下↓),触发走焦,“Second Button”获焦,onFocus回调响应,背景色变成绿色;“First Button”失焦、onBlur回调响应,背景色变回灰色。 + +4. 按下TAB键(或方向键下↓),触发走焦,“Third Button”获焦,onFocus回调响应,背景色变成绿色;“Second Button”失焦、onBlur回调响应,背景色变回灰色。 + + +## 设置组件是否获焦 + +通过focusable接口设置组件是否可获焦: + + +```ts +focusable(value: boolean) +``` + +按照组件的获焦能力可大致分为三类: + +- 默认可获焦的组件,通常是有交互行为的组件,例如Button、Checkbox,TextInput组件,此类组件无需设置任何属性,默认即可获焦。 + +- 有获焦能力,但默认不可获焦的组件,典型的是Text、Image组件,此类组件缺省情况下无法获焦,若需要使其获焦,可使用通用属性focusable(true)使能。 + +- 无获焦能力的组件,通常是无任何交互行为的展示类组件,例如Blank、Circle组件,此类组件即使使用focusable属性也无法使其可获焦。 + + +>**说明:** +> - focusable为false表示组件不可获焦,同样可以使组件变成不可获焦的还有通用属性[enabled](../reference/arkui-ts/ts-universal-attributes-enable.md)。 +> +> - 当某组件处于获焦状态时,将其的focusable属性或enabled属性设置为false,会自动使该组件失焦,然后焦点按照[走焦规则](#走焦规则)将焦点转移给其他组件。 + + **表1** 基础组件获焦能力 + +| 基础组件 | 是否有获焦能力 | focusable默认值 | 走焦规则 | +| ---------------------------------------- | ------- | ------------ | -------- | +| [AlphabetIndexer](../reference/arkui-ts/ts-container-alphabet-indexer.md) | 是 | true | 线性走焦 | +| [Blank](../reference/arkui-ts/ts-basic-components-blank.md) | 否 | false | / | +| [Button](../reference/arkui-ts/ts-basic-components-button.md) | 是 | true | / | +| [Checkbox](../reference/arkui-ts/ts-basic-components-checkbox.md) | 是 | true | / | +| [CheckboxGroup](../reference/arkui-ts/ts-basic-components-checkboxgroup.md) | 是 | true | / | +| [DataPanel](../reference/arkui-ts/ts-basic-components-datapanel.md) | 否 | false | / | +| [DatePicker](../reference/arkui-ts/ts-basic-components-datepicker.md) | 是 | true | 线性走焦 | +| [Divider](../reference/arkui-ts/ts-basic-components-divider.md) | 否 | false | / | +| [Formcomponent](../reference/arkui-ts/ts-basic-components-formcomponent.md) | 否 | false | / | +| [Gauge](../reference/arkui-ts/ts-basic-components-gauge.md) | 否 | false | / | +| [Image](../reference/arkui-ts/ts-basic-components-image.md) | 是 | false | / | +| [ImageAnimator](../reference/arkui-ts/ts-basic-components-imageanimator.md) | 是 | false | / | +| [LoadingProgress](../reference/arkui-ts/ts-basic-components-loadingprogress.md) | 否 | false | / | +| [Marquee](../reference/arkui-ts/ts-basic-components-marquee.md) | 否 | false | / | +| [Menu](../reference/arkui-ts/ts-basic-components-menu.md) | 是 | true | 线性走焦 | +| [MenuItem](../reference/arkui-ts/ts-basic-components-menuitem.md) | 是 | true | / | +| [MenuItemGroup](../reference/arkui-ts/ts-basic-components-menuitemgroup.md) | 是 | true | 线性走焦 | +| [Navigation](../reference/arkui-ts/ts-basic-components-navigation.md) | 否 | false | 组件自定义 | +| [NavRouter](../reference/arkui-ts/ts-basic-components-navrouter.md) | 否 | false | 跟随子容器 | +| [NavDestination](../reference/arkui-ts/ts-basic-components-navdestination.md) | 否 | false | 线性走焦 | +| [PatternLock](../reference/arkui-ts/ts-basic-components-patternlock.md) | 否 | false | / | +| [PluginComponent](../reference/arkui-ts/ts-basic-components-plugincomponent.md) | 否 | false | / | +| [Progress](../reference/arkui-ts/ts-basic-components-progress.md) | 否 | false | / | +| [QRCode](../reference/arkui-ts/ts-basic-components-qrcode.md) | 否 | false | / | +| [Radio](../reference/arkui-ts/ts-basic-components-radio.md) | 是 | true | / | +| [Rating](../reference/arkui-ts/ts-basic-components-rating.md) | 是 | true | / | +| [RemoteWindow](../reference/arkui-ts/ts-basic-components-remotewindow.md) | 否 | false | / | +| [RichText](../reference/arkui-ts/ts-basic-components-richtext.md) | 否 | false | / | +| [ScrollBar](../reference/arkui-ts/ts-basic-components-scrollbar.md) | 否 | false | / | +| [Search](../reference/arkui-ts/ts-basic-components-search.md) | 是 | true | / | +| [Select](../reference/arkui-ts/ts-basic-components-select.md) | 是 | true | 线性走焦 | +| [Slider](../reference/arkui-ts/ts-basic-components-slider.md) | 是 | true | / | +| [Span](../reference/arkui-ts/ts-basic-components-span.md) | 否 | false | / | +| [Stepper](../reference/arkui-ts/ts-basic-components-stepper.md) | 是 | true | / | +| [StepperItem](../reference/arkui-ts/ts-basic-components-stepperitem.md) | 是 | true | / | +| [Text](../reference/arkui-ts/ts-basic-components-text.md) | 是 | false | / | +| [TextArea](../reference/arkui-ts/ts-basic-components-textarea.md) | 是 | true | / | +| [TextClock](../reference/arkui-ts/ts-basic-components-textclock.md) | 否 | false | / | +| [TextInput](../reference/arkui-ts/ts-basic-components-textinput.md) | 是 | true | / | +| [TextPicker](../reference/arkui-ts/ts-basic-components-textpicker.md) | 是 | true | 线性走焦 | +| [TextTimer](../reference/arkui-ts/ts-basic-components-texttimer.md) | 否 | false | / | +| [TimePicker](../reference/arkui-ts/ts-basic-components-timepicker.md) | 是 | true | 线性走焦 | +| [Toggle](../reference/arkui-ts/ts-basic-components-toggle.md) | 是 | true | / | +| [Web](../reference/arkui-ts/ts-basic-components-web.md) | 是 | true | Web组件自定义 | +| [XComponent](../reference/arkui-ts/ts-basic-components-xcomponent.md) | 否 | false | / | + + **表2** 容器组件获焦能力 + +| 容器组件 | 是否可获焦 | focusable默认值 | 走焦规则 | +| ---------------------------------------- | ----- | ------------ | -------- | +| [AbilityComponent](../reference/arkui-ts/ts-container-ability-component.md) | 否 | false | / | +| [Badge](../reference/arkui-ts/ts-container-badge.md) | 否 | false | / | +| [Column](../reference/arkui-ts/ts-container-column.md) | 是 | true | 线性走焦 | +| [ColumnSplit](../reference/arkui-ts/ts-container-columnsplit.md) | 是 | true | / | +| [Counter](../reference/arkui-ts/ts-container-counter.md) | 是 | true | 线性走焦 | +| [Flex](../reference/arkui-ts/ts-container-flex.md) | 是 | true | 线性走焦 | +| [GridCol](../reference/arkui-ts/ts-container-gridcol.md) | 是 | true | 容器组件自定义 | +| [GridRow](../reference/arkui-ts/ts-container-gridrow.md) | 是 | true | 容器组件自定义 | +| [Grid](../reference/arkui-ts/ts-container-grid.md) | 是 | true | 容器组件自定义 | +| [GridItem](../reference/arkui-ts/ts-container-griditem.md) | 是 | true | 跟随子组件 | +| [List](../reference/arkui-ts/ts-container-list.md) | 是 | true | 线性走焦 | +| [ListItem](../reference/arkui-ts/ts-container-listitem.md) | 是 | true | 跟随子组件 | +| [ListItemGroup](../reference/arkui-ts/ts-container-listitemgroup.md) | 是 | true | 跟随List组件 | +| [Navigator](../reference/arkui-ts/ts-container-navigator.md) | 否 | true | 容器组件自定义 | +| [Panel](../reference/arkui-ts/ts-container-panel.md) | 否 | true | 跟随子组件 | +| [Refresh](../reference/arkui-ts/ts-container-refresh.md) | 否 | false | / | +| [RelativeContainer](../reference/arkui-ts/ts-container-relativecontainer.md) | 否 | true | 容器组件自定义 | +| [Row](../reference/arkui-ts/ts-container-row.md) | 是 | true | 线性走焦 | +| [RowSplit](../reference/arkui-ts/ts-container-rowsplit.md) | 是 | true | / | +| [Scroll](../reference/arkui-ts/ts-container-scroll.md) | 是 | true | 线性走焦 | +| [SideBarContainer](../reference/arkui-ts/ts-container-sidebarcontainer.md) | 是 | true | 线性走焦 | +| [Stack](../reference/arkui-ts/ts-container-stack.md) | 是 | true | 线性走焦 | +| [Swiper](../reference/arkui-ts/ts-container-swiper.md) | 是 | true | 容器组件自定义 | +| [Tabs](../reference/arkui-ts/ts-container-tabs.md) | 是 | true | 容器组件自定义 | +| [TabContent](../reference/arkui-ts/ts-container-tabcontent.md) | 是 | true | 跟随子组件 | + + **表3** 媒体组件获焦能力 + +| 媒体组件 | 是否可获焦 | focusable默认值 | 走焦规则 | +| ---------------------------------------- | ----- | ------------ | ---- | +| [Video](../reference/arkui-ts/ts-media-components-video.md) | 是 | true | / | + + **表4** 画布组件获焦能力 + +| 画布组件 | 是否可获焦 | focusable默认值 | 走焦规则 | +| ---------------------------------------- | ----- | ------------ | ---- | +| [Canvas](../reference/arkui-ts/ts-components-canvas-canvas.md) | 否 | false | / | + + +以下示例为大家展示focusable接口的使用方法: + + + +```ts +// xxx.ets +@Entry +@Component +struct FocusableExample { + @State textFocusable: boolean = true; + @State color1: Color = Color.Yellow; + @State color2: Color = Color.Yellow; + + build() { + Column({ space: 5 }) { + Text('Default Text') // 第一个Text组件未设置focusable属性,默认不可获焦 + .borderColor(this.color1) + .borderWidth(2) + .width(300) + .height(70) + .onFocus(() => { + this.color1 = Color.Blue; + }) + .onBlur(() => { + this.color1 = Color.Yellow; + }) + Divider() + + Text('focusable: ' + this.textFocusable) // 第二个Text设置了focusable属性,初始值为true + .borderColor(this.color2) + .borderWidth(2) + .width(300) + .height(70) + .focusable(this.textFocusable) + .onFocus(() => { + this.color2 = Color.Blue; + }) + .onBlur(() => { + this.color2 = Color.Yellow; + }) + + Divider() + + Row() { + Button('Button1') + .width(140).height(70) + Button('Button2') + .width(160).height(70) + } + + Divider() + Button('Button3') + .width(300).height(70) + + Divider() + }.width('100%').justifyContent(FlexAlign.Center) + .onKeyEvent((e) => { // 绑定onKeyEvent,在该Column组件获焦时,按下'F'键,可将第二个Text的focusable置反 + if (e.keyCode === 2022 && e.type === KeyType.Down) { + this.textFocusable = !this.textFocusable; + } + }) + } +} +``` + + +运行效果: + + +![zh-cn_image_0000001511900540](figures/zh-cn_image_0000001511900540.gif) + + +上述示例包含默认获焦和主动走焦两部分: + + +**默认获焦:** + + +- 根据默认焦点的说明,该应用打开后,默认第一个可获焦元素获焦: + +- 第一个Text组件没有设置focusable(true)属性,该Text组件无法获焦。 + +- 第二个Text组件的focusable属性显式设置为true,说明该组件可获焦,那么默认焦点将置到它身上。 + + +**主动走焦:** + + +按键盘F键,触发onKeyEvent,focusable置为false,Text组件变成不可获焦,焦点自动转移,按照被动走焦中的说明项,焦点会自动从Text组件先向上寻找下一个可获焦组件,由于上一个组件是一个不可获焦的Text,所以向下寻找下一个可获焦的组件,找到并使焦点转移到Row容器上,根据[走焦至容器规则](#走焦规则),计算Button1和Button2的位置,Button2比Button1更大,因此焦点会自动转移到Button2上。 + + +## 自定义默认焦点 + + +```ts +defaultFocus(value: boolean) +``` + +焦点系统在页面初次构建完成时,会搜索当前页下的所有组件,找到第一个绑定了defaultFocus(true)的组件,然后将该组件置为默认焦点,若无任何组件绑定defaultFocus(true),则将第一个找到的可获焦的组件置为默认焦点。 + +以如下应用为例,应用布局如下: + +![zh-cn_image_0000001563060793](figures/zh-cn_image_0000001563060793.png) + +以下是实现该应用的示例代码,且示例代码中没有设置defaultFocus: + + +```ts +// xxx.ets +import promptAction from '@ohos.promptAction'; + +class MyDataSource implements IDataSource { + private list: number[] = []; + private listener: DataChangeListener; + + constructor(list: number[]) { + this.list = list; + } + + totalCount(): number { + return this.list.length; + } + + getData(index: number): any { + return this.list[index]; + } + + registerDataChangeListener(listener: DataChangeListener): void { + this.listener = listener; + } + + unregisterDataChangeListener() { + } +} + +@Entry +@Component +struct SwiperExample { + private swiperController: SwiperController = new SwiperController() + private data: MyDataSource = new MyDataSource([]) + + aboutToAppear(): void { + let list = [] + for (let i = 1; i <= 4; i++) { + list.push(i.toString()); + } + this.data = new MyDataSource(list); + } + + build() { + Column({ space: 5 }) { + Swiper(this.swiperController) { + LazyForEach(this.data, (item: string) => { + Row({ space: 20 }) { + Column() { + Button('1').width(200).height(200) + .fontSize(40) + .backgroundColor('#dadbd9') + } + + Column({ space: 20 }) { + Row({ space: 20 }) { + Button('2') + .width(100) + .height(100) + .fontSize(40) + .type(ButtonType.Normal) + .borderRadius(20) + .backgroundColor('#dadbd9') + Button('3') + .width(100) + .height(100) + .fontSize(40) + .type(ButtonType.Normal) + .borderRadius(20) + .backgroundColor('#dadbd9') + } + + Row({ space: 20 }) { + Button('4') + .width(100) + .height(100) + .fontSize(40) + .type(ButtonType.Normal) + .borderRadius(20) + .backgroundColor('#dadbd9') + Button('5') + .width(100) + .height(100) + .fontSize(40) + .type(ButtonType.Normal) + .borderRadius(20) + .backgroundColor('#dadbd9') + } + + Row({ space: 20 }) { + Button('6') + .width(100) + .height(100) + .fontSize(40) + .type(ButtonType.Normal) + .borderRadius(20) + .backgroundColor('#dadbd9') + Button('7') + .width(100) + .height(100) + .fontSize(40) + .type(ButtonType.Normal) + .borderRadius(20) + .backgroundColor('#dadbd9') + } + } + } + .width(480) + .height(380) + .justifyContent(FlexAlign.Center) + .borderWidth(2) + .borderColor(Color.Gray) + .backgroundColor(Color.White) + }, item => item) + } + .cachedCount(2) + .index(0) + .interval(4000) + .indicator(true) + .loop(true) + .duration(1000) + .itemSpace(0) + .curve(Curve.Linear) + .onChange((index: number) => { + console.info(index.toString()); + }) + .margin({ left: 20, top: 20, right: 20 }) + + Row({ space: 40 }) { + Button('←') + .fontSize(40) + .fontWeight(FontWeight.Bold) + .fontColor(Color.Black) + .backgroundColor(Color.Transparent) + .onClick(() => { + this.swiperController.showPrevious(); + }) + Button('→') + .fontSize(40) + .fontWeight(FontWeight.Bold) + .fontColor(Color.Black) + .backgroundColor(Color.Transparent) + .onClick(() => { + this.swiperController.showNext(); + }) + } + .width(480) + .height(50) + .justifyContent(FlexAlign.Center) + .borderWidth(2) + .borderColor(Color.Gray) + .backgroundColor('#f7f6dc') + + Row({ space: 40 }) { + Button('Cancel') + .fontSize(30) + .fontColor('#787878') + .type(ButtonType.Normal) + .width(140) + .height(50) + .backgroundColor('#dadbd9') + + Button('OK') + .fontSize(30) + .fontColor('#787878') + .type(ButtonType.Normal) + .width(140) + .height(50) + .backgroundColor('#dadbd9') + .onClick(() => { + promptAction.showToast({ message: 'Button OK on clicked' }); + }) + } + .width(480) + .height(80) + .justifyContent(FlexAlign.Center) + .borderWidth(2) + .borderColor(Color.Gray) + .backgroundColor('#dff2e4') + .margin({ left: 20, bottom: 20, right: 20 }) + }.backgroundColor('#f2f2f2') + .margin({ left: 50, top: 50, right: 20 }) + } +} +``` + + +当前应用上无任何defaultFocus设置,所以第一个可获焦的组件默认获取焦点,按下TAB键/方向键让获焦的组件显示焦点态样式: + + +![zh-cn_image_0000001511421360](figures/zh-cn_image_0000001511421360.gif) + + +假设开发者想让应用打开的时候,无需执行多余的切换焦点操作,直接点击按键的空格/回车键,就可以执行Button-OK的onClick回调操作,那么就可以给这个Button绑定defaultFocus(true),让它成为该页面上的默认焦点: + + + +```ts +Button('OK') + .defaultFocus(true) // 设置Button-OK为defaultFocus + .fontSize(30) + .fontColor('#787878') + .type(ButtonType.Normal) + .width(140).height(50).backgroundColor('#dadbd9') + .onClick(() => { + promptAction.showToast({ message: 'Button OK on clicked' }); + }) +``` + + +![zh-cn_image_0000001562940617](figures/zh-cn_image_0000001562940617.gif) + + +打开应用后按TAB键,Button-OK显示了焦点态,说明默认焦点变更到了Button-OK上。然后按下空格,响应了Button-OK的onClick事件。 + + +## 自定义TAB键走焦顺序 + + +```ts +tabIndex(index: number) +``` + +tabIndex用于设置自定义TAB键走焦顺序,默认值为0。使用“TAB/Shift+TAB键”走焦时(方向键不影响),系统会自动获取到所有配置了tabIndex大于0的组件,然后按照递增/递减排序进行走焦。 + + +以[defaultFocus](#自定义默认焦点)提供的示例为例,默认情况下的走焦顺序如下: + + +![zh-cn_image_0000001511421364](figures/zh-cn_image_0000001511421364.gif) + + +默认的走焦顺序从第一个获焦组件一路走到最后一个获焦组件,会经历Button1->Button4->Button5->Button7->左箭头->右箭头->ButtonOK。这种走焦队列比较完整,遍历了大部分的组件。但缺点是从第一个走到最后一个所经历的路径较长。 + + +如果想实现快速的从第一个走到最后一个,又不想牺牲太多的遍历完整性,就可以使用tabIndex通用属性。 + + +比如:开发者把白色的区域当为一个整体,黄色的区域当为一个整体,绿色的区域当为一个整体,实现Button1->左箭头->ButtonOK这种队列的走焦顺序,只需要在Button1、左箭头、ButtonOK这三个组件上依次增加tabIndex(1)、tabIndex(2)、tabIndex(3)。tabIndex的参数表示TAB走焦的顺序(从大于0的数字开始,从小到大排列)。 + + + +```ts + Button('1').width(200).height(200) + .fontSize(40) + .backgroundColor('#dadbd9') + .tabIndex(1) // Button-1设置为第一个tabIndex节点 +``` + + + +```ts + Button('←') + .fontSize(40) + .fontWeight(FontWeight.Bold) + .fontColor(Color.Black) + .backgroundColor(Color.Transparent) + .onClick(() => { + this.swiperController.showPrevious(); + }) + .tabIndex(2) // Button-左箭头设置为第二个tabIndex节点 +``` + + + +```ts +Button('OK') + .fontSize(30) + .fontColor('#787878') + .type(ButtonType.Normal) + .width(140).height(50).backgroundColor('#dadbd9') + .onClick(() => { + promptAction.showToast({ message: 'Button OK on clicked' }); + }) + .tabIndex(3) // Button-OK设置为第三个tabIndex节点 +``` + + +![zh-cn_image_0000001511580976](figures/zh-cn_image_0000001511580976.gif) + + +>**说明:** +> - 当焦点处于tabIndex(大于0)节点上时,TAB/ShiftTAB会优先在tabIndex(大于0)的队列中寻找后置/前置的节点,存在则走焦至相应的tabIndex节点。若不存在,则使用默认的走焦逻辑继续往后/往前走焦。 +> +> - 当焦点处于tabIndex(等于0)节点上时,TAB/ShiftTAB使用默认的走焦逻辑走焦,走焦的过程中会跳过tabIndex(大于0)和tabIndex(小于0)的节点。 +> +> - 当焦点处于tabIndex(小于0)节点上时,TAB/ShiftTAB无法走焦。 + + +### groupDefaultFocus + + +```ts +groupDefaultFocus(value: boolean) +``` + +[自定义TAB键走焦顺序](#自定义tab键走焦顺序)中所展示的使用tabIndex完成快速走焦的能力有如下问题: + +每个区域(白色/黄色/绿色三个区域)都设置了某个组件为tabIndex节点(白色-Button1、黄色-左箭头、绿色-ButtonOK),但这样设置之后,只能在这3个组件上按TAB/ShiftTab键走焦时会有快速走焦的效果。 + +解决方案是给每个区域的容器设置tabIndex,但是这样设置的问题是:第一次走焦到容器上时,获焦的子组件是默认的第一个可获焦组件,并不是自己想要的组件(Button1、左箭头、ButtonOK)。 + +这样便引入了groupDefaultFocus通用属性,参数:boolean,默认值:false。 + +用法需和tabIndex组合使用,使用tabIndex给区域(容器)绑定走焦顺序,然后给Button1、左箭头、ButtonOK绑定groupDefaultFocus(true),这样在首次走焦到目标区域(容器)上时,它的绑定了groupDefaultFocus(true)的子组件同时获得焦点。 + + +```ts +// xxx.ets +import promptAction from '@ohos.promptAction'; + +class MyDataSource implements IDataSource { + private list: number[] = []; + private listener: DataChangeListener; + + constructor(list: number[]) { + this.list = list; + } + + totalCount(): number { + return this.list.length; + } + + getData(index: number): any { + return this.list[index]; + } + + registerDataChangeListener(listener: DataChangeListener): void { + this.listener = listener; + } + + unregisterDataChangeListener() { + } +} + +@Entry +@Component +struct SwiperExample { + private swiperController: SwiperController = new SwiperController() + private data: MyDataSource = new MyDataSource([]) + + aboutToAppear(): void { + let list = [] + for (let i = 1; i <= 4; i++) { + list.push(i.toString()); + } + this.data = new MyDataSource(list); + } + + build() { + Column({ space: 5 }) { + Swiper(this.swiperController) { + LazyForEach(this.data, (item: string) => { + Row({ space: 20 }) { // 设置该Row组件为tabIndex的第一个节点 + Column() { + Button('1').width(200).height(200) + .fontSize(40) + .backgroundColor('#dadbd9') + .groupDefaultFocus(true) // 设置Button-1为第一个tabIndex的默认焦点 + } + + Column({ space: 20 }) { + Row({ space: 20 }) { + Button('2') + .width(100) + .height(100) + .fontSize(40) + .type(ButtonType.Normal) + .borderRadius(20) + .backgroundColor('#dadbd9') + Button('3') + .width(100) + .height(100) + .fontSize(40) + .type(ButtonType.Normal) + .borderRadius(20) + .backgroundColor('#dadbd9') + } + + Row({ space: 20 }) { + Button('4') + .width(100) + .height(100) + .fontSize(40) + .type(ButtonType.Normal) + .borderRadius(20) + .backgroundColor('#dadbd9') + Button('5') + .width(100) + .height(100) + .fontSize(40) + .type(ButtonType.Normal) + .borderRadius(20) + .backgroundColor('#dadbd9') + } + + Row({ space: 20 }) { + Button('6') + .width(100) + .height(100) + .fontSize(40) + .type(ButtonType.Normal) + .borderRadius(20) + .backgroundColor('#dadbd9') + Button('7') + .width(100) + .height(100) + .fontSize(40) + .type(ButtonType.Normal) + .borderRadius(20) + .backgroundColor('#dadbd9') + } + } + } + .width(480) + .height(380) + .justifyContent(FlexAlign.Center) + .borderWidth(2) + .borderColor(Color.Gray) + .backgroundColor(Color.White) + .tabIndex(1) + }, item => item) + } + .cachedCount(2) + .index(0) + .interval(4000) + .indicator(true) + .loop(true) + .duration(1000) + .itemSpace(0) + .curve(Curve.Linear) + .onChange((index: number) => { + console.info(index.toString()); + }) + .margin({ left: 20, top: 20, right: 20 }) + + Row({ space: 40 }) { // 设置该Row组件为第二个tabIndex节点 + Button('←') + .fontSize(40) + .fontWeight(FontWeight.Bold) + .fontColor(Color.Black) + .backgroundColor(Color.Transparent) + .onClick(() => { + this.swiperController.showPrevious(); + }) + .groupDefaultFocus(true) // 设置Button-左箭头为第二个tabIndex节点的默认焦点 + Button('→') + .fontSize(40) + .fontWeight(FontWeight.Bold) + .fontColor(Color.Black) + .backgroundColor(Color.Transparent) + .onClick(() => { + this.swiperController.showNext(); + }) + } + .width(480) + .height(50) + .justifyContent(FlexAlign.Center) + .borderWidth(2) + .borderColor(Color.Gray) + .backgroundColor('#f7f6dc') + .tabIndex(2) + + Row({ space: 40 }) { // 设置该Row组件为第三个tabIndex节点 + Button('Cancel') + .fontSize(30) + .fontColor('#787878') + .type(ButtonType.Normal) + .width(140) + .height(50) + .backgroundColor('#dadbd9') + + Button('OK') + .fontSize(30) + .fontColor('#787878') + .type(ButtonType.Normal) + .width(140) + .height(50) + .backgroundColor('#dadbd9') + .defaultFocus(true) + .onClick(() => { + promptAction.showToast({ message: 'Button OK on clicked' }); + }) + .groupDefaultFocus(true) // 设置Button-OK为第三个tabIndex节点的默认焦点 + } + .width(480) + .height(80) + .justifyContent(FlexAlign.Center) + .borderWidth(2) + .borderColor(Color.Gray) + .backgroundColor('#dff2e4') + .margin({ left: 20, bottom: 20, right: 20 }) + .tabIndex(3) + }.backgroundColor('#f2f2f2') + .margin({ left: 50, top: 50, right: 20 }) + } +} +``` + +![zh-cn_image_0000001562700533](figures/zh-cn_image_0000001562700533.gif) + + +### focusOnTouch + + +```ts +focusOnTouch(value: boolean) +``` + +点击获焦能力,参数:boolean,默认值:false(输入类组件:TextInput、TextArea、Search、Web默认值是true)。 + +点击是指使用触屏或鼠标左键进行单击,默认为false的组件,例如Button,不绑定该API时,点击Button不会使其获焦,当给Button绑定focusOnTouch(true)时,点击Button会使Button立即获得焦点。 + +给容器绑定focusOnTouch(true)时,点击容器区域,会立即使容器的第一个可获焦组件获得焦点。 + +示例代码: + + +```ts +// requestFocus.ets +import promptAction from '@ohos.promptAction'; + +@Entry +@Component +struct RequestFocusExample { + @State idList: string[] = ['A', 'B', 'C', 'D', 'E', 'F', 'N'] + + build() { + Column({ space:20 }){ + Button("id: " + this.idList[0] + " focusOnTouch(true) + focusable(false)") + .width(400).height(70).fontColor(Color.White).focusOnTouch(true) + .focusable(false) + Button("id: " + this.idList[1] + " default") + .width(400).height(70).fontColor(Color.White) + Button("id: " + this.idList[2] + " focusOnTouch(false)") + .width(400).height(70).fontColor(Color.White).focusOnTouch(false) + Button("id: " + this.idList[3] + " focusOnTouch(true)") + .width(400).height(70).fontColor(Color.White).focusOnTouch(true) + }.width('100%').margin({ top:20 }) + } +} +``` + + +![zh-cn_image_0000001511580980](figures/zh-cn_image_0000001511580980.gif) + + +解读: + + +Button-A虽然设置了focusOnTouch(true),但是同时也设置了focusable(false),该组件无法获焦,因此点击后也无法获焦; + + +Button-B不设置相关属性,点击后不会获焦; + + +Button-C设置了focusOnTouch(false),同Button-B,点击后也不会获焦; + + +Button-D设置了focusOnTouch(true),点击即可使其获焦; + + +>**说明:** +> +>由于焦点态的阐述的特性,焦点态在屏幕接收点击事件后会立即清除。因此该示例代码在每次点击后,需要再次按下TAB键使焦点态再次显示,才可知道当前焦点所在的组件。 + + +### focusControl.requestFocus + + +```ts +focusControl.requestFocus(id: string) +``` + +主动申请焦点能力的全局方法,参数:string,参数表示被申请组件的id(通用属性id设置的字符串)。 + + +使用方法为:在任意执行语句中调用该API,指定目标组件的id为方法参数,当程序执行到该语句时,会立即给指定的目标组件申请焦点。 + + +代码示例: + + + +```ts +// requestFocus.ets +import promptAction from '@ohos.promptAction'; + +@Entry +@Component +struct RequestFocusExample { + @State idList: string[] = ['A', 'B', 'C', 'D', 'E', 'F', 'N'] + @State requestId: number = 0 + + build() { + Column({ space:20 }){ + Row({space: 5}) { + Button("id: " + this.idList[0] + " focusable(false)") + .width(200).height(70).fontColor(Color.White) + .id(this.idList[0]) + .focusable(false) + Button("id: " + this.idList[1]) + .width(200).height(70).fontColor(Color.White) + .id(this.idList[1]) + } + Row({space: 5}) { + Button("id: " + this.idList[2]) + .width(200).height(70).fontColor(Color.White) + .id(this.idList[2]) + Button("id: " + this.idList[3]) + .width(200).height(70).fontColor(Color.White) + .id(this.idList[3]) + } + Row({space: 5}) { + Button("id: " + this.idList[4]) + .width(200).height(70).fontColor(Color.White) + .id(this.idList[4]) + Button("id: " + this.idList[5]) + .width(200).height(70).fontColor(Color.White) + .id(this.idList[5]) + } + }.width('100%').margin({ top:20 }) + .onKeyEvent((e) => { + if (e.keyCode >= 2017 && e.keyCode <= 2022) { + this.requestId = e.keyCode - 2017; + } else if (e.keyCode === 2030) { + this.requestId = 6; + } else { + return; + } + if (e.type !== KeyType.Down) { + return; + } + let res = focusControl.requestFocus(this.idList[this.requestId]); + if (res) { + promptAction.showToast({message: 'Request success'}); + } else { + promptAction.showToast({message: 'Request failed'}); + } + }) + } +} +``` + + +![zh-cn_image_0000001562820905](figures/zh-cn_image_0000001562820905.gif) + + +解读:页面中共6个Button组件,其中Button-A组件设置了focusable(false),表示其不可获焦,在外部容器的onKeyEvent中,监听按键事件,当按下A ~ F按键时,分别去申请Button A ~ F 的焦点,另外按下N键,是给当前页面上不存在的id的组件去申请焦点。 + + +1. 按下TAB键,由于第一个组件Button-A设置了无法获焦,那么默认第二个组件Button-B获焦,Button-B展示焦点态样式; + +2. 键盘上按下A键,申请Button-A的焦点,气泡显示Request failed,表示无法获取到焦点,焦点位置未改变; + +3. 键盘上按下B键,申请Button-B的焦点,气泡显示Request success,表示获焦到了焦点,焦点位置原本就在Button-B,位置未改变; + +4. 键盘上按下C键,申请Button-C的焦点,气泡显示Request success,表示获焦到了焦点,焦点位置从Button-B变更为Button-C; + +5. 键盘上按下D键,申请Button-D的焦点,气泡显示Request success,表示获焦到了焦点,焦点位置从Button-C变更为Button-D; + +6. 键盘上按下E键,申请Button-E的焦点,气泡显示Request success,表示获焦到了焦点,焦点位置从Button-D变更为Button-E; + +7. 键盘上按下F键,申请Button-F的焦点,气泡显示Request success,表示获焦到了焦点,焦点位置从Button-E变更为Button-F; + +8. 键盘上按下N键,申请未知组件的焦点,气泡显示Request failed,表示无法获取到焦点,焦点位置不变; diff --git a/zh-cn/application-dev/ui/arkts-common-events-touch-screen-event.md b/zh-cn/application-dev/ui/arkts-common-events-touch-screen-event.md new file mode 100644 index 0000000000000000000000000000000000000000..750d16635237f51c73b50e87f53d7186c457c3a0 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-common-events-touch-screen-event.md @@ -0,0 +1,315 @@ +# 触屏事件 + + +触屏事件指当手指/手写笔在组件上按下、滑动、抬起时触发的回调事件。包括[点击事件](#点击事件)、[拖拽事件](#拖拽事件)和[触摸事件](#触摸事件)。 + + +**图1 ** 触摸事件原理 + + +![zh-cn_image_0000001562700461](figures/zh-cn_image_0000001562700461.png) + + +## 点击事件 + +点击事件是指通过手指或手写笔做出一次完整的按下和抬起动作。当发生点击事件时,会触发以下回调函数: + + + +```ts +onClick(event: (event?: ClickEvent) => void) +``` + + +event参数提供点击事件相对于窗口或组件的坐标位置,以及发生点击的事件源。 + + + 例如通过按钮的点击事件控制图片的显示和隐藏。 + +```ts +@Entry +@Component +struct IfElseTransition { + @State flag: boolean = true; + @State btnMsg: string = 'show'; + + build() { + Column() { + Button(this.btnMsg).width(80).height(30).margin(30) + .onClick(() => { + if (this.flag) { + this.btnMsg = 'hide'; + } else { + this.btnMsg = 'show'; + } + // 点击Button控制Image的显示和消失 + this.flag = !this.flag; + }) + if (this.flag) { + Image($r('app.media.icon')).width(200).height(200) + } + }.height('100%').width('100%') + } +} +``` + + +## 拖拽事件 + +拖拽事件指手指/手写笔长按组件(>=500ms),并拖拽到接收区域释放的事件。拖拽事件触发流程: + + +![zh-cn_image_0000001562820825](figures/zh-cn_image_0000001562820825.png) + + +拖拽事件的触发通过长按、拖动平移判定,手指平移的距离达到5vp即可触发拖拽事件。ArkUI支持应用内、跨应用的拖拽事件。 + + +拖拽事件提供以下[接口](../reference/arkui-ts/ts-universal-events-drag-drop.md): + + +| 接口名称 | 描述 | +| ---------------------------------------- | ---------------------------------------- | +| onDragStart(event: (event?: DragEvent, extraParams?: string) => CustomBuilder \| DragItemInfo) | 拖拽启动接口。当前仅支持自定义pixelmap和自定义组件。 | +| onDragEnter(event: (event?: DragEvent, extraParams?: string) => void) | 拖拽进入组件接口。DragEvent定义拖拽发生位置,extraParmas表示用户自定义信息 | +| onDragLeave(event: (event?: DragEvent, extraParams?: string) => void) | 拖拽离开组件接口。DragEvent定义拖拽发生位置,extraParmas表示拖拽事件额外信息。 | +| onDragMove(event: (event?: DragEvent, extraParams?: string) => void) | 拖拽移动接口。DragEvent定义拖拽发生位置,extraParmas表示拖拽事件额外信息。 | +| onDrop(event: (event?: DragEvent, extraParams?: string) => void) | 拖拽释放组件接口。DragEvent定义拖拽发生位置,extraParmas表示拖拽事件额外信息。 | + + +如下是跨窗口拖拽,拖出窗口示例: + + + +```ts +import image from '@ohos.multimedia.image'; + +@Entry +@Component +struct Index { + @State text: string = '' + @State bool1: boolean = false + @State bool2: boolean = false + @State visible: Visibility = Visibility.Visible + @State pixelMap: PixelMap = undefined + private pixelMapReader = undefined + + aboutToAppear() { + console.info('begin to create pixmap has info message: ') + this.createPixelMap() + } + + createPixelMap() { + let color = new ArrayBuffer(4 * 96 * 96); + var buffer = new Uint8Array(color); + for (var i = 0; i < buffer.length; i++) { + buffer[i] = (i + 1) % 255; + } + let opts = { + alphaType: 0, + editable: true, + pixelFormat: 4, + scaleMode: 1, + size: { height: 96, width: 96 } + } + const promise = image.createPixelMap(color, opts); + promise.then((data) => { + console.info('create pixmap has info message: ' + JSON.stringify(data)) + this.pixelMap = data; + this.pixelMapReader = data; + }) + } + + @Builder pixelMapBuilder() { + Text('drag item') + .width('100%') + .height(100) + .fontSize(16) + .textAlign(TextAlign.Center) + .borderRadius(10) + .backgroundColor(0xFFFFFF) + } + + build() { + Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { + Text('App1') + .width('40%') + .height(80) + .fontSize(20) + .margin(30) + .textAlign(TextAlign.Center) + .backgroundColor(Color.Pink) + .visibility(Visibility.Visible) + + Text('Across Window Drag This') + .width('80%') + .height(80) + .fontSize(16) + .margin(30) + .textAlign(TextAlign.Center) + .backgroundColor(Color.Pink) + .visibility(this.visible) + .onDragStart(() => { //启动跨窗口拖拽 + console.info('Text onDrag start') + this.bool1 = true + this.text = 'TextDrag' + return { pixelMap: this.pixelMapReader, extraInfo: 'custom extra info.' } + }) + .onDrop((event: DragEvent, extraParams: string) => { + console.info('Text onDragDrop, ') + this.visible = Visibility.None //拖动结束后,使源不可见 + }) + } + + .width('100%') + .height('100%') + } +} +``` + + +跨窗口拖拽,拖入示例: + + + +```ts + +@Entry +@Component +struct Index { + @State number: string[] = ['drag here'] + @State text: string = '' + @State bool1: boolean = false + @State bool2: boolean = false + @State visible: Visibility = Visibility.Visible + @State visible2: Visibility = Visibility.None + scroller: Scroller = new Scroller() + + build() { + Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { + Text('App2') + .width('40%') + .height(80) + .fontSize(20) + .margin(30) + .textAlign(TextAlign.Center) + .backgroundColor(Color.Pink) + .visibility(Visibility.Visible) + + List({ space: 20, initialIndex: 0 }) { + ForEach(this.number, (item) => { + ListItem() { + Text('' + item) + .width('100%') + .height(80) + .fontSize(16) + .borderRadius(10) + .textAlign(TextAlign.Center) + .backgroundColor(0xFFFFFF) + } + }, item => item) + + ListItem() { + Text('Across Window Drag This') + .width('80%') + .height(80) + .fontSize(16) + .margin(30) + .textAlign(TextAlign.Center) + .backgroundColor(Color.Pink) + .visibility(this.visible2) + } + } + .height('50%') + .width('90%') + .border({ width: 1 }) + .divider({ strokeWidth: 2, color: 0xFFFFFF, startMargin: 20, endMargin: 20 }) + .onDragEnter((event: DragEvent, extraParams: string) => { //拖拽进去组件 + console.info('List onDragEnter, ' + extraParams) + }) + .onDragMove((event: DragEvent, extraParams: string) => { //拖拽时移动 + console.info('List onDragMove, ' + extraParams) + }) + .onDragLeave((event: DragEvent, extraParams: string) => { //拖拽离开组件 + console.info('List onDragLeave, ' + extraParams) + }) + .onDrop((event: DragEvent, extraParams: string) => { //释放组件 + console.info('List onDragDrop, ' + extraParams) + this.visible2 = Visibility.Visible //拖拽完成使拖入目标可见 + }) + } + .width('100%') + .height('100%') + } +} +``` + + +## 触摸事件 + +当手指或手写笔在组件上触碰时,会触发不同动作所对应的事件响应,包括按下(Down)、滑动(Move)、抬起(Up)事件: + + +```ts +onTouch(event: (event?: TouchEvent) => void) +``` + +- event.type为TouchType.Down:表示手指按下。 + +- event.type为TouchType.Up:表示手指抬起。 + +- event.type为TouchType.Move:表示手指按住移动。 + +触摸事件可以同时多指触发,通过event参数可获取触发的手指位置、手指唯一标志、当前发生变化的手指和输入的设备源等信息。 + + +```ts +// xxx.ets +@Entry +@Component +struct TouchExample { + @State text: string = ''; + @State eventType: string = ''; + + build() { + Column() { + Button('Touch').height(40).width(100) + .onTouch((event: TouchEvent) => { + if (event.type === TouchType.Down) { + this.eventType = 'Down'; + } + if (event.type === TouchType.Up) { + this.eventType = 'Up'; + } + if (event.type === TouchType.Move) { + this.eventType = 'Move'; + } + this.text = 'TouchType:' + this.eventType + '\nDistance between touch point and touch element:\nx: ' + + event.touches[0].x + '\n' + 'y: ' + event.touches[0].y + '\nComponent globalPos:(' + + event.target.area.globalPosition.x + ',' + event.target.area.globalPosition.y + ')\nwidth:' + + event.target.area.width + '\nheight:' + event.target.area.height + }) + Button('Touch').height(50).width(200).margin(20) + .onTouch((event: TouchEvent) => { + if (event.type === TouchType.Down) { + this.eventType = 'Down'; + } + if (event.type === TouchType.Up) { + this.eventType = 'Up'; + } + if (event.type === TouchType.Move) { + this.eventType = 'Move'; + } + this.text = 'TouchType:' + this.eventType + '\nDistance between touch point and touch element:\nx: ' + + event.touches[0].x + '\n' + 'y: ' + event.touches[0].y + '\nComponent globalPos:(' + + event.target.area.globalPosition.x + ',' + event.target.area.globalPosition.y + ')\nwidth:' + + event.target.area.width + '\nheight:' + event.target.area.height + }) + Text(this.text) + }.width('100%').padding(30) + } +} +``` + + +![zh-cn_image_0000001511900468](figures/zh-cn_image_0000001511900468.gif) diff --git a/zh-cn/application-dev/ui/arkts-drawing-customization-on-canvas.md b/zh-cn/application-dev/ui/arkts-drawing-customization-on-canvas.md new file mode 100644 index 0000000000000000000000000000000000000000..6ab0c64456faca68440b8806926794edcda16603 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-drawing-customization-on-canvas.md @@ -0,0 +1,346 @@ +# 使用画布绘制自定义图形 + + +Canvas提供画布组件,用于自定义绘制图形,开发者使用CanvasRenderingContext2D对象和OffscreenCanvasRenderingContext2D对象在Canvas组件上进行绘制,绘制对象可以是基础形状、文本、图片等。 + + +## 使用画布组件绘制自定义图形 + +可以由以下三种形式在画布绘制自定义图形: + + +- 使用[CanvasRenderingContext2D对象](../reference/arkui-ts/ts-canvasrenderingcontext2d.md)在Canvas画布上绘制。 + + ```ts + @Entry + @Component + struct CanvasExample1 { + //用来配置CanvasRenderingContext2D对象的参数,包括是否开启抗锯齿,true表明开启抗锯齿。 + private settings: RenderingContextSettings = new RenderingContextSettings(true) + //用来创建CanvasRenderingContext2D对象,通过在canvas中调用CanvasRenderingContext2D对象来绘制。 + private context: CanvasRenderingContext2D= new CanvasRenderingContext2D(this.settings) + build() { + Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { + //在canvas中调用CanvasRenderingContext2D对象。 + Canvas(this.context) + .width('100%') + .height('100%') + .backgroundColor('#F5DC62') + .onReady(() =>{ + //可以在这里绘制内容。 + this.context.strokeRect(50, 50, 200, 150); + }) + } + .width('100%') + .height('100%') + } + } + + ``` + + ![2023022793003(1)](figures/2023022793003(1).jpg) + +- 离屏绘制是指将需要绘制的内容先绘制在缓存区,再将其转换成图片,一次性绘制到Canvas上,加快了绘制速度。过程为: + 1. 通过transferToImageBitmap方法将离屏画布最近渲染的图像创建为一个ImageBitmap对象。 + 2. 通过CanvasRenderingContext2D对象的transferFromImageBitmap方法显示给定的ImageBitmap对象。 + + 具体使用参考[OffscreenCanvasRenderingContext2D对象](../reference/arkui-ts/ts-offscreencanvasrenderingcontext2d.md)。 + + ```ts + @Entry + @Component + struct CanvasExample2 { + //用来配置CanvasRenderingContext2D对象和OffscreenCanvasRenderingContext2D对象的参数,包括是否开启抗锯齿。true表明开启抗锯齿 + private settings: RenderingContextSettings = new RenderingContextSettings(true) + private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings) + //用来创建OffscreenCanvasRenderingContext2D对象,width为离屏画布的宽度,height为离屏画布的高度。通过在canvas中调用OffscreenCanvasRenderingContext2D对象来绘制。 + private offContext: OffscreenCanvasRenderingContext2D = new OffscreenCanvasRenderingContext2D(600, 600, this.settings) + + build() { + Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { + Canvas(this.context) + .width('100%') + .height('100%') + .backgroundColor('#F5DC62') + .onReady(() =>{ + //可以在这里绘制内容 + this.offContext.strokeRect(50, 50, 200, 150); + //将离屏绘值渲染的图像在普通画布上显示 + let image = this.offContext.transferToImageBitmap(); + this.context.transferFromImageBitmap(image); + }) + } + .width('100%') + .height('100%') + } + } + + ``` + + ![2023022793003(1)](figures/2023022793003(1).jpg) + + >**说明:** + > + >在画布组件中,通过CanvasRenderingContext2D对象和OffscreenCanvasRenderingContext2D对象在Canvas组件上进行绘制时调用的接口相同,另接口参数如无特别说明,单位均为vp。 + +- 在Canvas上加载Lottie动画时,需要先按照如下方式下载Lottie。 + + ```ts + import lottie from '@ohos/lottie' + ``` + + 具体接口参考[Lottie](../reference/arkui-ts/ts-components-canvas-lottie.md),相关实例请参考[Lottie实例](https://gitee.com/openharmony/applications_app_samples/tree/master/ETSUI/Lottie)。 + + >**说明:** + > + >在第一次使用Lottie之前,需要在Terminal窗口运行ohpm install \@ohos/lottieETS命令下载Lottie。 + + +## 初始化画布组件 + +onReady(event: () => void)是Canvas组件初始化完成时的事件回调,调用该事件后,可获取Canvas组件的确定宽高,进一步使用CanvasRenderingContext2D对象和OffscreenCanvasRenderingContext2D对象调用相关API进行图形绘制。 + +```ts +Canvas(this.context) + .width('100%') + .height('100%') + .backgroundColor('#F5DC62') + .onReady(() => { + this.context.fillStyle = '#0097D4'; + this.context.fillRect(50, 50, 100, 100); + }) + +``` + +![2023022793350(1)](figures/2023022793350(1).jpg) + + +## 画布组件绘制方式 + +在Canvas组件生命周期接口onReady()调用之后,开发者可以直接使用canvas组件进行绘制。或者可以脱离Canvas组件和onready生命周期,单独定义Path2d对象构造理想的路径,并在onready调用之后使用Canvas组件进行绘制。 + +- 通过CanvasRenderingContext2D对象和OffscreenCanvasRenderingContext2D对象直接调用相关API进行绘制。 + + ```ts + Canvas(this.context) + .width('100%') + .height('100%') + .backgroundColor('#F5DC62') + .onReady(() =>{ + this.context.beginPath(); + this.context.moveTo(50, 50); + this.context.lineTo(280, 160); + this.context.stroke(); + }) + ``` + + ![2023022793719(1)](figures/2023022793719(1).jpg) + +- 先单独定义path2d对象构造理想的路径,再通过调用CanvasRenderingContext2D对象和OffscreenCanvasRenderingContext2D对象的stroke接口或者fill接口进行绘制,具体使用可以参考[Path2D对象](../reference/arkui-ts/ts-components-canvas-path2d.md)。 + + ```ts + Canvas(this.context) + .width('100%') + .height('100%') + .backgroundColor('#F5DC62') + .onReady(() =>{ + let region = new Path2D(); + region.arc(100, 75, 50, 0, 6.28); + this.context.stroke(region); + }) + ``` + + ![2023022794031(1)](figures/2023022794031(1).jpg) + + +## 画布组件常用方法 + +OffscreenCanvasRenderingContext2D对象和CanvasRenderingContext2D对象提供了大量的属性和方法,可以用来绘制文本、图形,处理像素等,是Canvas组件的核心。常用接口有[fill](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#fill)(对封闭路径进行填充)、[clip](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#clip)(设置当前路径为剪切路径)、[stroke](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#stroke)(进行边框绘制操作)等等,同时提供了[fillStyle](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#fillstyle)(指定绘制的填充色)、[globalAlpha](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#globalalpha)(设置透明度)与[strokeStyle](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#strokestyle)(设置描边的颜色)等属性修改绘制内容的样式。将通过以下几个方面简单介绍画布组件常见使用方法: + +- 基础形状绘制。 + 可以通过[arc](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#arc)(绘制弧线路径)、 [ellipse](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#ellipse)(绘制一个椭圆)、[rect](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#rect)(创建矩形路径)等接口绘制基础形状。 + + ```ts + Canvas(this.context) + .width('100%') + .height('100%') + .backgroundColor('#F5DC62') + .onReady(() =>{ + //绘制矩形 + this.context.beginPath(); + this.context.rect(100, 50, 100, 100); + this.context.stroke(); + //绘制圆形 + this.context.beginPath(); + this.context.arc(150, 250, 50, 0, 6.28); + this.context.stroke(); + //绘制椭圆 + this.context.beginPath(); + this.context.ellipse(150, 450, 50, 100, Math.PI * 0.25, Math.PI * 0, Math.PI * 2); + this.context.stroke(); + }) + + ``` + + ![2023022794521(1)](figures/2023022794521(1).jpg) + +- 文本绘制。 + + 可以通过[fillText](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#filltext)(绘制填充类文本)、[strokeText](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#stroketext)(绘制描边类文本)等接口进行文本绘制。 + + ```ts + Canvas(this.context) + .width('100%') + .height('100%') + .backgroundColor('#F5DC62') + .onReady(() =>{ + //绘制填充类文本 + this.context.font = '50px sans-serif'; + this.context.fillText("Hello World!", 50, 100); + //绘制描边类文本 + this.context.font = '55px sans-serif'; + this.context.strokeText("Hello World!", 50, 150); + }) + ``` + + ![2023022795105(1)](figures/2023022795105(1).jpg) + +- 绘制图片和图像像素信息处理。 + + 可以通过[drawImage](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#drawimage)(图像绘制)、[putImageData](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#putimagedata)(使用[ImageData](../reference/arkui-ts/ts-components-canvas-imagedata.md)数据填充新的矩形区域)等接口绘制图片,通过[createImageData](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#createimagedata)(创建新的ImageData 对象)、[getPixelMap](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#getpixelmap)(以当前canvas指定区域内的像素创建[PixelMap](../reference/apis/js-apis-image.md#pixelmap7)对象)、[getImageData](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#getimagedata)(以当前canvas指定区域内的像素创建ImageData对象)等接口进行图像像素信息处理。 + + ```ts + @Entry + @Component + struct GetImageData { + private settings: RenderingContextSettings = new RenderingContextSettings(true) + private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings) + private offContext: OffscreenCanvasRenderingContext2D = new OffscreenCanvasRenderingContext2D(600, 600, this.settings) + private img:ImageBitmap = new ImageBitmap("/common/images/1234.png") + + build() { + Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { + Canvas(this.context) + .width('100%') + .height('100%') + .backgroundColor('#F5DC62') + .onReady(() =>{ + // 使用drawImage接口将图片画在(0,0)为起点,宽高130的区域 + this.offContext.drawImage(this.img,0,0,130,130); + // 使用getImageData接口,获得canvas组件区域中,(50,50)为起点,宽高130范围内的绘制内容 + let imagedata = this.offContext.getImageData(50,50,130,130); + // 使用putImageData接口将得到的ImageData画在起点为(150, 150)的区域中 + this.offContext.putImageData(imagedata,150,150); + // 将离屏绘制的内容画到canvas组件上 + let image = this.offContext.transferToImageBitmap(); + this.context.transferFromImageBitmap(image); + }) + } + .width('100%') + .height('100%') + } + } + ``` + + ![drawimage](figures/drawimage.PNG) + +- 其他方法。 + Canvas中还提供其他类型的方法。渐变([CanvasGradient对象](../reference/arkui-ts/ts-components-canvas-canvasgradient.md))相关的方法:[createLinearGradient](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#createlineargradient)(创建一个线性渐变色)、[createRadialGradient](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#createradialgradient)(创建一个径向渐变色)等。 + + ```ts + Canvas(this.context) + .width('100%') + .height('100%') + .backgroundColor('#F5DC62') + .onReady(() =>{ + //创建一个径向渐变色的CanvasGradient对象 + let grad = this.context.createRadialGradient(200,200,50, 200,200,200) + //为CanvasGradient对象设置渐变断点值,包括偏移和颜色 + grad.addColorStop(0.0, '#E87361'); + grad.addColorStop(0.5, '#FFFFF0'); + grad.addColorStop(1.0, '#BDDB69'); + //用CanvasGradient对象填充矩形 + this.context.fillStyle = grad; + this.context.fillRect(0, 0, 400, 400); + }) + ``` + + ![2023022700701(1)](figures/2023022700701(1).jpg) + + +## 场景示例 + +- 规则基础形状绘制: + + ```ts + @Entry + @Component + struct ClearRect { + private settings: RenderingContextSettings = new RenderingContextSettings(true); + private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings); + + build() { + Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { + Canvas(this.context) + .width('100%') + .height('100%') + .backgroundColor('#F5DC62') + .onReady(() =>{ + // 设定填充样式,填充颜色设为蓝色 + this.context.fillStyle = '#0097D4'; + // 以(50, 50)为左上顶点,画一个宽高200的矩形 + this.context.fillRect(50,50,200,200); + // 以(70, 70)为左上顶点,清除宽150高100的区域 + this.context.clearRect(70,70,150,100); + }) + } + .width('100%') + .height('100%') + } + } + + ``` + + ![2023022701120(1)](figures/2023022701120(1).jpg) + +- 不规则图形绘制。 + + ```ts + @Entry + @Component + struct Path2d { + private settings: RenderingContextSettings = new RenderingContextSettings(true); + private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings); + + build() { + Row() { + Column() { + Canvas(this.context) + .width('100%') + .height('100%') + .backgroundColor('#F5DC62') + .onReady(() =>{ + // 使用Path2D的接口构造一个五边形 + let path = new Path2D(); + path.moveTo(150, 50); + path.lineTo(50, 150); + path.lineTo(100, 250); + path.lineTo(200, 250); + path.lineTo(250, 150); + path.closePath(); + // 设定填充色为蓝色 + this.context.fillStyle = '#0097D4'; + // 使用填充的方式,将Path2D描述的五边形绘制在canvas组件内部 + this.context.fill(path); + }) + } + .width('100%') + } + .height('100%') + } + } + ``` + + ![2023032422159](figures/2023032422159.jpg) + + ​ \ No newline at end of file diff --git a/zh-cn/application-dev/ui/arkts-event-overview.md b/zh-cn/application-dev/ui/arkts-event-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..c3cee4a8467513a08f6fc898770db72dba0839b1 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-event-overview.md @@ -0,0 +1,23 @@ +# 概述 + + +交互事件按照触发类型来分类,包括触屏事件、键鼠事件和焦点事件。 + + +- [触屏事件](arkts-common-events-touch-screen-event.md):手指或手写笔在触屏上的单指或单笔操作。 + +- [键鼠事件](arkts-common-events-device-input-event.md):包括外设鼠标或触控板的操作事件和外设键盘的按键事件。 + - 鼠标事件是指通过连接和使用外设鼠标/触控板操作时所响应的事件。 + - 按键事件是指通过连接和使用外设键盘操作时所响应的事件。 + +- [焦点事件](arkts-common-events-focus-event.md):通过以上方式控制组件焦点的能力和响应的事件。 + + +手势事件由绑定手势方法和绑定的手势组成,绑定的手势可以分为单一手势和组合手势两种类型,根据手势的复杂程度进行区分。 + + +- [绑定手势方法](arkts-gesture-events-binding.md):用于在组件上绑定单一手势或组合手势,并声明所绑定的手势的响应优先级。 + +- [单一手势](arkts-gesture-events-single-gesture.md):手势的基本单元,是所有复杂手势的组成部分。 + +- [组合手势](arkts-gesture-events-combined-gestures.md):由多个单一手势组合而成,可以根据声明的类型将多个单一手势按照一定规则组合成组合手势,并进行使用。 diff --git a/zh-cn/application-dev/ui/arkts-geometric-shape-drawing.md b/zh-cn/application-dev/ui/arkts-geometric-shape-drawing.md new file mode 100644 index 0000000000000000000000000000000000000000..f665a9dea4de305774169316400025ba3d89bd74 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-geometric-shape-drawing.md @@ -0,0 +1,288 @@ +# 绘制几何图形 + + +绘制组件用于在页面绘制图形,Shape组件是绘制组件的父组件,父组件中会描述所有绘制组件均支持的通用属性。具体用法请参考[Shape](../reference/arkui-ts/ts-drawing-components-shape.md)。 + + +## 创建绘制组件 + +绘制组件可以由以下两种形式创建: + +- 绘制组件使用Shape作为父组件,实现类似SVG的效果。接口调用为以下形式: + + ```ts + Shape(value?: PixelMap) + ``` + + 该接口用于创建带有父组件的绘制组件,其中value用于设置绘制目标,可将图形绘制在指定的PixelMap对象中,若未设置,则在当前绘制目标中进行绘制。 + + ```ts + Shape() { + Rect().width(300).height(50) + } + ``` + + +- 绘制组件单独使用,用于在页面上绘制指定的图形。有7种绘制类型,分别为[Circle](../reference/arkui-ts/ts-drawing-components-circle.md)(圆形)、[Ellipse](../reference/arkui-ts/ts-drawing-components-ellipse.md)(椭圆形)、[Line](../reference/arkui-ts/ts-drawing-components-line.md)(直线)、[Polyine](../reference/arkui-ts/ts-drawing-components-polyline.md)(折线)、[Polygon](../reference/arkui-ts/ts-drawing-components-polygon.md)(多边形)、[Path](../reference/arkui-ts/ts-drawing-components-path.md)(路径)、[Rect](../reference/arkui-ts/ts-drawing-components-rect.md)(矩形)。以Circle的接口调用为例: + + ```ts + Circle(options?: {width?: string | number, height?: string | number} + ``` + + 该接口用于在页面绘制圆形,其中width用于设置圆形的宽度,height用于设置圆形的高度,圆形直径由宽高最小值确定。 + + ```ts + Circle({ width: 150, height: 150 }) + ``` + + ![创建2](figures/创建2.jpg) + + +## 形状视口viewport + + +```ts +viewPort{ x?: number | string, y?: number | string, width?: number | string, height?: number | string } +``` + +形状视口viewport指定用户空间中的一个矩形,该矩形映射到为关联的 SVG 元素建立的视区边界。viewport属性的值包含x、y、width和height四个可选参数,x 和 y 表示视区的左上角坐标,width和height表示其尺寸。 + +以下3个示例讲解Viewport具体用法: + +- 通过形状视口对图形进行放大与缩小。 + + ```ts + // 画一个宽高都为150的圆 + Text('原始尺寸Circle组件') + Circle({width: 75, height: 75}).fill('#E87361') + + Row({space:10}) { + Column() { + // 创建一个宽高都为150的shape组件,背景色为黄色,一个宽高都为75的viewport。用一个蓝色的矩形来填充viewport,在viewport中绘制一个直径为75的圆。 + // 绘制结束,viewport会根据组件宽高放大两倍 + Text('shape内放大的Circle组件') + Shape() { + Rect().width('100%').height('100%').fill('#0097D4') + Circle({width: 75, height: 75}).fill('#E87361') + } + .viewPort({x: 0, y: 0, width: 75, height: 75}) + .width(150) + .height(150) + .backgroundColor('#F5DC62') + } + Column() { + // 创建一个宽高都为150的shape组件,背景色为黄色,一个宽高都为300的viewport。用一个绿色的矩形来填充viewport,在viewport中绘制一个直径为75的圆。 + // 绘制结束,viewport会根据组件宽高缩小两倍。 + Text('Shape内缩小的Circle组件') + Shape() { + Rect().width('100%').height('100%').fill('#BDDB69') + Circle({width: 75, height: 75}).fill('#E87361') + } + .viewPort({x: 0, y: 0, width: 300, height: 300}) + .width(150) + .height(150) + .backgroundColor('#F5DC62') + } + } + ``` + + ![2023032401632](figures/2023032401632.jpg) + +- 创建一个宽高都为300的shape组件,背景色为黄色,一个宽高都为300的viewport。用一个蓝色的矩形来填充viewport,在viewport中绘制一个半径为75的圆。 + + ```ts + Shape() { + Rect().width("100%").height("100%").fill("#0097D4") + Circle({ width: 150, height: 150 }).fill("#E87361") + } + .viewPort({ x: 0, y: 0, width: 300, height: 300 }) + .width(300) + .height(300) + .backgroundColor("#F5DC62") + ``` + + ![viewport(2)](figures/viewport(2).jpg) + +- 创建一个宽高都为300的shape组件,背景色为黄色,创建一个宽高都为300的viewport。用一个蓝色的矩形来填充viewport,在viewport中绘制一个半径为75的圆,将viewport向右方和下方各平移150。 + + ```ts + Shape() { + Rect().width("100%").height("100%").fill("#0097D4") + Circle({ width: 150, height: 150 }).fill("#E87361") + } + .viewPort({ x: -150, y: -150, width: 300, height: 300 }) + .width(300) + .height(300) + .backgroundColor("#F5DC62") + + ``` + + ![viewport(3)](figures/viewport(3).jpg) + + +## 自定义样式 + +绘制组件支持通过各种属性对组件样式进行更改。 + +- 通过fill可以设置组件填充区域颜色。 + + ```ts + Path() + .width(100) + .height(100) + .commands('M150 0 L300 300 L0 300 Z') + .fill("#E87361") + ``` + + ![2023022792216(1)](figures/2023022792216(1).jpg) + +- 通过stroke可以设置组件边框颜色。 + + ```ts + Path() + .width(100) + .height(100) + .fillOpacity(0) + .commands('M150 0 L300 300 L0 300 Z') + .stroke(Color.Red) + ``` + + ![stroke](figures/stroke.jpg) + +- 通过strokeOpacity可以设置边框透明度。 + + ```ts + Path() + .width(100) + .height(100) + .fillOpacity(0) + .commands('M150 0 L300 300 L0 300 Z') + .stroke(Color.Red) + .strokeWidth(10) + .strokeOpacity(0.2) + ``` + + ![strokeopacity](figures/strokeopacity.jpg) + +- 通过strokeLineJoin可以设置线条拐角绘制样式。拐角绘制样式分为Bevel(使用斜角连接路径段)、Miter(使用尖角连接路径段)、Round(使用圆角连接路径段)。 + + ```ts + Polyline() + .width(100) + .height(100) + .fillOpacity(0) + .stroke(Color.Red) + .strokeWidth(8) + .points([[20, 0], [0, 100], [100, 90]]) + // 设置折线拐角处为圆弧 + .strokeLineJoin(LineJoinStyle.Round) + ``` + + ![strokeLineJoin](figures/strokeLineJoin.jpg) + +- 通过strokeMiterLimit设置斜接长度与边框宽度比值的极限值。 + 斜接长度表示外边框外边交点到内边交点的距离,边框宽度即strokeWidth属性的值。strokeMiterLimit取值需大于等于1,且在strokeLineJoin属性取值LineJoinStyle.Miter时生效。 + + ```ts + Polyline() + .width(100) + .height(100) + .fillOpacity(0) + .stroke(Color.Red) + .strokeWidth(10) + .points([[20, 0], [20, 100], [100, 100]]) + // 设置折线拐角处为尖角 + .strokeLineJoin(LineJoinStyle.Miter) + // 设置斜接长度与线宽的比值 + .strokeMiterLimit(1/Math.sin(45)) + Polyline() + .width(100) + .height(100) + .fillOpacity(0) + .stroke(Color.Red) + .strokeWidth(10) + .points([[20, 0], [20, 100], [100, 100]]) + .strokeLineJoin(LineJoinStyle.Miter) + .strokeMiterLimit(1.42) + ``` + + ![2023032405917](figures/2023032405917.jpg) + +- 通过antiAlias设置是否开启抗锯齿,默认值为true(开启抗锯齿)。 + + ```ts + //开启抗锯齿 + Circle() + .width(150) + .height(200) + .fillOpacity(0) + .strokeWidth(5) + .stroke(Color.Black) + ``` + + ![无标题](figures/无标题.png) + + ```ts + //关闭抗锯齿 + Circle() + .width(150) + .height(200) + .fillOpacity(0) + .strokeWidth(5) + .stroke(Color.Black) + .antiAlias(false) + ``` + + ![2023032411518](figures/2023032411518.jpg) + + +## 场景示例 + +- 在Shape的(-80, -5)点绘制一个封闭路径,填充颜色0x317AF7,线条宽度10,边框颜色红色,拐角样式锐角(默认值)。 + + ```ts + @Entry + @Component + struct ShapeExample { + build() { + Column({ space: 10 }) { + Shape() { + Path().width(200).height(60).commands('M0 0 L400 0 L400 150 Z') + } + .viewPort({ x: -80, y: -5, width: 500, height: 300 }) + .fill(0x317AF7) + .stroke(Color.Red) + .strokeWidth(3) + .strokeLineJoin(LineJoinStyle.Miter) + .strokeMiterLimit(5) + }.width('100%').margin({ top: 15 }) + } + } + ``` + + ![场景1](figures/场景1.jpg) + +- 绘制一个直径为150的圆,和一个直径为150、线条为红色虚线的圆环(宽高设置不一致时以短边为直径)。 + + ```ts + @Entry + @Component + struct CircleExample { + build() { + Column({ space: 10 }) { + //绘制一个直径为150的圆 + Circle({ width: 150, height: 150 }) + //绘制一个直径为150、线条为红色虚线的圆环 + Circle() + .width(150) + .height(200) + .fillOpacity(0) + .strokeWidth(3) + .stroke(Color.Red) + .strokeDashArray([1, 2]) + }.width('100%') + } + } + ``` + + ![场景2](figures/场景2.jpg) diff --git a/zh-cn/application-dev/ui/arkts-gesture-events-binding.md b/zh-cn/application-dev/ui/arkts-gesture-events-binding.md new file mode 100644 index 0000000000000000000000000000000000000000..54a577e7ca62abad6c5e5db46878c60463fb18d9 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-gesture-events-binding.md @@ -0,0 +1,125 @@ +# 绑定手势方法 + + +通过给各个组件绑定不同的手势事件,并设计事件的响应方式,当手势识别成功时,ArkUI框架将通过事件回调通知组件手势识别的结果。 + + +## gesture(常规手势绑定方法) + + +```ts +.gesture(gesture: GestureType, mask?: GestureMask) +``` + +gesture为通用的一种手势绑定方法,可以将手势绑定到对应的组件上。 + +例如,可以将点击手势TapGesture通过gesture手势绑定方法绑定到Text组件上。 + + +```ts +// xxx.ets +@Entry +@Component +struct Index { + build() { + Column() { + Text('Gesture').fontSize(28) + // 采用gesture手势绑定方法绑定TapGesture + .gesture( + TapGesture() + .onAction(() => { + console.info('TapGesture is onAction'); + })) + } + .height(200) + .width(250) + } +} +``` + + +## priorityGesture(带优先级的手势绑定方法) + + +```ts +.priorityGesture(gesture: GestureType, mask?: GestureMask) +``` + +priorityGesture是带优先级的手势绑定方法,可以在组件上绑定优先识别的手势。 + +在默认情况下,当父组件和子组件使用gesture绑定同类型的手势时,子组件优先识别通过gesture绑定的手势。当父组件使用priorityGesture绑定与子组件同类型的手势时,父组件优先识别通过priorityGesture绑定的手势。 + +例如,当父组件Column和子组件Text同时绑定TapGesture手势时,父组件以带优先级手势priorityGesture的形式进行绑定时,优先响应父组件绑定的TapGesture。 + + + +```ts +// xxx.ets +@Entry +@Component +struct Index { + build() { + Column() { + Text('Gesture').fontSize(28) + .gesture( + TapGesture() + .onAction(() => { + console.info('Text TapGesture is onAction'); + })) + } + .height(200) + .width(250) + // 设置为priorityGesture时,点击文本区域会忽略Text组件的TapGesture手势事件,优先响应父组件Column的TapGesture手势事件 + .priorityGesture( + TapGesture() + .onAction(() => { + console.info('Column TapGesture is onAction'); + }), GestureMask.IgnoreInternal) + } +} +``` + + +## parallelGesture(并行手势绑定方法) + + +```ts +.parallelGesture(gesture: GestureType, mask?: GestureMask) +``` + +parallelGesture是并行的手势绑定方法,可以在父子组件上绑定可以同时响应的相同手势。 + +在默认情况下,手势事件为非冒泡事件,当父子组件绑定相同的手势时,父子组件绑定的手势事件会发生竞争,最多只有一个组件的手势事件能够获得响应。而当父组件绑定了并行手势parallelGesture时,父子组件相同的手势事件都可以触发,实现类似冒泡效果。 + + + +```ts +// xxx.ets +@Entry +@Component +struct Index { + build() { + Column() { + Text('Gesture').fontSize(28) + .gesture( + TapGesture() + .onAction(() => { + console.info('Text TapGesture is onAction'); + })) + } + .height(200) + .width(250) + // 设置为parallelGesture时,点击文本区域会同时响应父组件Column和子组件Text的TapGesture手势事件 + .parallelGesture( + TapGesture() + .onAction(() => { + console.info('Column TapGesture is onAction'); + }), GestureMask.IgnoreInternal) + } +} +``` + + +>**说明:** +> +>当父组件和子组件同时绑定单击手势事件和双击手势事件时,父组件和子组件均只响应单击手势事件。 diff --git a/zh-cn/application-dev/ui/arkts-gesture-events-combined-gestures.md b/zh-cn/application-dev/ui/arkts-gesture-events-combined-gestures.md new file mode 100644 index 0000000000000000000000000000000000000000..0daac802ab62a4411978ce6b39df04560a8af4c3 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-gesture-events-combined-gestures.md @@ -0,0 +1,202 @@ +# 组合手势 + + +组合手势由多种单一手势组合而成,通过在GestureGroup中使用不同的GestureMode来声明该组合手势的类型,支持[连续识别](#连续识别)、[并行识别](#并行识别)和[互斥识别](#互斥识别)三种类型。 + + + +```ts +GestureGroup(mode:GestureMode, ...gesture:GestureType[]) +``` + + +- mode:必选参数,为GestureMode枚举类。用于声明该组合手势的类型。 + +- gesture:必选参数,为由多个手势组合而成的数组。用于声明组合成该组合手势的各个手势。 + + +## 连续识别 + +连续识别组合手势对应的GestureMode为Sequence。连续识别组合手势将按照手势的注册顺序识别手势,直到所有的手势识别成功。当连续识别组合手势中有一个手势识别失败时,所有的手势识别失败。 + +以一个由长按手势和拖动手势组合而成的连续手势为例: + +在一个Column组件上绑定了translate属性,通过修改该属性可以设置组件的位置移动。然后在该组件上绑定LongPressGesture和PanGesture组合而成的Sequence组合手势。当触发LongPressGesture时,更新显示的数字。当长按后进行拖动时,根据拖动手势的回调函数,实现组件的拖动。 + + + +```ts +// xxx.ets +@Entry +@Component +struct Index { + @State offsetX: number = 0; + @State offsetY: number = 0; + @State count: number = 0; + @State positionX: number = 0; + @State positionY: number = 0; + @State borderStyles: BorderStyle = BorderStyle.Solid + + build() { + Column() { + Text('sequence gesture\n' + 'LongPress onAction:' + this.count + '\nPanGesture offset:\nX: ' + this.offsetX + '\n' + 'Y: ' + this.offsetY) + .fontSize(28) + } + // 绑定translate属性可以实现组件的位置移动 + .translate({ x: this.offsetX, y: this.offsetY, z: 0 }) + .height(250) + .width(300) + //以下组合手势为顺序识别,当长按手势事件未正常触发时不会触发拖动手势事件 + .gesture( + // 声明该组合手势的类型为Sequence类型 + GestureGroup(GestureMode.Sequence, + // 该组合手势第一个触发的手势为长按手势,且长按手势可多次响应 + LongPressGesture({ repeat: true }) + // 当长按手势识别成功,增加Text组件上显示的count次数 + .onAction((event: GestureEvent) => { + if (event.repeat) { + this.count++; + } + console.info('LongPress onAction'); + }) + .onActionEnd(() => { + console.info('LongPress end'); + }), + // 当长按之后进行拖动,PanGesture手势被触发 + PanGesture() + .onActionStart(() => { + this.borderStyles = BorderStyle.Dashed; + console.info('pan start'); + }) + // 当该手势被触发时,根据回调获得拖动的距离,修改该组件的位移距离从而实现组件的移动 + .onActionUpdate((event: GestureEvent) => { + this.offsetX = this.positionX + event.offsetX; + this.offsetY = this.positionY + event.offsetY; + console.info('pan update'); + }) + .onActionEnd(() => { + this.positionX = this.offsetX; + this.positionY = this.offsetY; + this.borderStyles = BorderStyle.Solid; + }) + ) + ) + } +} +``` + + +![sequence](figures/sequence.gif) + + +>**说明:** +> +>拖拽事件是一种典型的连续识别组合手势事件,由长按手势事件和滑动手势事件组合而成。只有先长按达到长按手势事件预设置的时间后进行滑动才会触发拖拽事件。如果长按事件未达到或者长按后未进行滑动,拖拽事件均识别失败。 + + +## 并行识别 + +并行识别组合手势对应的GestureMode为Parallel。并行识别组合手势中注册的手势将同时进行识别,直到所有手势识别结束。并行识别手势组合中的手势进行识别时互不影响。 + +以在一个Column组件上绑定点击手势和双击手势组成的并行识别手势为例,由于单击手势和双击手势是并行识别,因此两个手势可以同时进行识别,二者互不干涉。 + + + +```ts +// xxx.ets +@Entry +@Component +struct Index { + @State count1: number = 0; + @State count2: number = 0; + + build() { + Column() { + Text('parallel gesture\n' + 'tapGesture count is 1:' + this.count1 + '\ntapGesture count is 2:' + this.count2 + '\n') + .fontSize(28) + } + .height(200) + .width(250) + // 以下组合手势为并行并别,单击手势识别成功后,若在规定时间内再次点击,双击手势也会识别成功 + .gesture( + GestureGroup(GestureMode.Parallel, + TapGesture({ count: 1 }) + .onAction(() => { + this.count1++; + }), + TapGesture({ count: 2 }) + .onAction(() => { + this.count2++; + }) + ) + ) + } +} +``` + + +![parallel](figures/parallel.gif) + + +>**说明:** +> +>当由单击手势和双击手势组成一个并行识别组合手势后,在区域内进行点击时,单击手势和双击手势将同时进行识别。 +> +>当只有单次点击时,单击手势识别成功,双击手势识别失败。 +> +>当有两次点击时,若两次点击相距时间在规定时间内(默认规定时间为300毫秒),触发两次单击事件和一次双击事件。 +> +>当有两次点击时,若两次点击相距时间超出规定时间,触发两次单击事件不触发双击事件。 + + +## 互斥识别 + +互斥识别组合手势对应的GestureMode为Exclusive。互斥识别组合手势中注册的手势将同时进行识别,若有一个手势识别成功,则结束手势识别,其他所有手势识别失败。 + +以在一个Column组件上绑定单击手势和双击手势组合而成的互斥识别组合手势为例,由于单击手势只需要一次点击即可触发而双击手势需要两次,每次的点击事件均被单击手势消费而不能积累成双击手势,所以双击手势无法触发。 + + + +```ts +// xxx.ets +@Entry +@Component +struct Index { + @State count1: number = 0; + @State count2: number = 0; + + build() { + Column() { + Text('parallel gesture\n' + 'tapGesture count is 1:' + this.count1 + '\ntapGesture count is 2:' + this.count2 + '\n') + .fontSize(28) + } + .height(200) + .width(250) + //以下组合手势为互斥并别,单击手势识别成功后,双击手势会识别失败 + .gesture( + GestureGroup(GestureMode.Exclusive, + TapGesture({ count: 1 }) + .onAction(() => { + this.count1++; + }), + TapGesture({ count: 2 }) + .onAction(() => { + this.count2++; + }) + ) + ) + } +} +``` + + +![exclusive](figures/exclusive.gif) + + +>**说明:** +> +>当由单击手势和双击手势组成一个互斥识别组合手势后,在区域内进行点击时,单击手势和双击手势将同时进行识别。 +> +>当只有单次点击时,单击手势识别成功,双击手势识别失败。 +> +>当有两次点击时,单击手势在第一次点击时即宣告识别成功,此时双击手势已经失败。即使在规定时间内进行了第二次点击,双击手势事件也不会进行响应,此时会触发单击手势事件的第二次识别成功。 diff --git a/zh-cn/application-dev/ui/arkts-gesture-events-single-gesture.md b/zh-cn/application-dev/ui/arkts-gesture-events-single-gesture.md new file mode 100644 index 0000000000000000000000000000000000000000..2e270b42c716f67f30fabc4dbcf4c905e70f663e --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-gesture-events-single-gesture.md @@ -0,0 +1,377 @@ +# 单一手势 + + +## 点击手势(TapGesture) + + +```ts +TapGesture(value?:{count?:number; fingers?:number}) +``` + + +点击手势支持单次点击和多次点击,拥有两个可选参数: + + +- count:非必填参数,声明该点击手势识别的连续点击次数。默认值为1,若设置小于1的非法值会被转化为默认值。如果配置多次点击,上一次抬起和下一次按下的超时时间为300毫秒。 + +- fingers:非必填参数,用于声明触发点击的手指数量,最小值为1,最大值为10,默认值为1。当配置多指时,若第一根手指按下300毫秒内未有足够的手指数按下则手势识别失败。当实际点击手指数超过配置值时,手势识别失败。 + 以在Text组件上绑定双击手势(count值为2的点击手势)为例: + + ```ts + // xxx.ets + @Entry + @Component + struct Index { + @State value: string = ""; + + build() { + Column() { + Text('Click twice').fontSize(28) + .gesture( + // 绑定count为2的TapGesture + TapGesture({ count: 2 }) + .onAction((event: GestureEvent) => { + this.value = JSON.stringify(event.fingerList[0]); + })) + Text(this.value) + } + .height(200) + .width(250) + .padding(20) + .border({ width: 3 }) + .margin(30) + } + } + ``` + + ![tap](figures/tap.gif) + + +## 长按手势(LongPressGesture) + + +```ts +LongPressGesture(value?:{fingers?:number; repeat?:boolean; duration?:number}) +``` + + +长按手势用于触发长按手势事件,触发长按手势的最少手指数量为1,最短长按事件为500毫秒,拥有三个可选参数: + + +- fingers:非必选参数,用于声明触发长按手势所需要的最少手指数量,最小值为1,最大值为10,默认值为1。 + +- repeat:非必选参数,用于声明是否连续触发事件回调,默认值为false。 + +- duration:非必选参数,用于声明触发长按所需的最短时间,单位为毫秒,默认值为500。 + + +以在Text组件上绑定可以重复触发的长按手势为例: + + + +```ts +// xxx.ets +@Entry +@Component +struct Index { + @State count: number = 0; + + build() { + Column() { + Text('LongPress OnAction:' + this.count).fontSize(28) + .gesture( + // 绑定可以重复触发的LongPressGesture + LongPressGesture({ repeat: true }) + .onAction((event: GestureEvent) => { + if (event.repeat) { + this.count++; + } + }) + .onActionEnd(() => { + this.count = 0; + }) + ) + } + .height(200) + .width(250) + .padding(20) + .border({ width: 3 }) + .margin(30) + } +} +``` + + +![longPress](figures/longPress.gif) + + +## 拖动手势(PanGesture) + + +```ts +PanGestureOptions(value?:{ fingers?:number; direction?:PanDirection; distance?:number}) +``` + + +拖动手势用于触发拖动手势事件,滑动达到最小滑动距离(默认值为5vp)时拖动手势识别成功,拥有三个可选参数: + + +- fingers:非必选参数,用于声明触发拖动手势所需要的最少手指数量,最小值为1,最大值为10,默认值为1。 + +- direction:非必选参数,用于声明触发拖动的手势方向,此枚举值支持逻辑与(&)和逻辑或(|)运算。默认值为Pandirection.All。 + +- distance:非必选参数,用于声明触发拖动的最小拖动识别距离,单位为vp,默认值为5。 + + +以在Text组件上绑定拖动手势为例,可以通过在拖动手势的回调函数中修改组件的布局位置信息来实现组件的拖动: + + + +```ts +// xxx.ets +@Entry +@Component +struct Index { + @State offsetX: number = 0; + @State offsetY: number = 0; + @State positionX: number = 0; + @State positionY: number = 0; + + build() { + Column() { + Text('PanGesture Offset:\nX: ' + this.offsetX + '\n' + 'Y: ' + this.offsetY) + .fontSize(28) + .height(200) + .width(300) + .padding(20) + .border({ width: 3 }) + // 在组件上绑定布局位置信息 + .translate({ x: this.offsetX, y: this.offsetY, z: 0 }) + .gesture( + // 绑定拖动手势 + PanGesture() + .onActionStart((event: GestureEvent) => { + console.info('Pan start'); + }) + // 当触发拖动手势时,根据回调函数修改组件的布局位置信息 + .onActionUpdate((event: GestureEvent) => { + this.offsetX = this.positionX + event.offsetX; + this.offsetY = this.positionY + event.offsetY; + }) + .onActionEnd(() => { + this.positionX = this.offsetX; + this.positionY = this.offsetY; + }) + ) + } + .height(200) + .width(250) + } +} +``` + + +![pan](figures/pan.gif) + + +>**说明:** +> +>大部分可滑动组件,如List、Grid、Scroll、Tab等组件是通过PanGesture实现滑动,在组件内部的子组件绑定[拖动手势(PanGesture)](#拖动手势pangesture)或者[滑动手势(SwipeGesture)](#滑动手势swipegesture)会导致手势竞争。 +> +>当在子组件绑定PanGesture时,在子组件区域进行滑动仅触发子组件的PanGesture。如果需要父组件响应,需要通过修改手势绑定方法或者子组件向父组件传递消息进行实现,或者通过修改父子组件的PanGesture参数distance使得拖动更灵敏。当子组件绑定SwipeGesture时,由于PanGesture和SwipeGesture触发条件不同,需要修改PanGesture和SwipeGesture的参数以达到所需效果。 + + +## 捏合手势(PinchGesture) + + +```ts +PinchGesture(value?:{fingers?:number; distance?:number}) +``` + + +捏合手势用于触发捏合手势事件,触发捏合手势的最少手指数量为2指,最大为5指,最小识别距离为3vp,拥有两个可选参数: + + +- fingers:非必选参数,用于声明触发捏合手势所需要的最少手指数量,最小值为2,最大值为5,默认值为2。 + +- distance:非必选参数,用于声明触发捏合手势的最小距离,单位为vp,默认值为3。 + + +以在Column组件上绑定三指捏合手势为例,可以通过在捏合手势的函数回调中获取缩放比例,实现对组件的缩小或放大: + + + +```ts +// xxx.ets +@Entry +@Component +struct Index { + @State scaleValue: number = 1; + @State pinchValue: number = 1; + @State pinchX: number = 0; + @State pinchY: number = 0; + + build() { + Column() { + Column() { + Text('PinchGesture scale:\n' + this.scaleValue) + Text('PinchGesture center:\n(' + this.pinchX + ',' + this.pinchY + ')') + } + .height(200) + .width(300) + .border({ width: 3 }) + .margin({ top: 100 }) + // 在组件上绑定缩放比例,可以通过修改缩放比例来实现组件的缩小或者放大 + .scale({ x: this.scaleValue, y: this.scaleValue, z: 1 }) + .gesture( + // 在组件上绑定三指触发的捏合手势 + PinchGesture({ fingers: 3 }) + .onActionStart((event: GestureEvent) => { + console.info('Pinch start'); + }) + // 当捏合手势触发时,可以通过回调函数获取缩放比例,从而修改组件的缩放比例 + .onActionUpdate((event: GestureEvent) => { + this.scaleValue = this.pinchValue * event.scale; + this.pinchX = event.pinchCenterX; + this.pinchY = event.pinchCenterY; + }) + .onActionEnd(() => { + this.pinchValue = this.scaleValue; + console.info('Pinch end'); + }) + ) + } + } +} +``` + + +![pinch](figures/pinch.png) + + +## 旋转手势(RotationGesture) + + +```ts +RotationGesture(value?:{fingers?:number; angle?:number}) +``` + + +旋转手势用于触发旋转手势事件,触发旋转手势的最少手指数量为2指,最大为5指,最小改变度数为1度,拥有两个可选参数: + + +- fingers:非必选参数,用于声明触发旋转手势所需要的最少手指数量,最小值为2,最大值为5,默认值为2。 + +- angle:非必选参数,用于声明触发旋转手势的最小改变度数,单位为deg,默认值为1。 + + +以在Text组件上绑定旋转手势实现组件的旋转为例,可以通过在旋转手势的回调函数中获取旋转角度,从而实现组件的旋转: + + + +```ts +// xxx.ets +@Entry +@Component +struct Index { + @State angle: number = 0; + @State rotateValue: number = 0; + + build() { + Column() { + Text('RotationGesture angle:' + this.angle).fontSize(28) + // 在组件上绑定旋转布局,可以通过修改旋转角度来实现组件的旋转 + .rotate({ angle: this.angle }) + .gesture( + RotationGesture() + .onActionStart((event: GestureEvent) => { + console.info('RotationGesture is onActionStart'); + }) + // 当旋转手势生效时,通过旋转手势的回调函数获取旋转角度,从而修改组件的旋转角度 + .onActionUpdate((event: GestureEvent) => { + this.angle = this.rotateValue + event.angle; + console.info('RotationGesture is onActionEnd'); + }) + // 当旋转结束抬手时,固定组件在旋转结束时的角度 + .onActionEnd(() => { + this.rotateValue = this.angle; + console.info('RotationGesture is onActionEnd'); + }) + .onActionCancel(() => { + console.info('RotationGesture is onActionCancel'); + }) + ) + } + .height(200) + .width(250) + } +} +``` + + +![rotation](figures/rotation.png) + + +## 滑动手势(SwipeGesture) + + +```ts +SwipeGesture(value?:{fingers?:number; direction?:SwipeDirection; speed?:number}) +``` + + +滑动手势用于触发滑动事件,当滑动速度大于100vp/s时可以识别成功,拥有三个可选参数: + + +- fingers:非必选参数,用于声明触发滑动手势所需要的最少手指数量,最小值为1,最大值为10,默认值为1。 + +- direction:非必选参数,用于声明触发滑动手势的方向,此枚举值支持逻辑与(&)和逻辑或(|)运算。默认值为SwipeDirection.All。 + +- speed:非必选参数,用于声明触发滑动的最小滑动识别速度,单位为vp/s,默认值为100。 + + +以在Column组件上绑定滑动手势实现组件的旋转为例: + + + +```ts +// xxx.ets +@Entry +@Component +struct Index { + @State rotateAngle: number = 0; + @State speed: number = 1; + + build() { + Column() { + Column() { + Text("SwipeGesture speed\n" + this.speed) + Text("SwipeGesture angle\n" + this.rotateAngle) + } + .border({ width: 3 }) + .width(300) + .height(200) + .margin(100) + // 在Column组件上绑定旋转,通过滑动手势的滑动速度和角度修改旋转的角度 + .rotate({ angle: this.rotateAngle }) + .gesture( + // 绑定滑动手势且限制仅在竖直方向滑动时触发 + SwipeGesture({ direction: SwipeDirection.Vertical }) + // 当滑动手势触发时,获取滑动的速度和角度,实现对组件的布局参数的修改 + .onAction((event: GestureEvent) => { + this.speed = event.speed; + this.rotateAngle = event.angle; + }) + ) + } + } +} +``` + + +![swipe](figures/swipe.gif) + + +>**说明:** +> +>当SwipeGesture和PanGesture同时绑定时,若二者是以默认方式或者互斥方式进行绑定时,会发生竞争。SwipeGesture的触发条件为滑动速度达到100vp/s,PanGesture的触发条件为滑动距离达到5vp,先达到触发条件的手势触发。可以通过修改SwipeGesture和PanGesture的参数以达到不同的效果。 diff --git a/zh-cn/application-dev/ui/arkts-graphics-display.md b/zh-cn/application-dev/ui/arkts-graphics-display.md new file mode 100644 index 0000000000000000000000000000000000000000..9caa1099285ce8c39dc0d043f174ba98656aff41 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-graphics-display.md @@ -0,0 +1,327 @@ +# 显示图片 + + +开发者经常需要在应用中显示一些图片,例如:按钮中的logo、网络图片、本地图片等。在应用中显示图片需要使用Image组件实现,Image支持多种图片格式,包括png、jpg、bmp、svg和gif,具体用法请参考[Image](../reference/arkui-ts/ts-basic-components-image.md)组件。 + + +Image通过调用接口来创建,接口调用形式如下: + + + +```ts +Image(src: string | Resource | media.PixelMap) +``` + + +该接口通过图片数据源获取图片,支持本地图片和网络图片的渲染展示。其中,src是图片的数据源,加载方式请参考[加载图片资源](#加载图片资源)。 + + +## 加载图片资源 + +Image支持加载存档图、多媒体像素图两种类型。 + + +### 存档图类型数据源 + +存档图类型的数据源可以分为本地资源、网络资源、Resource资源、媒体库datashare资源和base64。 + +- 本地资源 + 创建文件夹,将本地图片放入ets文件夹下的任意位置。 + + Image组件引入本地图片路径,即可显示图片(根目录为ets文件夹)。 + + ```ts + Image('images/view.jpg') + .width(200) + ``` + +- 网络资源 + 引入网络图片需申请权限ohos.permission.INTERNET,具体申请方式请参考[权限申请声明](../security/accesstoken-guidelines.md)。此时,Image组件的src参数为网络图片的链接。 + + ```ts + Image('https://www.example.com/example.JPG') // 实际使用时请替换为真实地址 + ``` + +- Resource资源 + 使用资源格式可以跨包/跨模块引入图片,resources文件夹下的图片都可以通过$r资源接口读 取到并转换到Resource格式。 + + **图1** resouces   + + ![image-resource](figures/image-resource.jpg) + + 调用方式: + + ``` + Image($r('app.media.icon')) + ``` + + 还可以将图片放在rawfile文件夹下。 + + **图2** rawfile   + + ![image-rawfile](figures/image-rawfile.jpg) + + 调用方式: + + ``` + Image($rawfile('snap')) + ``` + +- 媒体库datashare + 支持datashare://路径前缀的字符串,用于访问通过媒体库提供的图片路径。 + + 1. 调用接口获取图库的照片url。 + ​ + ```ts + import picker from '@ohos.file.picker'; + + @Entry + @Component + struct Index { + private imgDatas: string[] = []; + // 获取照片url集 + async getAllImg() { + let photoPicker = new picker.PhotoViewPicker(); + let result = new Array(); + try { + let PhotoSelectOptions = new picker.PhotoSelectOptions(); + PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE; + PhotoSelectOptions.maxSelectNumber = 5; + let photoPicker = new picker.PhotoViewPicker(); + photoPicker.select(PhotoSelectOptions).then((PhotoSelectResult) => { + result = PhotoSelectResult.photoUris; + }).catch((err) => { + console.error(`PhotoViewPicker.select failed with. Code: ${err.code}, message: ${err.message}`); + }); + } catch (err) { + console.error(`PhotoViewPicker failed with. Code: ${err.code}, message: ${err.message}`); } + return result; + } + + // aboutToAppear中调用上述函数,获取图库的所有图片url,存在imgDatas中 + async aboutToAppear() { + this.imgDatas = await this.getAllImg(); + } + // 使用imgDatas的url加载图片。 + build() { + Grid() { + ForEach(this.imgDatas, item => { + GridItem() { + Image(item) + .width(200) + } + }, item => JSON.stringify(item)) + } + } + } + ``` + 2. 从媒体库获取的url格式通常如下。 + ​ + ```ts + Image('datashare:///media/5') + .width(200) + ``` + +- base64 + 路径格式为data:image/[png|jpeg|bmp|webp];base64,[base64 data],其中[base64 data]为Base64字符串数据。 + + Base64格式字符串可用于存储图片的像素数据,在网页上使用较为广泛。 + + +### 多媒体像素图 + +PixelMap是图片解码后的像素图,具体用法请参考[图片开发指导](../media/image.md)。以下示例将加载的网络图片返回的数据解码成PixelMap格式,再显示在Image组件上, + +1. 创建PixelMap状态变量。 + + ```ts + @State image: PixelMap = undefined; + ``` + +2. 引用多媒体。 + 请求网络图片请求,解码编码PixelMap。 + + 1. 引用网络权限与媒体库权限。 + ​ + ```ts + import http from '@ohos.net.http'; + import ResponseCode from '@ohos.net.http'; + import image from '@ohos.multimedia.image'; + ``` + 2. 填写网络图片地址。 + ​ + ```ts + http.createHttp().request("https://www.example.com/xxx.png", + (error, data) => { + if (error){ + console.error(`http reqeust failed with. Code: ${error.code}, message: ${error.message}`); + } else { + } + } + ) + ``` + 3. 将网络地址成功返回的数据,编码转码成pixelMap的图片格式。 + ​ + ```ts + let code = data.responseCode; + if(ResponseCode.ResponseCode.OK === code) { + let imageSource = image.createImageSource(data.result); + let options = { + alphaType: 0, // 透明度 + editable: false, // 是否可编辑 + pixelFormat: 3, // 像素格式 + scaleMode: 1, // 缩略值 + size: {height: 100, width: 100} + } // 创建图片大小 + imageSource.createPixelMap(options).then((pixelMap) => { + this.image = pixelMap + }) + ``` + 4. 显示图片。 + ​ + ```ts + Button("获取网络图片") + .onClick(() => { + this.httpRequest() + }) + Image(this.image).height(100).width(100) + ``` + + +## 显示矢量图 + +Image组件可显示矢量图(svg格式的图片),支持的svg标签为:svg、rect、circle、ellipse、path、line、polyline、polygon和animate。 + +svg格式的图片可以使用fillColor属性改变图片的绘制颜色。 + + +```ts +Image($r('app.media.cloud')).width(50) +.fillColor(Color.Blue) +``` + + **图3** 原始图片   +![屏幕截图_20230223_141141](figures/屏幕截图_20230223_141141.png) + + **图4** 设置绘制颜色后的svg图片   +![屏幕截图_20230223_141404](figures/屏幕截图_20230223_141404.png) + + +## 添加属性 + +给Image组件设置属性可以使图片显示更灵活,达到一些自定义的效果。以下是几个常用属性的使用示例,完整属性信息详见[Image](../reference/arkui-ts/ts-basic-components-image.md)。 + + +### 设置图片缩放类型 + +通过objectFit属性使图片缩放到高度和宽度确定的框内。 + + +```ts +@Entry +@Component +struct MyComponent { + scroller: Scroller = new Scroller() + + build() { + Scroll(this.scroller) { + Row() { + Image($r('app.media.img_2')).width(200).height(150) + .border({ width: 1 }) + .objectFit(ImageFit.Contain).margin(15) // 保持宽高比进行缩小或者放大,使得图片完全显示在显示边界内。 + .overlay('Contain', { align: Alignment.Bottom, offset: { x: 0, y: 20 } }) + Image($r('app.media.ic_img_2')).width(200).height(150) + .border({ width: 1 }) + .objectFit(ImageFit.Cover).margin(15) + // 保持宽高比进行缩小或者放大,使得图片两边都大于或等于显示边界。 + .overlay('Cover', { align: Alignment.Bottom, offset: { x: 0, y: 20 } }) + Image($r('app.media.img_2')).width(200).height(150) + .border({ width: 1 }) + // 自适应显示。 + .objectFit(ImageFit.Auto).margin(15) + .overlay('Auto', { align: Alignment.Bottom, offset: { x: 0, y: 20 } }) + } + Row() { + Image($r('app.media.img_2')).width(200).height(150) + .border({ width: 1 }) + .objectFit(ImageFit.Fill).margin(15) + // 不保持宽高比进行放大缩小,使得图片充满显示边界。 + .overlay('Fill', { align: Alignment.Bottom, offset: { x: 0, y: 20 } }) + Image($r('app.media.img_2')).width(200).height(150) + .border({ width: 1 }) + // 保持宽高比显示,图片缩小或者保持不变。 + .objectFit(ImageFit.ScaleDown).margin(15) + .overlay('ScaleDown', { align: Alignment.Bottom, offset: { x: 0, y: 20 } }) + Image($r('app.media.img_2')).width(200).height(150) + .border({ width: 1 }) + // 保持原有尺寸显示。 + .objectFit(ImageFit.None).margin(15) + .overlay('None', { align: Alignment.Bottom, offset: { x: 0, y: 20 } }) + } + } + } +} +``` + +![zh-cn_image_0000001511421240](figures/zh-cn_image_0000001511421240.png) + + +### 同步加载图片 + +一般情况下,图片加载流程会异步进行,以避免阻塞主线程,影响UI交互。但是特定情况下,图片刷新时会出现闪烁,这时可以使用syncLoad属性,使图片同步加载,从而避免出现闪烁。不建议图片加载较长时间时使用,会导致页面无法响应。 + + +```ts +Image($r('app.media.icon')) + .syncLoad(true) +``` + + +## 事件调用 + +通过在Image组件上绑定onComplete事件,图片加载成功后可以获取图片的必要信息。如果图片加载失败,也可以通过绑定onError回调来获得结果。 + + +```ts +@Entry +@Component +struct MyComponent { + @State widthValue: number = 0 + @State heightValue: number = 0 + @State componentWidth: number = 0 + @State componentHeight: number = 0 + + build() { + Column() { + Row() { + Image($r('app.media.ic_img_2')) + .width(200) + .height(150) + .margin(15) + .onComplete((msg: { + width: number, + height: number, + componentWidth: number, + componentHeight: number + }) => { + this.widthValue = msg.width + this.heightValue = msg.height + this.componentWidth = msg.componentWidth + this.componentHeight = msg.componentHeight + }) + // 图片获取失败,打印结果 + .onError(() => { + console.info('load image fail') + }) + .overlay('\nwidth: ' + String(this.widthValue) + ', height: ' + String(this.heightValue) + '\ncomponentWidth: ' + String(this.componentWidth) + '\ncomponentHeight: ' + String(this.componentHeight), { + align: Alignment.Bottom, + offset: { x: 0, y: 60 } + }) + } + } + } +} +``` + + +![zh-cn_image_0000001511740460](figures/zh-cn_image_0000001511740460.png) diff --git a/zh-cn/application-dev/ui/arkts-layout-development-create-grid.md b/zh-cn/application-dev/ui/arkts-layout-development-create-grid.md new file mode 100644 index 0000000000000000000000000000000000000000..743acc8765bf38604e720d5c1cceafc7d74a0c22 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-layout-development-create-grid.md @@ -0,0 +1,338 @@ +# 创建网格 + + +## 概述 + +网格布局是由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局。网格布局具有较强的页面均分能力,子组件占比控制能力,是一种重要自适应布局,其使用场景有九宫格图片展示、日历、计算器等。 + +ArkUI提供了[Grid](../reference/arkui-ts/ts-container-grid.md)容器组件和子组件[GridItem](../reference/arkui-ts/ts-container-griditem.md),用于构建网格布局。Grid用于设置网格布局相关参数,GridItem定义子组件相关特征。Grid组件支持使用条件渲染、循环渲染、[懒加载](../quick-start/arkts-rendering-control-lazyforeach.md)等方式生成子组件。 + + +## 布局与约束 + +Grid组件为网格容器,其中容器内个条目对应一个GridItem组件,如下图所示。 + + **图1** Grid与GridItem组件关系 +![zh-cn_image_0000001511900472](figures/zh-cn_image_0000001511900472.png) + +>**说明:** +> +>Grid的子组件必须是GridItem组件。 + +网格布局是一种二维布局。Grid组件支持自定义行列数和每行每列尺寸占比、设置子组件横跨几行或者几列,同时提供了垂直和水平布局能力。当网格容器组件尺寸发生变化时,所有子组件以及间距会等比例调整,从而实现网格布局的自适应能力。根据Grid的这些布局能力,可以构建出不同样式的网格布局,如下图所示。 + + **图2** 网格布局   +![zh-cn_image_0000001562700473](figures/zh-cn_image_0000001562700473.png) + +如果Grid组件设置了宽高属性,则其尺寸为设置值。如果没有设置宽高属性,Grid组件的尺寸默认适应其父组件的尺寸。 + +Grid组件根据行列数量与占比属性的设置,可以分为三种布局情况: + +- 行、列数量与占比同时设置:Grid只展示固定行列数的元素,其余元素不展示,且Grid不可滚动。 + +- 只设置行、列数量与占比中的一个:元素按照设置的方向进行排布,超出的元素可通过滚动的方式展示。 + +- 行列数量与占比都不设置:元素在布局方向上排布,其行列数由布局方向、单个网格的宽高等多个属性共同决定。超出行列容纳范围的元素不展示,且Grid不可滚动。 + +>**说明:** +> +>推荐优先采用确定的行或列数量占比方式进行布局。 + + +## 设置排列方式 + + +### 设置行列数量与占比 + +通过设置行列数量与尺寸占比可以确定网格布局的整体排列方式。Grid组件提供了rowsTemplate和columnsTemplate属性用于设置网格布局行列数量与尺寸占比。 + +rowsTemplate和columnsTemplate属性值是一个由多个空格和'数字+fr'间隔拼接的字符串,fr的个数即网格布局的行或列数,fr前面的数值大小,用于计算该行或列在网格布局宽度上的占比,最终决定该行或列宽度。 + + **图3** 行列数量占比示例 +![zh-cn_image_0000001562820833](figures/zh-cn_image_0000001562820833.png) + +如上图所示,构建的是一个三行三列的的网格布局,其在垂直方向上分为三等份,每行占一份;在水平方向上分为四等份,第一列占一份,第二列占两份,第三列占一份。 + +只要将rowsTemplate的值为'1fr 1fr 1fr',同时将columnsTemplate的值为'1fr 2fr 1fr',即可实现上述网格布局。 + + +```ts +Grid() { + ... +} +.rowsTemplate('1fr 1fr 1fr') +.columnsTemplate('1fr 2fr 1fr') +``` + +>**说明:** +> +>当Grid组件设置了rowsTemplate或columnsTemplate时,Grid的layoutDirection、maxCount、minCount、cellLength属性不生效,属性说明可参考[Grid-属性](../reference/arkui-ts/ts-container-grid.md#属性)。 + + +### 设置子组件所占行列数 + +除了大小相同的等比例网格布局,由不同大小的网格组成不均匀分布的网格布局场景在实际应用中十分常见,如下图所示。在Grid组件中,通过设置GridItem的rowStart、rowEnd、columnStart和columnEnd可以实现如图所示的单个网格横跨多行或多列的场景。 + + **图4** 不均匀网格布局 + +![zh-cn_image_0000001511900480](figures/zh-cn_image_0000001511900480.png) + +例如计算器的按键布局就是常见的不均匀网格布局场景。如下图,计算器中的按键“0”和“=”,按键“0”横跨第一、二两列,按键“=”横跨第五、六两行。使用Grid构建的网格布局,其行列标号从1开始,依次编号。 + + **图5** 计算器   + +![zh-cn_image_0000001511421292](figures/zh-cn_image_0000001511421292.png) + +在单个网格单元中,rowStart和rowEnd属性表示指定当前元素起始行号和终点行号,columnStart和columnEnd属性表示指定当前元素的起始列号和终点列号。 + +所以“0”按键横跨第一列和第二列,只要将“0”对应GridItem的columnStart和columnEnd设为1和2,将“=”对应GridItem的的rowStart和rowEnd设为5和6即可。 + + +```ts +GridItem() { + Text(key) + ... +} +.columnStart(1) +.columnEnd(2) +``` + +“=”按键横跨第五行和第六行,只要将将“=”对应GridItem的的rowStart和rowEnd设为5和6即可。 + + +```ts +GridItem() { + Text(key) + ... +} +.rowStart(5) +.rowEnd(6) +``` + + +### 设置主轴方向 + +使用Grid构建网格布局时,若没有设置行列数量与占比,可以通过layoutDirection可以设置网格布局的主轴方向,决定子组件的排列方式。此时可以结合minCount和maxCount属性来约束主轴方向上的网格数量。 + + **图6** 主轴方向示意图 + +![zh-cn_image_0000001562700469](figures/zh-cn_image_0000001562700469.png) + +当前layoutDirection设置为Row时,先从左到右排列,排满一行再排一下一行。当前layoutDirection设置为Column时,先从上到下排列,排满一列再排一下一列,如上图所示。此时,将maxCount属性设为3,表示主轴方向上最大显示的网格单元数量为3。 + + +```ts +Grid() { + ... +} +.maxCount(3) +.layoutDirection(GridDirection.Row) +``` + +>**说明:** +> +>- layoutDirection属性仅在不设置rowsTemplate和columnsTemplate时生效,此时元素在layoutDirection方向上排列。 +>- 仅设置rowsTemplate时,Grid主轴为水平方向,交叉轴为垂直方向。 +>- 仅设置columnsTemplate时,Grid主轴为垂直方向,交叉轴为水平方向。 + + +## 在网格布局中显示数据 + +网格布局采用二维布局的方式组织其内部元素,如下图所示。 + + **图7** 通用办公服务   + +![zh-cn_image_0000001563060729](figures/zh-cn_image_0000001563060729.png) + +Grid组件可以通过二维布局的方式显示一组GridItem子组件。 + + +```ts +Grid() { + GridItem() { + Text('会议') + ... + } + + GridItem() { + Text('签到') + ... + } + + GridItem() { + Text('投票') + ... + } + + GridItem() { + Text('打印') + ... + } +} +.rowsTemplate('1fr 1fr') +.rowsTemplate('1fr 1fr') +``` + +对于内容结构相似的多个GridItem,通常更推荐使用ForEach语句中嵌套GridItem的形式,来减少重复代码。 + + +```ts +@Component +struct OfficeService { + @State services: Array = ['会议', '投票', '签到', '打印'] + ... + + build() { + Column() { + Grid() { + ForEach(this.services, service => { + GridItem() { + Text(service) + ... + } + }, service => service) + } + .rowsTemplate('1fr 1fr') + .rowsTemplate('1fr 1fr') + ... + } + ... + } +} +``` + + +## 设置行列间距 + +在两个网格单元之间的网格横向间距称为行间距,网格纵向间距称为列间距,如下图所示。 + + **图8** 网格的行列间距   + +![zh-cn_image_0000001511580908](figures/zh-cn_image_0000001511580908.png) +通过Grid的rowsGap和columnsGap可以设置网格布局的行列间距。在图5所示的计算器中,行间距为15vp,列间距为10vp。 + + +```ts +Grid() { + ... +} +.columnsGap(10) +.rowsGap(15) +``` + + +## 构建可滚动的网格布局 + +可滚动的网格布局常用在文件管理、购物或视频列表等页面中,如下图所示。在设置Grid的行列数量与占比时,如果仅设置行、列数量与占比中的一个,即仅设置rowsTemplate或仅设置columnsTemplate属性,网格单元按照设置的方向排列,超出Grid显示区域后,Grid拥有可滚动能力。 + + **图9** 横向可滚动网格布局 + +![zh-cn_image_0000001511740512](figures/zh-cn_image_0000001511740512.gif) + +如果设置的是columnsTemplate,Grid的滚动方向为垂直方向;如果设置的是rowsTemplate,Grid的滚动方向为水平方向。 + +如上图所示的横向可滚动网格布局,只要设置rowsTemplate属性的值且不设置columnsTemplate属性,当内容超出Grid组件宽度时,Grid可横向滚动进行内容展示。 + + +```ts +@Component +struct Shopping { + @State services: Array = ['直播', '进口', ...] + ... + + build() { + Column({ space: 5 }) { + Grid() { + ForEach(this.services, (service: string, index) => { + GridItem() { + ... + } + .width('25%') + }, service => service) + } + .rowsTemplate('1fr 1fr') // 只设置rowsTemplate属性,当内容超出Grid区域时,可水平滚动。 + .rowsGap(15) + ... + } + ... + } +} +``` + + +## 控制滚动位置 + +与新闻列表的返回顶部场景类似,控制滚动位置功能在网格布局中也很常用,例如下图所示日历的翻页功能。 + + **图10** 日历翻页   + +![zh-cn_image_0000001562940549](figures/zh-cn_image_0000001562940549.gif) + +Grid组件初始化时,可以绑定一个[Scroller](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-container-scroll.md/#scroller)对象,用于进行滚动控制,例如通过Scroller对象的[scrollPage](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-container-scroll.md/#scrollpage)方法进行翻页。 + + +```ts +private scroller: Scroller = new Scroller() +``` + +在日历页面中,用户在点击“下一页”按钮时,应用响应点击事件,通过指定scrollPage方法的参数next为true,滚动到下一页。 + + +```ts +Column({ space: 5 }) { + Grid(this.scroller) { + ... + } + .columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr') + ... + + Row({space: 20}) { + Button('上一页') + .onClick(() => { + this.scroller.scrollPage({ + next: false + }) + }) + + Button('下一页') + .onClick(() => { + this.scroller.scrollPage({ + next: true + }) + }) + } +} +... +``` + + +## 性能优化 + +与[长列表的处理](arkts-layout-development-create-list.md#长列表的处理)类似,[循环渲染](../quick-start/arkts-rendering-control-foreach.md)适用于数据量较小的布局场景,当构建具有大量网格项的可滚动网格布局时,推荐使用[数据懒加载](../quick-start/arkts-rendering-control-lazyforeach.md)方式实现按需迭代加载数据,从而提升列表性能。 + +关于按需加载优化的具体实现可参考[数据懒加载](../quick-start/arkts-rendering-control-lazyforeach.md)章节中的示例。 + +当使用懒加载方式渲染网格时,为了更好的滚动体验,减少滑动时出现白块,Grid组件中也可通过cachedCount属性设置GridItem的预加载数量,只在懒加载LazyForEach中生效。 + + 设置预加载数量后,会在Grid显示区域前后各缓存cachedCount\*列数个GridItem,超出显示和缓存范围的GridItem会被释放。 + +```ts +Grid() { + LazyForEach(this.dataSource, item => { + GridItem() { + ... + } + }) +} +.cachedCount(3) +``` + +>**说明:** +> +>cachedCount的增加会增大UI的CPU、内存开销。使用时需要根据实际情况,综合性能和用户体验进行调整。 + +## 相关实例 + +如需详细了解网格布局的实现,请参考以下示例: + +- [分布式计算器](https://gitee.com/openharmony/applications_app_samples/tree/master/Preset/EtsDistributedCalc) diff --git a/zh-cn/application-dev/ui/arkts-layout-development-create-list.md b/zh-cn/application-dev/ui/arkts-layout-development-create-list.md new file mode 100644 index 0000000000000000000000000000000000000000..e3761f3817d8d51c1181f3dc21847fdd34ff5aa2 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-layout-development-create-list.md @@ -0,0 +1,842 @@ +# 创建列表 + + +## 概述 + +列表是一种复杂的容器,当列表项达到一定数量,内容超过屏幕大小时,可以自动提供滚动功能。它适合用于呈现同类数据类型或数据类型集,例如图片和文本。在列表中显示数据集合是许多应用程序中的常见要求(如通讯录、音乐列表、购物清单等)。 + +使用列表可以轻松高效地显示结构化、可滚动的信息。通过在[List](../reference/arkui-ts/ts-container-list.md/)组件中按垂直或者水平方向线性排列子组件[ListItemGroup](../reference/arkui-ts/ts-container-listitemgroup.md/)或[ListItem](../reference/arkui-ts/ts-container-listitem.md),为列表中的行或列提供单个视图,或使用[循环渲染](../quick-start/arkts-rendering-control-foreach.md)迭代一组行或列,或混合任意数量的单个视图和ForEach结构,构建一个列表。List组件支持使用条件渲染、循环渲染、懒加载等[渲染控制](../quick-start/arkts-rendering-control-overview.md)方式生成子组件。 + + +## 布局与约束 + +列表作为一种容器,会自动按其滚动方向排列子组件,向列表中添加组件或从列表中移除组件会重新排列子组件。 + +如下图所示,在垂直列表中,List按垂直方向自动排列ListItemGroup或ListItem。 + +ListItemGroup用于列表数据的分组展示,其子组件也是ListItem。ListItem表示单个列表项,可以包含单个子组件。 + + **图1** List、ListItemGroup和ListItem组件关系   + +![zh-cn_image_0000001562940589](figures/zh-cn_image_0000001562940589.png) + +>**说明:** +> +>List的子组件必须是ListItemGroup或ListItem,ListItem和ListItemGroup必须配合List来使用。 + + +### 布局 + +List除了提供垂直和水平布局能力、超出屏幕时可以滚动的自适应[延伸能力](../key-features/multi-device-app-dev/adaptive-layout.md/#%E5%BB%B6%E4%BC%B8%E8%83%BD%E5%8A%9B)之外,还提供了自适应交叉轴方向上排列个数的布局能力。 + +利用垂直布局能力可以构建单列或者多列垂直滚动列表,如下图所示。 + + **图2** 垂直滚动列表(左:单列;右:多列)   + +![zh-cn_image_0000001511580940](figures/zh-cn_image_0000001511580940.png) + +利用水平布局能力可以是构建单行或多行水平滚动列表,如下图所示。 + + **图3** 水平滚动列表(左:单行;右:多行)   + +![zh-cn_image_0000001511421344](figures/zh-cn_image_0000001511421344.png) + + +### 约束 + +列表的主轴方向是指子组件列的排列方向,也是列表的滚动方向。垂直于主轴的轴称为交叉轴,其方向与主轴方向相互垂直。 + +如下图所示,垂直列表的主轴是垂直方向,交叉轴是水平方向;水平列表的主轴是水平方向,交叉轴是水平方向。 + + **图4** 列表的主轴与交叉轴   + +![zh-cn_image_0000001562940581](figures/zh-cn_image_0000001562940581.png) + +如果List组件主轴或交叉轴方向设置了尺寸,则其对应方向上的尺寸为设置值。 + +如果List组件主轴方向没有设置尺寸,当List子组件主轴方向总尺寸小于List的父组件尺寸时,List主轴方向尺寸自动适应子组件的总尺寸。 + +如下图所示,一个垂直列表B没有设置高度时,其父组件A高度为200vp,若其所有子组件C的高度总和为150vp,则此时列表B的高度为150vp。 + + **图5** 列表主轴高度约束示例1(**A**: List的父组件; **B**: List组件; **C**: List的所有子组件)   + +![zh-cn_image_0000001511580956](figures/zh-cn_image_0000001511580956.png) + +如果子组件主轴方向总尺寸超过List父组件尺寸时,List主轴方向尺寸适应List的父组件尺寸。 + +如下图所示,同样是没有设置高度的垂直列表B,其父组件A高度为200vp,若其所有子组件C的高度总和为300vp,则此时列表B的高度为200vp。 + + **图6** 列表主轴高度约束示例2(**A**: List的父组件; **B**: List组件; **C**: List的所有子组件)   + +![zh-cn_image_0000001511740548](figures/zh-cn_image_0000001511740548.png) + +List组件交叉轴方向在没有设置尺寸时,其尺寸默认自适应父组件尺寸。 + + +## 开发布局 + + +### 设置主轴方向 + +List组件主轴默认是垂直方向,即默认情况下不需要手动设置List方向,就可以构建一个垂直滚动列表。 + +若是水平滚动列表场景,将List的listDirection属性设置为Axis.Horizontal即可实现。listDirection默认为Axis.Vertical,即主轴默认是垂直方向。 + + +```ts +List() { + ... +} +.listDirection(Axis.Horizontal) +``` + + +### 设置交叉轴布局 + +List组件的交叉轴布局可以通过lanes和alignListItem属性进行设置,lanes属性用于确定交叉轴排列的列表项数量,alignListItem用于设置子组件在交叉轴方向的对齐方式。 + +List组件的lanes属性通常用于在不同尺寸的设备自适应构建不同行数或列数的列表,即一次开发、多端部署的场景,例如[歌单列表](../key-features/multi-device-app-dev/music-album-page.md#%E6%AD%8C%E5%8D%95%E5%88%97%E8%A1%A8)。lanes属性的取值类型是"number | [LengthConstrain](../reference/arkui-ts/ts-types.md/#lengthconstrain)",即整数或者LengthConstrain类型。以垂直列表为例,如果将lanes属性设为2,表示构建的是一个两列的垂直列表,如图2中右图所示。lanes的默认值为1,即默认情况下,垂直列表的列数是1。 + + +```ts +List() { + ... +} +.lanes(2) +``` + +当其取值为LengthConstrain类型时,表示会根据LengthConstrain与List组件的尺寸自适应决定行或列数。 + + +```ts +List() { + ... +} +.lanes({ minLength: 200, maxLength: 300 }) +``` + +例如,假设在垂直列表中设置了lanes的值为{ minLength: 200, maxLength: 300 }。此时, + +- 当List组件宽度为300vp时,由于minLength为200vp,此时列表为一列。 + +- 当List组件宽度变化至400vp时,符合两倍的minLength,则此时列表自适应为两列。 + +>**说明:** +> +>当lanes为LengthConstrain类型时,仅用于计算当前列表的行或列数,不影响列表项本身的尺寸。 + +同样以垂直列表为例,当alignListItem属性设置为ListItemAlign.Center表示列表项在水平方向上居中对齐。alignListItem的默认值是ListItemAlign.Start,即列表项在列表交叉轴方向上默认按首部对齐。 + + +```ts +List() { + ... +} +.alignListItem(ListItemAlign.Center) +``` + + +## 在列表中显示数据 + +列表视图垂直或水平显示项目集合,在行或列超出屏幕时提供滚动功能,使其适合显示大型数据集合。在最简单的列表形式中,List静态地创建其列表项ListItem的内容。 + + **图7** 城市列表   + +![zh-cn_image_0000001563060761](figures/zh-cn_image_0000001563060761.png) + +```ts +@Component +struct CityList { + build() { + List() { + ListItem() { + Text('北京').fontSize(24) + } + + ListItem() { + Text('杭州').fontSize(24) + } + + ListItem() { + Text('上海').fontSize(24) + } + } + .backgroundColor('#FFF1F3F5') + .alignListItem(ListItemAlign.Center) + } +} +``` + +由于在ListItem中只能有一个根节点组件,不支持以平铺形式使用多个组件。因此,若列表项是由多个组件元素组成的,则需要将这多个元素组合到一个容器组件内或组成一个自定义组件。 + + **图8** 联系人列表项示例   + +![zh-cn_image_0000001511421328](figures/zh-cn_image_0000001511421328.png) + +如上图所示,联系人列表的列表项中,每个联系人都有头像和名称。此时,需要将Image和Text封装到一个Row容器内。 + + +```ts +List() { + ListItem() { + Row() { + Image($r('app.media.iconE')) + .width(40) + .height(40) + .margin(10) + + Text('小明') + .fontSize(20) + } + } + + ListItem() { + Row() { + Image($r('app.media.iconF')) + .width(40) + .height(40) + .margin(10) + + Text('小红') + .fontSize(20) + } + } +} +``` + + +## 迭代列表内容 + +通常更常见的是,应用通过数据集合动态地创建列表。使用[循环渲染](../quick-start/arkts-rendering-control-foreach.md)可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件,降低代码复杂度。 + +ArkTS通过[ForEach](../quick-start/arkts-rendering-control-foreach.md)提供了组件的循环渲染能力。以简单形式的联系人列表为例,将联系人名称和头像数据以Contact类结构存储到contacts数组,使用ForEach中嵌套ListItem的形式来代替多个平铺的、内容相似的ListItem,从而减少重复代码。 + + +```ts +import util from '@ohos.util'; + +class Contact { + key: string = util.generateRandomUUID(true); + name: string; + icon: Resource; + + constructor(name: string, icon: Resource) { + this.name = name; + this.icon = icon; + } +} + +@Entry +@Component +struct SimpleContacts { + private contacts = [ + new Contact('小明', $r("app.media.iconA")), + new Contact('小红', $r("app.media.iconB")), + ... + ] + + build() { + List() { + ForEach(this.contacts, (item: Contact) => { + ListItem() { + Row() { + Image(item.icon) + .width(40) + .height(40) + .margin(10) + Text(item.name).fontSize(20) + } + .width('100%') + .justifyContent(FlexAlign.Start) + } + }, item => item.key) + } + .width('100%') + } +} +``` + +在List组件中,ForEach除了可以用来循环渲染ListItem,也可以用来循环渲染ListItemGroup。ListItemGroup的循环渲染详细使用请参见[支持分组列表](#支持分组列表)。 + + +## 自定义列表样式 + + +### 设置内容间距 + +在初始化列表时,如需在列表项之间添加间距,可以使用space参数。例如,在每个列表项之间沿主轴方向添加10vp的间距: + + +```ts +List({ space: 10 }) { + ... +} +``` + + +### 添加分隔线 + +分隔线用来将界面元素隔开,使单个元素更加容易识别。如下图所示,当列表项左边有图标(如蓝牙图标),由于图标本身就能很好的区分,此时分隔线从图标之后开始显示即可。 + + **图9** 设置列表分隔线样式   + +![zh-cn_image_0000001511580960](figures/zh-cn_image_0000001511580960.png) + +List提供了divider属性用于给列表项之间添加分隔线。在设置divider属性时,可以通过strokeWidth和color属性设置分隔线的粗细和颜色。 + +startMargin和endMargin属性分别用于设置分隔线距离列表侧边起始端的距离和距离列表侧边结束端的距离。 + + +```ts +List() { + ... +} +.divider({ + strokeWidth: 1, + startMargin: 60, + endMargin: 10, + color: '#ffe9f0f0' +}) +``` + +此示例表示从距离列表侧边起始端60vp开始到距离结束端10vp的位置,画一条粗细为1vp的分割线,可以实现图8设置列表分隔线的样式。 + +>**说明:** +> +>1. 分隔线的宽度会使ListItem之间存在一定间隔,当List设置的内容间距小于分隔线宽度时,ListItem之间的间隔会使用分隔线的宽度。 +> +>2. 当List存在多列时,分割线的startMargin和endMargin作用于每一列上。 +> +>3. List组件的分隔线画在两个ListItem之间,第一个ListItem上方和最后一个ListItem下方不会绘制分隔线。 + + +### 添加滚动条 + +当列表项高度(宽度)超出屏幕高度(宽度)时,列表可以沿垂直(水平)方向滚动。在页面内容很多时,若用户需快速定位,可拖拽滚动条,如下图所示。 + + **图10** 列表的滚动条  + +![zh-cn_image_0000001511740544](figures/zh-cn_image_0000001511740544.gif) + +在使用List组件时,可通过scrollBar属性控制列表滚动条的显示。scrollBar的取值类型为[BarState](../reference/arkui-ts/ts-appendix-enums.md/#barstate),当取值为BarState.Auto表示按需显示滚动条。此时,当触摸到滚动条区域时显示控件,可上下拖拽滚动条快速浏览内容,拖拽时会变粗。若不进行任何操作,2秒后滚动条自动消失。 + + +```ts +List() { + ... +} +.scrollBar(BarState.Auto) +``` + + +## 支持分组列表 + +在列表中支持数据的分组展示,可以使列表显示结构清晰,查找方便,从而提高使用效率。分组列表在实际应用中十分常见,如下图所示联系人列表。 + + **图11** 联系人分组列表  + +![zh-cn_image_0000001511580948](figures/zh-cn_image_0000001511580948.png) + +在List组件中使用ListItemGroup对项目进行分组,可以构建二维列表。 + +在List组件中可以直接使用一个或者多个ListItemGroup组件,ListItemGroup的宽度默认充满List组件。在初始化ListItemGroup时,可通过header参数设置列表分组的头部组件。 + + +```ts +@Component +struct ContactsList { + ... + + @Builder itemHead(text: string) { + // 列表分组的头部组件,对应联系人分组A、B等位置的组件 + Text(text) + .fontSize(20) + .backgroundColor('#fff1f3f5') + .width('100%') + .padding(5) + } + + build() { + List() { + ListItemGroup({ header: this.itemHead('A') }) { + // 循环渲染分组A的ListItem + ... + } + ... + + ListItemGroup({ header: this.itemHead('B') }) { + // 循环渲染分组B的ListItem + ... + } + ... + } + } +} +``` + +如果多个ListItemGroup结构类似,可以将多个分组的数据组成数组,然后使用ForEach对多个分组进行循环渲染。例如在联系人列表中,将每个分组的联系人数据contacts(可参考[迭代列表内容](#迭代列表内容)章节)和对应分组的标题title数据进行组合,定义为数组contactsGroups。 + + +```ts +contactsGroups: object[] = [ + { + title: 'A', + contacts: [ + new Contact('艾佳', $r('app.media.iconA')), + new Contact('安安', $r('app.media.iconB')), + new Contact('Angela', $r('app.media.iconC')), + ], + }, + { + title: 'B', + contacts: [ + new Contact('白叶', $r('app.media.iconD')), + new Contact('伯明', $r('app.media.iconE')), + ], + }, + ... +] +``` + +然后在ForEach中对contactsGroups进行循环渲染,即可实现多个分组的联系人列表。 + + +```ts +List() { + // 循环渲染ListItemGroup,contactsGroups为多个分组联系人contacts和标题title的数据集合 + ForEach(this.contactsGroups, item => { + ListItemGroup({ header: this.itemHead(item.title) }) { + // 循环渲染ListItem + ForEach(item.contacts, (contact) => { + ListItem() { + ... + } + }, item => item.key) + } + ... + }) +} +``` + + +## 添加粘性标题 + +粘性标题是一种常见的标题模式,常用于定位字母列表的头部元素。如下图所示,在联系人列表中滚动A部分时,B部分开始的头部元素始终处于A的下方。而在开始滚动B部分时,B的头部会固定在屏幕顶部,直到所有B的项均完成滚动后,才被后面的头部替代。 + +粘性标题不仅有助于阐明列表中数据的表示形式和用途,还可以帮助用户在大量信息中进行数据定位,从而避免用户在标题所在的表的顶部与感兴趣区域之间反复滚动。 + + **图12** 粘性标题   + +![zh-cn_image_0000001511740552](figures/zh-cn_image_0000001511740552.gif) + +List组件的sticky属性配合ListItemGroup组件使用,用于设置ListItemGroup中的头部组件是否呈现吸顶效果或者尾部组件是否呈现吸底效果。 + +通过给List组件设置sticky属性为StickyStyle.Header,即可实现列表的粘性标题效果。如果需要支持吸底效果,可以通过footer参数初始化ListItemGroup的底部组件,并将sticky属性设置为StickyStyle.Footer。 + + +```ts +@Component +struct ContactsList { + // 定义分组联系人数据集合contactsGroups数组 + ... + + @Builder itemHead(text: string) { + // 列表分组的头部组件,对应联系人分组A、B等位置的组件 + Text(text) + .fontSize(20) + .backgroundColor('#fff1f3f5') + .width('100%') + .padding(5) + } + + build() { + List() { + // 循环渲染ListItemGroup,contactsGroups为多个分组联系人contacts和标题title的数据集合 + ForEach(this.contactsGroups, item => { + ListItemGroup({ header: this.itemHead(item.title) }) { + // 循环渲染ListItem + ForEach(item.contacts, (contact) => { + ListItem() { + ... + } + }, item => item.key) + } + ... + }) + } + .sticky(StickyStyle.Header) // 设置吸顶,实现粘性标题效果 + } +} +``` + + +## 控制滚动位置 + +控制滚动位置在实际应用中十分常见,例如当新闻页列表项数量庞大,用户滚动列表到一定位置时,希望快速滚动到列表底部或返回列表顶部。此时,可以通过控制滚动位置来实现列表的快速定位,如下图所示。 + + **图13** 返回列表顶部   + +![zh-cn_image_0000001511900520](figures/zh-cn_image_0000001511900520.gif) + +List组件初始化时,可以通过scroller参数绑定一个[Scroller](../reference/arkui-ts/ts-container-scroll.md/#scroller)对象,进行列表的滚动控制。例如,用户在新闻应用中,点击新闻页面底部的返回顶部按钮时,就可以通过Scroller对象的scrollToIndex方法使列表滚动到指定的列表项索引位置。 + +首先,需要创建一个Scroller的对象listScroller。 + + +```ts +private listScroller: Scroller = new Scroller(); +``` + +然后,通过将listScroller用于初始化List组件的scroller参数,完成listScroller与列表的绑定。在需要跳转的位置指定scrollToIndex的参数为0,表示返回列表顶部。 + + +```ts +Stack({ alignContent: Alignment.BottomEnd }) { + // 将listScroller用于初始化List组件的scroller参数,完成listScroller与列表的绑定。 + List({ space: 20, scroller: this.listScroller }) { + ... + } + ... + + Button() { + ... + } + .onClick(() => { + // 点击按钮时,指定跳转位置,返回列表顶部 + this.listScroller.scrollToIndex(0) + }) + ... +} +``` + + +## 响应滚动位置 + +许多应用需要监听列表的滚动位置变化并作出响应。例如,在联系人列表滚动时,如果跨越了不同字母开头的分组,则侧边字母索引栏也需要更新到对应的字母位置。 + +除了字母索引之外,滚动列表结合多级分类索引在应用开发过程中也很常见,例如购物应用的商品分类页面,多级分类也需要监听列表的滚动位置。 + + **图14** 字母索引响应联系人列表滚动   + +![zh-cn_image_0000001563060769](figures/zh-cn_image_0000001563060769.gif) + +如上图所示,当联系人列表从A滚动到B时,右侧索引栏也需要同步从选中A状态变成选中B状态。此场景可以通过监听List组件的onScrollIndex事件来实现,右侧索引栏需要使用字母表索引组件[AlphabetIndexer](../reference/arkui-ts/ts-container-alphabet-indexer.md/)。 + +在列表滚动时,根据列表此时所在的索引值位置firstIndex,重新计算字母索引栏对应字母的位置selectedIndex。由于AlphabetIndexer组件通过selected属性设置了选中项索引值,当selectedIndex变化时会触发AlphabetIndexer组件重新渲染,从而显示为选中对应字母的状态。 + + +```ts +... +const alphabets = ['#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', + 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']; + +@Entry +@Component +struct ContactsList { + @State selectedIndex: number = 0; + private listScroller: Scroller = new Scroller(); + ... + + build() { + Stack({ alignContent: Alignment.End }) { + List({ scroller: this.listScroller }) { + ... + } + .onScrollIndex((firstIndex: number) => { + // 根据列表滚动到的索引值,重新计算对应联系人索引栏的位置this.selectedIndex + ... + }) + ... + + // 字母表索引组件 + AlphabetIndexer({ arrayValue: alphabets, selected: 0 }) + .selected(this.selectedIndex) + ... + } + } +} +``` + +>**说明:** +> +>计算索引值时,ListItemGroup作为一个整体占一个索引值,不计算ListItemGroup内部ListItem的索引值。 + + +## 响应列表项侧滑 + +侧滑菜单在许多应用中都很常见。例如,通讯类应用通常会给消息列表提供侧滑删除功能,即用户可以通过向左侧滑列表的某一项,再点击删除按钮删除消息,如下图所示。 + + **图15** 侧滑删除列表项   + +![zh-cn_image_0000001563060773](figures/zh-cn_image_0000001563060773.gif) +ListItem的swipeAction属性可用于实现列表项的左右滑动功能。swipeAction属性方法初始化时有必填参数SwipeActionOptions,其中,start参数表示设置列表项右滑时起始端滑出的组件,end参数表示设置列表项左滑时尾端滑出的组件。 + +在消息列表中,end参数表示设置ListItem左滑时尾端划出自定义组件,即删除按钮。在初始化end方法时,将滑动列表项的索引传入删除按钮组件,当用户点击删除按钮时,可以根据索引值来删除列表项对应的数据,从而实现侧滑删除功能。 + + +```ts +@Entry +@Component +struct MessageList { + @State messages: object[] = [ + // 初始化消息列表数据 + ... + ]; + + @Builder itemEnd(index: number) { + // 侧滑后尾端出现的组件 + Button({ type: ButtonType.Circle }) { + Image($r('app.media.ic_public_delete_filled')) + .width(20) + .height(20) + } + .onClick(() => { + this.messages.splice(index, 1); + }) + ... + } + + build() { + ... + List() { + ForEach(this.messages, (item, index) => { + ListItem() { + ... + } + .swipeAction({ end: this.itemEnd.bind(this, index) }) // 设置侧滑属性 + }, item => item.id.toString()) + } + ... + } +} +``` + + +## 给列表项添加标记 + +添加标记是一种无干扰性且直观的方法,用于显示通知或将注意力集中到应用内的某个区域。例如,当消息列表接收到新消息时,通常对应的联系人头像的右上方会出现标记,提示有若干条未读消息,如下图所示。 + + **图16** 给列表项添加标记   + +![zh-cn_image_0000001511580952](figures/zh-cn_image_0000001511580952.png) + +在ListItem中使用[Badge](../reference/arkui-ts/ts-container-badge.md/)组件可实现给列表项添加标记功能。Badge是可以附加在单个组件上用于信息标记的容器组件。 + +在消息列表中,若希望在联系人头像右上角添加标记,可在实现消息列表项ListItem的联系人头像时,将头像Image组件作为Badge的子组件。 + +在Badge组件中,count和position参数用于设置需要展示的消息数量和提示点显示位置,还可以通过style参数灵活设置标记的样式。 + + +```ts +Badge({ + count: 1, + position: BadgePosition.RightTop, + style: { badgeSize: 16, badgeColor: '#FA2A2D' } +}) { + // Image组件实现消息联系人头像 + ... +} +... +``` + + +## 下拉刷新与上拉加载 + +页面的下拉刷新与上拉加载功能在移动应用中十分常见,例如,新闻页面的内容刷新和加载。这两种操作的原理都是通过响应用户的[触摸事件](../reference/arkui-ts/ts-universal-events-touch.md/),在顶部或者底部显示一个刷新或加载视图,完成后再将此视图隐藏。 + +以下拉刷新为例,其实现主要分成三步: + +1. 监听手指按下事件,记录其初始位置的值。 + +2. 监听手指按压移动事件,记录并计算当前移动的位置与初始值的差值,大于0表示向下移动,同时设置一个允许移动的最大值。 + +3. 监听手指抬起事件,若此时移动达到最大值,则触发数据加载并显示刷新视图,加载完成后将此视图隐藏。 + +下拉刷新与上拉加载的具体实现可参考Codelab:[新闻数据加载](https://gitee.com/openharmony/codelabs/tree/master/NetworkManagement/NewsDataArkTS)。若开发者希望快速实现此功能,也可使用三方组件[PullToRefresh](https://gitee.com/openharmony-sig/PullToRefresh)。 + + +## 编辑列表 + +列表的编辑模式用途十分广泛,常见于待办事项管理、文件管理、备忘录的记录管理等应用场景。在列表的编辑模式下,新增和删除列表项是最基础的功能,其核心是对列表项对应的数据集合进行数据添加和删除。 + +下面以待办事项管理为例,介绍如何快速实现新增和删除列表项功能。 + + +### 新增列表项 + +如下图所示,当用户点击添加按钮时,提供用户新增列表项内容选择或填写的交互界面,用户点击确定后,列表中新增对应的项目。 + + **图17** 新增待办   + +![zh-cn_image_0000001511740556](figures/zh-cn_image_0000001511740556.gif) + +添加列表项功能实现主要流程如下: + +1. 定义列表项数据结构和初始化列表数据,构建列表整体布局和列表项。 + 以待办事项管理为例,首先定义待办数据结构: + + ```ts + import util from '@ohos.util'; + + export class ToDo { + key: string = util.generateRandomUUID(true); + name: string; + + constructor(name: string) { + this.name = name; + } + } + ``` + + 然后,初始化待办列表数据和可选事项: + + ```ts + @State toDoData: ToDo[] = []; + private availableThings: string[] = ['读书', '运动', '旅游', '听音乐', '看电影', '唱歌']; + ``` + + 最后,构建列表布局和列表项: + + ```ts + List({ space: 10 }) { + ForEach(this.toDoData, (toDoItem) => { + ListItem() { + ... + } + }, toDoItem => toDoItem.key) + } + ``` + +2. 提供新增列表项入口,即给新增按钮添加点击事件。 + +3. 响应用户确定新增事件,更新列表数据。 + 待办事项管理示例的步骤2和步骤3功能实现如下: + + ```ts + Text('+') + .onClick(() => { + TextPickerDialog.show({ + range: this.availableThings, + onAccept: (value: TextPickerResult) => { + this.toDoData.push(new ToDo(this.availableThings[value.index])); // 新增列表项数据toDoData + }, + }) + }) + ``` + + +### 删除列表项 + +如下图所示,当用户长按列表项进入删除模式时,提供用户删除列表项选择的交互界面,用户勾选完成后点击删除按钮,列表中删除对应的项目。 + + **图18** 长按删除待办事项   + +![zh-cn_image_0000001562820877](figures/zh-cn_image_0000001562820877.gif) + +删除列表项功能实现主要流程如下: + +1. 列表的删除功能一般进入编辑模式后才可使用,所以需要提供编辑模式的入口。 + 以待办列表为例,通过监听列表项的长按事件,当用户长按列表项时,进入编辑模式。 + + ```ts + // ToDoListItem.ets + + Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { + ... + } + .gesture( + GestureGroup(GestureMode.Exclusive, + LongPressGesture() + .onAction(() => { + if (!this.isEditMode) { + this.isEditMode = true; //进入编辑模式 + this.selectedItems.push(this.toDoItem); // 记录长按时选中的列表项 + } + }) + ) + ) + ``` + +2. 需要响应用户的选择交互,记录要删除的列表项数据。 + 在待办列表中,通过勾选框的勾选或取消勾选,响应用户勾选列表项变化,记录所有选择的列表项。 + + ```ts + // ToDoListItem.ets + + if (this.isEditMode) { + Checkbox() + .onChange((isSelected) => { + if (isSelected) { + this.selectedItems.push(this.toDoItem) // 勾选时,记录选中的列表项 + } else { + let index = this.selectedItems.indexOf(this.toDoItem) + if (index !== -1) { + this.selectedItems.splice(index, 1) // 取消勾选时,则将此项从selectedItems中删除 + } + } + }) + ... + } + ``` + +3. 需要响应用户点击删除按钮事件,删除列表中对应的选项。 + + ```ts + // ToDoList.ets + + Button('删除') + .onClick(() => { + // 删除选中的列表项对应的toDoData数据 + let leftData = this.toDoData.filter((item) => { + return this.selectedItems.find((selectedItem) => selectedItem !== item); + }) + + this.toDoData = leftData; + this.isEditMode = false; + }) + ... + ``` + + +## 长列表的处理 + +[循环渲染](../quick-start/arkts-rendering-control-foreach.md)适用于短列表,当构建具有大量列表项的长列表时,如果直接采用循环渲染方式,会一次性加载所有的列表元素,会导致页面启动时间过长,影响用户体验。因此,推荐使用[数据懒加载](../quick-start/arkts-rendering-control-lazyforeach.md)(LazyForEach)方式实现按需迭代加载数据,从而提升列表性能。 + +关于长列表按需加载优化的具体实现可参考[数据懒加载](../quick-start/arkts-rendering-control-lazyforeach.md)章节中的示例。 + +当使用懒加载方式渲染列表时,为了更好的列表滚动体验,减少列表滑动时出现白块,List组件提供了cachedCount参数用于设置列表项缓存数,只在懒加载LazyForEach中生效。 + + +```ts +List() { + LazyForEach(this.dataSource, item => { + ListItem() { + ... + } + }) +}.cachedCount(3) +``` + +以垂直列表为例: + +- 若懒加载是用于ListItem,当列表为单列模式时,会在List显示的ListItem前后各缓存cachedCount个ListItem;若是多列模式下,会在List显示的ListItem前后各缓存cachedCount\*列数个ListItem。 + +- 若懒加载是用于ListItemGroup,无论单列模式还是多列模式,都是在List显示的ListItem前后各缓存cachedCount个ListItemGroup。 + +>**说明:** +> +>1. cachedCount的增加会增大UI的CPU、内存开销。使用时需要根据实际情况,综合性能和用户体验进行调整。 +> +>2. 列表使用数据懒加载时,除了显示区域的列表项和前后缓存的列表项,其他列表项会被销毁。 + +## 相关实例 + +如需详细了解ArkUI中列表的创建与使用,请参考以下示例: + +- [List组件的使用之商品列表](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/List) + +- [新闻数据加载](https://gitee.com/openharmony/codelabs/tree/master/NetworkManagement/NewsDataArkTS) + +- [音乐专辑页](../key-features/multi-device-app-dev/music-album-page.md) + +[../reference/arkui-ts/ts-container-listitem.md]: \ No newline at end of file diff --git a/zh-cn/application-dev/ui/arkts-layout-development-create-looping.md b/zh-cn/application-dev/ui/arkts-layout-development-create-looping.md new file mode 100644 index 0000000000000000000000000000000000000000..41436324259f168c0ddf74f3c5ee137bb51a183a --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-layout-development-create-looping.md @@ -0,0 +1,318 @@ +# 创建轮播 + + +[Swiper](../reference/arkui-ts/ts-container-swiper.md)组件提供滑动轮播显示的能力。Swiper本身是一个容器组件,当设置了多个子组件后,可以对这些子组件进行轮播显示。通常,在一些应用首页显示推荐的内容时,需要用到轮播显示的能力。 + + +## 布局与约束 + +Swiper作为一个容器组件,在自身尺寸属性未被设置时,会自动根据子组件的大小设置自身的尺寸。如果开发者对Swiper组件设置了固定的尺寸,则在轮播显示过程中均以该尺寸生效;否则,在轮播过程中,会根据子组件的大小自动调整自身的尺寸。 + + +## 循环播放 + +通过loop属性控制是否循环播放,该属性默认值为true。 + +当loop为true时,在显示第一页或最后一页时,可以继续往前切换到前一页或者往后切换到后一页。如果loop为false,则在第一页或最后一页时,无法继续向前或者向后切换页面。 + + loop为true: + +```ts +... +private swiperController: SwiperController = new SwiperController() +... +Swiper(this.swiperController) { + Text("0") + .width('90%') + .height('100%') + .backgroundColor(Color.Gray) + .textAlign(TextAlign.Center) + .fontSize(30) + + Text("1") + .width('90%') + .height('100%') + .backgroundColor(Color.Green) + .textAlign(TextAlign.Center) + .fontSize(30) + + Text("2") + .width('90%') + .height('100%') + .backgroundColor(Color.Blue) + .textAlign(TextAlign.Center) + .fontSize(30) +} +.loop(true) +``` + +![loop_true](figures/loop_true.gif) + + loop为false: + +```ts +Swiper(this.swiperController) { + Text("0") + .width('90%') + .height('100%') + .backgroundColor(Color.Gray) + .textAlign(TextAlign.Center) + .fontSize(30) + + Text("1") + .width('90%') + .height('100%') + .backgroundColor(Color.Green) + .textAlign(TextAlign.Center) + .fontSize(30) + + Text("2") + .width('90%') + .height('100%') + .backgroundColor(Color.Blue) + .textAlign(TextAlign.Center) + .fontSize(30) +} +.loop(false) +``` + +![loop_false](figures/loop_false.gif) + + +## 自动轮播 + +Swiper通过设置autoPlay属性,控制是否自动轮播子组件。该属性默认值为false。 + +autoPlay为true时,会自动切换播放子组件,子组件与子组件之间的播放间隔通过interval属性设置。interval属性默认值为3000,单位毫秒。 + + autoPlay为true: + +```ts +Swiper(this.swiperController) { + Text("0") + .width('90%') + .height('100%') + .backgroundColor(Color.Gray) + .textAlign(TextAlign.Center) + .fontSize(30) + + Text("1") + .width('90%') + .height('100%') + .backgroundColor(Color.Green) + .textAlign(TextAlign.Center) + .fontSize(30) + + Text("2") + .width('90%') + .height('100%') + .backgroundColor(Color.Pink) + .textAlign(TextAlign.Center) + .fontSize(30) +} +.loop(true) +.autoPlay(true) +.interval(1000) +``` + +![autoPlay](figures/autoPlay.gif) + + +## 导航点样式 + +Swiper提供了默认的导航点样式,导航点默认显示在Swiper下方居中位置,开发者也可以通过indicatorStyle属性自定义导航点的位置和样式。 + +通过indicatorStyle属性,开发者可以设置导航点相对于Swiper组件上下左右四个方位的位置,同时也可以设置每个导航点的尺寸、颜色、蒙层和被选中导航点的颜色。 + + 导航点使用默认样式: + +```ts +Swiper(this.swiperController) { + Text("0") + .width('90%') + .height('100%') + .backgroundColor(Color.Gray) + .textAlign(TextAlign.Center) + .fontSize(30) + + Text("1") + .width('90%') + .height('100%') + .backgroundColor(Color.Green) + .textAlign(TextAlign.Center) + .fontSize(30) + + Text("2") + .width('90%') + .height('100%') + .backgroundColor(Color.Pink) + .textAlign(TextAlign.Center) + .fontSize(30) +} +``` + +![indicator](figures/indicator.PNG) + + 自定义导航点样式(示例:导航点直径设为30VP,左边距为0,导航点颜色设为红色): + +```ts +Swiper(this.swiperController) { + Text("0") + .width('90%') + .height('100%') + .backgroundColor(Color.Gray) + .textAlign(TextAlign.Center) + .fontSize(30) + + Text("1") + .width('90%') + .height('100%') + .backgroundColor(Color.Green) + .textAlign(TextAlign.Center) + .fontSize(30) + + Text("2") + .width('90%') + .height('100%') + .backgroundColor(Color.Pink) + .textAlign(TextAlign.Center) + .fontSize(30) +} +.indicatorStyle({ + size: 30, + left: 0, + color: Color.Red +}) +``` + +![ind](figures/ind.PNG) + + +## 页面切换方式 + +Swiper支持三种页面切换方式:手指滑动、点击导航点和通过控制器。 + + 通过控制器切换页面: + +```ts +@Entry +@Component +struct SwiperDemo { + private swiperController: SwiperController = new SwiperController(); + + build() { + Column({ space: 5 }) { + Swiper(this.swiperController) { + Text("0") + .width(250) + .height(250) + .backgroundColor(Color.Gray) + .textAlign(TextAlign.Center) + .fontSize(30) + Text("1") + .width(250) + .height(250) + .backgroundColor(Color.Green) + .textAlign(TextAlign.Center) + .fontSize(30) + Text("2") + .width(250) + .height(250) + .backgroundColor(Color.Pink) + .textAlign(TextAlign.Center) + .fontSize(30) + } + .indicator(true) + + Row({ space: 12 }) { + Button('showNext') + .onClick(() => { + this.swiperController.showNext(); // 通过controller切换到后一页 + }) + Button('showPrevious') + .onClick(() => { + this.swiperController.showPrevious(); // 通过controller切换到前一页 + }) + }.margin(5) + }.width('100%') + .margin({ top: 5 }) + } +} +``` + +![controll](figures/controll.gif) + + +## 轮播方向 + +Swiper支持水平和垂直方向上进行轮播,主要通过vertical属性控制。 + +当vertical为true时,表示在垂直方向上进行轮播;为false时,表示在水平方向上进行轮播。vertical默认值为false。 + + + 设置水平方向上轮播: + +```ts +Swiper(this.swiperController) { + ... +} +.indicator(true) +.vertical(false) +``` + + +![截图2](figures/截图2.PNG) + + + 设置垂直方向轮播: + +```ts +Swiper(this.swiperController) { + ... +} +.indicator(true) +.vertical(true) +``` + + +![截图3](figures/截图3.PNG) + + +## 每页显示多个子页面 + +Swiper支持在一个页面内同时显示多个子组件,通过[displayCount](../reference/arkui-ts/ts-container-swiper.md#%E5%B1%9E%E6%80%A7)属性设置。 + + 设置一个页面内显示两个子组件: + +```ts +Swiper(this.swiperController) { + Text("0") + .width(250) + .height(250) + .backgroundColor(Color.Gray) + .textAlign(TextAlign.Center) + .fontSize(30) + Text("1") + .width(250) + .height(250) + .backgroundColor(Color.Green) + .textAlign(TextAlign.Center) + .fontSize(30) + Text("2") + .width(250) + .height(250) + .backgroundColor(Color.Pink) + .textAlign(TextAlign.Center) + .fontSize(30) + Text("3") + .width(250) + .height(250) + .backgroundColor(Color.Blue) + .textAlign(TextAlign.Center) + .fontSize(30) +} +.indicator(true) +.displayCount(2) +``` + +![two](figures/two.PNG) diff --git a/zh-cn/application-dev/ui/arkts-layout-development-flex-layout.md b/zh-cn/application-dev/ui/arkts-layout-development-flex-layout.md new file mode 100644 index 0000000000000000000000000000000000000000..b1b551a4a8d1644618e3fa270214892b9a1cf58c --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-layout-development-flex-layout.md @@ -0,0 +1,626 @@ +# 弹性布局 + + +## 概述 + +弹性布局([Flex](../reference/arkui-ts/ts-container-flex.md))提供更加有效的方式对容器中的子元素进行排列、对齐和分配剩余空间。容器默认存在主轴与交叉轴,子元素默认沿主轴排列,子元素在主轴方向的尺寸称为主轴尺寸,在交叉轴方向的尺寸称为交叉轴尺寸。弹性布局在开发场景中用例特别多,比如页面头部导航栏的均匀分布、页面框架的搭建、多行数据的排列等等。 + + + **图1** 主轴为水平方向的Flex容器示意图   + +![flex-layout](figures/flex-layout.png) + + +## 基本概念 + +- 主轴:Flex组件布局方向的轴线,子元素默认沿着主轴排列。主轴开始的位置称为主轴起始端,结束位置称为主轴终点端。 + +- 交叉轴:垂直于主轴方向的轴线。交叉轴起始的位置称为交叉轴首部,结束位置称为交叉轴尾部。 + + +## 布局方向 + +在弹性布局中,容器的子元素可以按照任意方向排列。通过设置参数direction,可以决定主轴的方向,从而控制子组件的排列方向。 + + **图2** 弹性布局方向图   + +![flex-layout-direction](figures/flex-layout-direction.png) + +- FlexDirection.Row(默认值):主轴为水平方向,子组件从起始端沿着水平方向开始排布。 + + + ```ts + Flex({ direction: FlexDirection.Row }) { + Text('1').width('33%').height(50).backgroundColor(0xF5DEB3) + Text('2').width('33%').height(50).backgroundColor(0xD2B48C) + Text('3').width('33%').height(50).backgroundColor(0xF5DEB3) + } + .height(70) + .width('90%') + .padding(10) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001562820817](figures/zh-cn_image_0000001562820817.png) + +- FlexDirection.RowReverse:主轴为水平方向,子组件从终点端沿着FlexDirection. Row相反的方向开始排布。 + + + ```ts + Flex({ direction: FlexDirection.RowReverse }) { + Text('1').width('33%').height(50).backgroundColor(0xF5DEB3) + Text('2').width('33%').height(50).backgroundColor(0xD2B48C) + Text('3').width('33%').height(50).backgroundColor(0xF5DEB3) + } + .height(70) + .width('90%') + .padding(10) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001511900464](figures/zh-cn_image_0000001511900464.png) + +- FlexDirection.Column:主轴为垂直方向,子组件从起始端沿着垂直方向开始排布。 + + + ```ts + Flex({ direction: FlexDirection.Column }) { + Text('1').width('100%').height(50).backgroundColor(0xF5DEB3) + Text('2').width('100%').height(50).backgroundColor(0xD2B48C) + Text('3').width('100%').height(50).backgroundColor(0xF5DEB3) + } + .height(70) + .width('90%') + .padding(10) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001511580884](figures/zh-cn_image_0000001511580884.png) + +- FlexDirection.ColumnReverse:主轴为垂直方向,子组件从终点端沿着FlexDirection. Column相反的方向开始排布。 + + + ```ts + Flex({ direction: FlexDirection.ColumnReverse }) { + Text('1').width('100%').height(50).backgroundColor(0xF5DEB3) + Text('2').width('100%').height(50).backgroundColor(0xD2B48C) + Text('3').width('100%').height(50).backgroundColor(0xF5DEB3) + } + .height(70) + .width('90%') + .padding(10) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001562940541](figures/zh-cn_image_0000001562940541.png) + + +## 布局换行 + +弹性布局分为单行布局和多行布局。默认情况下,Flex容器中的子元素都排在一条线(又称“轴线”)上。wrap属性控制当子元素主轴尺寸之和大于容器主轴尺寸时,Flex是单行布局还是多行布局。在多行布局时,通过交叉轴方向,确认新行堆叠方向。 + +- FlexWrap. NoWrap(默认值):不换行。如果子组件的宽度总和大于父元素的宽度,则子组件会被压缩宽度。 + + + ```ts + Flex({ wrap: FlexWrap.NoWrap }) { + Text('1').width('50%').height(50).backgroundColor(0xF5DEB3) + Text('2').width('50%').height(50).backgroundColor(0xD2B48C) + Text('3').width('50%').height(50).backgroundColor(0xF5DEB3) + } + .width('90%') + .padding(10) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001562700425](figures/zh-cn_image_0000001562700425.png) + +- FlexWrap. Wrap:换行,每一行子组件按照主轴方向排列。 + + + ```ts + Flex({ wrap: FlexWrap.Wrap }) { + Text('1').width('50%').height(50).backgroundColor(0xF5DEB3) + Text('2').width('50%').height(50).backgroundColor(0xD2B48C) + Text('3').width('50%').height(50).backgroundColor(0xD2B48C) + } + .width('90%') + .padding(10) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001511740468](figures/zh-cn_image_0000001511740468.png) + +- FlexWrap. WrapReverse:换行,每一行子组件按照主轴反方向排列。 + + + ```ts + Flex({ wrap: FlexWrap.WrapReverse}) { + Text('1').width('50%').height(50).backgroundColor(0xF5DEB3) + Text('2').width('50%').height(50).backgroundColor(0xD2B48C) + Text('3').width('50%').height(50).backgroundColor(0xF5DEB3) + } + .width('90%') + .padding(10) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001562940521](figures/zh-cn_image_0000001562940521.png) + + +## 主轴对齐方式 + +通过justifyContent参数设置在主轴方向的对齐方式。 + +![flex-spindle-alignment](figures/flex-spindle-alignment.png) + + +- FlexAlign.Start(默认值):子组件在主轴方向起始端对齐, 第一个子组件与父元素边沿对齐,其他元素与前一个元素对齐。 + + + ```ts + Flex({ justifyContent: FlexAlign.Start }) { + Text('1').width('20%').height(50).backgroundColor(0xF5DEB3) + Text('2').width('20%').height(50).backgroundColor(0xD2B48C) + Text('3').width('20%').height(50).backgroundColor(0xF5DEB3) + } + .width('90%') + .padding({ top: 10, bottom: 10 }) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001511421280](figures/zh-cn_image_0000001511421280.png) + +- FlexAlign.Center:子组件在主轴方向居中对齐。 + + + ```ts + Flex({ justifyContent: FlexAlign.Center }) { + Text('1').width('20%').height(50).backgroundColor(0xF5DEB3) + Text('2').width('20%').height(50).backgroundColor(0xD2B48C) + Text('3').width('20%').height(50).backgroundColor(0xF5DEB3) + } + .width('90%') + .padding({ top: 10, bottom: 10 }) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001563060681](figures/zh-cn_image_0000001563060681.png) + +- FlexAlign.End:子组件在主轴方向终点端对齐, 最后一个子组件与父元素边沿对齐,其他元素与后一个元素对齐。 + + + ```ts + Flex({ justifyContent: FlexAlign.End }) { + Text('1').width('20%').height(50).backgroundColor(0xF5DEB3) + Text('2').width('20%').height(50).backgroundColor(0xD2B48C) + Text('3').width('20%').height(50).backgroundColor(0xF5DEB3) + } + .width('90%') + .padding({ top: 10, bottom: 10 }) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001562820809](figures/zh-cn_image_0000001562820809.png) + +- FlexAlign.SpaceBetween:Flex主轴方向均匀分配弹性元素,相邻子组件之间距离相同。第一个子组件和最后一个子组件与父元素边沿对齐。 + + + ```ts + Flex({ justifyContent: FlexAlign.SpaceBetween }) { + Text('1').width('20%').height(50).backgroundColor(0xF5DEB3) + Text('2').width('20%').height(50).backgroundColor(0xD2B48C) + Text('3').width('20%').height(50).backgroundColor(0xF5DEB3) + } + .width('90%') + .padding({ top: 10, bottom: 10 }) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001511421288](figures/zh-cn_image_0000001511421288.png) + +- FlexAlign.SpaceAround:Flex主轴方向均匀分配弹性元素,相邻子组件之间距离相同。第一个子组件到主轴起始端的距离和最后一个子组件到主轴终点端的距离是相邻元素之间距离的一半。 + + + ```ts + Flex({ justifyContent: FlexAlign.SpaceAround }) { + Text('1').width('20%').height(50).backgroundColor(0xF5DEB3) + Text('2').width('20%').height(50).backgroundColor(0xD2B48C) + Text('3').width('20%').height(50).backgroundColor(0xF5DEB3) + } + .width('90%') + .padding({ top: 10, bottom: 10 }) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001511900436](figures/zh-cn_image_0000001511900436.png) + +- FlexAlign.SpaceEvenly:Flex主轴方向元素等间距布局,相邻子组件之间的间距、第一个子组件与主轴起始端的间距、最后一个子组件到主轴终点端的间距均相等。 + + + ```ts + Flex({ justifyContent: FlexAlign.SpaceEvenly }) { + Text('1').width('20%').height(50).backgroundColor(0xF5DEB3) + Text('2').width('20%').height(50).backgroundColor(0xD2B48C) + Text('3').width('20%').height(50).backgroundColor(0xF5DEB3) + } + .width('90%') + .padding({ top: 10, bottom: 10 }) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001563060713](figures/zh-cn_image_0000001563060713.png) + + +## 交叉轴对齐方式 + +容器和子元素都可以设置交叉轴对齐方式,且子元素设置的对齐方式优先级较高。 + + +### 容器组件设置交叉轴对齐 + +可以通过Flex组件的alignItems参数设置子组件在交叉轴的对齐方式。 + + +- ItemAlign.Auto:使用Flex容器中默认配置。 + + + ```ts + Flex({ alignItems: ItemAlign.Auto }) { + Text('1').width('33%').height(30).backgroundColor(0xF5DEB3) + Text('2').width('33%').height(40).backgroundColor(0xD2B48C) + Text('3').width('33%').height(50).backgroundColor(0xF5DEB3) + } + .size({ width: '90%', height: 80 }) + .padding(10) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001563060677](figures/zh-cn_image_0000001563060677.png) + +- ItemAlign.Start:交叉轴方向首部对齐。 + + + ```ts + Flex({ alignItems: ItemAlign.Start }) { + Text('1').width('33%').height(30).backgroundColor(0xF5DEB3) + Text('2').width('33%').height(40).backgroundColor(0xD2B48C) + Text('3').width('33%').height(50).backgroundColor(0xF5DEB3) + } + .size({ width: '90%', height: 80 }) + .padding(10) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001562700453](figures/zh-cn_image_0000001562700453.png) + +- ItemAlign.Center:交叉轴方向居中对齐。 + + + ```ts + Flex({ alignItems: ItemAlign.Center }) { + Text('1').width('33%').height(30).backgroundColor(0xF5DEB3) + Text('2').width('33%').height(40).backgroundColor(0xD2B48C) + Text('3').width('33%').height(50).backgroundColor(0xF5DEB3) + } + .size({ width: '90%', height: 80 }) + .padding(10) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001511740484](figures/zh-cn_image_0000001511740484.png) + +- ItemAlign.End:交叉轴方向底部对齐。 + + + ```ts + Flex({ alignItems: ItemAlign.End }) { + Text('1').width('33%').height(30).backgroundColor(0xF5DEB3) + Text('2').width('33%').height(40).backgroundColor(0xD2B48C) + Text('3').width('33%').height(50).backgroundColor(0xF5DEB3) + } + .size({ width: '90%', height: 80 }) + .padding(10) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001511580876](figures/zh-cn_image_0000001511580876.png) + +- ItemAlign.Stretch:交叉轴方向拉伸填充,在未设置尺寸时,拉伸到容器尺寸。 + + + ```ts + Flex({ alignItems: ItemAlign.Stretch }) { + Text('1').width('33%').height(30).backgroundColor(0xF5DEB3) + Text('2').width('33%').height(40).backgroundColor(0xD2B48C) + Text('3').width('33%').height(50).backgroundColor(0xF5DEB3) + } + .size({ width: '90%', height: 80 }) + .padding(10) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001511421252](figures/zh-cn_image_0000001511421252.png) + +- ItemAlign. Baseline:交叉轴方向文本基线对齐。 + + + ```ts + Flex({ alignItems: ItemAlign.Baseline }) { + Text('1').width('33%').height(30).backgroundColor(0xF5DEB3) + Text('2').width('33%').height(40).backgroundColor(0xD2B48C) + Text('3').width('33%').height(50).backgroundColor(0xF5DEB3) + } + .size({ width: '90%', height: 80 }) + .padding(10) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001511900440](figures/zh-cn_image_0000001511900440.png) + + +### 子组件设置交叉轴对齐 + +子组件的[alignSelf](../reference/arkui-ts/ts-universal-attributes-flex-layout.md)属性也可以设置子组件在父容器交叉轴的对齐格式,且会覆盖Flex布局容器中alignItems配置。如下例所示: + + + +```ts +Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center }) { // 容器组件设置子组件居中 + Text('alignSelf Start').width('25%').height(80) + .alignSelf(ItemAlign.Start) + .backgroundColor(0xF5DEB3) + Text('alignSelf Baseline') + .alignSelf(ItemAlign.Baseline) + .width('25%') + .height(80) + .backgroundColor(0xD2B48C) + Text('alignSelf Baseline').width('25%').height(100) + .backgroundColor(0xF5DEB3) + .alignSelf(ItemAlign.Baseline) + Text('no alignSelf').width('25%').height(100) + .backgroundColor(0xD2B48C) + Text('no alignSelf').width('25%').height(100) + .backgroundColor(0xF5DEB3) + +}.width('90%').height(220).backgroundColor(0xAFEEEE) +``` + + +![zh-cn_image_0000001562940533](figures/zh-cn_image_0000001562940533.png) + + +上例中,Flex容器中alignItems设置交叉轴子组件的对齐方式为居中,子组件自身设置了alignSelf属性的情况,覆盖父组件的alignItems值,表现为alignSelf的定义。 + + +### 内容对齐 + +可以通过[alignContent](../reference/arkui-ts/ts-container-flex.md)参数设置子组件各行在交叉轴剩余空间内的对齐方式,只在多行的flex布局中生效,可选值有: + +- FlexAlign.Start:子组件各行与交叉轴起点对齐。 + + + ```ts + Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.Start }) { + Text('1').width('30%').height(20).backgroundColor(0xF5DEB3) + Text('2').width('60%').height(20).backgroundColor(0xD2B48C) + Text('3').width('40%').height(20).backgroundColor(0xD2B48C) + Text('4').width('30%').height(20).backgroundColor(0xF5DEB3) + Text('5').width('20%').height(20).backgroundColor(0xD2B48C) + } + .width('90%') + .height(100) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001511900460](figures/zh-cn_image_0000001511900460.png) + +- FlexAlign.Center:子组件各行在交叉轴方向居中对齐。 + + + ```ts + Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.Center }) { + Text('1').width('30%').height(20).backgroundColor(0xF5DEB3) + Text('2').width('60%').height(20).backgroundColor(0xD2B48C) + Text('3').width('40%').height(20).backgroundColor(0xD2B48C) + Text('4').width('30%').height(20).backgroundColor(0xF5DEB3) + Text('5').width('20%').height(20).backgroundColor(0xD2B48C) + } + .width('90%') + .height(100) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001511421256](figures/zh-cn_image_0000001511421256.png) + +- FlexAlign.End:子组件各行与交叉轴终点对齐。 + + + ```ts + Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.End }) { + Text('1').width('30%').height(20).backgroundColor(0xF5DEB3) + Text('2').width('60%').height(20).backgroundColor(0xD2B48C) + Text('3').width('40%').height(20).backgroundColor(0xD2B48C) + Text('4').width('30%').height(20).backgroundColor(0xF5DEB3) + Text('5').width('20%').height(20).backgroundColor(0xD2B48C) + } + .width('90%') + .height(100) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001562820801](figures/zh-cn_image_0000001562820801.png) + +- FlexAlign.SpaceBetween:子组件各行与交叉轴两端对齐,各行间垂直间距平均分布。 + + + ```ts + Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceBetween }) { + Text('1').width('30%').height(20).backgroundColor(0xF5DEB3) + Text('2').width('60%').height(20).backgroundColor(0xD2B48C) + Text('3').width('40%').height(20).backgroundColor(0xD2B48C) + Text('4').width('30%').height(20).backgroundColor(0xF5DEB3) + Text('5').width('20%').height(20).backgroundColor(0xD2B48C) + } + .width('90%') + .height(100) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001511900448](figures/zh-cn_image_0000001511900448.png) + +- FlexAlign.SpaceAround:子组件各行间距相等,是元素首尾行与交叉轴两端距离的两倍。 + + + ```ts + Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceAround }) { + Text('1').width('30%').height(20).backgroundColor(0xF5DEB3) + Text('2').width('60%').height(20).backgroundColor(0xD2B48C) + Text('3').width('40%').height(20).backgroundColor(0xD2B48C) + Text('4').width('30%').height(20).backgroundColor(0xF5DEB3) + Text('5').width('20%').height(20).backgroundColor(0xD2B48C) + } + .width('90%') + .height(100) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001562700445](figures/zh-cn_image_0000001562700445.png) + +- FlexAlign.SpaceEvenly: 子组件各行间距,子组件首尾行与交叉轴两端距离都相等。 + + + ```ts + Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceEvenly }) { + Text('1').width('30%').height(20).backgroundColor(0xF5DEB3) + Text('2').width('60%').height(20).backgroundColor(0xD2B48C) + Text('3').width('40%').height(20).backgroundColor(0xD2B48C) + Text('4').width('30%').height(20).backgroundColor(0xF5DEB3) + Text('5').width('20%').height(20).backgroundColor(0xD2B48C) + } + .width('90%') + .height(100) + .backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001511580864](figures/zh-cn_image_0000001511580864.png) + + +## 自适应拉伸 + +在弹性布局父组件尺寸不够大的时候,通过子组件的下面几个属性设置其在父容器的占比,达到自适应布局能力。 + +- flexBasis:设置子组件在父容器主轴方向上的基准尺寸。如果设置了该值,则子项占用的空间为设置的值;如果没设置该属性,那子项的空间为width/height的值。 + + + ```ts + Flex() { + Text('flexBasis("auto")') + .flexBasis('auto') // 未设置width以及flexBasis值为auto,内容自身宽松 + .height(100) + .backgroundColor(0xF5DEB3) + Text('flexBasis("auto")'+' width("40%")') + .width('40%') + .flexBasis('auto') //设置width以及flexBasis值auto,使用width的值 + .height(100) + .backgroundColor(0xD2B48C) + + Text('flexBasis(100)') // 未设置width以及flexBasis值为100,宽度为100vp + .flexBasis(100) + .height(100) + .backgroundColor(0xF5DEB3) + + Text('flexBasis(100)') + .flexBasis(100) + .width(200) // flexBasis值为100,覆盖width的设置值,宽度为100vp + .height(100) + .backgroundColor(0xD2B48C) + }.width('90%').height(120).padding(10).backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001562940505](figures/zh-cn_image_0000001562940505.png) + +- flexGrow:设置父容器的剩余空间分配给此属性所在组件的比例。用于“瓜分”父组件的剩余空间。 + + + ```ts + Flex() { + Text('flexGrow(1)') + .flexGrow(2) + .width(100) + .height(100) + .backgroundColor(0xF5DEB3) + + Text('flexGrow(2)') + .flexGrow(2) + .width(100) + .height(100) + .backgroundColor(0xD2B48C) + + Text('no flexGrow') + .width(100) + .height(100) + .backgroundColor(0xF5DEB3) + }.width(400).height(120).padding(10).backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001562700449](figures/zh-cn_image_0000001562700449.png) + + 父容器宽度400vp,三个子组件原始宽度为100vp,总和300vp,剩余空间100vp根据flexGrow值的占比分配给子组件,未设置flexGrow的子组件不参与“瓜分”。 + + 第一个元素以及第二个元素以2:3分配剩下的100vp。第一个元素为100vp+100vp2/5=140vp,第二个元素为100vp+100vp3/5=160vp。 + +- flexShrink: 当父容器空间不足时,子组件的压缩比例。 + + + ```ts + Flex({ direction: FlexDirection.Row }) { + Text('flexShrink(3)') + .flexShrink(3) + .width(200) + .height(100) + .backgroundColor(0xF5DEB3) + + Text('no flexShrink') + .width(200) + .height(100) + .backgroundColor(0xD2B48C) + + Text('flexShrink(2)') + .flexShrink(2) + .width(200) + .height(100) + .backgroundColor(0xF5DEB3) + }.width(400).height(120).padding(10).backgroundColor(0xAFEEEE) + ``` + + ![zh-cn_image_0000001562820813](figures/zh-cn_image_0000001562820813.png) + + +## 相关实例 + +使用弹性布局,可以实现子组件沿水平方向排列,两端对齐,子组件间距平分,竖直方向上子组件居中的效果。 + + +```ts +@Entry +@Component +struct FlexExample { + build() { + Column() { + Column({ space: 5 }) { + Flex({ direction: FlexDirection.Row, wrap: FlexWrap.NoWrap, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { + Text('1').width('30%').height(50).backgroundColor(0xF5DEB3) + Text('2').width('30%').height(50).backgroundColor(0xD2B48C) + Text('3').width('30%').height(50).backgroundColor(0xF5DEB3) + } + .height(70) + .width('90%') + .backgroundColor(0xAFEEEE) + }.width('100%').margin({ top: 5 }) + }.width('100%') + } +} +``` + +![zh-cn_image_0000001511900452](figures/zh-cn_image_0000001511900452.png) diff --git a/zh-cn/application-dev/ui/arkts-layout-development-grid-layout.md b/zh-cn/application-dev/ui/arkts-layout-development-grid-layout.md new file mode 100644 index 0000000000000000000000000000000000000000..b148fb8cdd1e8dd939475199d8abda32772d1a6a --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-layout-development-grid-layout.md @@ -0,0 +1,481 @@ +# 栅格布局 + + +## 概述 + +栅格布局是一种通用的辅助定位工具,对移动设备的界面设计有较好的借鉴作用。主要优势包括: + +1. 提供可循的规律:栅格布局可以为布局提供规律性的结构,解决多尺寸多设备的动态布局问题。通过将页面划分为等宽的列数和行数,可以方便地对页面元素进行定位和排版。 + +2. 统一的定位标注:栅格布局可以为系统提供一种统一的定位标注,保证不同设备上各个模块的布局一致性。这可以减少设计和开发的复杂度,提高工作效率。 + +3. 灵活的间距调整方法:栅格布局可以提供一种灵活的间距调整方法,满足特殊场景布局调整的需求。通过调整列与列之间和行与行之间的间距,可以控制整个页面的排版效果。 + +4. 自动换行和自适应:栅格布局可以完成一对多布局的自动换行和自适应。当页面元素的数量超出了一行或一列的容量时,他们会自动换到下一行或下一列,并且在不同的设备上自适应排版,使得页面布局更加灵活和适应性强。 + +[GridRow](../reference/arkui-ts/ts-container-gridrow.md)为栅格容器组件,需与栅格子组件[GridCol](../reference/arkui-ts/ts-container-gridcol.md)在栅格布局场景中联合使用。 + + +## 栅格容器GridRow + + +### 栅格系统断点 + +栅格系统以设备的水平宽度(屏幕密度像素值,单位vp)作为断点依据,定义设备的宽度类型,形成了一套断点规则。开发者可根据需求在不同的断点区间实现不同的页面布局效果。 + +栅格系统默认断点将设备宽度分为xs、sm、md、lg四类,尺寸范围如下: + +| 断点名称 | 取值范围(vp) | 设备描述 | +| ---- | --------------- | --------- | +| xs | [0, 320) | 最小宽度类型设备。 | +| sm | [320, 520) | 小宽度类型设备。 | +| md | [520, 840) | 中等宽度类型设备。 | +| lg | [840, +∞) | 大宽度类型设备。 | + +在GridRow栅格组件中,允许开发者使用breakpoints自定义修改断点的取值范围,最多支持6个断点,除了默认的四个断点外,还可以启用xl,xxl两个断点,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备的布局设置。 + +| 断点名称 | 设备描述 | +| ---- | --------- | +| xs | 最小宽度类型设备。 | +| sm | 小宽度类型设备。 | +| md | 中等宽度类型设备。 | +| lg | 大宽度类型设备。 | +| xl | 特大宽度类型设备。 | +| xxl | 超大宽度类型设备。 | + +- 针对断点位置,开发者根据实际使用场景,通过一个单调递增数组设置。由于breakpoints最多支持六个断点,单调递增数组长度最大为5。 + + + ```ts + breakpoints: {value: ['100vp', '200vp']} + ``` + + 表示启用xs、sm、md共3个断点,小于100vp为xs,100vp-200vp为sm,大于200vp为md。 + + + ```ts + breakpoints: {value: ['320vp', '520vp', '840vp', '1080vp']} + ``` + + 表示启用xs、sm、md、lg、xl共5个断点,小于320vp为xs,320vp-520vp为sm,520vp-840vp为md,840vp-1080vp为lg,大于1080vp为xl。 + +- 栅格系统通过监听窗口或容器的尺寸变化进行断点,通过reference设置断点切换参考物。 考虑到应用可能以非全屏窗口的形式显示,以应用窗口宽度为参照物更为通用。 + +例如,使用栅格的默认列数12列,通过断点设置将应用宽度分成六个区间,在各区间中,每个栅格子元素占用的列数均不同。 + + +```ts +@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown]; +... +GridRow({ + breakpoints: { + value: ['200vp', '300vp', '400vp', '500vp', '600vp'], + reference: BreakpointsReference.WindowSize + } +}) { + ForEach(this.bgColors, (color, index) => { + GridCol({ + span: { + xs: 2, + sm: 3, + md: 4, + lg: 6, + xl: 8, + xxl: 12 + } + }) { + Row() { + Text(`${index}`) + }.width("100%").height('50vp') + }.backgroundColor(color) + }) +} +``` + +![zh-cn_image_0000001511421272](figures/zh-cn_image_0000001511421272.gif) + + +### 布局的总列数 + +GridRow中通过columns设置栅格布局的总列数。 + +- columns默认值为12,即在未设置columns时,任何断点下,栅格布局被分成12列。 + + + ```ts + @State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown]; + ... + GridRow() { + ForEach(this.bgColors, (item, index) => { + GridCol() { + Row() { + Text(`${index + 1}`) + }.width('100%').height('50') + }.backgroundColor(item) + }) + } + ``` + + ![zh-cn_image_0000001563060709](figures/zh-cn_image_0000001563060709.png) + +- 当columns为自定义值,栅格布局在任何尺寸设备下都被分为columns列。下面分别设置栅格布局列数为4和8,子元素默认占一列,效果如下: + + + ```ts + @State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown]; + @State currentBp: string = 'unknown'; + ... + Row() { + GridRow({ columns: 4 }) { + ForEach(this.bgColors, (item, index) => { + GridCol() { + Row() { + Text(`${index + 1}`) + }.width('100%').height('50') + }.backgroundColor(item) + }) + } + .width('100%').height('100%') + .onBreakpointChange((breakpoint) => { + this.currentBp = breakpoint + }) + } + .height(160) + .border({ color: Color.Blue, width: 2 }) + .width('90%') + + Row() { + GridRow({ columns: 8 }) { + ForEach(this.bgColors, (item, index) => { + GridCol() { + Row() { + Text(`${index + 1}`) + }.width('100%').height('50') + }.backgroundColor(item) + }) + } + .width('100%').height('100%') + .onBreakpointChange((breakpoint) => { + this.currentBp = breakpoint + }) + } + .height(160) + .border({ color: Color.Blue, width: 2 }) + .width('90%') + ``` + + ![zh-cn_image_0000001511421268](figures/zh-cn_image_0000001511421268.png) + +- 当columns类型为GridRowColumnOption时,支持下面六种不同尺寸(xs, sm, md, lg, xl, xxl)设备的总列数设置,各个尺寸下数值可不同。 + + + ```ts + @State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown] + GridRow({ columns: { sm: 4, md: 8 }, breakpoints: { value: ['200vp', '300vp', '400vp', '500vp', '600vp'] } }) { + ForEach(this.bgColors, (item, index) => { + GridCol() { + Row() { + Text(`${index + 1}`) + }.width('100%').height('50') + }.backgroundColor(item) + }) + } + ``` + + ![zh-cn_image_0000001563060689](figures/zh-cn_image_0000001563060689.gif) + + 若只设置sm, md的栅格总列数,则较小的尺寸使用默认columns值12,较大的尺寸使用前一个尺寸的columns。这里只设置sm:8, md:10,则较小尺寸的xs:12,较大尺寸的参照md的设置,lg:10, xl:10, xxl:10 + + +### 排列方向 + +栅格布局中,可以通过设置GridRow的direction属性来指定栅格子组件在栅格容器中的排列方向。该属性可以设置为GridRowDirection.Row(从左往右排列)或GridRowDirection.RowReverse(从右往左排列),以满足不同的布局需求。通过合理的direction属性设置,可以使得页面布局更加灵活和符合设计要求。 + +- 子组件默认从左往右排列。 + + + ```ts + GridRow({ direction: GridRowDirection.Row }){} + ``` + + ![zh-cn_image_0000001511740488](figures/zh-cn_image_0000001511740488.png) + +- 子组件从右往左排列。 + + + ```ts + GridRow({ direction: GridRowDirection.RowReverse }){} + ``` + + ![zh-cn_image_0000001562940517](figures/zh-cn_image_0000001562940517.png) + + +### 子组件间距 + +GridRow中通过gutter属性设置子元素在水平和垂直方向的间距。 + +- 当gutter类型为number时,同时设置栅格子组件间水平和垂直方向边距且相等。下例中,设置子组件水平与垂直方向距离相邻元素的间距为10。 + + + ```ts + GridRow({ gutter: 10 }){} + ``` + + ![zh-cn_image_0000001511740476](figures/zh-cn_image_0000001511740476.png) + +- 当gutter类型为GutterOption时,单独设置栅格子组件水平垂直边距,x属性为水平方向间距,y为垂直方向间距。 + + + ```ts + GridRow({ gutter: { x: 20, y: 50 } }){} + ``` + + ![zh-cn_image_0000001511900456](figures/zh-cn_image_0000001511900456.png) + + +## 子组件GridCol + +GridCol组件作为GridRow组件的子组件,通过给GridCol传参或者设置属性两种方式,设置span(占用列数),offset(偏移列数),order(元素序号)的值。 + +- 设置span。 + + + ```ts + GridCol({ span: 2 }){} + GridCol({ span: { xs: 1, sm: 2, md: 3, lg: 4 } }){} + GridCol(){}.span(2) + GridCol(){}.span({ xs: 1, sm: 2, md: 3, lg: 4 }) + ``` + +- 设置offset。 + + + ```ts + GridCol({ offset: 2 }){} + GridCol({ offset: { xs: 2, sm: 2, md: 2, lg: 2 } }){} + GridCol(){}.offset(2) + GridCol(){}.offset({ xs: 1, sm: 2, md: 3, lg: 4 }) + ``` + +- 设置order。 + + + ```ts + GridCol({ order: 2 }){} + GridCol({ order: { xs: 1, sm: 2, md: 3, lg: 4 } }){} + GridCol(){}.order(2) + GridCol(){}.order({ xs: 1, sm: 2, md: 3, lg: 4 }) + ``` + + +### span + +子组件占栅格布局的列数,决定了子组件的宽度,默认为1。 + +- 当类型为number时,子组件在所有尺寸设备下占用的列数相同。 + + + ```ts + @State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown]; + ... + GridRow({ columns: 8 }) { + ForEach(this.bgColors, (color, index) => { + GridCol({ span: 2 }) { + Row() { + Text(`${index}`) + }.width('100%').height('50vp') + } + .backgroundColor(color) + }) + } + ``` + + ![zh-cn_image_0000001511421264](figures/zh-cn_image_0000001511421264.png) + +- 当类型为GridColColumnOption时,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备中子组件所占列数设置,各个尺寸下数值可不同。 + + + ```ts + @State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown]; + ... + GridRow({ columns: 8 }) { + ForEach(this.bgColors, (color, index) => { + GridCol({ span: { xs: 1, sm: 2, md: 3, lg: 4 } }) { + Row() { + Text(`${index}`) + }.width('100%').height('50vp') + } + .backgroundColor(color) + }) + } + ``` + + ![zh-cn_image_0000001511740492](figures/zh-cn_image_0000001511740492.gif) + + +### offset + +栅格子组件相对于前一个子组件的偏移列数,默认为0。 + +- 当类型为number时,子组件偏移相同列数。 + + + ```ts + @State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown]; + ... + GridRow() { + ForEach(this.bgColors, (color, index) => { + GridCol({ offset: 2 }) { + Row() { + Text('' + index) + }.width('100%').height('50vp') + } + .backgroundColor(color) + }) + } + ``` + + ![zh-cn_image_0000001563060705](figures/zh-cn_image_0000001563060705.png) + + 栅格默认分成12列,每一个子组件默认占1列,偏移2列,每个子组件及间距共占3列,一行放四个子组件。 + +- 当类型为GridColColumnOption时,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备中子组件所占列数设置,各个尺寸下数值可不同。 + + + ```ts + @State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown]; + ... + + GridRow() { + ForEach(this.bgColors, (color, index) => { + GridCol({ offset: { xs: 1, sm: 2, md: 3, lg: 4 } }) { + Row() { + Text('' + index) + }.width('100%').height('50vp') + } + .backgroundColor(color) + }) + } + ``` + + ![zh-cn_image_0000001562700433](figures/zh-cn_image_0000001562700433.gif) + + +### order + +栅格子组件的序号,决定子组件排列次序。当子组件不设置order或者设置相同的order, 子组件按照代码顺序展示。当子组件设置不同的order时,order较小的组件在前,较大的在后。 + +当子组件部分设置order,部分不设置order时,未设置order的子组件依次排序靠前,设置了order的子组件按照数值从小到大排列。 + +- 当类型为number时,子组件在任何尺寸下排序次序一致。 + + + ```ts + GridRow() { + GridCol({ order: 4 }) { + Row() { + Text('1') + }.width('100%').height('50vp') + }.backgroundColor(Color.Red) + GridCol({ order: 3 }) { + Row() { + Text('2') + }.width('100%').height('50vp') + }.backgroundColor(Color.Orange) + GridCol({ order: 2 }) { + Row() { + Text('3') + }.width('100%').height('50vp') + }.backgroundColor(Color.Yellow) + GridCol({ order: 1 }) { + Row() { + Text('4') + }.width('100%').height('50vp') + }.backgroundColor(Color.Green) + } + ``` + + ![zh-cn_image_0000001511580892](figures/zh-cn_image_0000001511580892.png) + +- 当类型为GridColColumnOption时,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备中子组件排序次序设置。在xs设备中,子组件排列顺序为1234;sm为2341,md为3412,lg为2431。 + + + ```ts + GridRow() { + GridCol({ order: { xs:1, sm:5, md:3, lg:7}}) { + Row() { + Text('1') + }.width('100%').height('50vp') + }.backgroundColor(Color.Red) + GridCol({ order: { xs:2, sm:2, md:6, lg:1} }) { + Row() { + Text('2') + }.width('100%').height('50vp') + }.backgroundColor(Color.Orange) + GridCol({ order: { xs:3, sm:3, md:1, lg:6} }) { + Row() { + Text('3') + }.width('100%').height('50vp') + }.backgroundColor(Color.Yellow) + GridCol({ order: { xs:4, sm:4, md:2, lg:5} }) { + Row() { + Text('4') + }.width('100%').height('50vp') + }.backgroundColor(Color.Green) + } + ``` + + ![zh-cn_image_0000001511900444](figures/zh-cn_image_0000001511900444.gif) + + +## 栅格组件的嵌套使用 + +栅格组件也可以嵌套使用,完成一些复杂的布局。 + +以下示例中,栅格把整个空间分为12份。第一层GridRow嵌套GridCol,分为中间大区域以及“footer”区域。第二层GridRow嵌套GridCol,分为“left”和“right”区域。子组件空间按照上一层父组件的空间划分,粉色的区域是屏幕空间的12列,绿色和蓝色的区域是父组件GridCol的12列,依次进行空间的划分。 + + + +```ts +@Entry +@Component +struct GridRowExample { + build() { + GridRow() { + GridCol({ span: { sm: 12 } }) { + GridRow() { + GridCol({ span: { sm: 2 } }) { + Row() { + Text('left').fontSize(24) + } + .justifyContent(FlexAlign.Center) + .height('90%') + }.backgroundColor('#ff41dbaa') + + GridCol({ span: { sm: 10 } }) { + Row() { + Text('right').fontSize(24) + } + .justifyContent(FlexAlign.Center) + .height('90%') + }.backgroundColor('#ff4168db') + } + .backgroundColor('#19000000') + .height('100%') + } + + GridCol({ span: { sm: 12 } }) { + Row() { + Text('footer').width('100%').textAlign(TextAlign.Center) + }.width('100%').height('10%').backgroundColor(Color.Pink) + } + }.width('100%').height(300) + } +} +``` + + +![zh-cn_image_0000001563060697](figures/zh-cn_image_0000001563060697.png) + + +综上所述,栅格组件提供了丰富的自定义能力,功能异常灵活和强大。只需要明确栅格在不同断点下的Columns、Margin、Gutter及span等参数,即可确定最终布局,无需关心具体的设备类型及设备状态(如横竖屏)等。 diff --git a/zh-cn/application-dev/ui/arkts-layout-development-linear.md b/zh-cn/application-dev/ui/arkts-layout-development-linear.md new file mode 100644 index 0000000000000000000000000000000000000000..9623a681192b05aa22eb310d3083422e8b112dc9 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-layout-development-linear.md @@ -0,0 +1,629 @@ +# 线性布局 + + +## 概述 + +线性布局(LinearLayout)是开发中最常用的布局,通过线性容器[Row](../reference/arkui-ts/ts-container-row.md)和[Column](../reference/arkui-ts/ts-container-column.md)构建。线性布局是其他布局的基础,其子元素在线性方向上(水平方向和垂直方向)依次排列。线性布局的排列方向由所选容器组件决定,Column容器内子元素按照垂直方向排列,Row容器内子元素按照水平方向排列。根据不同的排列方向,开发者可选择使用Row或Column容器创建线性布局。 + + + **图1** Column容器内子元素排列示意图   + +![arrangement-child-elements-column](figures/arrangement-child-elements-column.png) + + + **图2** Row容器内子元素排列示意图   + +![arrangement-child-elements-row](figures/arrangement-child-elements-row.png) + + +## 基本概念 + +- 布局容器:具有布局能力的容器组件,可以承载其他元素作为其子元素,布局容器会对其子元素进行尺寸计算和布局排列。 + +- 布局子元素:布局容器内部的元素。 + +- 主轴:线性布局容器在布局方向上的轴线,子元素默认沿主轴排列。Row容器主轴为纵向,Column容器主轴为横向。 + +- 交叉轴:垂直于主轴方向的轴线。Row容器交叉轴为横向,Column容器交叉轴为纵向。 + +- 间距:布局子元素的纵向间距。 + + +## 布局子元素在排列方向上的间距 + +在布局容器内,可以通过space属性设置排列方向上子元素的间距,使各子元素在排列方向上有等间距效果。 + + +### Column容器内排列方向上的间距 + + **图3** Column容器内排列方向的间距图   + +![arrangement-direction-column](figures/arrangement-direction-column.png) + +```ts +Column({ space: 20 }) { + Text('space: 20').fontSize(15).fontColor(Color.Gray).width('90%') + Row().width('90%').height(50).backgroundColor(0xF5DEB3) + Row().width('90%').height(50).backgroundColor(0xD2B48C) + Row().width('90%').height(50).backgroundColor(0xF5DEB3) +}.width('100%') +``` + + +![arrangement-direction-column-sample](figures/arrangement-direction-column-sample.png) + + +### Row容器内排列方向上的间距 + + **图4** Row容器内排列方向的间距图   + +![arrangement-direction-row](figures/arrangement-direction-row.png) + + +```ts +Row({ space: 35 }) { + Text('space: 35').fontSize(15).fontColor(Color.Gray) + Row().width('10%').height(150).backgroundColor(0xF5DEB3) + Row().width('10%').height(150).backgroundColor(0xD2B48C) + Row().width('10%').height(150).backgroundColor(0xF5DEB3) +}.width('90%') +``` + +![zh-cn_image_0000001562700509](figures/zh-cn_image_0000001562700509.png) + + +## 布局子元素在交叉轴上的对齐方式 + +在布局容器内,可以通过alignItems属性设置子元素在交叉轴(排列方向的垂直方向)上的对齐方式。且在各类尺寸屏幕中,表现一致。其中,交叉轴为垂直方向时,取值为[VerticalAlign类型](../reference/arkui-ts/ts-appendix-enums.md#verticalalign),水平方向取值为[HorizontalAlign](../reference/arkui-ts/ts-appendix-enums.md#horizontalalign)。 + +alignSelf属性用于控制单个子元素在容器主轴上的对齐方式,其优先级高于alignItems属性,如果设置了alignSelf属性,则在单个子元素上会覆盖alignItems属性。 + + +### Column容器内子元素在水平方向上的排列 + + **图5** Column容器内子元素在水平方向上的排列图   + +![horizontal-arrangement-child-column](figures/horizontal-arrangement-child-column.png) + +- HorizontalAlign.Start:子元素在水平方向左对齐。 + + ```ts + Column({}) { + Column() { + }.width('80%').height(50).backgroundColor(0xF5DEB3) + + Column() { + }.width('80%').height(50).backgroundColor(0xD2B48C) + + Column() { + }.width('80%').height(50).backgroundColor(0xF5DEB3) + }.width('100%').alignItems(HorizontalAlign.Start).backgroundColor('rgb(242,242,242)') + ``` + + ![zh-cn_image_0000001511580964](figures/zh-cn_image_0000001511580964.png) + +- HorizontalAlign.Center:子元素在水平方向居中对齐。 + + ```ts + Column({}) { + Column() { + }.width('80%').height(50).backgroundColor(0xF5DEB3) + + Column() { + }.width('80%').height(50).backgroundColor(0xD2B48C) + + Column() { + }.width('80%').height(50).backgroundColor(0xF5DEB3) + }.width('100%').alignItems(HorizontalAlign.Center).backgroundColor('rgb(242,242,242)') + ``` + + ![zh-cn_image_0000001562820897](figures/zh-cn_image_0000001562820897.png) + +- HorizontalAlign.End:子元素在水平方向右对齐。 + + ```ts + Column({}) { + Column() { + }.width('80%').height(50).backgroundColor(0xF5DEB3) + + Column() { + }.width('80%').height(50).backgroundColor(0xD2B48C) + + Column() { + }.width('80%').height(50).backgroundColor(0xF5DEB3) + }.width('100%').alignItems(HorizontalAlign.End).backgroundColor('rgb(242,242,242)') + ``` + + ![zh-cn_image_0000001511421348](figures/zh-cn_image_0000001511421348.png) + + +### Row容器内子元素在垂直方向上的排列 + + **图6** Row容器内子元素在垂直方向上的排列图   + +![horizontal-arrangement-child-row](figures/horizontal-arrangement-child-row.png) + +- VerticalAlign.Top:子元素在垂直方向顶部对齐。 + + ```ts + Row({}) { + Column() { + }.width('20%').height(30).backgroundColor(0xF5DEB3) + + Column() { + }.width('20%').height(30).backgroundColor(0xD2B48C) + + Column() { + }.width('20%').height(30).backgroundColor(0xF5DEB3) + }.width('100%').height(200).alignItems(VerticalAlign.Top).backgroundColor('rgb(242,242,242)') + ``` + + ![zh-cn_image_0000001563060765](figures/zh-cn_image_0000001563060765.png) + +- VerticalAlign.Center:子元素在垂直方向居中对齐。 + + ```ts + Row({}) { + Column() { + }.width('20%').height(30).backgroundColor(0xF5DEB3) + + Column() { + }.width('20%').height(30).backgroundColor(0xD2B48C) + + Column() { + }.width('20%').height(30).backgroundColor(0xF5DEB3) + }.width('100%').height(200).alignItems(VerticalAlign.Center).backgroundColor('rgb(242,242,242)') + ``` + + ![zh-cn_image_0000001562700505](figures/zh-cn_image_0000001562700505.png) + +- VerticalAlign.Bottom:子元素在垂直方向底部对齐。 + + ```ts + Row({}) { + Column() { + }.width('20%').height(30).backgroundColor(0xF5DEB3) + + Column() { + }.width('20%').height(30).backgroundColor(0xD2B48C) + + Column() { + }.width('20%').height(30).backgroundColor(0xF5DEB3) + }.width('100%').height(200).alignItems(VerticalAlign.Bottom).backgroundColor('rgb(242,242,242)') + ``` + + ![zh-cn_image_0000001563060781](figures/zh-cn_image_0000001563060781.png) + + +## 布局子元素在主轴上的排列方式 + +在布局容器内,可以通过justifyContent属性设置子元素在容器主轴上的排列方式。可以从主轴起始位置开始排布,也可以从主轴结束位置开始排布,或者均匀分割主轴的空间。 + + +### Column容器内子元素在主轴上的排列 + + **图7** Column容器内子元素在主轴上的排列图  + +![vertial-arrangement-child-column](figures/vertial-arrangement-child-column.png) + +- justifyContent(FlexAlign.Start):元素在主轴方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐。 + + ```ts + Column({}) { + Column() { + }.width('80%').height(50).backgroundColor(0xF5DEB3) + + Column() { + }.width('80%').height(50).backgroundColor(0xD2B48C) + + Column() { + }.width('80%').height(50).backgroundColor(0xF5DEB3) + }.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.Start) + ``` + + ![zh-cn_image_0000001562700501](figures/zh-cn_image_0000001562700501.png) + +- justifyContent(FlexAlign.Center):元素在主轴方向中心对齐,第一个元素与行首的距离与最后一个元素与行尾距离相同。 + + ```ts + Column({}) { + Column() { + }.width('80%').height(50).backgroundColor(0xF5DEB3) + + Column() { + }.width('80%').height(50).backgroundColor(0xD2B48C) + + Column() { + }.width('80%').height(50).backgroundColor(0xF5DEB3) + }.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.Center) + ``` + + ![zh-cn_image_0000001562700517](figures/zh-cn_image_0000001562700517.png) + +- justifyContent(FlexAlign.End):元素在主轴方向尾部对齐,最后一个元素与行尾对齐,其他元素与后一个对齐。 + + ```ts + Column({}) { + Column() { + }.width('80%').height(50).backgroundColor(0xF5DEB3) + + Column() { + }.width('80%').height(50).backgroundColor(0xD2B48C) + + Column() { + }.width('80%').height(50).backgroundColor(0xF5DEB3) + }.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.End) + ``` + + ![zh-cn_image_0000001562940585](figures/zh-cn_image_0000001562940585.png) + +- justifyContent(FlexAlign.Spacebetween):主轴方向均匀分配元素,相邻元素之间距离相同。第一个元素与行首对齐,最后一个元素与行尾对齐。 + + ```ts + Column({}) { + Column() { + }.width('80%').height(50).backgroundColor(0xF5DEB3) + + Column() { + }.width('80%').height(50).backgroundColor(0xD2B48C) + + Column() { + }.width('80%').height(50).backgroundColor(0xF5DEB3) + }.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceBetween) + ``` + + ![zh-cn_image_0000001511900532](figures/zh-cn_image_0000001511900532.png) + +- justifyContent(FlexAlign.SpaceAround):主轴方向均匀分配元素,相邻元素之间距离相同。第一个元素到行首的距离和最后一个元素到行尾的距离是相邻元素之间距离的一半。 + + ```ts + Column({}) { + Column() { + }.width('80%').height(50).backgroundColor(0xF5DEB3) + + Column() { + }.width('80%').height(50).backgroundColor(0xD2B48C) + + Column() { + }.width('80%').height(50).backgroundColor(0xF5DEB3) + }.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceAround) + ``` + + ![zh-cn_image_0000001562700525](figures/zh-cn_image_0000001562700525.png) + +- justifyContent(FlexAlign.SpaceEvenly):主轴方向均匀分配元素,相邻元素之间的距离、第一个元素与行首的间距、最后一个元素到行尾的间距都完全一样。 + + ```ts + Column({}) { + Column() { + }.width('80%').height(50).backgroundColor(0xF5DEB3) + + Column() { + }.width('80%').height(50).backgroundColor(0xD2B48C) + + Column() { + }.width('80%').height(50).backgroundColor(0xF5DEB3) + }.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceEvenly) + ``` + + ![zh-cn_image_0000001563060785](figures/zh-cn_image_0000001563060785.png) + + +### Row容器内子元素在主轴上的排列 + + **图8** Row容器内子元素在主轴上的排列图   + +![vertial-arrangement-child-row](figures/vertial-arrangement-child-row.png) + +- justifyContent(FlexAlign.Start):元素在主轴方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐。 + + ```ts + Row({}) { + Column() { + }.width('20%').height(30).backgroundColor(0xF5DEB3) + + Column() { + }.width('20%').height(30).backgroundColor(0xD2B48C) + + Column() { + }.width('20%').height(30).backgroundColor(0xF5DEB3) + }.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.Start) + ``` + + ![zh-cn_image_0000001511421356](figures/zh-cn_image_0000001511421356.png) + +- justifyContent(FlexAlign.Center):元素在主轴方向中心对齐,第一个元素与行首的距离与最后一个元素与行尾距离相同。 + + ```ts + Row({}) { + Column() { + }.width('20%').height(30).backgroundColor(0xF5DEB3) + + Column() { + }.width('20%').height(30).backgroundColor(0xD2B48C) + + Column() { + }.width('20%').height(30).backgroundColor(0xF5DEB3) + }.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.Center) + ``` + + ![zh-cn_image_0000001511900516](figures/zh-cn_image_0000001511900516.png) + +- justifyContent(FlexAlign.End):元素在主轴方向尾部对齐,最后一个元素与行尾对齐,其他元素与后一个对齐。 + + ```ts + Row({}) { + Column() { + }.width('20%').height(30).backgroundColor(0xF5DEB3) + + Column() { + }.width('20%').height(30).backgroundColor(0xD2B48C) + + Column() { + }.width('20%').height(30).backgroundColor(0xF5DEB3) + }.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.End) + ``` + + ![zh-cn_image_0000001562940601](figures/zh-cn_image_0000001562940601.png) + +- justifyContent(FlexAlign.Spacebetween):主轴方向均匀分配元素,相邻元素之间距离相同。第一个元素与行首对齐,最后一个元素与行尾对齐。 + + ```ts + Row({}) { + Column() { + }.width('20%').height(30).backgroundColor(0xF5DEB3) + + Column() { + }.width('20%').height(30).backgroundColor(0xD2B48C) + + Column() { + }.width('20%').height(30).backgroundColor(0xF5DEB3) + }.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceBetween) + ``` + + ![zh-cn_image_0000001562700521](figures/zh-cn_image_0000001562700521.png) + +- justifyContent(FlexAlign.SpaceAround):主轴方向均匀分配元素,相邻元素之间距离相同。第一个元素到行首的距离和最后一个元素到行尾的距离是相邻元素之间距离的一半。 + + ```ts + Row({}) { + Column() { + }.width('20%').height(30).backgroundColor(0xF5DEB3) + + Column() { + }.width('20%').height(30).backgroundColor(0xD2B48C) + + Column() { + }.width('20%').height(30).backgroundColor(0xF5DEB3) + }.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceAround) + ``` + + ![zh-cn_image_0000001562820893](figures/zh-cn_image_0000001562820893.png) + +- justifyContent(FlexAlign.SpaceEvenly):主轴方向均匀分配元素,相邻元素之间的距离、第一个元素与行首的间距、最后一个元素到行尾的间距都完全一样。 + + ```ts + Row({}) { + Column() { + }.width('20%').height(30).backgroundColor(0xF5DEB3) + + Column() { + }.width('20%').height(30).backgroundColor(0xD2B48C) + + Column() { + }.width('20%').height(30).backgroundColor(0xF5DEB3) + }.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceEvenly) + ``` + + ![zh-cn_image_0000001511421352](figures/zh-cn_image_0000001511421352.png) + + +## 自适应拉伸 + +在线性布局下,常用空白填充组件[Blank](../reference/arkui-ts/ts-basic-components-blank.md),在容器主轴方向自动填充空白空间,达到自适应拉伸效果。Row和Column作为容器,只需要添加宽高为百分比,当屏幕宽高发生变化时,会产生自适应效果。 + + +```ts +@Entry +@Component +struct BlankExample { + build() { + Column() { + Row() { + Text('Bluetooth').fontSize(18) + Blank() + Toggle({ type: ToggleType.Switch, isOn: true }) + }.backgroundColor(0xFFFFFF).borderRadius(15).padding({ left: 12 }).width('100%') + }.backgroundColor(0xEFEFEF).padding(20).width('100%') + } +} +``` + + **图9** 竖屏   + +![zh-cn_image_0000001562820881](figures/zh-cn_image_0000001562820881.png) + + **图10** 横屏   + +![zh-cn_image_0000001511421332](figures/zh-cn_image_0000001511421332.png) + + +## 自适应缩放 + +自适应缩放是指子组件随容器尺寸的变化而按照预设的比例自动调整尺寸,适应各种不同大小的设备。在线性布局中,可以使用以下两种方法实现自适应缩放。 + + +- 父容器尺寸确定时,使用layoutWeight属性设置子组件和兄弟元素在主轴上的权重,忽略元素本身尺寸设置,使它们在任意尺寸的设备下自适应占满剩余空间。 + + ```ts + @Entry + @Component + struct layoutWeightExample { + build() { + Column() { + Text('1:2:3').width('100%') + Row() { + Column() { + Text('layoutWeight(1)') + .textAlign(TextAlign.Center) + }.layoutWeight(2).backgroundColor(0xF5DEB3).height('100%') + + Column() { + Text('layoutWeight(2)') + .textAlign(TextAlign.Center) + }.layoutWeight(4).backgroundColor(0xD2B48C).height('100%') + + Column() { + Text('layoutWeight(6)') + .textAlign(TextAlign.Center) + }.layoutWeight(6).backgroundColor(0xF5DEB3).height('100%') + + }.backgroundColor(0xffd306).height('30%') + + Text('2:5:3').width('100%') + Row() { + Column() { + Text('layoutWeight(2)') + .textAlign(TextAlign.Center) + }.layoutWeight(2).backgroundColor(0xF5DEB3).height('100%') + + Column() { + Text('layoutWeight(5)') + .textAlign(TextAlign.Center) + }.layoutWeight(5).backgroundColor(0xD2B48C).height('100%') + + Column() { + Text('layoutWeight(3)') + .textAlign(TextAlign.Center) + }.layoutWeight(3).backgroundColor(0xF5DEB3).height('100%') + }.backgroundColor(0xffd306).height('30%') + } + } + } + ``` + + **图11** 横屏   + + ![zh-cn_image_0000001511421336](figures/zh-cn_image_0000001511421336.png) + + **图12** 竖屏   + + ![zh-cn_image_0000001511580968](figures/zh-cn_image_0000001511580968.png) + +- 父容器尺寸确定时,使用百分比设置子组件和兄弟元素的宽度,使他们在任意尺寸的设备下保持固定的自适应占比。 + + ```ts + @Entry + @Component + struct WidthExample { + build() { + Column() { + Row() { + Column() { + Text('left width 20%') + .textAlign(TextAlign.Center) + }.width('20%').backgroundColor(0xF5DEB3).height('100%') + + Column() { + Text('center width 50%') + .textAlign(TextAlign.Center) + }.width('50%').backgroundColor(0xD2B48C).height('100%') + + Column() { + Text('right width 30%') + .textAlign(TextAlign.Center) + }.width('30%').backgroundColor(0xF5DEB3).height('100%') + }.backgroundColor(0xffd306).height('30%') + } + } + } + ``` + + **图13** 横屏   + + ![zh-cn_image_0000001563060777](figures/zh-cn_image_0000001563060777.png) + + **图14** 竖屏   + + ![zh-cn_image_0000001511740564](figures/zh-cn_image_0000001511740564.png) + + +## 自适应延伸 + +自适应延伸是指在不同尺寸设备下,当页面的内容超出屏幕大小而无法完全显示时,可以通过滚动条进行拖动展示。这种方法适用于线性布局中内容无法一屏展示的场景。通常有以下两种实现方式。 + +- [在List中添加滚动条](arkts-layout-development-create-list.md#添加滚动条):当List子项过多一屏放不下时,可以将每一项子元素放置在不同的组件中,通过滚动条进行拖动展示。可以通过scrollBar属性设置滚动条的常驻状态,edgeEffect属性设置拖动到内容最末端的回弹效果。 + +- 使用Scroll组件:在线性布局中,开发者可以进行竖向或者横向的布局。当一屏无法完全显示时,可以在Column或Row组件的外层包裹一个可滚动的容器组件Scroll来实现可滑动的线性布局。 + 竖向布局中使用Scroll组件: + + ```ts + @Entry + @Component + struct ScrollExample { + scroller: Scroller = new Scroller(); + private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + + build() { + Scroll(this.scroller) { + Column() { + ForEach(this.arr, (item) => { + Text(item.toString()) + .width('90%') + .height(150) + .backgroundColor(0xFFFFFF) + .borderRadius(15) + .fontSize(16) + .textAlign(TextAlign.Center) + .margin({ top: 10 }) + }, item => item) + }.width('100%') + } + .backgroundColor(0xDCDCDC) + .scrollable(ScrollDirection.Vertical) // 滚动方向纵向 + .scrollBar(BarState.On) // 滚动条常驻显示 + .scrollBarColor(Color.Gray) // 滚动条颜色 + .scrollBarWidth(10) // 滚动条宽度 + .edgeEffect(EdgeEffect.Spring) // 滚动到边沿后回弹 + } + } + ``` + + ![zh-cn_image_0000001511900524](figures/zh-cn_image_0000001511900524.gif) + + 横向布局中使用Scroll组件: + + + ```ts + @Entry + @Component + struct ScrollExample { + scroller: Scroller = new Scroller(); + private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + + build() { + Scroll(this.scroller) { + Row() { + ForEach(this.arr, (item) => { + Text(item.toString()) + .height('90%') + .width(150) + .backgroundColor(0xFFFFFF) + .borderRadius(15) + .fontSize(16) + .textAlign(TextAlign.Center) + .margin({ left: 10 }) + }) + }.height('100%') + } + .backgroundColor(0xDCDCDC) + .scrollable(ScrollDirection.Horizontal) // 滚动方向横向 + .scrollBar(BarState.On) // 滚动条常驻显示 + .scrollBarColor(Color.Gray) // 滚动条颜色 + .scrollBarWidth(10) // 滚动条宽度 + .edgeEffect(EdgeEffect.Spring) // 滚动到边沿后回弹 + } + } + ``` + + ![zh-cn_image_0000001562940609](figures/zh-cn_image_0000001562940609.gif) diff --git a/zh-cn/application-dev/ui/arkts-layout-development-media-query.md b/zh-cn/application-dev/ui/arkts-layout-development-media-query.md new file mode 100644 index 0000000000000000000000000000000000000000..fab4f67486d0349f529b3af52d6e65c673c01147 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-layout-development-media-query.md @@ -0,0 +1,263 @@ +# 媒体查询 + + +## 概述 + +[媒体查询](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/apis/js-apis-mediaquery.md/)作为响应式设计的核心,在移动设备上应用十分广泛。媒体查询可根据不同设备类型或同设备不同状态修改应用的样式。媒体查询常用于下面两种场景: + +1. 针对设备和应用的属性信息(比如显示区域、深浅色、分辨率),设计出相匹配的布局。 + +2. 当屏幕发生动态改变时(比如分屏、横竖屏切换),同步更新应用的页面布局。 + + +## 引入与使用流程 + +媒体查询通过mediaquery模块接口,设置查询条件并绑定回调函数,在对应的条件的回调函数里更改页面布局或者实现业务逻辑,实现页面的响应式设计。具体步骤如下: + +首先导入媒体查询模块。 + + +```ts +import mediaquery from '@ohos.mediaquery'; +``` + +通过matchMediaSync接口设置媒体查询条件,保存返回的条件监听句柄listener。例如监听横屏事件: + + +```ts +let listener = mediaquery.matchMediaSync('(orientation: landscape)'); +``` + +给条件监听句柄listener绑定回调函数onPortrait,当listener检测设备状态变化时执行回调函数。在回调函数内,根据不同设备状态更改页面布局或者实现业务逻辑。 + + +```ts +onPortrait(mediaQueryResult) { + if (mediaQueryResult.matches) { + // do something here + } else { + // do something here + } +} + +listener.on('change', onPortrait); +``` + + +## 媒体查询条件 + +媒体查询条件由媒体类型、逻辑操作符、媒体特征组成,其中媒体类型可省略,逻辑操作符用于连接不同媒体类型与媒体特征,其中,媒体特征要使用“()”包裹且可以有多个。具体规则如下: + + +### 语法规则 + +语法规则包括[媒体类型(media-type)](#媒体类型media-type)、[媒体逻辑操作(media-logic-operations)](#媒体逻辑操作media-logic-operations)和[媒体特征(media-feature)](#媒体特征media-feature)。 + + +```ts +[media-type] [media-logic-operations] [(media-feature)] +``` + +例如: + +- screen and (round-screen: true) :表示当设备屏幕是圆形时条件成立。 + +- (max-height: 800) :表示当高度小于等于800时条件成立。 + +- (height <= 800) :表示当高度小于等于800时条件成立。 + +- screen and (device-type: tv) or (resolution < 2) :表示包含多个媒体特征的多条件复杂语句查询,当设备类型为tv或设备分辨率小于2时条件成立。 + + +### 媒体类型(media-type) + +| **类型** | **说明** | +| ------ | -------------- | +| screen | 按屏幕相关参数进行媒体查询。 | + + +### 媒体逻辑操作(media-logic-operations) + +媒体逻辑操作符:and、or、not、only用于构成复杂媒体查询,也可以通过comma(, )将其组合起来,详细解释说明如下表。 + + **表1** 媒体逻辑操作符 + +| 类型 | 说明 | +| -------------- | ---------------------------------------- | +| and | 将多个媒体特征(Media Feature)以“与”的方式连接成一个媒体查询,只有当所有媒体特征都为true,查询条件成立。另外,它还可以将媒体类型和媒体功能结合起来。例如:screen and (device-type: wearable) and (max-height: 600) 表示当设备类型是智能穿戴且应用的最大高度小于等于600个像素单位时成立。 | +| or | 将多个媒体特征以“或”的方式连接成一个媒体查询,如果存在结果为true的媒体特征,则查询条件成立。例如:screen and (max-height: 1000) or (round-screen: true) 表示当应用高度小于等于1000个像素单位或者设备屏幕是圆形时,条件成立。 | +| not | 取反媒体查询结果,媒体查询结果不成立时返回true,否则返回false。例如:not screen and (min-height: 50) and (max-height: 600) 表示当应用高度小于50个像素单位或者大于600个像素单位时成立。
使用not运算符时必须指定媒体类型。 | +| only | 当整个表达式都匹配时,才会应用选择的样式,可以应用在防止某些较早的版本的浏览器上产生歧义的场景。一些较早版本的浏览器对于同时包含了媒体类型和媒体特征的语句会产生歧义,比如:screen and (min-height: 50)。老版本浏览器会将这句话理解成screen,从而导致仅仅匹配到媒体类型(screen),就应用了指定样式,使用only可以很好地规避这种情况。
使用only时必须指定媒体类型。 | +| comma(, ) | 将多个媒体特征以“或”的方式连接成一个媒体查询,如果存在结果为true的媒体特征,则查询条件成立。其效果等同于or运算符。例如:screen and (min-height: 1000), (round-screen: true) 表示当应用高度大于等于1000个像素单位或者设备屏幕是圆形时,条件成立。 | + +媒体范围操作符包括<=,>=,<,>,详细解释说明如下表。 + + **表2** 媒体逻辑范围操作符 + +| 类型 | 说明 | +| ----- | ---------------------------------------- | +| <= | 小于等于,例如:screen and (height <= 50)。 | +| >= | 大于等于,例如:screen and (height >= 600)。 | +| < | 小于,例如:screen and (height < 50)。 | +| > | 大于,例如:screen and (height > 600)。 | + + +### 媒体特征(media-feature) + +媒体特征包括应用显示区域的宽高、设备分辨率以及设备的宽高等属性,详细说明如下表。 + + **表3** 媒体特征说明表 + +| 类型 | 说明 | +| ----------------- | ---------------------------------------- | +| height | 应用页面显示区域的高度。 | +| min-height | 应用页面显示区域的最小高度。 | +| max-height | 应用页面显示区域的最大高度。 | +| width | 应用页面显示区域的宽度。 | +| min-width | 应用页面显示区域的最小宽度。 | +| max-width | 应用页面显示区域的最大宽度。 | +| resolution | 设备的分辨率,支持dpi,dppx和dpcm单位。其中:
- dpi表示每英寸中物理像素个数,1dpi ≈ 0.39dpcm;
- dpcm表示每厘米上的物理像素个数,1dpcm ≈ 2.54dpi;
- dppx表示每个px中的物理像素数(此单位按96px = 1英寸为基准,与页面中的px单位计算方式不同),1dppx = 96dpi。 | +| min-resolution | 设备的最小分辨率。 | +| max-resolution | 设备的最大分辨率。 | +| orientation | 屏幕的方向。
可选值:
-  orientation:  portrait(设备竖屏);
-  orientation:  landscape(设备横屏)。 | +| device-height | 设备的高度。 | +| min-device-height | 设备的最小高度。 | +| max-device-height | 设备的最大高度。 | +| device-width | 设备的宽度。 | +| device-type | 设备的类型。
可选值:default、tablet。 | +| min-device-width | 设备的最小宽度。 | +| max-device-width | 设备的最大宽度。 | +| round-screen | 屏幕类型,圆形屏幕为true,非圆形屏幕为false。 | +| dark-mode | 系统为深色模式时为true,否则为false。 | + + +## 场景示例 + +下例中使用媒体查询,实现屏幕横竖屏切换时,给页面文本应用添加不同的内容和样式。 + +Stage模型下的示例: + + +```ts +import mediaquery from '@ohos.mediaquery'; +import window from '@ohos.window'; +import common from '@ohos.app.ability.common'; + +let portraitFunc = null; + +@Entry +@Component +struct MediaQueryExample { + @State color: string = '#DB7093'; + @State text: string = 'Portrait'; + // 当设备横屏时条件成立 + listener = mediaquery.matchMediaSync('(orientation: landscape)'); + + // 当满足媒体查询条件时,触发回调 + onPortrait(mediaQueryResult) { + if (mediaQueryResult.matches) { // 若设备为横屏状态,更改相应的页面布局 + this.color = '#FFD700'; + this.text = 'Landscape'; + } else { + this.color = '#DB7093'; + this.text = 'Portrait'; + } + } + + aboutToAppear() { + // 绑定当前应用实例 + portraitFunc = this.onPortrait.bind(this); + // 绑定回调函数 + this.listener.on('change', portraitFunc); + } + + // 改变设备横竖屏状态函数 + private changeOrientation(isLandscape: boolean) { + // 获取UIAbility实例的上下文信息 + let context = getContext(this) as common.UIAbilityContext; + // 调用该接口手动改变设备横竖屏状态 + window.getLastWindow(context).then((lastWindow) => { + lastWindow.setPreferredOrientation(isLandscape ? window.Orientation.LANDSCAPE : window.Orientation.PORTRAIT) + }); + } + + build() { + Column({ space: 50 }) { + Text(this.text).fontSize(50).fontColor(this.color) + Text('Landscape').fontSize(50).fontColor(this.color).backgroundColor(Color.Orange) + .onClick(() => { + this.changeOrientation(true); + }) + Text('Portrait').fontSize(50).fontColor(this.color).backgroundColor(Color.Orange) + .onClick(() => { + this.changeOrientation(false); + }) + } + .width('100%').height('100%') + } +} +``` + +FA模型下的示例: + + +```ts +import mediaquery from '@ohos.mediaquery'; +import featureAbility from '@ohos.ability.featureAbility'; + +let portraitFunc = null; + +@Entry +@Component +struct MediaQueryExample { + @State color: string = '#DB7093'; + @State text: string = 'Portrait'; + listener = mediaquery.matchMediaSync('(orientation: landscape)'); // 当设备横屏时条件成立 + + onPortrait(mediaQueryResult) { // 当满足媒体查询条件时,触发回调 + if (mediaQueryResult.matches) { // 若设备为横屏状态,更改相应的页面布局 + this.color = '#FFD700'; + this.text = 'Landscape'; + } else { + this.color = '#DB7093'; + this.text = 'Portrait'; + } + } + + aboutToAppear() { + portraitFunc = this.onPortrait.bind(this); // 绑定当前应用实例 + this.listener.on('change', portraitFunc); //绑定回调函数 + } + + build() { + Column({ space: 50 }) { + Text(this.text).fontSize(50).fontColor(this.color) + Text('Landscape').fontSize(50).fontColor(this.color).backgroundColor(Color.Orange) + .onClick(() => { + let context = featureAbility.getContext(); + context.setDisplayOrientation(0); //调用该接口手动改变设备横竖屏状态 + }) + Text('Portrait').fontSize(50).fontColor(this.color).backgroundColor(Color.Orange) + .onClick(() => { + let context = featureAbility.getContext(); + context.setDisplayOrientation(1); //调用该接口手动改变设备横竖屏状态 + }) + } + .width('100%').height('100%') + } +} +``` + + **图1** 竖屏   + +![portralit](figures/portralit.jpg) + + **图2** 横屏   + +![landscape](figures/landscape.jpg) + +## 相关实例 + +基于媒体查询,可参考以下实例: + +[媒体查询](https://gitee.com/openharmony/applications_app_samples/tree/master/ETSUI/MediaQuery):使用媒体查询,完成在不同设备上显示不同的界面效果。 diff --git a/zh-cn/application-dev/ui/arkts-layout-development-overview.md b/zh-cn/application-dev/ui/arkts-layout-development-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..93398ca32fe0078e09fa2c19bcdf8da5fc425c47 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-layout-development-overview.md @@ -0,0 +1,79 @@ +# 布局概述 + + +组件按照布局的要求依次排列,构成应用的页面。在声明式UI中,所有的页面都是由自定义组件构成,开发者可以根据自己的需求,选择合适的布局进行页面开发。 + + +布局指用特定的组件或者属性来管理用户页面所放置UI组件的大小和位置。在实际的开发过程中,需要遵守以下流程保证整体的布局效果: + + +- 确定页面的布局结构。 + +- 分析页面中的元素构成。 + +- 选用适合的布局容器组件或属性控制页面中各个元素的位置和大小约束。 + + +## 布局结构 + +布局的结构通常是分层级的,代表了用户界面中的整体架构。一个常见的页面结构如下所示: + + **图1** 常见页面结构图   + +![common-page-structure](figures/common-page-structure.png) + +为实现上述效果,开发者需要在页面中声明对应的元素。其中,Page表示页面的根节点,Column/Row等元素为系统组件。针对不同的页面结构,ArkUI提供了不同的布局组件来帮助开发者实现对应布局的效果,例如Row用于实现线性布局。 + + +## 布局元素的组成 + +布局相关的容器组件可形成对应的布局效果。例如,List组件可构成线性布局。 + + **图2** 布局元素组成图   + +![layout-element-omposition](figures/layout-element-omposition.png) + +- 组件区域(蓝区方块):组件区域表明组件的大小,width、height属性设置该区域的大小。 + +- 组件内容区(黄色方块):组件区域大小减去组件的Padding值,组件内容区大小会作为组件内容(或者子组件)进行大小测算时的布局测算限制。 + +- 组件内容(绿色方块):组件内容本身占用的大小,比如文本内容占用的大小。组件内容和组件内容区不一定匹配,比如设置了固定的width和height,此时组件内容区大小就是设置的width和height减去Padding值,但文本内容则是通过文本布局引擎测算后得到的大小,可能出现文本真实大小小于设置的组件内容区大小。当组件内容和组件内容区大小不一致时,align属性生效,定义组件内容在组件内容区的对齐方式,如居中对齐。 + +- 组件布局边界(虚线部分):组件通过Margin属性设置外边距时,组件布局边界就是组件区域加上Margin的大小。 + + +## 如何选择布局 + +声明式UI提供了以下8种常见布局,开发者可根据实际应用场景选择合适的布局进行页面开发。 + +| 布局 | 应用场景 | +| ---------------------------------------- | ---------------------------------------- | +| [线性布局](arkts-layout-development-linear.md)(Row、Column) | 如果布局内子元素为复数个,且能够以某种方式线性排列时优先考虑此布局。 | +| [层叠布局](arkts-layout-development-stack-layout.md)(Stack) | 组件需要有堆叠效果时优先考虑此布局,层叠布局的堆叠效果不会占用或影响其他同容器内子组件的布局空间。例如[Panel](../reference/arkui-ts/ts-container-panel.md)作为子组件弹出时将其他组件覆盖更为合理,则优先考虑在外层使用堆叠布局。 | +| [弹性布局](arkts-layout-development-flex-layout.md)(Flex) | 弹性布局是与线性布局类似的布局方式。区别在于弹性布局默认能够使子组件压缩或拉伸。在子组件需要计算拉伸或压缩比例时优先使用此布局,可使得多个容器内子组件能有更好的视觉上的填充容器效果。 | +| [相对布局](arkts-layout-development-relative-layout.md)(RelativeContainer) | 相对布局是在二维空间中的布局方式,不需要遵循线性布局的规则,布局方式更为自由。通过在子组件上设置锚点规则(AlignRules)使子组件能够将自己在横轴、纵轴中的位置与容器或容器内其他子组件的位置对齐。设置的锚点规则可以天然支持子元素压缩、拉伸,堆叠或形成多行效果。在页面元素分布复杂或通过线性布局会使容器嵌套层数过深时推荐使用。 | +| [栅格布局](arkts-layout-development-grid-layout.md)(GridRow、GridCol) | 栅格是多设备场景下通用的辅助定位工具,通过将空间分割为有规律的栅格。栅格不同于网格布局固定的空间划分,可以实现不同设备下不同的布局,空间划分更随心所欲,从而显著降低适配不同屏幕尺寸的设计及开发成本,使得整体设计和开发流程更有秩序和节奏感,同时也保证多设备上应用显示的协调性和一致性,提升用户体验。推荐内容相同但布局不同时使用。 | +| [媒体查询](arkts-layout-development-media-query.md)(\@ohos.mediaquery) | 媒体查询可根据不同设备类型或同设备不同状态修改应用的样式。例如根据设备和应用的不同属性信息设计不同的布局,以及屏幕发生动态改变时更新应用的页面布局。 | +| [列表](arkts-layout-development-create-list.md)(List) | 使用列表可以轻松高效地显示结构化、可滚动的信息。在ArkUI中,列表具有垂直和水平布局能力和自适应交叉轴方向上排列个数的布局能力,超出屏幕时可以滚动。列表适合用于呈现同类数据类型或数据类型集,例如图片和文本。 | +| [网格](arkts-layout-development-create-grid.md)(Grid) | 网格布局具有较强的页面均分能力,子组件占比控制能力,是一种重要自适应布局。网格布局可以控制元素所占的网格数量、设置子组件横跨几行或者几列,当网格容器尺寸发生变化时,所有子组件以及间距等比例调整。推荐在需要按照固定比例或者均匀分配空间的布局场景下使用,例如计算器、相册、日历等。 | +| [轮播](arkts-layout-development-create-looping.md)(Swiper) | 轮播组件通常用于实现广告轮播、图片预览、可滚动应用等。 | + + +## 布局位置 + +position、offset等属性影响了布局容器相对于自身或其他组件的位置。 + +| 定位能力 | 使用场景 | 实现方式 | +| ---- | ---------------------------------------- | ---------------------------------------- | +| 绝对定位 | 对于不同尺寸的设备,使用绝对定位的适应性会比较差,在屏幕的适配上有缺陷。 | 使用[position](../reference/arkui-ts/ts-universal-attributes-location.md)实现绝对定位,设置元素左上角相对于父容器左上角偏移位置。在布局容器中,设置该属性不影响父容器布局,仅在绘制时进行位置调整。 | +| 相对定位 | 相对定位不脱离文档流,即原位置依然保留,不影响元素本身的特性,仅相对于原位置进行偏移。 | 使用[offset](../reference/arkui-ts/ts-universal-attributes-location.md)可以实现相对定位,设置元素相对于自身的偏移量。设置该属性,不影响父容器布局,仅在绘制时进行位置调整。 | + + +## 对子元素的约束 + +| 对子元素的约束能力 | 使用场景 | 实现方式 | +| --------- | ---------------------------------------- | ---------------------------------------- | +| 拉伸 | 容器组件尺寸发生变化时,增加或减小的空间全部分配给容器组件内指定区域。 | [flexGrow](../reference/arkui-ts/ts-universal-attributes-flex-layout.md)和[flexShrink](../reference/arkui-ts/ts-universal-attributes-flex-layout.md)属性:
1. flexGrow基于父容器的剩余空间分配来控制组件拉伸。
2. flexShrink设置父容器的压缩尺寸来控制组件拉伸。 | +| 缩放 | 子组件的宽高按照预设的比例,随容器组件发生变化,且变化过程中子组件的宽高比不变。 | [aspectRatio](../reference/arkui-ts/ts-universal-attributes-layout-constraints.md)属性指定当前组件的宽高比来控制缩放,公式为:aspectRatio=width/height。 | +| 占比 | 占比能力是指子组件的宽高按照预设的比例,随父容器组件发生变化。 | 基于通用属性的两种实现方式:
1. 将子组件的宽高设置为父组件宽高的百分比。
2. [layoutWeight](../reference/arkui-ts/ts-universal-attributes-size.md)属性,使得子元素自适应占满剩余空间。 | +| 隐藏 | 隐藏能力是指容器组件内的子组件,按照其预设的显示优先级,随容器组件尺寸变化显示或隐藏,其中相同显示优先级的子组件同时显示或隐藏。 | 通过[displayPriority](../reference/arkui-ts/ts-universal-attributes-layout-constraints.md)属性来控制页面的显示和隐藏。 | diff --git a/zh-cn/application-dev/ui/arkts-layout-development-performance-boost.md b/zh-cn/application-dev/ui/arkts-layout-development-performance-boost.md new file mode 100644 index 0000000000000000000000000000000000000000..cace34193f8de687b16d743ce8238aea0cdd3961 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-layout-development-performance-boost.md @@ -0,0 +1,62 @@ +# 改善布局性能 + + +Flex为采用弹性布局的容器。容器内部的所有子元素,会自动参与弹性布局。子元素默认沿主轴排列,子元素在主轴方向的尺寸称为主轴尺寸。 + + +在单行布局场景下,子组件的主轴尺寸长度和可能存在不等于容器主轴尺寸长度的情况,部分子组件会被布局两次来填充容器,即需要二次布局,导致布局效率下降。 + + +## 场景一 + +所有子组件未设置DisplayPriority属性(或DisplayPriority设置为默认值1)和LayoutWeight属性(或LayoutWeight设置为默认值0)时,所有子组件先按序布局一次。 + + +- 第一次布局子组件主轴尺寸长度总和等于容器主轴尺寸长度,不需要二次布局。 + ![layout-performance-1](figures/layout-performance-1.png) + +- 第一次布局子组件主轴尺寸长度总和小于容器主轴尺寸长度,且包含设置有效的flexGrow属性的子组件,设置有效的flexGrow属性的子组件会触发二次布局,拉伸布局填满容器。 + ![layout-performace-2](figures/layout-performace-2.gif) + +- 第一次布局子组件主轴尺寸长度总和大于容器主轴尺寸长度,且包含设置有效的flexShrink属性(flex子组件默认值为1,为有效值)的子组件,设置有效的flexShrink属性的子组件会触发二次布局,压缩布局填满容器。 + ![layout-performace-3](figures/layout-performace-3.gif) + + +## 场景二 + +子组件存在设置DisplayPriority属性,不存在设置LayoutWeight属性。 + + +根据DisplayPriority从大到小顺序,布局每组同DisplayPriority值的子组件,直到子组件主轴尺寸长度总和最大且不超过容器主轴尺寸长度,舍弃未布局的低优先级DisplayPriority(可能存在一组临界DisplayPriority值的子组件布局但未使用的情况)。 + + +- 第一次布局子组件主轴尺寸长度总和等于容器主轴尺寸长度,不需要二次布局。 + ![layout-performance-4](figures/layout-performance-4.png) + +- 第一次布局子组件主轴尺寸长度总和小于容器主轴尺寸长度,且包含设置有效的flexGrow属性的子组件,设置有效的flexGrow属性的子组件会触发二次布局,拉伸布局填满容器。 + ![layout-performace-5](figures/layout-performace-5.gif) + + +## 场景三 + +子组件中存在设置LayoutWeight属性。 + +根据DisplayPriority从大到小顺序,布局每组DisplayPriority值相同且不设置LayoutWeight属性的子组件,直到子组件主轴尺寸长度总和最大且不超过容器主轴尺寸长度,舍弃未布局的低优先级DisplayPriority,可能存在一组临界DisplayPriority值的子组件布局但未使用的情况。 + + +剩余空间按设置LayoutWeight属性的子组件的LayoutWeight比例填满容器。 + + +- 两次遍历都只布局一次组件,不会触发二次布局。 + ![layout-performace-6](figures/layout-performace-6.gif) + + +## 如何优化Flex的布局性能 + +- 使用Column/Row代替Flex。 + +- 大小不需要变更的子组件主动设置flexShrink属性值为0。 + +- 优先使用LayoutWeight属性替代flexGrow属性和flexShrink属性。 + +- 子组件主轴长度分配设置为最常用场景的布局结果,使子组件主轴长度总和等于Flex容器主轴长度。 diff --git a/zh-cn/application-dev/ui/arkts-layout-development-relative-layout.md b/zh-cn/application-dev/ui/arkts-layout-development-relative-layout.md new file mode 100644 index 0000000000000000000000000000000000000000..6f61183dd9b41e91eec078222b2c5e8928f3414a --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-layout-development-relative-layout.md @@ -0,0 +1,158 @@ +# 相对布局 + + +## 概述 + +[RelativeContainer](../reference/arkui-ts/ts-container-relativecontainer.md)为采用相对布局的容器,支持容器内部的子元素设置相对位置关系。子元素支持指定兄弟元素作为锚点,也支持指定父容器作为锚点,基于锚点做相对位置布局。下图是一个RelativeContainer的概念图,图中的虚线表示位置的依赖关系。 + + + **图1** 相对布局示意图   + +![relative-layout](figures/relative-layout.png) + + +子元素并不完全是上图中的依赖关系。比如,Item4可以以Item2为依赖锚点,也可以以RelativeContainer父容器为依赖锚点。 + + +## 基本概念 + +- 锚点:通过锚点设置当前元素基于哪个元素确定位置。 + +- 对齐方式:通过对齐方式,设置当前元素是基于锚点的上中下对齐,还是基于锚点的左中右对齐。 + + +## 设置依赖关系 + + +### 锚点设置 + +锚点设置是指设置子元素相对于父元素或兄弟元素的位置依赖关系。在水平方向上,可以设置left、middle、right的锚点。在竖直方向上,可以设置top、center、bottom的锚点。为了明确定义锚点,必须为RelativeContainer及其子元素设置ID,用于指定锚点信息。ID默认为“container”,其余子元素的ID通过id属性设置。未设置ID的子元素在RelativeContainer中不会显示。 + +>**说明:** +> +>在使用锚点时要注意子元素的相对位置关系,避免出现错位或遮挡的情况。 + +- RelativeContainer父组件为锚点,__container__代表父容器的id。 + + ```ts + RelativeContainer() { + Row() + // 添加其他属性 + .alignRules({ + top: { anchor: '__container__', align: VerticalAlign.Top }, + left: { anchor: '__container__', align: HorizontalAlign.Start } + }) + .id("row1") + + Row() + ... + .alignRules({ + top: { anchor: '__container__', align: VerticalAlign.Top }, + right: { anchor: '__container__', align: HorizontalAlign.End } + }) + .id("row2") + } + ... + ``` + + ![zh-cn_image_0000001562820901](figures/zh-cn_image_0000001562820901.png) + +- 以子元素为锚点。 + + ```ts + RelativeContainer() { + ... + top: { anchor: 'row1', align: VerticalAlign.Bottom }, + ... + } + .width(300).height(300) + .margin({ left: 20 }) + .border({ width: 2, color: '#6699FF' }) + ``` + + ![zh-cn_image_0000001562940613](figures/zh-cn_image_0000001562940613.png) + + +### 设置相对于锚点的对齐位置 + +设置了锚点之后,可以通过align设置相对于锚点的对齐位置。 + +在水平方向上,对齐位置可以设置为HorizontalAlign.Start、HorizontalAlign.Center、HorizontalAlign.End。 + +![alignment-relative-anchor-horizontal](figures/alignment-relative-anchor-horizontal.png) + +在竖直方向上,对齐位置可以设置为VerticalAlign.Top、VerticalAlign.Center、VerticalAlign.Bottom。 + +![alignment-relative-anchor-vertical](figures/alignment-relative-anchor-vertical.png) + + +## 场景实例 + +相对布局内的子元素相对灵活,只要在RelativeContainer容器内,均可以通过alignRules进行相应相应的位置移动。 + + +```ts +@Entry +@Component +struct Index { + build() { + Row() { + RelativeContainer() { + Row() + .width(100) + .height(100) + .backgroundColor('#FF3333') + .alignRules({ + top: { anchor: '__container__', align: VerticalAlign.Top }, //以父容器为锚点,竖直方向顶头对齐 + middle: { anchor: '__container__', align: HorizontalAlign.Center } //以父容器为锚点,水平方向居中对齐 + }) + .id('row1') //设置锚点为row1 + + Row() { + Image($r('app.media.icon')) + } + .height(100).width(100) + .alignRules({ + top: { anchor: 'row1', align: VerticalAlign.Bottom }, //以row1组件为锚点,竖直方向低端对齐 + left: { anchor: 'row1', align: HorizontalAlign.Start } //以row1组件为锚点,水平方向开头对齐 + }) + .id('row2') //设置锚点为row2 + + Row() + .width(100) + .height(100) + .backgroundColor('#FFCC00') + .alignRules({ + top: { anchor: 'row2', align: VerticalAlign.Top } + }) + .id('row3') //设置锚点为row3 + + Row() + .width(100) + .height(100) + .backgroundColor('#FF9966') + .alignRules({ + top: { anchor: 'row2', align: VerticalAlign.Top }, + left: { anchor: 'row2', align: HorizontalAlign.End }, + }) + .id('row4') //设置锚点为row4 + + Row() + .width(100) + .height(100) + .backgroundColor('#FF66FF') + .alignRules({ + top: { anchor: 'row2', align: VerticalAlign.Bottom }, + middle: { anchor: 'row2', align: HorizontalAlign.Center } + }) + .id('row5') //设置锚点为row5 + } + .width(300).height(300) + .border({ width: 2, color: '#6699FF' }) + } + .height('100%').margin({ left: 30 }) + } +} +``` + +![zh-cn_image_0000001562700529](figures/zh-cn_image_0000001562700529.png) diff --git a/zh-cn/application-dev/ui/arkts-layout-development-stack-layout.md b/zh-cn/application-dev/ui/arkts-layout-development-stack-layout.md new file mode 100644 index 0000000000000000000000000000000000000000..73b4fc615c9b71112b9b245b5cdcaa593616a7bf --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-layout-development-stack-layout.md @@ -0,0 +1,135 @@ +# 层叠布局 + + +## 概述 + +层叠布局(StackLayout)用于在屏幕上预留一块区域来显示组件中的元素,提供元素可以重叠的布局。层叠布局通过[Stack](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-container-stack.md/)容器组件实现位置的固定定位与层叠,容器中的子元素(子组件)依次入栈,后一个子元素覆盖前一个子元素,子元素可以叠加,也可以设置位置。 + +层叠布局具有较强的页面层叠、位置定位能力,其使用场景有广告、卡片层叠效果等。 + +如图1,Stack作为容器,容器内的子元素(子组件)的顺序为Item1->Item2->Item3。 + + + **图1** 层叠布局   + +![stack-layout](figures/stack-layout.png) + + +## 开发布局 + +Stack组件为容器组件,容器内可包含各种子组件。其中的子组件根据自己的大小默认进行居中堆叠。子元素被约束在Stack下,进行自己的样式定义以及排列。 + + + +```ts +Column(){ + Stack({ }) { + Column(){}.width('90%').height('100%').backgroundColor('#ff58b87c') + Text('text').width('60%').height('60%').backgroundColor('#ffc3f6aa') + Button('button').width('30%').height('30%').backgroundColor('#ff8ff3eb').fontColor('#000') + }.width('100%').height(150).margin({ top: 50 }) +} +``` + + +![stack-layout-sample](figures/stack-layout-sample.png) + + +## 对齐方式 + +Stack组件通过[alignContent参数](../reference/arkui-ts/ts-appendix-enums.md#alignment)实现位置的相对移动。如图2所示,支持九种对齐方式。 + + **图2** Stack容器内元素的对齐方式   + +![zh-cn_image_0000001562940621](figures/zh-cn_image_0000001562940621.png) + + +## Z序控制 + +Stack容器中兄弟组件显示层级关系可以通过[Z序控制](../reference/arkui-ts/ts-universal-attributes-z-order.md)的zIndex属性改变。zIndex值越大,显示层级越高,即zIndex值大的组件会覆盖在zIndex值小的组件上方。 + + 在层叠布局中,如果后面子元素尺寸大于前面子元素尺寸,则前面子元素完全隐藏。 + +```ts +Stack({ alignContent: Alignment.BottomStart }) { + Column() { + Text('Stack子元素1').textAlign(TextAlign.End).fontSize(20) + }.width(100).height(100).backgroundColor(0xffd306) + + Column() { + Text('Stack子元素2').fontSize(20) + }.width(150).height(150).backgroundColor(Color.Pink) + + Column() { + Text('Stack子元素3').fontSize(20) + }.width(200).height(200).backgroundColor(Color.Grey) +}.margin({ top: 100 }).width(350).height(350).backgroundColor(0xe0e0e0) +``` + +![zh-cn_image_0000001511900544](figures/zh-cn_image_0000001511900544.png) + +下图中,最后的子元素3的尺寸大于前面的所有子元素,所以,前面两个元素完全隐藏。改变子元素1,子元素2的zIndex属性后,可以将元素展示出来。 + + +```ts +Stack({ alignContent: Alignment.BottomStart }) { + Column() { + Text('Stack子元素1').fontSize(20) + }.width(100).height(100).backgroundColor(0xffd306).zIndex(2) + + Column() { + Text('Stack子元素2').fontSize(20) + }.width(150).height(150).backgroundColor(Color.Pink).zIndex(1) + + Column() { + Text('Stack子元素3').fontSize(20) + }.width(200).height(200).backgroundColor(Color.Grey) +}.margin({ top: 100 }).width(350).height(350).backgroundColor(0xe0e0e0) +``` + +![zh-cn_image_0000001563060797](figures/zh-cn_image_0000001563060797.png) + + +## 场景示例 + +使用层叠布局快速搭建页面显示模型。 + + +```ts +@Entry +@Component +struct StackSample { + private arr: string[] = ['APP1', 'APP2', 'APP3', 'APP4', 'APP5', 'APP6', 'APP7', 'APP8']; + + build() { + Stack({ alignContent: Alignment.Bottom }) { + Flex({ wrap: FlexWrap.Wrap }) { + ForEach(this.arr, (item) => { + Text(item) + .width(100) + .height(100) + .fontSize(16) + .margin(10) + .textAlign(TextAlign.Center) + .borderRadius(10) + .backgroundColor(0xFFFFFF) + }, item => item) + }.width('100%').height('100%') + + Flex({ justifyContent: FlexAlign.SpaceAround, alignItems: ItemAlign.Center }) { + Text('联系人').fontSize(16) + Text('设置').fontSize(16) + Text('短信').fontSize(16) + } + .width('50%') + .height(50) + .backgroundColor('#16302e2e') + .margin({ bottom: 15 }) + .borderRadius(15) + }.width('100%').height('100%').backgroundColor('#CFD0CF') + } +} +``` + + +![zh-cn_image_0000001511421368](figures/zh-cn_image_0000001511421368.png) diff --git a/zh-cn/application-dev/ui/arkts-layout-update-animation.md b/zh-cn/application-dev/ui/arkts-layout-update-animation.md new file mode 100644 index 0000000000000000000000000000000000000000..bb0787e27dca59fce8bb5dd5d78ae89bece61bbf --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-layout-update-animation.md @@ -0,0 +1,270 @@ +# 布局更新动画 + + +[属性动画](../reference/arkui-ts/ts-animatorproperty.md)(animation)和[显式动画](../reference/arkui-ts/ts-explicit-animation.md)(animateTo)是ArkUI提供的最基础和常用的动画功能。在布局属性(如[尺寸属性](../reference/arkui-ts/ts-universal-attributes-size.md)、[位置属性](../reference/arkui-ts/ts-universal-attributes-location.md))发生变化时,可以通过属性动画或显式动画,按照动画参数过渡到新的布局参数状态。 + + +| 动画类型 | 特点 | 适用场景 | +| ---- | ---------------------------------------- | -------- | +| 属性动画 | 动画设置简单,属性变化时自动触发动画。 | 较简单的动画场景 | +| 显式动画 | 闭包内的变化均会触发动画,包括由数据变化引起的组件的增删、组件属性的变化等,可以做较为复杂的动画。 | 较复杂的动画场景 | + + +## 使用显式动画产生布局更新动画 + +显式动画的接口为: + + +```ts +animateTo(value: AnimateParam, event: () => void): void +``` + +第一个参数指定动画参数,第二个参数为动画的闭包函数。 + +以下是使用显式动画产生布局更新动画的示例。示例中,当Column组件的alignItems属性改变后,其子组件的布局位置结果发生变化。只要该属性是在animateTo的闭包函数中修改的,那么由其引起的所有变化都会按照animateTo的动画参数执行动画过渡到终点值。 + + +```ts +@Entry +@Component +struct LayoutChange { + // 用于控制Column的alignItems属性 + @State itemAlign: HorizontalAlign = HorizontalAlign.Start; + allAlign: HorizontalAlign[] = [HorizontalAlign.Start, HorizontalAlign.Center, HorizontalAlign.End]; + alignIndex: number = 0; + + build() { + Column() { + Column({ space: 10 }) { + Button("1").width(100).height(50) + Button("2").width(100).height(50) + Button("3").width(100).height(50) + } + .margin(20) + .alignItems(this.itemAlign) + .borderWidth(2) + .width("90%") + .height(200) + + Button("click").onClick(() => { + // 动画时长为1000ms,曲线为EaseInOut + animateTo({ duration: 1000, curve: Curve.EaseInOut }, () => { + this.alignIndex = (this.alignIndex + 1) % this.allAlign.length; + // 在闭包函数中修改this.itemAlign参数,使Column容器内部孩子的布局方式变化,使用动画过渡到新位置 + this.itemAlign = this.allAlign[this.alignIndex]; + }); + }) + } + .width("100%") + .height("100%") + } +} +``` + + +![layoutChange1](figures/layoutChange1.gif) + + +除直接改变布局方式外,也可直接修改组件的宽、高、位置。 + + + +```ts +@Entry +@Component +struct LayoutChange2 { + @State myWidth: number = 100; + @State myHeight: number = 50; + // 标志位,true和false分别对应一组myWidth、myHeight值 + @State flag: boolean = false; + + build() { + Column({ space: 10 }) { + Button("text") + .type(ButtonType.Normal) + .width(this.myWidth) + .height(this.myHeight) + .margin(20) + Button("area: click me") + .fontSize(12) + .margin(20) + .onClick(() => { + animateTo({ duration: 1000, curve: Curve.Ease }, () => { + // 动画闭包中根据标志位改变控制第一个Button宽高的状态变量,使第一个Button做宽高动画 + if (this.flag) { + this.myWidth = 100; + this.myHeight = 50; + } else { + this.myWidth = 200; + this.myHeight = 100; + } + this.flag = !this.flag; + }); + }) + } + .width("100%") + .height("100%") + } +} +``` + + +在第二个Button的点击事件中,使用animateTo函数,在闭包中修改this.myWidth和this.myHeight状态变量,而这两个状态变量分别为第一个Button的宽、高属性值,所以第一个Button做了宽高动画。效果如下图。 + + +![layoutChange2_animateTo](figures/layoutChange2_animateTo.gif) + + +与此同时,第二个Button也产生了一个位置动画。这是由于第一个Button的宽高变化后,引起了Column内部其他组件的布局结果也发生了变化,第二个Button的布局发生变化也是由于闭包内改变第一个Button的宽高造成的。 + + +如果不希望第二个Button有动画效果,有两种方式可以实现。一种是给做第一个Button外面再加一个容器,使其动画前后的大小都在容器的范围内,这样第二个Button的位置不会被第一个Button的位置所影响。修改后的核心代码如下。 + + + +```ts +Column({ space: 10 }) { + Column() { + // Button放在足够大的容器内,使其不影响更外层的组件位置 + Button("text") + .type(ButtonType.Normal) + .width(this.myWidth) + .height(this.myHeight) + } + .margin(20) + .width(200) + .height(100) + + Button("area: click me") + .fontSize(12) + .onClick(() => { + animateTo({ duration: 1000, curve: Curve.Ease }, () => { + // 动画闭包中根据标志位改变控制第一个Button宽高的状态变量,使第一个Button做宽高动画 + if (this.flag) { + this.myWidth = 100; + this.myHeight = 50; + } else { + this.myWidth = 200; + this.myHeight = 100; + } + this.flag = !this.flag; + }); + }) +} +.width("100%") +.height("100%") +``` + + +![layoutChange2_animateTo_change](figures/layoutChange2_animateTo_change.gif) + + +另一种方式是给第二个Button添加布局约束,如position的位置约束,使其位置不被第一个Button的宽高影响。核心代码如下: + + + +```ts +Column({ space: 10 }) { + Button("text") + .type(ButtonType.Normal) + .width(this.myWidth) + .height(this.myHeight) + .margin(20) + + Button("area: click me") + .fontSize(12) + // 配置position属性固定,使自己的布局位置不被第一个Button的宽高影响 + .position({ x: "30%", y: 200 }) + .onClick(() => { + animateTo({ duration: 1000, curve: Curve.Ease }, () => { + // 动画闭包中根据标志位改变控制第一个Button宽高的状态变量,使第一个Button做宽高动画 + if (this.flag) { + this.myWidth = 100; + this.myHeight = 50; + } else { + this.myWidth = 200; + this.myHeight = 100; + } + this.flag = !this.flag; + }); + }) +} +.width("100%") +.height("100%") +``` + + +## 使用属性动画产生布局更新动画 + +显式动画把要执行动画的属性的修改放在闭包函数中触发动画,而属性动画则无需使用闭包,把animation属性加在要做属性动画的组件的属性后即可。 + +属性动画的接口为: + + +```ts +animation(value: AnimateParam) +``` + +其入参为动画参数。想要组件随某个属性值的变化而产生动画,此属性需要加在animation属性之前。有的属性变化不希望通过animation产生属性动画,可以放在animation之后。上面显式动画的示例很容易改为用属性动画实现。例如: + + + +```ts +@Entry +@Component +struct LayoutChange2 { + @State myWidth: number = 100; + @State myHeight: number = 50; + @State flag: boolean = false; + @State myColor: Color = Color.Blue; + + build() { + Column({ space: 10 }) { + Button("text") + .type(ButtonType.Normal) + .width(this.myWidth) + .height(this.myHeight) + // animation只对其上面的type、width、height属性生效,时长为1000ms,曲线为Ease + .animation({ duration: 1000, curve: Curve.Ease }) + .backgroundColor(this.myColor) + .margin(20) + // animation对下面的backgroundColor、margin属性不生效 + + Button("area: click me") + .fontSize(12) + .onClick(() => { + // 改变属性值,配置了属性动画的属性会进行动画过渡 + if (this.flag) { + this.myWidth = 100; + this.myHeight = 50; + this.myColor = Color.Blue; + } else { + this.myWidth = 200; + this.myHeight = 100; + this.myColor = Color.Pink; + } + this.flag = !this.flag; + }) + } + } +} +``` + + +上述示例中,第一个button上的animation属性,只对写在animation之前的type、width、height属性生效,而对写在animation之后的backgroundColor、margin属性无效。运行结果是width、height属性会按照animation的动画参数执行动画,而backgroundColor会直接跳变,不会产生动画。效果如下图: + + + + + + +![size-change-animation](figures/size-change-animation.gif) + + +>**说明:** +> +> 1. 使用属性动画时,会按照指定的属性动画参数执行动画。每个组件可为自己的属性配置不同参数的属性动画。 +> +> 2. 显式动画会对动画闭包前后造成的所有界面差异执行动画,且使用同一动画参数,适用于统一执行的场景。此外,显式动画也可以用于一些非属性变量造成的动画,如if/else的条件,ForEach使用的数组元素的删减。 +> +> 3. 如果一个属性配置了属性动画,且在显式动画闭包中改变该属性值,属性动画优先生效,会使用属性动画的动画参数。 diff --git a/zh-cn/application-dev/ui/arkts-navigation-navigation.md b/zh-cn/application-dev/ui/arkts-navigation-navigation.md new file mode 100644 index 0000000000000000000000000000000000000000..95b29d505205845a7b2429a18eecdda82569b1eb --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-navigation-navigation.md @@ -0,0 +1,202 @@ +# Navigation + + +[Navigation](../reference/arkui-ts/ts-basic-components-navigation.md)组件一般作为页面的根容器,包括单页面、分栏和自适应三种显示模式。同时,Navigation提供了属性来设置页面的标题栏、工具栏、导航栏等。 + + +Navigation组件的页面包含主页和内容页。主页由标题栏、内容区和工具栏组成,可在内容区中使用[NavRouter](../reference/arkui-ts/ts-basic-components-navrouter.md)子组件实现导航栏功能。内容页主要显示[NavDestination](../reference/arkui-ts/ts-basic-components-navdestination.md)子组件中的内容。 + + +NavRouter是和Navigation搭配使用的特殊子组件,默认提供点击响应处理,不需要开发者自定义点击事件逻辑。NavRouter有且仅有两个根节点,第二个根节点是NavDestination。NavDestination是和NavRouter搭配使用的特殊子组件,用于显示Navigation组件的内容页。当开发者点击NavRouter组件时,会跳转到对应的NavDestination内容区。 + + +## 设置页面显示模式 + +Navigation组件通过mode属性设置页面的显示模式。 + +- 自适应模式 + Navigation组件默认为自适应模式,此时mode属性为NavigationMode.Auto。自适应模式下,当设备宽度大于520vp时,Navigation组件采用分栏模式,反之采用单页面模式。 + + + ``` + Navigation() { + ... + } + .mode(NavigationMode.Auto) + ``` + +- 单页面模式 + + **图1** 单页面布局示意图   + + ![zh-cn_image_0000001511740532](figures/zh-cn_image_0000001511740532.png) + + 将mode属性设置为NavigationMode.Stack,Navigation组件即可设置为单页面显示模式。 + + + ```ts + Navigation() { + ... + } + .mode(NavigationMode.Stack) + ``` + + ![单页面1](figures/单页面1.jpg) + +- 分栏模式 + + **图2** 分栏布局示意图 + + ![zh-cn_image_0000001562820845](figures/zh-cn_image_0000001562820845.png) + + 将mode属性设置为NavigationMode.Split,Navigation组件即可设置为分栏显示模式。 + + + ```ts + @Entry + @Component + struct NavigationExample { + private arr: number[] = [1, 2, 3]; + + build() { + Column() { + Navigation() { + TextInput({ placeholder: 'search...' }) + .width("90%") + .height(40) + .backgroundColor('#FFFFFF') + + List({ space: 12 }) { + ForEach(this.arr, (item) => { + ListItem() { + NavRouter() { + Text("NavRouter" + item) + .width("100%") + .height(72) + .backgroundColor('#FFFFFF') + .borderRadius(24) + .fontSize(16) + .fontWeight(500) + .textAlign(TextAlign.Center) + NavDestination() { + Text("NavDestinationContent" + item) + } + .title("NavDestinationTitle" + item) + } + } + }, item => item) + } + .width("90%") + .margin({ top: 12 }) + } + .title("主标题") + .mode(NavigationMode.Split) + .menus([ + {value: "", icon: "./image/ic_public_search.svg", action: ()=> {}}, + {value: "", icon: "./image/ic_public_add.svg", action: ()=> {}}, + {value: "", icon: "./image/ic_public_add.svg", action: ()=> {}}, + {value: "", icon: "./image/ic_public_add.svg", action: ()=> {}}, + {value: "", icon: "./image/ic_public_add.svg", action: ()=> {}} + ]) + .toolBar({items: [ + {value: "func", icon: "./image/ic_public_highlights.svg", action: ()=> {}}, + {value: "func", icon: "./image/ic_public_highlights.svg", action: ()=> {}}, + {value: "func", icon: "./image/ic_public_highlights.svg", action: ()=> {}} + ]}) + } + .height('100%') + .width('100%') + .backgroundColor('#F1F3F5') + } + } + ``` + + ![分栏](figures/分栏.jpg) + + +## 设置标题栏模式 + +标题栏在界面顶部,用于呈现界面名称和操作入口,Navigation组件通过titleMode属性设置标题栏模式。 + +- Mini模式 + 普通型标题栏,用于一级页面不需要突出标题的场景。 + + **图3** Mini模式标题栏   + + ![mini](figures/mini.jpg) + + + ```ts + Navigation() { + ... + } + .titleMode(NavigationTitleMode.Mini) + ``` + + +- Full模式 + 强调型标题栏,用于一级页面需要突出标题的场景。 + + **图4** Full模式标题栏   + + ![free1](figures/free1.jpg) + + + ```ts + Navigation() { + ... + } + .titleMode(NavigationTitleMode.Full) + ``` + + +## 设置菜单栏 + +菜单栏位于Navigation组件的右上角,开发者可以通过menus属性进行设置。menus支持Array<[NavigationMenuItem](../reference/arkui-ts/ts-basic-components-navigation.md#navigationmenuitem%E7%B1%BB%E5%9E%8B%E8%AF%B4%E6%98%8E)>和CustomBuilder两种参数类型。使用Array<NavigationMenuItem>类型时,竖屏最多支持显示3个图标,横屏最多支持显示5个图标,多余的图标会被放入自动生成的更多图标。 + +**图5** 设置了3个图标的菜单栏   + +![菜单栏2](figures/菜单栏2.jpg) + +```ts +Navigation() { + ... +} +.menus([{value: "", icon: "./image/ic_public_search.svg", action: ()=>{}}, + {value: "", icon: "./image/ic_public_add.svg", action: ()=>{}}, + {value: "", icon: "./image/ic_public_add.svg", action: ()=>{}}]) +``` + +**图6** 设置了4个图标的菜单栏   + +![菜单栏](figures/菜单栏.jpg) + +```ts +Navigation() { + ... +} +.menus([{value: "", icon: "./image/ic_public_search.svg", action: ()=>{}}, + {value: "", icon: "./image/ic_public_add.svg", action: ()=>{}}, + {value: "", icon: "./image/ic_public_add.svg", action: ()=>{}}, + {value: "", icon: "./image/ic_public_add.svg", action: ()=>{}}]) +``` + + +## 设置工具栏 + +工具栏位于Navigation组件的底部,开发者可以通过toolBar属性进行设置。 + + + **图7** 工具栏   + +![free3](figures/free3.jpg) + +```ts +Navigation() { + ... +} +.toolBar({items:[ + {value: "func", icon: "./image/ic_public_highlights.svg", action: ()=>{}}, + {value: "func", icon: "./image/ic_public_highlights.svg", action: ()=>{}}, + {value: "func", icon: "./image/ic_public_highlights.svg", action: ()=>{}}]}) +``` diff --git a/zh-cn/application-dev/ui/arkts-navigation-tabs.md b/zh-cn/application-dev/ui/arkts-navigation-tabs.md new file mode 100644 index 0000000000000000000000000000000000000000..ba97f9bb0ad707f7c0f38afc94d777fd367096f2 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-navigation-tabs.md @@ -0,0 +1,378 @@ +# Tabs + + +当页面信息较多时,为了让用户能够聚焦于当前显示的内容,需要对页面内容进行分类,提高页面空间利用率。[Tabs](../reference/arkui-ts/ts-container-tabs.md)组件可以在一个页面内快速实现视图内容的切换,一方面提升查找信息的效率,另一方面精简用户单次获取到的信息量。 + + +## 基本布局 + + Tabs组件的页面组成包含两个部分,分别是TabContent和TabBar。TabContent是内容页,TabBar是导航页签栏,页面结构如下图所示,根据不同的导航类型,布局会有区别,可以分为底部导航、顶部导航、侧边导航,其导航栏分别位于底部、顶部和侧边。 + **图1** Tabs组件布局示意图 + +![tabs-layout](figures/tabs-layout.png) + + +>**说明:** +> +> - TabContent组件不支持设置通用宽度属性,其宽度默认撑满Tabs父组件。 +> +> - TabContent组件不支持设置通用高度属性,其高度由Tabs父组件高度与TabBar组件高度决定。 + + +Tabs使用花括号包裹TabContent,如图2,其中TabContent显示相应的内容页。 + + + **图2** Tabs与TabContent使用   + +![tabs-tabscontent](figures/tabs-tabscontent.png) + + +每一个TabContent对应的内容需要有一个页签,可以通过TabContent的tabBar属性进行配置。在如下TabContent组件上设置属性tabBar,可以设置其对应页签中的内容,tabBar作为内容的页签。 + + + +```ts + TabContent() { + Text('首页的内容').fontSize(30) + } +.tabBar('首页') +``` + + +设置多个内容时,需在Tabs内按照顺序放置。 + + + +```ts +Tabs() { + TabContent() { + Text('首页的内容').fontSize(30) + } + .tabBar('首页') + + TabContent() { + Text('推荐的内容').fontSize(30) + } + .tabBar('推荐') + + TabContent() { + Text('发现的内容').fontSize(30) + } + .tabBar('发现') + + TabContent() { + Text('我的内容').fontSize(30) + } + .tabBar("我的") +} +``` + + +## 底部导航 + +底部导航是应用中最常见的一种导航方式。底部导航位于应用一级页面的底部,用户打开应用,能够分清整个应用的功能分类,以及页签对应的内容,并且其位于底部更加方便用户单手操作。底部导航一般作为应用的主导航形式存在,其作用是将用户关心的内容按照功能进行分类,迎合用户使用习惯,方便在不同模块间的内容切换。 + + + **图3** 底部导航栏   + +![底部导航](figures/底部导航.gif) + + +导航栏位置使用Tabs的参数barPosition进行设置,默认情况下,导航栏位于顶部,参数默认值为Start。设置为底部导航需要在Tabs传递参数,设置barPosition为End。 + + +```ts +Tabs({ barPosition: BarPosition.End }) { + // TabContent的内容:首页、发现、推荐、我的 + ... +} +``` + + +## 顶部导航 + +当内容分类较多,用户对不同内容的浏览概率相差不大,需要经常快速切换时,一般采用顶部导航模式进行设计,作为对底部导航内容的进一步划分,常见一些资讯类应用对内容的分类为关注、视频、数码,或者主题应用中对主题进行进一步划分为图片、视频、字体等。 + + **图4** 顶部导航栏   + +![顶部导航](figures/顶部导航.gif) + +Tabs组件默认的barPosition参数为Start,即顶部导航模式。 + + +```ts +Tabs({ barPosition: BarPosition.Start }) { + // TabContent的内容:关注、视频、游戏、数码、科技、体育、影视 + ... +} +``` + + +## 侧边导航 + +侧边导航是应用较为少见的一种导航模式,更多适用于横屏界面,用于对应用进行导航操作,由于用户的视觉习惯是从左到右,侧边导航栏默认为左侧侧边栏。 + + + **图5** 侧边导航栏   + +![侧边导航](figures/侧边导航.png) + + +实现侧边导航栏需要设置Tabs的属性vertical为true。在底部导航和顶部导航实现中,其默认值为false,表明内容页和导航栏垂直方向排列。 + + + +```ts +Tabs({ barPosition: BarPosition.Start }) { + // TabContent的内容:首页、发现、推荐、我的 + ... +} +.vertical(true) +.barWidth(100) +.barHeight(200) +``` + + +>**说明:** +> +> - vertical为true时,tabbar宽度会默认撑满屏幕的宽度,需要设置barWidth为合适值。 +> +> - vertical为true时,tabbar的高度会默认实际内容高度,需要设置barHeight为合适值。 + + +## 限制导航栏的滑动切换 + + 默认情况下,导航栏都支持滑动切换,在一些内容信息量需要进行多级分类的页面,如支持底部导航+顶部导航组合的情况下,底部导航栏的滑动效果与顶部导航出现冲突,此时需要限制底部导航的滑动,避免引起不好的用户体验。 + **图6** 限制底部导航栏滑动   + +![限制导航](figures/限制导航.gif) + + +控制滑动切换的属性为scrollable,默认值为true,表示可以滑动,若要限制滑动切换页签则需要设置为false。 + + + +```ts +Tabs({ barPosition: BarPosition.End }) { + TabContent(){ + Column(){ + Tabs(){ + // 顶部导航栏内容 + ... + } + } + .backgroundColor('#ff08a8f1') + .width('100%') + } + .tabBar('首页') + + // 其他TabContent内容:发现、推荐、我的 + ... +} +.scrollable(false) +``` + + +## 固定导航栏 + +当内容分类较为固定且不具有拓展性时,例如底部导航内容分类一般固定,分类数量一般在3-5个,此时使用固定导航栏。固定导航栏不可滚动,无法被拖拽滚动,内容均分tabBar的宽度。 + + + **图7** 固定导航栏  + +![固定导航](figures/固定导航.gif) + + +Tabs的属性barMode是控制导航栏是否可以滚动,默认值为Fixed。 + + + +```ts +Tabs({ barPosition: BarPosition.End }) { + // TabContent的内容:首页、发现、推荐、我的 + ... +} +.barMode(BarMode.Fixed) +``` + + +## 滚动导航栏 + +滚动导航栏可以用于顶部导航栏或者侧边导航栏的设置,内容分类较多,屏幕宽度无法容纳所有分类页签的情况下,需要使用可滚动的导航栏,支持用户点击和滑动来加载隐藏的页签内容。 + + + **图8** 可滚动导航栏   + +![滚动导航](figures/滚动导航.gif) + + +滚动导航栏需要设置Tabs组件的barMode属性,默认情况下其值为Fixed,表示为固定导航栏,设置为Scrollable即可设置为可滚动导航栏。 + + + +```ts +Tabs({ barPosition: BarPosition.Start }) { + // TabContent的内容:关注、视频、游戏、数码、科技、体育、影视、人文、艺术、自然、军事 + ... +} +.barMode(BarMode.Scrollable) +``` + + +## 自定义导航栏 + +对于底部导航栏,一般作为应用主页面功能区分,为了更好的用户体验,会组合文字以及对应语义图标表示页签内容,这种情况下,需要自定义导航页签的样式。 + + + **图9** 自定义导航栏图   + +![custom-navigation-bar](figures/custom-navigation-bar.png) + + +系统默认情况下采用了下划线标志当前活跃的页签,而自定义导航栏需要自行实现相应的样式,用于区分当前活跃页签和未活跃页签。 + + +设置自定义导航栏需要使用tabBar的参数,以其支持的CustomBuilder的方式传入自定义的函数组件样式。例如这里声明TabBuilder的自定义函数组件,传入参数包括页签文字title,对应位置index,以及选中状态和未选中状态的图片资源。通过当前活跃的currentIndex和页签对应的targetIndex匹配与否,决定UI显示的样式。 + + + +```ts +@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) +} +``` + + +在TabContent对应tabBar属性中传入自定义函数组件,并传递相应的参数。 + + + +```ts +TabContent() { + Column(){ + Text('我的内容') + } + .width('100%') + .height('100%') + .backgroundColor('#007DFF') +} +.tabBar(this.TabBuilder('我的', 0, $r('app.media.mine_selected'), $r('app.media.mine_normal'))) +``` + + +## 切换至指定页签 + +在不使用自定义导航栏时,系统默认的Tabs会实现切换逻辑。在使用了自定义导航栏后,切换页签的逻辑需要手动实现。即用户点击对应页签时,屏幕需要显示相应的内容页。 + + + **图10** 使用自定义导航栏实现切换指定页签   + +![切换指定页签](figures/切换指定页签.gif) + + +切换指定页签需要使用TabsController,TabsController是Tabs组件的控制器,用于控制Tabs组件进行页签切换。通过TabsController的changeIndex方法来实现跳转至指定索引值对应的TabContent内容。 + + + +```ts +private tabsController : TabsController = new TabsController() +@State currentIndex:number = 0; + +@Builder TabBuilder(title: string, targetIndex: number) { + Column() { + Text(title) + .fontColor(this.currentIndex === targetIndex ? '#1698CE' : '#6B6B6B') + } + ... + .onClick(() => { + this.currentIndex = targetIndex; + this.tabsController.changeIndex(this.currentIndex); + }) +} +``` + + +使用自定义导航栏时,在tabBar属性中传入对应的\@Builder,并传入相应的参数。 + + + +```ts +Tabs({ barPosition: BarPosition.End, controller: this.tabsController }) { + TabContent(){ + ... + }.tabBar(this.TabBuilder('首页',0)) + + TabContent(){ + ... + }.tabBar(this.TabBuilder('发现',1)) + + TabContent(){ + ... + }.tabBar(this.TabBuilder('推荐',2)) + + TabContent(){ + ... + } + .tabBar(this.TabBuilder('我的',3)) +} +``` + + +## 滑动切换导航栏 + +在不使用自定义导航栏的情况下,Tabs默认会实现tabBar与TabContent的切换联动。但在使用了自定义导航栏后,使用TabsController可以实现点击页签与页面内容的联动,但不能实现滑动页面时,页面内容对应页签的联动。即用户在使用滑动屏幕切换页面内容时,页签栏需要同步切换至内容对应的页签。 + + + **图11** 滑动切换时页签内容不联动   + +![最终效果11](figures/最终效果11.gif) + + +此时需要使用Tabs提供的onChange事件方法,监听索引index的变化,并将其当前活跃的index值传递给currentIndex,实现页签内容的切换。 + + + + +```ts +Tabs({ barPosition: BarPosition.End, controller: this.tabsController }) { + TabContent() { + ... + }.tabBar(this.TabBuilder('首页', 0)) + + TabContent() { + ... + }.tabBar(this.TabBuilder('发现', 1)) + + TabContent() { + ... + }.tabBar(this.TabBuilder('推荐', 2)) + + TabContent() { + ... + } + .tabBar(this.TabBuilder('我的', 3)) +}.onChange((index) => { + this.currentIndex = index +}) +``` + + + **图12** 内容与页签联动  + +![最终效果](figures/最终效果.gif) + +## 相关实例 + +如需详细了解Tabs的更多实现,请参考以下示例: + +- [健康生活](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/Healthy_life) + +- [常用组件与布局](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/ArkTSComponents) diff --git a/zh-cn/application-dev/ui/arkts-page-transition-animation.md b/zh-cn/application-dev/ui/arkts-page-transition-animation.md new file mode 100644 index 0000000000000000000000000000000000000000..0342e53704d69dde11856ba506d007a784503e1e --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-page-transition-animation.md @@ -0,0 +1,361 @@ +# 页面转场动画 + + +两个页面间发生跳转,一个页面消失,另一个页面出现,这时可以配置各自页面的页面转场参数实现自定义的页面转场效果。[页面转场](../reference/arkui-ts/ts-page-transition-animation.md)效果写在pageTransition函数中,通过PageTransitionEnter和PageTransitionExit指定页面进入和退出的动画效果。 + + +PageTransitionEnter的接口为: + + + +```ts +PageTransitionEnter({type?: RouteType,duration?: number,curve?: Curve | string,delay?: number}) +``` + + +PageTransitionExit的接口为: + + + +```ts +PageTransitionExit({type?: RouteType,duration?: number,curve?: Curve | string,delay?: number}) +``` + + +上述接口定义了PageTransitionEnter和PageTransitionExit组件,可通过slide、translate、scale、opacity属性定义不同的页面转场效果。对于PageTransitionEnter而言,这些效果表示入场时起点值,对于PageTransitionExit而言,这些效果表示退场的终点值,这一点与组件转场transition配置方法类似。此外,PageTransitionEnter提供了onEnter接口进行自定义页面入场动画的回调,PageTransitionExit提供了onExit接口进行自定义页面退场动画的回调。 + + +上述接口中的参数type,表示路由生效的类型,这一点开发者容易混淆其含义。页面转场的两个页面,必定有一个页面退出,一个页面进入。如果通过router.pushUrl操作从页面A跳转到页面B,则页面A退出,做页面退场动画,页面B进入,做页面入场动画。如果通过router.back操作从页面B返回到页面A,则页面B退出,做页面退场动画,页面A进入,做页面入场动画。即页面的PageTransitionEnter既可能是由于新增页面(push,入栈)引起的新页面的入场动画,也可能是由于页面返回(back,或pop,出栈)引起的页面栈中老页面的入场动画,为了能区分这两种形式的入场动画,提供了type参数,这样开发者能完全定义所有类型的页面转场效果。 + + +## type配置为RouteType.None + +type为RouteType.None表示对页面栈的push、pop操作均生效,type的默认值为RouteType.None。 + + +```ts +// page A +pageTransition() { + // 定义页面进入时的效果,从左侧滑入,时长为1200ms,无论页面栈发生push还是pop操作均可生效 + PageTransitionEnter({ type: RouteType.None, duration: 1200 }) + .slide(SlideEffect.Left) + // 定义页面退出时的效果,向左侧滑出,时长为1000ms,无论页面栈发生push还是pop操作均可生效 + PageTransitionExit({ type: RouteType.None, duration: 1000 }) + .slide(SlideEffect.Left) +} +``` + + + +```ts +// page B +pageTransition() { + // 定义页面进入时的效果,从右侧滑入,时长为1000ms,无论页面栈发生push还是pop操作均可生效 + PageTransitionEnter({ type: RouteType.None, duration: 1000 }) + .slide(SlideEffect.Right) + // 定义页面退出时的效果,向右侧滑出,时长为1200ms,无论页面栈发生push还是pop操作均可生效 + PageTransitionExit({ type: RouteType.None, duration: 1200 }) + .slide(SlideEffect.Right) +} +``` + + +假设页面栈为标准实例模式,即页面栈中允许存在重复的页面。可能会有4种场景,对应的页面转场效果如下表。 + + +| 路由操作 | 页面A转场效果 | 页面B转场效果 | +| ---------------------------- | ---------------------------------- | ---------------------------------- | +| router.pushUrl,从页面A跳转到新增的页面B | 页面退出,PageTransitionExit生效,向左侧滑出屏幕 | 页面进入,PageTransitionEnter生效,从右侧滑入屏幕 | +| router.back,从页面B返回到页面A | 页面进入,PageTransitionEnter生效,从左侧滑入屏幕 | 页面退出,PageTransitionExit生效,向右侧滑出屏幕 | +| router.pushUrl,从页面B跳转到新增的页面A | 页面进入,PageTransitionEnter生效,从左侧滑入屏幕 | 页面退出,PageTransitionExit生效,向右侧滑出屏幕 | +| router.back,从页面A返回到页面B | 页面退出,PageTransitionExit生效,向左侧滑出屏幕 | 页面进入,PageTransitionEnter生效,从右侧滑入屏幕 | + + +如果希望pushUrl进入的页面总是从右侧滑入,back时退出的页面总是从右侧滑出,则上表中的第3、4种情况不满足要求,那么需要完整的定义4个页面转场效果。 + + +## type配置为RouteType.Push或RouteType.Pop + +type为RouteType.Push表示仅对页面栈的push操作生效,type为RouteType.Pop表示仅对页面栈的pop操作生效。 + + +```ts +// page A +pageTransition() { + // 定义页面进入时的效果,从右侧滑入,时长为1200ms,页面栈发生push操作时该效果才生效 + PageTransitionEnter({ type: RouteType.Push, duration: 1200 }) + .slide(SlideEffect.Right) + // 定义页面进入时的效果,从左侧滑入,时长为1200ms,页面栈发生pop操作时该效果才生效 + PageTransitionEnter({ type: RouteType.Pop, duration: 1200 }) + .slide(SlideEffect.Left) + // 定义页面退出时的效果,向左侧滑出,时长为1000ms,页面栈发生push操作时该效果才生效 + PageTransitionExit({ type: RouteType.Push, duration: 1000 }) + .slide(SlideEffect.Left) + // 定义页面退出时的效果,向右侧滑出,时长为1000ms,页面栈发生pop操作时该效果才生效 + PageTransitionExit({ type: RouteType.Pop, duration: 1000 }) + .slide(SlideEffect.Right) +} +``` + + + +```ts +// page B +pageTransition() { + // 定义页面进入时的效果,从右侧滑入,时长为1000ms,页面栈发生push操作时该效果才生效 + PageTransitionEnter({ type: RouteType.Push, duration: 1000 }) + .slide(SlideEffect.Right) + // 定义页面进入时的效果,从左侧滑入,时长为1000ms,页面栈发生pop操作时该效果才生效 + PageTransitionEnter({ type: RouteType.Pop, duration: 1000 }) + .slide(SlideEffect.Left) + // 定义页面退出时的效果,向左侧滑出,时长为1200ms,页面栈发生push操作时该效果才生效 + PageTransitionExit({ type: RouteType.Push, duration: 1200 }) + .slide(SlideEffect.Left) + // 定义页面退出时的效果,向右侧滑出,时长为1200ms,页面栈发生pop操作时该效果才生效 + PageTransitionExit({ type: RouteType.Pop, duration: 1200 }) + .slide(SlideEffect.Right) +} +``` + + +以上代码则完整的定义了所有可能的页面转场样式。假设页面栈为标准实例模式,即页面栈中允许存在重复的页面。可能会有4种场景,对应的页面转场效果如下表。 + + +| 路由操作 | 页面A转场效果 | 页面B转场效果 | +| ---------------------------- | ---------------------------------------- | ---------------------------------------- | +| router.pushUrl,从页面A跳转到新增的页面B | 页面退出,PageTransitionExit且type为RouteType.Push的转场样式生效,向左侧滑出屏幕 | 页面进入,PageTransitionEnter且type为RouteType.Push的转场样式生效,从右侧滑入屏幕 | +| router.back,从页面B返回到页面A | 页面进入,PageTransitionEnter且type为RouteType.Pop的转场样式生效,从左侧滑入屏幕 | 页面退出,PageTransitionExit且type为RouteType.Pop的转场样式生效,向右侧滑出屏幕 | +| router.pushUrl,从页面B跳转到新增的页面A | 页面进入,PageTransitionEnter且type为RouteType.Push的转场样式生效,从右侧滑入屏幕 | 页面退出,PageTransitionExit且type为RouteType.Push的转场样式生效,向左侧滑出屏幕 | +| router.back,从页面A返回到页面B | 页面退出,PageTransitionExit且type为RouteType.Pop的转场样式生效,向右侧滑出屏幕 | 页面进入,PageTransitionEnter且type为RouteType.Pop的转场样式生效,从左侧滑入屏幕 | + + +>**说明:** +> +> 1. 由于每个页面的页面转场样式都可由开发者独立配置,而页面转场涉及到两个页面,开发者应考虑两个页面的页面转场效果的衔接,如时长尽量保持一致。 +> +> 2. 如果没有定义匹配的页面转场样式,则该页面使用系统默认的页面转场样式。 + + +## 禁用某页面的页面转场 + + +```ts +pageTransition() { + PageTransitionEnter({ type: RouteType.None, duration: 0 }) + PageTransitionExit({ type: RouteType.None, duration: 0 }) +} +``` + + +通过设置页面转场的时长为0,可使该页面无页面转场动画。 + + +## 场景示例 + +下面介绍定义了所有的四种页面转场样式的页面转场动画示例。 + + + +```ts +// page A +import router from '@ohos.router'; +@Entry +@Component +struct PageTransitionSrc1 { + build() { + Column() { + Image($r('app.media.mountain')) + .width('90%') + .height('80%') + .objectFit(ImageFit.Fill) + .syncLoad(true) // 同步加载图片,使页面出现时图片已经加载完成 + .margin(30) + + Row({ space: 10 }) { + Button("pushUrl") + .onClick(() => { + // 路由到下一个页面,push操作 + router.pushUrl({ url: 'pages/myTest/pageTransitionDst1' }); + }) + Button("back") + .onClick(() => { + // 返回到上一页面,相当于pop操作 + router.back(); + }) + }.justifyContent(FlexAlign.Center) + } + .width("100%").height("100%") + .alignItems(HorizontalAlign.Center) + } + + pageTransition() { + // 定义页面进入时的效果,从右侧滑入,时长为1000ms,页面栈发生push操作时该效果才生效 + PageTransitionEnter({ type: RouteType.Push, duration: 1000 }) + .slide(SlideEffect.Right) + // 定义页面进入时的效果,从左侧滑入,时长为1000ms,页面栈发生pop操作时该效果才生效 + PageTransitionEnter({ type: RouteType.Pop, duration: 1000 }) + .slide(SlideEffect.Left) + // 定义页面退出时的效果,向左侧滑出,时长为1000ms,页面栈发生push操作时该效果才生效 + PageTransitionExit({ type: RouteType.Push, duration: 1000 }) + .slide(SlideEffect.Left) + // 定义页面退出时的效果,向右侧滑出,时长为1000ms,页面栈发生pop操作时该效果才生效 + PageTransitionExit({ type: RouteType.Pop, duration: 1000 }) + .slide(SlideEffect.Right) + } +} +``` + + + + +```ts +// page B +import router from '@ohos.router'; +@Entry +@Component +struct PageTransitionDst1 { + build() { + Column() { + Image($r('app.media.forest')) + .width('90%') + .height('80%') + .objectFit(ImageFit.Fill) + .syncLoad(true) // 同步加载图片,使页面出现时图片已经加载完成 + .margin(30) + + Row({ space: 10 }) { + Button("pushUrl") + .onClick(() => { + // 路由到下一页面,push操作 + router.pushUrl({ url: 'pages/myTest/pageTransitionSrc1' }); + }) + Button("back") + .onClick(() => { + // 返回到上一页面,相当于pop操作 + router.back(); + }) + }.justifyContent(FlexAlign.Center) + } + .width("100%").height("100%") + .alignItems(HorizontalAlign.Center) + } + + pageTransition() { + // 定义页面进入时的效果,从右侧滑入,时长为1000ms,页面栈发生push操作时该效果才生效 + PageTransitionEnter({ type: RouteType.Push, duration: 1000 }) + .slide(SlideEffect.Right) + // 定义页面进入时的效果,从左侧滑入,时长为1000ms,页面栈发生pop操作时该效果才生效 + PageTransitionEnter({ type: RouteType.Pop, duration: 1000 }) + .slide(SlideEffect.Left) + // 定义页面退出时的效果,向左侧滑出,时长为1000ms,页面栈发生push操作时该效果才生效 + PageTransitionExit({ type: RouteType.Push, duration: 1000 }) + .slide(SlideEffect.Left) + // 定义页面退出时的效果,向右侧滑出,时长为1000ms,页面栈发生pop操作时该效果才生效 + PageTransitionExit({ type: RouteType.Pop, duration: 1000 }) + .slide(SlideEffect.Right) + } +} +``` + + + +![pageTransition_PushPop](figures/pageTransition_PushPop.gif) + + +下面介绍使用了type为None的页面转场动画示例。 + + + +```ts +// page A +import router from '@ohos.router'; +@Entry +@Component +struct PageTransitionSrc2 { + build() { + Column() { + Image($r('app.media.mountain')) + .width('90%') + .height('80%') + .objectFit(ImageFit.Fill) + .syncLoad(true) // 同步加载图片,使页面出现时图片已经加载完成 + .margin(30) + + Row({ space: 10 }) { + Button("pushUrl") + .onClick(() => { + // 路由到下一页面,push操作 + router.pushUrl({ url: 'pages/myTest/pageTransitionDst2' }); + }) + Button("back") + .onClick(() => { + // 返回到上一页面,相当于pop操作 + router.back(); + }) + }.justifyContent(FlexAlign.Center) + } + .width("100%").height("100%") + .alignItems(HorizontalAlign.Center) + } + + pageTransition() { + // 定义页面进入时的效果,从左侧滑入,时长为1000ms,无论页面栈发生push还是pop操作均可生效 + PageTransitionEnter({ duration: 1000 }) + .slide(SlideEffect.Left) + // 定义页面退出时的效果,相对于正常页面位置x方向平移100vp,y方向平移100vp,透明度变为0,时长为1200ms,无论页面栈发生push还是pop操作均可生效 + PageTransitionExit({ duration: 1200 }) + .translate({ x: 100.0, y: 100.0 }) + .opacity(0) + } +} +``` + + + +```ts +// page B +import router from '@ohos.router'; +@Entry +@Component +struct PageTransitionDst2 { + build() { + Column() { + Image($r('app.media.forest')) + .width('90%') + .height('80%') + .objectFit(ImageFit.Fill) + .syncLoad(true) // 同步加载图片,使页面出现时图片已经加载完成 + .margin(30) + + Row({ space: 10 }) { + Button("pushUrl") + .onClick(() => { + // 路由到下一页面,push操作 + router.pushUrl({ url: 'pages/myTest/pageTransitionSrc2' }); + }) + Button("back") + .onClick(() => { + // 返回到上一页面,相当于pop操作 + router.back(); + }) + }.justifyContent(FlexAlign.Center) + } + .width("100%").height("100%") + .alignItems(HorizontalAlign.Center) + } + + pageTransition() { + // 定义页面进入时的效果,从左侧滑入,时长为1200ms,无论页面栈发生push还是pop操作均可生效 + PageTransitionEnter({ duration: 1200 }) + .slide(SlideEffect.Left) + // 定义页面退出时的效果,相对于正常页面位置x方向平移100vp,y方向平移100vp,透明度变为0,时长为1000ms,无论页面栈发生push还是pop操作均可生效 + PageTransitionExit({ duration: 1000 }) + .translate({ x: 100.0, y: 100.0 }) + .opacity(0) + } +} +``` + + + +![pageTransition_None](figures/pageTransition_None.gif) diff --git a/zh-cn/application-dev/ui/ui-ts-performance-improvement-recommendation.md b/zh-cn/application-dev/ui/arkts-performance-improvement-recommendation.md similarity index 100% rename from zh-cn/application-dev/ui/ui-ts-performance-improvement-recommendation.md rename to zh-cn/application-dev/ui/arkts-performance-improvement-recommendation.md diff --git a/zh-cn/application-dev/ui/arkts-popup-and-menu-components-menu.md b/zh-cn/application-dev/ui/arkts-popup-and-menu-components-menu.md new file mode 100644 index 0000000000000000000000000000000000000000..1431af7662a57ee4f796a40a459b7f67279965e6 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-popup-and-menu-components-menu.md @@ -0,0 +1,111 @@ +# 菜单 + + +Menu是菜单接口,一般用于鼠标右键弹窗、点击弹窗等。具体用法请参考[Menu控制](../reference/arkui-ts/ts-universal-attributes-menu.md)。 + + +## 创建默认样式的菜单 + +菜单需要调用bindMenu接口来实现。bindMenu响应绑定组件的点击事件,绑定组件后手势点击对应组件后即可弹出。 + + + +```ts +Button('click for Menu') + .bindMenu([ + { + value: 'Menu1', + action: () => { + console.info('handle Menu1 select') + } + } +]) +``` + + +![zh-cn_image_0000001562940565](figures/zh-cn_image_0000001562940565.png) + + +## 创建自定义样式的菜单 + +当默认样式不满足开发需求时,可使用\@CustomBuilder自定义菜单内容。可通过bindContextMenu接口进行菜单的自定义。 + + +### \@Builder开发菜单内的内容 + + +```ts +@State select: boolean = true +private iconStr: ResourceStr = $r("app.media.view_list_filled") +private iconStr2: ResourceStr = $r("app.media.view_list_filled") +@Builder +SubMenu() { + Menu() { + MenuItem({ content: "复制", labelInfo: "Ctrl+C" }) + MenuItem({ content: "粘贴", labelInfo: "Ctrl+V" }) + } +} + +@Builder +MyMenu(){ + Menu() { + MenuItem({ startIcon: $r("app.media.icon"), content: "菜单选项" }) + MenuItem({ startIcon: $r("app.media.icon"), content: "菜单选项" }).enabled(false) + MenuItem({ + startIcon: this.iconStr, + content: "菜单选项", + endIcon: $r("app.media.arrow_right_filled"), + // 当builder参数进行配置时,表示与menuItem项绑定了子菜单。鼠标hover在该菜单项时,会显示子菜单。 + builder: this.SubMenu.bind(this), + }) + MenuItemGroup({ header: '小标题' }) { + MenuItem({ content: "菜单选项" }) + .selectIcon(true) + .selected(this.select) + .onChange((selected) => { + console.info("menuItem select" + selected); + this.iconStr2 = $r("app.media.icon"); + }) + MenuItem({ + startIcon: $r("app.media.view_list_filled"), + content: "菜单选项", + endIcon: $r("app.media.arrow_right_filled"), + builder: this.SubMenu.bind(this)\ + }) + } + MenuItem({ + startIcon: this.iconStr2, + content: "菜单选项", + endIcon: $r("app.media.arrow_right_filled") + }) + } +} + +``` + + +### bindMenu属性绑定组件 + + +```ts +Button('click for Menu') + .bindMenu(this.MyMenu) +``` + + +![zh-cn_image_0000001511580924](figures/zh-cn_image_0000001511580924.png) + + +## 创建支持右键或长按的菜单 + +通过bindContextMenu接口进行菜单的自定义及菜单弹出的触发方式:右键或长按。使用bindContextMenu弹出的菜单项是在独立子窗口内的,可显示在应用窗口外部。 + + +- [@Builder开发菜单内的内容](#builder开发菜单内的内容)与上文写法相同。 + +- 确认菜单的弹出方式,使用bindContextMenu属性绑定组件。示例中为右键弹出菜单。 + + ```ts + Button('click for Menu') + .bindContextMenu(this.MyMenu, ResponseType.RightClick) + ``` diff --git a/zh-cn/application-dev/ui/arkts-popup-and-menu-components-popup.md b/zh-cn/application-dev/ui/arkts-popup-and-menu-components-popup.md new file mode 100644 index 0000000000000000000000000000000000000000..a0b540d4149c90e794f765fc2242c0e556c5f54a --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-popup-and-menu-components-popup.md @@ -0,0 +1,158 @@ +# 气泡提示 + + +Popup属性可绑定在组件上显示气泡弹窗提示,设置弹窗内容、交互逻辑和显示状态。主要用于屏幕录制、信息弹出提醒等显示状态。 + + +气泡分为两种类型,一种是系统提供的气泡[PopupOptions](../reference/arkui-ts/ts-universal-attributes-popup.md#popupoptions%E7%B1%BB%E5%9E%8B%E8%AF%B4%E6%98%8E),一种是开发者可以自定义的气泡[CustomPopupOptions](../reference/arkui-ts/ts-universal-attributes-popup.md#custompopupoptions8%E7%B1%BB%E5%9E%8B%E8%AF%B4%E6%98%8E)。其中PopupOptions为系统提供的气泡,通过配置primaryButton、secondaryButton来设置带按钮的气泡。CustomPopupOptions通过配置[builder](../quick-start/arkts-builder.md)参数来设置自定义的气泡。 + + +## 文本提示气泡 + +文本提示气泡常用于只展示带有文本的信息提示,不带有任何交互的场景。Popup属性需绑定组件,当bindPopup属性中参数show为true的时候会弹出气泡提示。 + +在Button组件上绑定Popup属性,每次点击Button按钮,handlePopup会切换布尔值,当其为true时,触发bindPopup弹出气泡。 + + + +```ts +@Entry +@Component +struct PopupExample { + @State handlePopup: boolean = false + + build() { + Column() { + Button('PopupOptions') + .onClick(() => { + this.handlePopup = !this.handlePopup + }) + .bindPopup(this.handlePopup, { + message: 'This is a popup with PopupOptions', + }) + }.width('100%').padding({ top: 5 }) + } +} +``` + + +![zh-cn_image_0000001511740524](figures/zh-cn_image_0000001511740524.png) + + +## 带按钮的提示气泡 + +通过primaryButton、secondaryButton属性为气泡最多设置两个Button按钮,通过此按钮进行简单的交互;开发者可以通过配置action参数来设置想要触发的操作。 + + + +```ts +@Entry +@Component +struct PopupExample22 { + @State handlePopup: boolean = false + build() { + Column() { + Button('PopupOptions').margin({top:200}) + .onClick(() => { + this.handlePopup = !this.handlePopup + }) + .bindPopup(this.handlePopup, { + message: 'This is a popup with PopupOptions', + primaryButton:{ + value:'Confirm', + action: () => { + this.handlePopup = !this.handlePopup + console.info('confirm Button click') + } + }, + secondaryButton: { + value: 'Cancel', + action: () => { + this.handlePopup = !this.handlePopup + } + }, + }) + }.width('100%').padding({ top: 5 }) + } +} +``` + + +![zh-cn_other_0000001500740342](figures/zh-cn_other_0000001500740342.jpeg) + + +## 自定义气泡 + +开发者可以使用构建器CustomPopupOptions创建自定义气泡,\@Builder中可以放自定义的内容。除此之外,还可以通过popupColor等参数控制气泡样式。 + + + +```ts +@Entry +@Component +struct Index { + @State customPopup: boolean = false + // popup构造器定义弹框内容 + @Builder popupBuilder() { + Row({ space: 2 }) { + Image($r("app.media.icon")).width(24).height(24).margin({ left: 5 }) + Text('This is Custom Popup').fontSize(15) + }.width(200).height(50).padding(5) + } + build() { + Column() { + Button('CustomPopupOptions') + .position({x:100,y:200}) + .onClick(() => { + this.customPopup = !this.customPopup + }) + .bindPopup(this.customPopup, { + builder: this.popupBuilder, // 气泡的内容 + placement:Placement.Bottom, // 气泡的弹出位置 + popupColor:Color.Pink // 气泡的背景色 + }) + } + .height('100%') + } +} +``` + + +使用者通过配置placement参数将弹出的气泡放到需要提示的位置。弹窗构造器会触发弹出提示信息,来引导使用者完成操作,也让使用者有更好的UI体验。 + + +![zh-cn_other_0000001500900234](figures/zh-cn_other_0000001500900234.jpeg) + + + +```ts +@Entry +@Component +struct Index { + @State customPopup: boolean = false + // popup构造器定义弹框内容 + @Builder popupBuilder() { + Row({ space: 2 }) { + Image('/images/shengWhite.png').width(30).objectFit(ImageFit.Contain) + Column(){ + Text('控制人生').fontSize(14).fontWeight(900).fontColor(Color.White).width('100%') + Text('想要跟唱时,数千万歌曲任你选择,人声随心调整。').fontSize(12).fontColor('#ffeeeeee').width('100%') + } + }.width(230).height(80).padding(5) + } + build() { + Row() { + Text('我要K歌') + Image('/images/sheng.png').width(35).objectFit(ImageFit.Contain) + .onClick(() => { + this.customPopup = !this.customPopup + }) + .bindPopup(this.customPopup, { + builder: this.popupBuilder, + }) + } + .margin(20) + .height('100%') + } +} +``` diff --git a/zh-cn/application-dev/ui/arkts-routing.md b/zh-cn/application-dev/ui/arkts-routing.md new file mode 100644 index 0000000000000000000000000000000000000000..ce4ea37d5280c74b0fafb694313122b9c1de79e7 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-routing.md @@ -0,0 +1,326 @@ +# 页面路由 + + +页面路由指在应用程序中实现不同页面之间的跳转和数据传递。OpenHarmony提供了Router模块,通过不同的url地址,可以方便地进行页面路由,轻松地访问不同的页面。本文将从[页面跳转](#页面跳转)、[页面返回](#页面返回)和[页面返回前增加一个询问框](#页面返回前增加一个询问框)几个方面介绍Router模块提供的功能。 + + +## 页面跳转 + +页面跳转是开发过程中的一个重要组成部分。在使用应用程序时,通常需要在不同的页面之间跳转,有时还需要将数据从一个页面传递到另一个页面。 + + **图1** 页面跳转 +![router-jump-to-detail](figures/router-jump-to-detail.gif) + +Router模块提供了两种跳转模式,分别是[router.pushUrl()](../reference/apis/js-apis-router.md#routerpushurl9)和[router.replaceUrl()](../reference/apis/js-apis-router.md#routerreplaceurl9)。这两种模式决定了目标页是否会替换当前页。 + +- router.pushUrl():目标页不会替换当前页,而是压入[页面栈](../application-models/page-mission-stack.md)。这样可以保留当前页的状态,并且可以通过返回键或者调用[router.back()](../reference/apis/js-apis-router.md#routerback)方法返回到当前页。 + +- router.replaceUrl():目标页会替换当前页,并销毁当前页。这样可以释放当前页的资源,并且无法返回到当前页。 + +>**说明:** +> +>页面栈的最大容量为32个页面。如果超过这个限制,可以调用[router.clear()](../reference/apis/js-apis-router.md#routerclear)方法清空历史页面栈,释放内存空间。 + +同时,Router模块提供了两种实例模式,分别是Standard和Single。这两种模式决定了目标url是否会对应多个实例。 + +- Standard:标准实例模式,也是默认情况下的实例模式。每次调用该方法都会新建一个目标页,并压入栈顶。 + +- Single:单实例模式。即如果目标页的url在页面栈中已经存在同url页面,则离栈顶最近的同url页面会被移动到栈顶,并重新加载;如果目标页的url在页面栈中不存在同url页面,则按照标准模式跳转。 + +在使用页面路由Router相关功能之前,需要在代码中先导入Router模块。 + + +```ts +import router from '@ohos.router'; +``` + +- 场景一:有一个主页(Home)和一个详情页(Detail),希望从主页点击一个商品,跳转到详情页。同时,需要保留主页在页面栈中,以便返回时恢复状态。这种场景下,可以使用pushUrl()方法,并且使用Standard实例模式(或者省略)。 + + + ```ts + // 在Home页面中 + function onJumpClick(): void { + router.pushUrl({ + url: 'pages/Detail' // 目标url + }, router.RouterMode.Standard, (err) => { + if (err) { + console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`); + return; + } + console.info('Invoke pushUrl succeeded.'); + }); + } + ``` + + >**说明:** + > + >标准实例模式下,router.RouterMode.Standard参数可以省略。 + +- 场景二:有一个登录页(Login)和一个个人中心页(Profile),希望从登录页成功登录后,跳转到个人中心页。同时,销毁登录页,在返回时直接退出应用。这种场景下,可以使用replaceUrl()方法,并且使用Standard实例模式(或者省略)。 + + + ```ts + // 在Login页面中 + function onJumpClick(): void { + router.replaceUrl({ + url: 'pages/Profile' // 目标url + }, router.RouterMode.Standard, (err) => { + if (err) { + console.error(`Invoke replaceUrl failed, code is ${err.code}, message is ${err.message}`); + return; + } + console.info('Invoke replaceUrl succeeded.'); + }) + } + ``` + + >**说明:** + > + >标准实例模式下,router.RouterMode.Standard参数可以省略。 + +- 场景三:有一个设置页(Setting)和一个主题切换页(Theme),希望从设置页点击主题选项,跳转到主题切换页。同时,需要保证每次只有一个主题切换页存在于页面栈中,在返回时直接回到设置页。这种场景下,可以使用pushUrl()方法,并且使用Single实例模式。 + + + ```ts + // 在Setting页面中 + function onJumpClick(): void { + router.pushUrl({ + url: 'pages/Theme' // 目标url + }, router.RouterMode.Single, (err) => { + if (err) { + console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`); + return; + } + console.info('Invoke pushUrl succeeded.'); + }); + } + ``` + +- 场景四:有一个搜索结果列表页(SearchResult)和一个搜索结果详情页(SearchDetail),希望从搜索结果列表页点击某一项结果,跳转到搜索结果详情页。同时,如果该结果已经被查看过,则不需要再新建一个详情页,而是直接跳转到已经存在的详情页。这种场景下,可以使用replaceUrl()方法,并且使用Single实例模式。 + + + ```ts + // 在SearchResult页面中 + function onJumpClick(): void { + router.replaceUrl({ + url: 'pages/SearchDetail' // 目标url + }, router.RouterMode.Single, (err) => { + if (err) { + console.error(`Invoke replaceUrl failed, code is ${err.code}, message is ${err.message}`); + return; + } + console.info('Invoke replaceUrl succeeded.');}) + } + ``` + +以上是不带参数传递的场景。 + +如果需要在跳转时传递一些数据给目标页,则可以在调用Router模块的方法时,添加一个params属性,并指定一个对象作为参数。例如: + + +```ts +class DataModelInfo { + age: number; +} + +class DataModel { + id: number; + info: DataModelInfo; +} + +function onJumpClick(): void { + // 在Home页面中 + let paramsInfo: DataModel = { + id: 123, + info: { + age: 20 + } + }; + + router.pushUrl({ + url: 'pages/Detail', // 目标url + params: paramsInfo // 添加params属性,传递自定义参数 + }, (err) => { + if (err) { + console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`); + return; + } + console.info('Invoke pushUrl succeeded.'); + }) +} +``` + +在目标页中,可以通过调用Router模块的[getParams()](../reference/apis/js-apis-router.md#routergetparams)方法来获取传递过来的参数。例如: + + +```ts +const params = router.getParams(); // 获取传递过来的参数对象 +const id = params['id']; // 获取id属性的值 +const age = params['info'].age; // 获取age属性的值 +``` + + +## 页面返回 + +当用户在一个页面完成操作后,通常需要返回到上一个页面或者指定页面,这就需要用到页面返回功能。在返回的过程中,可能需要将数据传递给目标页,这就需要用到数据传递功能。 + + **图2** 页面返回   + +![router-back-to-home](figures/router-back-to-home.gif) + +在使用页面路由Router相关功能之前,需要在代码中先导入Router模块。 + + +```ts +import router from '@ohos.router'; +``` + +可以使用以下几种方式进行页面返回: + +- 方式一:返回到上一个页面。 + + + ```ts + router.back(); + ``` + + 这种方式会返回到上一个页面,即上一个页面在页面栈中的位置。但是,上一个页面必须存在于页面栈中才能够返回,否则该方法将无效。 + +- 方式二:返回到指定页面。 + + + ```ts + router.back({ + url: 'pages/Home' + }); + ``` + + 这种方式可以返回到指定页面,需要指定目标页的路径。目标页必须存在于页面栈中才能够返回。 + +- 方式三:返回到指定页面,并传递自定义参数信息。 + + + ```ts + router.back({ + url: 'pages/Home', + params: { + info: '来自Home页' + } + }); + ``` + + 这种方式不仅可以返回到指定页面,还可以在返回的同时传递自定义参数信息。这些参数信息可以在目标页中通过调用router.getParams()方法进行获取和解析。 + +在目标页中,在需要获取参数的位置调用router.getParams()方法即可,例如在onPageShow()生命周期回调中: + + +```ts +onPageShow() { + const params = router.getParams(); // 获取传递过来的参数对象 + const info = params['info']; // 获取info属性的值 +} +``` + +>**说明:** +> +>当使用router.back()方法返回到指定页面时,该页面会被重新压入栈顶,而原栈顶页面(包括)到指定页面(不包括)之间的所有页面栈都将被销毁。 +> +> 另外,如果使用router.back()方法返回到原来的页面,原页面不会被重复创建,因此使用\@State声明的变量不会重复声明,也不会触发页面的aboutToAppear()生命周期回调。如果需要在原页面中使用返回页面传递的自定义参数,可以在需要的位置进行参数解析。例如,在onPageShow()生命周期回调中进行参数解析。 + + +## 页面返回前增加一个询问框 + +在开发应用时,为了避免用户误操作或者丢失数据,有时候需要在用户从一个页面返回到另一个页面之前,弹出一个询问框,让用户确认是否要执行这个操作。 + +本文将从[系统默认询问框](#系统默认询问框)和[自定义询问框](#自定义询问框)两个方面来介绍如何实现页面返回前增加一个询问框的功能。 + + **图3** 页面返回前增加一个询问框   + +![router-add-query-box-before-back](figures/router-add-query-box-before-back.gif) + + +### 系统默认询问框 + +为了实现这个功能,可以使用页面路由Router模块提供的两个方法:[router.showAlertBeforeBackPage()](../reference/apis/js-apis-router.md#routershowalertbeforebackpage9)和[router.back()](../reference/apis/js-apis-router.md#routerback)来实现这个功能。 + +在使用页面路由Router相关功能之前,需要在代码中先导入Router模块。 + + +```ts +import router from '@ohos.router'; +``` + +如果想要在目标界面开启页面返回询问框,需要在调用[router.back()](../reference/apis/js-apis-router.md#routerback)方法之前,通过调用[router.showAlertBeforeBackPage()](../reference/apis/js-apis-router.md#routershowalertbeforebackpage9)方法设置返回询问框的信息。例如,在支付页面中定义一个返回按钮的点击事件处理函数: + + +```ts +// 定义一个返回按钮的点击事件处理函数 +function onBackClick(): void { + // 调用router.showAlertBeforeBackPage()方法,设置返回询问框的信息 + try { + router.showAlertBeforeBackPage({ + message: '您还没有完成支付,确定要返回吗?' // 设置询问框的内容 + }); + } catch (err) { + console.error(`Invoke showAlertBeforeBackPage failed, code is ${err.code}, message is ${err.message}`); + } + + // 调用router.back()方法,返回上一个页面 + router.back(); +} +``` + +其中,router.showAlertBeforeBackPage()方法接收一个对象作为参数,该对象包含以下属性: + +- message:string类型,表示询问框的内容。 + 如果调用成功,则会在目标界面开启页面返回询问框;如果调用失败,则会抛出异常,并通过err.code和err.message获取错误码和错误信息。 + + 当用户点击“返回”按钮时,会弹出确认对话框,询问用户是否确认返回。选择“取消”将停留在当前页目标页;选择“确认”将触发router.back()方法,并根据参数决定如何执行跳转。 + + +### 自定义询问框 + +自定义询问框的方式,可以使用[弹窗](../reference/apis/js-apis-promptAction.md#promptactionshowdialog)或者自定义弹窗实现。这样可以让应用界面与系统默认询问框有所区别,提高应用的用户体验度。本文以弹窗为例,介绍如何实现自定义询问框。 + +在使用页面路由Router相关功能之前,需要在代码中先导入Router模块。 + + +```ts +import router from '@ohos.router'; +``` + +在事件回调中,调用弹窗的[promptAction.showDialog()](../reference/apis/js-apis-promptAction.md#promptactionshowdialog)方法: + + +```ts +function onBackClick() { + // 弹出自定义的询问框 + promptAction.showDialog({ + message: '您还没有完成支付,确定要返回吗?', + buttons: [ + { + text: '取消', + color: '#FF0000' + }, + { + text: '确认', + color: '#0099FF' + } + ] + }).then((result) => { + if (result.index === 0) { + // 用户点击了“取消”按钮 + console.info('User canceled the operation.'); + } else if (result.index === 1) { + // 用户点击了“确认”按钮 + console.info('User confirmed the operation.'); + // 调用router.back()方法,返回上一个页面 + router.back(); + } + }).catch((err) => { + console.error(`Invoke showDialog failed, code is ${err.code}, message is ${err.message}`); + }) +} +``` + +当用户点击“返回”按钮时,会弹出自定义的询问框,询问用户是否确认返回。选择“取消”将停留在当前页目标页;选择“确认”将触发router.back()方法,并根据参数决定如何执行跳转。 diff --git a/zh-cn/application-dev/ui/arkts-spring-animation.md b/zh-cn/application-dev/ui/arkts-spring-animation.md new file mode 100644 index 0000000000000000000000000000000000000000..6ad3d408a7a3ab018a6ac6e5be1ab7672b528a68 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-spring-animation.md @@ -0,0 +1,166 @@ +# 弹簧曲线动画 + + +ArkUI提供了[预置动画曲线](../reference/arkui-ts/ts-appendix-enums.md#curve),指定了动画属性从起始值到终止值的变化规律,如Linear、Ease、EaseIn等。同时,ArkUI也提供了由弹簧振子物理模型产生的弹簧曲线。通过弹簧曲线,开发者可以设置超过设置的终止值,在终止值附近震荡,直至最终停下来的效果。弹簧曲线的动画效果比其他曲线具有更强的互动性、可玩性。 + + +弹簧曲线的接口包括两类,一类是[springCurve](../reference/apis/js-apis-curve.md#curvesspringcurve9),另一类是[springMotion](../reference/apis/js-apis-curve.md#curvesspringmotion9)和[responsiveSpringMotion](../reference/apis/js-apis-curve.md#curvesresponsivespringmotion9),这两种方式都可以产生弹簧曲线。 + + +## 使用springCurve + +springCurve的接口为: + + +```ts +springCurve(velocity: number, mass: number, stiffness: number, damping: number) +``` + +构造参数包括初速度,弹簧系统的质量、刚度、阻尼。构建springCurve时,可指定质量为1,根据springCurve中的参数说明,调节刚度、阻尼两个参数,达到想要的震荡效果。 + + + +```ts +import curves from '@ohos.curves'; +@Entry +@Component +struct SpringTest { + @State translateX: number = 0; + + private jumpWithSpeed(speed: number) { + this.translateX = -1; + animateTo({ duration: 2000, curve: curves.springCurve(speed, 1, 1, 1.2) }, () => { + // 以指定初速度进行x方向的平移的弹簧动画 + this.translateX = 0; + }) + } + + build() { + Column() { + Button("button") + .fontSize(14) + .width(100) + .height(50) + .margin(30) + .translate({ x: this.translateX }) + Row({space:50}) { + Button("jump 50").fontSize(14) + .onClick(() => { + // 以初速度50的弹簧曲线进行平移 + this.jumpWithSpeed(50); + }) + Button("jump 200").fontSize(14) + .onClick(() => { + // 以初速度200的弹簧曲线进行平移 + this.jumpWithSpeed(200); + }) + }.margin(30) + }.height('100%').width('100%') + } +} +``` + + +![springCurve](figures/springCurve.gif) + + +以上示例中,点击不同的按钮,给定springCurve的不同初速度,button会有“弹性”的到达指定位置,且button的振幅随着速度的增大而变大。另外也可以修改springCurve的质量、刚度、阻尼参数,达到想要的弹性的程度。 + + +>**说明:** +> +>速度只是放大了振荡的效果,但系统能否产生振荡的效果,取决于弹簧振子本身的物理参数,即质量、刚度、阻尼三个参数。刚度越小、阻尼越大,springCurve的“弹性”越弱,振荡效果越弱。随着刚度减小或阻尼变大,达到过阻尼状态后,无论速度为多大,都不会有在终点值附近振荡的效果。 + + +## 使用springMotion和responsiveSpringMotion + +[springMotion](../reference/apis/js-apis-curve.md#curvesspringmotion9)的接口为: + + +```ts +springMotion(response?: number, dampingFraction?: number, overlapDuration?: number) +``` + +[responsiveSpringMotion](../reference/apis/js-apis-curve.md#curvesresponsivespringmotion9)的接口为: + + +```ts +responsiveSpringMotion(response?: number, dampingFraction?: number, overlapDuration?: number) +``` + +它们的构造参数包括弹簧自然振动周期、阻尼系数、弹性动画衔接时长这三个可选参数,参数的含义请参考其文档。 + + +使用springMotion和responsiveSpringMotion曲线时,duration不生效,适合于跟手动画。 + + + +```ts +import curves from '@ohos.curves'; + +@Entry +@Component +struct SpringMotionTest { + @State positionX: number = 100; + @State positionY: number = 100; + diameter: number = 50; + + build() { + Column() { + Row() { + Circle({ width: this.diameter, height: this.diameter }) + .fill(Color.Blue) + .position({ x: this.positionX, y: this.positionY }) + .onTouch((event: TouchEvent) => { + if (event.type === TouchType.Move) { + // 跟手过程,使用responsiveSpringMotion曲线 + animateTo({ curve: curves.responsiveSpringMotion() }, () => { + // 减去半径,以使球的中心运动到手指位置 + this.positionX = event.touches[0].screenX - this.diameter / 2; + this.positionY = event.touches[0].screenY - this.diameter / 2; + console.info(`move, animateTo x:${this.positionX}, y:${this.positionY}`); + }) + } else if (event.type === TouchType.Up) { + // 离手时,使用springMotion曲线 + animateTo({ curve: curves.springMotion() }, () => { + this.positionX = 100; + this.positionY = 100; + console.info(`touchUp, animateTo x:100, y:100`); + }) + } + }) + } + .width("100%").height("80%") + .clip(true) // 如果球超出父组件范围,使球不可见 + .backgroundColor(Color.Orange) + + Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Start, justifyContent: FlexAlign.Center }) { + Text("拖动小球").fontSize(16) + } + .width("100%") + + Row() { + Text('点击位置: [x: ' + Math.round(this.positionX) + ', y:' + Math.round(this.positionY) + ']').fontSize(16) + } + .padding(10) + .width("100%") + }.height('100%').width('100%') + } +} +``` + + +以上代码是跟手动画的一个示例。通过在onTouch事件中,捕捉触摸的位置,改变组件的translate或者position属性,使其在跟手过程中运动到触摸位置,松手后回到原位置。跟手动画的效果如下: + + +![springMotion](figures/springMotion.gif) + + +跟手过程推荐使用responsiveSpringMotion曲线,松手过程推荐使用springMotion曲线。跟手过程随着手的位置变化会被多次触发,所以会接连启动多次responsiveSpringMotion动画,松手时启动一次springMotion动画。跟手、松手过程在对同一对象的同一属性上执行动画,且使用了springMotion或responsiveSpringMotion曲线,每次新启动的动画会继承上次动画使用的速度,实现平滑过渡。 + + +>**说明:** +> +> 1. springCurve可以设置初速度,单一属性存在多个动画时不会互相影响,观察到的是多个动画效果的叠加。 +> +> 2. springMotion虽然内部有速度机制,但不可由开发者设置。在单一属性存在多个动画时,后一动画会取代前一动画,并继承前一动画的速度。 diff --git a/zh-cn/application-dev/ui/arkts-transition-animation-within-component.md b/zh-cn/application-dev/ui/arkts-transition-animation-within-component.md new file mode 100644 index 0000000000000000000000000000000000000000..00a8f08c31241b96158750a39f92b581618f0bfb --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-transition-animation-within-component.md @@ -0,0 +1,256 @@ +# 组件内转场动画 + + +组件的插入、删除过程即为组件本身的转场过程,组件的插入、删除动画称为组件内转场动画。通过组件内转场动画,可定义组件出现、消失的效果。 + + +组件内转场动画的接口为: + + + +```ts +transition(value: TransitionOptions) +``` + + +[transition](../reference/arkui-ts/ts-transition-animation-component.md)函数的入参为组件内转场的效果,可以定义平移、透明度、旋转、缩放这几种转场样式的单个或者组合的转场效果,必须和[animateTo](arkts-layout-update-animation.md#使用显式动画产生布局更新动画)一起使用才能产生组件转场效果。 + + +## transition常见用法 + +type用于指定当前的transition动效生效在组件的变化场景,类型为[TransitionType](../reference/arkui-ts/ts-appendix-enums.md#transitiontype)。 + +- 组件的插入、删除使用同一个动画效果 + + ```ts + Button() + .transition({ type: TransitionType.All, scale: { x: 0, y: 0 } }) + ``` + + 当type属性为TransitionType.All时,表示指定转场动效生效在组件的所有变化(插入和删除)场景。此时,删除动画和插入动画是相反的过程,删除动画是插入动画的逆播。例如,以上代码定义了一个Button控件。在插入时,组件从scale的x、y均为0的状态,变化到scale的x、y均为1(即完整显示)的默认状态,以逐渐放大的方式出现。在删除时,组件从scale的x、y均为1的默认状态,变化到指定的scale的x、y均为0的状态,逐渐缩小至尺寸为0。 + + +- 组件的插入、删除使用不同的动画效果 + + ```ts + Button() + .transition({ type: TransitionType.Insert, translate: { x: 200, y: -200 }, opacity: 0 }) + .transition({ type: TransitionType.Delete, rotate: { x: 0, y: 0, z: 1, angle: 360 } }) + ``` + + 当组件的插入和删除需要实现不同的转场动画效果时,可以调用两次transition函数,分别设置type属性为TransitionType.Insert和TransitionType.Delete。例如,以上代码定义了一个Button控件。在插入时,组件从相对于组件正常布局位置x方向平移200vp、y方向平移-200vp的位置、透明度为0的初始状态,变化到x、y方向平移量为0、透明度为1的默认状态,插入动画为平移动画和透明度动画的组合。在删除时,组件从旋转角为0的默认状态,变化到绕z轴旋转360度的终止状态,即绕z轴旋转一周。 + + +- 只定义组件的插入或删除其中一种动画效果。 + + ```ts + Button() + .transition({ type: TransitionType.Delete, translate: { x: 200, y: -200 } }) + ``` + + 当只需要组件的插入或删除的转场动画效果时,仅需设置type属性为TransitionType.Insert或TransitionType.Delete的transition效果。例如,以上代码定义了一个Button控件。删除时,组件从正常位置、没有平移的默认状态,变化到从相对于正常布局位置x方向平移200vp、y方向平移-200vp的位置的状态。插入该组件并不会产生该组件的转场动画。 + + +## if/else产生组件内转场动画 + +if/else语句可以控制组件的插入和删除。如下代码即可通过Button的点击事件,控制if的条件是否满足,来控制if下的Image组件是否显示。 + + + +```ts +@Entry +@Component +struct IfElseTransition { + @State flag: boolean = true; + @State show: string = 'show'; + + build() { + Column() { + Button(this.show).width(80).height(30).margin(30) + .onClick(() => { + if (this.flag) { + this.show = 'hide'; + } else { + this.show = 'show'; + } + // 点击Button控制Image的显示和消失 + this.flag = !this.flag; + }) + if (this.flag) { + Image($r('app.media.mountain')).width(200).height(200) + } + }.height('100%').width('100%') + } +} +``` + + +以上代码没有配置任何动画。接下来,我们将给以上代码加入组件内转场的效果。首先Image组件是由if控制的组件,需要给其加上transition的参数,以指定组件内转场的具体效果。例如,可以如以下代码,给其插入时加上平移效果,删除时加上缩放和透明度效果。 + + + +```ts +if (this.flag) { + Image($r('app.media.mountain')).width(200).height(200) + .transition({ type: TransitionType.Insert, translate: { x: 200, y: -200 } }) + .transition({ type: TransitionType.Delete, opacity: 0, scale: { x: 0, y: 0 } }) +} +``` + + +以上代码虽然指定了动画的样式,但是未指定动画参数,尚不知道需要用多长时间、怎样的曲线完成该动画。transition必须配合animateTo一起使用,并在animateTo的闭包中,控制组件的插入、删除。对于以上示例代码,即为在animateTo闭包中改变flag的值,该部分代码如下所示。指定动画时长为1000ms,曲线使用animateTo函数默认的曲线,改变flag的值。则由flag变化所引起的一切变化,都会按照该动画参数,产生动画。在这里,flag会影响Image的出现和消失。 + + + +```ts +animateTo({ duration: 1000 }, () => { + this.flag = !this.flag; +}) +``` + + +经过以上过程,当animateTo和transition一起使用时,即产生了组件内转场动画。完整示例代码如下: + + + +```ts +@Entry +@Component +struct IfElseTransition { + @State flag: boolean = true; + @State show: string = 'show'; + + build() { + Column() { + Button(this.show).width(80).height(30).margin(30) + .onClick(() => { + if (this.flag) { + this.show = 'hide'; + } else { + this.show = 'show'; + } + + animateTo({ duration: 1000 }, () => { + // 动画闭包内控制Image组件的出现和消失 + this.flag = !this.flag; + }) + }) + if (this.flag) { + // Image的出现和消失配置为不同的过渡效果 + Image($r('app.media.mountain')).width(200).height(200) + .transition({ type: TransitionType.Insert, translate: { x: 200, y: -200 } }) + .transition({ type: TransitionType.Delete, opacity: 0, scale: { x: 0, y: 0 } }) + } + }.height('100%').width('100%') + } +} +``` + + +![ifElseTransition](figures/ifElseTransition.gif) + + +>**说明:** +> +>当配置transition的效果为translate或scale时,本身位置叠加上平移或放大倍数后,动画过程中有可能超过父组件的范围。如果超出父组件的范围时,希望子组件完整的显示,那么可以设置父组件的clip属性为false,使父组件不对子组件产生裁剪。如果超出父组件的范围时,希望超出的子组件部分不显示,那么可以设置父组件的clip属性为true,裁剪掉子组件超出的部分。 + + +## ForEach产生组件内转场动画 + +和if/else类似,ForEach可以通过控制数组中的元素个数,来控制组件的插入和删除。通过ForEach来产生组件内转场动画,仍然需要两个条件: + +- ForEach里的组件配置了transition效果。 + +- 在animateTo的闭包中控制组件的插入或删除,即控制数组的元素添加和删除。 + + +以下代码是使用ForEach产生组件内转场动画的一个示例。 + + + +```ts +@Entry +@Component +struct ForEachTransition { + @State numbers: string[] = ["1", "2", "3", "4", "5"] + startNumber: number = 6; + + build() { + Column({ space: 10 }) { + Column() { + ForEach(this.numbers, (item) => { + // ForEach下的直接组件需配置transition效果 + Text(item) + .width(240) + .height(60) + .fontSize(18) + .borderWidth(1) + .backgroundColor(Color.Orange) + .textAlign(TextAlign.Center) + .transition({ type: TransitionType.All, translate: { x: 200 }, scale: { x: 0, y: 0 } }) + }, item => item) + } + .margin(10) + .justifyContent(FlexAlign.Start) + .alignItems(HorizontalAlign.Center) + .width("90%") + .height("70%") + + Button('向头部添加元素') + .fontSize(16) + .width(160) + .onClick(() => { + animateTo({ duration: 1000 }, () => { + // 往数组头部插入一个元素,导致ForEach在头部增加对应的组件 + this.numbers.unshift(this.startNumber.toString()); + this.startNumber++; + }) + }) + Button('向尾部添加元素') + .width(160) + .fontSize(16) + .onClick(() => { + animateTo({ duration: 1000 }, () => { + // 往数组尾部插入一个元素,导致ForEach在尾部增加对应的组件 + this.numbers.push(this.startNumber.toString()); + this.startNumber++; + }) + }) + Button('删除头部元素') + .width(160) + .fontSize(16) + .onClick(() => { + animateTo({ duration: 1000 }, () => { + // 删除数组的头部元素,导致ForEach删除头部的组件 + this.numbers.shift(); + }) + }) + Button('删除尾部元素') + .width(160) + .fontSize(16) + .onClick(() => { + animateTo({ duration: 1000 }, () => { + // 删除数组的尾部元素,导致ForEach删除头部的组件 + this.numbers.pop(); + }) + }) + } + .width('100%') + .height('100%') + } +} +``` + + +效果如下图: + + +![forEachTransition2](figures/forEachTransition2.gif) + + +由于Column布局方式设为了FlexAlign.Start,即竖直方向从头部开始布局。所以往数组末尾添加元素时,并不会对数组中现存元素对应的组件位置造成影响,只会触发新增组件的插入动画。而往数组头部添加元素时,原来数组中的所有元素的下标都增加了,虽然不会触发其添加或者删除,但是会影响到对应组件的位置。所以除新增的组件会做transition动画以外,之前存在于ForEach中组件也会做位置动画。 + + +>**说明:** +> +>if/else、ForEach为语法节点,配置组件内转场效果的组件应直接作为语法节点的孩子。由语法节点的增删引起的组件增删,只能触发其直接孩子组件的组件内转场动画,开发者不应期望其对更深层次的组件产生组件转场动画。 diff --git a/zh-cn/application-dev/ui/arkts-ui-development-overview.md b/zh-cn/application-dev/ui/arkts-ui-development-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..dd58416c25b2100297bd93e9e021ad1bba430f7b --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-ui-development-overview.md @@ -0,0 +1,88 @@ +# UI开发(ArkTS声明式开发范式)概述 + + +基于ArkTS的声明式开发范式的方舟开发框架是一套开发极简、高性能、支持跨设备的UI开发框架,提供了构建OpenHarmony应用UI所必需的能力,主要包括: + + +- **ArkTS** + ArkTS是UI开发语言,基于TypeScript(简称TS)语言扩展而来,是TS的超集。扩展能力包含各种装饰器、自定义组件、UI描述机制。状态数据管理作为基于ArkTS的声明式开发范式的特色,通过功能不同的装饰器给开发者提供了清晰的页面更新渲染流程和管道。状态管理包括UI组件状态和应用程序状态,两者协作可以使开发者完整地构建整个应用的数据更新和UI渲染。ArkTS语言的基础知识请参考[学习ArkTS语言](../quick-start/arkts-get-started.md)。 + +- **布局** + 布局是UI的必要元素,它定义了组件在界面中的位置。ArkUI框架提供了多种布局方式,除了基础的线性布局、层叠布局、弹性布局、相对布局、栅格布局外,也提供了相对复杂的列表、宫格、轮播。 + +- **组件** + 组件是UI的必要元素,形成了在界面中的样子,由框架直接提供的称为**系统组件**,由开发者定义的称为**自定义组件**。系统内置组件包括按钮、单选框、进度条、文本等。开发者可以通过链式调用的方式设置系统内置组件的渲染效果。开发者可以将系统内置组件组合为自定义组件,通过这种方式将页面组件化为一个个独立的UI单元,实现页面不同单元的独立创建、开发和复用,具有更强的工程性。 + +- **页面路由和组件导航** + 应用可能包含多个页面,可通过页面路由实现页面间的跳转。一个页面内可能存在组件间的导航如典型的分栏,可通过导航组件实现组件间的导航。 + +- **图形** + 方舟开发框架提供了多种类型图片的显示能力和多种自定义绘制的能力,以满足开发者的自定义绘图需求,支持绘制形状、填充颜色、绘制文本、变形与裁剪、嵌入图片等。 + +- **动画** + 动画是UI的重要元素之一。优秀的动画设计能够极大地提升用户体验,框架提供了丰富的动画能力,除了组件内置动画效果外,还包括属性动画、显式动画、自定义转场动画以及动画API等,开发者可以通过封装的物理模型或者调用动画能力API来实现自定义动画轨迹。 + +- **交互事件** + 交互事件是UI和用户交互的必要元素。方舟开发框架提供了多种交互事件,除了触摸事件、鼠标事件、键盘按键事件、焦点事件等通用事件外,还包括基于通用事件进行进一步识别的手势事件。手势事件有单一手势如点击手势、长按手势、拖动手势、捏合手势、旋转手势、滑动手势,以及通过单一手势事件进行组合的组合手势事件。 + + +## 特点 + +- 开发效率高,开发体验好 + - 代码简洁:通过接近自然语义的方式描述UI,不必关心框架如何实现UI绘制和渲染。 + - 数据驱动UI变化:让开发者更专注自身业务逻辑的处理。当UI发生变化时,开发者无需编写在不同的UI之间进行切换的UI代码, 开发人员仅需要编写引起界面变化的数据,具体UI如何变化交给框架。 + - 开发体验好:界面也是代码,让开发者的编程体验得到提升。 + +- 性能优越 + - 声明式UI前端和UI后端分层:UI后端采用C++语言构建,提供对应前端的基础组件、布局、动效、交互事件、组件状态管理和渲染管线。 + - 语言编译器和运行时的优化:统一字节码、高效FFI-Foreign Function Interface、AOT-Ahead Of Time、引擎极小化、类型优化等。 + +- 生态容易快速推进 + 能够借力主流语言生态快速推进,语言相对中立友好,有相应的标准组织可以逐步演进。 + + +## 整体架构 + + **图1** 整体架构图   + +![arkui-arkts-framework](figures/arkui-arkts-framework.png) + + + + +- **声明式UI前端** + 提供了UI开发范式的基础语言规范,并提供内置的UI组件、布局和动画,提供了多种状态管理机制,为应用开发者提供一系列接口支持。 + +- **语言运行时** + 选用方舟语言运行时,提供了针对UI范式语法的解析能力、跨语言调用支持的能力和TS语言高性能运行环境。 + +- **声明式UI后端引擎** + 后端引擎提供了兼容不同开发范式的UI渲染管线,提供多种基础组件、布局计算、动效、交互事件,提供了状态管理和绘制能力。 + +- **渲染引擎** + 提供了高效的绘制能力,将渲染管线收集的渲染指令,绘制到屏幕的能力。 + +- **平台适配层** + 提供了对系统平台的抽象接口,具备接入不同系统的能力,如系统渲染管线、生命周期调度等。 + + +## 开发流程 + +使用UI开发框架开发应用时,主要涉及如下开发过程。开发者可以先通过[第一个入门](../quick-start/start-with-ets-stage.md)实例了解整个应用的UI开发过程。 + + +| 任务 | 简介 | 相关指导 | +| ----------- | ----------------------------------- | ---------------------------------------- | +| 学习ArkTS | 介绍了ArkTS的基本语法、状态管理和渲染控制的场景。 | - [基本语法](../quick-start/arkts-basic-syntax-overview.md)
- [状态管理](../quick-start/arkts-state-management-overview.md)
- [渲染控制](../quick-start/arkts-rendering-control-overview.md) | +| 开发布局 | 介绍了几种常用的布局方式以及如何提升布局性能。 | - [常用布局](arkts-layout-development-overview.md)
- [布局性能](arkts-layout-development-performance-boost.md) | +| 添加组件 | 介绍了几种常用的内置组件、自定义组件以及通过API方式支持的界面元素。 | - [常用组件](arkts-common-components-button.md)
- [自定义组件](../quick-start/arkts-create-custom-components.md)
- [气泡和菜单](arkts-popup-and-menu-components-popup.md) | +| 设置页面路由和组件导航 | 介绍了如何设置页面路由以及组件间的导航。 | - [页面路由](arkts-routing.md)
- [组件导航](arkts-navigation-navigation.md) | +| 显示图形 | 介绍了如何显示图片、绘制自定义几何图形以及使用画布绘制自定义图形。 | - [图片](arkts-graphics-display.md)
- [几何图形](arkts-geometric-shape-drawing.md)
- [画布](arkts-drawing-customization-on-canvas.md) | +| 使用动画 | 介绍了组件和页面使用动画的典型场景。 | - [页面内的动画](arkts-layout-update-animation.md)
- [页面间的动画](arkts-zoom-animation.md) | +| 绑定事件 | 介绍了事件的基本概念和如何使用通用事件和手势事件。 | - [通用事件](arkts-common-events-touch-screen-event.md)
- [手势事件](arkts-gesture-events-binding.md) | + +## 相关实例 + +基于ArkTS的声明式开发范式,可参考以下实例: + +[ArkTS组件集](https://gitee.com/openharmony/applications_app_samples/tree/master/code/UI/ArkTsComponentClollection/ComponentCollection):组件、通用方法、动画、全局方法的集合。 diff --git a/zh-cn/application-dev/ui/arkts-zoom-animation.md b/zh-cn/application-dev/ui/arkts-zoom-animation.md new file mode 100644 index 0000000000000000000000000000000000000000..59decbcb3f8d566eca5715a758a3045164f60721 --- /dev/null +++ b/zh-cn/application-dev/ui/arkts-zoom-animation.md @@ -0,0 +1,98 @@ +# 放大缩小视图 + + +在不同页面间,有使用相同的元素(例如同一幅图)的场景,可以使用[共享元素转场](../reference/arkui-ts/ts-transition-animation-shared-elements.md)动画衔接。为了突出不同页面间相同元素的关联性,可为它们添加共享元素转场动画。如果相同元素在不同页面间的大小有明显差异,即可达到放大缩小视图的效果。 + + +共享元素转场的接口为: + + + +```ts +sharedTransition(id: string, options?: sharedTransitionOptions) +``` + + +其中根据sharedTransitionOptions中的type参数,共享元素转场分为Exchange类型的共享元素转场和Static类型的共享元素转场。 + + +## Exchange类型的共享元素转场 + +交换型的共享元素转场,需要两个页面中,存在通过sharedTransition函数配置为相同id的组件,它们称为共享元素。这种类型的共享元素转场适用于两个页面间相同元素的衔接,会从起始页共享元素的位置、大小过渡到目标页的共享元素的位置、大小。如果不指定type,默认为Exchange类型的共享元素转场,这也是最常见的共享元素转场的方式。使用Exchange类型的共享元素转场时,共享元素转场的动画参数由目标页options中的动画参数决定。 + + +## Static类型的共享元素转场 + +静态型的共享元素转场通常用于页面跳转时,标题逐渐出现或隐藏的场景,只需要在一个页面中有Static的共享元素,不能在两个页面中出现相同id的Static类型的共享元素。在跳转到该页面(即目标页)时,配置Static类型sharedTransition的组件做透明度从0到该组件设定的透明度的动画,位置保持不变。在该页面(即起始页)消失时,做透明度逐渐变为0的动画,位置保持不变。 + +共享元素转场的动画参数由该组件sharedTransition属性中的动画参数决定。 + + +## 场景示例 + +下面介绍使用共享元素转场进行放大缩小图片的示例。 + + +```ts +// src page +import router from '@ohos.router'; + +@Entry +@Component +struct SharedTransitionSrc { + build() { + Column() { + // 配置Exchange类型的共享元素转场,共享元素id为"sharedImage1" + Image($r('app.media.mountain')).width(50).height(50) + .sharedTransition('sharedImage1', { duration: 1000, curve: Curve.Linear }) + .onClick(() => { + // 点击小图时路由跳转至下一页面 + router.pushUrl({ url: 'pages/myTest/sharedTransitionDst' }); + }) + } + .padding(10) + .width("100%") + .alignItems(HorizontalAlign.Start) + } +} +``` + + + + +```ts +// dest page +import router from '@ohos.router'; +@Entry +@Component +struct SharedTransitionDest { + build() { + Column() { + // 配置Static类型的共享元素转场 + Text("SharedTransition dest page") + .fontSize(16) + .sharedTransition('text', { duration: 500, curve: Curve.Linear, type: SharedTransitionEffectType.Static }) + .margin({ top: 10 }) + + // 配置Exchange类型的共享元素转场,共享元素id为"sharedImage1" + Image($r('app.media.mountain')) + .width(150) + .height(150) + .sharedTransition('sharedImage1', { duration: 500, curve: Curve.Linear }) + .onClick(() => { + // 点击图片时路由返回至上一页面 + router.back(); + }) + } + .width("100%") + .alignItems(HorizontalAlign.Center) + } +} +``` + + +上述示例中,第一个页面(src page)和第二个页面(dest page)都配置了id为"sharedImage1"的共享元素转场,使两个页面能匹配到这一组共享元素。从第一个页面跳转到第二个页面时,第一个页面为起始页,第二个页面为目标页。配置id为"sharedImage1"的组件按照目标页中500ms的时长进行共享元素转场,达到放大视图的效果,id为"text"的组件按照配置的Static类型sharedTransition参数中的500ms的时长进行共享元素转场,标题逐渐出现。从第二个页面返回到第一个页面时,第二个页面为起始页,第一个页面为目标页。配置id为"sharedImage1"的组件按照目标页中1000ms的时长进行共享元素转场,缩小为原始视图,id为"text"的组件按照配置的Static类型sharedTransition参数中的500ms的时长进行共享元素转场,标题逐渐隐藏。 + + + +![sharedTransition](figures/sharedTransition.gif) diff --git a/zh-cn/application-dev/ui/arkui-overview.md b/zh-cn/application-dev/ui/arkui-overview.md index f788b416e46528ccebe5a5c8f84693ee74cbb619..086e10fd36686346794581b1f3d0934f01550785 100644 --- a/zh-cn/application-dev/ui/arkui-overview.md +++ b/zh-cn/application-dev/ui/arkui-overview.md @@ -1,59 +1,46 @@ # 方舟开发框架概述 -方舟开发框架(简称:ArkUI),是一套构建OpenHarmony应用界面的UI开发框架,它提供了极简的UI语法与包括UI组件、动画机制、事件交互等在内的UI开发基础设施,以满足应用开发者的可视化界面开发需求。 -## 基本概念 - -- **组件:** 组件是界面搭建与显示的最小单位。开发者通过多种组件的组合,构建出满足自身应用诉求的完整界面。 +方舟开发框架(简称ArkUI)为OpenHarmony应用的UI开发提供了完整的基础设施,包括简洁的UI语法、丰富的UI功能(组件、布局、动画以及交互事件),以及实时界面预览工具等,可以支持开发者进行可视化界面开发。 -- **页面:** page页面是方舟开发框架最小的调度分割单位。开发者可以将应用设计为多个功能页面,每个页面进行单独的文件管理,并通过[页面路由](../reference/apis/js-apis-router.md)API完成页面间的调度管理,以实现应用内功能的解耦。 -## 主要特征 +## 基本概念 -- **UI组件:** 方舟开发框架内置了丰富的多态组件,包括文本、图片、按钮等基础组件,可包含一个或多个子组件的容器组件,满足开发者自定义绘图需求的绘制组件,以及提供视频播放能力的媒体组件等。其中“多态”是指组件针对不同类型设备进行了设计,提供了在不同平台上的样式适配能力。 +- **UI:** 即用户界面。开发者可以将应用的用户界面设计为多个功能页面,每个页面进行单独的文件管理,并通过[页面路由](arkts-routing.md)API完成页面间的调度管理如跳转、回退等操作,以实现应用内的功能解耦。 -- **布局:** UI界面设计离不开布局的参与。方舟开发框架提供了多种布局方式,除了基础的线性布局、弹性布局外,也提供了相对复杂的列表、宫格、栅格布局,以及自适应多分辨率场景开发的原子布局能力。 +- **组件:** UI构建与显示的最小单位,如列表、网格、按钮、单选框、进度条、文本等。开发者通过多种组件的组合,构建出满足自身应用诉求的完整界面。 -- **动画:** 动画是UI界面的重要元素之一,优秀的动画设计能够极大地提升用户体验,方舟开发框架提供了丰富的动画能力,除了组件内置动画效果外,还包括属性动画、自定义转场动画以及动画API等。 -- **绘制:** 方舟开发框架提供了多种绘制能力,以满足开发者的自定义绘图需求,支持绘制形状、颜色填充、绘制文本、变形与裁剪、嵌入图片等。 +## 两种开发范式 -- **交互事件:** 方舟开发框架提供了多种交互能力,以满足应用在不同平台通过不同输入设备进行UI交互响应的需求,默认适配了触摸手势、遥控器按键输入、键鼠输入,同时提供了相应的事件回调以便开发者添加交互逻辑。 +针对不用的应用场景及技术背景,方舟开发框架提供了两种开发范式,分别是[基于ArkTS的声明式开发范式](arkts-ui-development-overview.md)(简称“声明式开发范式”)和[兼容JS的类Web开发范式](../ui/ui-js-overview.md)(简称“类Web开发范式”)。 -- **平台API通道:** 方舟开发框架提供了API扩展机制,可通过该机制对平台能力进行封装,提供风格统一的JS接口。 +- **声明式开发范式**:采用基于TypeScript声明式UI语法扩展而来的[ArkTS语言](../quick-start/arkts-get-started.md),从组件、动画和状态管理三个维度提供UI绘制能力。 -- **两种开发范式:** 方舟开发框架针对不同的应用场景以及不同技术背景的开发者提供了两种开发范式,分别是[基于ArkTS的声明式开发范式](./ui-ts-overview.md)(简称“声明式开发范式”)和[兼容JS的类Web开发范式](./ui-js-overview.md)(简称“类Web开发范式”)。 +- **类Web开发范式**:采用经典的HML、CSS、JavaScript三段式开发方式,即使用HML标签文件搭建布局、使用CSS文件描述样式、使用JavaScript文件处理逻辑。该范式更符合于Web前端开发者的使用习惯,便于快速将已有的Web应用改造成方舟开发框架应用。 - | 开发范式名称 | 简介 | 适用场景 | 适用人群 | - | -------- | ---------------------------------------- | ---------------- | ------------------- | - | 声明式开发范式 | 采用基于TypeScript进行声明式UI语法扩展而来的[ArkTS语言](../quick-start/arkts-get-started.md),从组件、动画和状态管理三个维度提供了UI绘制能力。声明式开发范式更接近自然语义的编程方式,让开发者直观地描述UI界面,不必关心框架如何实现UI绘制和渲染,实现极简高效开发。 | 复杂度较大、团队合作度较高的应用 | 移动系统应用开发人员、系统应用开发人员 | - | 类Web开发范式 | 采用经典的HML、CSS、JavaScript三段式开发方式,使用HML标签文件进行布局搭建,使用CSS文件进行样式描述,使用JavaScript文件进行逻辑处理。UI组件与数据之间通过单向数据绑定的方式建立关联,当数据发生变化时,UI界面自动触发刷新。该开发方式更接近Web前端开发者的使用习惯,便于快速将已有的Web应用改造成方舟开发框架应用。 | 界面较简单的中小型应用和卡片 | Web前端开发人员 | +在开发一款新应用时,推荐采用声明式开发范式来构建UI,主要基于以下几点考虑: -## 框架结构 +- **开发效率:** 声明式开发范式更接近自然语义的编程方式,开发者可以直观地描述UI,无需关心如何实现UI绘制和渲染,开发高效简洁。 -![zh-cn_image_0000001183709904](figures/zh-cn_image_0000001183709904.png) +- **应用性能:** 如下图所示,两种开发范式的UI后端引擎和语言运行时是共用的,但是相比类Web开发范式,声明式开发范式无需JS框架进行页面DOM管理,渲染更新链路更为精简,占用内存更少,应用性能更佳。 -从上图可以看出,类Web开发范式与声明式开发范式的UI后端引擎和语言运行时是共用的,其中,UI后端引擎实现了方舟开发框架的六种基本能力。声明式开发范式无需JS Framework进行页面DOM管理,渲染更新链路更为精简,占用内存更少,因此更推荐开发者选用声明式开发范式来搭建应用UI界面。 +- **发展趋势**:声明式开发范式后续会作为主推的开发范式持续演进,为开发者提供更丰富、更强大的能力。 -## UI与Ability框架的关系 + **图1** 方舟开发框架示意图   -OpenHarmony提供了FA模型与Stage模型两种[应用模型](../application-models/application-model-description.md)。下表给出了两种模型分别与方舟开发框架的两种开发范式的关系。 + ![arkui-framework](figures/arkui-framework.png) - **FA模型:** -| 类型 | UI开发范式 | 说明 | -| ---- | -------- | ---------------------------------------- | -| 应用 | 类web开发范式 | UI开发语言:使用hml/css/js
业务入口:使用固定文件名app.ets(Page类型Ability)/service.ts(Service类型Ability)/data.ts(Data类型Ability)
业务逻辑语言:js/ts | -| | 声明式开发范式 | UI开发语言:ArkTS
业务入口:使用固定文件名app.ets(Page类型Ability)/service.ts(Service类型Ability)/data.ts(Data类型Ability)
业务逻辑语言:js/ts | -| 服务卡片 | 类web开发范式 | UI开发语言:卡片显示使用hml+css+json(action)
业务入口:form.ts
卡片业务逻辑语言:js/ts | -| | 声明式开发范式 | 当前不支持 | +## 不同应用类型支持的开发范式 - **Stage模型:** +根据所选用OpenHarmony[应用模型](../application-models/application-model-composition.md)(Stage模型、FA模型)和页面形态(应用或服务的普通页面、卡片)的不同,对应支持的UI开发范式也有所差异,详见下表。 -| 类型 | UI开发范式 | 说明 | -| -------- | -------------- | ------------------------------------------------------------ | -| 应用 | 类web开发范式 | 当前不支持 | -| | 声明式开发范式 | UI开发语言:ArkTS
业务入口:应用模型基于ohos.app.ability.UIAbility/ExtensionAbility等派生
业务逻辑语言:ts | -| 服务卡片 | 类web开发范式 | UI开发语言:卡片显示使用hml+css+json(action)
业务入口:从FormExtensionAbility派生
业务逻辑语言:ts | -| | 声明式开发范式 | 当前不支持 | + **表1** 支持的UI开发范式 +| 应用模型 | 页面形态 | 支持的UI开发范式 | +| ----------- | -------- | ------------------------ | +| Stage模型(推荐) | 应用或服务的页面 | 声明式开发范式(推荐) | +| | 卡片 | 声明式开发范式(推荐)
类Web开发范式 | +| FA模型 | 应用或服务的页面 | 声明式开发范式
类Web开发范式 | +| | 卡片 | 类Web开发范式 | diff --git a/zh-cn/application-dev/ui/figures/2023022700701(1).jpg b/zh-cn/application-dev/ui/figures/2023022700701(1).jpg new file mode 100644 index 0000000000000000000000000000000000000000..53a5dba3a495af6562da8e2bee506c88b0c7d7ba Binary files /dev/null and b/zh-cn/application-dev/ui/figures/2023022700701(1).jpg differ diff --git a/zh-cn/application-dev/ui/figures/2023022701120(1).jpg b/zh-cn/application-dev/ui/figures/2023022701120(1).jpg new file mode 100644 index 0000000000000000000000000000000000000000..950c2ea49f2407f07277f319ec8099cf1e2ce5dd Binary files /dev/null and b/zh-cn/application-dev/ui/figures/2023022701120(1).jpg differ diff --git a/zh-cn/application-dev/ui/figures/2023022792216(1).jpg b/zh-cn/application-dev/ui/figures/2023022792216(1).jpg new file mode 100644 index 0000000000000000000000000000000000000000..41d92c28e660fffdfa518982ef633416941fbb9d Binary files /dev/null and b/zh-cn/application-dev/ui/figures/2023022792216(1).jpg differ diff --git a/zh-cn/application-dev/ui/figures/2023022793003(1).jpg b/zh-cn/application-dev/ui/figures/2023022793003(1).jpg new file mode 100644 index 0000000000000000000000000000000000000000..5f89a57ecd79bf7f46cc19eb49acf9dbbaaa1edd Binary files /dev/null and b/zh-cn/application-dev/ui/figures/2023022793003(1).jpg differ diff --git a/zh-cn/application-dev/ui/figures/2023022793350(1).jpg b/zh-cn/application-dev/ui/figures/2023022793350(1).jpg new file mode 100644 index 0000000000000000000000000000000000000000..74205f7d264efc72ccac31d2aeb9af7da0255d55 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/2023022793350(1).jpg differ diff --git a/zh-cn/application-dev/ui/figures/2023022793719(1).jpg b/zh-cn/application-dev/ui/figures/2023022793719(1).jpg new file mode 100644 index 0000000000000000000000000000000000000000..328ea27be52901f0aa1e2a77764840c7e14f3e80 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/2023022793719(1).jpg differ diff --git a/zh-cn/application-dev/ui/figures/2023022794031(1).jpg b/zh-cn/application-dev/ui/figures/2023022794031(1).jpg new file mode 100644 index 0000000000000000000000000000000000000000..67104a5b1dfc638e56e7ce322d68e555b009c8a7 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/2023022794031(1).jpg differ diff --git a/zh-cn/application-dev/ui/figures/2023022794521(1).jpg b/zh-cn/application-dev/ui/figures/2023022794521(1).jpg new file mode 100644 index 0000000000000000000000000000000000000000..fef7086bd78fdccf2833994a0824613d8519466a Binary files /dev/null and b/zh-cn/application-dev/ui/figures/2023022794521(1).jpg differ diff --git a/zh-cn/application-dev/ui/figures/2023022795105(1).jpg b/zh-cn/application-dev/ui/figures/2023022795105(1).jpg new file mode 100644 index 0000000000000000000000000000000000000000..e729f45969660af14b82ed8c8e5f3c0b1af0e00c Binary files /dev/null and b/zh-cn/application-dev/ui/figures/2023022795105(1).jpg differ diff --git a/zh-cn/application-dev/ui/figures/2023032401632.jpg b/zh-cn/application-dev/ui/figures/2023032401632.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6fff6151134082a6c575aa728c1ff9dee6d2c2e9 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/2023032401632.jpg differ diff --git a/zh-cn/application-dev/ui/figures/2023032405917.jpg b/zh-cn/application-dev/ui/figures/2023032405917.jpg new file mode 100644 index 0000000000000000000000000000000000000000..aafc6a07236e00055dcd7eb7e1264c930c28ecff Binary files /dev/null and b/zh-cn/application-dev/ui/figures/2023032405917.jpg differ diff --git a/zh-cn/application-dev/ui/figures/2023032411518.jpg b/zh-cn/application-dev/ui/figures/2023032411518.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f9dba940013d9662c93ea5b1174aab8c49d590b6 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/2023032411518.jpg differ diff --git a/zh-cn/application-dev/ui/figures/2023032422159.jpg b/zh-cn/application-dev/ui/figures/2023032422159.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f9fb080dee7e023179932a79b1d5421f0d91824e Binary files /dev/null and b/zh-cn/application-dev/ui/figures/2023032422159.jpg differ diff --git a/zh-cn/application-dev/ui/figures/GIF.gif b/zh-cn/application-dev/ui/figures/GIF.gif new file mode 100644 index 0000000000000000000000000000000000000000..d350b220ebc2646c5aafdc845cce92ba1142853b Binary files /dev/null and b/zh-cn/application-dev/ui/figures/GIF.gif differ diff --git a/zh-cn/application-dev/ui/figures/alignment-relative-anchor-horizontal.png b/zh-cn/application-dev/ui/figures/alignment-relative-anchor-horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..8e5b4a9d14c2fc9ad97f8c068587761db693fecc Binary files /dev/null and b/zh-cn/application-dev/ui/figures/alignment-relative-anchor-horizontal.png differ diff --git a/zh-cn/application-dev/ui/figures/alignment-relative-anchor-vertical.png b/zh-cn/application-dev/ui/figures/alignment-relative-anchor-vertical.png new file mode 100644 index 0000000000000000000000000000000000000000..cc03160dda45d12be4cd52593390ab0905d552df Binary files /dev/null and b/zh-cn/application-dev/ui/figures/alignment-relative-anchor-vertical.png differ diff --git a/zh-cn/application-dev/ui/figures/alignself.png b/zh-cn/application-dev/ui/figures/alignself.png deleted file mode 100644 index 6ab6ba66e53bf7dc3edd72d44727fe40c7ff0fb9..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/alignself.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/arkui-arkts-framework.png b/zh-cn/application-dev/ui/figures/arkui-arkts-framework.png new file mode 100644 index 0000000000000000000000000000000000000000..f687e6060ed772638c29637dd23dc808d0e3ce61 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/arkui-arkts-framework.png differ diff --git a/zh-cn/application-dev/ui/figures/arkui-framework.png b/zh-cn/application-dev/ui/figures/arkui-framework.png new file mode 100644 index 0000000000000000000000000000000000000000..8dd724a13408cf0f2f8086f961c8a900cf4a23df Binary files /dev/null and b/zh-cn/application-dev/ui/figures/arkui-framework.png differ diff --git a/zh-cn/application-dev/ui/figures/arrangement-child-elements-column.png b/zh-cn/application-dev/ui/figures/arrangement-child-elements-column.png new file mode 100644 index 0000000000000000000000000000000000000000..049559f1910fd074ceba79d17c50aea1b71d7916 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/arrangement-child-elements-column.png differ diff --git a/zh-cn/application-dev/ui/figures/arrangement-child-elements-row.png b/zh-cn/application-dev/ui/figures/arrangement-child-elements-row.png new file mode 100644 index 0000000000000000000000000000000000000000..d9c027339002e35b3a880fd45fc8e6cf5ea9a3aa Binary files /dev/null and b/zh-cn/application-dev/ui/figures/arrangement-child-elements-row.png differ diff --git a/zh-cn/application-dev/ui/figures/arrangement-direction-column-sample.png b/zh-cn/application-dev/ui/figures/arrangement-direction-column-sample.png new file mode 100644 index 0000000000000000000000000000000000000000..88ed20fb601f1106e5015d94edd2ea8a9aa01826 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/arrangement-direction-column-sample.png differ diff --git a/zh-cn/application-dev/ui/figures/arrangement-direction-column.png b/zh-cn/application-dev/ui/figures/arrangement-direction-column.png new file mode 100644 index 0000000000000000000000000000000000000000..2d0d772e2f36c4e822d56565bfbbfa7e840b042b Binary files /dev/null and b/zh-cn/application-dev/ui/figures/arrangement-direction-column.png differ diff --git a/zh-cn/application-dev/ui/figures/arrangement-direction-row.png b/zh-cn/application-dev/ui/figures/arrangement-direction-row.png new file mode 100644 index 0000000000000000000000000000000000000000..d2cd9037542c7700c127a8a4f4631e1e51a080a2 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/arrangement-direction-row.png differ diff --git a/zh-cn/application-dev/ui/figures/autoPlay.gif b/zh-cn/application-dev/ui/figures/autoPlay.gif new file mode 100644 index 0000000000000000000000000000000000000000..bc75b78ba2bd3bf34f76a351779af874598c33d3 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/autoPlay.gif differ diff --git a/zh-cn/application-dev/ui/figures/columnGap.png b/zh-cn/application-dev/ui/figures/columnGap.png deleted file mode 100644 index 1b7017a280158e88baabca3dfd1e77c6068c4655..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/columnGap.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/columnTemplate.png b/zh-cn/application-dev/ui/figures/columnTemplate.png deleted file mode 100644 index 730abeccaffa22f88e916fb199cbc1e44a982789..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/columnTemplate.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/columnalign.png b/zh-cn/application-dev/ui/figures/columnalign.png deleted file mode 100644 index a73a28a23c3719aee5379e0c8b3f9cc3627ca3e2..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/columnalign.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/columnjustify.png b/zh-cn/application-dev/ui/figures/columnjustify.png deleted file mode 100644 index 34e68343a8b621f4a0567ea9424169f6a7e5ef1b..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/columnjustify.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/columnspace.png b/zh-cn/application-dev/ui/figures/columnspace.png deleted file mode 100644 index 6af047e34c3e1e2ecd8d3d1d99af17ca01c5b58b..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/columnspace.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/common-page-structure.png b/zh-cn/application-dev/ui/figures/common-page-structure.png new file mode 100644 index 0000000000000000000000000000000000000000..789ff18aa63910adf675b09786cf3909195f04d7 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/common-page-structure.png differ diff --git a/zh-cn/application-dev/ui/figures/controll.gif b/zh-cn/application-dev/ui/figures/controll.gif new file mode 100644 index 0000000000000000000000000000000000000000..583ea20ede42d5201c523d5b0f3dba2af879de85 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/controll.gif differ diff --git a/zh-cn/application-dev/ui/figures/create.png b/zh-cn/application-dev/ui/figures/create.png new file mode 100644 index 0000000000000000000000000000000000000000..25aee7530420ef2a5463e92fd79891472e6829af Binary files /dev/null and b/zh-cn/application-dev/ui/figures/create.png differ diff --git a/zh-cn/application-dev/ui/figures/custom-navigation-bar.png b/zh-cn/application-dev/ui/figures/custom-navigation-bar.png new file mode 100644 index 0000000000000000000000000000000000000000..67a15c0d9b94b1b3e5824a71738113504817bcb1 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/custom-navigation-bar.png differ diff --git a/zh-cn/application-dev/ui/figures/customLayout.png b/zh-cn/application-dev/ui/figures/customLayout.png deleted file mode 100644 index 1231a0e0e693e1648c06b25898596bdc8546a3e0..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/customLayout.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/direction.png b/zh-cn/application-dev/ui/figures/direction.png deleted file mode 100644 index 702c9b07fa00b8685415bb83879ead774001ee56..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/direction.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/drawimage.PNG b/zh-cn/application-dev/ui/figures/drawimage.PNG new file mode 100644 index 0000000000000000000000000000000000000000..f5061126ce9c48cea3cffcef05ecf5ea994735bd Binary files /dev/null and b/zh-cn/application-dev/ui/figures/drawimage.PNG differ diff --git a/zh-cn/application-dev/ui/figures/drawing-feature.png b/zh-cn/application-dev/ui/figures/drawing-feature.png deleted file mode 100644 index c1468667bdf74b80629c1eaa32cbef935ea5048c..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/drawing-feature.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/drawing-feature1.png b/zh-cn/application-dev/ui/figures/drawing-feature1.png deleted file mode 100644 index 6b3ca4fa90e25b5906bdadcdcbf134bd00b7b34b..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/drawing-feature1.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/drawing-feature2.png b/zh-cn/application-dev/ui/figures/drawing-feature2.png deleted file mode 100644 index 42fc7cdf9436680a30413c5bb07e02fd3230ec0c..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/drawing-feature2.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/drawing-feature3.png b/zh-cn/application-dev/ui/figures/drawing-feature3.png deleted file mode 100644 index c090ff948d7333f3dea17dd7ec54488638788c0c..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/drawing-feature3.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/drawing-feature4.png b/zh-cn/application-dev/ui/figures/drawing-feature4.png deleted file mode 100644 index bcfee4728b4d5fc9bfc57e6bb743e708ad0b2379..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/drawing-feature4.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/drawing-feature5.png b/zh-cn/application-dev/ui/figures/drawing-feature5.png deleted file mode 100644 index e70c63ed2f601cf4cd30f319dc6ebd74f216c909..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/drawing-feature5.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/drawing-feature6.png b/zh-cn/application-dev/ui/figures/drawing-feature6.png deleted file mode 100644 index 772bd122cd1ecb252625f59af5ea3e5ff61689fc..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/drawing-feature6.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/drawing-feature7.png b/zh-cn/application-dev/ui/figures/drawing-feature7.png deleted file mode 100644 index c77366f46008f173bc117f95bed6bc7827af11cd..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/drawing-feature7.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/drawing-feature8.png b/zh-cn/application-dev/ui/figures/drawing-feature8.png deleted file mode 100644 index 17cdf2ba3b6e3eeabbe93ed3115c34801634ba8e..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/drawing-feature8.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/exclusive.gif b/zh-cn/application-dev/ui/figures/exclusive.gif new file mode 100644 index 0000000000000000000000000000000000000000..2899db3f8340a309d8542af9d067e1a8cc5c4df7 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/exclusive.gif differ diff --git a/zh-cn/application-dev/ui/figures/flex-layout-direction.png b/zh-cn/application-dev/ui/figures/flex-layout-direction.png new file mode 100644 index 0000000000000000000000000000000000000000..c1ccffc523c28e9886213f8a63adf0a454e73ac1 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/flex-layout-direction.png differ diff --git a/zh-cn/application-dev/ui/figures/flex-layout.png b/zh-cn/application-dev/ui/figures/flex-layout.png new file mode 100644 index 0000000000000000000000000000000000000000..e38b472351f36d22098c20c473d4ff9702689792 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/flex-layout.png differ diff --git a/zh-cn/application-dev/ui/figures/flex-spindle-alignment.png b/zh-cn/application-dev/ui/figures/flex-spindle-alignment.png new file mode 100644 index 0000000000000000000000000000000000000000..160cde8b8a463a3f320764030371baebefbae872 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/flex-spindle-alignment.png differ diff --git a/zh-cn/application-dev/ui/figures/flex.png b/zh-cn/application-dev/ui/figures/flex.png deleted file mode 100644 index 848ceef3873ed6f83466d9ab42f6aa68cb341fe9..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/flex.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/flexbasis.png b/zh-cn/application-dev/ui/figures/flexbasis.png deleted file mode 100644 index df26a3272410052e3df94b2c4207015d5d23b613..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/flexbasis.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/forEachTransition2.gif b/zh-cn/application-dev/ui/figures/forEachTransition2.gif new file mode 100644 index 0000000000000000000000000000000000000000..c4c467902536f0daca7a43aadcc96b685267a2b8 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/forEachTransition2.gif differ diff --git a/zh-cn/application-dev/ui/figures/free1.jpg b/zh-cn/application-dev/ui/figures/free1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..04588b20c976426ea54add478198ae42935830a0 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/free1.jpg differ diff --git a/zh-cn/application-dev/ui/figures/free3.jpg b/zh-cn/application-dev/ui/figures/free3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f4cdd38b6e3ddf67c49af564075b61ce88c8f3dd Binary files /dev/null and b/zh-cn/application-dev/ui/figures/free3.jpg differ diff --git a/zh-cn/application-dev/ui/figures/gridExp1.png b/zh-cn/application-dev/ui/figures/gridExp1.png deleted file mode 100644 index 6767d92cee335418b9c82aa842359dfce1d887c9..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/gridExp1.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/gridExp2.png b/zh-cn/application-dev/ui/figures/gridExp2.png deleted file mode 100644 index c484c27c079f3490b32d7e6c2c8e3f0b84a0151a..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/gridExp2.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/griditem.png b/zh-cn/application-dev/ui/figures/griditem.png deleted file mode 100644 index 9560e2291add8a9117be6a5ccb90033b960190d5..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/griditem.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/gridlayout.png b/zh-cn/application-dev/ui/figures/gridlayout.png deleted file mode 100644 index cc0db8ce530d60191adf8ffaefdbf11b97305067..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/gridlayout.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/horizontal-arrangement-child-column.png b/zh-cn/application-dev/ui/figures/horizontal-arrangement-child-column.png new file mode 100644 index 0000000000000000000000000000000000000000..2cc5e50cb9bc992ef155af9d8a8456cc1193b6d8 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/horizontal-arrangement-child-column.png differ diff --git a/zh-cn/application-dev/ui/figures/horizontal-arrangement-child-row.png b/zh-cn/application-dev/ui/figures/horizontal-arrangement-child-row.png new file mode 100644 index 0000000000000000000000000000000000000000..b2c1d11e2694a576e4a1cfb19919cb49aafa63c4 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/horizontal-arrangement-child-row.png differ diff --git a/zh-cn/application-dev/ui/figures/hoverEffect.gif b/zh-cn/application-dev/ui/figures/hoverEffect.gif new file mode 100644 index 0000000000000000000000000000000000000000..5b8d3816fd803a3a7ba65e8e0f65c9f546ad1657 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/hoverEffect.gif differ diff --git a/zh-cn/application-dev/ui/figures/ifElseTransition.gif b/zh-cn/application-dev/ui/figures/ifElseTransition.gif new file mode 100644 index 0000000000000000000000000000000000000000..516475ccf8f95815b1f92de9fb9c1355564cdce9 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/ifElseTransition.gif differ diff --git a/zh-cn/application-dev/ui/figures/image-rawfile.jpg b/zh-cn/application-dev/ui/figures/image-rawfile.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1629225e1db6c261156615de42a3ea00d2537f32 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/image-rawfile.jpg differ diff --git a/zh-cn/application-dev/ui/figures/image-resource.jpg b/zh-cn/application-dev/ui/figures/image-resource.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3256f3792813e1c34f978f290630cca92485d5d8 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/image-resource.jpg differ diff --git a/zh-cn/application-dev/ui/figures/ind.PNG b/zh-cn/application-dev/ui/figures/ind.PNG new file mode 100644 index 0000000000000000000000000000000000000000..4bc8211932e96bf6dd1c806cbd7a4b88e3414084 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/ind.PNG differ diff --git a/zh-cn/application-dev/ui/figures/indicator.PNG b/zh-cn/application-dev/ui/figures/indicator.PNG new file mode 100644 index 0000000000000000000000000000000000000000..6ff8a073e6fb1414628c75641097de248e0873f5 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/indicator.PNG differ diff --git a/zh-cn/application-dev/ui/figures/justifyContent.png b/zh-cn/application-dev/ui/figures/justifyContent.png deleted file mode 100644 index 2e997f9cba6ce5c2ad93c1f2e33728b7db17bdd3..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/justifyContent.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/landscape.jpg b/zh-cn/application-dev/ui/figures/landscape.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f82c49f882a9cc7b7f0398d9a4ccdf2fe8a3a92a Binary files /dev/null and b/zh-cn/application-dev/ui/figures/landscape.jpg differ diff --git a/zh-cn/application-dev/ui/figures/layout-element-omposition.png b/zh-cn/application-dev/ui/figures/layout-element-omposition.png new file mode 100644 index 0000000000000000000000000000000000000000..c428929b2e171504d94cf5aef99772d517e226ba Binary files /dev/null and b/zh-cn/application-dev/ui/figures/layout-element-omposition.png differ diff --git a/zh-cn/application-dev/ui/figures/layout-performace-2.gif b/zh-cn/application-dev/ui/figures/layout-performace-2.gif new file mode 100644 index 0000000000000000000000000000000000000000..59c81dd976a8a48d3b1d77c1a787a62b053c82e7 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/layout-performace-2.gif differ diff --git a/zh-cn/application-dev/ui/figures/layout-performace-3.gif b/zh-cn/application-dev/ui/figures/layout-performace-3.gif new file mode 100644 index 0000000000000000000000000000000000000000..7c0973e179375d8ada5b3a5ff5f59d356d7c0768 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/layout-performace-3.gif differ diff --git a/zh-cn/application-dev/ui/figures/layout-performace-5.gif b/zh-cn/application-dev/ui/figures/layout-performace-5.gif new file mode 100644 index 0000000000000000000000000000000000000000..0b31f4a2428146a01ab85f764dc14523136451f5 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/layout-performace-5.gif differ diff --git a/zh-cn/application-dev/ui/figures/layout-performace-6.gif b/zh-cn/application-dev/ui/figures/layout-performace-6.gif new file mode 100644 index 0000000000000000000000000000000000000000..47abbed7d245d4add5cd81a685aece4f0b96b958 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/layout-performace-6.gif differ diff --git a/zh-cn/application-dev/ui/figures/layout-performance-1.png b/zh-cn/application-dev/ui/figures/layout-performance-1.png new file mode 100644 index 0000000000000000000000000000000000000000..e364b396eecea9b0abf6b990e024d3011ca0866c Binary files /dev/null and b/zh-cn/application-dev/ui/figures/layout-performance-1.png differ diff --git a/zh-cn/application-dev/ui/figures/layout-performance-4.png b/zh-cn/application-dev/ui/figures/layout-performance-4.png new file mode 100644 index 0000000000000000000000000000000000000000..5d0361b9f86af2adac1d0bafe04a19ca5a99b4f8 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/layout-performance-4.png differ diff --git a/zh-cn/application-dev/ui/figures/layoutChange1.gif b/zh-cn/application-dev/ui/figures/layoutChange1.gif new file mode 100644 index 0000000000000000000000000000000000000000..3e03ce67601e2c8b1b4702ede58a7b95c9c65414 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/layoutChange1.gif differ diff --git a/zh-cn/application-dev/ui/figures/layoutChange2_animateTo.gif b/zh-cn/application-dev/ui/figures/layoutChange2_animateTo.gif new file mode 100644 index 0000000000000000000000000000000000000000..ad94cb26c2874bc45ca84a2aaf5d58bbcd864685 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/layoutChange2_animateTo.gif differ diff --git a/zh-cn/application-dev/ui/figures/layoutChange2_animateTo_change.gif b/zh-cn/application-dev/ui/figures/layoutChange2_animateTo_change.gif new file mode 100644 index 0000000000000000000000000000000000000000..14d804c7a0f1e843f218b2551af6f8b51dc421bb Binary files /dev/null and b/zh-cn/application-dev/ui/figures/layoutChange2_animateTo_change.gif differ diff --git a/zh-cn/application-dev/ui/figures/longPress.gif b/zh-cn/application-dev/ui/figures/longPress.gif new file mode 100644 index 0000000000000000000000000000000000000000..5e5ba5d0083a1ec7599927efa0ff2005c8eef6fb Binary files /dev/null and b/zh-cn/application-dev/ui/figures/longPress.gif differ diff --git a/zh-cn/application-dev/ui/figures/loop_false.gif b/zh-cn/application-dev/ui/figures/loop_false.gif new file mode 100644 index 0000000000000000000000000000000000000000..29da104b99cad5fedbe41ea99593741497596b90 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/loop_false.gif differ diff --git a/zh-cn/application-dev/ui/figures/loop_true.gif b/zh-cn/application-dev/ui/figures/loop_true.gif new file mode 100644 index 0000000000000000000000000000000000000000..1c999814a654ef2a1fc55c096c616350abe058ee Binary files /dev/null and b/zh-cn/application-dev/ui/figures/loop_true.gif differ diff --git a/zh-cn/application-dev/ui/figures/mini.jpg b/zh-cn/application-dev/ui/figures/mini.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a409cb45f780202d179afd726b28487be1a2ce70 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/mini.jpg differ diff --git a/zh-cn/application-dev/ui/figures/onHover.gif b/zh-cn/application-dev/ui/figures/onHover.gif new file mode 100644 index 0000000000000000000000000000000000000000..beb62cb06bfe22b071ff02760fddeb99df7d43ce Binary files /dev/null and b/zh-cn/application-dev/ui/figures/onHover.gif differ diff --git a/zh-cn/application-dev/ui/figures/onMouse1.gif b/zh-cn/application-dev/ui/figures/onMouse1.gif new file mode 100644 index 0000000000000000000000000000000000000000..b92b55b094f1daf97cfd92d0192d24977e28d410 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/onMouse1.gif differ diff --git a/zh-cn/application-dev/ui/figures/pageTransition_None.gif b/zh-cn/application-dev/ui/figures/pageTransition_None.gif new file mode 100644 index 0000000000000000000000000000000000000000..7f5fb82f2c6c54151218faa7f843c466a3c6ddd5 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/pageTransition_None.gif differ diff --git a/zh-cn/application-dev/ui/figures/pageTransition_PushPop.gif b/zh-cn/application-dev/ui/figures/pageTransition_PushPop.gif new file mode 100644 index 0000000000000000000000000000000000000000..3b446b948717f1eeb543bd4352664353a1560b16 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/pageTransition_PushPop.gif differ diff --git a/zh-cn/application-dev/ui/figures/pan.gif b/zh-cn/application-dev/ui/figures/pan.gif new file mode 100644 index 0000000000000000000000000000000000000000..2e0c4212e1d3bab9e63f517061c4e25ca2c1ce2d Binary files /dev/null and b/zh-cn/application-dev/ui/figures/pan.gif differ diff --git a/zh-cn/application-dev/ui/figures/parallel.gif b/zh-cn/application-dev/ui/figures/parallel.gif new file mode 100644 index 0000000000000000000000000000000000000000..d56438e38f732801e8502c16354552085b525f36 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/parallel.gif differ diff --git a/zh-cn/application-dev/ui/figures/pinch.png b/zh-cn/application-dev/ui/figures/pinch.png new file mode 100644 index 0000000000000000000000000000000000000000..d2978b1a8c243e8de2942f2c290c3888c908051a Binary files /dev/null and b/zh-cn/application-dev/ui/figures/pinch.png differ diff --git a/zh-cn/application-dev/ui/figures/portralit.jpg b/zh-cn/application-dev/ui/figures/portralit.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f656f49433be7787f9d0fbd9106cf6c9d366f25c Binary files /dev/null and b/zh-cn/application-dev/ui/figures/portralit.jpg differ diff --git a/zh-cn/application-dev/ui/figures/progress.gif b/zh-cn/application-dev/ui/figures/progress.gif new file mode 100644 index 0000000000000000000000000000000000000000..a89a731a3413fa7af964b372682120e84b7e5cab Binary files /dev/null and b/zh-cn/application-dev/ui/figures/progress.gif differ diff --git a/zh-cn/application-dev/ui/figures/progress_captule.png b/zh-cn/application-dev/ui/figures/progress_captule.png new file mode 100644 index 0000000000000000000000000000000000000000..83362165297f7ddc073d4eb63a7ad643614a8bab Binary files /dev/null and b/zh-cn/application-dev/ui/figures/progress_captule.png differ diff --git a/zh-cn/application-dev/ui/figures/progress_circle.png b/zh-cn/application-dev/ui/figures/progress_circle.png new file mode 100644 index 0000000000000000000000000000000000000000..727793807b679b1668d39a95adbe530fefa32aa7 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/progress_circle.png differ diff --git a/zh-cn/application-dev/ui/figures/progress_ring.png b/zh-cn/application-dev/ui/figures/progress_ring.png new file mode 100644 index 0000000000000000000000000000000000000000..4d9555c12ae981f36d118ac93452aaccaf9edf65 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/progress_ring.png differ diff --git a/zh-cn/application-dev/ui/figures/progress_scalering.png b/zh-cn/application-dev/ui/figures/progress_scalering.png new file mode 100644 index 0000000000000000000000000000000000000000..3f38cd740d4400ce423ea01f13dc0a5871270339 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/progress_scalering.png differ diff --git a/zh-cn/application-dev/ui/figures/relative-layout.png b/zh-cn/application-dev/ui/figures/relative-layout.png new file mode 100644 index 0000000000000000000000000000000000000000..a790ca8d5e78d1f659b61175a4e2d424522dec51 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/relative-layout.png differ diff --git a/zh-cn/application-dev/ui/figures/rotation.png b/zh-cn/application-dev/ui/figures/rotation.png new file mode 100644 index 0000000000000000000000000000000000000000..8ea5e0fba794e6d1b5e77f58bd8b0b91599b53cd Binary files /dev/null and b/zh-cn/application-dev/ui/figures/rotation.png differ diff --git a/zh-cn/application-dev/ui/figures/router-add-query-box-before-back.gif b/zh-cn/application-dev/ui/figures/router-add-query-box-before-back.gif new file mode 100644 index 0000000000000000000000000000000000000000..3e24d2d902e480f994f14b603cb41a602ce08426 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/router-add-query-box-before-back.gif differ diff --git a/zh-cn/application-dev/ui/figures/router-back-to-home.gif b/zh-cn/application-dev/ui/figures/router-back-to-home.gif new file mode 100644 index 0000000000000000000000000000000000000000..8cf1e3ec5d3686045a00a618db95f5b0a61d28e5 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/router-back-to-home.gif differ diff --git a/zh-cn/application-dev/ui/figures/router-jump-to-detail.gif b/zh-cn/application-dev/ui/figures/router-jump-to-detail.gif new file mode 100644 index 0000000000000000000000000000000000000000..51b251d485d1fc02cce5dbdab46a8bde02ef149e Binary files /dev/null and b/zh-cn/application-dev/ui/figures/router-jump-to-detail.gif differ diff --git a/zh-cn/application-dev/ui/figures/rowalign.png b/zh-cn/application-dev/ui/figures/rowalign.png deleted file mode 100644 index 1f87579bb75ea031951dc073f0cd0a3a65698ee6..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/rowalign.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/rowjustify.png b/zh-cn/application-dev/ui/figures/rowjustify.png deleted file mode 100644 index 7ed6dfc34d5c3dee1202925b373d5c7b7928c248..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/rowjustify.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/rowspace.png b/zh-cn/application-dev/ui/figures/rowspace.png deleted file mode 100644 index 9bf2d2da0b1634d8051214908b83bc5e41f1702d..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/rowspace.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/sequence.gif b/zh-cn/application-dev/ui/figures/sequence.gif new file mode 100644 index 0000000000000000000000000000000000000000..7e1982fa74f7f477ae5fcf92230e70b46baa0928 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/sequence.gif differ diff --git a/zh-cn/application-dev/ui/figures/sharedTransition.gif b/zh-cn/application-dev/ui/figures/sharedTransition.gif new file mode 100644 index 0000000000000000000000000000000000000000..34a47f871af170c17700052e24dd2e9c55b4215d Binary files /dev/null and b/zh-cn/application-dev/ui/figures/sharedTransition.gif differ diff --git a/zh-cn/application-dev/ui/figures/size-change-animation.gif b/zh-cn/application-dev/ui/figures/size-change-animation.gif new file mode 100644 index 0000000000000000000000000000000000000000..fe95f4162af64d2f5cbf4c71645a0317a8fff019 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/size-change-animation.gif differ diff --git a/zh-cn/application-dev/ui/figures/springCurve.gif b/zh-cn/application-dev/ui/figures/springCurve.gif new file mode 100644 index 0000000000000000000000000000000000000000..9d4ba21e93b3b7138ec4484fbedc8c3d663384b6 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/springCurve.gif differ diff --git a/zh-cn/application-dev/ui/figures/springMotion.gif b/zh-cn/application-dev/ui/figures/springMotion.gif new file mode 100644 index 0000000000000000000000000000000000000000..906d982c4932f0d3d84601de4dce86d97f4d8478 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/springMotion.gif differ diff --git a/zh-cn/application-dev/ui/figures/stack-layout-sample.png b/zh-cn/application-dev/ui/figures/stack-layout-sample.png new file mode 100644 index 0000000000000000000000000000000000000000..d6b9af36a604d05e79e77bef184dfea42341d261 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/stack-layout-sample.png differ diff --git a/zh-cn/application-dev/ui/figures/stack-layout.png b/zh-cn/application-dev/ui/figures/stack-layout.png new file mode 100644 index 0000000000000000000000000000000000000000..2e01796a7e972c0b0279ad239768a15632df8b1e Binary files /dev/null and b/zh-cn/application-dev/ui/figures/stack-layout.png differ diff --git a/zh-cn/application-dev/ui/figures/stackbottom.png b/zh-cn/application-dev/ui/figures/stackbottom.png deleted file mode 100644 index d4a1d76611bb608becded581f24b69b48185d96d..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/stackbottom.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/stackbottomend.png b/zh-cn/application-dev/ui/figures/stackbottomend.png deleted file mode 100644 index 6f34877f2ed88390eca31bab2370c187d012fd73..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/stackbottomend.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/stackbottomstart.png b/zh-cn/application-dev/ui/figures/stackbottomstart.png deleted file mode 100644 index d6539785544de70d20ce382465974cfb24d955c7..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/stackbottomstart.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/stackcenter.png b/zh-cn/application-dev/ui/figures/stackcenter.png deleted file mode 100644 index 0ba76abeba693ae3d1654b6b3fbde91661669237..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/stackcenter.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/stackend.png b/zh-cn/application-dev/ui/figures/stackend.png deleted file mode 100644 index 2bf91c717a94ce47a2f3c9ca766eb89fae513a71..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/stackend.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/stackstart.png b/zh-cn/application-dev/ui/figures/stackstart.png deleted file mode 100644 index 3936627a8d4134acd3e02833cb2c3c593f52c66e..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/stackstart.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/stacktop.png b/zh-cn/application-dev/ui/figures/stacktop.png deleted file mode 100644 index 01a649049c24df96510727918689455ad9898559..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/stacktop.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/stacktopend.png b/zh-cn/application-dev/ui/figures/stacktopend.png deleted file mode 100644 index 95e8fef4f2c2a6905ed0efc00e9cacd14852c5de..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/stacktopend.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/stacktopstart.png b/zh-cn/application-dev/ui/figures/stacktopstart.png deleted file mode 100644 index 7b742984076e7066cca56a5647bfa3c096b317f2..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/stacktopstart.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/stroke.jpg b/zh-cn/application-dev/ui/figures/stroke.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ac49df0c160480cc83360cba49a1e02ced97c687 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/stroke.jpg differ diff --git a/zh-cn/application-dev/ui/figures/strokeLineJoin.jpg b/zh-cn/application-dev/ui/figures/strokeLineJoin.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6611794bd75d7037a4e01f1c9caea96d6c1f8d0d Binary files /dev/null and b/zh-cn/application-dev/ui/figures/strokeLineJoin.jpg differ diff --git a/zh-cn/application-dev/ui/figures/strokeopacity.jpg b/zh-cn/application-dev/ui/figures/strokeopacity.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7c131ebabbd41fcd331d903da9d0aaf2ccbacfb7 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/strokeopacity.jpg differ diff --git a/zh-cn/application-dev/ui/figures/swipe.gif b/zh-cn/application-dev/ui/figures/swipe.gif new file mode 100644 index 0000000000000000000000000000000000000000..d368f149d8a61a94ef29a4bf5c60da5ab2a17d28 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/swipe.gif differ diff --git a/zh-cn/application-dev/ui/figures/tabs-layout.png b/zh-cn/application-dev/ui/figures/tabs-layout.png new file mode 100644 index 0000000000000000000000000000000000000000..48bc04e38fcd0a09b2956420c3a3b795c9a2c7bc Binary files /dev/null and b/zh-cn/application-dev/ui/figures/tabs-layout.png differ diff --git a/zh-cn/application-dev/ui/figures/tabs-tabscontent.png b/zh-cn/application-dev/ui/figures/tabs-tabscontent.png new file mode 100644 index 0000000000000000000000000000000000000000..58abb27682abd5aaa93827efb5d23b873873daa8 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/tabs-tabscontent.png differ diff --git a/zh-cn/application-dev/ui/figures/tap.gif b/zh-cn/application-dev/ui/figures/tap.gif new file mode 100644 index 0000000000000000000000000000000000000000..bee782263d059fe48a71766b0f607f2ad581209d Binary files /dev/null and b/zh-cn/application-dev/ui/figures/tap.gif differ diff --git a/zh-cn/application-dev/ui/figures/two.PNG b/zh-cn/application-dev/ui/figures/two.PNG new file mode 100644 index 0000000000000000000000000000000000000000..c692643e6b500ec305d9cab801a40f5dbc2fcbca Binary files /dev/null and b/zh-cn/application-dev/ui/figures/two.PNG differ diff --git a/zh-cn/application-dev/ui/figures/vertial-arrangement-child-column.png b/zh-cn/application-dev/ui/figures/vertial-arrangement-child-column.png new file mode 100644 index 0000000000000000000000000000000000000000..dd4e9106e882055dc637bb110508a66b783f7c9d Binary files /dev/null and b/zh-cn/application-dev/ui/figures/vertial-arrangement-child-column.png differ diff --git a/zh-cn/application-dev/ui/figures/vertial-arrangement-child-row.png b/zh-cn/application-dev/ui/figures/vertial-arrangement-child-row.png new file mode 100644 index 0000000000000000000000000000000000000000..160cde8b8a463a3f320764030371baebefbae872 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/vertial-arrangement-child-row.png differ diff --git "a/zh-cn/application-dev/ui/figures/viewport\357\274\2102\357\274\211.jpg" "b/zh-cn/application-dev/ui/figures/viewport\357\274\2102\357\274\211.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..eeca7909de92b85a73e5552567c9ae485ff2f47a Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/viewport\357\274\2102\357\274\211.jpg" differ diff --git "a/zh-cn/application-dev/ui/figures/viewport\357\274\2103\357\274\211.jpg" "b/zh-cn/application-dev/ui/figures/viewport\357\274\2103\357\274\211.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..a5ec30f22410b7e7d9b467738e7fdd0cd9cace49 Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/viewport\357\274\2103\357\274\211.jpg" differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image1_0000001204776353.png b/zh-cn/application-dev/ui/figures/zh-cn_image1_0000001204776353.png deleted file mode 100644 index 25330d63162d9999d2c57d3c33bcc20731df7851..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image1_0000001204776353.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001168410342.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001168410342.png deleted file mode 100644 index 67b8d1571853fe13079a13ed32aff66bc2fc4452..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001168410342.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001168728872.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001168728872.png deleted file mode 100644 index ddf24cd804055371cbd8a753089263f6bcc32b79..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001168728872.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001168888224.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001168888224.png deleted file mode 100644 index 81f79df7d05de05704fdbe9565d4078e026efd60..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001168888224.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001169532276.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001169532276.png deleted file mode 100644 index eca2b53342a2bf63d089c835bfdae16b2608e70f..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001169532276.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001169599582.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001169599582.png deleted file mode 100644 index a4eb400dcd217074d6e76902723e9fffd34fd4bd..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001169599582.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001169678922.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001169678922.png deleted file mode 100644 index 9c89860f26331dc11cf8104711be1ad3be918111..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001169678922.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001169759552.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001169759552.png deleted file mode 100644 index f910230ebfab9c5315eb1c2bc99f0ca35b3cbe23..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001169759552.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001169918548.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001169918548.gif deleted file mode 100644 index b59ae3d79b2bc926634a50c1f3f6aecce247763c..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001169918548.gif and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001170008198.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001170008198.gif deleted file mode 100644 index c88150c77afccf736d42fe7253df27f2b1d27cd5..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001170008198.gif and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001170167520.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001170167520.gif deleted file mode 100644 index fb5399a47b281cb586dbc4460fc0db6381d1e4ab..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001170167520.gif and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001170167520.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001170167520.png deleted file mode 100644 index 2441a46f00b3083dfaa8ec2dcdb1760aa7e2aeb7..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001170167520.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001170411978.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001170411978.gif deleted file mode 100644 index a7d9b572abb833cc7cd52e63d25c4c261a10af65..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001170411978.gif and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001183709904.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001183709904.png deleted file mode 100644 index fb34869ae9a78d655a30e62e1936840d0aa6bb4d..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001183709904.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001195119619.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001195119619.png deleted file mode 100644 index d5bed5e4ae3a322db0e4f05482913fdbd828cbed..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001195119619.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001204538065.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001204538065.png deleted file mode 100644 index b775a2bf408dd710861afa0dfa9f756d5181e811..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001204538065.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001204776353.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001204776353.png deleted file mode 100644 index b27a7f5358c954fe7e1bd912358d29d456870c2a..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001204776353.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001213968747.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001213968747.png deleted file mode 100644 index b60416b59cb77e096d615ba1b25d2b14056abe00..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001213968747.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001214128687.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001214128687.png deleted file mode 100644 index 3f2f15792563ec89015abce1fcf30248b3c0288e..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001214128687.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001214210217.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001214210217.png deleted file mode 100644 index 18abb7b725fcf0172f189c0f1cf70e9c5ae31642..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001214210217.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001214998349.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001214998349.png deleted file mode 100644 index 6a845d64a542809c05f008eef5d1e1ed9d1c22a5..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001214998349.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001215079443.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001215079443.png deleted file mode 100644 index 0a53a5742ac5fda3501a93f576b945e21bd2addf..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001215079443.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001215113569.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001215113569.png deleted file mode 100644 index c7b4a7dfc70a6ee3ce7a837d1e8e91acc7d05d22..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001215113569.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001215199399.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001215199399.png deleted file mode 100644 index c508bb8764c28f228e2c0a33dd6ee97e48dfe682..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001215199399.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001215433095.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001215433095.png deleted file mode 100644 index d015869874aecf7235aa892993d3d872a4cfe9df..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001215433095.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001262954829.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001262954829.png deleted file mode 100644 index a606cbe3c297f267e77a71816d21def7e3d51d2d..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001262954829.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001263074739.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001263074739.png deleted file mode 100644 index e2f0ff5761d3c5a19a300f7c891a717e13c2374d..0000000000000000000000000000000000000000 Binary files a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001263074739.png and /dev/null differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421208.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421208.png new file mode 100644 index 0000000000000000000000000000000000000000..681843c65ae68e47ce4a0e95bee06e01d349c512 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421208.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421216.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421216.png new file mode 100644 index 0000000000000000000000000000000000000000..9b683fdb6849cb3dc5119732a0452f7b9be7a4cb Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421216.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421228.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421228.png new file mode 100644 index 0000000000000000000000000000000000000000..c8ea36c478eb3992fb59aa5337cb283e550b3204 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421228.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421232.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421232.png new file mode 100644 index 0000000000000000000000000000000000000000..97637fd86371c50d5cbacfaa24343fb0b1d71306 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421232.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421240.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421240.png new file mode 100644 index 0000000000000000000000000000000000000000..1dcef5d17c1a081235b1ce2fb61878675a1c15ab Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421240.png differ diff --git a/zh-cn/application-dev/ui/figures/itemalignstretch.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421252.png similarity index 100% rename from zh-cn/application-dev/ui/figures/itemalignstretch.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421252.png diff --git a/zh-cn/application-dev/ui/figures/crossCenter.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421256.png similarity index 100% rename from zh-cn/application-dev/ui/figures/crossCenter.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421256.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421260.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421260.png new file mode 100644 index 0000000000000000000000000000000000000000..0042aa4d4875e6a688d1974e2a036c90a83d3f58 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421260.png differ diff --git a/zh-cn/application-dev/ui/figures/span1.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421264.png similarity index 100% rename from zh-cn/application-dev/ui/figures/span1.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421264.png diff --git a/zh-cn/application-dev/ui/figures/columns2.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421268.png similarity index 100% rename from zh-cn/application-dev/ui/figures/columns2.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421268.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421272.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421272.gif new file mode 100644 index 0000000000000000000000000000000000000000..f2e18ecafe8202705a34e6c76b4fdc59d5cca8d6 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421272.gif differ diff --git a/zh-cn/application-dev/ui/figures/mainStart.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421280.png similarity index 100% rename from zh-cn/application-dev/ui/figures/mainStart.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421280.png diff --git a/zh-cn/application-dev/ui/figures/mainSpacebetween.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421288.png similarity index 100% rename from zh-cn/application-dev/ui/figures/mainSpacebetween.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421288.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421292.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421292.png new file mode 100644 index 0000000000000000000000000000000000000000..76555811ed1be6b8409475b36fbafcdfaf31bb42 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421292.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421320.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421320.png new file mode 100644 index 0000000000000000000000000000000000000000..4114d18a2268916d7181f08a27966d46d642b36b Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421320.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421324.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421324.gif new file mode 100644 index 0000000000000000000000000000000000000000..7501a89d11ac180095f623426b58c3e1f9aa0657 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421324.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421328.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421328.png new file mode 100644 index 0000000000000000000000000000000000000000..a3ca83d7cfd5e9fb14cf67e504edd75a3008864c Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421328.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421332.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421332.png new file mode 100644 index 0000000000000000000000000000000000000000..6d8fe648cee5e8677c5a58cd699a831c8d504975 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421332.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421336.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421336.png new file mode 100644 index 0000000000000000000000000000000000000000..eadeec34e24156f33ff55a84c633009ed00ab341 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421336.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421344.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421344.png new file mode 100644 index 0000000000000000000000000000000000000000..4d97b954671fce83fa8e0728a197c00b0cfb8b16 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421344.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421348.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421348.png new file mode 100644 index 0000000000000000000000000000000000000000..3dccdeed9c73a367170d4ba028adac0533329514 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421348.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421352.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421352.png new file mode 100644 index 0000000000000000000000000000000000000000..3e9c6ec747da89479ec2938e66628d515874d2b4 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421352.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421356.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421356.png new file mode 100644 index 0000000000000000000000000000000000000000..fc6d0fa33e614c536f7f46296becd248e226ed88 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421356.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421360.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421360.gif new file mode 100644 index 0000000000000000000000000000000000000000..e29eee8769672e03926070e651b7d771565f3a70 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421360.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421364.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421364.gif new file mode 100644 index 0000000000000000000000000000000000000000..1194ed8cd42f00c1eeef7062a736149eb0caae14 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421364.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421368.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421368.png new file mode 100644 index 0000000000000000000000000000000000000000..1f4fc5b9cb8e8970dddc77fa38f0e71b4f27f104 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511421368.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580828.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580828.png new file mode 100644 index 0000000000000000000000000000000000000000..96c1d74e3c387c8953602591e33c537749a3a32b Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580828.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580836.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580836.png new file mode 100644 index 0000000000000000000000000000000000000000..4ca2c4f59778b7bed8f1b6c8d9d8e8e69fadb646 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580836.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580840.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580840.png new file mode 100644 index 0000000000000000000000000000000000000000..8fcf5f4902f4e139dc99bdb8fe581da79acf2554 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580840.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580844.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580844.png new file mode 100644 index 0000000000000000000000000000000000000000..8c4263b7748f2c35765a0d0334ba5349ec5fa783 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580844.png differ diff --git a/zh-cn/application-dev/ui/figures/crossSpaceevenly.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580864.png similarity index 100% rename from zh-cn/application-dev/ui/figures/crossSpaceevenly.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580864.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580868.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580868.png new file mode 100644 index 0000000000000000000000000000000000000000..e13be6136ef180b21e736e73668af3637fd90a24 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580868.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580872.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580872.png new file mode 100644 index 0000000000000000000000000000000000000000..7ce85b7535836ddd5a651fa49aad94cf86718690 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580872.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001218579610.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580876.png similarity index 100% rename from zh-cn/application-dev/ui/figures/zh-cn_image_0000001218579610.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580876.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001263019457.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580884.png similarity index 100% rename from zh-cn/application-dev/ui/figures/zh-cn_image_0000001263019457.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580884.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580888.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580888.png new file mode 100644 index 0000000000000000000000000000000000000000..6496f585a1c53c9f8bf09ad9e875229333fba3d2 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580888.png differ diff --git a/zh-cn/application-dev/ui/figures/order1.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580892.png similarity index 100% rename from zh-cn/application-dev/ui/figures/order1.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580892.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580908.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580908.png new file mode 100644 index 0000000000000000000000000000000000000000..490ed3e8c0760fb756e9b4bad1adb54539e6d09d Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580908.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580924.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580924.png new file mode 100644 index 0000000000000000000000000000000000000000..e4e90982652f27353f43ce7e596a512d31141f6c Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580924.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580940.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580940.png new file mode 100644 index 0000000000000000000000000000000000000000..9a13e9c6cb2940aac6cddc9e7130d1af77ba0845 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580940.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580944.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580944.png new file mode 100644 index 0000000000000000000000000000000000000000..369ccf1ed914bd33b689654012d9aedbc70ef6ed Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580944.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580948.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580948.png new file mode 100644 index 0000000000000000000000000000000000000000..0dab803fce687b5a39ba9cf464bb4238974d0a28 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580948.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580952.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580952.png new file mode 100644 index 0000000000000000000000000000000000000000..e7a1c32317e56abc77b8a73f86e477c6d11be827 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580952.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580956.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580956.png new file mode 100644 index 0000000000000000000000000000000000000000..f74edf41b4a0bd995d4121af61672308ff473226 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580956.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580960.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580960.png new file mode 100644 index 0000000000000000000000000000000000000000..1bca1c658300a14fa458bd9223829a0946c31ac5 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580960.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580964.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580964.png new file mode 100644 index 0000000000000000000000000000000000000000..99b123c307de1a6eae2bdc7e8bb410c4b42bf300 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580964.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580968.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580968.png new file mode 100644 index 0000000000000000000000000000000000000000..a584b0bdff74f412dc2d219d99e9bbf5d78bf85a Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580968.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580976.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580976.gif new file mode 100644 index 0000000000000000000000000000000000000000..e1e255edeeb4ac6e8a7300275269b9c6364c3bbe Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580976.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580980.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580980.gif new file mode 100644 index 0000000000000000000000000000000000000000..c4bd0b6d77ec21b4de1e71e3c7977fb452777293 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511580980.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740428.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740428.png new file mode 100644 index 0000000000000000000000000000000000000000..e75b4784102291f479f2aa8573464d08c076d041 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740428.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740436.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740436.png new file mode 100644 index 0000000000000000000000000000000000000000..9f2a2cec4b631aa360635c995623b3dd15f65f68 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740436.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740444.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740444.png new file mode 100644 index 0000000000000000000000000000000000000000..9a5dac961e72d473881548b6d90ea5fd61cb14df Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740444.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740448.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740448.png new file mode 100644 index 0000000000000000000000000000000000000000..ce34734cc84091cc2daa1a1f82d18a8819d6f163 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740448.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740460.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740460.png new file mode 100644 index 0000000000000000000000000000000000000000..ae0a8c18848c94a7a271a0c0b9424a3fa8ffb930 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740460.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001218419614.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740468.png similarity index 100% rename from zh-cn/application-dev/ui/figures/zh-cn_image_0000001218419614.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740468.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740472.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740472.png new file mode 100644 index 0000000000000000000000000000000000000000..785f83000d3f4708dfdfa22a0885a55e1a5eaa8b Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740472.png differ diff --git a/zh-cn/application-dev/ui/figures/gutter1.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740476.png similarity index 100% rename from zh-cn/application-dev/ui/figures/gutter1.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740476.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740480.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740480.png new file mode 100644 index 0000000000000000000000000000000000000000..fde8f90b8439f9dde3ec79a69c417738588df80a Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740480.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001218259636.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740484.png similarity index 100% rename from zh-cn/application-dev/ui/figures/zh-cn_image_0000001218259636.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740484.png diff --git a/zh-cn/application-dev/ui/figures/direction1.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740488.png similarity index 100% rename from zh-cn/application-dev/ui/figures/direction1.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740488.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740492.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740492.gif new file mode 100644 index 0000000000000000000000000000000000000000..eba3ab4a41c44642c47f1864858094771043dcc7 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740492.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740512.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740512.gif new file mode 100644 index 0000000000000000000000000000000000000000..a2f6d3594d09441d235e7c95334fc221e6104d78 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740512.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740524.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740524.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6634a697640a8c0a3d390e0e3e9e300162fff2 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740524.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740532.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740532.png new file mode 100644 index 0000000000000000000000000000000000000000..1baea34fedac1c14f5a9662a3b3e5fb9153d2bca Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740532.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740544.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740544.gif new file mode 100644 index 0000000000000000000000000000000000000000..d86e426dcffcd60213f37faf7c9209ca5a45233f Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740544.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740548.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740548.png new file mode 100644 index 0000000000000000000000000000000000000000..69a1ffe2c68de589e134b7c552a896184b4d7889 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740548.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740552.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740552.gif new file mode 100644 index 0000000000000000000000000000000000000000..f6fc004144fec36d4c94abc65ae9e553c859a135 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740552.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740556.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740556.gif new file mode 100644 index 0000000000000000000000000000000000000000..6df8e5a53ef683d8a6889b0a447497c7e87f056b Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740556.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740564.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740564.png new file mode 100644 index 0000000000000000000000000000000000000000..a08497fe9d7337e09524fd998dee304c0d4712dd Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740564.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740580.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740580.png new file mode 100644 index 0000000000000000000000000000000000000000..35008963fd6d055be6f1f1e50676fe4b7862850c Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740580.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740584.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740584.gif new file mode 100644 index 0000000000000000000000000000000000000000..56da31d78f38871ae7d752e0eeb1938699502aa2 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511740584.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900392.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900392.png new file mode 100644 index 0000000000000000000000000000000000000000..26e369d5782e67701f7fba342225dcbdf5a0dbf1 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900392.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900400.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900400.png new file mode 100644 index 0000000000000000000000000000000000000000..734be949251f4d187fc20b24496f17cbd7263a6f Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900400.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900404.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900404.png new file mode 100644 index 0000000000000000000000000000000000000000..4449c175df2c61ff6c0af6ddcf4e598cf79f3842 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900404.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900428.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900428.png new file mode 100644 index 0000000000000000000000000000000000000000..53fb84e3c75c85055eb4e7a0797dc4600f9dd304 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900428.png differ diff --git a/zh-cn/application-dev/ui/figures/mainSpacearound.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900436.png similarity index 100% rename from zh-cn/application-dev/ui/figures/mainSpacearound.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900436.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001263019463.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900440.png similarity index 100% rename from zh-cn/application-dev/ui/figures/zh-cn_image_0000001263019463.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900440.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900444.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900444.gif new file mode 100644 index 0000000000000000000000000000000000000000..315c82e417d240d0624a8206c9adf47c4c0a3ec3 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900444.gif differ diff --git a/zh-cn/application-dev/ui/figures/crossSpacebetween.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900448.png similarity index 100% rename from zh-cn/application-dev/ui/figures/crossSpacebetween.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900448.png diff --git a/zh-cn/application-dev/ui/figures/flexExample.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900452.png similarity index 100% rename from zh-cn/application-dev/ui/figures/flexExample.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900452.png diff --git a/zh-cn/application-dev/ui/figures/gutter2.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900456.png similarity index 100% rename from zh-cn/application-dev/ui/figures/gutter2.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900456.png diff --git a/zh-cn/application-dev/ui/figures/crossStart.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900460.png similarity index 100% rename from zh-cn/application-dev/ui/figures/crossStart.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900460.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001218739566.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900464.png similarity index 100% rename from zh-cn/application-dev/ui/figures/zh-cn_image_0000001218739566.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900464.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900468.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900468.gif new file mode 100644 index 0000000000000000000000000000000000000000..69f9763394cc6406b29ba8710b73c6441df29048 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900468.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900472.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900472.png new file mode 100644 index 0000000000000000000000000000000000000000..10a9d3f4a7cc753be677f7c02ca3114c893f15c3 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900472.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900480.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900480.png new file mode 100644 index 0000000000000000000000000000000000000000..5bd5e147555a2fdf7c68afe4e1ed54cd65d55d09 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900480.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900504.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900504.png new file mode 100644 index 0000000000000000000000000000000000000000..44b17e42fd2fd4808317ef56e4de54dbced91a4a Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900504.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900508.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900508.gif new file mode 100644 index 0000000000000000000000000000000000000000..60ff6784556d71eeb466fde4399746c32fbdd12b Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900508.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900516.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900516.png new file mode 100644 index 0000000000000000000000000000000000000000..136fbcbb5192d5c935f64b4723e72e909c96e4a3 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900516.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900520.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900520.gif new file mode 100644 index 0000000000000000000000000000000000000000..875d53e13f92ab953b13ae666c92170afc4fc142 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900520.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900524.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900524.gif new file mode 100644 index 0000000000000000000000000000000000000000..80ee68958c80502be31ddcecc22f198c14a4c335 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900524.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900532.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900532.png new file mode 100644 index 0000000000000000000000000000000000000000..77dcb681451df58a99e4b8572f6573e12a39d6aa Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900532.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900540.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900540.gif new file mode 100644 index 0000000000000000000000000000000000000000..d78bd919f391ed021e3d305eef05097c561598ee Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900540.gif differ diff --git a/zh-cn/application-dev/ui/figures/stack2.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900544.png similarity index 100% rename from zh-cn/application-dev/ui/figures/stack2.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001511900544.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700385.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700385.png new file mode 100644 index 0000000000000000000000000000000000000000..01f4454576ee74897e623ff833908644752d615c Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700385.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700393.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700393.png new file mode 100644 index 0000000000000000000000000000000000000000..3e41f2230432d26c9bf8e4cfd8bf94d724da499c Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700393.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700409.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700409.png new file mode 100644 index 0000000000000000000000000000000000000000..11fc91eef9cb70678f014fe1f7266179d24dd152 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700409.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700417.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700417.png new file mode 100644 index 0000000000000000000000000000000000000000..c89c7b8750b89903b21d57c080d4d2028ac50227 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700417.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001263139409.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700425.png similarity index 100% rename from zh-cn/application-dev/ui/figures/zh-cn_image_0000001263139409.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700425.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700433.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700433.gif new file mode 100644 index 0000000000000000000000000000000000000000..b517e892ed6ff4ad337e57d044aa518ff78a8792 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700433.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700437.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700437.png new file mode 100644 index 0000000000000000000000000000000000000000..6eb044c77e05e06b6a3ce3c1b987c4bb23d07906 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700437.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700441.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700441.png new file mode 100644 index 0000000000000000000000000000000000000000..358484503f09f1948815e71550295b92da37656b Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700441.png differ diff --git a/zh-cn/application-dev/ui/figures/crossSpacearound.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700445.png similarity index 100% rename from zh-cn/application-dev/ui/figures/crossSpacearound.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700445.png diff --git a/zh-cn/application-dev/ui/figures/flexgrow.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700449.png similarity index 100% rename from zh-cn/application-dev/ui/figures/flexgrow.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700449.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001263259401.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700453.png similarity index 100% rename from zh-cn/application-dev/ui/figures/zh-cn_image_0000001263259401.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700453.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700457.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700457.png new file mode 100644 index 0000000000000000000000000000000000000000..76675d3c8549a0258e3509230dea17339adb6185 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700457.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700461.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700461.png new file mode 100644 index 0000000000000000000000000000000000000000..60f5a816dffbf2c76ff208420e592d3fbe84ed44 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700461.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700469.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700469.png new file mode 100644 index 0000000000000000000000000000000000000000..afd3964aa6c5a351cb570fd2f0822df0b1e71242 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700469.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700473.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700473.png new file mode 100644 index 0000000000000000000000000000000000000000..ba85d89f29fd165f8ee0cd8ca05ec81bf7fd3e64 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700473.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700493.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700493.png new file mode 100644 index 0000000000000000000000000000000000000000..7db4a60fbc2e6ae31184a9ae40586d25f7e467f7 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700493.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700501.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700501.png new file mode 100644 index 0000000000000000000000000000000000000000..ee24968f7a63f08dba9c6eb7b71bdcfe2bf8c736 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700501.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700505.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700505.png new file mode 100644 index 0000000000000000000000000000000000000000..937a2584e24522a7c38e274cb8031cb796e08b4c Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700505.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700509.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700509.png new file mode 100644 index 0000000000000000000000000000000000000000..a23476d1a84a709d41ce9a19d17216baae2cfbf2 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700509.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700517.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700517.png new file mode 100644 index 0000000000000000000000000000000000000000..d830f303086d7ab27244f88cffa7a359ec62f92a Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700517.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700521.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700521.png new file mode 100644 index 0000000000000000000000000000000000000000..f2a737c020c5ea9c1a9bf4ac73cafe677c73917c Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700521.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700525.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700525.png new file mode 100644 index 0000000000000000000000000000000000000000..88552579e7181155498b23934ff4707317b1e84e Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700525.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700529.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700529.png new file mode 100644 index 0000000000000000000000000000000000000000..b503f477aaed9b842cd66dd82c5ad35e72ab5624 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700529.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700533.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700533.gif new file mode 100644 index 0000000000000000000000000000000000000000..fade44c6342ec3a6051877d3d0ea059781769548 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700533.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700537.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700537.png new file mode 100644 index 0000000000000000000000000000000000000000..e1b3e263c9da5e80a05d417797f7d4aed2b0ad80 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562700537.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820753.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820753.png new file mode 100644 index 0000000000000000000000000000000000000000..8fc23c8ad1a94fe0e9644e5deb0e04a54b3c10cb Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820753.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820757.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820757.png new file mode 100644 index 0000000000000000000000000000000000000000..ee7966d0a498dd541acc578ed31174fc310e75f6 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820757.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820761.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820761.png new file mode 100644 index 0000000000000000000000000000000000000000..772d4b70447cb63cdbec8dd965cc8b5ac3af9a8a Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820761.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820765.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820765.png new file mode 100644 index 0000000000000000000000000000000000000000..08bc3637c5c3429e7606aff8fbba5db49be13b98 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820765.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820789.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820789.png new file mode 100644 index 0000000000000000000000000000000000000000..2ca89265b2c00d4d502302bf3b40cd1443af8cbf Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820789.png differ diff --git a/zh-cn/application-dev/ui/figures/crossEnd.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820801.png similarity index 100% rename from zh-cn/application-dev/ui/figures/crossEnd.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820801.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820805.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820805.png new file mode 100644 index 0000000000000000000000000000000000000000..235d1fbbf20dc80dc72db1e867c83ebe0de796ec Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820805.png differ diff --git a/zh-cn/application-dev/ui/figures/mainEnd.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820809.png similarity index 100% rename from zh-cn/application-dev/ui/figures/mainEnd.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820809.png diff --git a/zh-cn/application-dev/ui/figures/flexshrink.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820813.png similarity index 100% rename from zh-cn/application-dev/ui/figures/flexshrink.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820813.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001218579606.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820817.png similarity index 100% rename from zh-cn/application-dev/ui/figures/zh-cn_image_0000001218579606.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820817.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820821.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820821.png new file mode 100644 index 0000000000000000000000000000000000000000..d37cbb478a454650c64e19349967d1bcc8a3355e Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820821.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820825.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820825.png new file mode 100644 index 0000000000000000000000000000000000000000..b5c1c900b3fe25b86c6c991a0b30a5cf849f4f9b Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820825.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820833.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820833.png new file mode 100644 index 0000000000000000000000000000000000000000..2ed408f55ee20c0a8b3790c72088b5a6c297933f Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820833.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820845.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820845.png new file mode 100644 index 0000000000000000000000000000000000000000..0e7e1f84a9668303f33a057f4f79de48f1c51ba3 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820845.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820877.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820877.gif new file mode 100644 index 0000000000000000000000000000000000000000..2241c494257d51062dcb4b47911430d49bbadb7b Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820877.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820881.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820881.png new file mode 100644 index 0000000000000000000000000000000000000000..592e6591f61e6e9d62023849cbcf1a651bb1691c Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820881.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820893.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820893.png new file mode 100644 index 0000000000000000000000000000000000000000..804c9f6958b6a58b2fad943bcab3149f31820fe0 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820893.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820897.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820897.png new file mode 100644 index 0000000000000000000000000000000000000000..e3b670e7d9cb1fff6d373323d4ec73740370c052 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820897.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820901.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820901.png new file mode 100644 index 0000000000000000000000000000000000000000..a7d1be0ea679f662413ff186cd9504e58b7066a5 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820901.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820905.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820905.gif new file mode 100644 index 0000000000000000000000000000000000000000..f5ce4c339333e842d46f0ea3b447ef68c8d877cd Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562820905.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940473.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940473.png new file mode 100644 index 0000000000000000000000000000000000000000..39794977697ae7ab57855b1dabeff5e61da579c7 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940473.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940477.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940477.png new file mode 100644 index 0000000000000000000000000000000000000000..ac00e7ec5859d6dfa03e2ffc1fbba073d7be5775 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940477.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940481.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940481.png new file mode 100644 index 0000000000000000000000000000000000000000..6cf037607ea99e4b99738d9a9d610939a881f980 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940481.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940485.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940485.png new file mode 100644 index 0000000000000000000000000000000000000000..45196f697e4718ef99ca175bb6c0f1526e2a580d Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940485.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940505.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940505.png new file mode 100644 index 0000000000000000000000000000000000000000..3fc091e4e68bf1ef8c57ad082549e88b31d94caf Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940505.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940513.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940513.png new file mode 100644 index 0000000000000000000000000000000000000000..7527389d971a173f8e5e8b20ef5db4a881a2cd56 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940513.png differ diff --git a/zh-cn/application-dev/ui/figures/direction2.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940517.png similarity index 100% rename from zh-cn/application-dev/ui/figures/direction2.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940517.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001263259399.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940521.png similarity index 100% rename from zh-cn/application-dev/ui/figures/zh-cn_image_0000001263259399.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940521.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940525.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940525.png new file mode 100644 index 0000000000000000000000000000000000000000..c194dadaaa7b411ea1173a063f525d68e5484d49 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940525.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940529.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940529.png new file mode 100644 index 0000000000000000000000000000000000000000..58c643d4925935bf771835147aaf8296088b32ae Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940529.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940533.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940533.png new file mode 100644 index 0000000000000000000000000000000000000000..f81f60b5d9e3f4eb15535f97c7ba439fc285d149 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940533.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001263339459.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940541.png similarity index 100% rename from zh-cn/application-dev/ui/figures/zh-cn_image_0000001263339459.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940541.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940549.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940549.gif new file mode 100644 index 0000000000000000000000000000000000000000..a7d9f0af6d583cdc4eaf4ad867033bdaeb1e1611 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940549.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940565.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940565.png new file mode 100644 index 0000000000000000000000000000000000000000..8753b55548c79ce2730235bc0ad0e97b0a0f3669 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940565.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940581.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940581.png new file mode 100644 index 0000000000000000000000000000000000000000..313f2a2944175d63307b31540ce1ec17b08f1670 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940581.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940585.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940585.png new file mode 100644 index 0000000000000000000000000000000000000000..f6a9c832826019ed0d831c8b513954c87264d3c6 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940585.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940589.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940589.png new file mode 100644 index 0000000000000000000000000000000000000000..d01c3595f202b50050ff1763293cd7cedba234b6 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940589.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940601.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940601.png new file mode 100644 index 0000000000000000000000000000000000000000..5631f463b0e3860ddf113c32b138550f765cb0c8 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940601.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940609.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940609.gif new file mode 100644 index 0000000000000000000000000000000000000000..14f3d25cbf137366ce3c2bb93f683d75a9297597 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940609.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940613.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940613.png new file mode 100644 index 0000000000000000000000000000000000000000..121d50a895a8ad99813b9fc9d440e4cd90779fb8 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940613.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940617.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940617.gif new file mode 100644 index 0000000000000000000000000000000000000000..a1772959dbfa036adfd304632e612b21f1e8c850 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940617.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940621.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940621.png new file mode 100644 index 0000000000000000000000000000000000000000..38c5abece8a9bb2a73552aebcd2bfb626e4ee3e3 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001562940621.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060641.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060641.png new file mode 100644 index 0000000000000000000000000000000000000000..ee7966d0a498dd541acc578ed31174fc310e75f6 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060641.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060653.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060653.png new file mode 100644 index 0000000000000000000000000000000000000000..71b01bc206f9432451fa91e99f3f46283649634a Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060653.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060657.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060657.png new file mode 100644 index 0000000000000000000000000000000000000000..8fbc741238652bae53bac247643954eb1aafc697 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060657.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001218419616.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060677.png similarity index 100% rename from zh-cn/application-dev/ui/figures/zh-cn_image_0000001218419616.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060677.png diff --git a/zh-cn/application-dev/ui/figures/mainCenter.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060681.png similarity index 100% rename from zh-cn/application-dev/ui/figures/mainCenter.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060681.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060685.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060685.png new file mode 100644 index 0000000000000000000000000000000000000000..9ca9fa88b2239304a0986e43240697373ec2bf6a Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060685.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060689.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060689.gif new file mode 100644 index 0000000000000000000000000000000000000000..e44df1b36066095ed6e8741e26d9f04a4c9a2b0b Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060689.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060693.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060693.png new file mode 100644 index 0000000000000000000000000000000000000000..d6da72706ca73f16aef40108a374325f0b5f7de4 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060693.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060697.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060697.png new file mode 100644 index 0000000000000000000000000000000000000000..b46c1ea016b3f018d9729499e3df2b98e10467f9 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060697.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060701.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060701.png new file mode 100644 index 0000000000000000000000000000000000000000..7511094aeee8e6c854f7271a5118fb6a1df99328 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060701.png differ diff --git a/zh-cn/application-dev/ui/figures/offset1.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060705.png similarity index 100% rename from zh-cn/application-dev/ui/figures/offset1.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060705.png diff --git a/zh-cn/application-dev/ui/figures/columns1.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060709.png similarity index 100% rename from zh-cn/application-dev/ui/figures/columns1.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060709.png diff --git a/zh-cn/application-dev/ui/figures/mainSpaceevenly.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060713.png similarity index 100% rename from zh-cn/application-dev/ui/figures/mainSpaceevenly.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060713.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060729.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060729.png new file mode 100644 index 0000000000000000000000000000000000000000..65e3505a934bab1de3779b89737279e670f9ac33 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060729.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060761.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060761.png new file mode 100644 index 0000000000000000000000000000000000000000..907e3ff12b33dbdcaff72ff4d5fc45236de04e79 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060761.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060765.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060765.png new file mode 100644 index 0000000000000000000000000000000000000000..de4848b9c7750dcef6e9216e9f68604203b1620a Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060765.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060769.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060769.gif new file mode 100644 index 0000000000000000000000000000000000000000..bb1ac0f266c1e2eaa4756496832bb28beed4bf39 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060769.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060773.gif b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060773.gif new file mode 100644 index 0000000000000000000000000000000000000000..b1760403750123b2f3c589d0f20697c57080966d Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060773.gif differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060777.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060777.png new file mode 100644 index 0000000000000000000000000000000000000000..bfb4159928f562004b4551d36b4b13e495a4e3c0 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060777.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060781.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060781.png new file mode 100644 index 0000000000000000000000000000000000000000..1216da4afc8b43ab12b27993c33a8078c6a2e44c Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060781.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060785.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060785.png new file mode 100644 index 0000000000000000000000000000000000000000..fc052ef293a27904248d7d6af2a7e13da3be666a Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060785.png differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060793.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060793.png new file mode 100644 index 0000000000000000000000000000000000000000..eab5b1b3b240a8a80f913684ae892b7b775f3e66 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060793.png differ diff --git a/zh-cn/application-dev/ui/figures/stack1.png b/zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060797.png similarity index 100% rename from zh-cn/application-dev/ui/figures/stack1.png rename to zh-cn/application-dev/ui/figures/zh-cn_image_0000001563060797.png diff --git a/zh-cn/application-dev/ui/figures/zh-cn_other_0000001500740342.jpeg b/zh-cn/application-dev/ui/figures/zh-cn_other_0000001500740342.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..ace71361eb29956ec0f80cab785122b546b64bc1 Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_other_0000001500740342.jpeg differ diff --git a/zh-cn/application-dev/ui/figures/zh-cn_other_0000001500900234.jpeg b/zh-cn/application-dev/ui/figures/zh-cn_other_0000001500900234.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..5741b5407f11587de9f927dd29d5e1a199fe65bd Binary files /dev/null and b/zh-cn/application-dev/ui/figures/zh-cn_other_0000001500900234.jpeg differ diff --git "a/zh-cn/application-dev/ui/figures/\344\276\247\350\276\271\345\257\274\350\210\252.png" "b/zh-cn/application-dev/ui/figures/\344\276\247\350\276\271\345\257\274\350\210\252.png" new file mode 100644 index 0000000000000000000000000000000000000000..93a4a782c696b2bf5d15133264bfa9d04d775e9f Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\344\276\247\350\276\271\345\257\274\350\210\252.png" differ diff --git "a/zh-cn/application-dev/ui/figures/\345\210\206\346\240\217.jpg" "b/zh-cn/application-dev/ui/figures/\345\210\206\346\240\217.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..316cd89a45e277376921833dfd992a33a38412c7 Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\345\210\206\346\240\217.jpg" differ diff --git "a/zh-cn/application-dev/ui/figures/\345\210\207\346\215\242\346\214\207\345\256\232\351\241\265\347\255\276.gif" "b/zh-cn/application-dev/ui/figures/\345\210\207\346\215\242\346\214\207\345\256\232\351\241\265\347\255\276.gif" new file mode 100644 index 0000000000000000000000000000000000000000..824c0b29c31d1d21767f602f00ba49e8652dee4f Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\345\210\207\346\215\242\346\214\207\345\256\232\351\241\265\347\255\276.gif" differ diff --git "a/zh-cn/application-dev/ui/figures/\345\210\233\345\273\2722.jpg" "b/zh-cn/application-dev/ui/figures/\345\210\233\345\273\2722.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..183bc320533bc991e38655ea2f4ab441238c9ec7 Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\345\210\233\345\273\2722.jpg" differ diff --git "a/zh-cn/application-dev/ui/figures/\345\215\225\351\241\265\351\235\2421.jpg" "b/zh-cn/application-dev/ui/figures/\345\215\225\351\241\265\351\235\2421.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..93bef8378ac6f275a1d244743d1ddf832f8c2c01 Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\345\215\225\351\241\265\351\235\2421.jpg" differ diff --git "a/zh-cn/application-dev/ui/figures/\345\233\272\345\256\232\345\257\274\350\210\252.gif" "b/zh-cn/application-dev/ui/figures/\345\233\272\345\256\232\345\257\274\350\210\252.gif" new file mode 100644 index 0000000000000000000000000000000000000000..21e0aaf7ee9c2a6a3c5311c183bbaa72189056fb Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\345\233\272\345\256\232\345\257\274\350\210\252.gif" differ diff --git "a/zh-cn/application-dev/ui/figures/\345\233\276\347\211\2071.png" "b/zh-cn/application-dev/ui/figures/\345\233\276\347\211\2071.png" new file mode 100644 index 0000000000000000000000000000000000000000..47750ee238fc411f26ed86d99d2f554d3f185b64 Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\345\233\276\347\211\2071.png" differ diff --git "a/zh-cn/application-dev/ui/figures/\345\233\276\347\211\2072.png" "b/zh-cn/application-dev/ui/figures/\345\233\276\347\211\2072.png" new file mode 100644 index 0000000000000000000000000000000000000000..869998f78209dd5a29b8b1f276299f3d18aef9b0 Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\345\233\276\347\211\2072.png" differ diff --git "a/zh-cn/application-dev/ui/figures/\345\233\276\347\211\2073.png" "b/zh-cn/application-dev/ui/figures/\345\233\276\347\211\2073.png" new file mode 100644 index 0000000000000000000000000000000000000000..ff32cdae3e35bd9b05e0f676fd3774fd357036ff Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\345\233\276\347\211\2073.png" differ diff --git "a/zh-cn/application-dev/ui/figures/\345\234\272\346\231\2571.jpg" "b/zh-cn/application-dev/ui/figures/\345\234\272\346\231\2571.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..04c5ef7e1ed61e42419ec4d40efd44e096df28a3 Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\345\234\272\346\231\2571.jpg" differ diff --git "a/zh-cn/application-dev/ui/figures/\345\234\272\346\231\2572.jpg" "b/zh-cn/application-dev/ui/figures/\345\234\272\346\231\2572.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..76b5c6d464e74460a9e3c74ff4a29d351225984a Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\345\234\272\346\231\2572.jpg" differ diff --git "a/zh-cn/application-dev/ui/figures/\345\261\217\345\271\225\346\210\252\345\233\276_20230223_141141.png" "b/zh-cn/application-dev/ui/figures/\345\261\217\345\271\225\346\210\252\345\233\276_20230223_141141.png" new file mode 100644 index 0000000000000000000000000000000000000000..ea5491bcfc21ba653f88100b0d4796152dc9f203 Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\345\261\217\345\271\225\346\210\252\345\233\276_20230223_141141.png" differ diff --git "a/zh-cn/application-dev/ui/figures/\345\261\217\345\271\225\346\210\252\345\233\276_20230223_141404.png" "b/zh-cn/application-dev/ui/figures/\345\261\217\345\271\225\346\210\252\345\233\276_20230223_141404.png" new file mode 100644 index 0000000000000000000000000000000000000000..040d48397ab11cb433043ce0b50e8e46cc94476a Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\345\261\217\345\271\225\346\210\252\345\233\276_20230223_141404.png" differ diff --git "a/zh-cn/application-dev/ui/figures/\345\272\225\351\203\250\345\257\274\350\210\252.gif" "b/zh-cn/application-dev/ui/figures/\345\272\225\351\203\250\345\257\274\350\210\252.gif" new file mode 100644 index 0000000000000000000000000000000000000000..19347f2d6324e7e86e0d4ac5e9a5143588526589 Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\345\272\225\351\203\250\345\257\274\350\210\252.gif" differ diff --git "a/zh-cn/application-dev/ui/figures/\346\210\252\345\233\2762.PNG" "b/zh-cn/application-dev/ui/figures/\346\210\252\345\233\2762.PNG" new file mode 100644 index 0000000000000000000000000000000000000000..1a9ff7709260b87c724834631bb5723b2ccaaba8 Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\346\210\252\345\233\2762.PNG" differ diff --git "a/zh-cn/application-dev/ui/figures/\346\210\252\345\233\2763.PNG" "b/zh-cn/application-dev/ui/figures/\346\210\252\345\233\2763.PNG" new file mode 100644 index 0000000000000000000000000000000000000000..07e1c228c93ccc4f45d88a82f6b2e5773bc4629a Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\346\210\252\345\233\2763.PNG" differ diff --git "a/zh-cn/application-dev/ui/figures/\346\227\240\346\240\207\351\242\230.png" "b/zh-cn/application-dev/ui/figures/\346\227\240\346\240\207\351\242\230.png" new file mode 100644 index 0000000000000000000000000000000000000000..00e1533c790c45f83781b2657e94dcc17e3ff8b2 Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\346\227\240\346\240\207\351\242\230.png" differ diff --git "a/zh-cn/application-dev/ui/figures/\346\234\200\347\273\210\346\225\210\346\236\234.gif" "b/zh-cn/application-dev/ui/figures/\346\234\200\347\273\210\346\225\210\346\236\234.gif" new file mode 100644 index 0000000000000000000000000000000000000000..e530e58a239d0c76893046f0b73b061f5550820b Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\346\234\200\347\273\210\346\225\210\346\236\234.gif" differ diff --git "a/zh-cn/application-dev/ui/figures/\346\234\200\347\273\210\346\225\210\346\236\23411.gif" "b/zh-cn/application-dev/ui/figures/\346\234\200\347\273\210\346\225\210\346\236\23411.gif" new file mode 100644 index 0000000000000000000000000000000000000000..c0dde5b1ef76e44606de04a979c5e62cf4464438 Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\346\234\200\347\273\210\346\225\210\346\236\23411.gif" differ diff --git "a/zh-cn/application-dev/ui/figures/\346\273\232\345\212\250\345\257\274\350\210\252.gif" "b/zh-cn/application-dev/ui/figures/\346\273\232\345\212\250\345\257\274\350\210\252.gif" new file mode 100644 index 0000000000000000000000000000000000000000..7699d8659a5a9b288c4d9bfd9f8f6ee9a6d17adb Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\346\273\232\345\212\250\345\257\274\350\210\252.gif" differ diff --git "a/zh-cn/application-dev/ui/figures/\350\217\234\345\215\225\346\240\217.jpg" "b/zh-cn/application-dev/ui/figures/\350\217\234\345\215\225\346\240\217.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..931156abc1f61123f6e8b8dbfe91397e03b05450 Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\350\217\234\345\215\225\346\240\217.jpg" differ diff --git "a/zh-cn/application-dev/ui/figures/\350\217\234\345\215\225\346\240\2172.jpg" "b/zh-cn/application-dev/ui/figures/\350\217\234\345\215\225\346\240\2172.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..6d288203ebed2067587cf7ebec77fe7d27a69e1c Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\350\217\234\345\215\225\346\240\2172.jpg" differ diff --git "a/zh-cn/application-dev/ui/figures/\351\231\220\345\210\266\345\257\274\350\210\252.gif" "b/zh-cn/application-dev/ui/figures/\351\231\220\345\210\266\345\257\274\350\210\252.gif" new file mode 100644 index 0000000000000000000000000000000000000000..43ea4148ee3e5fdcb9497481a230fb7da8de9d7e Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\351\231\220\345\210\266\345\257\274\350\210\252.gif" differ diff --git "a/zh-cn/application-dev/ui/figures/\351\241\266\351\203\250\345\257\274\350\210\252.gif" "b/zh-cn/application-dev/ui/figures/\351\241\266\351\203\250\345\257\274\350\210\252.gif" new file mode 100644 index 0000000000000000000000000000000000000000..8526ec78f67b77db19fb4e59bf827cfa5fa53135 Binary files /dev/null and "b/zh-cn/application-dev/ui/figures/\351\241\266\351\203\250\345\257\274\350\210\252.gif" differ diff --git a/zh-cn/application-dev/ui/ui-ts-animation-feature.md b/zh-cn/application-dev/ui/ui-ts-animation-feature.md deleted file mode 100644 index 4373539cca6f91f044e37bc702aeb5fa6e6663f7..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/ui/ui-ts-animation-feature.md +++ /dev/null @@ -1,346 +0,0 @@ -# 添加动画效果 - -动画主要包含了组件动画和页面间动画,并提供了[插值计算](../reference/apis/js-apis-curve.md)和[矩阵变换](../reference/apis/js-apis-matrix4.md)的动画能力接口,让开发者极大程度的自主设计动画效果。 - -在本节主要完成两个动画效果: - -1. 启动页的闪屏动画,即Logo图标的渐出和放大效果; -2. 食物列表页和食物详情页的共享元素转场动画效果。 - -## animateTo实现闪屏动画 - -组件动画包括属性动画和animateTo显式动画: - -1. 属性动画:设置组件通用属性变化的动画效果。 -2. 显式动画:设置组件从状态A到状态B的变化动画效果,包括样式、位置信息和节点的增加删除等,开发者无需关注变化过程,只需指定起点和终点的状态。animateTo还提供播放状态的回调接口,是对属性动画的增强与封装。 - -闪屏页面的动画效果是Logo图标的渐出和放大,动画结束后跳转到食物分类列表页面。接下来,我们就使用animateTo来实现启动页动画的闪屏效果。 - -1. 动画效果自动播放。闪屏动画的预期效果是,进入Logo页面后,animateTo动画效果自动开始播放,可以借助于组件显隐事件的回调接口来实现。调用Shape的onAppear方法,设置其显式动画。 - - ```ts - Shape() { - ... - } - .onAppear(() => { - animateTo() - }) - ``` - -2. 创建opacity和scale数值的成员变量,用装饰器@State修饰。表示其为有状态的数据,即改变会触发页面的刷新。 - - ```ts - @Entry - @Component - struct Logo { - @State private opacityValue: number = 0 - @State private scaleValue: number = 0 - build() { - Shape() { - ... - } - .scale({ x: this.scaleValue, y: this.scaleValue }) - .opacity(this.opacityValue) - .onAppear(() => { - animateTo() - }) - } - } - ``` - -3. 设置animateTo的动画曲线curve。Logo的加速曲线为先慢后快,使用贝塞尔曲线cubicBezier,cubicBezier(0.4, 0, 1, 1)。 - - 需要使用动画能力接口中的插值计算,首先要导入curves模块。 - - ```ts - import Curves from '@ohos.curves' - ``` - - @ohos.curves模块提供了线性Curve. Linear、阶梯step、三阶贝塞尔(cubicBezier)和弹簧(spring)插值曲线的初始化函数,可以根据入参创建一个插值曲线对象。 - - ```ts - @Entry - @Component - struct Logo { - @State private opacityValue: number = 0 - @State private scaleValue: number = 0 - private curve1 = Curves.cubicBezier(0.4, 0, 1, 1) - - build() { - Shape() { - ... - } - .scale({ x: this.scaleValue, y: this.scaleValue }) - .opacity(this.opacityValue) - .onAppear(() => { - animateTo({ - curve: this.curve1 - }) - }) - } - } - ``` - -4. 设置动画时长为1s,延时0.1s开始播放,设置显示动效event的闭包函数,即起点状态到终点状态为透明度opacityValue和大小scaleValue从0到1,实现Logo的渐出和放大效果。 - - ```ts - @Entry - @Component - struct Logo { - @State private opacityValue: number = 0 - @State private scaleValue: number = 0 - private curve1 = Curves.cubicBezier(0.4, 0, 1, 1) - - build() { - Shape() { - ... - } - .scale({ x: this.scaleValue, y: this.scaleValue }) - .opacity(this.opacityValue) - .onAppear(() => { - animateTo({ - duration: 1000, - curve: this.curve1, - delay: 100, - }, () => { - this.opacityValue = 1 - this.scaleValue = 1 - }) - }) - } - } - ``` - -5. 闪屏动画播放结束后定格1s,进入FoodCategoryList页面。设置animateTo的onFinish回调接口,调用定时器Timer的setTimeout接口延时1s后,调用router.replaceUrl,显示FoodCategoryList页面。 - - ```ts - import router from '@ohos.router' - - @Entry - @Component - struct Logo { - @State private opacityValue: number = 0 - @State private scaleValue: number = 0 - private curve1 = Curves.cubicBezier(0.4, 0, 1, 1) - - build() { - Shape() { - ... - } - .scale({ x: this.scaleValue, y: this.scaleValue }) - .opacity(this.opacityValue) - .onAppear(() => { - - animateTo({ - duration: 1000, - curve: this.curve1, - delay: 100, - onFinish: () => { - setTimeout(() => { - router.replaceUrl({ url: "pages/FoodCategoryList" }) - }, 1000); - } - }, () => { - this.opacityValue = 1 - this.scaleValue = 1 - }) - }) - } - } - ``` - - 整体代码如下。 - - ```ts - import Curves from '@ohos.curves' - import router from '@ohos.router' - - @Entry - @Component - struct Logo { - @State private opacityValue: number = 0 - @State private scaleValue: number = 0 - private curve1 = Curves.cubicBezier(0.4, 0, 1, 1) - private pathCommands1: string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z' - private pathCommands2: string = 'M270.6 128.1 h48.6 c51.6 0 98.4 21 132.3 54.6 a411 411 0 0 3 -45.6 123 c-25.2 45.6 -56.4 84 -87.6 110.4 a206.1 206.1 0 0 0 -47.7 -288 z' - - build() { - Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { - Shape() { - Path() - .commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36') - .fill(Color.White) - .stroke(Color.Transparent) - Path() - .commands(this.pathCommands1) - .fill('none') - .stroke(Color.Transparent) - .linearGradient( - { - angle: 30, - colors: [["#C4FFA0", 0], ["#ffffff", 1]] - }) - .clip(new Path().commands(this.pathCommands1)) - - Path() - .commands(this.pathCommands2) - .fill('none') - .stroke(Color.Transparent) - .linearGradient( - { - angle: 50, - colors: [['#8CC36A', 0.1], ["#B3EB90", 0.4], ["#ffffff", 0.7]] - }) - .clip(new Path().commands(this.pathCommands2)) - } - .height('630px') - .width('630px') - .scale({ x: this.scaleValue, y: this.scaleValue }) - .opacity(this.opacityValue) - .onAppear(() => { - animateTo({ - duration: 1000, - curve: this.curve1, - delay: 100, - onFinish: () => { - setTimeout(() => { - router.replaceUrl({ url: "pages/FoodCategoryList" }) - }, 1000); - } - }, () => { - this.opacityValue = 1 - this.scaleValue = 1 - }) - }) - - Text('Healthy Diet') - .fontSize(26) - .fontColor(Color.White) - .margin({ top: 300 }) - - Text('Healthy life comes from a balanced diet') - .fontSize(17) - .fontColor(Color.White) - .margin({ top: 4 }) - } - .width('100%') - .height('100%') - .linearGradient( - { - angle: 180, - colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]] - }) - } - } - ``` - - ![animation-feature](figures/animation-feature.gif) - -## 页面转场动画 - -食物分类列表页和食物详情页之间的共享元素转场,即点击FoodListItem/FoodGridItem后,食物缩略图会放大,随着页面跳转,到食物详情页的大图。 - -1. 设置FoodListItem和FoodGridItem的Image组件的共享元素转场方法(sharedTransition)。转场id为foodItem.id,转场动画时长为1s,延时0.1s播放,变化曲线为贝塞尔曲线Curves.cubicBezier(0.2, 0.2, 0.1, 1.0) ,需引入curves模块。 - - 共享转场时会携带当前元素的被设置的属性,所以创建Row组件,使其作为Image的父组件,设置背景颜色在Row上。 - - 在FoodListItem的Image组件上设置autoResize为false,因为image组件默认会根据最终展示的区域,去调整图源的大小,以优化图片渲染性能。在转场动画中,图片在放大的过程中会被重新加载,所以为了转场动画的流畅,autoResize设置为false。 - - ```ts - // FoodList.ets - import Curves from '@ohos.curves' - - @Component - struct FoodListItem { - private foodItem: FoodData - build() { - Navigator({ target: 'pages/FoodDetail' }) { - Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { - Row() { - Image(this.foodItem.image) - .objectFit(ImageFit.Contain) - .autoResize(false) - .height(40) - .width(40) - .sharedTransition(this.foodItem.id, { duration: 1000, curve: Curves.cubicBezier(0.2, 0.2, 0.1, 1.0), delay: 100 }) - } - - .margin({ right: 16 }) - Text(this.foodItem.name) - .fontSize(14) - .flexGrow(1) - Text(this.foodItem.calories + ' kcal') - .fontSize(14) - } - .height(64) - } - .params({ foodData: this.foodItem }) - .margin({ right: 24, left:32 }) - } - } - - @Component - struct FoodGridItem { - private foodItem: FoodData - build() { - Column() { - Row() { - Image(this.foodItem.image) - .objectFit(ImageFit.Contain) - .autoResize(false) - .height(152) - .width('100%') - .sharedTransition(this.foodItem.id, { duration: 1000, curve: Curves.cubicBezier(0.2, 0.2, 0.1, 1.0), delay: 100 }) - } - Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { - Text(this.foodItem.name) - .fontSize(14) - .flexGrow(1) - .padding({ left: 8 }) - Text(this.foodItem.calories + 'kcal') - .fontSize(14) - .margin({ right: 6 }) - } - .height(32) - .width('100%') - .backgroundColor('#FFe5e5e5') - } - .height(184) - .width('100%') - .onClick(() => { - router.pushUrl({ url: 'pages/FoodDetail', params: { foodData: this.foodItem } }) - }) - } - } - - - ``` - -2. 设置FoodDetail页面的FoodImageDisplay的Image组件的共享元素转场方法(sharedTransition)。设置方法同上。 - - ```ts - import Curves from '@ohos.curves' - - @Component - struct FoodImageDisplay { - private foodItem: FoodData - build() { - Stack({ alignContent: Alignment.BottomStart }) { - Image(this.foodItem.image) - .objectFit(ImageFit.Contain) - .sharedTransition(this.foodItem.id, { duration: 1000, curve: Curves.cubicBezier(0.2, 0.2, 0.1, 1.0), delay: 100 }) - Text(this.foodItem.name) - .fontSize(26) - .fontWeight(500) - .margin({ left: 26, bottom: 17.4 }) - } - .height(357) - } - } - ``` - - ![animation-feature1](figures/animation-feature1.gif) - - 通过对绘制组件和动画的学习,我们已完成了启动Logo的绘制、启动页动画和页面间的转场动画,声明式UI框架提供了丰富的动效接口,合理地应用和组合可以让应用更具有设计感。 - - diff --git a/zh-cn/application-dev/ui/ui-ts-building-category-grid-layout.md b/zh-cn/application-dev/ui/ui-ts-building-category-grid-layout.md deleted file mode 100644 index 8df2f7bd9c6d81a5fe9778a3cfeca9185e19f19c..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/ui/ui-ts-building-category-grid-layout.md +++ /dev/null @@ -1,371 +0,0 @@ -# 构建食物分类Grid布局 - -健康饮食应用在主页提供给用户两种食物显示方式:列表显示和网格显示。开发者将实现通过页签切换不同食物分类的网格布局。 - - -1. 将Category枚举类型引入FoodCategoryList页面。 - - ```ts - import { Category, FoodData } from '../model/FoodData' - ``` - -2. 创建FoodCategoryList和FoodCategory组件,其中FoodCategoryList作为新的页面入口组件,在入口组件调用initializeOnStartup方法。 - - ```ts - @Component - struct FoodList { - private foodItems: FoodData[] - build() { - ...... - } - } - - @Component - struct FoodCategory { - private foodItems: FoodData[] - build() { - ...... - } - } - - @Entry - @Component - struct FoodCategoryList { - private foodItems: FoodData[] = initializeOnStartup() - build() { - ...... - } - } - ``` - -3. 在FoodCategoryList组件内创建showList成员变量,用于控制List布局和Grid布局的渲染切换。需要用到条件渲染语句if...else...。 - - ```ts - @Entry - @Component - struct FoodCategoryList { - private foodItems: FoodData[] = initializeOnStartup() - private showList: boolean = false - - build() { - Stack() { - if (this.showList) { - FoodList({ foodItems: this.foodItems }) - } else { - FoodCategory({ foodItems: this.foodItems }) - } - } - } - } - ``` - -4. 在页面右上角创建切换List/Grid布局的图标。设置Stack对齐方式为顶部尾部对齐TopEnd,创建Image组件,设置其点击事件,即showList取反。 - - ```ts - @Entry - @Component - struct FoodCategoryList { - private foodItems: FoodData[] = initializeOnStartup() - private showList: boolean = false - - build() { - Stack({ alignContent: Alignment.TopEnd }) { - if (this.showList) { - FoodList({ foodItems: this.foodItems }) - } else { - FoodCategory({ foodItems: this.foodItems }) - } - Image($r('app.media.Switch')) - .height(24) - .width(24) - .margin({ top: 15, right: 10 }) - .onClick(() => { - this.showList = !this.showList - }) - }.height('100%') - } - } - ``` - -5. 添加@State装饰器。点击右上角的switch标签后,页面没有任何变化,这是因为showList不是有状态数据,它的改变不会触发页面的刷新。需要为其添加\@State装饰器,使其成为状态数据,它的改变会引起其所在组件的重新渲染。 - - ```ts - @Entry - @Component - struct FoodCategoryList { - private foodItems: FoodData[] = initializeOnStartup() - @State private showList: boolean = false - - build() { - Stack({ alignContent: Alignment.TopEnd }) { - if (this.showList) { - FoodList({ foodItems: this.foodItems }) - } else { - FoodCategory({ foodItems: this.foodItems }) - } - Image($r('app.media.Switch')) - .height(24) - .width(24) - .margin({ top: 15, right: 10 }) - .onClick(() => { - this.showList = !this.showList - }) - }.height('100%') - } - } - - ``` - - 点击切换图标,FoodList组件出现,再次点击,FoodList组件消失。 - - ![zh-cn_image_0000001170411978](figures/zh-cn_image_0000001170411978.gif) - -6. 创建显示所有食物的页签(All)。在FoodCategory组件内创建Tabs组件和其子组件TabContent,设置tabBar为All。设置TabBars的宽度为280,布局模式为Scrollable,即超过总长度后可以滑动。Tabs是一种可以通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图TabContent。 - - ```ts - @Component - struct FoodCategory { - private foodItems: FoodData[] - build() { - Stack() { - Tabs() { - TabContent() {}.tabBar('All') - } - .barWidth(280) - .barMode(BarMode.Scrollable) - } - } - } - ``` - - ![zh-cn_image_0000001204538065](figures/zh-cn_image_0000001204538065.png) - -7. 创建FoodGrid组件,作为TabContent的子组件。 - - ```ts - @Component - struct FoodGrid { - private foodItems: FoodData[] - build() {} - } - - @Component - struct FoodCategory { - private foodItems: FoodData[] - build() { - Stack() { - Tabs() { - TabContent() { - FoodGrid({ foodItems: this.foodItems }) - }.tabBar('All') - } - .barWidth(280) - .barMode(BarMode.Scrollable) - } - } - } - ``` - -8. 实现2 \* 6的网格布局(一共12个食物数据资源)。创建Grid组件,设置列数columnsTemplate('1fr 1fr'),行数rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr'),行间距和列间距rowsGap和columnsGap为8。创建Scroll组件,使其可以滑动。 - - ```ts - @Component - struct FoodGrid { - private foodItems: FoodData[] - build() { - Scroll() { - Grid() { - ForEach(this.foodItems, (item: FoodData) => { - GridItem() {} - }, (item: FoodData) => item.id.toString()) - } - .rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr') - .columnsTemplate('1fr 1fr') - .columnsGap(8) - .rowsGap(8) - } - .scrollBar(BarState.Off) - .padding({left: 16, right: 16}) - } - } - ``` - -9. 创建FoodGridItem组件,展示食物图片、名称和卡路里,实现其UI布局,为GridItem的子组件。每个FoodGridItem高度为184,行间距为8,设置Grid总高度为(184 + 8) \* 6 - 8 = 1144。 - - ```ts - @Component - struct FoodGridItem { - private foodItem: FoodData - build() { - Column() { - Row() { - Image(this.foodItem.image) - .objectFit(ImageFit.Contain) - .height(152) - .width('100%') - } - Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { - Text(this.foodItem.name) - .fontSize(14) - .flexGrow(1) - .padding({ left: 8 }) - Text(this.foodItem.calories + 'kcal') - .fontSize(14) - .margin({ right: 6 }) - } - .height(32) - .width('100%') - .backgroundColor('#FFe5e5e5') - } - .height(184) - .width('100%') - } - } - - @Component - struct FoodGrid { - private foodItems: FoodData[] - build() { - Scroll() { - Grid() { - ForEach(this.foodItems, (item: FoodData) => { - GridItem() { - FoodGridItem({foodItem: item}) - } - }, (item: FoodData) => item.id.toString()) - } - .rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr') - .columnsTemplate('1fr 1fr') - .columnsGap(8) - .rowsGap(8) - .height(1144) - } - .scrollBar(BarState.Off) - .padding({ left: 16, right: 16 }) - } - } - ``` - - ![zh-cn_image_0000001170167520](figures/zh-cn_image_0000001170167520.png) - -10. 创建展示蔬菜(Category.Vegetable)、水果(Category.Fruit)、坚果(Category.Nut)、海鲜(Category.SeaFood)和甜品(Category.Dessert)分类的页签。 - - ```ts - @Component - struct FoodCategory { - private foodItems: FoodData[] - - build() { - Stack() { - Tabs() { - TabContent() { - FoodGrid({ foodItems: this.foodItems }) - }.tabBar('All') - - TabContent() { - FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Vegetable)) }) - }.tabBar('Vegetable') - - TabContent() { - FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Fruit)) }) - }.tabBar('Fruit') - - TabContent() { - FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Nut)) }) - }.tabBar('Nut') - - TabContent() { - FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Seafood)) }) - }.tabBar('Seafood') - - TabContent() { - FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Dessert)) }) - }.tabBar('Dessert') - } - .barWidth(280) - .barMode(BarMode.Scrollable) - } - } - } - ``` - -11. 设置不同食物分类的Grid的行数和高度。因为不同分类的食物数量不同,所以不能用'1fr 1fr 1fr 1fr 1fr 1fr '常量来统一设置成6行。 - 创建gridRowTemplate和HeightValue成员变量,通过成员变量设置Grid行数和高度。 - - ```ts - @Component - struct FoodGrid { - private foodItems: FoodData[] - private gridRowTemplate: string = '' - private heightValue: number - - build() { - Scroll() { - Grid() { - ForEach(this.foodItems, (item: FoodData) => { - GridItem() { - FoodGridItem({ foodItem: item }) - } - }, (item: FoodData) => item.id.toString()) - } - .rowsTemplate(this.gridRowTemplate) - .columnsTemplate('1fr 1fr') - .columnsGap(8) - .rowsGap(8) - .height(this.heightValue) - } - .scrollBar(BarState.Off) - .padding({ left: 16, right: 16 }) - } - } - ``` - - 调用aboutToAppear接口计算行数(gridRowTemplate )和高度(heightValue)。 - - ```ts - aboutToAppear() { - var rows = Math.round(this.foodItems.length / 2); - this.gridRowTemplate = '1fr '.repeat(rows); - this.heightValue = rows * 192 - 8; - } - ``` - - 自定义组件提供了两个生命周期的回调接口aboutToAppear和aboutToDisappear。aboutToAppear的执行时机在创建自定义组件后,执行自定义组件build方法之前。aboutToDisappear在自定义组件销毁之前的时机执行。 - - ![zh-cn_image_0000001215113569](figures/zh-cn_image_0000001215113569.png) - - ```ts - @Component - struct FoodGrid { - private foodItems: FoodData[] - private gridRowTemplate: string = '' - private heightValue: number - - aboutToAppear() { - var rows = Math.round(this.foodItems.length / 2); - this.gridRowTemplate = '1fr '.repeat(rows); - this.heightValue = rows * 192 - 8; - } - - build() { - Scroll() { - Grid() { - ForEach(this.foodItems, (item: FoodData) => { - GridItem() { - FoodGridItem({ foodItem: item }) - } - }, (item: FoodData) => item.id.toString()) - } - .rowsTemplate(this.gridRowTemplate) - .columnsTemplate('1fr 1fr') - .columnsGap(8) - .rowsGap(8) - .height(this.heightValue) - } - .scrollBar(BarState.Off) - .padding({ left: 16, right: 16 }) - } - } - ``` - - ![zh-cn_image_0000001170008198](figures/zh-cn_image_0000001170008198.gif) \ No newline at end of file diff --git a/zh-cn/application-dev/ui/ui-ts-building-category-list-layout.md b/zh-cn/application-dev/ui/ui-ts-building-category-list-layout.md deleted file mode 100644 index 24516e9220bbf5015abff68f1306507ec5e6ded6..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/ui/ui-ts-building-category-list-layout.md +++ /dev/null @@ -1,232 +0,0 @@ -# 构建食物列表List布局 - -使用List组件和ForEach循环渲染,构建食物列表布局。 - - -1. 在pages目录新建页面FoodCategoryList.ets,将index.ets改名为FoodDetail.ets。 - -2. 新建FoodList组件作为页面入口组件,FoodListItem为其子组件。List组件是列表组件,适用于重复同类数据的展示,其子组件为ListItem,适用于展示列表中的单元。 - ```ts - @Component - struct FoodListItem { - build() {} - } - - @Entry - @Component - struct FoodList { - build() { - List() { - ListItem() { - FoodListItem() - } - } - } - } - ``` - -3. 引入FoodData类和initializeOnStartup方法。 - -应用代码中文件访问方法主要有下面两种: -- **相对路径**:使用相对路径引用代码文件,以"../"访问上一级目录,以"./"访问当前目录,也可以省略不写。 -- **绝对路径**:使用当前模块根路径引用代码文件,比如:common/utils/utils。 -这里使用相对路径访问: - - ``` - import { FoodData } from '../model/FoodData' - import { initializeOnStartup } from '../model/FoodDataModels' - ``` - -4. FoodList和FoodListItem组件数值传递。在FoodList组件内创建类型为FoodData[]成员变量foodItems,调用initializeOnStartup方法为其赋值。在FoodListItem组件内创建类型为FoodData的成员变量foodItem。将父组件foodItems数组的第一个元素的foodItems[0]作为参数传递给FoodListItem。 - ```ts - import { FoodData } from '../model/FoodData' - import { initializeOnStartup } from '../model/FoodDataModels' - - @Component - struct FoodListItem { - private foodItem: FoodData - build() {} - } - - @Entry - @Component - struct FoodList { - private foodItems: FoodData[] = initializeOnStartup() - build() { - List() { - ListItem() { - FoodListItem({ foodItem: this.foodItems[0] }) - } - } - } - } - ``` - -5. 声明子组件FoodListItem 的UI布局。创建Flex组件,包含食物图片缩略图,食物名称,和食物对应的卡路里。 - ```ts - import { FoodData } from '../model/FoodData' - import { initializeOnStartup } from '../model/FoodDataModels' - - @Component - struct FoodListItem { - private foodItem: FoodData - build() { - Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { - Image(this.foodItem.image) - .objectFit(ImageFit.Contain) - .height(40) - .width(40) - .margin({ right: 16 }) - Text(this.foodItem.name) - .fontSize(14) - .flexGrow(1) - Text(this.foodItem.calories + ' kcal') - .fontSize(14) - } - .height(64) - .margin({ right: 24, left:32 }) - } - } - - @Entry - @Component - struct FoodList { - private foodItems: FoodData[] = initializeOnStartup() - build() { - List() { - ListItem() { - FoodListItem({ foodItem: this.foodItems[0] }) - } - } - } - } - ``` - - - ![zh-cn_image_0000001204776353](figures/zh-cn_image_0000001204776353.png) - -6. 创建两个FoodListItem。在List组件创建两个FoodListItem,分别给FoodListItem传递foodItems数组的第一个元素this.foodItems[0]和第二个元素foodItem: this.foodItems[1]。 - - ```ts - import { FoodData } from '../model/FoodData' - import { initializeOnStartup } from '../model/FoodDataModels' - - @Component - struct FoodListItem { - private foodItem: FoodData - - build() { - Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { - Image(this.foodItem.image) - .objectFit(ImageFit.Contain) - .height(40) - .width(40) - .margin({ right: 16 }) - Text(this.foodItem.name) - .fontSize(14) - .flexGrow(1) - Text(this.foodItem.calories + ' kcal') - .fontSize(14) - } - .height(64) - .margin({ right: 24, left: 32 }) - } - } - - @Entry - @Component - struct FoodList { - private foodItems: FoodData[] = initializeOnStartup() - - build() { - List() { - ListItem() { - FoodListItem({ foodItem: this.foodItems[0] }) - } - - ListItem() { - FoodListItem({ foodItem: this.foodItems[1] }) - } - } - } - } - ``` - - - ![zh-cn_image1_0000001204776353](figures/zh-cn_image1_0000001204776353.png) - -7. 单独创建每一个FoodListItem肯定是不合理的,这就需要引入[ForEach循环渲染](../quick-start/arkts-rendering-control.md#循环渲染)。 - - ```ts - import { FoodData } from '../model/FoodData' - import { initializeOnStartup } from '../model/FoodDataModels' - - @Component - struct FoodListItem { - private foodItem: FoodData - build() { - Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { - Image(this.foodItem.image) - .objectFit(ImageFit.Contain) - .height(40) - .width(40) - .margin({ right: 16 }) - Text(this.foodItem.name) - .fontSize(14) - .flexGrow(1) - Text(this.foodItem.calories + ' kcal') - .fontSize(14) - } - .height(64) - .margin({ right: 24, left:32 }) - } - } - - @Entry - @Component - struct FoodList { - private foodItems: FoodData[] = initializeOnStartup() - build() { - List() { - ForEach(this.foodItems, item => { - ListItem() { - FoodListItem({ foodItem: item }) - } - }, item => item.id.toString()) - } - } - } - ``` - -8. 添加FoodList标题。 - - ``` - @Entry - @Component - struct FoodList { - private foodItems: FoodData[] = initializeOnStartup() - - build() { - Column() { - Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { - Text('Food List') - .fontSize(20) - .margin({ left: 20 }) - } - .height('7%') - .backgroundColor('#FFf1f3f5') - - List() { - ForEach(this.foodItems, item => { - ListItem() { - FoodListItem({ foodItem: item }) - } - }, item => item.id.toString()) - } - .height('93%') - } - } - } - ``` - - ![zh-cn_image_0000001169678922](figures/zh-cn_image_0000001169678922.png) \ No newline at end of file diff --git a/zh-cn/application-dev/ui/ui-ts-building-data-model.md b/zh-cn/application-dev/ui/ui-ts-building-data-model.md deleted file mode 100644 index c2fe6aa7c739a19db3a450a17f055150cf5d28b9..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/ui/ui-ts-building-data-model.md +++ /dev/null @@ -1,90 +0,0 @@ -# 构建食物数据模型 - -在创建视图中,我们逐一去表述食物的各个信息,如食物名称、卡路里、蛋白质、脂肪、碳水和维生素C。这样的编码形式在实际的开发中肯定是不切实际的,所以要创建食物数据模型来统一存储和管理数据。 - - -![zh-cn_image_0000001215433095](figures/zh-cn_image_0000001215433095.png) - - -1. 新建model文件夹,在model目录下创建FoodData.ets。 - - ![zh-cn_image_0000001195119619](figures/zh-cn_image_0000001195119619.png) - -2. 定义食物数据的存储模型FoodData和枚举变量Category,FoodData类包含食物id、名称(name)、分类(category)、图片(image)、热量(calories)、蛋白质(protein)、脂肪(fat)、碳水(carbohydrates)和维生素C(vitaminC)属性。 - ArkTS语言是在ts语言的基础上的扩展,同样支持ts语法。 - - ```ts - enum Category { - Fruit, - Vegetable, - Nut, - Seafood, - Dessert - } - - let NextId = 0; - class FoodData { - id: string; - name: string; - image: Resource; - category: Category; - calories: number; - protein: number; - fat: number; - carbohydrates: number; - vitaminC: number; - - constructor(name: string, image: Resource, category: Category, calories: number, protein: number, fat: number, carbohydrates: number, vitaminC: number) { - this.id = `${ NextId++ }`; - this.name = name; - this.image = image; - this.category = category; - this.calories = calories; - this.protein = protein; - this.fat = fat; - this.carbohydrates = carbohydrates; - this.vitaminC = vitaminC; - } - } - ``` - -3. 存入食物图片资源。在resources >base> media目录下存入食物图片资源,图片名称为食物名称。 - -4. 创建食物资源数据。在model文件夹下创建FoodDataModels.ets,在该页面中声明食物成分数组FoodComposition。以下示例创建了两个食物数据。 - - ```ts - const FoodComposition: any[] = [ - { 'name': 'Tomato', 'image': $r('app.media.Tomato'), 'category': Category.Vegetable, 'calories': 17, 'protein': 0.9, 'fat': 0.2, 'carbohydrates': 3.9, 'vitaminC': 17.8 }, - { 'name': 'Walnut', 'image': $r('app.media.Walnut'), 'category': Category.Nut, 'calories': 654 , 'protein': 15, 'fat': 65, 'carbohydrates': 14, 'vitaminC': 1.3 } - ] - ``` - - 实际开发中,开发者可以自定义更多的数据资源,当食物资源很多时,建议使用[数据懒加载LazyForEach](../quick-start/arkts-rendering-control.md#数据懒加载)。 - -5. 创建initializeOnStartUp方法来初始化FoodData的数组。在FoodDataModels.ets中使用了定义在FoodData.ets的FoodData和Category,所以要将FoodData.ets的FoodData类export,在FoodDataModels.ets内import FoodData和Category。 - ```ts - // FoodData.ets - export enum Category { - ...... - } - export class FoodData { - ...... - } - // FoodDataModels.ets - import { Category, FoodData } from './FoodData' - - export function initializeOnStartup(): Array { - let FoodDataArray: Array = [] - FoodComposition.forEach(item => { - FoodDataArray.push(new FoodData(item.name, item.image, item.category, item.calories, item.protein, item.fat, item.carbohydrates, item.vitaminC )); - }) - return FoodDataArray; - } - ``` - - -已完成好健康饮食应用的数据资源准备,接下来将通过加载这些数据来创建食物列表页面。 - -## 相关实例 -针对构建食物分类列表页面和食物详情页,有以下相关实例可供参考: -- [DefiningPageLayoutAndConnection:页面布局和连接(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/master/ETSUI/DefiningPageLayoutAndConnection) diff --git a/zh-cn/application-dev/ui/ui-ts-components-intro.md b/zh-cn/application-dev/ui/ui-ts-components-intro.md deleted file mode 100644 index 5994b890582b097ae73d3cc706b8cb0a8878f4c2..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/ui/ui-ts-components-intro.md +++ /dev/null @@ -1,36 +0,0 @@ -# 常用组件说明 - -组件是构建页面的核心,每个组件通过对数据和方法的简单封装,实现独立的可视、可交互功能单元。组件之间相互独立,随取随用,也可以在需求相同的地方重复使用。 - -声明式开发范式目前可供选择的组件如下表所示。 - -| 组件类型 | 主要组件 | -| ---------------------------- | ---------------------------------------------------------- | -| [基础组件](../reference/arkui-ts/ts-basic-components-blank.md) | Blank、Button、Checkbox、CheckboxGroup、DataPanel、DatePicker、Divider、Gauge、Image、ImageAnimator、LoadingProgress、Marquee、Navigation、PatternLock、PluginComponent、Progress、QRCode、Radio、Rating、RemoteWindow、RichText、ScrollBar、Search、Select、Slider、Span、Stepper、StepperItem、Text、TextArea、TextClock、TextInput、TextPicker、TextTimer、TimePicker、Toggle、Web、XComponent | -| [容器组件](../reference/arkui-ts/ts-container-ability-component.md) | AbilityComponent、AlphabetIndexer、Badge、Column、ColumnSplit、Counter、Flex、FlowItem、GridContainer、GridCol、GridRow、Grid、GridItem、List、ListItem、ListItemGroup、Navigator、Panel、Refresh、RelativeContainer、Row、RowSplit、Scroll、SideBarContainer、Stack、Swiper、Tabs、TabContent、WaterFlow | -| [媒体组件](../reference/arkui-ts/ts-media-components-video.md) | Video | -| [绘制组件](../reference/arkui-ts/ts-drawing-components-circle.md) | Circle、Ellipse、Line、Polyline、Polygon、Path、Rect、Shape | -| [画布组件](../reference/arkui-ts/ts-components-canvas-canvas.md) | Canvas | - - -## 相关实例 - -基于ArkTS的常用组件开发,有以下相关实例可供参考: - -- [`ComponentCollection`:组件集合(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/UI/ArkTsComponentClollection/ComponentCollection) - -- [`OrangeShopping`:购物示例应用(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/Solutions/Shopping/OrangeShopping) - -- [`Canvas`:画布组件(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/master/ETSUI/Canvas) - -- [`ArkTSClock`:简单时钟(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/Solutions/Tools/ArkTSClock) - -- [`PatternLock`:图案密码锁组件(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/Solutions/Tools/PatternLock) - -- [`Search`:Search组件(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/master/ETSUI/Search) - -- [`Gallery`:组件集合(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/master/ETSUI/Gallery) - -- [List组件的使用之商品列表(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/List_HDC) - -- [List组件的使用之设置项(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/List_HDC) \ No newline at end of file diff --git a/zh-cn/application-dev/ui/ui-ts-components-web.md b/zh-cn/application-dev/ui/ui-ts-components-web.md deleted file mode 100644 index a23282f45dd6564051bcd1043cd0c578fda415b2..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/ui/ui-ts-components-web.md +++ /dev/null @@ -1,273 +0,0 @@ -# Web - -Web是提供网页显示能力的组件,具体用法请参考 [Web API](../reference/arkui-ts/ts-basic-components-web.md)。 - -## 创建组件 - -在main/ets/entryability/pages目录下的ets文件中创建一个Web组件。在web组件中通过src指定引用的网页路径,controller为组件的控制器,通过controller绑定Web组件,用于调用Web组件的方法。 - - ```ts - // xxx.ets - import web_webview from '@ohos.web.webview'; - - @Entry - @Component - struct WebComponent { - controller: web_webview.WebviewController = new web_webview.WebviewController(); - build() { - Column() { - Web({ src: 'https://www.example.com', controller: this.controller }) - } - } - } - ``` - -## 设置样式和属性 - -Web组件的使用需要添加丰富的页面样式和功能属性。设置height、padding样式可为Web组件添加高和内边距,设置fileAccess属性可为Web组件添加文件访问权限,设置javaScriptAccess属性为true使Web组件具有执行JavaScript代码的能力。 - -```ts -// xxx.ets -import web_webview from '@ohos.web.webview'; - -@Entry -@Component -struct WebComponent { - fileAccess: boolean = true; - controller: web_webview.WebviewController = new web_webview.WebviewController(); - build() { - Column() { - Text('Hello world!') - .fontSize(20) - Web({ src: 'https://www.example.com', controller: this.controller }) - // 设置高和内边距 - .height(500) - .padding(20) - // 设置文件访问权限和脚本执行权限 - .fileAccess(this.fileAccess) - .javaScriptAccess(true) - Text('End') - .fontSize(20) - } - } -} -``` -## 添加事件和方法 - -为Web组件添加onProgressChange事件,该事件回调Web引擎加载页面的进度值。将该进度值赋值给Progress组件的value,控制Progress组件的状态。当进度为100%时隐藏Progress组件,Web页面加载完成。 - -```ts -// xxx.ets -import web_webview from '@ohos.web.webview'; - -@Entry -@Component -struct WebComponent { - @State progress: number = 0; - @State hideProgress: boolean = true; - fileAccess: boolean = true; - controller: web_webview.WebviewController = new web_webview.WebviewController(); - build() { - Column() { - Text('Hello world!') - .fontSize(20) - Progress({value: this.progress, total: 100}) - .color('#0000ff') - .visibility(this.hideProgress ? Visibility.None : Visibility.Visible) - Web({ src: 'https://www.example.com', controller: this.controller }) - .fileAccess(this.fileAccess) - .javaScriptAccess(true) - .height(500) - .padding(20) - // 添加onProgressChange事件 - .onProgressChange(e => { - this.progress = e.newProgress; - // 当进度100%时,进度条消失 - if (this.progress === 100) { - this.hideProgress = true; - } else { - this.hideProgress = false; - } - }) - Text('End') - .fontSize(20) - } - } -} -``` -在onPageEnd事件中添加runJavaScript方法。onPageEnd事件是网页加载完成时的回调,runJavaScript方法可以执行HTML中的JavaScript脚本。当页面加载完成时,触发onPageEnd事件,调用HTML文件中的test方法,在控制台打印信息。 - -```ts -// xxx.ets -import web_webview from '@ohos.web.webview'; - -@Entry -@Component -struct WebComponent { - @State progress: number = 0; - @State hideProgress: boolean = true; - @State webResult: string = '' - fileAccess: boolean = true; - // 定义Web组件的控制器controller - controller: web_webview.WebviewController = new web_webview.WebviewController(); - build() { - Column() { - Text('Hello world!') - .fontSize(20) - Progress({value: this.progress, total: 100}) - .color('#0000ff') - .visibility(this.hideProgress ? Visibility.None : Visibility.Visible) - // 初始化Web组件,并绑定controller - Web({ src: $rawfile('index.html'), controller: this.controller }) - .fileAccess(this.fileAccess) - .javaScriptAccess(true) - .height(500) - .padding(20) - .onProgressChange(e => { - this.progress = e.newProgress; - if (this.progress === 100) { - this.hideProgress = true; - } else { - this.hideProgress = false; - } - }) - .onPageEnd(e => { - // test()在index.html中定义 - try { - this.controller.runJavaScript( - 'test()', - (error, result) => { - if (error) { - console.info(`run JavaScript error: ` + JSON.stringify(error)) - return; - } - if (result) { - this.webResult = result - console.info(`The test() return value is: ${result}`) - } - }); - console.info('url: ', e.url); - } catch (error) { - console.error(`ErrorCode: ${error.code}, Message: ${error.message}`); - } - }) - Text('End') - .fontSize(20) - } - } -} -``` - -在main/resources/rawfile目录下创建一个HTML文件。 - -```html - - - - - - Hello world! - - - -``` - -## 开启网页调试 -在PC上启用端口转发,以及设置Web组件属性webDebuggingAccess为true后,便可以在PC上调试通过USB连接的开发设备上的Web组件里的网页。 - -设置步骤如下: - -1、首先设置Web组件属性webDebuggingAccess为true。 - ```ts - // xxx.ets - import web_webview from '@ohos.web.webview'; - - @Entry - @Component - struct WebComponent { - controller: web_webview.WebviewController = new web_webview.WebviewController(); - build() { - Column() { - Web({ src: 'www.example.com', controller: this.controller }) - .webDebuggingAccess(true) // true表示启用调试功能 - } - } - } - ``` - -2、PC上启用端口转发功能,添加TCP端口9222映射。 - ```ts - hdc fport tcp:9222 tcp:9222 - ``` - 添加是否成功可以通过如下命令来查看已存在的映射关系表。 - ```ts - hdc fport ls - ``` -如上设置完成后,首先打开应用Web组件、访问要调试的网页,然后在PC上使用chrome浏览器访问:http://localhost:9222, 就可以在PC上调试开发设备刚才访问的网页。 - -## 场景示例 - -该场景实现了Web组件中视频的动态播放。首先在HTML页面内嵌入视频资源,再使用Web组件的控制器调用onActive和onInactive方法激活和暂停页面渲染。点击onInactive按钮,Web页面停止渲染,视频暂停播放;点击onActive按钮,激活Web组件,视频继续播放。 - - ```ts - // xxx.ets -import web_webview from '@ohos.web.webview'; - -@Entry -@Component -struct WebComponent { - controller: web_webview.WebviewController = new web_webview.WebviewController(); - build() { - Column() { - Row() { - Button('onActive').onClick(() => { - console.info("Web Component onActive"); - try { - this.controller.onActive(); - } catch (error) { - console.error(`Errorcode: ${error.code}, Message: ${error.message}`); - } - }) - Button('onInactive').onClick(() => { - console.info("Web Component onInactive"); - try { - this.controller.onInactive(); - } catch (error) { - console.error(`Errorcode: ${error.code}, Message: ${error.message}`); - } - }) - } - Web({ src: $rawfile('index.html'), controller: this.controller }) - .fileAccess(true) - } - } -} - ``` - - ```html - - - - - - - - - ``` - -## 相关实例 - -针对Web开发,有以下相关实例可供参考: - -- [`Browser`:浏览器(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/BasicFeature/Web/Browser) - -- [Web组件的使用(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/WebCookie) - -- [Web组件抽奖案例(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/WebComponent) diff --git a/zh-cn/application-dev/ui/ui-ts-creating-simple-page.md b/zh-cn/application-dev/ui/ui-ts-creating-simple-page.md deleted file mode 100644 index 9d97b81f9cdf635bdf9abd14c6389c6fbd3f1b99..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/ui/ui-ts-creating-simple-page.md +++ /dev/null @@ -1,519 +0,0 @@ -# 创建简单视图 - -在这一小节中,我们将开始食物详情页的开发,学习如何通过容器组件Stack、Flex和基础组件Image、Text,构建用户自定义组件,完成图文并茂的食物介绍。 - -在创建页面前,请先创建ArkTS工程,Stage模型请参考[创建Stage模型的ArkTS工程](../quick-start/start-with-ets-stage.md#创建arkts工程),FA模型请参考[创建FA模型的ArkTS工程](../quick-start/start-with-ets-fa.md#创建arkts工程)。 - - -## 构建Stack布局 - -1. 创建食物名称。 - - 在index.ets文件中,创建Stack组件,将Text组件放进Stack组件的花括号中,使其成为Stack组件的子组件。Stack组件为堆叠组件,可以包含一个或多个子组件,其特点是后一个子组件覆盖前一个子组件。 - - ```ts - @Entry - @Component - struct MyComponent { - build() { - Stack() { - Text('Tomato') - .fontSize(26) - .fontWeight(500) - } - } - } - ``` - - ![zh-cn_image_0000001214128687](figures/zh-cn_image_0000001214128687.png) - -2. 食物图片展示。 - 创建Image组件,指定Image组件的url,Image组件是必选构造参数组件。为了让Text组件在Image组件上方显示,所以要先声明Image组件。图片资源放在resources下的rawfile文件夹内,引用rawfile下资源时使用`$rawfile('filename')`的形式,filename为rawfile目录下的文件相对路径。当前`$rawfile`仅支持Image控件引用图片资源。 - - ```ts - @Entry - @Component - struct MyComponent { - build() { - Stack() { - Image($rawfile('Tomato.png')) - Text('Tomato') - .fontSize(26) - .fontWeight(500) - } - } - } - ``` - - - ![zh-cn_image_0000001168410342](figures/zh-cn_image_0000001168410342.png) - -3. 通过资源访问图片。 - 除指定图片路径外,也可以使用引用媒体资源符$r引用资源,需要遵循resources文件夹的资源限定词的规则。右键resources文件夹,点击New>Resource Directory,选择Resource Type为Media(图片资源)。 - - 将Tomato.png放入media文件夹内。就可以通过`$r('app.type.name')`的形式引用应用资源,即`$r('app.media.Tomato')`。 - - ```ts - @Entry - @Component - struct MyComponent { - build() { - Stack() { - Image($r('app.media.Tomato')) - .objectFit(ImageFit.Contain) - .height(357) - Text('Tomato') - .fontSize(26) - .fontWeight(500) - } - } - } - ``` - -4. 设置Image宽高,并且将image的objectFit属性设置为ImageFit.Contain,即保持图片长宽比的情况下,使得图片完整地显示在边界内。 - 如果Image填满了整个屏幕,原因如下: - 1. Image没有设置宽高。 - - 2. Image的objectFit默认属性是ImageFit.Cover,即在保持长宽比的情况下放大或缩小,使其填满整个显示边界。 - - ```ts - @Entry - @Component - struct MyComponent { - build() { - Stack() { - Image($r('app.media.Tomato')) - .objectFit(ImageFit.Contain) - .height(357) - Text('Tomato') - .fontSize(26) - .fontWeight(500) - } - } - } - ``` - - ![zh-cn_image_0000001214210217](figures/zh-cn_image_0000001214210217.png) - -5. 设置食物图片和名称布局。设置Stack的对齐方式为底部起始端对齐,Stack默认为居中对齐。设置Stack构造参数alignContent为Alignment.BottomStart。其中Alignment和FontWeight一样,都是框架提供的内置枚举类型。 - - ```ts - @Entry - @Component - struct MyComponent { - build() { - Stack({ alignContent: Alignment.BottomStart }) { - Image($r('app.media.Tomato')) - .objectFit(ImageFit.Contain) - .height(357) - Text('Tomato') - .fontSize(26) - .fontWeight(500) - } - } - } - ``` - - ![zh-cn_image_0000001168728872](figures/zh-cn_image_0000001168728872.png) - -6. 调整Text组件的外边距margin,使其距离左侧和底部有一定的距离。margin是简写属性,可以统一指定四个边的外边距,也可以分别指定。具体设置方式如下: - - 1. 参数为Length时,即统一指定四个边的外边距,比如margin(20),即上、右、下、左四个边的外边距都是20。 - 2. 参数为{top?: Length, right?: Length, bottom?: Length, left?:Length},即分别指定四个边的边距,比如margin({ left: 26, bottom: 17.4 }),即左边距为26,下边距为17.4。 - - ```ts - @Entry - @Component - struct MyComponent { - build() { - Stack({ alignContent: Alignment.BottomStart }) { - Image($r('app.media.Tomato')) - .objectFit(ImageFit.Contain) - .height(357) - Text('Tomato') - .fontSize(26) - .fontWeight(500) - .margin({left: 26, bottom: 17.4}) - } - } - } - ``` - - ![zh-cn_image_0000001213968747](figures/zh-cn_image_0000001213968747.png) - -7. 调整组件间的结构,语义化组件名称。创建页面入口组件为FoodDetail,在FoodDetail中创建Column,设置水平方向上居中对齐 alignItems(HorizontalAlign.Center)。MyComponent组件名改为FoodImageDisplay,为FoodDetail的子组件。 - - Column是子组件竖直排列的容器组件,本质为线性布局,所以只能设置交叉轴方向的对齐。 - - ```ts - @Component - struct FoodImageDisplay { - build() { - Stack({ alignContent: Alignment.BottomStart }) { - Image($r('app.media.Tomato')) - .objectFit(ImageFit.Contain) - Text('Tomato') - .fontSize(26) - .fontWeight(500) - .margin({ left: 26, bottom: 17.4 }) - } - .height(357) - } - } - - @Entry - @Component - struct FoodDetail { - build() { - Column() { - FoodImageDisplay() - } - .alignItems(HorizontalAlign.Center) - } - } - ``` - -## 构建Flex布局 - -开发者可以使用Flex弹性布局来构建食物的食物成分表,弹性布局在本场景的优势在于可以免去多余的宽高计算,通过比例来设置不同单元格的大小,更加灵活。 - -1. 创建ContentTable组件,使其成为页面入口组件FoodDetail的子组件。 - - ```ts - @Component - struct FoodImageDisplay { - build() { - Stack({ alignContent: Alignment.BottomStart }) { - Image($r('app.media.Tomato')) - .objectFit(ImageFit.Contain) - .height(357) - Text('Tomato') - .fontSize(26) - .fontWeight(500) - .margin({ left: 26, bottom: 17.4 }) - } - } - } - - @Component - struct ContentTable { - build() { - } - } - - @Entry - @Component - struct FoodDetail { - build() { - Column() { - FoodImageDisplay() - ContentTable() - } - .alignItems(HorizontalAlign.Center) - } - } - ``` - -2. 创建Flex组件展示Tomato两类成分。 - 一类是热量Calories,包含卡路里(Calories);一类是营养成分Nutrition,包含蛋白质(Protein)、脂肪(Fat)、碳水化合物(Carbohydrates)和维生素C(VitaminC)。 - - 先创建热量这一类。创建Flex组件,高度为280,上、右、左内边距为30,包含三个Text子组件分别代表类别名(Calories),含量名称(Calories)和含量数值(17kcal)。Flex组件默认为水平排列方式。 - - 已省略FoodImageDisplay代码,只针对ContentTable进行扩展。 - - ```ts - @Component - struct ContentTable { - build() { - Flex() { - Text('Calories') - .fontSize(17.4) - .fontWeight(FontWeight.Bold) - Text('Calories') - .fontSize(17.4) - Text('17kcal') - .fontSize(17.4) - } - .height(280) - .padding({ top: 30, right: 30, left: 30 }) - } - } - - @Entry - @Component - struct FoodDetail { - build() { - Column() { - FoodImageDisplay() - ContentTable() - } - .alignItems(HorizontalAlign.Center) - } - } - ``` - - - ![zh-cn_image_0000001169759552](figures/zh-cn_image_0000001169759552.png) - -3. 调整布局,设置各部分占比。分类名占比(layoutWeight)为1,成分名和成分含量一共占比(layoutWeight)2。成分名和成分含量位于同一个Flex中,成分名占据所有剩余空间flexGrow(1)。 - - ```ts - @Component - struct FoodImageDisplay { - build() { - Stack({ alignContent: Alignment.BottomStart }) { - Image($r('app.media.Tomato')) - .objectFit(ImageFit.Contain) - .height(357) - Text('Tomato') - .fontSize(26) - .fontWeight(500) - .margin({ left: 26, bottom: 17.4 }) - } - } - } - - @Component - struct ContentTable { - build() { - Flex() { - Text('Calories') - .fontSize(17.4) - .fontWeight(FontWeight.Bold) - .layoutWeight(1) - Flex() { - Text('Calories') - .fontSize(17.4) - .flexGrow(1) - Text('17kcal') - .fontSize(17.4) - } - .layoutWeight(2) - } - .height(280) - .padding({ top: 30, right: 30, left: 30 }) - } - } - - @Entry - @Component - struct FoodDetail { - build() { - Column() { - FoodImageDisplay() - ContentTable() - } - .alignItems(HorizontalAlign.Center) - } - } - ``` - - ![zh-cn_image_0000001215079443](figures/zh-cn_image_0000001215079443.png) - -4. 仿照热量分类创建营养成分分类。营养成分部分(Nutrition)包含:蛋白质(Protein)、脂肪(Fat)、碳水化合物(Carbohydrates)和维生素C(VitaminC)四个成分,后三个成分在表格中省略分类名,用空格代替。 - 设置外层Flex为竖直排列FlexDirection.Column, 在主轴方向(竖直方向)上等距排列FlexAlign.SpaceBetween,在交叉轴方向(水平轴方向)上首部对齐排列ItemAlign.Start。 - - ```ts - @Component - struct ContentTable { - build() { - Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) { - Flex() { - Text('Calories') - .fontSize(17.4) - .fontWeight(FontWeight.Bold) - .layoutWeight(1) - Flex() { - Text('Calories') - .fontSize(17.4) - .flexGrow(1) - Text('17kcal') - .fontSize(17.4) - } - .layoutWeight(2) - } - Flex() { - Text('Nutrition') - .fontSize(17.4) - .fontWeight(FontWeight.Bold) - .layoutWeight(1) - Flex() { - Text('Protein') - .fontSize(17.4) - .flexGrow(1) - Text('0.9g') - .fontSize(17.4) - } - .layoutWeight(2) - } - Flex() { - Text(' ') - .fontSize(17.4) - .fontWeight(FontWeight.Bold) - .layoutWeight(1) - Flex() { - Text('Fat') - .fontSize(17.4) - .flexGrow(1) - Text('0.2g') - .fontSize(17.4) - } - .layoutWeight(2) - } - Flex() { - Text(' ') - .fontSize(17.4) - .fontWeight(FontWeight.Bold) - .layoutWeight(1) - Flex() { - Text('Carbohydrates') - .fontSize(17.4) - .flexGrow(1) - Text('3.9g') - .fontSize(17.4) - } - .layoutWeight(2) - } - Flex() { - Text(' ') - .fontSize(17.4) - .fontWeight(FontWeight.Bold) - .layoutWeight(1) - Flex() { - Text('vitaminC') - .fontSize(17.4) - .flexGrow(1) - Text('17.8mg') - .fontSize(17.4) - } - .layoutWeight(2) - } - } - .height(280) - .padding({ top: 30, right: 30, left: 30 }) - } - } - - @Entry - @Component - struct FoodDetail { - build() { - Column() { - FoodImageDisplay() - ContentTable() - } - .alignItems(HorizontalAlign.Center) - } - } - ``` - - ![zh-cn_image_0000001215199399](figures/zh-cn_image_0000001215199399.png) - -5. 使用自定义构造函数\@Builder简化代码。可以发现,每个成分表中的成分单元其实都是一样的UI结构。 - ![zh-cn_image_0000001169599582](figures/zh-cn_image_0000001169599582.png) - - 当前对每个成分单元都进行了声明,造成了代码的重复和冗余。可以使用\@Builder来构建自定义方法,抽象出相同的UI结构声明。\@Builder修饰的方法和Component的build方法都是为了声明一些UI渲染结构,遵循一样的ArkTS语法。开发者可以定义一个或者多个\@Builder修饰的方法,但Component的build方法必须只有一个。 - - 在ContentTable内声明\@Builder修饰的IngredientItem方法,用于声明分类名、成分名称和成分含量UI描述。 - - ```ts - @Component - struct ContentTable { - @Builder IngredientItem(title:string, name: string, value: string) { - Flex() { - Text(title) - .fontSize(17.4) - .fontWeight(FontWeight.Bold) - .layoutWeight(1) - Flex({ alignItems: ItemAlign.Center }) { - Text(name) - .fontSize(17.4) - .flexGrow(1) - Text(value) - .fontSize(17.4) - } - .layoutWeight(2) - } - } - } - ``` - - 在ContentTable的build方法内调用IngredientItem接口,需要用this去调用该Component作用域内的方法,以此来区分全局的方法调用。 - - ```ts - @Component - struct ContentTable { - ...... - build() { - Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) { - this.IngredientItem('Calories', 'Calories', '17kcal') - this.IngredientItem('Nutrition', 'Protein', '0.9g') - this.IngredientItem('', 'Fat', '0.2g') - this.IngredientItem('', 'Carbohydrates', '3.9g') - this.IngredientItem('', 'VitaminC', '17.8mg') - } - .height(280) - .padding({ top: 30, right: 30, left: 30 }) - } - } - ``` - - ContentTable组件整体代码如下。 - - ```ts - @Component - struct ContentTable { - @Builder IngredientItem(title:string, name: string, value: string) { - Flex() { - Text(title) - .fontSize(17.4) - .fontWeight(FontWeight.Bold) - .layoutWeight(1) - Flex() { - Text(name) - .fontSize(17.4) - .flexGrow(1) - Text(value) - .fontSize(17.4) - } - .layoutWeight(2) - } - } - - build() { - Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) { - this.IngredientItem('Calories', 'Calories', '17kcal') - this.IngredientItem('Nutrition', 'Protein', '0.9g') - this.IngredientItem('', 'Fat', '0.2g') - this.IngredientItem('', 'Carbohydrates', '3.9g') - this.IngredientItem('', 'VitaminC', '17.8mg') - } - .height(280) - .padding({ top: 30, right: 30, left: 30 }) - } - } - - @Entry - @Component - struct FoodDetail { - build() { - Column() { - FoodImageDisplay() - ContentTable() - } - .alignItems(HorizontalAlign.Center) - } - } - ``` - - ![zh-cn_image_0000001215199399](figures/zh-cn_image_0000001215199399.png) - -通过学习Stack布局和Flex布局已完成食物的图文展示和营养成分表,构建出第一个普通视图的食物详情页。在下一个章节中,将开发食物分类列表页,并完成食物分类列表页面和食物详情页面的跳转和数据传递,现在我们就进入下一个章节的学习吧。 - -## 相关实例 - -针对创建简单视图,有以下示例工程可供参考: - -- [`BuildCommonView`:创建简单视图(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/master/ETSUI/BuildCommonView) - - 本示例为构建了简单页面展示食物番茄的图片和营养信息,主要为了展示简单页面的Stack布局和Flex布局。 diff --git a/zh-cn/application-dev/ui/ui-ts-custom-component-lifecycle-callbacks.md b/zh-cn/application-dev/ui/ui-ts-custom-component-lifecycle-callbacks.md deleted file mode 100644 index e7ceeaf716393984877a77d34ee9d53c2608e62c..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/ui/ui-ts-custom-component-lifecycle-callbacks.md +++ /dev/null @@ -1,239 +0,0 @@ -# 自定义组件的生命周期 - -自定义组件的生命周期回调函数用于通知用户该自定义组件的生命周期,这些回调函数是私有的,在运行时由开发框架在特定的时间进行调用,不能从应用程序中手动调用这些回调函数。 - -> **说明:** -> -> - 允许在生命周期函数中使用Promise和异步回调函数,比如网络资源获取,定时器设置等; -> -> - 不允许在生命周期函数中使用async await。 - - -## aboutToAppear - -aboutToAppear?(): void - -aboutToAppear函数在创建自定义组件的新实例后,在执行其build函数之前执行。允许在aboutToAppear函数中改变状态变量,更改将在后续执行build函数中生效。 - -从API version 9开始,该接口支持在ArkTS卡片中使用。 - -## aboutToDisappear - -aboutToDisappear?(): void - -aboutToDisappear函数在自定义组件析构销毁之前执行。不允许在aboutToDisappear函数中改变状态变量,特别是@Link变量的修改可能会导致应用程序行为不稳定。 - -从API version 9开始,该接口支持在ArkTS卡片中使用。 - -**示例1:** - -```ts -// xxx.ets -@Entry -@Component -struct CountDownTimerComponent { - @State countDownFrom: number = 10 - private timerId: number = -1 - - aboutToAppear(): void { - this.timerId = setInterval(() => { - if (this.countDownFrom <= 1) { - clearTimeout(this.timerId) // countDownFrom小于等于1时清除计时器 - } - this.countDownFrom -= 1 - }, 1000) // countDownFrom每1s减1 - } - - aboutToDisappear(): void { - if (this.timerId > 0) { - clearTimeout(this.timerId) - this.timerId = -1 - } - } - - build() { - Column() { - Text(`${this.countDownFrom} sec left`) - .fontSize(30) - .margin(30) - }.width('100%') - } -} -``` - -![aboutToAppear](figures/aboutToAppear.gif) - -上述示例表明,生命周期函数对于允许CountDownTimerComponent管理其计时器资源至关重要,类似的函数也包括异步从网络请求加载资源。 - -## onPageShow - -onPageShow?(): void - -页面每次显示时触发一次,包括路由过程、应用进入前后台等场景,仅@Entry修饰的自定义组件生效。 - -## onPageHide - -onPageHide?(): void - -页面每次隐藏时触发一次,包括路由过程、应用进入前后台等场景,仅@Entry修饰的自定义组件生效。 - -## onBackPress - -onBackPress?(): void - -当用户点击返回按钮时触发,仅@Entry修饰的自定义组件生效。 - -**示例2:** - -```ts -// xxx.ets -@Entry -@Component -struct IndexComponent { - @State textColor: Color = Color.Black - - onPageShow() { - this.textColor = Color.Blue - console.info('IndexComponent onPageShow') - } - - onPageHide() { - this.textColor = Color.Transparent - console.info('IndexComponent onPageHide') - } - - onBackPress() { - this.textColor = Color.Red - console.info('IndexComponent onBackPress') - } - - build() { - Column() { - Text('Hello World') - .fontColor(this.textColor) - .fontSize(30) - .margin(30) - }.width('100%') - } -} -``` - -![lifecycle](figures/lifecycle.PNG) - -## onLayout9+ - -onLayout?(children: Array\, constraint: ConstraintSizeOptions): void - -框架会在自定义组件布局时,将该自定义组件的子节点信息和自身的尺寸范围通过onLayout传递给该自定义组件。不允许在onLayout函数中改变状态变量。 - -该接口支持在ArkTS卡片中使用。 - -**参数:** - -| 参数名 | 类型 | 说明 | -| ---------- | -------------------------------------------------------------------------------- | ---------------------- | -| children | Array\<[LayoutChild](#layoutchild9)\> | 子组件布局信息。 | -| constraint | [ConstraintSizeOptions](../reference/arkui-ts/ts-types.md#constraintsizeoptions) | 父组件constraint信息。 | - -## onMeasure9+ - -onMeasure?(children: Array\, constraint: ConstraintSizeOptions): void - -框架会在自定义组件确定尺寸时,将该自定义组件的子节点信息和自身的尺寸范围通过onMeasure传递给该自定义组件。不允许在onMeasure函数中改变状态变量。 - -该接口支持在ArkTS卡片中使用。 - -**参数:** - -| 参数名 | 类型 | 说明 | -| ---------- | -------------------------------------------------------------------------------- | ---------------------- | -| children | Array\<[LayoutChild](#layoutchild9)\> | 子组件布局信息。 | -| constraint | [ConstraintSizeOptions](../reference/arkui-ts/ts-types.md#constraintsizeoptions) | 父组件constraint信息。 | - -## LayoutChild9+ - -子组件布局信息。 - -该接口支持在ArkTS卡片中使用。 - -| 参数 | 参数类型 | 描述 | -| ---------- | ----------------------------------------------------------------------------------------------------------- | -------------------------------------- | -| name | string | 子组件名称。 | -| id | string | 子组件id。 | -| constraint | [ConstraintSizeOptions](../reference/arkui-ts/ts-types.md#constraintsizeoptions) | 子组件约束尺寸。 | -| borderInfo | [LayoutBorderInfo](#layoutborderinfo9) | 子组件border信息。 | -| position | [Position](../reference/arkui-ts/ts-types.md#position) | 子组件位置坐标。 | -| measure | (childConstraint: [ConstraintSizeOptions](../reference/arkui-ts/ts-types.md#constraintsizeoptions)) => void | 调用此方法对子组件的尺寸范围进行限制。 | -| layout | (LayoutInfo: [LayoutInfo](#layoutinfo9)) => void | 调用此方法对子组件的位置信息进行限制。 | - -## LayoutBorderInfo9+ - -子组件border信息。 - -该接口支持在ArkTS卡片中使用。 - -| 参数 | 参数类型 | 描述 | -| ----------- | ---------------------------------------------------------- | ---------------------------------------------- | -| borderWidth | [EdgeWidths](../reference/arkui-ts/ts-types.md#edgewidths) | 边框宽度类型,用于描述组件边框不同方向的宽度。 | -| margin | [Margin](../reference/arkui-ts/ts-types.md#margin) | 外边距类型,用于描述组件不同方向的外边距。 | -| padding | [Padding](../reference/arkui-ts/ts-types.md#padding) | 内边距类型,用于描述组件不同方向的内边距。 | - -## LayoutInfo9+ - -子组件layout信息。 - -该接口支持在ArkTS卡片中使用。 - -| 参数 | 参数类型 | 描述 | -| ---------- | -------------------------------------------------------------------------------- | ---------------- | -| position | [Position](../reference/arkui-ts/ts-types.md#position) | 子组件位置坐标。 | -| constraint | [ConstraintSizeOptions](../reference/arkui-ts/ts-types.md#constraintsizeoptions) | 子组件约束尺寸。 | - -**示例3:** - -```ts -// xxx.ets -@Entry -@Component -struct Index { - build() { - Column() { - CustomLayout() { - ForEach([1, 2, 3], (index) => { - Text('Sub' + index) - .fontSize(30) - .borderWidth(2) - }) - } - } - } -} - - -@Component -struct CustomLayout { - @BuilderParam builder: () => {} - - onLayout(children: Array, constraint: ConstraintSizeOptions) { - let pos = 0 - children.forEach((child) => { - child.layout({ position: { x: pos, y: pos }, constraint: constraint }) - pos += 100 - }) - } - - onMeasure(children: Array, constraint: ConstraintSizeOptions) { - let size = 100 - children.forEach((child) => { - child.measure({ minHeight: size, minWidth: size, maxWidth: size, maxHeight: size }) - size += 50 - }) - } - - build() { - this.builder() - } -} -``` - -![customlayout](figures/customLayout.png) diff --git a/zh-cn/application-dev/ui/ui-ts-developing-intro.md b/zh-cn/application-dev/ui/ui-ts-developing-intro.md deleted file mode 100644 index db7e9da5930a56d406c7b376cd31563d863a5bdc..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/ui/ui-ts-developing-intro.md +++ /dev/null @@ -1,158 +0,0 @@ -# 声明式UI开发指导 - -## 开发说明 - -| 任务 | 简介 | 相关资源 | -| ----------- | ---------------------------------------- | ---------------------------------------- | -| 准备开发环境 | 了解声明式UI的工程结构。
了解资源分类与访问。 | [OpenHarmony工程介绍](https://developer.harmonyos.com/cn/docs/documentation/doc-guides/ohos-project-overview-0000001218440650)
[资源分类与访问](../quick-start/resource-categories-and-access.md) | -| 学习ArkTS语言 | ArkTS是OpenHarmony优选的主力应用开发语言,当前,ArkTS在TS基础上主要扩展了声明式UI能力。 | [学习ArkTS语言](../quick-start/arkts-get-started.md) | -| 开发页面 | 根据页面的使用场景,选择合适的布局。
根据页面需要实现的内容,添加系统内置组件,并修改组件样式。
更新页面内容,丰富页面展现形式。 | [创建页面](#创建页面)
  [常见布局开发指导](ui-ts-layout-linear.md)
  [常见组件说明](ui-ts-components-intro.md)
[修改组件样式](#修改组件样式)
[更新页面内容](#更新页面内容) | -| (可选)页面多样化 | 绘图和动画。 | [绘图组件](../reference/arkui-ts/ts-drawing-components-circle.md)
[画布组件](../reference/arkui-ts/ts-components-canvas-canvas.md)
[动画](../reference/arkui-ts/ts-animatorproperty.md) | -| (可选)页面之间的跳转 | 使用页面路由实现多个页面之前的跳转。 | [页面路由](../reference/apis/js-apis-router.md) | -| (可选)性能提升 | 避免低性能代码对应用的性能造成负面影响。 | [性能提升的推荐方法](ui-ts-performance-improvement-recommendation.md) | - -## 创建页面 - -请先根据页面预期效果选择布局结构创建页面,并在页面中添加基础的系统内置组件。下述示例采用了[弹性布局(Flex)](ui-ts-layout-flex.md),对页面中的Text组件进行横纵向居中布局显示。 - - ```ts - // xxx.ets - @Entry - @Component - struct MyComponent { - build() { - Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { - Text('Hello') - } - .width('100%') - .height('100%') - } - } - ``` - -## 修改组件样式 - -在页面中添加系统内置组件时,若不设置属性方法,则会显示其默认样式。通过更改组件的属性样式或者组件支持的[通用属性](../reference/arkui-ts/ts-universal-attributes-size.md)样式,改变组件的UI显示。 - -1. 通过修改Text组件的构造参数,将Text组件的显示内容修改为“Tomato”。 -2. 修改Text组件的fontSize属性更改组件的字体大小,将字体大小设置为26,通过fontWeight属性更改字体粗细,将其设置为500。 - - ```ts - // xxx.ets - @Entry - @Component - struct MyComponent { - build() { - Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { - Text('Tomato') - .fontSize(26) - .fontWeight(500) - } - .width('100%') - .height('100%') - } - } - ``` - - ![zh-cn_image_0000001168888224](figures/zh-cn_image_0000001168888224.png) - -## 更新页面内容 - -在创建基本的页面之后,可根据组件的状态来更新页面内容。以下示例展示了简单的更新页面方法。 - -> **说明:** -> -> 更新组件的状态之前,请先初始化组件的成员变量。自定义组件的成员变量可以通过[本地初始化](../quick-start/arkts-restrictions-and-extensions.md#自定义组件成员变量初始化的方式与约束)和[在构造组件时通过构造参数初始化](../quick-start/arkts-restrictions-and-extensions.md#自定义组件成员变量初始化的方式与约束)两种方式实现,具体允许哪种方式取决于该变量所使用的装饰器。 - -**示例:** - -```ts -// xxx.ets -@Entry -@Component -struct ParentComp { - @State isCountDown: boolean = true - - build() { - Column() { - Text(this.isCountDown ? 'Count Down' : 'Stopwatch').fontSize(20).margin(20) - if (this.isCountDown) { - // 图片资源放在media目录下 - Image($r("app.media.countdown")).width(120).height(120) - TimerComponent({ counter: 10, changePerSec: -1, showInColor: Color.Red }) - } else { - // 图片资源放在media目录下 - Image($r("app.media.stopwatch")).width(120).height(120) - TimerComponent({ counter: 0, changePerSec: +1, showInColor: Color.Black }) - } - Button(this.isCountDown ? 'Switch to Stopwatch' : 'Switch to Count Down') - .onClick(() => { - this.isCountDown = !this.isCountDown - }) - }.width('100%') - } -} - -// 自定义计时器/倒计时组件 -@Component -struct TimerComponent { - @State counter: number = 0 - private changePerSec: number = -1 - private showInColor: Color = Color.Black - private timerId: number = -1 - - build() { - Text(`${this.counter}sec`) - .fontColor(this.showInColor) - .fontSize(20) - .margin(20) - } - - aboutToAppear() { - this.timerId = setInterval(() => { - this.counter += this.changePerSec - }, 1000) - } - - aboutToDisappear() { - if (this.timerId > 0) { - clearTimeout(this.timerId) - this.timerId = -1 - } - } -} -``` - -![component](figures/component.gif) - -**初始创建和渲染:** - -1. 创建父组件ParentComp; - -2. 本地初始化ParentComp的状态变量isCountDown; - -3. 执行ParentComp的build函数; - -4. 创建Column组件; - 1. 创建Text组件,设置其文本展示内容,并将Text组件实例添加到Column中; - 2. 判断if条件,创建true条件下的元素; - 1. 创建Image组件,并设置其图片源地址; - 2. 使用给定的构造函数创建TimerComponent; - 3. 创建Button内置组件,设置相应的内容。 - -**状态更新:** - -用户单击按钮时: - -1. ParentComp的isCountDown状态变量的值更改为false; - -2. 执行ParentComp的build函数; - -3. Column组件被重用并执行重新初始化; - -4. Column的子组件会重用内存中的对象,但会进行重新初始化; - 1. Text组件会被重用,但使用新的文本内容进行重新初始化; - 2. 判断if条件,使用false条件下的元素; - 1. 原来true条件的组件不再使用,将这些组件销毁; - 2. 创建false条件下的组件; - 3. 重用Button组件,使用新的图片源地址。 \ No newline at end of file diff --git a/zh-cn/application-dev/ui/ui-ts-drawing-feature.md b/zh-cn/application-dev/ui/ui-ts-drawing-feature.md deleted file mode 100644 index 909dcf6cd812a7ef61d84de7ddb96847871e04a1..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/ui/ui-ts-drawing-feature.md +++ /dev/null @@ -1,415 +0,0 @@ -# 绘制图形 - -绘制能力主要是通过框架提供的绘制组件来支撑,支持svg标准绘制命令。 - -本节主要学习如何使用绘制组件,绘制详情页食物成分标签(基本几何图形)和应用Logo(自定义图形)。 - -## 绘制基本几何图形 - -绘制组件封装了一些常见的基本几何图形,比如矩形Rect、圆形Circle、椭圆形Ellipse等,为开发者省去了路线计算的过程。 - -FoodDetail页面的食物成分表里,给每一项成分名称前都加上一个圆形的图标作为成分标签。 - -1. 创建Circle组件,在每一项含量成分前增加一个圆形图标作为标签。设置Circle的直径为 6vp。修改FoodDetail页面的ContentTable组件里的IngredientItem方法,在成分名称前添加Circle。 - - ```ts - // FoodDetail.ets - @Component - struct ContentTable { - private foodItem: FoodData - - @Builder IngredientItem(title:string, colorValue: string, name: string, value: string) { - Flex() { - Text(title) - .fontSize(17.4) - .fontWeight(FontWeight.Bold) - .layoutWeight(1) - Flex({ alignItems: ItemAlign.Center }) { - Circle({width: 6, height: 6}) - .margin({right: 12}) - .fill(colorValue) - Text(name) - .fontSize(17.4) - .flexGrow(1) - Text(value) - .fontSize(17.4) - } - .layoutWeight(2) - } - } - - build() { - ...... - } - } - ``` - -2. 每个成分的标签颜色不一样,所以我们在build方法中,调用IngredientItem,给每个Circle填充不一样的颜色。 - - ```ts - // FoodDetail.ets - @Component - struct ContentTable { - private foodItem: FoodData - - @Builder IngredientItem(title:string, colorValue: string, name: string, value: string) { - Flex() { - Text(title) - .fontSize(17.4) - .fontWeight(FontWeight.Bold) - .layoutWeight(1) - Flex({ alignItems: ItemAlign.Center }) { - Circle({width: 6, height: 6}) - .margin({right: 12}) - .fill(colorValue) - Text(name) - .fontSize(17.4) - .flexGrow(1) - Text(value) - .fontSize(17.4) - } - .layoutWeight(2) - } - } - - build() { - Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) { - this.IngredientItem('Calories', '#FFf54040', 'Calories', this.foodItem.calories + 'kcal') - this.IngredientItem('Nutrition', '#FFcccccc', 'Protein', this.foodItem.protein + 'g') - this.IngredientItem(' ', '#FFf5d640', 'Fat', this.foodItem.fat + 'g') - this.IngredientItem(' ', '#FF9e9eff', 'Carbohydrates', this.foodItem.carbohydrates + 'g') - this.IngredientItem(' ', '#FF53f540', 'VitaminC', this.foodItem.vitaminC + 'mg') - } - .height(280) - .padding({ top: 30, right: 30, left: 30 }) - } - } - ``` - - ![drawing-feature](figures/drawing-feature.png) - -## 绘制自定义几何图形 - -除绘制基础几何图形,开发者还可以使用Path组件来绘制自定义的路线,下面进行绘制应用的Logo图案。 - -1. 在pages文件夹下创建新的页面Logo.ets。 - - ![drawing-feature1](figures/drawing-feature1.png) - -2. Logo.ets中删掉模板代码,创建Logo Component。 - - ```ts - @Entry - @Component - struct Logo { - build() { - } - } - ``` - -3. 创建Flex组件为根节点,宽高设置为100%,设置其在主轴方向和交叉轴方向的对齐方式都为Center,创建Shape组件为Flex子组件。 - - Shape组件是所有绘制组件的父组件。如果需要组合多个绘制组件成为一个整体,需要创建Shape作为其父组件。 - - 我们要绘制的Logo的大小630px * 630px。声明式UI范式支持多种长度单位的设置,在前面的章节中,我们直接使用number作为参数,即采用了默认长度单位vp,虚拟像素单位。vp和设备分辨率以及屏幕密度有关。比如设备分辨率为1176 * 2400,屏幕基准密度(resolution)为3,vp = px / resolution,则该设备屏幕宽度是392vp。 - - 但是绘制组件采用svg标准,默认采取px为单位的,为方便统一,在这绘制Logo这一部分,统一采取px为单位。声明式UI框架同样也支持px单位,入参类型为string,设置宽度为630px,即210vp,设置方式为width('630px')或者width(210)。 - - ```ts - @Entry - @Component - struct Logo { - build() { - Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { - Shape() { - - } - .height('630px') - .width('630px') - } - .width('100%') - .height('100%') - } - } - ``` - -4. 给页面填充渐变色。设置为线性渐变,偏移角度为180deg,三段渐变 #BDE895 -->95DE7F --> #7AB967,其区间分别为[0, 0.1], (0.1, 0.6], (0.6, 1]。 - - ```ts - .linearGradient( - { - angle: 180, - colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]] - }) - ``` - - ![drawing-feature2](figures/drawing-feature2.png) - - ```ts - @Entry - @Component - struct Logo { - build() { - Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { - Shape() { - - } - .height('630px') - .width('630px') - } - .width('100%') - .height('100%') - .linearGradient( - { - angle: 180, - colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]] - }) - } - } - ``` - - ![drawing-feature3](figures/drawing-feature3.png) - -5. 绘制第一条路线Path,设置其绘制命令。 - - ```ts - Path() - .commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36') - ``` - - Path的绘制命令采用svg标准,上述命令可分解为: - - ```ts - M162 128.7 - ``` - - 将笔触移动到(Moveto)坐标点(162, 128.7)。 - - ```ts - a222 222 0 0 1 100.8 374.4 - ``` - - 画圆弧线(elliptical arc)半径rx,ry为222,x轴旋转角度x-axis-rotation为0,角度大小large-arc-flag为0,即小弧度角,弧线方向(sweep-flag)为1,即逆时针画弧线,小写a为相对位置,即终点坐标为(162 + 100.8 = 262.8, 128.7 + 374.4 = 503.1)。 - - ```ts - H198 - ``` - - 画水平线(horizontal lineto)到198,即画(262.8, 503.1)到(198, 503.1)的水平线。 - - ```ts - a36 36 0 0 3 -36 -36 - ``` - - 画圆弧线(elliptical arc),含义同上,结束点为(198 - 36 = 162, 503.1 - 36 = 467.1)。 - - ```ts - V128.7 - ``` - - 画垂直线(vertical lineto)到128.7,即画(162, 467.1)到(162, 128.7)的垂直线。 - - ```ts - z - ``` - - 关闭路径(closepath)。 - - ![drawing-feature4](figures/drawing-feature4.png) - - 填充颜色为白色,线条颜色为透明。 - - ```ts - .fill(Color.White) - .stroke(Color.Transparent) - ``` - - ```ts - @Entry - @Component - struct Logo { - build() { - Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { - Shape() { - Path() - .commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36') - .fill(Color.White) - .stroke(Color.Transparent) - } - .height('630px') - .width('630px') - } - .width('100%') - .height('100%') - .linearGradient( - { - angle: 180, - colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]] - }) - } - } - ``` - - ![drawing-feature5](figures/drawing-feature5.png) - -6. 在Shape组件内绘制第二个Path。第二条Path的背景色为渐变色,但是渐变色的填充是其整体的box,所以需要clip将其裁剪,入参为Shape,即按照Shape的形状进行裁剪。 - - ```ts - Path() - .commands('M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z') - .fill('none') - .stroke(Corlor.Transparent) - .linearGradient( - { - angle: 30, - colors: [["#C4FFA0", 0], ["#ffffff", 1]] - }) - .clip(new Path().commands('M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z')) - ``` - - Path的绘制命令比较长,可以将其作为组件的成员变量,通过this调用。 - - ```ts - @Entry - @Component - struct Logo { - private pathCommands1:string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z' - build() { - ...... - Path() - .commands(this.pathCommands1) - .fill('none') - .stroke(Color.Transparent) - .linearGradient( - { - angle: 30, - colors: [["#C4FFA0", 0], ["#ffffff", 1]] - }) - .clip(new Path().commands(this.pathCommands1)) - ...... - } - } - ``` - - ![drawing-feature6](figures/drawing-feature6.png) - -7. 在Shape组件内绘制第二个Path。 - - ```ts - @Entry - @Component - struct Logo { - private pathCommands1:string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z' - private pathCommands2:string = 'M270.6 128.1 h48.6 c51.6 0 98.4 21 132.3 54.6 a411 411 0 0 3 -45.6 123 c-25.2 45.6 -56.4 84 -87.6 110.4 a206.1 206.1 0 0 0 -47.7 -288 z' - build() { - Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { - Shape() { - Path() - .commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36') - .fill(Color.White) - .stroke(Color.Transparent) - - Path() - .commands(this.pathCommands1) - .fill('none') - .stroke(Color.Transparent) - .linearGradient( - { - angle: 30, - colors: [["#C4FFA0", 0], ["#ffffff", 1]] - }) - .clip(new Path().commands(this.pathCommands1)) - - Path() - .commands(this.pathCommands2) - .fill('none') - .stroke(Color.Transparent) - .linearGradient( - { - angle: 50, - colors: [['#8CC36A', 0.1], ["#B3EB90", 0.4], ["#ffffff", 0.7]] - }) - .clip(new Path().commands(this.pathCommands2)) - } - .height('630px') - .width('630px') - } - .width('100%') - .height('100%') - .linearGradient( - { - angle: 180, - colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]] - }) - } - } - ``` - - ![drawing-feature7](figures/drawing-feature7.png) - - 完成应用Logo的绘制。Shape组合了三个Path组件,通过svg命令绘制出一个艺术的叶子,寓意绿色健康饮食方式。 - -8. 添加应用的标题和slogan。 - - ```ts - @Entry - @Component - struct Logo { - private pathCommands1: string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z' - private pathCommands2: string = 'M270.6 128.1 h48.6 c51.6 0 98.4 21 132.3 54.6 a411 411 0 0 3 -45.6 123 c-25.2 45.6 -56.4 84 -87.6 110.4 a206.1 206.1 0 0 0 -47.7 -288 z' - - build() { - Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { - Shape() { - Path() - .commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36') - .fill(Color.White) - .stroke(Color.Transparent) - - Path() - .commands(this.pathCommands1) - .fill('none') - .stroke(Color.Transparent) - .linearGradient( - { - angle: 30, - colors: [["#C4FFA0", 0], ["#ffffff", 1]] - }) - .clip(new Path().commands(this.pathCommands1)) - - Path() - .commands(this.pathCommands2) - .fill('none') - .stroke(Color.Transparent) - .linearGradient( - { - angle: 50, - colors: [['#8CC36A', 0.1], ["#B3EB90", 0.4], ["#ffffff", 0.7]] - }) - .clip(new Path().commands(this.pathCommands2)) - } - .height('630px') - .width('630px') - - Text('Healthy Diet') - .fontSize(26) - .fontColor(Color.White) - .margin({ top: 300 }) - - Text('Healthy life comes from a balanced diet') - .fontSize(17) - .fontColor(Color.White) - .margin({ top: 4 }) - } - .width('100%') - .height('100%') - .linearGradient( - { - angle: 180, - colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]] - }) - } - } - ``` - - ![drawing-feature8](figures/drawing-feature8.png) \ No newline at end of file diff --git a/zh-cn/application-dev/ui/ui-ts-layout-flex.md b/zh-cn/application-dev/ui/ui-ts-layout-flex.md deleted file mode 100644 index 36c046269b477392e1269e4d3750a02ac958f738..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/ui/ui-ts-layout-flex.md +++ /dev/null @@ -1,586 +0,0 @@ -# 弹性布局 - -弹性布局(Flex布局)是自适应布局中使用最为灵活的布局。弹性布局提供一种更加有效的方式来对容器中的子组件进行排列、对齐和分配空白空间。弹性布局 - - -- 容器: [Flex组件](../reference/arkui-ts/ts-container-flex.md)作为Flex布局的容器,用于设置布局相关属性。 -- 子组件: Flex组件内的子组件自动成为布局的子组件。 -- 主轴: Flex组件布局方向的轴线,子组件默认沿着主轴排列。主轴开始的位置称为主轴起始端,结束位置称为主轴终点端。 -- 交叉轴: 垂直于主轴方向的轴线。交叉轴起始的位置称为交叉轴首部,结束位置称为交叉轴尾部。 - -以FlexDirection.Row的Flex为例: - -![](figures/flex.png) - -## 容器组件属性 - -通过Flex组件提供的Flex接口创建弹性布局。如下: - -`Flex(options?: { direction?: FlexDirection, wrap?: FlexWrap, justifyContent?: FlexAlign, alignItems?: ItemAlign, alignContent?: FlexAlign })` - - - -### 弹性布局方向 -参数direction决定主轴的方向,即子组件的排列方向。可选值有: - -![](figures/direction.png) - -- FlexDirection.Row(默认值):主轴为水平方向,子组件从起始端沿着水平方向开始排布。 - - ```ts - Flex({ direction: FlexDirection.Row }) { - Text('1').width('33%').height(50).backgroundColor(0xF5DEB3) - Text('2').width('33%').height(50).backgroundColor(0xD2B48C) - Text('3').width('33%').height(50).backgroundColor(0xF5DEB3) - } - .height(70) - .width('90%') - .padding(10) - .backgroundColor(0xAFEEEE) - ``` - ![zh-cn_image_0000001218579606](figures/zh-cn_image_0000001218579606.png) - -- FlexDirection.RowReverse:主轴为水平方向,子组件从终点端沿着FlexDirection. Row相反的方向开始排布。 - - ```ts - Flex({ direction: FlexDirection.RowReverse }) { - Text('1').width('33%').height(50).backgroundColor(0xF5DEB3) - Text('2').width('33%').height(50).backgroundColor(0xD2B48C) - Text('3').width('33%').height(50).backgroundColor(0xF5DEB3) - } - .height(70) - .width('90%') - .padding(10) - .backgroundColor(0xAFEEEE) - ``` - - ![zh-cn_image_0000001218739566](figures/zh-cn_image_0000001218739566.png) - -- FlexDirection.Column:主轴为垂直方向,子组件从起始端沿着垂直方向开始排布。 - - ```ts - Flex({ direction: FlexDirection.Column }) { - Text('1').width('100%').height(50).backgroundColor(0xF5DEB3) - Text('2').width('100%').height(50).backgroundColor(0xD2B48C) - Text('3').width('100%').height(50).backgroundColor(0xF5DEB3) - } - .height(70) - .width('90%') - .padding(10) - .backgroundColor(0xAFEEEE) - ``` - - ![zh-cn_image_0000001263019457](figures/zh-cn_image_0000001263019457.png) - -- FlexDirection.ColumnReverse:主轴为垂直方向,子组件从终点端沿着FlexDirection. Column相反的方向开始排布。 - - ```ts - Flex({ direction: FlexDirection.ColumnReverse }) { - Text('1').width('100%').height(50).backgroundColor(0xF5DEB3) - Text('2').width('100%').height(50).backgroundColor(0xD2B48C) - Text('3').width('100%').height(50).backgroundColor(0xF5DEB3) - } - .height(70) - .width('90%') - .padding(10) - .backgroundColor(0xAFEEEE) - ``` - - ![zh-cn_image_0000001263339459](figures/zh-cn_image_0000001263339459.png) - -### 弹性布局换行 - -默认情况下,子组件在Flex容器中都排在一条线(又称"轴线")上。通过wrap参数设置子组件换行方式。可选值有: - -- FlexWrap. NoWrap(默认值): 不换行。如果子组件的宽度总和大于父元素的宽度,则子组件会被压缩宽度。 - - ```ts - Flex({ wrap: FlexWrap.NoWrap }) { - Text('1').width('50%').height(50).backgroundColor(0xF5DEB3) - Text('2').width('50%').height(50).backgroundColor(0xD2B48C) - Text('3').width('50%').height(50).backgroundColor(0xF5DEB3) - } - .width('90%') - .padding(10) - .backgroundColor(0xAFEEEE) - ``` - - ![zh-cn_image_0000001263139409](figures/zh-cn_image_0000001263139409.png) - -- FlexWrap. Wrap:换行,每一行子组件按照主轴方向排列。 - - ```ts - Flex({ wrap: FlexWrap.Wrap }) { - Text('1').width('50%').height(50).backgroundColor(0xF5DEB3) - Text('2').width('50%').height(50).backgroundColor(0xD2B48C) - Text('3').width('50%').height(50).backgroundColor(0xD2B48C) - } - .width('90%') - .padding(10) - .backgroundColor(0xAFEEEE) - ``` - - ![zh-cn_image_0000001218419614](figures/zh-cn_image_0000001218419614.png) - -- FlexWrap. WrapReverse:换行,每一行子组件按照主轴反方向排列。 - - ```ts - Flex({ wrap: FlexWrap.WrapReverse}) { - Text('1').width('50%').height(50).backgroundColor(0xF5DEB3) - Text('2').width('50%').height(50).backgroundColor(0xD2B48C) - Text('3').width('50%').height(50).backgroundColor(0xF5DEB3) - } - .width('90%') - .padding(10) - .backgroundColor(0xAFEEEE) - ``` - - ![zh-cn_image_0000001263259399](figures/zh-cn_image_0000001263259399.png) - -### 弹性布局对齐方式 - -#### 主轴对齐 - -通过justifyContent参数设置在主轴方向的对齐方式,存在下面六种情况: - -![](figures/justifyContent.png) - -- FlexAlign.Start(默认值): 子组件在主轴方向起始端对齐, 第一个子组件与父元素边沿对齐,其他元素与前一个元素对齐。 - - ```ts - Flex({ justifyContent: FlexAlign.Start }) { - Text('1').width('20%').height(50).backgroundColor(0xF5DEB3) - Text('2').width('20%').height(50).backgroundColor(0xD2B48C) - Text('3').width('20%').height(50).backgroundColor(0xF5DEB3) - } - .width('90%') - .padding({ top: 10, bottom: 10 }) - .backgroundColor(0xAFEEEE) - ``` - - ![zh-cn_image_0000001218259634](figures/mainStart.png) - -- FlexAlign.Center: 子组件在主轴方向居中对齐。 - - ```ts - Flex({ justifyContent: FlexAlign.Center }) { - Text('1').width('20%').height(50).backgroundColor(0xF5DEB3) - Text('2').width('20%').height(50).backgroundColor(0xD2B48C) - Text('3').width('20%').height(50).backgroundColor(0xF5DEB3) - } - .width('90%') - .padding({ top: 10, bottom: 10 }) - .backgroundColor(0xAFEEEE) - ``` - - ![zh-cn_image_0000001218579608](figures/mainCenter.png) - -- FlexAlign.End: 子组件在主轴方向终点端对齐, 最后一个子组件与父元素边沿对齐,其他元素与后一个元素对齐。 - - ```ts - Flex({ justifyContent: FlexAlign.End }) { - Text('1').width('20%').height(50).backgroundColor(0xF5DEB3) - Text('2').width('20%').height(50).backgroundColor(0xD2B48C) - Text('3').width('20%').height(50).backgroundColor(0xF5DEB3) - } - .width('90%') - .padding({ top: 10, bottom: 10 }) - .backgroundColor(0xAFEEEE) - ``` - - ![zh-cn_image_0000001218739568](figures/mainEnd.png) - -- FlexAlign.SpaceBetween: Flex主轴方向均匀分配弹性元素,相邻子组件之间距离相同。第一个子组件和最后一个子组件与父元素边沿对齐。 - - ```ts - Flex({ justifyContent: FlexAlign.SpaceBetween }) { - Text('1').width('20%').height(50).backgroundColor(0xF5DEB3) - Text('2').width('20%').height(50).backgroundColor(0xD2B48C) - Text('3').width('20%').height(50).backgroundColor(0xF5DEB3) - } - .width('90%') - .padding({ top: 10, bottom: 10 }) - .backgroundColor(0xAFEEEE) - ``` - - ![zh-cn_image_0000001263019461](figures/mainSpacebetween.png) - -- FlexAlign.SpaceAround: Flex主轴方向均匀分配弹性元素,相邻子组件之间距离相同。第一个子组件到主轴起始端的距离和最后一个子组件到主轴终点端的距离是相邻元素之间距离的一半。 - - ```ts - Flex({ justifyContent: FlexAlign.SpaceAround }) { - Text('1').width('20%').height(50).backgroundColor(0xF5DEB3) - Text('2').width('20%').height(50).backgroundColor(0xD2B48C) - Text('3').width('20%').height(50).backgroundColor(0xF5DEB3) - } - .width('90%') - .padding({ top: 10, bottom: 10 }) - .backgroundColor(0xAFEEEE) - ``` - - ![zh-cn_image_0000001263339461](figures/mainSpacearound.png) - -- FlexAlign.SpaceEvenly: Flex主轴方向元素等间距布局,相邻子组件之间的间距、第一个子组件与主轴起始端的间距、最后一个子组件到主轴终点端的间距均相等。 - - ```ts - Flex({ justifyContent: FlexAlign.SpaceEvenly }) { - Text('1').width('20%').height(50).backgroundColor(0xF5DEB3) - Text('2').width('20%').height(50).backgroundColor(0xD2B48C) - Text('3').width('20%').height(50).backgroundColor(0xF5DEB3) - } - .width('90%') - .padding({ top: 10, bottom: 10 }) - .backgroundColor(0xAFEEEE) - ``` - - ![zh-cn_image_0000001263139411](figures/mainSpaceevenly.png) - -#### 交叉轴对齐 - -容器和子组件都可以设置交叉轴对齐方式,且子组件设置的对齐方式优先级较高。 - -##### 容器组件设置交叉轴对齐 -可以通过Flex组件的alignItems参数设置子组件在交叉轴的对齐方式,可选值有: - -- ItemAlign.Auto: 使用Flex容器中默认配置。 - - ```ts - Flex({ alignItems: ItemAlign.Auto }) { - Text('1').width('33%').height(30).backgroundColor(0xF5DEB3) - Text('2').width('33%').height(40).backgroundColor(0xD2B48C) - Text('3').width('33%').height(50).backgroundColor(0xF5DEB3) - } - .size({width: '90%', height: 80}) - .padding(10) - .backgroundColor(0xAFEEEE) - ``` - - ![zh-cn_image_0000001218419616](figures/zh-cn_image_0000001218419616.png) - -- ItemAlign.Start: 交叉轴方向首部对齐。 - - ```ts - Flex({ alignItems: ItemAlign.Start }) { - Text('1').width('33%').height(30).backgroundColor(0xF5DEB3) - Text('2').width('33%').height(40).backgroundColor(0xD2B48C) - Text('3').width('33%').height(50).backgroundColor(0xF5DEB3) - } - .size({width: '90%', height: 80}) - .padding(10) - .backgroundColor(0xAFEEEE) - ``` - - ![zh-cn_image_0000001263259401](figures/zh-cn_image_0000001263259401.png) - -- ItemAlign.Center: 交叉轴方向居中对齐。 - - ```ts - Flex({ alignItems: ItemAlign.Center }) { - Text('1').width('33%').height(30).backgroundColor(0xF5DEB3) - Text('2').width('33%').height(40).backgroundColor(0xD2B48C) - Text('3').width('33%').height(50).backgroundColor(0xF5DEB3) - } - .size({width: '90%', height: 80}) - .padding(10) - .backgroundColor(0xAFEEEE) - ``` - - ![zh-cn_image_0000001218259636](figures/zh-cn_image_0000001218259636.png) - -- ItemAlign.End:交叉轴方向底部对齐。 - - ```ts - Flex({ alignItems: ItemAlign.End }) { - Text('1').width('33%').height(30).backgroundColor(0xF5DEB3) - Text('2').width('33%').height(40).backgroundColor(0xD2B48C) - Text('3').width('33%').height(50).backgroundColor(0xF5DEB3) - } - .size({width: '90%', height: 80}) - .padding(10) - .backgroundColor(0xAFEEEE) - ``` - - ![zh-cn_image_0000001218579610](figures/zh-cn_image_0000001218579610.png) - -- ItemAlign.Stretch:交叉轴方向拉伸填充,在未设置尺寸时,拉伸到容器尺寸。 - - ```ts - Flex({ alignItems: ItemAlign.Stretch }) { - Text('1').width('33%').height(30).backgroundColor(0xF5DEB3) - Text('2').width('33%').height(40).backgroundColor(0xD2B48C) - Text('3').width('33%').height(50).backgroundColor(0xF5DEB3) - } - .size({width: '90%', height: 80}) - .padding(10) - .backgroundColor(0xAFEEEE) - ``` - - ![zh-cn_image_0000001218739570](figures/itemalignstretch.png) - -- ItemAlign. Baseline:交叉轴方向文本基线对齐。 - - ```ts - Flex({ alignItems: ItemAlign.Baseline }) { - Text('1').width('33%').height(30).backgroundColor(0xF5DEB3) - Text('2').width('33%').height(40).backgroundColor(0xD2B48C) - Text('3').width('33%').height(50).backgroundColor(0xF5DEB3) - } - .size({width: '90%', height: 80}) - .padding(10) - .backgroundColor(0xAFEEEE) - ``` - - ![zh-cn_image_0000001263019463](figures/zh-cn_image_0000001263019463.png) - -##### 子组件设置交叉轴对齐 -子组件的alignSelf属性也可以设置子组件在父容器交叉轴的对齐格式,且会覆盖Flex布局容器中alignItems默认配置。如下例所示: - -```ts -Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center }) { //容器组件设置子组件居中 - Text('alignSelf Start').width('25%').height(80) - .alignSelf(ItemAlign.Start) - .backgroundColor(0xF5DEB3) - Text('alignSelf Baseline') - .alignSelf(ItemAlign.Baseline) - .width('25%') - .height(80) - .backgroundColor(0xD2B48C) - Text('alignSelf Baseline').width('25%').height(100) - .backgroundColor(0xF5DEB3) - .alignSelf(ItemAlign.Baseline) - Text('no alignSelf').width('25%').height(100) - .backgroundColor(0xD2B48C) - Text('no alignSelf').width('25%').height(100) - .backgroundColor(0xF5DEB3) - -}.width('90%').height(220).backgroundColor(0xAFEEEE) -``` - -![alignself](figures/alignself.png) - -上例中,Flex容器中alignItems设置交叉轴子组件的对齐方式为居中,子组件自身设置了alignSelf属性的情况,覆盖父组件的alignItem值,表现为alignSelf的定义。 - -#### 内容对齐 - -可以通过alignContent参数设置子组件各行在交叉轴剩余空间内的对齐方式,只在多行的flex布局中生效,可选值有: - -- FlexAlign.Start: 子组件各行与交叉轴起点对齐。 - - ```ts - Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.Start }) { - Text('1').width('30%').height(20).backgroundColor(0xF5DEB3) - Text('2').width('60%').height(20).backgroundColor(0xD2B48C) - Text('3').width('40%').height(20).backgroundColor(0xD2B48C) - Text('4').width('30%').height(20).backgroundColor(0xF5DEB3) - Text('5').width('20%').height(20).backgroundColor(0xD2B48C) - } - .width('90%') - .height(100) - .backgroundColor(0xAFEEEE) - ``` - - ![crossStart.png](figures/crossStart.png) - -- FlexAlign.Center: 子组件各行在交叉轴方向居中对齐。 - - ```ts - Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.Center }) { - Text('1').width('30%').height(20).backgroundColor(0xF5DEB3) - Text('2').width('60%').height(20).backgroundColor(0xD2B48C) - Text('3').width('40%').height(20).backgroundColor(0xD2B48C) - Text('4').width('30%').height(20).backgroundColor(0xF5DEB3) - Text('5').width('20%').height(20).backgroundColor(0xD2B48C) - } - .width('90%') - .height(100) - .backgroundColor(0xAFEEEE) - ``` - - ![crossCenter.png](figures/crossCenter.png) - -- FlexAlign.End: 子组件各行与交叉轴终点对齐。 - - ```ts - Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.End }) { - Text('1').width('30%').height(20).backgroundColor(0xF5DEB3) - Text('2').width('60%').height(20).backgroundColor(0xD2B48C) - Text('3').width('40%').height(20).backgroundColor(0xD2B48C) - Text('4').width('30%').height(20).backgroundColor(0xF5DEB3) - Text('5').width('20%').height(20).backgroundColor(0xD2B48C) - } - .width('90%') - .height(100) - .backgroundColor(0xAFEEEE) - ``` - - ![crossEnd.png](figures/crossEnd.png) - -- FlexAlign.SpaceBetween: 子组件各行与交叉轴两端对齐,各行间垂直间距平均分布。 - - ```ts - Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceBetween }) { - Text('1').width('30%').height(20).backgroundColor(0xF5DEB3) - Text('2').width('60%').height(20).backgroundColor(0xD2B48C) - Text('3').width('40%').height(20).backgroundColor(0xD2B48C) - Text('4').width('30%').height(20).backgroundColor(0xF5DEB3) - Text('5').width('20%').height(20).backgroundColor(0xD2B48C) - } - .width('90%') - .height(100) - .backgroundColor(0xAFEEEE) - ``` - - ![crossSpacebetween.png](figures/crossSpacebetween.png) - -- FlexAlign.SpaceAround: 子组件各行间距相等,是元素首尾行与交叉轴两端距离的两倍。 - - ```ts - Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceAround }) { - Text('1').width('30%').height(20).backgroundColor(0xF5DEB3) - Text('2').width('60%').height(20).backgroundColor(0xD2B48C) - Text('3').width('40%').height(20).backgroundColor(0xD2B48C) - Text('4').width('30%').height(20).backgroundColor(0xF5DEB3) - Text('5').width('20%').height(20).backgroundColor(0xD2B48C) - } - .width('90%') - .height(100) - .backgroundColor(0xAFEEEE) - ``` - - ![crossSpacearound.png](figures/crossSpacearound.png) - -- FlexAlign.SpaceEvenly: 子组件各行间距,子组件首尾行与交叉轴两端距离都相等。 - - ```ts - Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceEvenly }) { - Text('1').width('30%').height(20).backgroundColor(0xF5DEB3) - Text('2').width('60%').height(20).backgroundColor(0xD2B48C) - Text('3').width('40%').height(20).backgroundColor(0xD2B48C) - Text('4').width('30%').height(20).backgroundColor(0xF5DEB3) - Text('5').width('20%').height(20).backgroundColor(0xD2B48C) - } - .width('90%') - .height(100) - .backgroundColor(0xAFEEEE) - ``` - - ![crossSpaceevenly.png](figures/crossSpaceevenly.png) - -### 弹性布局的自适应拉伸 - -在弹性布局父组件尺寸不够大的时候,通过子组件的下面几个属性设置其再父容器的占比,达到自适应布局能力。 -- flexBasis: 设置子组件在父容器主轴方向上的基准尺寸。如果设置了该值,则子项占用的空间为设置的值;如果没设置或者为auto,那子项的空间为width/height的值。 - - ```ts - Flex() { - Text('flexBasis("auto")') - .flexBasis('auto') // 未设置width以及flexBasis值为auto,内容自身宽松 - .height(100) - .backgroundColor(0xF5DEB3) - Text('flexBasis("auto")'+' width("40%")') - .width('40%') - .flexBasis('auto') //设置width以及flexBasis值auto,使用width的值 - .height(100) - .backgroundColor(0xD2B48C) - - Text('flexBasis(100)') // 未设置width以及flexBasis值为100,宽度为100vp - .flexBasis(100) - .height(100) - .backgroundColor(0xF5DEB3) - - Text('flexBasis(100)') - .flexBasis(100) - .width(200) // flexBasis值为100,覆盖width的设置值,宽度为100vp - .height(100) - .backgroundColor(0xD2B48C) - }.width('90%').height(120).padding(10).backgroundColor(0xAFEEEE) - ``` - - ![flexbasis](figures/flexbasis.png) - -- flexGrow: 设置父容器的剩余空间分配给此属性所在组件的比例。用于"瓜分"父组件的剩余空间。 - - ```ts - Flex() { - Text('flexGrow(1)') - .flexGrow(2) - .width(100) - .height(100) - .backgroundColor(0xF5DEB3) - - Text('flexGrow(2)') - .flexGrow(2) - .width(100) - .height(100) - .backgroundColor(0xD2B48C) - - Text('no flexGrow') - .width(100) - .height(100) - .backgroundColor(0xF5DEB3) - }.width(400).height(120).padding(10).backgroundColor(0xAFEEEE) - ``` - - ![flexgrow](figures/flexgrow.png) - -上图中,父容器宽度400vp, 三个子组件原始宽度为100vp,综合300vp,剩余空间100vp根据flexGrow值的占比分配给子组件,未设置flexGrow的子组件不参与“瓜分”。 -第一个元素以及第二个元素以2:3分配剩下的100vp。第一个元素为100vp+100vp*2/5=140vp,第二个元素为100vp+100vp*3/5=160vp。 - -- flexShrink: 当父容器空间不足时,子组件的压缩比例。 - - ```ts - Flex({ direction: FlexDirection.Row }) { - Text('flexShrink(3)') - .flexShrink(3) - .width(200) - .height(100) - .backgroundColor(0xF5DEB3) - - Text('no flexShrink') - .width(200) - .height(100) - .backgroundColor(0xD2B48C) - - Text('flexShrink(2)') - .flexShrink(2) - .width(200) - .height(100) - .backgroundColor(0xF5DEB3) - }.width(400).height(120).padding(10).backgroundColor(0xAFEEEE) - ``` - - ![flexshrink](figures/flexshrink.png) - -## 场景示例 - -使用弹性布局,可以实现子组件沿水平方向排列,两端对齐,子组件间距平分,竖直方向上子组件居中的效果。示例如下: - -```ts -@Entry -@Component -struct FlexExample { - build() { - Column() { - Column({ space: 5 }) { - Flex({ direction: FlexDirection.Row, wrap: FlexWrap.NoWrap, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { - Text('1').width('30%').height(50).backgroundColor(0xF5DEB3) - Text('2').width('30%').height(50).backgroundColor(0xD2B48C) - Text('3').width('30%').height(50).backgroundColor(0xF5DEB3) - } - .height(70) - .width('90%') - .backgroundColor(0xAFEEEE) - }.width('100%').margin({ top: 5 }) - }.width('100%') - } -} -``` - -![zh-cn_image_0000001261605867](figures/flexExample.png) - -## 相关实例 - -针对弹性布局开发,有以下相关实例可供参考: - -- [弹性布局(ArkTS)(API8)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/FlowLayoutEts) - diff --git a/zh-cn/application-dev/ui/ui-ts-layout-grid-container-new.md b/zh-cn/application-dev/ui/ui-ts-layout-grid-container-new.md deleted file mode 100644 index 0e87a6bcab413b2ffcf1281cfbb808579a4a6ced..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/ui/ui-ts-layout-grid-container-new.md +++ /dev/null @@ -1,397 +0,0 @@ -# 栅格布局 - -栅格系统作为一种辅助布局的定位工具,在平面设计和网站设计都起到了很好的作用,对移动设备的界面设计有较好的借鉴作用。总结栅格系统对于移动设备的优势主要有: - -1. 给布局提供一种可循的规律,解决多尺寸多设备的动态布局问题。 -2. 给系统提供一种统一的定位标注,保证各模块各设备的布局一致性。 -3. 给应用提供一种灵活的间距调整方法,满足特殊场景布局调整的可能性。 - -推荐使用栅格组件[GridRow](../reference/arkui-ts/ts-container-gridrow.md)和[GridCol](../reference/arkui-ts/ts-container-gridcol.md)来实现栅格布局效果, -相对于目前已废弃的[GridContainer](../reference/arkui-ts/ts-container-gridcontainer.md)组件,GridRow和GridCol提供了更灵活、更全面的栅格系统实现方案。GridRow为栅格容器组件,只能与栅格子组件GridCol在栅格布局场景中使用。 - - -## 栅格容器GridRow - - -栅格容器有columns、gutter、direction、breakpoints四个属性。 -- columns: 栅格布局的主要定位工具,设置栅格布局的总列数。 -- gutter: 设置元素之间的距离,决定内容间的紧密程度。 -- direction: 设置栅格子组件在栅格容器中的排列方向。 -- breakpoints:以设备宽度为基准,将应用宽度分成了几个不同的区间,即不同的断点。开发者可根据需要在不同的区间下实现不同的页面布局效果。 - - -首先通过设置断点,得到一系列断点区间;然后,借助栅格组件能力监听应用窗口大小的变化,判断应用当前处于哪个断点区间,最后调整应用的布局。 - -### 栅格系统断点 - -栅格系统以设备的水平宽度(屏幕密度像素值,单位vp)作为断点依据,定义设备的宽度类型,形成了一套断点规则。开发者可根据需求在不同的断点区间实现不同的页面布局效果。 -栅格系统默认断点将设备宽度分为xs、sm、md、lg四类,尺寸范围如下: - -| 断点名称 | 取值范围(vp)| -| --------| ------ | -| xs | [0, 320) | -| sm | [320, 600) | -| md | [600, 840) | -| lg | [840, +∞) | - -在GridRow新栅格组件中,允许开发者使用breakpoints自定义修改断点的取值范围,最多支持6个断点,除了默认的四个断点外, -还可以启用xl,xxl两个断点,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备的布局设置。 - -| 断点名称 | 设备描述 | -| ----- | ---------------------------------------- | -| xs | 最小宽度类型设备。 | -| sm | 小宽度类型设备。 | -| md | 中等宽度类型设备。 | -| lg | 大宽度类型设备。 | -| xl | 特大宽度类型设备。 | -| xxl | 超大宽度类型设备。 | - -- 针对断点位置,开发者根据实际使用场景,通过一个单调递增数组设置。由于breakpoints最多支持六个断点,单调递增数组长度最大为5。 - - ```ts - breakpoints: {value: ["100vp", "200vp"]} - ``` - - 表示启用xs、sm、md共3个断点,小于100vp为xs,100vp-200vp为sm,大于200vp为md。 - - ```ts - breakpoints: {value: ["320vp", "600vp", "840vp", "1080vp"]} - ``` - - 表示启用xs、sm、md、lg、xl共5个断点,小于320vp为xs,320vp-600vp为sm,600vp-840vp为md,840vp-1080vp为lg,大于1080vp为xl。 - - -- 栅格系统通过监听窗口或容器的尺寸变化进行断点,通过reference设置断点切换参考物。 考虑到应用可能以非全屏窗口的形式显示,以应用窗口宽度为参照物更为通用。 - -下例中,使用栅格的默认列数12列,通过断点设置将应用宽度分成六个区间,在各区间中,每个栅格子元素占用的列数均不同。效果如图: - ```ts -GridRow({ - breakpoints: { - value: ['200vp', '300vp', '400vp', '500vp', '600vp'], - reference: BreakpointsReference.WindowSize - } -}) { - ForEach(this.bgColors, (color, index) => { - GridCol({ - span: { - xs: 2, - sm: 3, - md: 4, - lg: 6, - xl: 8, - xxl: 12 - } - }) { - Row() { - Text(`${index}`) - }.width("100%").height("50vp") - }.backgroundColor(color) - }) -} - ``` - -![](figures/breakpoints.gif) - - - -### 栅格布局的总列数 - -GridRow中通过columns设置栅格布局的总列数。 - -- columns默认值为12,当未设置columns时,在任何断点下,栅格布局被分成12列。 - ```ts - GridRow() { - ForEach(this.bgColors, (item, index) => { - GridCol() { - Row() { - Text(`${index + 1}`) - }.width("100%").height("50") - }.backgroundColor(item) - }) - } - ``` - - ![](figures/columns1.png) - -- 当columns类型为number时,栅格布局在任何尺寸设备下都被分为columns列。下面分别设置栅格布局列数为4和8,子元素默认占一列,效果如下: - - ```ts - Row() { - GridRow({ columns: 4 }) { - ForEach(this.bgColors, (item, index) => { - GridCol() { - Row() { - Text(`${index + 1}`) - }.width("100%").height("50") - }.backgroundColor(item) - }) - } - .width("100%").height("100%") - .onBreakpointChange((breakpoint) => { - this.currentBp = breakpoint - }) - } - .height(160) - .border({ color: Color.Blue, width: 2 }) - .width('90%') - - Row() { - GridRow({ columns: 8 }) { - ForEach(this.bgColors, (item, index) => { - GridCol() { - Row() { - Text(`${index + 1}`) - }.width("100%").height("50") - }.backgroundColor(item) - }) - } - .width("100%").height("100%") - .onBreakpointChange((breakpoint) => { - this.currentBp = breakpoint - }) - } - .height(160) - .border({ color: Color.Blue, width: 2 }) - .width('90%') - ``` - - ![](figures/columns2.png) - - -- 当columns类型为GridRowColumnOption时,支持下面六种不同尺寸(xs, sm, md, lg, xl, xxl)设备的总列数设置,各个尺寸下数值可不同。 - - ```ts - GridRow({ columns: { sm: 4, md: 8 }, breakpoints: { value: ['200vp', '300vp', '400vp', '500vp', '600vp'] } }) { - ForEach(this.bgColors, (item, index) => { - GridCol() { - Row() { - Text(`${index + 1}`) - }.width("100%").height("50") - }.backgroundColor(item) - }) - } - ``` - ![](figures/columns3.gif) - - 如上,若只设置sm, md的栅格总列数,则较小的尺寸使用默认columns值12,较大的尺寸使用前一个尺寸的columns。这里只设置sm:8, md:10,则较小尺寸的xs:12,较大尺寸的参照md的设置,lg:10, xl:10, xxl:10。 - -### 栅格子组件间距 - -GridRow中通过gutter设置子元素在水平和垂直方向的间距。 - -- 当gutter类型为number时,同时设置栅格子组件间水平和垂直方向边距且相等。下例中,设置子组件水平与垂直方向距离相邻元素的间距为10。 - - ```ts - GridRow({ gutter: 10 }){} - ``` - - ![](figures/gutter1.png) - - - -- 当gutter类型为GutterOption时,单独设置栅格子组件水平垂直边距,x属性为水平方向间距,y为垂直方向间距。 - - ```ts - GridRow({ gutter: { x: 20, y: 50 } }){} - ``` - - ![](figures/gutter2.png) - - - -### 排列方向 - -通过GridRow的direction属性设置栅格子组件在栅格容器中的排列方向。 - -- 子组件默认从左往右排列。 - - ```ts - GridRow({ direction: GridRowDirection.Row }){} - ``` - ![](figures/direction1.png) - -- 子组件从右往左排列。 - - ```ts - GridRow({ direction: GridRowDirection.RowReverse }){} - ``` - - ![](figures/direction2.png) - - - -## 栅格子组件GridCol - -GridCol组件作为GridRow组件的子组件,通过给GridCol传参或者设置属性两种方式,设置span,offset,order的值。 - -- span的设置 - - ```ts - GridCol({ span: 2 }){} - GridCol({ span: { xs: 1, sm: 2, md: 3, lg: 4 } }){} - GridCol(){}.span(2) - GridCol(){}.span({ xs: 1, sm: 2, md: 3, lg: 4 }) - ``` - -- offset的设置 - - ```ts - GridCol({ offset: 2 }){} - GridCol({ offset: { xs: 2, sm: 2, md: 2, lg: 2 } }){} - GridCol(){}.offset(2) - GridCol(){}.offset({ xs: 1, sm: 2, md: 3, lg: 4 }) - ``` - -- order的设置 - - ```ts - GridCol({ order: 2 }){} - GridCol({ order: { xs: 1, sm: 2, md: 3, lg: 4 } }){} - GridCol(){}.order(2) - GridCol(){}.order({ xs: 1, sm: 2, md: 3, lg: 4 }) - ``` - - 下面使用传参的方式演示各属性的使用。 - -### span - -子组件占栅格布局的列数,决定了子组件的宽度,默认为1。 - -- 当类型为number时,子组件在所有尺寸设备下占用的列数相同。 - - ```ts - GridRow({ columns: 8 }) { - ForEach(this.bgColors, (color, index) => { - GridCol({ span: 2 }) { - Row() { - Text(`${index}`) - }.width("100%").height("50vp") - } - .backgroundColor(color) - }) - } - ``` - - ![](figures/span1.png) - -- 当类型为GridColColumnOption时,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备中子组件所占列数设置,各个尺寸下数值可不同。 - - ```ts - GridRow({ columns: 8 }) { - ForEach(this.bgColors, (color, index) => { - GridCol({ span: { xs: 1, sm: 2, md: 3, lg: 4 } }) { - Row() { - Text(`${index}`) - }.width("100%").height("50vp") - } - .backgroundColor(color) - }) - } - ``` - - ![](figures/span2.gif) - -### offset - -栅格子组件相对于前一个子组件的偏移列数,默认为0。 -- 当类型为number时,子组件偏移相同列数。 - - ```ts - GridRow() { - ForEach(this.bgColors, (color, index) => { - GridCol({ offset: 2 }) { - Row() { - Text("" + index) - }.width("100%").height("50vp") - } - .backgroundColor(color) - }) - } - ``` - - ![](figures/offset1.png) - - 栅格默认分成12列,每一个子组件默认占1列,偏移2列,每个子组件及间距共占3列,一行放四个子组件。 - - -- 当类型为GridColColumnOption时,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备中子组件所占列数设置,各个尺寸下数值可不同。 - - ```ts - GridRow() { - ForEach(this.bgColors, (color, index) => { - GridCol({ offset: { xs: 1, sm: 2, md: 3, lg: 4 } }) { - Row() { - Text("" + index) - }.width("100%").height("50vp") - } - .backgroundColor(color) - }) - } - ``` - - ![](figures/offset2.gif) - -### order - - 栅格子组件的序号,决定子组件排列次序。当子组件不设置order或者设置相同的order, 子组件按照代码顺序展示。当子组件设置不同的order时,order较小的组件在前,较大的在后。 - 当子组件部分设置order,部分不设置order时,未设置order的子组件依次排序靠前,设置了order的子组件按照数值从小到大排列。 - - -- 当类型为number时,子组件在任何尺寸下排序次序一致。 - - ```ts - GridRow() { - GridCol({ order: 5 }) { - Row() { - Text("1") - }.width("100%").height("50vp") - }.backgroundColor(Color.Red) - GridCol({ order: 4 }) { - Row() { - Text("2") - }.width("100%").height("50vp") - }.backgroundColor(Color.Orange) - GridCol({ order: 3 }) { - Row() { - Text("3") - }.width("100%").height("50vp") - }.backgroundColor(Color.Yellow) - GridCol({ order: 2 }) { - Row() { - Text("4") - }.width("100%").height("50vp") - }.backgroundColor(Color.Green) - } - ``` - - ![](figures/order1.png) -- 当类型为GridColColumnOption时,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备中子组件排序次序设置。 - - ```ts - GridRow() { - GridCol({ order: { xs:1, sm:5, md:3, lg:7}}) { - Row() { - Text("1") - }.width("100%").height("50vp") - }.backgroundColor(Color.Red) - GridCol({ order: { xs:2, sm:2, md:6, lg:1} }) { - Row() { - Text("2") - }.width("100%").height("50vp") - }.backgroundColor(Color.Orange) - GridCol({ order: { xs:3, sm:3, md:1, lg:6} }) { - Row() { - Text("3") - }.width("100%").height("50vp") - }.backgroundColor(Color.Yellow) - GridCol({ order: { xs:4, sm:4, md:2, lg:5} }) { - Row() { - Text("4") - }.width("100%").height("50vp") - }.backgroundColor(Color.Green) - } - ``` - - ![](figures/order2.gif) - - diff --git a/zh-cn/application-dev/ui/ui-ts-layout-grid.md b/zh-cn/application-dev/ui/ui-ts-layout-grid.md deleted file mode 100644 index 28099d42680db276c42ad1f1de0441fa02bca1ea..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/ui/ui-ts-layout-grid.md +++ /dev/null @@ -1,273 +0,0 @@ -# 网格布局 - -网格布局(GridLayout)是自适应布局中一种重要的布局,具备较强的页面均分能力,子组件占比控制能力。 -通过[Grid](../reference/arkui-ts/ts-container-grid.md)容器组件和子组件[GridItem](../reference/arkui-ts/ts-container-griditem.md)实现, -Grid用于设置网格布局相关参数,GridItem定义子组件相关特征。优势如下: - -1. 容器组件尺寸发生变化时,所有子组件以及间距等比例调整,实现布局的自适应能力。 -2. 支持自定义网格布局行数和列数,以及每行每列尺寸占比。 -3. 支持设置网格布局中子组件的行列间距。 -4. 支持设置子组件横跨几行或者几列。 - - - -## 容器组件Grid设置 - -### 行列数量占比 -通过Grid的组件的columnsTemplate和rowTemplate属性设置网格布局行列数量与尺寸占比。 - -下面以columnsTemplate为例,介绍该属性的设置,该属性值是一个由多个空格和'数字+fr'间隔拼接的字符串,fr的个数即网格布局的列数,fr前面的数值大小,用于计算该列在网格布局宽度上的占比,最终决定该列的宽度。 - -```ts -struct GridExample { - @State Number: Array = ['1', '2', '3', '4'] - - build() { - Column({ space: 5 }) { - Grid() { - ForEach(this.Number, (num: string) => { - GridItem() { - Text(`列${num}`) - .fontSize(16) - .textAlign(TextAlign.Center) - .backgroundColor(0xd0d0d0) - .width('100%') - .height('100%') - .borderRadius(5) - } - }) - } - .columnsTemplate('1fr 1fr 1fr 1fr') - .rowsTemplate('1fr') - .columnsGap(10) - .rowsGap(20) - .width('90%') - .backgroundColor(0xF0F0F0) - .height(100) - }.width('100%') - } -} -``` - -定义了四个等分的列,每列宽度相等。 - -```ts -struct GridExample { - @State Number: Array = ['1', '2', '3', '4'] - - build() { - Column({ space: 5 }) { - Grid() { - ForEach(this.Number, (num: string) => { - GridItem() { - Text(`列${num}`) - .fontSize(16) - .textAlign(TextAlign.Center) - .backgroundColor(0xd0d0d0) - .width('100%') - .height('100%') - .borderRadius(5) - } - }) - } - .columnsTemplate('1fr 2fr 3fr 4fr') - .rowsTemplate('1fr') - .columnsGap(10) - .rowsGap(20) - .width('90%') - .backgroundColor(0xF0F0F0) - .height(100) - }.width('100%') - } -} -``` - -定义了四列,每列宽度比值为1:2:3:4。 - -```ts -struct GridExample { - @State Number: Array = ['1', '2', '3'] - - build() { - Column({ space: 5 }) { - Grid() { - ForEach(this.Number, (num: string) => { - GridItem() { - Text(`列${num}`) - .fontSize(16) - .textAlign(TextAlign.Center) - .backgroundColor(0xd0d0d0) - .width('100%') - .height('100%') - .borderRadius(5) - } - }) - } - .columnsTemplate('4fr 2fr 3fr') - .rowsTemplate('1fr') - .columnsGap(10) - .rowsGap(20) - .width('90%') - .backgroundColor(0xF0F0F0) - .height(100) - }.width('100%') - } -} -``` - -定义了三列,每列宽度比值为4:2:3。 - -效果如下: - -![](figures/columnTemplate.png) - -### 排列方式 - -通过layoutDirection可以设置网格布局的主轴方向,决定子组件的排列方式。 -可选值包括Row,RowReverse, Column, ColumnReverse四种情况。 -效果如下: - -![](figures/gridlayout.png) - -### 行列间距 - -columnsGap用于设置网格子组件GridItem垂直方向的间距,rowsGap用于设置GridItem水平方向的间距。 - -```ts -Grid() -.columnsTemplate('1fr 1fr 1fr 1fr') -.columnsGap(10) -.rowsGap(20) -``` - -![](figures/columnGap.png) - -上图中,设置网格布局子组件间的垂直间距为20,水平间距为10。 - -## 网格子组件GridItem设置 - -### 设置子组件占的行列数 - -网格布局的行列标号从1开始,依次编号。 - -子组件横跨多行时,通过rowStart设置子组件起始行编号,rowEnd设置终点行编号。当rowStart值与rowEnd值相同时,子组件只占一个网格。示例如下: - -```ts -Grid() { - GridItem() { - Text('5') - .fontSize(16) - .textAlign(TextAlign.Center) - .textStyle() - }.rowStart(2).rowEnd(3) // 5子组件从第二行到第三行 - - GridItem() { - Text('4') - .fontSize(16) - .textAlign(TextAlign.Center) - .textStyle() - }.columnStart(4).columnEnd(5) // 4从第四列到第五列 - - GridItem() { - Text('6') - .fontSize(16) - .textAlign(TextAlign.Center) - .textStyle() - }.columnStart(2).columnEnd(4) // 6从第二列到第四列 - - GridItem() { - Text('9') - .fontSize(16) - .textAlign(TextAlign.Center) - .textStyle() - }.columnStart(3).columnEnd(4) // 从第三列到第四列 -} -.columnsTemplate('1fr 1fr 1fr 1fr 1fr') -.rowsTemplate('1fr 1fr 1fr') -.columnsGap(10) -.rowsGap(20) -.width('90%') -.backgroundColor(0xF0F0F0) -.height('200vp') -.layoutDirection(GridDirection.Column) -``` - -![](figures/griditem.png) - -## 场景示例 - -使用grid布局实现一个计算器的排布效果,代码如下: - -```ts -@Entry -@Component -struct GridExample { - @State Number: Array = ['1', '2', '3', '+', '4', '5', '6', '-', '7', '8', '9', '*', '0', '.', '/'] - - @Styles textStyle(){ - .backgroundColor(0xd0d0d0) - .width('100%') - .height('100%') - .borderRadius(5) - } - - build() { - Column({ space: 5 }) { - Grid() { - GridItem() { - Text('0') - .fontSize(30) - .textStyle() - }.columnStart(1).columnEnd(4) - - GridItem() { - Text('清空') - .fontSize(16) - .textAlign(TextAlign.Center) - .textStyle() - }.columnStart(1).columnEnd(2) - - GridItem() { - Text('回退') - .fontSize(16) - .textAlign(TextAlign.Center) - .textStyle() - }.columnStart(3).columnEnd(4) - - ForEach(this.Number, (day: string) => { - if (day === '0') { - GridItem() { - Text(day) - .fontSize(16) - .textAlign(TextAlign.Center) - .textStyle() - }.columnStart(1).columnEnd(2) - } else { - GridItem() { - Text(day) - .fontSize(16) - .textAlign(TextAlign.Center) - .textStyle() - } - } - }) - } - .columnsTemplate('1fr 1fr 1fr 1fr') - .rowsTemplate('2fr 1fr 1fr 1fr 1fr 1fr') - .columnsGap(10) - .rowsGap(15) - .width('90%') - .backgroundColor(0xF0F0F0) - .height('70%') - }.width('100%').margin({ top: 5 }) - } -} -``` - -在大屏设备上展示效果如下: - -![](figures/gridExp1.png) - -在小屏设备下展示效果如下: - -![](figures/gridExp2.png) diff --git a/zh-cn/application-dev/ui/ui-ts-layout-linear.md b/zh-cn/application-dev/ui/ui-ts-layout-linear.md deleted file mode 100644 index 0e89b56ad71bb008cae24b6b3151ea7464cd7862..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/ui/ui-ts-layout-linear.md +++ /dev/null @@ -1,370 +0,0 @@ -# 线性布局 - -线性布局(LinearLayout)是开发中最常用的布局。线性布局的子组件在线性方向上(水平方向和垂直方向)依次排列。 - -通过线性容器[Row](../reference/arkui-ts/ts-container-row.md)和[Column](../reference/arkui-ts/ts-container-column.md)实现线性布局。Column容器内子组件按照垂直方向排列,Row组件中,子组件按照水平方向排列。 - -## 线性布局的排列 - -线性布局的排列方向由所选容器组件决定。根据不同的排列方向,选择使用Row或Column容器创建线性布局,通过调整space,alignItems,justifyContent属性调整子组件的间距,水平垂直方向的对齐方式。 -1. 通过space参数设置主轴(排列方向)上子组件的间距。达到各子组件在排列方向上的等间距效果。 -2. 通过alignItems属性设置子组件在交叉轴(排列方向的垂直方向)的对齐方式。且在各类尺寸屏幕中,表现一致。其中,交叉轴为垂直方向时,取值为[VerticalAlign类型](../reference/arkui-ts/ts-appendix-enums.md#verticalalign),水平方向取值为[HorizontalAlign类型](../reference/arkui-ts/ts-appendix-enums.md#horizontalalign)。 -3. 通过justifyContent属性设置子组件在主轴(排列方向)上的对齐方式。实现布局的自适应均分能力。取值为[FlexAlign类型](../reference/arkui-ts/ts-appendix-enums.md#flexalign)。 - -具体使用以及效果如下表所示: - -|属性名|描述|Row效果图|Column效果图| -|------|---------------------------|----------------------------|---------------------------| -|space |- 横向布局中各子组件的在水平方向的间距
- 纵向布局中个子组件垂直方向间距| ![](figures/rowspace.png) | ![](figures/columnspace.png) | -|alignItems |容器排列方向的垂直方向上,子组件在父容器中的对齐方式|![](figures/rowalign.png) |![](figures/columnalign.png)| -|justifyContent |容器排列方向上,子组件在父容器中的对齐方式 |![](figures/rowjustify.png) |![](figures/columnjustify.png)| - -## 自适应拉伸 - -在线性布局下,常用空白填充组件[Blank](../reference/arkui-ts/ts-basic-components-blank.md),在容器主轴方向自动填充空白空间,达到自适应拉伸效果。 - -```ts -@Entry -@Component -struct BlankExample { - build() { - Column() { - Row() { - Text('Bluetooth').fontSize(18) - Blank() - Toggle({ type: ToggleType.Switch, isOn: true }) - }.backgroundColor(0xFFFFFF).borderRadius(15).padding({ left: 12 }).width('100%') - }.backgroundColor(0xEFEFEF).padding(20).width('100%') - } -} -``` - -![](figures/blank.gif) - -## 自适应缩放 - -自适应缩放是指在各种不同大小设备中,子组件按照预设的比例,尺寸随容器尺寸的变化而变化。在线性布局中有下列方法实现: - -1. 父容器尺寸确定时,设置了layoutWeight属性的子组件与兄弟元素占主轴尺寸按照权重进行分配,忽略元素本身尺寸设置,在任意尺寸设备下,自适应占满剩余空间。 - - ```ts - @Entry - @Component - struct layoutWeightExample { - build() { - Column() { - Text('1:2:3').width('100%') - Row() { - Column() { - Text('layoutWeight(1)') - .textAlign(TextAlign.Center) - }.layoutWeight(2).backgroundColor(0xffd306).height('100%') - - Column() { - Text('layoutWeight(2)') - .textAlign(TextAlign.Center) - }.layoutWeight(4).backgroundColor(0xffed97).height('100%') - - Column() { - Text('layoutWeight(6)') - .textAlign(TextAlign.Center) - }.layoutWeight(6).backgroundColor(0xffd306).height('100%') - - }.backgroundColor(0xffd306).height('30%') - - Text('2:5:3').width('100%') - Row() { - Column() { - Text('layoutWeight(2)') - .textAlign(TextAlign.Center) - }.layoutWeight(2).backgroundColor(0xffd306).height('100%') - - Column() { - Text('layoutWeight(5)') - .textAlign(TextAlign.Center) - }.layoutWeight(5).backgroundColor(0xffed97).height('100%') - - Column() { - Text('layoutWeight(3)') - .textAlign(TextAlign.Center) - }.layoutWeight(3).backgroundColor(0xffd306).height('100%') - }.backgroundColor(0xffd306).height('30%') - } - } - } - ``` - - ![](figures/layoutWeight.gif) - - -3. 父容器尺寸确定时,使用百分比设置子组件以及兄弟组件的width宽度,可以保证各自元素在任意尺寸下的自适应占比。 - - ```ts - @Entry - @Component - struct WidthExample { - build() { - Column() { - Row() { - Column() { - Text('left width 20%') - .textAlign(TextAlign.Center) - }.width('20%').backgroundColor(0xffd306).height('100%') - - Column() { - Text('center width 50%') - .textAlign(TextAlign.Center) - }.width('50%').backgroundColor(0xffed97).height('100%') - - Column() { - Text('right width 30%') - .textAlign(TextAlign.Center) - }.width('30%').backgroundColor(0xffd306).height('100%') - }.backgroundColor(0xffd306).height('30%') - } - } - } - ``` - - ![](figures/width.gif) - - 上例中,在任意大小的设备中,子组件的宽度占比固定。 - -## 定位能力 -- 相对定位 - - 使用组件的[offset属性](../reference/arkui-ts/ts-universal-attributes-location.md)可以实现相对定位,设置元素相对于自身的偏移量。设置该属性,不影响父容器布局,仅在绘制时进行位置调整。使用线性布局和offset可以实现大部分布局的开发。 - - ```ts - @Entry - @Component - struct OffsetExample { - @Styles eleStyle() { - .size({ width: 120, height: '50' }) - .backgroundColor(0xbbb2cb) - .border({ width: 1 }) - } - - build() { - Column({ space: 20 }) { - Row() { - Text('1').size({ width: '15%', height: '50' }).backgroundColor(0xdeb887).border({ width: 1 }).fontSize(16) - Text('2 offset(15, 30)') - .eleStyle() - .fontSize(16) - .align(Alignment.Start) - .offset({ x: 15, y: 30 }) - Text('3').size({ width: '15%', height: '50' }).backgroundColor(0xdeb887).border({ width: 1 }).fontSize(16) - Text('4 offset(-10%, 20%)') - .eleStyle() - .fontSize(16) - .offset({ x: '-5%', y: '20%' }) - }.width('90%').height(150).border({ width: 1, style: BorderStyle.Dashed }) - } - .width('100%') - .margin({ top: 25 }) - } - } - ``` - - ![](figures/offset.gif) - - -- 绝对定位 - - 线性布局中可以使用组件的[positon属性](../reference/arkui-ts/ts-universal-attributes-location.md)实现绝对布局(AbsoluteLayout),设置元素左上角相对于父容器左上角偏移位置。对于不同尺寸的设备,使用绝对定位的适应性会比较差,在屏幕的适配上有缺陷。 - - ```ts - @Entry - @Component - struct PositionExample { - @Styles eleStyle(){ - .backgroundColor(0xbbb2cb) - .border({ width: 1 }) - .size({ width: 120, height: 50 }) - } - - build() { - Column({ space: 20 }) { - // 设置子组件左上角相对于父组件左上角的偏移位置 - Row() { - Text('position(30, 10)') - .eleStyle() - .fontSize(16) - .position({ x: 10, y: 10 }) - - Text('position(50%, 70%)') - .eleStyle() - .fontSize(16) - .position({ x: '50%', y: '70%' }) - - Text('position(10%, 90%)') - .eleStyle() - .fontSize(16) - .position({ x: '10%', y: '80%' }) - }.width('90%').height('100%').border({ width: 1, style: BorderStyle.Dashed }) - } - .width('90%').margin(25) - } - } - ``` - - ![](figures/position.gif) - - -## 自适应延伸 - -自适应延伸是在不同尺寸设备下,当页面显示内容个数不一并延伸到屏幕外时,可通过滚动条拖动展示。适用于线性布局中内容无法一屏展示的场景。常见以下两类实现方法。 - - -- List组件 - - List子项过多一屏放不下时,未展示的子项通过滚动条拖动显示。通过scrollBar属性设置滚动条的常驻状态,edgeEffect属性设置拖动到极限的回弹效果。 - - - 纵向List: - ```ts - @Entry - @Component - struct ListExample1 { - @State arr: string[] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"] - @State alignListItem: ListItemAlign = ListItemAlign.Start - - build() { - Column() { - List({ space: 20, initialIndex: 0 }) { - ForEach(this.arr, (item) => { - ListItem() { - Text('' + item) - .width('100%') - .height(100) - .fontSize(16) - .textAlign(TextAlign.Center) - .borderRadius(10) - .backgroundColor(0xFFFFFF) - } - .border({ width: 2, color: Color.Green }) - }, item => item) - } - .border({ width: 2, color: Color.Red, style: BorderStyle.Dashed }) - .scrollBar(BarState.On) // 滚动条常驻 - .edgeEffect(EdgeEffect.Spring) // 滚动到边缘再拖动回弹效果 - - }.width('100%').height('100%').backgroundColor(0xDCDCDC).padding(20) - } - } - ``` - - ![](figures/listcolumn.gif) - - - 横向List: - - ```ts - @Entry - @Component - struct ListExample2 { - @State arr: string[] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"] - @State alignListItem: ListItemAlign = ListItemAlign.Start - - build() { - Column() { - List({ space: 20, initialIndex: 0 }) { - ForEach(this.arr, (item) => { - ListItem() { - Text('' + item) - .height('100%') - .width(100) - .fontSize(16) - .textAlign(TextAlign.Center) - .borderRadius(10) - .backgroundColor(0xFFFFFF) - } - .border({ width: 2, color: Color.Green }) - }, item => item) - } - .border({ width: 2, color: Color.Red, style: BorderStyle.Dashed }) - .scrollBar(BarState.On) // 滚动条常驻 - .edgeEffect(EdgeEffect.Spring) // 滚动到边缘再拖动回弹效果 - .listDirection(Axis.Horizontal) // 列表水平排列 - }.width('100%').height('100%').backgroundColor(0xDCDCDC).padding(20) - } - } - ``` - - ![](figures/listrow.gif) - -- Scroll组件 - - 线性布局中,当子组件的布局尺寸超过父组件的尺寸时,内容可以滚动。在Column或者Row外层包裹一个可滚动的容器组件Scroll实现。 - - 纵向Scroll: - - ```ts - @Entry - @Component - struct ScrollExample { - scroller: Scroller = new Scroller(); - private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; - - build() { - Scroll(this.scroller) { - Column() { - ForEach(this.arr, (item) => { - Text(item.toString()) - .width('90%') - .height(150) - .backgroundColor(0xFFFFFF) - .borderRadius(15) - .fontSize(16) - .textAlign(TextAlign.Center) - .margin({ top: 10 }) - }, item => item) - }.width('100%') - } - .backgroundColor(0xDCDCDC) - .scrollable(ScrollDirection.Vertical) // 滚动方向纵向 - .scrollBar(BarState.On) // 滚动条常驻显示 - .scrollBarColor(Color.Gray) // 滚动条颜色 - .scrollBarWidth(30) // 滚动条宽度 - .edgeEffect(EdgeEffect.Spring) // 滚动到边沿后回弹 - } - } - ``` - - ![](figures/scrollcolumn.gif) - - 横向Scroll: - - ```ts - @Entry - @Component - struct ScrollExample { - scroller: Scroller = new Scroller(); - private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; - - build() { - Scroll(this.scroller) { - Row() { - ForEach(this.arr, (item) => { - Text(item.toString()) - .height('90%') - .width(150) - .backgroundColor(0xFFFFFF) - .borderRadius(15) - .fontSize(16) - .textAlign(TextAlign.Center) - .margin({ left: 10 }) - }, item => item) - }.height('100%') - } - .backgroundColor(0xDCDCDC) - .scrollable(ScrollDirection.Horizontal) // 滚动方向横向 - .scrollBar(BarState.On) // 滚动条常驻显示 - .scrollBarColor(Color.Gray) // 滚动条颜色 - .scrollBarWidth(30) // 滚动条宽度 - .edgeEffect(EdgeEffect.Spring) // 滚动到边沿后回弹 - } - } - ``` - ![](figures/scrollrow.gif) diff --git a/zh-cn/application-dev/ui/ui-ts-layout-mediaquery.md b/zh-cn/application-dev/ui/ui-ts-layout-mediaquery.md deleted file mode 100644 index 26ce8fb6aeecca3aeee250c5b2dcc6b2fe851206..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/ui/ui-ts-layout-mediaquery.md +++ /dev/null @@ -1,168 +0,0 @@ -# 媒体查询 - -[媒体查询(Media Query)](../reference/apis/js-apis-mediaquery.md)作为响应式设计的核心,在移动设备上应用十分广泛。它根据不同设备类型或同设备不同状态修改应用的样式。媒体查询的优势有: - -1. 提供丰富的媒体特征监听能力,针对设备和应用的属性信息(比如显示区域、深浅色、分辨率),设计出相匹配的布局。 - -2. 当屏幕发生动态改变时(比如分屏、横竖屏切换),同步更新应用的页面布局。 - - - -## 媒体查询引入与使用流程 - -媒体查询通过媒体查询接口,设置查询条件并绑定回调函数,在对应的条件的回调函数里更改页面布局或者实现业务逻辑,实现页面的响应式设计。具体步骤如下: - -首先导入媒体查询模块。 - -```ts -import mediaquery from '@ohos.mediaquery' -``` - -通过matchMediaSync接口设置媒体查询条件,保存返回的条件监听句柄listener。 - -```ts -listener = mediaquery.matchMediaSync('(orientation: landscape)') -``` - -给条件监听句柄listener绑定回调函数onPortrait,当listener检测设备状态变化时执行回调函数。在回调函数内,根据不同设备状态更改页面布局或者实现业务逻辑。 - -```ts -onPortrait(mediaQueryResult) { - if (mediaQueryResult.matches) { - // do something here - } else { - // do something here - } -} -listener.on('change', onPortrait) -``` - -## 媒体查询条件 - -媒体查询条件由媒体类型,逻辑操作符,媒体特征组成,其中媒体类型可省略,逻辑操作符用于连接不同媒体类型与媒体特征,其中,媒体特征要使用()包裹且可以有多个。具体规则如下: - -### 语法规则 - -``` -[media-type] [and|not|only] [(media-feature)] -``` - -例如: - -`screen and (round-screen: true)` :当设备屏幕是圆形时条件成立。 - -`(max-height: 800)` :当高度小于等于800时条件成立。 - -`(height <= 800) ` :当高度小于等于800时条件成立。 - -`screen and (device-type: tv) or (resolution < 2)` :包含多个媒体特征的多条件复杂语句查询,当设备类型为tv或设备分辨率小于2时条件成立。 - -### 媒体类型(media-type) - -| 类型 | 说明 | -| ------ | -------------- | -| screen | 按屏幕相关参数进行媒体查询。 | - -### 媒体逻辑操作(and|or|not|only) - -媒体逻辑操作符:and、or、not、only用于构成复杂媒体查询,也可以通过comma(, )将其组合起来,详细解释说明如下表。 - - **表1** 媒体逻辑操作符 - -| 类型 | 说明 | -| -------- | ---------------------------------------- | -| and | 将多个媒体特征(Media  Feature)以“与”的方式连接成一个媒体查询,只有当所有媒体特征都为true,查询条件成立。另外,它还可以将媒体类型和媒体功能结合起来。
例如:screen  and  (device-type:  wearable)  and  (max-height:  600)  表示当设备类型是智能穿戴且应用的最大高度小于等于600个像素单位时成立。 | -| or | 将多个媒体特征以“或”的方式连接成一个媒体查询,如果存在结果为true的媒体特征,则查询条件成立。
例如:screen  and  (max-height:  1000)  or  (round-screen:true)表示当应用高度小于等于1000个像素单位或者设备屏幕是圆形时,条件成立。 | -| not | 取反媒体查询结果,媒体查询结果不成立时返回true,否则返回false。
例如:not  screen  and  (min-height:  50)  and  (max-height:  600)  表示当应用高度小于50个像素单位或者大于600个像素单位时成立。
使用not运算符时必须指定媒体类型。 | -| only | 当整个表达式都匹配时,才会应用选择的样式,可以应用在防止某些较早的版本的浏览器上产生歧义的场景。一些较早版本的浏览器对于同时包含了媒体类型和媒体特征的语句会产生歧义,比如:
screen  and  (min-height:  50)
老版本浏览器会将这句话理解成screen,从而导致仅仅匹配到媒体类型(screen),就应用了指定样式,使用only可以很好地规避这种情况。
使用only时必须指定媒体类型。 | -| , (comma) | 将多个媒体特征以“或”的方式连接成一个媒体查询,如果存在结果为true的媒体特征,则查询条件成立。其效果等同于or运算符。
例如:screen  and  (min-height:  1000),     (round-screen:true)  表示当应用高度大于等于1000个像素单位或者设备屏幕是圆形时,条件成立。 | - -在MediaQuery Level 4中引入了范围查询,使其能够使用max-,min-的同时,也支持了< =,> =,< ,> 操作符。 - - **表2** 媒体逻辑范围操作符 - -| 类型 | 说明 | -| ----- | ---------------------------------------- | -| < = | 小于等于,例如:screen  and  (height  < =  50)。 | -| > = | 大于等于,例如:screen  and  (height  > =  600)。 | -| < | 小于,例如:screen  and  (height  <   50)。 | -| > | 大于,例如:screen  and  (height  >   600)。 | - -### 媒体特征(media-feature) - -| 类型 | 说明 | -| ----------------- | ------------------------------------------------------------ | -| height | 应用页面可绘制区域的高度。 | -| min-height | 应用页面可绘制区域的最小高度。 | -| max-height | 应用页面可绘制区域的最大高度。 | -| width | 应用页面可绘制区域的宽度。 | -| min-width | 应用页面可绘制区域的最小宽度。 | -| max-width | 应用页面可绘制区域的最大宽度。 | -| resolution | 设备的分辨率,支持dpi,dppx和dpcm单位。其中:
-  dpi表示每英寸中物理像素个数,1dpi≈0.39dpcm;
-  dpcm表示每厘米上的物理像素个数,1dpcm  ≈  2.54dpi;
-  dppx表示每个px中的物理像素数(此单位按96px=1英寸为基准,与页面中的px单位计算方式不同),1dppx  =  96dpi。 | -| min-resolution | 设备的最小分辨率。 | -| max-resolution | 设备的最大分辨率。 | -| orientation | 屏幕的方向。
可选值:
-  orientation:  portrait(设备竖屏)
-  orientation:  landscape(设备横屏) | -| device-height | 设备的高度。 | -| min-device-height | 设备的最小高度。 | -| max-device-height | 设备的最大高度。 | -| device-width | 设备的宽度。 | -| device-type | 设备的类型。
可选值:default、tablet | -| min-device-width | 设备的最小宽度。 | -| max-device-width | 设备的最大宽度。 | -| round-screen | 屏幕类型,圆形屏幕为true,  非圆形屏幕为  false。 | -| dark-mode | 系统为深色模式时为true,否则为false。 | - -## 场景示例 - -下例中使用媒体查询,实现屏幕横竖屏切换时给页面文本应用不同的内容和样式的效果。 - -```ts -import mediaquery from '@ohos.mediaquery' - -let portraitFunc = null - -@Entry -@Component -struct MediaQueryExample { - @State color: string = '#DB7093' - @State text: string = 'Portrait' - listener = mediaquery.matchMediaSync('(orientation: landscape)') // 当设备横屏时条件成立 - - onPortrait(mediaQueryResult) { - if (mediaQueryResult.matches) { - this.color = '#FFD700' - this.text = 'Landscape' - } else { - this.color = '#DB7093' - this.text = 'Portrait' - } - } - - aboutToAppear() { - portraitFunc = this.onPortrait.bind(this) // 绑定当前应用实例 - this.listener.on('change', portraitFunc) - } - - build() { - Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { - Text(this.text).fontSize(50).fontColor(this.color) - } - .width('100%').height('100%') - } -} -``` - -横屏下文本内容为Landscape,颜色为#FFD700。 - -![zh-cn_image_0000001262954829](figures/zh-cn_image_0000001262954829.png) - -非横屏下文本内容为Portrait,颜色为#DB7093。 - -![zh-cn_image_0000001263074739](figures/zh-cn_image_0000001263074739.png) - -## 相关实例 - -使用媒体查询的自适应布局开发,有以下相关实例可供参考: - -- [`MediaQuery`:媒体查询(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/UI/ArkTsComponentClollection/MediaQuery) - diff --git a/zh-cn/application-dev/ui/ui-ts-layout-stack.md b/zh-cn/application-dev/ui/ui-ts-layout-stack.md deleted file mode 100644 index 19f7026c6f9868826fb716afeaaf0bf37e715b57..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/ui/ui-ts-layout-stack.md +++ /dev/null @@ -1,61 +0,0 @@ -# 层叠布局 - -层叠布局(StackLayout)用于在屏幕上预留一块区域来显示组件中的元素,提供元素可以重叠的布局。 -通过层叠容器[Stack](../reference/arkui-ts/ts-container-stack.md)实现,容器中的子元素依次入栈,后一个子元素覆盖前一个子元素显示。 - -## 对齐方式 - -设置子元素在容器内的对齐方式。支持左上,上中,右上,左,中,右,右下,中下,右下九种对齐方式,如下表所示: - -|名称| 描述| 图示 | -|---| ---|---| -|TopStart| 顶部起始端 |![](figures/stacktopstart.png)| -Top |顶部横向居中 |![](figures/stacktop.png)| -TopEnd| 顶部尾端 |![](figures/stacktopend.png)| -Start| 起始端纵向居中 |![](figures/stackstart.png)| -Center| 横向和纵向居中 |![](figures/stackcenter.png)| -End| 尾端纵向居中 |![](figures/stackend.png)| -BottomStart |底部起始端 |![](figures/stackbottomstart.png)| -Bottom| 底部横向居中 |![](figures/stackbottom.png)| -BottomEnd| 底部尾端 |![](figures/stackbottomend.png)| - -## Z序控制 - -Stack容器中兄弟组件显示层级关系可以通过[zIndex](../reference/arkui-ts/ts-universal-attributes-z-order.md) -属性改变。zIndex值越大,显示层级越高,即zIndex值大的组件会覆盖在zIndex值小的组件上方。 - -- 在层叠布局中,如果后面子元素尺寸大于前面子元素尺寸,则前面子元素完全隐藏。 - - ```ts - Stack({ alignContent: Alignment.BottomStart }) { - Column() { - Text('Stack子元素1').textAlign(TextAlign.End).fontSize(20) - }.width(100).height(100).backgroundColor(0xffd306) - Column() { - Text('Stack子元素2').fontSize(20) - }.width(150).height(150).backgroundColor(Color.Pink) - Column() { - Text('Stack子元素3').fontSize(20) - }.width(200).height(200).backgroundColor(Color.Grey) - }.margin({ top: 100 }).width(350).height(350).backgroundColor(0xe0e0e0) - ``` - - ![](figures/stack2.png) - - 上图中,最后的子元素3的尺寸大于前面的所有子元素,所以,前面两个元素完全隐藏。改变子元素1,子元素2的zIndex属性后,可以将元素展示出来。 - - ```ts - Stack({ alignContent: Alignment.BottomStart }) { - Column() { - Text('Stack子元素1').fontSize(20) - }.width(100).height(100).backgroundColor(0xffd306).zIndex(2) - Column() { - Text('Stack子元素2').fontSize(20) - }.width(150).height(150).backgroundColor(Color.Pink).zIndex(1) - Column() { - Text('Stack子元素3').fontSize(20) - }.width(200).height(200).backgroundColor(Color.Grey) - }.margin({ top: 100 }).width(350).height(350).backgroundColor(0xe0e0e0) - ``` - - ![](figures/stack1.png) diff --git a/zh-cn/application-dev/ui/ui-ts-overview.md b/zh-cn/application-dev/ui/ui-ts-overview.md deleted file mode 100644 index 2ea4568505d0858da89a57ad21687a034763a525..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/ui/ui-ts-overview.md +++ /dev/null @@ -1,89 +0,0 @@ -# 概述 - - -基于ArkTS的声明式开发范式的方舟开发框架是一套开发极简、高性能、支持跨设备的UI开发框架,支持开发者高效地构建OpenHarmony应用UI界面。 - -## 基础能力 - -使用基于ArkTS的声明式开发范式的方舟开发框架,采用更接近自然语义的编程方式,让开发者可以直观地描述UI界面,不必关心框架如何实现UI绘制和渲染,实现极简高效开发。开发框架不仅从组件、动画和状态管理三个维度来提供UI能力,还提供了系统能力接口以实现系统能力的极简调用。 -ArkTS语言的基础知识请参考[学习ArkTS语言](../quick-start/arkts-get-started.md)文档;此外,请参考[基于ArkTS的声明式开发范式API](../reference/arkui-ts/ts-universal-events-click.md)文档,全面地了解内置组件,更好地开发应用。 - -- **开箱即用的组件** - - 框架提供丰富的系统内置组件,可以通过链式调用的方式设置系统组件的渲染效果。开发者可以组合系统组件为自定义组件,通过这种方式将页面组件化为一个个独立的UI单元,实现页面不同单元的独立创建、开发和复用,使页面具有更强的工程性。 - -- **丰富的动效接口** - - 提供svg标准的绘制图形能力,同时开放了丰富的动效接口,开发者可以通过封装的物理模型或者调用动画能力接口来实现自定义动画轨迹。 - -- **状态与数据管理** - - 状态数据管理作为基于ArkTS的声明式开发范式的特色,通过功能不同的装饰器给开发者提供了清晰的页面更新渲染流程和管道。状态管理包括UI组件状态和应用程序状态,两者协作可以使开发者完整地构建整个应用的数据更新和UI渲染。 - -- **系统能力接口** - - 使用基于ArkTS的声明式开发范式的方舟开发框架,还封装了丰富的系统能力接口,开发者可以通过简单的接口调用,实现从UI设计到系统能力调用的极简开发。 - - -## 整体架构 - -![zh-cn_image_0000001169532276](figures/zh-cn_image_0000001169532276.png) - -- **声明式UI前端** - - 提供了UI开发范式的基础语言规范,并提供内置的UI组件、布局和动画,提供了多种状态管理机制,为应用开发者提供一系列接口支持。 - -- **语言运行时** - - 选用方舟语言运行时,提供了针对UI范式语法的解析能力、跨语言调用支持的能力和TS语言高性能运行环境。 - -- **声明式UI后端引擎** - - 后端引擎提供了兼容不同开发范式的UI渲染管线,提供多种基础组件、布局计算、动效、交互事件,提供了状态管理和绘制能力。 - -- **渲染引擎** - - 提供了高效的绘制能力,将渲染管线收集的渲染指令,绘制到屏幕的能力。 - -- **平台适配层** - - 提供了对系统平台的抽象接口,具备接入不同系统的能力,如系统渲染管线、生命周期调度等。 - - -## 相关实例 - -基于ArkTS的声明式开发范式的方舟开发框架,有以下相关实例可供参考: - -- [`Drag`:拖拽事件(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/master/ETSUI/Drag) - -- [`ArkUIAnimation`:动画(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/master/ETSUI/ArkUIAnimation) - -- [`MouseEvent`:鼠标事件(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/master/ETSUI/MouseEvent) - -- [`Chat`:聊天示例应用(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/Solutions/IM/Chat) - -- [`Shopping`:购物示例应用(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/master/AppSample/Shopping) - -- [`Lottie`:Lottie(ArkTS)(API9)(Full SDK)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/Solutions/Game/Lottie) - -- [`Flybird`:小鸟避障游戏(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/BasicFeature/TaskManagement/Flybird) - -- [`AdaptiveCapabilities`:多设备自适应能力(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/SuperFeature/MultiDeviceAppDev/AdaptiveCapabilities) - -- [`Game2048`:2048游戏(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/Solutions/Game/Game2048) - -- [`TransitionAnimation`:转场动画(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/master/ETSUI/TransitionAnimation) - -- [基础组件Slider的使用(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/SliderExample) - -- [转场动画的使用(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/TransitionAnimation) - -- [极简声明式UI范式(ArkTS)(API8)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/SimpleGalleryEts) - -- [购物应用(ArkTS)(API8)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/ShoppingEts) - -- [弹窗(ArkTS)(API8)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/CustomDialogEts) - -- [`UpgradePopup`:自定义弹窗(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/master/ETSUI/UpgradePopup) - -- [ComponentCollection:组件集合(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/UI/ArkTsComponentClollection/ComponentCollection) diff --git a/zh-cn/application-dev/ui/ui-ts-page-redirection-data-transmission.md b/zh-cn/application-dev/ui/ui-ts-page-redirection-data-transmission.md deleted file mode 100644 index c5701a8e1470f112a8c9c36bae7efd9aac02f8be..0000000000000000000000000000000000000000 --- a/zh-cn/application-dev/ui/ui-ts-page-redirection-data-transmission.md +++ /dev/null @@ -1,272 +0,0 @@ -# 页面跳转与数据传递 - -本节将学习页面跳转和数据传递,实现: - - -1. 页面跳转:点击食物分类列表页面的食物条目后,跳转到食物详情页;点击食物详情页的返回按钮,返回到食物列表页。 - -2. 页面间数据传递:点击不同的食物条目后,FoodDetail接受前一个页面的数据,渲染对应的食物详情页。 - - -## 页面跳转 - -声明式UI范式提供了两种机制来实现页面间的跳转: - -1. 路由容器组件Navigator,包装了页面路由的能力,指定页面target后,使其包裹的子组件都具有路由能力。 - -2. 路由RouterAPI接口,通过在页面上引入router,可以调用router的各种接口,从而实现页面路由的各种操作。 - -下面我们就分别学习这两种跳转机制来实现食物分类列表页面和食物详情页的链接。 - -1. 点击FoodListItem后跳转到FoodDetail页面。在FoodListItem内创建Navigator组件,使其子组件都具有路由功能,目标页面target为'pages/FoodDetail'。 - ```ts - @Component - struct FoodListItem { - private foodItem: FoodData - build() { - Navigator({ target: 'pages/FoodDetail' }) { - Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { - Image(this.foodItem.image) - .objectFit(ImageFit.Contain) - .height(40) - .width(40) - .backgroundColor('#FFf1f3f5') - .margin({ right: 16 }) - Text(this.foodItem.name) - .fontSize(14) - .flexGrow(1) - Text(this.foodItem.calories + ' kcal') - .fontSize(14) - } - .height(64) - } - .margin({ right: 24, left:32 }) - } - } - ``` - - ![zh-cn_image_0000001215318403](figures/zh-cn_image_0000001215318403.gif) - -2. 点击FoodGridItem后跳转到FoodDetail页面。调用页面路由router模块的push接口,将FoodDetail页面推到路由栈中,实现页面跳转。使用router路由API接口,需要先引入router。 - ```ts - import router from '@ohos.router' - - @Component - struct FoodGridItem { - private foodItem: FoodData - build() { - Column() { - ...... - } - .height(184) - .width('100%') - .onClick(() => { - router.pushUrl({ url: 'pages/FoodDetail' }) - }) - } - } - ``` - - ![zh-cn_image_0000001169918548](figures/zh-cn_image_0000001169918548.gif) - -3. 在FoodDetail页面增加回到食物列表页面的图标。在resources > base > media文件夹下存入回退图标Back.png。新建自定义组件PageTitle,包含后退的图标和Food Detail的文本,调用路由的router.back()接口,弹出路由栈最上面的页面,即返回上一级页面。 - ```ts - // FoodDetail.ets - import router from '@ohos.router' - - @Component - struct PageTitle { - build() { - Flex({ alignItems: ItemAlign.Start }) { - Image($r('app.media.Back')) - .width(21.8) - .height(19.6) - Text('Food Detail') - .fontSize(21.8) - .margin({left: 17.4}) - } - .height(61) - .backgroundColor('#FFedf2f5') - .padding({ top: 13, bottom: 15, left: 28.3 }) - .onClick(() => { - router.back() - }) - } - } - ``` - -4. 在FoodDetail组件内创建Stack组件,包含子组件FoodImageDisplay和PageTitle子组件,设置其对齐方式为左上对齐TopStart。 - ```ts - @Entry - @Component - struct FoodDetail { - build() { - Column() { - Stack( { alignContent: Alignment.TopStart }) { - FoodImageDisplay() - PageTitle() - } - ContentTable() - } - .alignItems(HorizontalAlign.Center) - } - } - ``` - - ![zh-cn_image_0000001214998349](figures/zh-cn_image_0000001214998349.png) - - -## 页面间数据传递 - -我们已经完成了FoodCategoryList页面和FoodDetail页面的跳转和回退,但是点击不同的FoodListItem/FoodGridItem,跳转的FoodDetail页面都是西红柿Tomato的详细介绍,这是因为没有构建起两个页面的数据传递,需要用到携带参数(parameter)路由。 - -1. 在FoodListItem组件的Navigator设置其params属性,params属性接受key-value的Object。 - ```ts - // FoodList.ets - @Component - struct FoodListItem { - private foodItem: FoodData - build() { - Navigator({ target: 'pages/FoodDetail' }) { - ...... - } - .params({ foodData: this.foodItem }) - } - } - ``` - - FoodGridItem调用的routerAPI同样有携带参数跳转的能力,使用方法和Navigator类似。 - - ```ts - router.pushUrl({ - url: 'pages/FoodDetail', - params: { foodData: this.foodItem } - }) - ``` - -2. FoodDetail页面引入FoodData类,在FoodDetail组件内添加foodItem成员变量。 - ```ts - // FoodDetail.ets - import { FoodData } from '../model/FoodData' - - @Entry - @Component - struct FoodDetail { - private foodItem: FoodData - build() { - ...... - } - } - ``` - -3. 获取foodData对应的value。调用router.getParams()['foodData']来获取到FoodCategoryList页面跳转来时携带的foodData对应的数据。 - ```ts - @Entry - @Component - struct FoodDetail { - private foodItem: FoodData = router.getParams()['foodData'] - - build() { - ...... - } - } - ``` - -4. 重构FoodDetail页面的组件。在构建视图时,FoodDetail页面的食物信息都是直接声明的常量,现在要用传递来的FoodData数据来对其进行重新赋值。整体的FoodDetail.ets代码如下。 - ```ts - @Component - struct PageTitle { - build() { - Flex({ alignItems: ItemAlign.Start }) { - Image($r('app.media.Back')) - .width(21.8) - .height(19.6) - Text('Food Detail') - .fontSize(21.8) - .margin({left: 17.4}) - } - .height(61) - .backgroundColor('#FFedf2f5') - .padding({ top: 13, bottom: 15, left: 28.3 }) - .onClick(() => { - router.back() - }) - } - } - - @Component - struct FoodImageDisplay { - private foodItem: FoodData - build() { - Stack({ alignContent: Alignment.BottomStart }) { - Image(this.foodItem.image) - .objectFit(ImageFit.Contain) - Text(this.foodItem.name) - .fontSize(26) - .fontWeight(500) - .margin({ left: 26, bottom: 17.4 }) - } - .height(357) - .backgroundColor('#FFedf2f5') - } - } - - @Component - struct ContentTable { - private foodItem: FoodData - - @Builder IngredientItem(title:string, name: string, value: string) { - Flex() { - Text(title) - .fontSize(17.4) - .fontWeight(FontWeight.Bold) - .layoutWeight(1) - Flex() { - Text(name) - .fontSize(17.4) - .flexGrow(1) - Text(value) - .fontSize(17.4) - } - .layoutWeight(2) - } - } - - build() { - Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) { - this.IngredientItem('Calories', 'Calories', this.foodItem.calories + 'kcal') - this.IngredientItem('Nutrition', 'Protein', this.foodItem.protein + 'g') - this.IngredientItem('', 'Fat', this.foodItem.fat + 'g') - this.IngredientItem('', 'Carbohydrates', this.foodItem.carbohydrates + 'g') - this.IngredientItem('', 'VitaminC', this.foodItem.vitaminC + 'mg') - } - .height(280) - .padding({ top: 30, right: 30, left: 30 }) - } - } - - @Entry - @Component - struct FoodDetail { - private foodItem: FoodData = router.getParams()['foodData'] - - build() { - Column() { - Stack( { alignContent: Alignment.TopStart }) { - FoodImageDisplay({ foodItem: this.foodItem }) - PageTitle() - } - ContentTable({ foodItem: this.foodItem }) - } - .alignItems(HorizontalAlign.Center) - } - } - ``` - -## 相关实例 - -针对页面布局与连接,有以下示例工程可供参考: - -- [`DefiningPageLayoutAndConnection`:页面布局和连接(ArkTS)(API8)](https://gitee.com/openharmony/app_samples/tree/master/ETSUI/DefiningPageLayoutAndConnection) - - 本示例构建了食物分类列表页面和食物详情页,向开发者展示了List布局、Grid布局以及页面路由的基本用法。 diff --git a/zh-cn/application-dev/web/Readme-CN.md b/zh-cn/application-dev/web/Readme-CN.md new file mode 100644 index 0000000000000000000000000000000000000000..7d74417e17ffc7e3468459dbbfc18f257855f0d9 --- /dev/null +++ b/zh-cn/application-dev/web/Readme-CN.md @@ -0,0 +1,17 @@ +# Web + +- [Web组件概述](web-component-overview.md) +- [使用Web组件加载页面](web-page-loading-with-web-components.md) +- 设置基本属性和事件 + - [设置深色模式](web-set-dark-mode.md) + - [上传文件](web-file-upload.md) + - [在新窗口中打开页面](web-open-in-new-window.md) + - [管理位置权限](web-geolocation-permission.md) +- 在应用中使用前端页面JavaScript + - [应用侧调用前端页面函数](web-in-app-frontend-page-function-invoking.md) + - [前端页面调用应用侧函数](web-in-page-app-function-invoking.md) + - [建立应用侧与前端页面数据通道](web-app-page-data-channel.md) +- [管理页面跳转及浏览记录导航](web-redirection-and-browsing-history-mgmt.md) +- [管理Cookie及数据存储](web-cookie-and-data-storage-mgmt.md) +- [自定义页面请求响应](web-resource-interception-request-mgmt.md) +- [使用Devtools工具调试前端页面](web-debugging-with-devtools.md) \ No newline at end of file diff --git a/zh-cn/application-dev/web/figures/debug-effect.png b/zh-cn/application-dev/web/figures/debug-effect.png new file mode 100644 index 0000000000000000000000000000000000000000..32c46cadbb99a6623532f50d14fa0750854c9a5d Binary files /dev/null and b/zh-cn/application-dev/web/figures/debug-effect.png differ diff --git a/zh-cn/application-dev/web/figures/resource-path.png b/zh-cn/application-dev/web/figures/resource-path.png new file mode 100644 index 0000000000000000000000000000000000000000..602c750894581e13296cb7cb77e9714f143983f9 Binary files /dev/null and b/zh-cn/application-dev/web/figures/resource-path.png differ diff --git a/zh-cn/application-dev/web/web-app-page-data-channel.md b/zh-cn/application-dev/web/web-app-page-data-channel.md new file mode 100644 index 0000000000000000000000000000000000000000..27e2ee162c7b4476dce38f650b29273d9a46fc0a --- /dev/null +++ b/zh-cn/application-dev/web/web-app-page-data-channel.md @@ -0,0 +1,143 @@ +# 建立应用侧与前端页面数据通道 + + +前端页面和应用侧之间可以用[createWebMessagePorts()](../reference/apis/js-apis-webview.md#createwebmessageports)接口创建消息端口来实现两端的通信。 + + +在下面的示例中,用侧页面中通过createWebMessagePorts方法创建消息端口,再把其中一个端口通过[postMessage()](../reference/apis/js-apis-webview.md#postmessage)接口发送到前端页面,便可以在前端页面和应用侧之间互相发送消息。 + + +- 应用侧代码。 + + ```ts + // xxx.ets + import web_webview from '@ohos.web.webview'; + + @Entry + @Component + struct WebComponent { + controller: web_webview.WebviewController = new web_webview.WebviewController(); + ports: web_webview.WebMessagePort[]; + @State sendFromEts: string = 'Send this message from ets to HTML'; + @State receivedFromHtml: string = 'Display received message send from HTML'; + + build() { + Column() { + // 展示接收到的来自HTML的内容 + Text(this.receivedFromHtml) + // 输入框的内容发送到html + TextInput({placeholder: 'Send this message from ets to HTML'}) + .onChange((value: string) => { + this.sendFromEts = value; + }) + + Button('postMessage') + .onClick(() => { + try { + // 1、创建两个消息端口。 + this.ports = this.controller.createWebMessagePorts(); + // 2、在应用侧的消息端口(如端口1)上注册回调事件。 + this.ports[1].onMessageEvent((result: web_webview.WebMessage) => { + let msg = 'Got msg from HTML:'; + if (typeof(result) === 'string') { + console.info(`received string message from html5, string is: ${result}`); + msg = msg + result; + } else if (typeof(result) === 'object') { + if (result instanceof ArrayBuffer) { + console.info(`received arraybuffer from html5, length is: ${result.byteLength}`); + msg = msg + 'lenght is ' + result.byteLength; + } else { + console.info('not support'); + } + } else { + console.info('not support'); + } + this.receivedFromHtml = msg; + }) + // 3、将另一个消息端口(如端口0)发送到HTML侧,由HTML侧保存并使用。 + this.controller.postMessage('__init_port__', [this.ports[0]], '*'); + } catch (error) { + console.error(`ErrorCode: ${error.code}, Message: ${error.message}`); + } + }) + + // 4、使用应用侧的端口给另一个已经发送到html的端口发送消息。 + Button('SendDataToHTML') + .onClick(() => { + try { + if (this.ports && this.ports[1]) { + this.ports[1].postMessageEvent(this.sendFromEts); + } else { + console.error(`ports is null, Please initialize first`); + } + } catch (error) { + console.error(`ErrorCode: ${error.code}, Message: ${error.message}`); + } + }) + Web({ src: $rawfile('xxx.html'), controller: this.controller }) + } + } + } + ``` + +- 前端页面代码。 + + ```html + + + + + + WebView Message Port Demo + + + + + +

WebView Message Port Demo

+
+
+
+
+

display received message send from ets

+ + + ``` diff --git a/zh-cn/application-dev/web/web-component-overview.md b/zh-cn/application-dev/web/web-component-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..194d99267c2ab27725aa78565530dd785ca7b224 --- /dev/null +++ b/zh-cn/application-dev/web/web-component-overview.md @@ -0,0 +1,14 @@ +# Web组件概述 + + +Web组件用于在应用程序中显示Web页面内容,为开发者提供页面加载、页面交互、页面调试等能力。 + + +- 页面加载:Web组件提供基础的前端页面加载的能力,包括加载网络页面、本地页面、Html格式文本数据。 + +- 页面交互:Web组件提供丰富的页面交互的方式,包括:设置前端页面深色模式,新窗口中加载页面,位置权限管理,Cookie管理,应用侧使用前端页面JavaScript等能力。 + +- 页面调试:Web组件支持使用Devtools工具调试前端页面。 + + +下面通过常见使用场景举例,来具体介绍Web组件功能特性。 diff --git a/zh-cn/application-dev/web/web-cookie-and-data-storage-mgmt.md b/zh-cn/application-dev/web/web-cookie-and-data-storage-mgmt.md new file mode 100644 index 0000000000000000000000000000000000000000..8f8dd1357c30b4c51a0434ffb8db34b029345eaf --- /dev/null +++ b/zh-cn/application-dev/web/web-cookie-and-data-storage-mgmt.md @@ -0,0 +1,131 @@ +# 管理Cookie及数据存储 + + +## Cookie管理 + +Cookie是网络访问过程中,由服务端发送给客户端的一小段数据。客户端可持有该数据,并在后续访问该服务端时,方便服务端快速对客户端身份、状态等进行识别。 + +Web组件提供了WebCookieManager类,用于管理Web组件的Cookie信息。Cookie信息保存在应用沙箱路径下/proc/{pid}/root/data/storage/el2/base/cache/web/Cookiesd的文件中。 + +下面以[setCookie()](../reference/apis/js-apis-webview.md#setcookie)接口举例,为“www.example.com”设置单个Cookie的值“value=test”。其他Cookie的相关功能及使用,请参考[WebCookieManager()](../reference/apis/js-apis-webview.md#webcookiemanager)接口文档。 + + +```ts +// xxx.ets +import web_webview from '@ohos.web.webview'; + +@Entry +@Component +struct WebComponent { + controller: web_webview.WebviewController = new web_webview.WebviewController(); + + build() { + Column() { + Button('setCookie') + .onClick(() => { + try { + web_webview.WebCookieManager.setCookie('https://www.example.com', 'value=test'); + } catch (error) { + console.error(`ErrorCode: ${error.code}, Message: ${error.message}`); + } + }) + Web({ src: 'www.example.com', controller: this.controller }) + } + } +} +``` + + +## 缓存与存储管理 + +在访问网站时,网络资源请求是相对比较耗时的。开发者可以通过Cache、Dom Storage等手段将资源保持至本地,以提升访问同一网站的速度。 + + +### Cache + +使用[cacheMode()](../reference/arkui-ts/ts-basic-components-web.md#cachemode%E6%9E%9A%E4%B8%BE%E8%AF%B4%E6%98%8E)配置页面资源的缓存模式,Web组件为开发者提供四种缓存模式,分别为: + +- Default : 优先使用未过期的缓存,如果缓存不存在,则从网络获取。 + +- None : 加载资源使用cache,如果cache中无该资源则从网络中获取。 + +- Online : 加载资源不使用cache,全部从网络中获取。 + +- Only :只从cache中加载资源。 + + +在下面的示例中,选用缓存设置为None模式。 + + + +```ts +// xxx.ets +import web_webview from '@ohos.web.webview'; + +@Entry +@Component +struct WebComponent { + @State mode: CacheMode = CacheMode.None; + controller: web_webview.WebviewController = new web_webview.WebviewController(); + build() { + Column() { + Web({ src: 'www.example.com', controller: this.controller }) + .cacheMode(this.mode) + } + } +} +``` + + + 同时,为了获取最新资源,开发者可以通过[removeCache()](../reference/apis/js-apis-webview.md#removecache)接口清除已经缓存的资源,示例代码如下: + +```ts +// xxx.ets +import web_webview from '@ohos.web.webview'; + +@Entry +@Component +struct WebComponent { + @State mode: CacheMode = CacheMode.None; + controller: web_webview.WebviewController = new web_webview.WebviewController(); + build() { + Column() { + Button('removeCache') + .onClick(() => { + try { + // 设置为true时同时清除rom和ram中的缓存,设置为false时只清除ram中的缓存 + this.controller.removeCache(true); + } catch (error) { + console.error(`ErrorCode: ${error.code}, Message: ${error.message}`); + } + }) + Web({ src: 'www.example.com', controller: this.controller }) + .cacheMode(this.mode) + } + } +} +``` + + +### Dom Storage + +Dom Storage包含了Session Storage和Local Storage两类。前者为临时数据,其存储与释放跟随会话生命周期;后者为可持久化数据,落盘在应用目录下。两者的数据均通过Key-Value的形式存储,通常在访问需要客户端存储的页面时使用。开发者可以通过Web组件的属性接口[domStorageAccess()](../reference/arkui-ts/ts-basic-components-web.md#domstorageaccess)进行使能配置,示例如下: + + + +```ts +// xxx.ets +import web_webview from '@ohos.web.webview'; + +@Entry +@Component +struct WebComponent { + controller: web_webview.WebviewController = new web_webview.WebviewController(); + build() { + Column() { + Web({ src: 'www.example.com', controller: this.controller }) + .domStorageAccess(true) + } + } +} +``` diff --git a/zh-cn/application-dev/web/web-debugging-with-devtools.md b/zh-cn/application-dev/web/web-debugging-with-devtools.md new file mode 100644 index 0000000000000000000000000000000000000000..2917bca7bb527dd54af497783ad06faf2bb802f7 --- /dev/null +++ b/zh-cn/application-dev/web/web-debugging-with-devtools.md @@ -0,0 +1,45 @@ +# 使用Devtools工具调试前端页面 + + +Web组件支持使用DevTools工具调试前端页面。DevTools是一个 Web前端开发调试工具,提供了电脑上调试移动设备前端页面的能力。开发者通过[setWebDebuggingAccess()](../reference/apis/js-apis-webview.md#setwebdebuggingaccess)接口开启Web组件前端页面调试能力,利用DevTools工具可以在电脑上调试移动设备上的前端网页。 + + +使用DevTools工具,可以执行以下步骤: + + +1. 在应用代码中开启Web调试开关,具体如下: + + ```ts + // xxx.ets + import web_webview from '@ohos.web.webview'; + + @Entry + @Component + struct WebComponent { + controller: web_webview.WebviewController = new web_webview.WebviewController(); + aboutToAppear() { + // 配置web开启调试模式 + web_webview.WebviewController.setWebDebuggingAccess(true); + } + build() { + Column() { + Web({ src: 'www.example.com', controller: this.controller }) + } + } + } + ``` + +2. 将设备连接上电脑,在电脑端配置端口映射,配置方法如下: + + ``` + // 添加映射 + hdc fport tcp:9222 tcp:9222 + // 查看映射 + hdc fport ls + ``` + +3. 在电脑端chrome浏览器地址栏中输入chrome://inspect/\#devices,页面识别到设备后,就可以开始页面调试。调试效果如下: + + **图1** 页面调试效果图 + + ![debug-effect](figures/debug-effect.png) diff --git a/zh-cn/application-dev/web/web-file-upload.md b/zh-cn/application-dev/web/web-file-upload.md new file mode 100644 index 0000000000000000000000000000000000000000..d2d37e6734fabb5f8dabd3a1df04a0ed62c20c89 --- /dev/null +++ b/zh-cn/application-dev/web/web-file-upload.md @@ -0,0 +1,52 @@ +# 上传文件 + + +Web组件支持前端页面选择文件上传功能,应用开发者可以使用[onShowFileSelector()](../reference/arkui-ts/ts-basic-components-web.md#onshowfileselector9)接口来处理前端页面文件上传的请求。 + + +下面的示例中,当用户在前端页面点击文件上传按钮,应用侧在[onShowFileSelector()](../reference/arkui-ts/ts-basic-components-web.md#onshowfileselector9)接口中收到文件上传请求,在此接口中开发者将上传的本地文件路径设置给前端页面。 + + +- 应用侧代码。 + + ```ts + // xxx.ets + import web_webview from '@ohos.web.webview'; + @Entry + @Component + struct WebComponent { + controller: WebController = new WebController() + build() { + Column() { + // 加载本地local.html页面 + Web({ src: $rawfile('local.html'), controller: this.controller }) + .onShowFileSelector((event) => { + // 开发者设置要上传的文件路径 + let fileList: Array = [ + 'xxx/test.png', + ] + event.result.handleFileList(fileList) + return true; + }) + } + } + } + ``` + + +- local.html页面代码。 + + ```html + + + + + Document + + + + +
+ + + ``` diff --git a/zh-cn/application-dev/web/web-geolocation-permission.md b/zh-cn/application-dev/web/web-geolocation-permission.md new file mode 100644 index 0000000000000000000000000000000000000000..82866d3ecd6f058d95b354e4c5d788f03b2a21ef --- /dev/null +++ b/zh-cn/application-dev/web/web-geolocation-permission.md @@ -0,0 +1,73 @@ +# 管理位置权限 + + +Web组件提供位置权限管理能力。开发者可以通过[onGeolocationShow()](../reference/arkui-ts/ts-basic-components-web.md#ongeolocationshow)接口对某个网站进行位置权限管理。Web组件根据接口响应结果,决定是否赋予前端页面权限。获取设备位置,需要开发者配置[ohos.permission.LOCATION](../security/accesstoken-guidelines.md)权限。 + + +在下面的示例中,用户点击前端页面"获取位置"按钮,Web组件通过弹窗的形式通知应用侧位置权限请求消息。 + + +- 前端页面代码。 + + ```html + + + +

位置信息

+ + + + + ``` + + +- 应用代码。 + + ```ts + // xxx.ets + import web_webview from '@ohos.web.webview'; + + @Entry + @Component + struct WebComponent { + controller: web_webview.WebviewController = new web_webview.WebviewController(); + build() { + Column() { + Web({ src:$rawfile('getLocation.html'), controller:this.controller }) + .geolocationAccess(true) + .onGeolocationShow((event) => { // 地理位置权限申请通知 + AlertDialog.show({ + title: '位置权限请求', + message: '是否允许获取位置信息', + primaryButton: { + value: 'cancel', + action: () => { + event.geolocation.invoke(event.origin, false, false); // 不允许此站点地理位置权限请求 + } + }, + secondaryButton: { + value: 'ok', + action: () => { + event.geolocation.invoke(event.origin, true, false); // 允许此站点地理位置权限请求 + } + }, + cancel: () => { + event.geolocation.invoke(event.origin, false, false); // 不允许此站点地理位置权限请求 + } + }) + }) + } + } + } + ``` diff --git a/zh-cn/application-dev/web/web-in-app-frontend-page-function-invoking.md b/zh-cn/application-dev/web/web-in-app-frontend-page-function-invoking.md new file mode 100644 index 0000000000000000000000000000000000000000..2a490281b417a51f408e09acf18b3e62c66a917e --- /dev/null +++ b/zh-cn/application-dev/web/web-in-app-frontend-page-function-invoking.md @@ -0,0 +1,48 @@ +# 应用侧调用前端页面函数 + + +应用侧可以通过[runJavaScript()](../reference/apis/js-apis-webview.md#runjavascript)方法调用前端页面的JavaScript相关函数。 + + +在下面的示例中,点击应用侧的“runJavaScript”按钮时,来触发前端页面的htmlTest()方法。 + + +- 前端页面代码。 + + ```html + + + + + + + + ``` + + +- 应用侧代码。 + + ```ts + // xxx.ets + import web_webview from '@ohos.web.webview'; + + @Entry + @Component + struct WebComponent { + webviewController: web_webview.WebviewController = new web_webview.WebviewController(); + + build() { + Column() { + Web({ src: $rawfile('index.html'), controller: this.webviewController}) + Button('runJavaScript') + .onClick(() => { + this.webviewController.runJavaScript('htmlTest()'); + }) + } + } + } + ``` diff --git a/zh-cn/application-dev/web/web-in-page-app-function-invoking.md b/zh-cn/application-dev/web/web-in-page-app-function-invoking.md new file mode 100644 index 0000000000000000000000000000000000000000..caffe3d2cc2a18efae36fd67650113202e5e4d2d --- /dev/null +++ b/zh-cn/application-dev/web/web-in-page-app-function-invoking.md @@ -0,0 +1,113 @@ +# 前端页面调用应用侧函数 + + +开发者使用Web组件将应用侧代码注册到前端页面中,注册完成之后,前端页面中使用注册的对象名称就可以调用应用侧的函数,实现在前端页面中调用应用侧方法。 + + +注册应用侧代码有两种方式,一种在Web组件初始化使用调用,使用[javaScriptProxy()](../reference/arkui-ts/ts-basic-components-web.md#javascriptproxy)接口。另外一种在Web组件初始化完成后调用,使用[registerJavaScriptProxy()](../reference/apis/js-apis-webview.md#registerjavascriptproxy)接口。 + + +在下面的示例中,将test()方法注册在前端页面中, 该函数可以在前端页面触发运行。 + + +- [javaScriptProxy()](../reference/arkui-ts/ts-basic-components-web.md#javascriptproxy)接口使用示例如下。 + + ```ts + // xxx.ets + import web_webview from '@ohos.web.webview'; + + @Entry + @Component + struct WebComponent { + webviewController: web_webview.WebviewController = new web_webview.WebviewController(); + // 声明需要注册的对象 + testObj = { + test: () => { + return 'ArkTS Hello World!'; + } + } + + build() { + Column() { + // web组件加载本地index.html页面 + Web({ src: $rawfile('index.html'), controller: this.webviewController}) + // 将对象注入到web端 + .javaScriptProxy({ + object: this.testObj, + name: "testObjName", + methodList: ["test"], + controller: this.webviewController + }) + } + } + } + ``` + + +- 应用侧使用registerJavaScriptProxy()接口注册。 + + ```ts + // xxx.ets + import web_webview from '@ohos.web.webview'; + + @Entry + @Component + struct Index { + webviewController: web_webview.WebviewController = new web_webview.WebviewController(); + testObj = { + test: (data) => { + return "ArkUI Web Component"; + }, + toString: () => { + console.info('Web Component toString'); + } + } + + build() { + Column() { + Button('refresh') + .onClick(() => { + try { + this.webviewController.refresh(); + } catch (error) { + console.error(`Errorcode: ${error.code}, Message: ${error.message}`); + } + }) + Button('Register JavaScript To Window') + .onClick(() => { + try { + this.webviewController.registerJavaScriptProxy(this.testObj, "objName", ["test", "toString"]); + } catch (error) { + console.error(`Errorcode: ${error.code}, Message: ${error.message}`); + } + }) + Web({ src: $rawfile('index.html'), controller: this.webviewController }) + } + } + } + ``` + + > **说明:** + > + > 使用[registerJavaScriptProxy()](../reference/apis/js-apis-webview.md#registerjavascriptproxy)接口注册方法时,注册后需调用[refresh()](../reference/apis/js-apis-webview.md#refresh)接口生效。 + + +- index.htm前端页面触发应用侧代码。 + + ```html + + + + + +

+ + + + ``` diff --git a/zh-cn/application-dev/web/web-open-in-new-window.md b/zh-cn/application-dev/web/web-open-in-new-window.md new file mode 100644 index 0000000000000000000000000000000000000000..99a244544598060f16881ab6b020eb431e6f8ecf --- /dev/null +++ b/zh-cn/application-dev/web/web-open-in-new-window.md @@ -0,0 +1,69 @@ +# 在新窗口中打开页面 + + +Web组件提供了在新窗口打开页面的能力,开发者可以通过[multiWindowAccess()](../reference/arkui-ts/ts-basic-components-web.md#multiwindowaccess9)接口来设置是否允许网页在新窗口打开。当有新窗口打开时,应用侧会在[onWindowNew()](../reference/arkui-ts/ts-basic-components-web.md#onwindownew9)接口中收到Web组件新窗口事件,开发者需要在此接口事件中,新建窗口来处理Web组件窗口请求。 + + +> **说明:** +> +> - [allowWindowOpenMethod()](../reference/arkui-ts/ts-basic-components-web.md#allowwindowopenmethod9)接口设置为true时,前端页面通过JavaScript函数调用的方式打开新窗口。 +> +> - 如果开发者在[onWindowNew()](../reference/arkui-ts/ts-basic-components-web.md#onwindownew9)接口通知中不需要打开新窗口,需要将[ControllerHandler.setWebController()](../reference/arkui-ts/ts-basic-components-web.md#onwindownew9)接口返回值设置成null。 + + +如下面的本地示例,当用户点击“新窗口中打开网页”按钮时,应用侧会在[onWindowNew()](../reference/arkui-ts/ts-basic-components-web.md#onwindownew9)接口中收到Web组件新窗口事件。 + + +- 应用侧代码。 + + 创建新窗口的方法可参考[Web开发相关例子](https://gitee.com/openharmony/applications_app_samples/tree/master/code/BasicFeature/Web/Browser)。 + + ```ts + // xxx.ets + import web_webview from '@ohos.web.webview'; + @Entry + @Component + struct WebComponent { + controller: web_webview.WebviewController = new web_webview.WebviewController(); + build() { + Column() { + Web({ src:$rawfile("window.html"), controller: this.controller }) + .multiWindowAccess(true) + .onWindowNew((event) => { + console.info("onWindowNew..."); + var popController: web_webview.WebviewController = new web_webview.WebviewController(); + // 开发者需要在此处新建窗口,跟popController关联,并且将popController返回给Web组件。如果不需要打开新窗口请将返回值设置为event.handler.setWebController(null); + event.handler.setWebController(popController); + }) + } + } + } + ``` + + +- window.html页面代码。 + + ```html + + + + + WindowEvent + + + + + + + + ``` diff --git a/zh-cn/application-dev/web/web-page-loading-with-web-components.md b/zh-cn/application-dev/web/web-page-loading-with-web-components.md new file mode 100644 index 0000000000000000000000000000000000000000..98a7ea1360330dd20be8fcfc3afe6e3f9de01922 --- /dev/null +++ b/zh-cn/application-dev/web/web-page-loading-with-web-components.md @@ -0,0 +1,140 @@ +# 使用Web组件加载页面 + + +页面加载是Web组件的基本功能。根据页面加载数据来源可以分为三种常用场景,包括加载网络页面、加载本地页面、加载HTML格式的富文本数据。 + + +页面加载过程中,若涉及网络资源获取,需要配置[ohos.permission.INTERNET](../security/accesstoken-guidelines.md)网络访问权限。 + + +## 加载网络页面 + +开发者可以在Web组件创建的时候指定默认加载的网络页面 。在默认页面加载完成后,如果开发者需要变更此Web组件显示的网络页面,可以通过调用[loadUrl()](../reference/apis/js-apis-webview.md#loadurl)接口加载指定网络网页。 + + +在下面的示例中,在Web组件加载完“www.example.com”页面后,开发者可通过loadUrl接口将此Web组件显示页面变更为“www.example1.com”。 + + + +```ts +// xxx.ets +import web_webview from '@ohos.web.webview'; + +@Entry +@Component +struct WebComponent { + webviewController: web_webview.WebviewController = new web_webview.WebviewController(); + + build() { + Column() { + Button('loadUrl') + .onClick(() => { + try { + // 点击按钮时,通过loadUrl,跳转到www.example1.com + this.webviewController.loadUrl('www.example1.com'); + } catch (error) { + console.error(`ErrorCode: ${error.code}, Message: ${error.message}`); + } + }) + // 组件创建时,加载www.example.com + Web({ src: 'www.example.com', controller: this.webviewController}) + } + } +} +``` + + +## 加载本地页面 + +将本地页面文件放在应用的rawfile目录下,开发者可以在Web组件创建的时候指定默认加载的本地页面 ,并且加载完成后可通过调用[loadUrl()](../reference/apis/js-apis-webview.md#loadurl)接口变更当前Web组件的页面。 + + +在下面的示例中展示加载本地页面文件的方法: + + +- 将资源文件放置在应用的resources/rawfile目录下。 + + **图1** 资源文件路径   + + ![resource-path](figures/resource-path.png) + + +- 应用侧代码。 + + ```ts + // xxx.ets + import web_webview from '@ohos.web.webview'; + + @Entry + @Component + struct WebComponent { + webviewController: web_webview.WebviewController = new web_webview.WebviewController(); + + build() { + Column() { + Button('loadUrl') + .onClick(() => { + try { + // 点击按钮时,通过loadUrl,跳转到local1.html + this.webviewController.loadUrl($rawfile("local1.html")); + } catch (error) { + console.error(`ErrorCode: ${error.code}, Message: ${error.message}`); + } + }) + // 组件创建时,通过$rawfile加载本地文件local.html + Web({ src: $rawfile("local.html"), controller: this.webviewController }) + } + } + } + ``` + + +- local.html页面代码。 + + ```html + + + + +

Hello World

+ + + ``` + + +## 加载HTML格式的文本数据 + +Web组件可以通过[loadData()](../reference/apis/js-apis-webview.md#loaddata)接口实现加载HTML格式的文本数据。当开发者不需要加载整个页面,只需要显示一些页面片段时,可通过此功能来快速加载页面。 + + + +```ts +// xxx.ets +import web_webview from '@ohos.web.webview'; + +@Entry +@Component +struct WebComponent { + controller: web_webview.WebviewController = new web_webview.WebviewController(); + + build() { + Column() { + Button('loadData') + .onClick(() => { + try { + // 点击按钮时,通过loadData,加载HTML格式的文本数据 + this.controller.loadData( + 'Source:
source
', + 'text/html', + 'UTF-8' + ); + } catch (error) { + console.error(`ErrorCode: ${error.code}, Message: ${error.message}`); + } + }) + // 组件创建时,加载www.example.com + Web({ src: 'www.example.com', controller: this.controller }) + } + } +} +``` diff --git a/zh-cn/application-dev/web/web-redirection-and-browsing-history-mgmt.md b/zh-cn/application-dev/web/web-redirection-and-browsing-history-mgmt.md new file mode 100644 index 0000000000000000000000000000000000000000..4d64482bf4e2ed1751e2e1f120064d481fa670cf --- /dev/null +++ b/zh-cn/application-dev/web/web-redirection-and-browsing-history-mgmt.md @@ -0,0 +1,157 @@ +# 管理页面跳转及浏览记录导航 + + +## 历史记录导航 + +使用者在前端页面点击网页中的链接时,Web组件默认会自动打开并加载目标网址。当前端页面替换为新的加载链接时,会自动记录已经访问的网页地址。可以通过[forward()](../reference/apis/js-apis-webview.md#forward)和[backward()](../reference/apis/js-apis-webview.md#backward)接口向前/向后浏览上一个/下一个历史记录。 + + 在下面的示例中,点击应用的按钮来触发前端页面的后退操作。 + +```ts +// xxx.ets +import web_webview from '@ohos.web.webview'; + +@Entry +@Component +struct WebComponent { + webviewController: web_webview.WebviewController = new web_webview.WebviewController(); + build() { + Column() { + Button('loadData') + .onClick(() => { + if (this.webviewController.accessBackward()) { + this.webviewController.backward(); + return true; + } + }) + Web({ src: 'https://www.example.com/cn/', controller: this.webviewController}) + } + } +} +``` + + +如果存在历史记录,[accessBackward()](../reference/apis/js-apis-webview.md#accessbackward)接口会返回true。同样,您可以使用[accessForward()](../reference/apis/js-apis-webview.md#accessforward)接口检查是否存在前进的历史记录。如果您不执行检查,那么当用户浏览到历史记录的末尾时,调用[forward()](../reference/apis/js-apis-webview.md#forward)和[backward()](../reference/apis/js-apis-webview.md#backward)接口时将不执行任何操作。 + + +## 页面跳转 + +当点击网页中的链接需要跳转到应用内其他页面时,可以通过使用Web组件的[onUrlLoadIntercept()](../reference/arkui-ts/ts-basic-components-web.md#onurlloadintercept)接口来实现。 + +在下面的示例中,应用首页Index.ets加载前端页面route.html,在前端route.html页面点击超链接,可跳转到应用的ProfilePage.ets页面。 + +- 应用首页index.ets页面代码。 + + ```ts + // index.ets + import web_webview from '@ohos.web.webview'; + import router from '@ohos.router'; + @Entry + @Component + struct WebComponent { + webviewController: web_webview.WebviewController = new web_webview.WebviewController(); + + build() { + Column() { + Web({ src: $rawfile('route.html'), controller: this.webviewController }) + .onUrlLoadIntercept((event) => { + let url: string = event.data as string; + if (url.indexOf('native://') === 0) { + // 跳转其他界面 + router.pushUrl({ url:url.substring(9) }) + return true; + } + return false; + }) + } + } + } + ``` + +- route.html前端页面代码。 + + ```html + + + + + + + + ``` + +- 跳转页面ProfilePage.ets代码。 + + ```ts + @Entry + @Component + struct ProfilePage { + @State message: string = 'Hello World'; + + build() { + Column() { + Text(this.message) + .fontSize(20) + } + } + } + ``` + + +## 跨应用跳转 + +Web组件可以实现点击前端页面超链接跳转到其他应用。 + +在下面的示例中,点击call.html前端页面中的超连接,跳转到电话应用的拨号界面。 + +- 应用侧代码。 + + ```ts + // xxx.ets + import web_webview from '@ohos.web.webview'; + import call from '@ohos.telephony.call'; + + @Entry + @Component + struct WebComponent { + webviewController: web_webview.WebviewController = new web_webview.WebviewController(); + + build() { + Column() { + Web({ src: $rawfile('xxx.html'), controller: this.webviewController}) + .onUrlLoadIntercept((event) => { + let url: string = event.data as string; + // 判断链接是否为拨号链接 + if (url.indexOf('tel://') === 0) { + // 跳转拨号界面 + call.makeCall(url.substring(6), (err) => { + if (!err) { + console.info('make call succeeded.'); + } else { + console.info('make call fail, err is:' + JSON.stringify(err)); + } + }); + return true; + } + return false; + }) + } + } + } + ``` + +- 前端页面call.html代码。 + + ```html + + + + + + + + ``` diff --git a/zh-cn/application-dev/web/web-resource-interception-request-mgmt.md b/zh-cn/application-dev/web/web-resource-interception-request-mgmt.md new file mode 100644 index 0000000000000000000000000000000000000000..6a01c946eaed3e56c21827337484bb105334390c --- /dev/null +++ b/zh-cn/application-dev/web/web-resource-interception-request-mgmt.md @@ -0,0 +1,70 @@ +# 自定义页面请求响应 + + +Web组件支持在应用拦截到页面请求后自定义响应请求能力。开发者通过[onInterceptRequest()](../reference/arkui-ts/ts-basic-components-web.md#oninterceptrequest9)接口来实现自定义资源请求响应 。自定义请求能力可以用于开发者自定义Web页面响应、自定义文件资源响应等场景。 + + +Web网页上发起资源加载请求,应用层收到资源请求消息。应用层构造本地资源响应消息发送给Web内核。Web内核解析应用层响应信息,根据此响应信息进行页面资源加载。 + + +在下面的示例中,Web组件通过拦截页面请求“https://www.intercept.com/test.html”, 在应用侧代码构建响应资源,实现自定义页面响应场景。 + + +- 前端页面example.html代码。 + + ```html + + + + + example + + + + intercept test! + + + ``` + +- 应用侧代码。 + + ```ts + // xxx.ets + import web_webview from '@ohos.web.webview'; + + @Entry + @Component + struct WebComponent { + controller: web_webview.WebviewController = new web_webview.WebviewController() + responseResource: WebResourceResponse = new WebResourceResponse() + // 开发者自定义响应数据 + @State webData: string = '\n' + + '\n'+ + '\n'+ + 'intercept test\n'+ + '\n'+ + '\n'+ + '

intercept ok

\n'+ + '\n'+ + '' + build() { + Column() { + Web({ src: $rawfile('example.html'), controller: this.controller }) + .onInterceptRequest((event) => { + console.info('url:' + event.request.getRequestUrl()); + // 拦截页面请求 + if (event.request.getRequestUrl() !== 'https://www.intercept.com/test.html') { + return null; + } + // 构造响应数据 + this.responseResource.setResponseData(this.webData); + this.responseResource.setResponseEncoding('utf-8'); + this.responseResource.setResponseMimeType('text/html'); + this.responseResource.setResponseCode(200); + this.responseResource.setReasonMessage('OK'); + return this.responseResource; + }) + } + } + } + ``` diff --git a/zh-cn/application-dev/web/web-set-dark-mode.md b/zh-cn/application-dev/web/web-set-dark-mode.md new file mode 100644 index 0000000000000000000000000000000000000000..687249c6d5ee57c5ef0fc32c2721cc3ec90ec821 --- /dev/null +++ b/zh-cn/application-dev/web/web-set-dark-mode.md @@ -0,0 +1,50 @@ +# 设置深色模式 + + +Web组件支持对前端页面进行深色模式配置。 + + +- 通过[darkMode()](../reference/arkui-ts/ts-basic-components-web.md#darkmode9)接口可以配置不同的深色模式,[WebDarkMode.Off](../reference/arkui-ts/ts-basic-components-web.md#webdarkmode9%E6%9E%9A%E4%B8%BE%E8%AF%B4%E6%98%8E)模式表示关闭深色模式。[WebDarkMode.On](../reference/arkui-ts/ts-basic-components-web.md#webdarkmode9%E6%9E%9A%E4%B8%BE%E8%AF%B4%E6%98%8E)表示开启深色模式,并且深色模式跟随前端页面。[WebDarkMode.Auto](../reference/arkui-ts/ts-basic-components-web.md#webdarkmode9%E6%9E%9A%E4%B8%BE%E8%AF%B4%E6%98%8E)表示开启深色模式,并且深色模式跟随系统。 + 在下面的示例中, 通过[darkMode()](../reference/arkui-ts/ts-basic-components-web.md#darkmode9)接口将页面深色模式配置为跟随系统。 + + ```ts + // xxx.ets + import web_webview from '@ohos.web.webview'; + + @Entry + @Component + struct WebComponent { + controller: web_webview.WebviewController = new web_webview.WebviewController(); + @State mode: WebDarkMode = WebDarkMode.Auto; + build() { + Column() { + Web({ src: 'www.example.com', controller: this.controller }) + .darkMode(this.mode) + } + } + } + ``` + + +- 通过[forceDarkAccess()](../reference/arkui-ts/ts-basic-components-web.md#forcedarkaccess9)接口可将前端页面强制配置深色模式,且深色模式不跟随前端页面和系统。配置该模式时候,需要将深色模式配置成WebDarkMode.On。 + 在下面的示例中, 通过[forceDarkAccess()](../reference/arkui-ts/ts-basic-components-web.md#forcedarkaccess9)接口将页面强制配置为深色模式。 + + ```ts + // xxx.ets + import web_webview from '@ohos.web.webview'; + + @Entry + @Component + struct WebComponent { + controller: web_webview.WebviewController = new web_webview.WebviewController(); + @State mode: WebDarkMode = WebDarkMode.On; + @State access: boolean = true; + build() { + Column() { + Web({ src: 'www.example.com', controller: this.controller }) + .darkMode(this.mode) + .forceDarkAccess(this.access) + } + } + } + ```