
【鸿蒙开发】第二十八章 应用状态的讲解、状态持久化、网络管理、应用数据持久化、文件上传下载
AppStorage是在应用启动的时候会被创建的单例。它的目的是为了提供应用状态数据的中心存储,这些状态数据在应用级别都是可访问的。AppStorage将在应用运行过程保留其属性。属性通过唯一的键字符串值访问。AppStorage可以和UI组件同步,且可以在应用业务逻辑中被访问。AppStorage支持应用的主线程内多个UIAbility实例间的状态共享。AppStorage中的属性可以被双向同步
目录
1.3 PersistentStorage:持久化存储UI状态
1. 普通跳转,通过页面的name去跳转,并可以携带param。
2. 带返回回调的跳转,跳转时添加onPop回调,能在页面出栈时获取返回信息,并进行处理。
3. 带错误码的跳转,跳转结束会触发异步回调,返回错误码信息。
1 应用状态
- LocalStorage:页面级UI状态存储,通常用于UIAbility内、页面间的状态共享。(内存- 注意:和前端的区分开,它非持久化,非全应用)
- AppStorage:特殊的单例LocalStorage对象,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储。(内存-非持久化-退出应用同样消失)
- PersistentStorage:持久化存储UI状态,通常和AppStorage配合使用,选择AppStorage存储的数据写入磁盘,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同。(写入磁盘-持久化状态-退出应用 数据同样存在)
- Environment:应用程序运行的设备的环境参数,环境参数会同步到AppStorage中,可以和 使用。
1.1 LocalStorage:页面级UI状态存储
LocalStorage
是页面级的UI状态存储,通过@Entry
装饰器接收的参数可以在页面内共享同一个LocalStorage
实例。LocalStorage
也可以在UIAbility
内,页面间共享状态。用法
- 创建
LocalStorage
实例:const storage = new LocalStorage({ key: value })
- 单向
@LocalStorageProp('user')
组件内可变- 双向
@LocalStorageLink('user')
全局均可变
1.1.1 两个页面共享一个对象
- 创建一个共享的状态
export class PersonModel {
username: string = '张三'
age: number = 18
}
let pp = new PersonModel()
pp.username = "东林"
pp.age = 29
// 创建新实例并使用给定对象初始化
let para: Record<string, PersonModel> = { 'user': pp };
let local: LocalStorage = new LocalStorage(para);
export { local }
- 页面1
import { local, PersonModel } from '../models/PersonModel'
import { router } from '@kit.ArkUI'
@Entry(local)
@Component
struct LocalStorageCaseA {
@LocalStorageLink("user")
user: PersonModel = new PersonModel
build() {
Column() {
Text(this.user.username)
.fontSize(40)
.onClick(() => {
this.user.username = "老王"
router.pushUrl({url:'pages/LocalStorageCaseB'})
})
}
.height('100%')
.width('100%')
}
}
- 页面2
import { local, PersonModel } from '../models/PersonModel'
import { router } from '@kit.ArkUI'
@Entry(local)
@Component
struct LocalStorageCaseB {
@LocalStorageLink("user")
user: PersonModel = new PersonModel()
build() {
Column() {
Text(this.user.username)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
this.user.username = "李四"
router.back()
})
Text(this.user.age.toString())
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.height('100%')
.width('100%')
}
}
1.1.2 页面间共享
如果你想在
UIAbility
中共享某个localStorage,可以在入口处直接初始化传入
- 可以在loadContent过程中直接传入创建的LocalStorage
const storage = LocalStorage.GetShared()
得到实例- 通过
@Entry(storage)
传入页面
- 在UIAbility进行初始化storage
class UserInfoClass {
name?: string = ""
age?: number = 0
}
let user: Record<string, UserInfoClass> = { "user": {
name: '东林',
age: 34
}};
const sharedStorage = new LocalStorage(user)
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(0x0000, 'testUIAbility', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index',local, (err) => {
if (err.code) {
hilog.error(0x0000, 'testUIAbility', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(0x0000, 'testUIAbility', 'Succeeded in loading the content.');
});
}
- 在页面中传入
const shareLocal = LocalStorage.GetShared()
@Entry(shareLocal)
1.1.3 应用逻辑中使用
@Entry
@Component
struct Index {
build() {
Button('应用逻辑中使用')
.onClick(()=>{
let para: Record<string,number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage(para); // 创建新实例并使用给定对象初始化
let propA: number | undefined = storage.get('PropA') // propA == 47
console.log(propA?.toString())
let link1: SubscribedAbstractProperty<number> = storage.link('PropA'); // link1.get() == 47
let link2: SubscribedAbstractProperty<number> = storage.link('PropA'); // link2.get() == 47
let prop: SubscribedAbstractProperty<number> = storage.prop('PropA'); // prop.get() == 47
link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48
prop.set(1); // one-way sync: prop.get() == 1; but link1.get() == link2.get() == 48
link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49
})
}
}
1.2 AppStorage:应用全局的UI状态存储
AppStorage是应用全局的UI状态存储,是和应用的进程绑定的,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储。
1.2.1 概述
- AppStorage是在应用启动的时候会被创建的单例。它的目的是为了提供应用状态数据的中心存储,这些状态数据在应用级别都是可访问的。AppStorage将在应用运行过程保留其属性。属性通过唯一的键字符串值访问。
- AppStorage可以和UI组件同步,且可以在应用业务逻辑中被访问。
- AppStorage支持应用的主线程内多个UIAbility实例间的状态共享。
- AppStorage中的属性可以被双向同步,数据可以是存在于本地或远程设备上,并具有不同的功能,比如数据持久化(详见PersistentStorage)。这些数据是通过业务逻辑中实现,与UI解耦,如果希望这些数据在UI中使用,需要用到@StorageProp和@StorageLink。
1.2.2 基本用法
AppStorage
是应用全局的UI状态存储,是和应用的进程绑定的,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储。-注意它也是内存数据,不会写入磁盘第一种用法-使用UI修饰符
- 如果是初始化使用
AppStorage.setOrCreate(key,value)
- 单向
@StorageProp('user')
组件内可变- 双向
@StorageLink('user')
全局均可变第二种用法 使用API方法
AppStorage.get<ValueType>(key)
获取数据AppStorage.set<ValueType>(key,value)
覆盖数据const link: SubscribedAbstractProperty<ValueType> = AppStorage.Link(key)
覆盖数据
link.set(value)
修改link.get()
获取
1.2.3 经常使用的方法
// 存数据
AppStorage.setOrCreate("demo", "123456")
AppStorage.setOrCreate<string>("username", "东林")
// 获取数据
const demo: string | undefined = AppStorage.get("demo")
const username = AppStorage.get<string>("username")
// 删除数据
AppStorage.delete("demo")
1.2.4 代码示例
- Index
import { promptAction, router } from '@kit.ArkUI'
@Entry
@Component
struct AppStorageCase {
@State
username: string = ""
@State
password: string = ""
login() {
if (this.username === 'admin' && this.password === "123456") {
// 要将当前用户的身份存入AppStorage
AppStorage.setOrCreate<UserInfoModel>("userInfo", new UserInfoModel('东林', 20))
router.pushUrl({ url: 'pages/Detail' })
} else {
promptAction.showToast({ message: '登录失败' })
}
}
build() {
Row() {
Column({ space: 20 }) {
TextInput({ placeholder: '请输入用户名', text: $$this.username })
TextInput({ placeholder: '请输入密码', text: $$this.password })
.type(InputType.Password)
Button("登录")
.width('100%')
.onClick(() => {
this.login()
})
}
.padding(20)
.width('100%')
}
.height('100%')
}
}
export class UserInfoModel {
username: string = ''
age: number = 0
constructor(username: string, age: number) {
this.username = username
this.age = age
}
}
- Detail
import { UserInfoModel } from './Index'
@Entry
@Component
struct Detail {
@State userInfo: UserInfoModel | null | undefined = null
aboutToAppear(): void {
const userInfo = AppStorage.get<UserInfoModel>("userInfo")
this.userInfo = userInfo
}
build() {
Column() {
Text(this.userInfo?.username)
Text(this.userInfo?.age.toString())
}
.height('100%')
.width('100%')
}
}
1.3 PersistentStorage:持久化存储UI状态
LocalStorage和AppStorage都是运行时的内存,但是在应用退出再次启动后,依然能保存选定的结果,是应用开发中十分常见的现象,这就需要用到PersistentStorage。
PersistentStorage是应用程序中的可选单例对象。此对象的作用是持久化存储选定的AppStorage属性,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同。
1.3.1 概述
- PersistentStorage将选定的AppStorage属性保留在设备磁盘上。应用程序通过API,以决定哪些AppStorage属性应借助PersistentStorage持久化。UI和业务逻辑不直接访问PersistentStorage中的属性,所有属性访问都是对AppStorage的访问,AppStorage中的更改会自动同步到PersistentStorage。
- PersistentStorage和AppStorage中的属性建立双向同步。应用开发通常通过AppStorage访问PersistentStorage,另外还有一些接口可以用于管理持久化属性,但是业务逻辑始终是通过AppStorage获取和设置属性的。
1.3.2 限制条件
PersistentStorage允许的类型和值有:
- number, string, boolean, enum 等简单类型。
- 可以被JSON.stringify()和JSON.parse()重构的对象。例如Date, Map, Set等内置类型则不支持,以及对象的属性方法不支持持久化。
PersistentStorage不允许的类型和值有:
- 不支持嵌套对象(对象数组,对象的属性是对象等)。因为目前框架无法检测AppStorage中嵌套对象(包括数组)值的变化,所以无法写回到PersistentStorage中。
- 不支持undefined 和 null 。
1.3.3 使用场景
从AppStorage中访问PersistentStorage初始化的属性
import { promptAction } from '@kit.ArkUI';
@Entry
@Component
struct Index {
build() {
Column() {
Button('存值')
.onClick(() => {
AppStorage.setOrCreate('test','123456')
promptAction.showToast({
message: '存值成功'
})
}).margin(100)
Button('获取值')
.onClick(() => {
const test = AppStorage.get<string>('test');
if(test){
promptAction.showToast({
message:test
})
}
})
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
注意:
切记需要获取值的时候对PersistentStorage进行再存一次操作,但是这次操作没法对你曾经存的有效数据做出变动。
上述代码你无法实现退出应用在进入系统,获取到之前存的值,但是你可以加一步操作
在EntryAbility页面进行多一步操作,加一行代码,存的操作,这个存的操作没法对你之前修改的数据做出影响,但是他会将磁盘数据加载到AppStorage里面,你之后可以在任何页面获取到你之前存的值
1.4 Environment:设备环境
1.4.1 概述
开发者如果需要应用程序运行的设备的环境参数,以此来作出不同的场景判断,比如多语言,深浅色模式等,需要用到Environment设备环境查询。
Environment是ArkUI框架在应用程序启动时创建的单例对象。它为AppStorage提供了一系列描述应用程序运行状态的属性。Environment的所有属性都是不可变的(即应用不可写入),所有的属性都是简单类型。
1.4.2 限制条件
Environment的所有属性都是不可变的(即应用不可写入),所有的属性都是简单类型。
1.4.3 使用场景
想要知道当前系统的所使用的语言,然后把信息共享给应用内所有页面使用
注意:记得使用模拟器或者真机读取语言
import i18n from '@ohos.i18n'
const lang=i18n.getSystemLanguage()
console.log('lang test:'+lang);
Environment.envProp('lang',lang)
@Entry
@Component
struct Index {
@StorageProp('lang')
lang:string=''
build() {
Column(){
Text(lang)
}.width('100%')
.height('100%')
}
}
2 网络管理
2.1 网络管理-应用权限
2.1.1 概述
系统提供了一种允许应用访问系统资源(如:通讯录等)和系统能力(如:访问摄像头、麦克风等)的通用权限访问方式,来保护系统数据(包括用户个人数据)或功能,避免它们被不当或恶意使用。
应用权限保护的对象可以分为数据和功能:
- 数据包含了个人数据(如照片、通讯录、日历、位置等)、设备数据(如设备标识、相机、麦克风等)、应用数据。
- 功能则包括了设备功能(如打电话、发短信、联网等)、应用功能(如弹出悬浮框、创建快捷方式等)等。
根据授权方式的不同,权限类型可分为system_grant(系统授权)和user_grant(用户授权)。
应用在申请权限时,需要在项目的配置文件中,逐个声明需要的权限,否则应用将无法获取授权。
2.1.2 配置方式
- 配置文件权限声明
- 向用户申请授权
2.1.3 用法
在module.json5文件中加上(也就是在module下面加上requestPermissions)
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone",
"tablet",
"2in1"
],
"requestPermissions":[
{
"name" : "ohos.permission.INTERNET",
"reason": "$string:reason",
"usedScene": {
"abilities": [
"FormAbility"
],
"when":"inuse"
}
}
],
2.2 网络管理-HTTP请求
2.2.1 概述
HTTP数据请求功能主要由http模块提供。
使用该功能需要申请ohos.permission.INTERNET权限。
2.2.2 接口
2.2.3 开发步骤
- 从@ohos.net.http中导入http命名空间。
- 调用createHttp()方法,创建一个HttpRequest对象。
- 调用该对象的on()方法,订阅http响应头事件,此接口会比request请求先返回。可以根据业务需要订阅此消息。
- 调用该对象的request()方法,传入http请求的url地址和可选参数,发起网络请求。
- 按照实际业务需要,解析返回结果。
- 调用该对象的off()方法,取消订阅http响应头事件。
- 当该请求使用完毕时,调用destroy()方法主动销毁。
2.2.4 简单代码示例
import http from '@ohos.net.http'
@Entry
@Component
struct Index {
build() {
Column() {
Text('测试http请求').onClick(()=>{
// 创建http对象
const httpRequest = http.createHttp()
// 发送请求
httpRequest.request('http://open.ibestservices.com/basic-api/platformCms/cms/door/index/info?siteId=1')
.then(res => {
console.log('http测试' + JSON.stringify(res.result))
})
})
}.width('100%')
.height('100%')
}
}
注意:console.log输出数据的大小不能超过1kb,要不然打印不出来
2.3 网络管理-第三方库axiso使用
2.3.1 概述
axios 是一个基于 JavaScript 的开源库,用于在浏览器和 Node.js 等环境中发送 HTTP 请求。它支持 Promise API,并简化了 XMLHttpRequests 和 Fetch API 的使用,为开发者提供了一种简洁易用的方式来实现 AJAX 请求。
2.3.2 主要特性
- 跨平台支持:Axios 在浏览器端通过 XMLHttpRequests 发送请求,在 Node.js 中则使用 http/https 模块发送请求。
- Promise API:所有网络请求方法都返回 Promise 对象,使得异步编程更加简洁和易于处理。
- 拦截请求与响应:提供了全局和实例级别的请求和响应拦截器,可以在请求发送前或响应返回后进行预处理、错误处理或数据转换等操作。
- 取消请求:支持主动取消已发出但还未完成的 HTTP 请求。
- 自动转换 JSON 数据:Axios 自动将来自服务器的 JSON 数据转换为 JavaScript 对象,并自动序列化 POST、PUT 等请求体中的 JSON 数据为字符串发送。
- 配置灵活性:允许自定义请求头、URL 参数、超时时间等多种配置项,适用于不同场景下的 API 调用需求。
- 请求方法多样:支持所有标准的 HTTP 方法(GET、POST、PUT、DELETE 等),并对 PATCH 等非标准方法提供良好支持。
- 上传下载进度监控:支持监听文件上传和下载的进度事件
2.3.3 用法
@ohos/axios
是基于 axios 库进行适配的模块,使其可以运行在 鸿蒙 中。它沿用了 axios 库的现有用法和特性,为 HarmonyOS 项目的开发提供了便利。
2.3.4 安装axios依赖
打开编辑器里面终端,输入以下命令
ohpm install @ohos/axios
2.3.5 申请网络权限
要请求网络数据,首先需要申请权限,需要在module.json5
文件中设置网络访问权限
在module.json5文件中加上(也就是在module下面加上requestPermissions)
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone",
"tablet",
"2in1"
],
"requestPermissions":[
{
"name" : "ohos.permission.INTERNET",
"reason": "$string:reason",
"usedScene": {
"abilities": [
"FormAbility"
],
"when":"inuse"
}
}
],
2.3.6 直接使用
import axios, { AxiosError, AxiosResponse } from '@ohos/axios';
@Entry
@Component
struct Index {
build() {
Column() {
Text('测试axios')
.onClick(() => {
axios.get('http://open.ibestservices.com/basic-api/platformCms/cms/door/index/info?siteId=1')
.then((res: AxiosResponse) => {
console.log("get - 返回数据:" + JSON.stringify(res));
})
.catch((err: AxiosError) => {
console.log("result:" + err.message);
});
})
}.width('100%')
.height('100%')
}
}
2.3.7 封装使用
注意 baseURL: 'http://****:8099/' 里面的ip和端口要切换成对应自己的接口ip和端口
- Request.ets
import axios, { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from '@ohos/axios'
import router from '@ohos.router';
import { promptAction } from '@kit.ArkUI';
/**
* axios封装
*/
const httpRequest = axios.create({
baseURL: 'http://****:8099/',
headers: {
'Content-Type': 'application/json',
"Channel": "B2B"
},
method: "post",
})
/**
* 添加请求拦截器
*/
httpRequest.interceptors.request.use((config: InternalAxiosRequestConfig) => {
// 获取数据
const token: string | undefined = AppStorage.get("token")
// 获取token
if (token) {
config.headers.Authorization = token
}
return config;
}, (error: AxiosError) => {
// 对请求错误做些什么
return Promise.reject(error);
});
/**
* 添加响应拦截器
*/
httpRequest.interceptors.response.use((response: AxiosResponse) => {
console.log('响应数据' + JSON.stringify(response))
// 判断响应状态码
if (response.status === 200) {
// 请求成功
if (response.data.code === 200) {
return Promise.resolve(response.data.data);
} else if (response.data.code === 401) {
clearLoginInfoAndGoLoginPage();
}
}
promptAction.showToast({
message: response.data.message || '请求错误',
duration: 2000,
})
return Promise.reject(response);
}, (error: AxiosError) => {
promptAction.showToast({
message: '网络错误,换个网络试试',
duration: 2000,
})
return Promise.reject(error);
});
/**
* 清除用户信息并跳到登录页面
*/
async function clearLoginInfoAndGoLoginPage() {
// 401错误 -> 清理用户信息,跳转到登录页
// 清理token,返回登录页
// 跳转首页路由
router.pushUrl({
url: 'pages/Login'
})
}
export default httpRequest;
- UserApi.ets
import httpRequest from '../request/Request'
/**
* 用户接口
*/
class UserApi {
/**
* 获取验证码接口
*/
getCode = (data: string) => {
return httpRequest.get('/v1/user/getCode?phone=' + data)
}
}
const userApi = new UserApi();
export default userApi as UserApi;
注意:手机号记得填写真实的
- Login.ets
import UserApi from '../api/UserApi';
@Entry
@Component
struct Login {
/**
* 获取验证码
*/
getCode() {
UserApi.getCode('你的手机号');
}
build() {
Column() {
Text('获取验证码').onClick(() => {
this.getCode()
})
}.width('100%')
.height('100%')
}
}
3 应用数据持久化
应用数据持久化,是指应用将内存中的数据通过文件或数据库的形式保存到设备上。内存中的数据形态通常是任意的数据结构或数据对象,存储介质上的数据形态可能是文本、数据库、二进制文件等。
HarmonyOS标准系统支持典型的存储数据形态,包括用户首选项、键值型数据库、关系型数据库。
- 用户首选项(Preferences):通常用于保存应用的配置信息。数据通过文本的形式保存在设备中,应用使用过程中会将文本中的数据全量加载到内存中,所以访问速度快、效率高,但不适合需要存储大量数据的场景。
- 键值型数据库(KV-Store):一种非关系型数据库,其数据以“键值”对的形式进行组织、索引和存储,其中“键”作为唯一标识符。适合很少数据关系和业务关系的业务数据存储,同时因其在分布式场景中降低了解决数据库版本兼容问题的复杂度,和数据同步过程中冲突解决的复杂度而被广泛使用。相比于关系型数据库,更容易做到跨设备跨版本兼容。
- 关系型数据库(RelationalStore):一种关系型数据库,以行和列的形式存储数据,广泛用于应用中的关系型数据的处理,包括一系列的增、删、改、查等接口,开发者也可以运行自己定义的SQL语句来满足复杂业务场景的需要。
3.1 应用数据持久化-用户首选项
3.1.1 概述
用户首选项为应用提供Key-Value键值型的数据处理能力,支持应用持久化轻量级数据,并对其修改和查询。当用户希望有一个全局唯一存储的地方,可以采用用户首选项来进行存储。Preferences会将该数据缓存在内存中,当用户读取的时候,能够快速从内存中获取数据,当需要持久化时可以使用flush接口将内存中的数据写入持久化文件中。Preferences会随着存放的数据量越多而导致应用占用的内存越大,因此,Preferences不适合存放过多的数据,也不支持通过配置加密,适用的场景一般为应用保存用户的个性化设置(字体大小,是否开启夜间模式)等。
3.1.2 约束限制
- 首选项无法保证进程并发安全,会有文件损坏和数据丢失的风险,不支持在多进程场景下使用。
- Key键为string类型,要求非空且长度不超过1024个字节。
- 如果Value值为string类型,请使用UTF-8编码格式,可以为空,不为空时长度不超过16 * 1024 * 1024个字节。
- 内存会随着存储数据量的增大而增大,所以存储的数据量应该是轻量级的,建议存储的数据不超过一万条,否则会在内存方面产生较大的开销。
3.1.3 封装用户首选项工具类
import dataPreferences from '@ohos.data.preferences'
import hilog from '@ohos.hilog';
/**
* 用户首选项(存储简单数据)
*/
export default class PreferencesUtil {
// 用户首选项名称
private static preferenceName: string = 'myStore'
/**
* 创建
* @param context
*/
static createPreferences(context) {
globalThis.getFontPreferences = (() => {
let preferences: Promise<dataPreferences.Preferences> =
dataPreferences.getPreferences(context, this.preferenceName);
return preferences;
});
}
/**
* 存放数据
* @param value
*/
static savePreferencesValue(key: string, value: string) {
globalThis.getFontPreferences().then((preferences) => {
preferences.has(key).then(async (isExist) => {
if (!isExist) {
await preferences.put(key, value);
preferences.flush();
}
}).catch((err) => {
hilog.info(0xFF00, 'preferencesTag', '%{public}s', 'save PreferencesValue fail');
});
}).catch((err) => {
hilog.info(0xFF00, 'preferencesTag', '%{public}s', 'save PreferencesValue fail');
});
}
/**
* 获取数据
* @returns
*/
static async getPreferencesValue(key: string) {
let value: string = '';
const preferences = await globalThis.getFontPreferences();
value = await preferences.get(key, value);
return value;
}
/**
* 删除数据
*/
static async deletePreferencesValue(key: string) {
const preferences: dataPreferences.Preferences = await globalThis.getFontPreferences();
let deleteValue = preferences.delete(key);
deleteValue.then(() => {
hilog.info(0xFF00, 'preferencesTag', '%{public}s', 'delete PreferencesValue success');
}).catch((err) => {
hilog.info(0xFF00, 'preferencesTag', '%{public}s', 'delete PreferencesValue fail');
});
}
}
注意:PreferencesUtil文件结尾是ts不是ets
3.1.4 初始化用户首选项
在EntryAbility文件中初始化用户首选项
- EntryAbility.ets
onWindowStageCreate(windowStage: window.WindowStage): void {
// 初始化用户首选项
PreferencesUtil.createPreferences(this.context);
// Main window is created, set main page for this ability
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
});
}
3.1.5 使用用户首选项
注意:用户首选项存的值全局都可以获取到
import PreferencesUtil from '../utils/PreferencesUtil'
@Entry
@Component
struct Index {
build() {
Column() {
Text('存值')
.onClick(() => {
PreferencesUtil.savePreferencesValue('username', '东林')
console.log('preferences save success')
}).margin({bottom:50})
Divider()
Text('点我获取preferences值')
.onClick(async () => {
const username = await PreferencesUtil.getPreferencesValue('username')
console.log('preferences username:' + JSON.stringify(username))
}).margin({bottom:50})
Divider()
Text('删除')
.onClick(async () => {
await PreferencesUtil.deletePreferencesValue('username')
console.log('preferences remove success')
})
}
.height('100%')
.width('100%')
}
}
3.2 应用数据持久化-键值型数据库
3.2.1 概述
键值型数据库存储键值对形式的数据,当需要存储的数据没有复杂的关系模型,比如存储商品名称及对应价格、员工工号及今日是否已出勤等,由于数据复杂度低,更容易兼容不同数据库版本和设备类型,因此推荐使用键值型数据库持久化此类数据。
3.2.2 约束限制
- 设备协同数据库,针对每条记录,Key的长度≤896 Byte,Value的长度<4 MB。
- 单版本数据库,针对每条记录,Key的长度≤1 KB,Value的长度<4 MB。
- 每个应用程序最多支持同时打开16个键值型分布式数据库。
- 键值型数据库事件回调方法中不允许进行阻塞操作,例如修改UI组件。
3.2.3 常用方法
1、在生命周期函数中添加代码
import KVStoreUtil from '../utils/KVStoreUtil';
onWindowStageCreate(windowStage: window.WindowStage): void {
// 创建键值型数据库的实例
let context = this.context;
const kvManagerConfig: distributedKVStore.KVManagerConfig = {
context: context,
bundleName: 'com.xt.myApplication'
};
try {
// 创建KVManager实例
let kvManager: distributedKVStore.KVManager | undefined = distributedKVStore.createKVManager(kvManagerConfig);
console.info('Succeeded in creating KVManager.');
// 创建数据库
let kvStore: distributedKVStore.SingleKVStore | undefined = undefined;
try {
const options: distributedKVStore.Options = {
createIfMissing: true,
encrypt: false,
backup: false,
autoSync: false,
// kvStoreType不填时,默认创建多设备协同数据库
kvStoreType: distributedKVStore.KVStoreType.SINGLE_VERSION,
// 多设备协同数据库:kvStoreType: distributedKVStore.KVStoreType.DEVICE_COLLABORATION,
securityLevel: distributedKVStore.SecurityLevel.S1
};
kvManager.getKVStore<distributedKVStore.SingleKVStore>('storeId', options,
(err, store: distributedKVStore.SingleKVStore) => {
if (err) {
console.error(`Failed to get KVStore: Code:${err.code},message:${err.message}`);
return;
}
console.info('Succeeded in getting KVStore.');
kvStore = store;
// 保存数据库
KVStoreUtil.setKVStore(kvStore)
});
} catch (e) {
let error = e as BusinessError;
console.error(`An unexpected error occurred. Code:${error.code},message:${error.message}`);
}
} catch (e) {
let error = e as BusinessError;
console.error(`Failed to create KVManager. Code:${error.code},message:${error.message}`);
}
// Main window is created, set main page for this ability
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
});
}
2、创建一个KVStoreUtil工具类
- KVStoreUtil.ets
import { distributedKVStore } from '@kit.ArkData';
import { data } from '@kit.TelephonyKit';
export default class KVStoreUtil {
private static kvStore: distributedKVStore.SingleKVStore
static setKVStore(kvStore: distributedKVStore.SingleKVStore) {
KVStoreUtil.kvStore = kvStore;
}
static getKVStore(): distributedKVStore.SingleKVStore {
return KVStoreUtil.kvStore;
}
/**
* 保存数据
* @param key
* @param value
* @returns
*/
static insert(key: string, value: string): Promise<string> {
return new Promise((resolve, reject) => {
KVStoreUtil.kvStore.put(key, value, (err) => {
if (err !== undefined) {
console.error(`Failed to put data. Code:${err.code},message:${err.message}`);
reject(err)
}
console.info('Succeeded in putting data.');
resolve(key + ':' + value)
});
})
}
/**
* 获取
* @param key
* @returns
*/
static get(key: string): Promise<string | number | boolean | Uint8Array | void> {
return new Promise((resolve, reject) => {
KVStoreUtil.kvStore.get(key, (err, data) => {
if (err != undefined) {
console.error(`Failed to get data. Code:${err.code},message:${err.message}`);
reject(err)
}
console.info(`Succeeded in getting data. Data:${data}`);
resolve(key + ':' + data)
});
})
}
/**
* 删除
* @param key
* @returns
*/
static delete(key: string): Promise<string> {
return new Promise((resolve, reject) => {
KVStoreUtil.kvStore.delete(key, (err) => {
if (err !== undefined) {
console.error(`Failed to delete data. Code:${err.code},message:${err.message}`);
reject(err)
}
console.info('Succeeded in deleting data.');
resolve(key)
});
})
}
}
3、在页面中操作数据库
-
Index.ets
import KVStoreUtil from '../utils/KVStoreUtil'
import { promptAction } from '@kit.ArkUI'
import { BusinessError } from '@ohos.base';
@Entry
@Component
struct Index {
build() {
Column() {
Button('插入数据')
.onClick(() => {
KVStoreUtil.insert('username', '东林').then((data) => {
promptAction.showToast({
message: '保存数据成功' + data
})
}).catch((error: BusinessError) => {
promptAction.showToast({
message: '保存数据失败 ' + error
})
})
}).margin({ bottom: 50 })
Button('查询数据')
.onClick(() => {
KVStoreUtil.get('username').then((data) => {
promptAction.showToast({
message: '查询数据成功:' + data
})
}).catch((error: BusinessError) => {
promptAction.showToast({
message: '查询数据失败 ' + error
})
})
}).margin({ bottom: 50 })
Button('删除数据')
.onClick(() => {
KVStoreUtil.delete('username').then((data) => {
promptAction.showToast({
message: '删除数据成功:' + data
})
}).catch((error: BusinessError) => {
promptAction.showToast({
message: '删除数据失败:' + error
})
})
}).margin({ bottom: 50 })
}
.height('100%')
.width('100%')
}
}
3.3 应用数据持久化-关系型数据库
3.3.1 概述
关系型数据库基于SQLite组件,适用于存储包含复杂关系数据的场景,比如一个班级的学生信息,需要包括姓名、学号、各科成绩等,又或者公司的雇员信息,需要包括姓名、工号、职位等,由于数据之间有较强的对应关系,复杂程度比键值型数据更高,此时需要使用关系型数据库来持久化保存数据。
3.3.2 运行机制
关系型数据库对应用提供通用的操作接口,底层使用SQLite作为持久化存储引擎,支持SQLite具有的数据库特性,包括但不限于事务、索引、视图、触发器、外键、参数化查询和预编译SQL语句。
3.3.3 约束限制
- 系统默认日志方式是WAL(Write Ahead Log)模式,系统默认落盘方式是FULL模式。
- 数据库中有4个读连接和1个写连接,线程获取到空闲读连接时,即可进行读取操作。当没有空闲读连接且有空闲写连接时,会将写连接当做读连接来使用。
- 为保证数据的准确性,数据库同一时间只能支持一个写操作。
- 当应用被卸载完成后,设备上的相关数据库文件及临时文件会被自动清除。
- ArkTS侧支持的基本数据类型:number、string、二进制类型数据、boolean。
- 为保证插入并读取数据成功,建议一条数据不要超过2M。超出该大小,插入成功,读取失败
3.3.4 常用方法
1、在生命周期函数中添加代码
- 导入模块代码
import { relationalStore } from '@kit.ArkData';
- 在onWindowStageCreate生命周期函数中添加代码,初始化数据库
// 创建数据库
const STORE_CONFIG: relationalStore.StoreConfig = {
name: 'RdbTest.db', // 数据库文件名
securityLevel: relationalStore.SecurityLevel.S3, // 数据库安全级别
encrypt: false, // 可选参数,指定数据库是否加密,默认不加密
customDir: 'customDir/subCustomDir', // 可选参数,数据库自定义路径。数据库将在如下的目录结构中被创建:context.databaseDir + '/rdb/' + customDir,其中context.databaseDir是应用沙箱对应的路径,'/rdb/'表示创建的是关系型数据库,customDir表示自定义的路径。当此参数不填时,默认在本应用沙箱目录下创建RdbStore实例。
isReadOnly: false // 可选参数,指定数据库是否以只读方式打开。该参数默认为false,表示数据库可读可写。该参数为true时,只允许从数据库读取数据,不允许对数据库进行写操作,否则会返回错误码801。
};
relationalStore.getRdbStore(this.context, STORE_CONFIG, (err, store) => {
if (err) {
console.error(`Failed to get RdbStore. Code:${err.code}, message:${err.message}`);
return;
}
console.info(`Succeeded in getting RdbStore.`);
//保存store, 方便后面我们对数据库的操作
RdbUtil.setStore(store)
})
2、创建实体类Student
查询的时候需要用实体类接收下数据库里面数据
- Student.ets
export default class Student {
id: number = 0
username: string
age: number = 0
constructor(id: number, username: string, age: number) {
this.id = id
this.username = username
this.age = age
}
}
3、创建工具类RdbUtil
- RdbUtil.ets
import relationalStore from '@ohos.data.relationalStore';
import Student from '../models/Student';
import { BusinessError } from '@ohos.base';
/**
* 关系型数据库工具类
*/
export default class RdbUtil {
/**
* 数据库对象
*/
private static rdbStore: relationalStore.RdbStore;
static setStore(store: relationalStore.RdbStore) {
RdbUtil.rdbStore = store;
}
static getStore(): relationalStore.RdbStore {
return RdbUtil.rdbStore;
}
/**
* 执行sql
* @param sql
* @returns
*/
static executeSql(sql: string): Promise<void> {
return RdbUtil.getStore().executeSql(sql);
}
/**
* 插入数据
* @param tableName
* @param data
* @returns
*/
static insert(tableName: string, data: relationalStore.ValuesBucket): Promise<number> {
return RdbUtil.getStore().insert(tableName, data);
}
/**
* 查询数据
* @returns
*/
static queryAll(): Promise<Array<Student>> {
let predicates = new relationalStore.RdbPredicates('STUDENT');
return new Promise<Array<Student>>((resolve, reject) => {
RdbUtil.getStore().query(predicates).then((result) => {
let students = new Array<Student>();
while (result.goToNextRow()) {
let student = new Student(
result.getLong(0),
result.getString(1),
result.getLong(2),
);
students.push(student);
}
resolve(students);
}).catch((error: BusinessError) => {
reject(error)
})
})
}
/**
* 删除
* @param id
* @returns
*/
static deleteById(id: number) {
let predicates = new relationalStore.RdbPredicates('STUDENT');
predicates.equalTo('ID', id)
return RdbUtil.getStore().delete(predicates);
}
/**
* 更新
* @param id
* @param data
* @returns
*/
static updateById(id: number, data: relationalStore.ValuesBucket) {
let predicates = new relationalStore.RdbPredicates('STUDENT');
predicates.equalTo('ID', id)
return RdbUtil.getStore().update(data, predicates);
}
}
注意:该工具类里面包含了创建表,新增数据,查询数据,更新数据,删除数据 的api
4、在界面中操作数据库
import RdbUtil from '../utils/RdbUtil';
import { BusinessError } from '@kit.BasicServicesKit';
import { promptAction } from '@kit.ArkUI';
import { relationalStore } from '@kit.ArkData';
import Student from '../models/Student';
@Entry
@Component
struct Index {
build() {
Column() {
Button('创建数据库表')
.onClick(() => {
const SQL_CREATE_TABLE =
'CREATE TABLE IF NOT EXISTS STUDENT (ID INTEGER PRIMARY KEY AUTOINCREMENT, USERNAME TEXT NOT NULL, AGE INTEGER)'; // 建表Sql语句
RdbUtil.executeSql(SQL_CREATE_TABLE)
.then(() => {
promptAction.showToast({
message: 'success create table'
})
}).catch((err: BusinessError) => {
promptAction.showToast({
message: 'fail create table'
})
})
}).margin({ bottom: 50 })
Button('插入数据')
.onClick(() => {
const valueBucket: relationalStore.ValuesBucket = {
'USERNAME': '东林',
'AGE': 18
};
RdbUtil.insert('STUDENT', valueBucket)
.then((updateNumber) => {
promptAction.showToast({
message: 'insert data success ' + updateNumber
})
}).catch((error: BusinessError) => {
promptAction.showToast({
message: 'insert data fail ' + error
})
})
}).margin({ bottom: 50 })
Button('查询数据')
.onClick(() => {
RdbUtil.queryAll()
.then((students: Array<Student>) => {
promptAction.showToast({
message: 'query students success ' + JSON.stringify(students)
})
}).catch((error: BusinessError) => {
promptAction.showToast({
message: ' query students fail ' + error
})
})
}).margin({ bottom: 50 })
Button('修改数据')
.onClick(() => {
const valueBucket: relationalStore.ValuesBucket = {
'USERNAME': '小红',
'AGE': 20
};
RdbUtil.updateById(1, valueBucket)
.then((updateNumber) => {
promptAction.showToast({
message: 'update student success ' + updateNumber.toString()
})
}).catch((err: BusinessError) => {
promptAction.showToast({
message: ' update student fail ' + err
})
})
}).margin({ bottom: 50 })
Button('删除数据')
.onClick(() => {
RdbUtil.deleteById(1)
.then((updateNumber) => {
promptAction.showToast({
message: 'delete student success ' + updateNumber.toString()
})
}).catch((err: BusinessError) => {
promptAction.showToast({
message: 'delete student fail ' + err
})
})
})
}
.height('100%')
.width('100%')
}
}
5、删除数据库
在生命周期函数里面删除数据库
onDestroy(): void {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
// 删除数据库
relationalStore.deleteRdbStore(this.context, 'RdbTest.db', (err) => {
if (err) {
console.error(`Failed to delete RdbStore. Code:${err.code}, message:${err.message}`);
return;
}
console.info('Succeeded in deleting RdbStore.');
});
}
4 文件操作
4.1 基本介绍
Core File Kit(文件基础服务)为开发者提供一套访问和管理应用文件和用户文件的能力。帮助用户更高效地管理、查找和备份各类文件,使用户能够轻松应对各种文件管理的需求。
先了解下鸿蒙里面文件的划分,分成了下面三种:
- 应用文件 文件所有者为应用,比如应用的安装包,自己的资源文件等。
- 用户文件 文件所有者为用户,比如用户自己的照片,录制的音视频等。
- 系统文件 文件所有者为系统,比如系统的配置文件等。
- 和安卓差不多吧,应用文件就是私有文件,不需要权限,用户文件的访问需要动态申请权限。
4.2 文件基础服务的能力
- 支持对应用文件进行查看、创建、读写、删除、移动、复制、获取属性等访问操作。
- 支持应用文件上传到网络服务器和网络服务器下载网络资源文件到本地应用文件目录。
- 支持获取当前应用的存储空间大小、指定文件系统的剩余空间大小和指定文件系统的总空间大小。
- 支持应用分享文件给其它应用和使用其它应用分享的文件。
- 支持应用接入数据备份恢复,在接入后,应用可通过修改配置文件定制备份恢复框架的行为,包括是否允许备份恢复、备份哪些数据。
- 提供用户文件访问框架,用于开发者访问和管理用户文件。例如选择与保存用户文件。
- 支持跨设备的文件访问和拷贝能力。
4.3 文件基础服务的亮点
- 沙箱隔离:
访问和管理应用文件,对于每个应用,系统会在内部存储空间映射出一个专属的“应用沙箱目录”,它是“应用文件目录”与一部分系统文件(应用运行必需的少量系统文件)所在的目录组成的集合。有以下优点:
- 隔离性:应用沙箱提供了一个完全隔离的环境,使用户可以安全地访问应用文件。
- 安全性:应用沙箱限制了应用可见的数据的最小范围,保护了应用文件的安全。
- 应用分享:
应用之间可以通过分享URI(Uniform Resource Identifier)或文件描述符FD(File Descriptor)的方式,进行文件共享。有以下优点:
- 便携性:应用之间进行文件分享,省去了用户在多个应用间切换的麻烦,简化了操作步骤,提高了效率。
- 高效性:应用间的文件分享能够更快地完成文件的传输,减少了因多次跳转和等待而浪费的时间。
- 数据一致性:应用间的文件分享能够确保数据的完整性和一致性,避免数据在传输过程中出现损坏或丢失的情况。
- 安全性:应用间的文件分享可以确保文件的安全性,避免文件被非法获取或篡改。同时,通过文件授权访问的方式,可以进一步增强文件的安全性。
4.4 应用文件
应用需要对应用文件目录下的应用文件进行查看、创建、读写、删除、移动、复制、获取属性等访问操作,我们可以通过“@ohos.file.fs”去操作。
4.4.1 新建并读写一个文件
import { common } from '@kit.AbilityKit';
import { fileIo as fs, ReadOptions } from '@kit.CoreFileKit';
import { buffer } from '@kit.ArkTS';
// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;
@Entry
@Component
struct Index {
build() {
Column() {
Text('写入文件').onClick(() => {
createFile()
})
}
.height('100%')
.width('100%')
}
}
function createFile(): void {
// 新建并打开文件
let file = fs.openSync(filesDir + '/test.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
// 写入一段内容至文件
let writeLen = fs.writeSync(file.fd, "我是新文件");
console.info("文件内容字节长度: " + writeLen);
// 从文件读取一段内容
let arrayBuffer = new ArrayBuffer(1024);
let readOptions: ReadOptions = {
offset: 0,
length: arrayBuffer.byteLength
};
let readLen = fs.readSync(file.fd, arrayBuffer, readOptions);
let buf = buffer.from(arrayBuffer, 0, readLen);
console.info("文件内容输出 " + buf.toString());
// 关闭文件
fs.closeSync(file);
}
注意:应用文件对于普通用户在手机文件管理查看是看不到的,开发者可以通过@ohos.file.fs来查看
4.4.2 查看文件列表
import { common } from '@kit.AbilityKit';
import { fileIo as fs, ListFileOptions, ReadOptions } from '@kit.CoreFileKit';
import { buffer } from '@kit.ArkTS';
// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;
@Entry
@Component
struct Index {
build() {
Column() {
Button('写入文件').onClick(() => {
createFile()
}).height('10%').width(100).margin(100)
Button('查看文件列表').onClick(() => {
getListFile()
})
.height('10%').width(100)
}
.height('100%')
.width('100%')
}
}
function createFile(): void {
// 新建并打开文件
let file = fs.openSync(filesDir + '/test.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
// 写入一段内容至文件
let writeLen = fs.writeSync(file.fd, "我是新文件");
console.info("文件内容字节长度: " + writeLen);
// 从文件读取一段内容
let arrayBuffer = new ArrayBuffer(1024);
let readOptions: ReadOptions = {
offset: 0,
length: arrayBuffer.byteLength
};
let readLen = fs.readSync(file.fd, arrayBuffer, readOptions);
let buf = buffer.from(arrayBuffer, 0, readLen);
console.info("文件内容输出 " + buf.toString());
// 关闭文件
fs.closeSync(file);
}
// 查看文件列表
function getListFile(): void {
let listFileOption: ListFileOptions = {
recursion: false,
listNum: 0,
filter: {
suffix: [".png", ".jpg", ".txt"],
displayName: ["test*"],
fileSizeOver: 0,
lastModifiedAfter: new Date(0).getTime()
}
};
let files = fs.listFileSync(filesDir, listFileOption);
for (let i = 0; i < files.length; i++) {
console.info(`文件: ${files[i]}`);
}
}
4.5 用户文件
用户文件:文件所有者为登录到该终端设备的用户,包括用户私有的图片、视频、音频、文档等。
- 用户文件存放在用户目录下,归属于该设备上登录的用户。
- 用户文件存储位置主要分为内置存储、外置存储。
- 应用对用户文件的创建、访问、删除等行为,需要提前获取用户授权,或由用户操作完成。
4.5.1 选择用户文件
用户需要分享文件、保存图片、视频等用户文件时,开发者可以通过系统预置的文件选择器(FilePicker),实现该能力。通过Picker访问相关文件,将拉起对应的应用,引导用户完成界面操作,接口本身无需申请权限。picker获取的uri只具有临时权限,获取持久化权限需要通过FilePicker设置永久授权方式获取。
根据用户文件的常见类型,选择器(FilePicker)分别提供以下选项:
- PhotoViewPicker:适用于图片或视频类型文件的选择与保存(该接口在后续版本不再演进)。请使用PhotoAccessHelper的PhotoViewPicker来选择图片文件。请使用安全控件创建媒体资源。
- DocumentViewPicker:适用于文件类型文件的选择与保存。DocumentViewPicker对接的选择资源来自于FilePicker, 负责文件类型的资源管理,文件类型不区分后缀,比如浏览器下载的图片、文档等,都属于文件类型。
- AudioViewPicker:适用于音频类型文件的选择与保存。AudioViewPicker目前对接的选择资源来自于FilePicker。
import { picker } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { buffer } from '@kit.ArkTS';
// 选择的文件uri
let uri: string = ''
let context = getContext(this) as common.Context; // 请确保 getContext(this) 返回结果为 UIAbilityContext
@Entry
@Component
struct Index {
build() {
Column() {
Button('选择文件').onClick(() => {
selectFile()
}).height('10%').width(100)
}
.height('100%')
.width('100%')
}
}
function selectFile(): void {
const documentSelectOptions = new picker.DocumentSelectOptions();
// 选择文档的最大数目(可选)
documentSelectOptions.maxSelectNumber = 1;
// 指定选择的文件或者目录路径(可选)
documentSelectOptions.defaultFilePathUri = "file://docs/storage/Users/currentUser/";
// 选择文件的后缀类型['后缀类型描述|后缀类型'](可选) 若选择项存在多个后缀名,则每一个后缀名之间用英文逗号进行分隔(可选),后缀类型名不能超过100,选择所有文件:'所有文件(*.*)|.*';
documentSelectOptions.fileSuffixFilters = ['图片(.png, .jpg)|.png,.jpg', '文档|.txt', '视频|.mp4', '.pdf'];
//选择是否对指定文件或目录授权,true为授权,当为true时,defaultFilePathUri为必选参数,拉起文管授权界面;false为非授权,拉起常规文管界面(可选)
documentSelectOptions.authMode = true;
// 创建文件选择器实例
const documentViewPicker = new picker.DocumentViewPicker(context);
documentViewPicker.select(documentSelectOptions).then((documentSelectResult: Array<string>) => {
//文件选择成功后,返回被选中文档的uri结果集。
uri = documentSelectResult[0];
console.info('选择的文件路径是' + uri);
//这里需要注意接口权限参数是fs.OpenMode.READ_ONLY。
let file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
console.info('文件描述符: ' + file.fd);
let arrayBuffer = new ArrayBuffer(4096);
let readLen = fs.readSync(file.fd, arrayBuffer);
console.info('读取文件内容成功,内容长度是:' + readLen);
// 输出文件内容
let buf = buffer.from(arrayBuffer, 0, readLen);
console.log('文件内容:' + buf.toString())
//读取完成后关闭fd。
fs.closeSync(file);
}).catch((err: BusinessError) => {
console.error(`Invoke documentViewPicker.select failed, code is ${err.code}, message is ${err.message}`);
})
}
4.5.2 保存用户文件
在从网络下载文件到本地、或将已有用户文件另存为新的文件路径等场景下,需要使用FilePicker提供的保存用户文件的能力。Picker获取的URI只具有临时权限,获取持久化权限需要通过FilePicker设置永久授权方式获取。
对音频、图片、视频、文档类文件的保存操作类似,均通过调用对应Picker的save()接口并传入对应的saveOptions来实现。通过Picker访问相关文件,无需申请权限
import { picker } from '@kit.CoreFileKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
// 保存的文件选择的路径
let uri: string = ''
let context = getContext(this) as common.Context; // 请确保 getContext(this) 返回结果为 UIAbilityContext
@Entry
@Component
struct Index {
build() {
Column() {
Button('保存文件').onClick(() => {
selectFile()
}).height('10%').width(100)
}
.height('100%')
.width('100%')
}
}
function selectFile(): void {
// 创建文件管理器选项实例
const documentSaveOptions = new picker.DocumentSaveOptions();
// 保存文件名(可选)
documentSaveOptions.newFileNames = ["hello.txt"];
// 保存文件类型['后缀类型描述|后缀类型'],选择所有文件:'所有文件(*.*)|.*'(可选) ,如果选择项存在多个后缀,默认选择第一个。
documentSaveOptions.fileSuffixChoices = ['文档|.txt', '.pdf'];
// 创建文件选择器实例。
const documentViewPicker = new picker.DocumentViewPicker(context);
//用户选择目标文件夹,用户选择与文件类型相对应的文件夹,即可完成文件保存操作。保存成功后,返回保存文档的URI。
documentViewPicker.save(documentSaveOptions).then((documentSaveResult: Array<string>) => {
uri = documentSaveResult[0];
console.info('需要保存到文件路径是:' + uri);
//这里需要注意接口权限参数是fs.OpenMode.READ_WRITE。
let file = fs.openSync(uri, fs.OpenMode.READ_WRITE);
console.info('文件描述符: ' + file.fd);
let writeLen: number = fs.writeSync(file.fd, 'hello, world');
console.info('写入文件的内容长度是:' + writeLen);
fs.closeSync(file);
}).catch((err: BusinessError) => {
console.error(`Invoke documentViewPicker.save failed, code is ${err.code}, message is ${err.message}`);
})
}
4.6 文件-上传下载
应用可以将应用文件上传到网络服务器,也可以从网络服务器下载网络资源文件到本地应用文件目录。
4.6.1 上传应用文件
开发者可以使用上传下载模块(ohos.request)的上传接口将本地文件上传。文件上传过程使用系统服务代理完成,在api12中request.agent.create接口增加了设置代理地址参数,支持用户设置自定义代理地址。
当前上传应用文件功能,仅支持上传应用缓存文件路径(cacheDir)下的文件。
使用上传下载模块,需声明权限:ohos.permission.INTERNET。
注意:如果需要上传用户文件,这需要先获取用户文件的读权限,然后将用户文件复制到应用缓存文件路径下面,最后在上传。
import { common } from '@kit.AbilityKit';
import fs from '@ohos.file.fs';
import { BusinessError, request } from '@kit.BasicServicesKit';
import { util } from '@kit.ArkTS';
// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let cacheDir = context.cacheDir;
@Entry
@Component
struct Index {
build() {
Column() {
Button('上传本地应用文件').onClick(() => {
uploadFile()
}).height('10%').width(100)
}
.height('100%')
.width('100%')
}
}
function uploadFile(): void {
// 新建一个本地应用文件
let file = fs.openSync(cacheDir + '/test.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
fs.writeSync(file.fd, '测试上传本地应用文件');
fs.closeSync(file);
// 上传任务配置项
let header = new Map<Object, string>();
header.set('Authorization', 'eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE3MzE1NzkyNTgsInBob25lIjoiMTczMzMwMjY3MDcifQ.BegJWUrCojMDjMb7gmVJOUDhnhk2N8UKe6UwlVom1TNxu7AErdbDKBc1JRoi1_JMVOnrlVOayrMW_nrCBb5eMA');
let files: Array<request.File> = [
//uri前缀internal://cache 对应cacheDir目录
{
filename: 'test.txt',
name: 'file', // 文件上传的key
uri: 'internal://cache/test.txt',
type: 'txt'
}
]
// 生成文件唯一id,通过后端将文件id和文件绑定起来,等会上传成功就可以根据文件id查询文件线上地址和所需要的文件信息
const fileId = util.generateRandomUUID(true)
let data: Array<request.RequestData> = [{ name: 'fileId', value: fileId }];
let uploadConfig: request.UploadConfig = {
url: 'http://10.10.164.154:8099/v1/user/upload/file',
header: header,
method: 'POST',
files: files,
data: data
}
// 将本地应用文件上传至网络服务器
try {
request.uploadFile(context, uploadConfig)
.then((uploadTask: request.UploadTask) => {
uploadTask.on('complete', (taskStates: Array<request.TaskState>) => {
for (let i = 0; i < taskStates.length; i++) {
console.info(`upload complete taskState: ${JSON.stringify(taskStates[i])}`);
}
});
})
.catch((err: BusinessError) => {
console.error(`Invoke uploadFile failed, code is ${err.code}, message is ${err.message}`);
})
} catch (error) {
let err: BusinessError = error as BusinessError;
console.error(`Invoke uploadFile failed, code is ${err.code}, message is ${err.message}`);
}
}
4.6.2 下载网络资源文件至应用文件目录
开发者可以使用上传下载模块(ohos.request)的下载接口将网络资源文件下载到应用文件目录。对已下载的网络资源文件,开发者可以使用基础文件IO接口(ohos.file.fs)对其进行访问,使用方式与应用文件访问一致。文件下载过程使用系统服务代理完成,在api12中request.agent.create接口增加了设置代理地址参数,支持用户设置自定义代理地址。
当前网络资源文件仅支持下载至应用文件目录。
使用上传下载模块,需声明权限:ohos.permission.INTERNET。
注意:如果需要在文件管理查看到当前下载的文件,这需要获取写文件权限,先将文件下载到应用文件,然后将文件复制到用户文件下面就好了。
import { common } from '@kit.AbilityKit';
import fs from '@ohos.file.fs';
import { BusinessError, request } from '@kit.BasicServicesKit';
import { buffer } from '@kit.ArkTS';
// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;
@Entry
@Component
struct Index {
build() {
Column() {
Button('下载文件').onClick(() => {
downloadFile()
}).height('10%').width(100)
}
.height('100%')
.width('100%')
}
}
function downloadFile(): void {
try {
// 判断文件是否存在,存在就删除
let path = context.filesDir + '/test.png';
if (fs.accessSync(path)) {
fs.unlinkSync(path);
}
request.downloadFile(context, {
url: 'https://www.baidu.com/link?url=8bSKCy3talaE0oHR0GO9EG8sZDONS13KbKlvZnrqtzt4tVkqzJBRd8Y5-C1GdvaCoCDiGQ7nu_KnacTLiVa9QynIa9oLBrWKb1Up4ihYFew_h6cszJuRhAizXpSXltSrRgyYqG7nyMlkuRYC35ayzHUcia2f_8GgQwHsjhhVuGrynu3CU3iTdutYJOHMpRENT86kM8GsM5tG8Lm3yBzP96wK8Y5rpn4b6pg8B2FL4bPktCXbzFK98xzppImvDyPyQhy1I7bJMuoIQwZ6FSBrTVoGcKU2sOmSSNjVGz8Q6vHWEuDinZwTJKJPoN3bAfUIhmDIi52ROIn04pP3Y-K-Py42jiSkECBnAR5v5gMzIonNDjKUeBHmIXo66hVizILvxzXIhm_i1lnYIA7aqmEFvzkNNNSgyZc81lAvDxzyYAcjyNsAE3bYGZ0jQdYZdCkRH1BTKnjselsTeqbfAg-FaAHNUD37cMx-Bvf-0hyfIystYNuiLTd1yCmbneBnxMGfm5FPMzDjRN2t5V-dkDVqMGM-rDAOlp7jpqow-mlh4S9dFjSG8EaO2woJvzCJm2nNXyjHYvHt7MemuJF7ky70iuGARcKk7vrhQUh_K1HKNCG8sFA5_t1luUI8U2AJEOZe-eBdN5k7zVot_WdKGFxVk7eTltW4qxX-CkuwuBMeIUa&wd=&eqid=8fa7c822000043380000000467347fbe',
filePath: filesDir + '/test.png'
}).then((downloadTask: request.DownloadTask) => {
downloadTask.on('complete', () => {
console.info('download complete');
let file = fs.openSync(filesDir + '/test.png', fs.OpenMode.READ_WRITE);
let arrayBuffer = new ArrayBuffer(1024);
let readLen = fs.readSync(file.fd, arrayBuffer);
let buf = buffer.from(arrayBuffer, 0, readLen);
console.info(`The content of file: ${buf.toString()}`);
fs.closeSync(file);
})
}).catch((err: BusinessError) => {
console.error(`Invoke downloadTask failed, code is ${err.code}, message is ${err.message}`);
});
} catch (error) {
let err: BusinessError = error as BusinessError;
console.error(`Invoke downloadFile failed, code is ${err.code}, message is ${err.message}`);
}
}
5 位置服务
5.1 基本介绍
移动终端设备已经深入人们日常生活的方方面面,如查看所在城市的天气、新闻轶事、出行打车、旅行导航、运动记录。这些习以为常的活动,都离不开定位用户终端设备的位置。
鸿蒙的位置服务(Location Kit)提供了基础的定位服务,还提供了地理围栏、地理编码、逆地理编码、国家码等功能和接口。
使用位置时服务请打开设备“位置”开关。如果位置功能关闭并且代码未设置捕获异常,可能导致应用异常。模拟器不行,大家记得在真机上测试使用位置服务。
5.2 申请位置权限
应用在使用Location Kit系统能力前,需要检查是否已经获取用户授权访问设备位置信息。如未获得授权,可以向用户申请需要的位置权限。
系统提供的定位权限有:
- ohos.permission.LOCATION:用于获取精准位置,精准度在米级别。
- ohos.permission.APPROXIMATELY_LOCATION:用于获取模糊位置,精确度为5公里。
- ohos.permission.LOCATION_IN_BACKGROUND:用于应用切换到后台仍然需要获取定位信息的场景。
当应用需要访问用户的隐私信息或使用系统能力时,例如获取位置信息、访问日历、使用相机拍摄照片或录制视频等,应该向用户请求授权,这部分权限是user_grant权限。
当应用申请user_grant权限时,需要完成以下步骤:
- 在配置文件中,声明应用需要请求的权限。
- 将应用中需要申请权限的目标对象与对应目标权限进行关联,让用户明确地知道,哪些操作需要用户向应用授予指定的权限。
- 运行应用时,在用户触发访问操作目标对象时应该调用接口,精准触发动态授权弹框。该接口的内部会检查当前用户是否已经授权应用所需的权限,如果当前用户尚未授予应用所需的权限,该接口会拉起动态授权弹框,向用户请求授权。
- 检查用户的授权结果,确认用户已授权才可以进行下一步操作。
"requestPermissions":[
{
"name" : "ohos.permission.APPROXIMATELY_LOCATION",
"reason": "$string:permission_location",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when":"inuse"
}
}
],
5.3 获取设备的位置信息
开发者可以调用HarmonyOS位置相关接口,获取设备实时位置,或者最近的历史位置,以及监听设备的位置变化。
对于位置敏感的应用业务,建议获取设备实时位置信息。如果不需要设备实时位置信息,并且希望尽可能的节省耗电,开发者可以考虑获取最近的历史位置。
5.3.1 接口说明
获取设备的位置信息接口介绍
接口名 |
功能描述 |
on(type: 'locationChange', request: LocationRequest | ContinuousLocationRequest, callback: Callback<Location>): void |
开启位置变化订阅,并发起定位请求。 |
off(type: 'locationChange', callback?: Callback<Location>): void |
关闭位置变化订阅,并删除对应的定位请求。 |
getCurrentLocation(request: CurrentLocationRequest | SingleLocationRequest, callback: AsyncCallback<Location>): void |
获取当前位置,使用callback回调异步返回结果。 |
getCurrentLocation(request?: CurrentLocationRequest | SingleLocationRequest): Promise<Location> |
获取当前位置,使用Promise方式异步返回结果。 |
getLastLocation(): Location |
获取最近一次定位结果。 |
5.3.2 代码实现
1、配置文件加上位置权限信息
注意:记得在module.json5配置位置权限
"requestPermissions":[
{
"name" : "ohos.permission.APPROXIMATELY_LOCATION",
"reason": "$string:permission_location",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when":"inuse"
}
}
],
2、获取坐标代码
import { geoLocationManager } from '@kit.LocationKit';
import { BusinessError } from '@kit.BasicServicesKit'
import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
@Entry
@Component
struct Index {
@State location: string = ''
@State latitude: number = 0
@State longitude: number = 0
/**
* 动态授权
*/
aboutToAppear(): void {
// 使用UIExtensionAbility:将common.UIAbilityContext 替换为common.UIExtensionContext
const context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
// 定义要申请的权限
const permissions: Array<Permissions> = ['ohos.permission.APPROXIMATELY_LOCATION'];
let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
// requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
atManager.requestPermissionsFromUser(context, permissions).then((data) => {
let grantStatus: Array<number> = data.authResults;
let length: number = grantStatus.length;
for (let i = 0; i < length; i++) {
if (grantStatus[i] === 0) {
// 用户授权,可以继续访问目标操作
promptAction.showToast({
message: '用户授权当前定位功能成功'
})
} else {
// 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限
promptAction.showToast({
message: '用户必须授权才能访问当前定位功能'
})
return;
}
}
// 授权成功
}).catch((err: BusinessError) => {
console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
})
}
/**
* 获取位置
*/
getLocation() {
let request: geoLocationManager.SingleLocationRequest = {
'locatingPriority': geoLocationManager.LocatingPriority.PRIORITY_LOCATING_SPEED,
'locatingTimeoutMs': 10000
}
try {
geoLocationManager.getCurrentLocation(request).then((result) => { // 调用getCurrentLocation获取当前设备位置,通过promise接收上报的位置
console.log('current location: ' + JSON.stringify(result));
this.latitude = result.latitude
this.longitude = result.longitude
})
.catch((error: BusinessError) => { // 接收上报的错误码
console.error('promise, getCurrentLocation: error=' + JSON.stringify(error));
});
} catch (err) {
console.error("errCode:" + JSON.stringify(err));
}
}
build() {
Column() {
Button('获取当前位置').onClick(() => {
this.getLocation()
}).margin(100)
Text('当前位置')
Text('纬度:'+this.latitude)
Text('经度:'+this.longitude)
}.width('100%')
.height('100%')
}
}
3、将经纬度坐标转换成实际地理位置信息(地理编码转化)
import { geoLocationManager } from '@kit.LocationKit';
import { BusinessError } from '@kit.BasicServicesKit'
import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
@Entry
@Component
struct Index {
@State location: string = ''
@State latitude: number = 0
@State longitude: number = 0
/**
* 动态授权
*/
aboutToAppear(): void {
// 使用UIExtensionAbility:将common.UIAbilityContext 替换为common.UIExtensionContext
const context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
// 定义要申请的权限
const permissions: Array<Permissions> = ['ohos.permission.APPROXIMATELY_LOCATION'];
let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
// requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
atManager.requestPermissionsFromUser(context, permissions).then((data) => {
let grantStatus: Array<number> = data.authResults;
let length: number = grantStatus.length;
for (let i = 0; i < length; i++) {
if (grantStatus[i] === 0) {
// 用户授权,可以继续访问目标操作
promptAction.showToast({
message: '用户授权当前定位功能成功'
})
} else {
// 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限
promptAction.showToast({
message: '用户必须授权才能访问当前定位功能'
})
return;
}
}
// 授权成功
}).catch((err: BusinessError) => {
console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
})
}
/**
* 获取位置
*/
getLocation() {
let request: geoLocationManager.SingleLocationRequest = {
'locatingPriority': geoLocationManager.LocatingPriority.PRIORITY_LOCATING_SPEED,
'locatingTimeoutMs': 10000
}
try {
geoLocationManager.getCurrentLocation(request).then((result) => { // 调用getCurrentLocation获取当前设备位置,通过promise接收上报的位置
console.log('current location: ' + JSON.stringify(result));
// 获取坐标
this.latitude = result.latitude
this.longitude = result.longitude
// 判断地理编码服务是否可用
try {
let isAvailable = geoLocationManager.isGeocoderAvailable();
if (isAvailable) {
let reverseGeocodeRequest: geoLocationManager.ReverseGeoCodeRequest =
{ "latitude": this.latitude, "longitude": this.longitude, "maxItems": 1 };
try {
geoLocationManager.getAddressesFromLocation(reverseGeocodeRequest, (err, data) => {
if (err) {
console.log('getAddressesFromLocation err: ' + JSON.stringify(err));
} else {
console.log('getAddressesFromLocation data: ' + JSON.stringify(data));
// 成功获取到位置信息
this.location = data[0].placeName+''
return
}
});
} catch (err) {
console.error("errCode:" + JSON.stringify(err));
}
}else{
promptAction.showToast({
message: '地理编码服务不可用'
})
}
} catch (err) {
console.error("errCode:" + JSON.stringify(err));
}
})
.catch((error: BusinessError) => { // 接收上报的错误码
console.error('promise, getCurrentLocation: error=' + JSON.stringify(error));
});
} catch (err) {
console.error("errCode:" + JSON.stringify(err));
}
}
build() {
Column() {
Button('获取当前位置').onClick(() => {
this.getLocation()
}).margin(100)
Text('当前位置')
Text('当前具体位置:' + this.location)
}.width('100%')
.height('100%')
}
}
6 视频播放
6.1 基本介绍
Video组件用于播放视频文件并控制其播放状态,常用于为短视频和应用内部视频的列表页面。当视频完整出现时会自动播放,用户点击视频区域则会暂停播放,同时显示播放进度条,通过拖动播放进度条指定视频播放到具体位置
Video提供构造参数 Video(value: VideoOptions)
VideoOptions对象包含参数src、currentProgressRate、previewUri、controller。其中,src指定视频播放源的路径,currentProgressRate用于设置视频播放倍速,previewUri指定视频未播放时的预览图片路径,controller设置视频控制器,用于自定义控制视频。
Video组件支持加载本地视频和网络视频。
6.2 加载本地视频
加载本地视频时,首先在本地rawfile目录指定对应的文件
再使用资源访问符$rawfile()引用视频资源。
@Entry
@Component
struct VideoCase {
build() {
Column() {
Video({
src: $rawfile('04功能体检基础质量测试.mp4')
}).width('100%')
.height('50%')
}
.height('100%')
.width('100%')
}
}
6.3 加载网络视频
加载网络视频时,需要申请权限ohos.permission.INTERNET
注意:网络视频地址是下载地址
@Entry
@Component
struct VideoCase {
build() {
Column() {
Video({
src: 'http://121.41.123.231:8888/f/df2d26723a7f4245ae57/?dl=1'
}).width('100%')
.height('50%')
}
.height('100%')
.width('100%')
}
}
6.5 常用属性
@Entry
@Component
struct VideoCase {
build() {
Column() {
Video({
src: $rawfile('04功能体检基础质量测试.mp4')
}).width('100%')
.height('50%')
.muted(false) //设置是否静音
.controls(false) //设置是否显示默认控制条
.autoPlay(false) //设置是否自动播放
.loop(false) //设置是否循环播放
.objectFit(ImageFit.Contain) //设置视频适配模式
}
.height('100%')
.width('100%')
}
}
6.6 事件调用
@Entry
@Component
struct VideoCase {
build() {
Column() {
Video({
src: $rawfile('04功能体检基础质量测试.mp4')
}).width('100%')
.height('50%')
.onUpdate((event) => { //更新事件回调
console.info("Video update.");
})
.onPrepared((event) => { //准备事件回调
console.info("Video prepared.");
})
.onError(() => { //失败事件回调
console.info("Video error.");
})
.onStop(() => { //停止事件回调
console.info("Video stoped.");
})
}
.height('100%')
.width('100%')
}
}
6.7 完整案例
@Entry
@Component
struct VideoCase {
@State
speed: number = 1
controller: VideoController = new VideoController()
build() {
Row() {
Tabs() {
TabContent() {
Column({ space: 20 }) {
Video({
controller: this.controller,
currentProgressRate: this.speed,
src: 'http://121.41.123.231:8888/f/df2d26723a7f4245ae57/?dl=1'
})
.width('100%')
.aspectRatio(1.4)
Slider({
value: this.speed,
min: 0.75,
step: 0.25,
max: 2,
style: SliderStyle.InSet
})
.showSteps(true)
.onChange(value => {
this.speed = value
})
Text(this.speed+"倍速").fontSize(14).textAlign(TextAlign.Center).width('100%')
Row({ space: 20 }) {
Button("播放")
.onClick(() => {
this.controller.start()
})
Button("暂停")
.onClick(() => {
this.controller.pause()
})
Button("移动进度")
.onClick(() => {
this.controller.setCurrentTime(30) // 单位为秒
})
Button("结束")
.onClick(() => {
this.controller.stop()
})
}
}
.width('100%')
}.tabBar("在线视频")
TabContent() {
Video({
src: $rawfile('04功能体检基础质量测试.mp4')
})
.width('100%')
.aspectRatio(1.4)
}
.tabBar("本地视频")
}
.animationDuration(300)
}
.height('100%')
}
}
7 绘图能力-基本用法
7.1 基本介绍
鸿蒙提供画布组件,用于自定义绘制图形,叫Canvas。
ArkUI里面的画布和前端的Canvas的用法基本一致
使用方法:
- 放置Canvas组件-给宽和高
- 初始化画笔对象 CanvasRenderingContext2D,将画笔对象作为构造参数传递给Canvas组件
- 可以在Canvas的onReady事件中进行动态绘制
- 绘制方法参考下面官方文档
7.2 接口方法
Canvas(context?: CanvasRenderingContext2D | DrawingRenderingContext)
7.3 开发步骤
1、定义一个画布
// 1、定义一个画布
Canvas().width('300').aspectRatio(1).backgroundColor('#ccc')
2、定义一个画笔
@Entry
@Component
struct Index {
// 给画笔设置属性,实现抗锯齿处理
setting = new RenderingContextSettings(true)
// 2、画笔
context = new CanvasRenderingContext2D(this.setting)
build() {
Column() {
// 1、定义一个画布
Canvas(this.context).width('300').aspectRatio(1).backgroundColor('#ccc')
}
.height('100%')
.width('100%')
}
}
3、画一个带边框的矩形
@Entry
@Component
struct Index {
// 给画笔设置属性,实现抗锯齿处理
setting = new RenderingContextSettings(true)
// 2、画笔
context = new CanvasRenderingContext2D(this.setting)
build() {
Column({space:15}) {
// 1、定义一个画布
Canvas(this.context).width('300').aspectRatio(1).backgroundColor('#ccc')
.onReady(()=>{
// 准备就绪
// 3、画一个带边框的矩形
this.context.strokeRect(100,100,50,50)
})
}
.height('100%')
.width('100%')
.justifyContent(FlexAlign.Center)
}
}
4、绘制一个带填充的矩形
@Entry
@Component
struct Index {
// 给画笔设置属性,实现抗锯齿处理
setting = new RenderingContextSettings(true)
// 2、画笔
context = new CanvasRenderingContext2D(this.setting)
build() {
Column({ space: 15 }) {
// 1、定义一个画布
Canvas(this.context).width('300').aspectRatio(1).backgroundColor('#ccc')
.onReady(() => {
// 准备就绪
// 3、画一个带填充的矩形
this.context.fillRect(100, 100, 100, 50)
})
}
.height('100%')
.width('100%')
.justifyContent(FlexAlign.Center)
}
}
8 用户通知服务
8.1 基本介绍
Notification Kit(用户通知服务)为开发者提供本地通知发布通道,开发者可借助Notification Kit将应用产生的通知直接在客户端本地推送给用户,本地通知根据通知类型及发布场景会产生对应的铃声、震动、横幅、锁屏、息屏、通知栏提醒和显示。
8.2 能力范围
Notification Kit支持的能力主要包括:
- 发布文本、进度条等类型通知。
- 携带或更新应用通知数字角标。
- 取消曾经发布的某条或全部通知。
- 查询已发布的通知列表。
- 查询应用自身通知开关状态。
- 应用通知用户的能力默认关闭,开发者可拉起授权框,请求用户授权发布通知。
8.3 业务流程
使用Notification Kit的主要业务流程如下:
- 请求通知授权。
- 应用发布通知到通知服务。
- 将通知展示到通知中心。
Notification Kit中常用的通知样式如下:
注意:
- 单个应用已发布的通知在通知中心等系统入口的留存数量有限(当前规格最多24条)。
- 通知的长度不能超过200KB(跨进程序列化大小限制)。
- 系统所有应用发布新通知的频次累计不能超过每秒10条,更新通知的频次累计不能超过每秒20条。
8.3.1 用户通知服务-基础通知
1、基本介绍
文本类型通知主要应用于发送短信息、提示信息等,支持普通文本类型和多行文本类型。
2、接口方法
接口名 |
描述 |
publish(request: NotificationRequest, callback: AsyncCallback<void>): void |
发布通知。 |
cancel(id: number, label: string, callback: AsyncCallback<void>): void |
取消指定的通知。 |
cancelAll(callback: AsyncCallback<void>): void |
取消所有该应用发布的通知。 |
3、代码示例
import { notificationManager } from '@kit.NotificationKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';
// 日志标识
const TAG: string = '[publishTest]';
const DOMAIN_NUMBER: number = 0xFF00;
let context = getContext(this) as common.UIAbilityContext;
@Entry
@Component
struct Index {
/**
* 请求通知授权
*/
aboutToAppear(): void {
notificationManager.isNotificationEnabled().then((data: boolean) => {
hilog.info(DOMAIN_NUMBER, TAG, "isNotificationEnabled success, data: " + JSON.stringify(data));
if (!data) {
notificationManager.requestEnableNotification(context).then(() => {
hilog.info(DOMAIN_NUMBER, TAG, `[ANS] requestEnableNotification success`);
}).catch((err: BusinessError) => {
if (1600004 == err.code) {
hilog.error(DOMAIN_NUMBER, TAG,
`[ANS] requestEnableNotification refused, code is ${err.code}, message is ${err.message}`);
} else {
hilog.error(DOMAIN_NUMBER, TAG,
`[ANS] requestEnableNotification failed, code is ${err.code}, message is ${err.message}`);
}
});
}
}).catch((err: BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG, `isNotificationEnabled fail, code is ${err.code}, message is ${err.message}`);
});
}
/**
* 通知方法
*/
publish() {
let notificationRequest: notificationManager.NotificationRequest = {
// 通知的唯一id
id: 1,
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, // 普通文本类型通知
normal: {
// 通知的标题
title: '百得知识库',
// 通知的内容
text: '百得知识库提醒你该学习了',
// 附加消息
additionalText: '百得',
}
}
};
notificationManager.publish(notificationRequest, (err: BusinessError) => {
if (err) {
hilog.error(DOMAIN_NUMBER, TAG,
`Failed to publish notification. Code is ${err.code}, message is ${err.message}`);
return;
}
hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in publishing notification.');
});
}
/**
* 取消指定的通知
*/
cancelPublish() {
notificationManager.cancel(1, (err: BusinessError) => {
if (err) {
hilog.error(DOMAIN_NUMBER, TAG,
`Failed to cancel notification. Code is ${err.code}, message is ${err.message}`);
return;
}
hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in cancel notification.');
});
}
build() {
Column({ space: 20 }) {
Button('发布文本类型通知')
.onClick(() => {
this.publish()
})
Button('取消通知')
.onClick(() => {
this.cancelPublish()
})
}
.height('100%')
.width('100%')
.justifyContent(FlexAlign.Center)
}
}
8.3.2 用户通知服务-进度条通知
1、基本介绍
进度条通知也是常见的通知类型,主要应用于文件下载、事务处理进度显示。当前系统提供了进度条模板,发布通知应用设置好进度条模板的属性值,如模板名、模板数据,通过通知子系统发送到通知栏显示。
2、接口方法
接口名 |
描述 |
isSupportTemplate(templateName: string): Promise<boolean> |
查询模板是否存在。 |
3、代码示例
import { notificationManager } from '@kit.NotificationKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { promptAction } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
const TAG: string = '[publishTest]';
const DOMAIN_NUMBER: number = 0xFF00;
let context = getContext(this) as common.UIAbilityContext;
@Entry
@Component
struct Page {
/**
* 请求通知授权
*/
aboutToAppear(): void {
notificationManager.isNotificationEnabled().then((data: boolean) => {
hilog.info(DOMAIN_NUMBER, TAG, "isNotificationEnabled success, data: " + JSON.stringify(data));
if (!data) {
notificationManager.requestEnableNotification(context).then(() => {
hilog.info(DOMAIN_NUMBER, TAG, `[ANS] requestEnableNotification success`);
}).catch((err: BusinessError) => {
if (1600004 == err.code) {
hilog.error(DOMAIN_NUMBER, TAG,
`[ANS] requestEnableNotification refused, code is ${err.code}, message is ${err.message}`);
} else {
hilog.error(DOMAIN_NUMBER, TAG,
`[ANS] requestEnableNotification failed, code is ${err.code}, message is ${err.message}`);
}
});
}
}).catch((err: BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG, `isNotificationEnabled fail, code is ${err.code}, message is ${err.message}`);
});
}
/**
* 发布通知
*/
publish() {
// 查询系统是否支持进度条模板
notificationManager.isSupportTemplate('downloadTemplate').then((data: boolean) => {
hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in supporting download template notification.');
let isSupportTpl: boolean = data;
// isSupportTpl的值为true表示支持downloadTemplate模板类通知,false表示不支持
if (isSupportTpl) {
let notificationRequest: notificationManager.NotificationRequest = {
id: 1,
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: '东林视频',
text: '东林视频下载',
additionalText: '东林'
}
},
// 构造进度条模板,name字段当前需要固定配置为downloadTemplate
template: {
name: 'downloadTemplate',
data: { title: '东林视频', fileName: '搞笑.mp4', progressValue: 80 }
}
}
// 发布通知
notificationManager.publish(notificationRequest, (err: BusinessError) => {
if (err) {
hilog.error(DOMAIN_NUMBER, TAG,
`Failed to publish notification. Code is ${err.code}, message is ${err.message}`);
return;
}
hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in publishing notification.');
});
} else {
promptAction.showToast({
message: '不支持进度条通知'
})
}
}).catch((err: BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG,
`Failed to support download template notification. Code is ${err.code}, message is ${err.message}`);
});
}
/**
* 取消发布
*/
cancelPublish() {
notificationManager.cancel(1, (err: BusinessError) => {
if (err) {
hilog.error(DOMAIN_NUMBER, TAG, `Failed to cancel notification. Code is ${err.code}, message is ${err.message}`);
return;
}
hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in cancel notification.');
});
}
build() {
Column() {
Button('发布进度条通知').onClick(() => {
this.publish()
}).margin(100)
Button('取消发布进度条通知').onClick(() => {
this.cancelPublish()
})
}
.height('100%')
.width('100%')
.justifyContent(FlexAlign.Center)
}
}
8.3.3 用户通知服务-通知行为意图
1、基本介绍
当发布通知时,如果期望用户可以通过点击通知栏拉起目标应用组件或发布公共事件,可以通过Ability Kit申请WantAgent封装至通知消息中。
2、接口方法
接口名 |
描述 |
getWantAgent(info: WantAgentInfo, callback: AsyncCallback<WantAgent>): void |
创建WantAgent。 |
3、代码示例
import { notificationManager } from '@kit.NotificationKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';
import { wantAgent, WantAgent } from '@kit.AbilityKit'
// 日志标识
const TAG: string = '[publishTest]';
const DOMAIN_NUMBER: number = 0xFF00;
let context = getContext(this) as common.UIAbilityContext;
@Entry
@Component
struct Page2 {
/**
* 请求通知授权
*/
aboutToAppear(): void {
notificationManager.isNotificationEnabled().then((data: boolean) => {
hilog.info(DOMAIN_NUMBER, TAG, "isNotificationEnabled success, data: " + JSON.stringify(data));
if (!data) {
notificationManager.requestEnableNotification(context).then(() => {
hilog.info(DOMAIN_NUMBER, TAG, `[ANS] requestEnableNotification success`);
}).catch((err: BusinessError) => {
if (1600004 == err.code) {
hilog.error(DOMAIN_NUMBER, TAG,
`[ANS] requestEnableNotification refused, code is ${err.code}, message is ${err.message}`);
} else {
hilog.error(DOMAIN_NUMBER, TAG,
`[ANS] requestEnableNotification failed, code is ${err.code}, message is ${err.message}`);
}
});
}
}).catch((err: BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG, `isNotificationEnabled fail, code is ${err.code}, message is ${err.message}`);
});
}
/**
* 通知方法
*/
publish() {
let wantAgentObj:WantAgent; // 用于保存创建成功的wantAgent对象,后续使用其完成触发的动作。
// 通过WantAgentInfo的operationType设置动作类型
let wantAgentInfo:wantAgent.WantAgentInfo = {
wants: [
{
deviceId: '',
bundleName: 'com.xt.myapplication',
abilityName: 'EntryAbility',
action: '',
entities: [],
uri: '',
parameters: {}
}
],
actionType: wantAgent.OperationType.START_ABILITY,
requestCode: 0,
wantAgentFlags:[wantAgent.WantAgentFlags.CONSTANT_FLAG]
};
// 创建WantAgent
wantAgent.getWantAgent(wantAgentInfo, (err: BusinessError, data:WantAgent) => {
if (err) {
hilog.error(DOMAIN_NUMBER, TAG, `Failed to get want agent. Code is ${err.code}, message is ${err.message}`);
return;
}
hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in getting want agent.');
wantAgentObj = data;
let notificationRequest: notificationManager.NotificationRequest = {
// 通知的唯一id
id: 1,
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, // 普通文本类型通知
normal: {
// 通知的标题
title: '百得知识库',
// 通知的内容
text: '百得知识库提醒你该学习了',
// 附加消息
additionalText: '百得',
}
},
// wantAgentObj使用前需要保证已被赋值(即步骤3执行完成)
wantAgent: wantAgentObj,
};
notificationManager.publish(notificationRequest, (err: BusinessError) => {
if (err) {
hilog.error(DOMAIN_NUMBER, TAG,
`Failed to publish notification. Code is ${err.code}, message is ${err.message}`);
return;
}
hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in publishing notification.');
});
});
}
/**
* 取消指定的通知
*/
cancelPublish() {
notificationManager.cancel(1, (err: BusinessError) => {
if (err) {
hilog.error(DOMAIN_NUMBER, TAG,
`Failed to cancel notification. Code is ${err.code}, message is ${err.message}`);
return;
}
hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in cancel notification.');
});
}
build() {
Column({ space: 20 }) {
Button('发布文本类型通知')
.onClick(() => {
this.publish()
})
Button('取消通知')
.onClick(() => {
this.cancelPublish()
})
}
.height('100%')
.width('100%')
.justifyContent(FlexAlign.Center)
}
}
9 Navigation组件导航
9.1 基本介绍
Navigation是路由导航的根视图容器,一般作为页面(@Entry)的根容器,包括单栏(Stack)、分栏(Split)和自适应(Auto)三种显示模式。Navigation组件适用于模块内和跨模块的路由切换,通过组件级路由能力实现更加自然流畅的转场体验,并提供多种标题栏样式来呈现更好的标题和内容联动效果。一次开发,多端部署场景下,Navigation组件能够自动适配窗口显示大小,在窗口较大的场景下自动切换分栏展示效果。
注意:页面路由推荐使用Navigation
Navigation的这种跳转方式耦合度较高,不适合大型的项目解耦开发。
9.2 子组件
可以包含子组件。
从API Version 9开始,推荐与NavRouter组件配合NavDestination属性进行页面路由
@Entry
@Component
struct Index {
build() {
Navigation() {
NavRouter() {
Row() {
Button("去A页面")
}
.height(60)
NavDestination() {
Text("A页面内容")
}
.title("A页面")
}
NavRouter() {
Row() {
Button("去B页面")
}
.height(60)
NavDestination() {
Text("B页面内容")
}
.title("B页面")
}
}
.title("测试")
.titleMode(NavigationTitleMode.Mini)
}
}
从API Version 10开始,推荐使用NavPathStack配合NavDestination属性进行页面路由。
@Entry
@Component
struct Index {
@Provide
stackPath: NavPathStack = new NavPathStack() // 声明一个pathStack对象
@Styles
gridStyle () {
.height(100)
.borderRadius(10)
.backgroundColor(Color.Red)
.margin(10)
}
@Builder
getPageContent (name: string) {
if(name === "friend") {
// 渲染好友组件
Friend()
}
else if(name === "my") {
// 渲染我的组件
My()
}
else if(name === "connect") {
// 渲染联系人组件
Connect()
}
else if(name === "chat") {
// 渲染聊天组件
Chat()
}
}
build() {
// 绑定关系
Navigation(this.stackPath) {
// 四个导航 导航不同的页面
// 好友 我的 联系人 聊天
GridRow ({ columns: 2 }) {
GridCol() {
Text("好友")
.fontColor(Color.White)
}
.gridStyle()
.onClick(() => {
this.stackPath.pushPathByName("friend", null)
})
GridCol() {
Text("我的")
.fontColor(Color.White)
} .gridStyle()
.onClick(() => {
this.stackPath.pushPathByName("my", null)
})
GridCol() {
Text("联系人")
.fontColor(Color.White)
} .gridStyle()
.onClick(() => {
this.stackPath.pushPathByName("connect", null)
})
GridCol() {
Text("聊天")
.fontColor(Color.White)
} .gridStyle()
.onClick(() => {
this.stackPath.pushPathByName("chat", null)
})
}
}
.title("主页")
.titleMode(NavigationTitleMode.Mini)
.navDestination(this.getPageContent)
}
}
@Component
struct Friend {
@Consume
stackPath: NavPathStack
build() {
NavDestination() {
Text("好友组件")
Button("到我的").onClick((event: ClickEvent) => {
this.stackPath.replacePathByName("my", null)
})
}
.title("好友")
}
}
@Component
struct My {
build() {
NavDestination() {
Text("我的")
}
.title("我的")
}
}
@Component
struct Connect {
build() {
NavDestination() {
Text("联系人")
}
.title("联系人")
}
}
@Component
struct Chat {
build() {
NavDestination() {
Text("聊天")
}
.title("聊天")
}
}
9.3 接口
Navigation()
绑定路由栈到Navigation组件。
Navigation(pathInfos: NavPathStack)
9.4 设置页面显示模式
Navigation组件通过mode属性设置页面的显示模式。
分为以下三种:
1、自适应模式(默认)
2、单页面模式
3、分栏模式
9.5 自适应模式
Navigation组件默认为自适应模式,此时mode属性为NavigationMode.Auto。自适应模式下,当页面宽度大于等于一定阈值( API version 9及以前:520vp,API version 10及以后:600vp )时,Navigation组件采用分栏模式,反之采用单栏模式
Navigation() {
// ...
}
.mode(NavigationMode.Auto)
9.6 单页面模式
将mode属性设置为NavigationMode.Stack,Navigation组件即可设置为单页面显示模式。
Navigation() {
// ...
}
.mode(NavigationMode.Stack)
9.7 分栏模式
将mode属性设置为NavigationMode.Split,Navigation组件即可设置为分栏显示模式。
Navigation() {
// ...
}
.mode(NavigationMode.Split)
9.8 设置标题栏模式
标题栏在界面顶部,用于呈现界面名称和操作入口,Navigation组件通过titleMode属性设置标题栏模式。
标题栏模式分为以下两种
Mini模式
Full模式
注意:设置标题直接用Navigation().title('标题')即可
9.9 Mini模式标题栏
普通型标题栏,用于一级页面不需要突出标题的场景。
Navigation() {
// ...
}
.titleMode(NavigationTitleMode.Mini)
9.11 Full模式
强调型标题栏,用于一级页面需要突出标题的场景。
Navigation() {
// ...
}
.titleMode(NavigationTitleMode.Full)
9.12 路由操作
Navigation路由相关的操作都是基于页面栈NavPathStack提供的方法进行,每个Navigation都需要创建并传入一个NavPathStack对象,用于管理页面。主要涉及页面跳转、页面返回、页面替换、页面删除、参数获取、路由拦截等功能。
1、初始化代码
@Entry
@Component
struct Index {
// 创建一个页面栈对象并传入Navigation
pageStack: NavPathStack = new NavPathStack()
build() {
Navigation(this.pageStack) {
}.title('登录页面') // 设置标题
.titleMode(NavigationTitleMode.Full) // 设置标题栏模式
}
}
2、页面跳转
NavPathStack通过Push相关的接口去实现页面跳转的功能,主要分为以下三类:
1. 普通跳转,通过页面的name去跳转,并可以携带param。
this.pageStack.pushPath({ name: "PageOne", param: "PageOne Param" })
this.pageStack.pushPathByName("PageOne", "PageOne Param")
2. 带返回回调的跳转,跳转时添加onPop回调,能在页面出栈时获取返回信息,并进行处理。
this.pageStack.pushPathByName('PageOne', "PageOne Param", (popInfo) => {
console.log('Pop page name is: ' + popInfo.info.name + ', result: ' + JSON.stringify(popInfo.result))
});
3. 带错误码的跳转,跳转结束会触发异步回调,返回错误码信息。
this.pageStack.pushDestinationByName('PageOne', "PageOne Param")
.catch((error: BusinessError) => {
console.error(`Push destination failed, error code = ${error.code}, error.message = ${error.message}.`);
}).then(() => {
console.info('Push destination succeed.');
});
3、页面返回
NavPathStack通过Pop相关接口去实现页面返回功能。
// 返回到上一页
this.pageStack.pop()
// 返回到上一个PageOne页面
this.pageStack.popToName("PageOne")
// 返回到索引为1的页面
this.pageStack.popToIndex(1)
// 返回到根首页(清除栈中所有页面)
this.pageStack.clear()
4、页面删除
NavPathStack通过Remove相关接口去实现删除页面栈中特定页面的功能。
// 删除栈中name为PageOne的所有页面
this.pageStack.removeByName("PageOne")
// 删除指定索引的页面
this.pageStack.removeByIndexes([1,3,5])
5、参数获取
NavPathStack通过Get相关接口去获取页面的一些参数。
// 获取栈中所有页面name集合
this.pageStack.getAllPathName()
// 获取索引为1的页面参数
this.pageStack.getParamByIndex(1)
// 获取PageOne页面的参数
this.pageStack.getParamByName("PageOne")
// 获取PageOne页面的索引集合
this.pageStack.getIndexByName("PageOne")
6、子页面
NavDestination是Navigation子页面的根容器,用于承载子页面的一些特殊属性以及生命周期等。NavDestination可以设置独立的标题栏和菜单栏等属性,使用方法与Navigation相同。NavDestination也可以通过mode属性设置不同的显示类型,用于满足不同页面的诉求。
7、页面显示类型
- 标准类型
NavDestination组件默认为标准类型,此时mode属性为NavDestinationMode.STANDARD。标准类型的NavDestination的生命周期跟随其在NavPathStack页面栈中的位置变化而改变。
- 弹窗类型
NavDestination设置mode为NavDestinationMode.DIALOG弹窗类型,此时整个NavDestination默认透明显示。弹窗类型的NavDestination显示和消失时不会影响下层标准类型的NavDestination的显示和生命周期,两者可以同时显示。
@Entry
@Component
struct Index {
@Provide('NavPathStack') pageStack: NavPathStack = new NavPathStack()
@Builder
PagesMap(name: string) {
if (name == 'DialogPage') {
DialogPage()
}
}
build() {
Navigation(this.pageStack) {
Button('Push DialogPage')
.margin(20)
.width('80%')
.onClick(() => {
this.pageStack.pushPathByName('DialogPage', '');
})
}
.mode(NavigationMode.Stack)
.title('Main')
.navDestination(this.PagesMap)
}
}
@Component
export struct DialogPage {
@Consume('NavPathStack') pageStack: NavPathStack;
build() {
NavDestination() {
Stack({ alignContent: Alignment.Center }) {
Column() {
Text("Dialog NavDestination")
.fontSize(20)
.margin({ bottom: 100 })
Button("Close").onClick(() => {
this.pageStack.pop()
}).width('30%')
}
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.White)
.borderRadius(10)
.height('30%')
.width('80%')
}.height("100%").width('100%')
}
.backgroundColor('rgba(0,0,0,0.5)')
.hideTitleBar(true)
.mode(NavDestinationMode.DIALOG)
}
}
8、页面生命周期
Navigation作为路由容器,其生命周期承载在NavDestination组件上,以组件事件的形式开放。
其生命周期大致可分为三类,自定义组件生命周期、通用组件生命周期和自有生命周期。
生命周期时序如下图所示:
- aboutToAppear:在创建自定义组件后,执行其build()函数之前执行(NavDestination创建之前),允许在该方法中改变状态变量,更改将在后续执行build()函数中生效。
- onWillAppear:NavDestination创建后,挂载到组件树之前执行,在该方法中更改状态变量会在当前帧显示生效。
- onAppear:通用生命周期事件,NavDestination组件挂载到组件树时执行。
- onWillShow:NavDestination组件布局显示之前执行,此时页面不可见(应用切换到前台不会触发)。
- onShown:NavDestination组件布局显示之后执行,此时页面已完成布局。
- onWillHide:NavDestination组件触发隐藏之前执行(应用切换到后台不会触发)。
- onHidden:NavDestination组件触发隐藏后执行(非栈顶页面push进栈,栈顶页面pop出栈或应用切换到后台)。
- onWillDisappear:NavDestination组件即将销毁之前执行,如果有转场动画,会在动画前触发(栈顶页面pop出栈)。
- onDisappear:通用生命周期事件,NavDestination组件从组件树上卸载销毁时执行。
- aboutToDisappear:自定义组件析构销毁之前执行,不允许在该方法中改变状态变量。
10 Tabs
10.1 基本介绍
当页面信息较多时,为了让用户能够聚焦于当前显示的内容,需要对页面内容进行分类,提高页面空间利用率。Tabs组件可以在一个页面内快速实现视图内容的切换,一方面提升查找信息的效率,另一方面精简用户单次获取到的信息量
Tabs组件的页面组成包含两个部分,分别是TabContent和TabBar。TabContent是内容页,TabBar是导航页签栏,页面结构如下图所示,根据不同的导航类型,布局会有区别,可以分为底部导航、顶部导航、侧边导航,其导航栏分别位于底部、顶部和侧边。
说明
- TabContent组件不支持设置通用宽度属性,其宽度默认撑满Tabs父组件。
- TabContent组件不支持设置通用高度属性,其高度由Tabs父组件高度与TabBar组件高度决定。
10.2 语法
Tabs使用花括号包裹TabContent
每一个TabContent对应的内容需要有一个页签,可以通过TabContent的tabBar属性进行配置。在如下TabContent组件上设置tabBar属性,可以设置其对应页签中的内容,tabBar作为内容的页签。
@Entry
@Component
struct TabsPage {
build() {
Column(){
Tabs() {
TabContent() {
Text('首页的内容').fontSize(30)
}
.tabBar('首页')
TabContent() {
Text('推荐的内容').fontSize(30)
}
.tabBar('推荐')
TabContent() {
Text('发现的内容').fontSize(30)
}
.tabBar('发现')
TabContent() {
Text('我的内容').fontSize(30)
}
.tabBar("我的")
}
}
.height('100%')
.width('100%')
}
}
10.3 底部导航
底部导航是应用中最常见的一种导航方式。底部导航位于应用一级页面的底部,用户打开应用,能够分清整个应用的功能分类,以及页签对应的内容,并且其位于底部更加方便用户单手操作。底部导航一般作为应用的主导航形式存在,其作用是将用户关心的内容按照功能进行分类,迎合用户使用习惯,方便在不同模块间的内容切换。
导航栏位置使用Tabs的barPosition参数进行设置。默认情况下,导航栏位于顶部,此时,barPosition为BarPosition.Start。设置为底部导航时,需要将barPosition设置为BarPosition.End。
@Entry
@Component
struct TabsPage {
build() {
Column(){
Tabs({barPosition:BarPosition.End}) {
TabContent() {
Text('首页的内容').fontSize(30)
}
.tabBar('首页')
TabContent() {
Text('推荐的内容').fontSize(30)
}
.tabBar('推荐')
TabContent() {
Text('发现的内容').fontSize(30)
}
.tabBar('发现')
TabContent() {
Text('我的内容').fontSize(30)
}
.tabBar("我的")
}
}
.height('100%')
.width('100%')
}
}
10.4 顶部导航
当内容分类较多,用户对不同内容的浏览概率相差不大,需要经常快速切换时,一般采用顶部导航模式进行设计,作为对底部导航内容的进一步划分,常见一些资讯类应用对内容的分类为关注、视频、数码,或者主题应用中对主题进行进一步划分为图片、视频、字体等。
@Entry
@Component
struct TabsPage {
build() {
Column(){
Tabs({barPosition:BarPosition.Start}) {
TabContent() {
Text('首页的内容').fontSize(30)
}
.tabBar('首页')
TabContent() {
Text('推荐的内容').fontSize(30)
}
.tabBar('推荐')
TabContent() {
Text('发现的内容').fontSize(30)
}
.tabBar('发现')
TabContent() {
Text('我的内容').fontSize(30)
}
.tabBar("我的")
}
}
.height('100%')
.width('100%')
}
}
10.5 侧边导航
侧边导航是应用较为少见的一种导航模式,更多适用于横屏界面,用于对应用进行导航操作,由于用户的视觉习惯是从左到右,侧边导航栏默认为左侧侧边栏。
实现侧边导航栏需要将Tabs的vertical属性设置为true,vertical默认值为false,表明内容页和导航栏垂直方向排列。
@Entry
@Component
struct TabsPage {
build() {
Column(){
Tabs({barPosition:BarPosition.Start}) {
TabContent() {
Text('首页的内容').fontSize(30)
}
.tabBar('首页')
TabContent() {
Text('推荐的内容').fontSize(30)
}
.tabBar('推荐')
TabContent() {
Text('发现的内容').fontSize(30)
}
.tabBar('发现')
TabContent() {
Text('我的内容').fontSize(30)
}
.tabBar("我的")
}.vertical(true)
.barWidth(100)
.barHeight(200)
}
.height('100%')
.width('100%')
}
}
10.6 自定义导航栏
对于底部导航栏,一般作为应用主页面功能区分,为了更好的用户体验,会组合文字以及对应语义图标表示页签内容,这种情况下,需要自定义导航页签的样式。
- 设置自定义导航栏需要使用tabBar的参数,以其支持的CustomBuilder的方式传入自定义的函数组件样式。例如这里声明tabBuilder的自定义函数组件,传入参数包括页签文字title,对应位置index,以及选中状态和未选中状态的图片资源。通过当前活跃的currentIndex和页签对应的targetIndex匹配与否,决定UI显示的样式。
- TabsPage.ets
@Entry
@Component
struct TabsPage {
@State currentIndex:number=0
@Builder
tabBuilder(title: string, targetIndex: number, normalImg: Resource, selectedImg: 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)
}
build() {
Column() {
Tabs({ barPosition: BarPosition.End }) {
TabContent() {
Text('首页的内容').fontSize(30)
}
.tabBar(this.tabBuilder('首页',0,$r('app.media.index_default'),$r('app.media.index_select')))
TabContent() {
Text('推荐的内容').fontSize(30)
}
.tabBar(this.tabBuilder('推荐',1,$r('app.media.index_default'),$r('app.media.index_select')))
TabContent() {
Text('发现的内容').fontSize(30)
}
.tabBar(this.tabBuilder('发现',2,$r('app.media.index_default'),$r('app.media.index_select')))
TabContent() {
Text('我的内容').fontSize(30)
}
.tabBar(this.tabBuilder('我的',3,$r('app.media.index_default'),$r('app.media.index_select')))
} .onChange((currentIndex: number) => {
this.currentIndex = currentIndex
})
}
.height('100%')
.width('100%')
}
}
原文地址:鸿蒙应用开发-进阶
更多推荐
所有评论(0)