特此声明
文章中的图片与资料,一部分来自B站黑马教育视频的截图,一部分来自HarmonyOs官网的教学视频截图
API地址
ArkTS接口参考
组件参考(基于ArkTS的声明式开发范式)
官网基础课堂
官网开发者认证
安装包
状态管理装饰器
组件同步
当父子组件之间需要数据同步时,可以使用@Prop和eLink装饰器:
需要注意的是,被 @Prop 和 @Link 装饰后,不能在本地(当前组件方法中)做初始化
@Prop 支持单向同步
-
@Prop只支持string、 number、 boolean、 enum类型
-
变量类型不可以是数组、any
-
如果父组件@Sate装饰的是对象类型,则子组件只要装饰的是对象的子属性,也会被允许
class StateInfo { message: string = 'hello' } @Entry @Component struct StateTaskList { @State state: StateInfo = new StateInfo() TaskTotalInfoComponent({ message: this.state.message }) } // 子组件 @Component struct TaskTotalInfoComponent { @Prop message: string build () { Text(this.message) } }
-
@Prop的实现相当于将父组件的值,拷贝一份到子组件,每次父组件的值被修改,都会拷贝一个新的值传递到子组件
@Entry @Component struct StateTaskList { @State message: number = 0 ChildComponent({ message: this.message }) } @Component struct ChildComponent { @Prop totalTask: number }
@Link 支持双向同步
-
父子类型一致:string、 number、 boolean、 enum、 object、 class,以及他们的数组
class StateInfo { message: string = 'hello' } @Entry @Component struct StateTaskList { @State state: StateInfo = new StateInfo() TaskTotalInfoComponent({ state: $state }) } // 子组件 @Component struct TaskTotalInfoComponent { @Lnik state: StateInfo build () { Text(this.state.message) } }
-
数组中元素增、删、替换会引起刷新
-
嵌套类型以及数组中的对象属性无法触发视图更新
-
使用 @Link 传递变量时,传递的是变量的引用,父子组件访问的都是同一个变量
@Link 装饰的属性,必须在初始化时带上
$
TaskList({ finishTask: $finishTask, totalTask: $totalTask })
@Provide 和 @Consume
@Provide 和 @Consume 可以跨组件提供类似于 @State 和 @Link 的双向同步
弊端就是因为不需要传参,就需要代码内部自动维护,从而会损耗资源
class StateInfo {
message: string = 'hello'
}
@Entry
@Component
struct StateTaskList {
@Provide state: StateInfo = new StateInfo()
// 使用 @Provide 时,无需传参
TaskTotalInfoComponent()
}
// 子组件
@Component
struct TaskTotalInfoComponent {
@Consume state: StateInfo
build () {
Text(this.state.message)
}
}
@Observed 和 @ObjectLink
@Observed 和 @ObjectLink 装饰器作用于在涉及 嵌套对象 和 数组元素为对象 的场景中进行双向数据同步
页面路由
路由说明
页面路由是指在应用程序中实现不同页面之间的跳转和数据传递。
在 HarmonyOS 中,访问过的页面并没有被销毁,而是被保存在页面栈的中间中,页面栈是先进后出(层层叠加的),当前访问的页面就在栈顶,最先访问的页面就在栈底。
当页面要操作返回上一页时,只需要将栈顶的页面消除,即可实现返回上一页的功能。
页面栈的最大容量上限为 32 个页面,使用 router.clear()
方法可以清空页面栈,释放内存
Router有两种页面跳转模式,分别是:
router.pushUrl()
目标页面不会替换当前页面,而是压入页面栈,因此可以用router.back()
返回当前页面;例如:搜索历史页面router.replaceUrl()
目标页替换当前页,当前页会被销毁并释放资源,无法返回当前页;例如:用户登录页面
Router有两种创建页面实例的模式,分别是:
Standard
:标准实例模式,每次跳转都会新建一个目标页并压入栈顶。默认就是这种模式Single
:单实例模式,如果目标页已经在栈中,则离栈顶最近的相同Url的页面,就会被移动到栈顶并重新加载
页面跳转
- 导入 HarmonyOS 提供的 Router 模块
import router from '@ohos.router'
-
利用 router 实现跳转、返回等操作
页面异常回调错误码:
100001
:內部错误,可能是渲染失败100002
:路由地址错误100003
:路由栈中的页面超过 32 个
import router from '@ohos.router';
class RouterInfo {
url: string
title: string
constructor(url: string, title: string) {
this.url = url
this.title = title
}
}
@Entry
@Component
struct RouterPage {
@State page_title: string = '页面列表'
private routers: RouterInfo[] = [
new RouterInfo('pages/basic/List', '商品列表')
]
build() {
Column() {
Text(this.page_title).lineHeight(50).padding({ left: 20 }).fontSize(20).fontWeight(FontWeight.Bold)
List({ space: 12 }) {
ForEach(this.routers, (item, index) => {
ListItem() {
this.RouterListItem(item, index + 1)
}
})
}.padding({ left: 20, right: 20 })
}
}
@Builder RouterListItem (item: RouterInfo, index: number) {
Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) {
Text(item.title).fontSize(16).fontColor('#ffffff')
Text((index).toString()).fontSize(16).fontColor('#ffffff')
}
.height(40)
.padding({ left: 20, right: 20 })
.backgroundColor('#278ff0')
.borderRadius(15)
.onClick(() => {
router.pushUrl(
{ url: item.url, params: { title: item.title } },
router.RouterMode.Single,
error => {
if (error) console.log(`路由失败,errCode: ${ error.code } message: ${ error.message }`)
}
)
})
}
}
-
配置路由
// entry/src/main/resources/base/profile/main_pages.json { "src": [ "page/router", "page/goods/detail" ] }
页面参数获取
// 获取路由传递过来的参数
parms: any = router.getParmas()
// 返回上一页
router.back()
// 返回到指定页面,并携带参数
router.back({ url: 'page/goods/detail', params: { id: 1 } })
动画
属性动画
属性动画 是通过设置组件的
animation
属性来给组件添加动画,当组件的width
、height
、Opacity
、backgroundColor
、scale
、rotate
、translate
等属性变更时,可以实现渐变过渡效果。
animation
需要放在样式属性之后,否则动画不会被生效
名称 | 参数类型 | 必填 | 描述 |
---|---|---|---|
duration | number | 否 | 设置动画时长 默认值: 1000,单位:毫秒 |
tempo | number | 否 | 动画播放速度,数值越大,速度越快 默认值:1 |
curve | string|Curve | 否 | 设置动画曲线 默认值:Curve.EaseInOut,平滑开始和结束 |
delay | number | 否 | 设置动画延迟执行的时长 默认值:0,单位:毫秒 |
iterations | number | 否 | 设置播放次数 默认值:1,取值范围:(-1, +∞) 说明:设置为 -1 时表示无限次播放 |
playMode | PlayMode | 否 | 设置动画播放模式,默认播放完成后重新开始播放 默认值:PlayMode.Normal |
onFinish | ()=>void | 否 | 状态回调,动画播放完成时触发 |
Image(this.src)
.width(40)
.height(40)
.position({ x: this.fishX - 20, y: this.fishY - 20 })
.rotate({
angle: this.angle, // 旋转角度
centerX: '50%', // 旋转中心横坐标
centerY: '50%' // 旋转中心纵坐标
}).animation({ duration: 500 })
Button('←').backgroundColor('#20101010').onClick(() => {
this.fishX -= 20
})
显式动画
显式动画 是通过全局
animateTo
函数来修改组件属性,实现属性变化时的渐变过渡效果。
Image(this.src)
.width(40)
.height(40)
.position({ x: this.fishX - 20, y: this.fishY - 20 })
.rotate({
angle: this.angle, // 旋转角度
centerX: '50%', // 旋转中心横坐标
centerY: '50%' // 旋转中心纵坐标
})
Button('←').backgroundColor('#20101010').onClick(() => {
animateTo({ duration: 500 }, () => {
this.fishX -= 20
})
})
组件转场动画
**
组件转场动画
**是在组件插入或移除时的过渡动画,通过组件的transition
属性来配置。
参数名称 | 参数类型 | 参数描述 | |
---|---|---|---|
type | TransitionType | 类型,默认包括组件新增和删除。默认是ALL | |
opacity | number | 不透明度,为插入时起点和删除时终点的值。 默认值:1,取值范围:[0, 1] | |
translate | { x?: number | string, y?: number string z?: number string } | 平移效果,为插入时起点和删除时终点的值。 -X:横向的平移距离。 y: 纵向的平移距离。 -z:竖向的平移距离。 |
scale | { x?: number y?: number z?: number centerX?: number string centerY?: numbe stringr } | 缩放效果,为插入时起点和删除时终点的值。 -x:横向放大倍数(或缩小比例)。 -y:纵向放大倍数(或缩小比例) -z:当前为二维显示,该參数无效。 centerx、 centerY指缩放中心点,centerX和 centerY默认值是“50%“。 中心点为0时,默认的是组件的左上角。 | |
rotate | { x?: number y?: number z?: number angle?: number string centerX?: number string centerY?: numbe stringr } | 旋转效果: angle是旋转角度,其它参数与scale类似 |
if (this.is_game_start) {
Image(this.src)
.width(40)
.height(40)
.position({ x: this.fishX - 20, y: this.fishY - 20 })
.rotate({
angle: this.angle, // 旋转角度
centerX: '50%', // 旋转中心横坐标
centerY: '50%' // 旋转中心纵坐标
})
.transition({
type: TransitionType.Insert,
opacity: 0,
translate: { x: -250 }
})
}
Button('开始游戏').backgroundColor('#278ff0').fontColor('#ffffff').onClick(() => {
animateTo({ duration: 1000 }, () => {
this.is_game_start = true
})
})
Stage模型
基本概念
Stage模型
HAP-Entry
HAP-Feature
HAP-Feature
HAP-Feature
HSP
打包编译
entry
Ability-Module
共享依赖模块
Library-Module
...
Bundle
APP
-
为了降低不同功能模块的耦合,每个模块都可以独立进行编译和运行
- 所有的
Ability
类型模块都将编译成HAP(HarmonyAbilityPackage-鸿蒙能力类型包)
文件 - 所有的
Library
类型模块都将编译成HSP(HarmonySharePackage-鸿蒙共享类型包)
文件
- 所有的
编译期
-
一个应用内部,只有存在一个
Entry
类型的HAP
,可以有存在多个Frature
类型的HAP
-
HAP
打包后将会合并到一起,形成一个Bundle
-
Bundle
有个自己的名字,成为BundleName
,BundleName
是整个应用的唯一标识,整个Bundle
的打包合并后,最终会形成一个APP
-
之所以采用多
HAP
文件的打包模式:- 降低不同模块之前的耦合
- 可以对应用进行选择性的安装,可以先安装
Entry
模式,其他的Feature
可以进行选择性安装
运行期
-
因为每个
HAP
都可以独立运行,HAP
在运行时都会创建一个AbilityStage
实例(应用组件‘舞台’) -
AbilityStage
有很多中类型,其中比较常见的有:UIAbility
UI能力组件ExtensionAbility
拓展能力组件
-
UIAbility
包含UI界面的应用组件,是系统调度的基本单元,在UIAbility
展示组件时,首先会持有一个WindowStage
实例对象WindowStage
组件窗口‘舞台’,组件窗口中含有Window
,而在Window
窗口中就可以进行ArkUI Page
的页面绘制
配置文件
Stage 模型的配置文件分为两类:
-
针对整个应用的全局配置文件
project-name/ │ ├── AppScope │ ├── app.json5
{ "app": { "bundleName": "com.example.harmonyapp", // 应用唯一标识,命名格式域名倒置 "vendor": "example", "versionCode": 1000000, "versionName": "1.0.0", "icon": "$media:app_icon", // 应用图标 AppScope/resources/base/media "label": "$string:app_name" // 应用名称 AppScope/resources/base/element } }
-
每个模块中的
module.json5
文件就是当前模块的配置文件{ "module": { "name": "entry", // 当前模块名称 "type": "entry", // 当前模块类型 entry | feature | shared "description": "$string:module_desc", // 当前模块描述 "mainElement": "EntryAbility", // 当前模块入口 entry/src/main/ets/entryability/EntryAbility "deviceTypes": [ // 当前模块设备类型 "phone", "tablet" ], "deliveryWithInstall": true, // 当前模块的Feature模块是否要跟随整个APP一起安装 "pages": "$profile:main_pages", // 当前模块内部所有的页面(entry/src/main/resources/base/profile/main_pages.json) /* * 声明访问权限 应用授权列表(https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/permission-list-0000001544464017-V2) * name 权限名称 必填,权限类型是用户授权(user_grant)时,才需要填写下面的信息 * reason 申请原因 * usedScene 使用场景 */ "requestPermissions": [ // 申请网络权限,用于加载网络图片等 { "name": "ohos.permission.INTERNET", } ], // 一个模块下可以创建多个 Ability, 每创建一个 Ability 都需要在这里指定处理 "abilities": [ { "name": "EntryAbility", // Ability 名称 "srcEntry": "./ets/entryability/EntryAbility.ts", // Ability源码地址 "description": "$string:EntryAbility_desc", // Ability描述 "icon": "$media:icon", // 当前 Ability 图标(入口应用的图标) "label": "$string:EntryAbility_label", // 当前 Ability 名称(入口应用的名称) "startWindowIcon": "$media:icon", // 应用启动时的图标 "startWindowBackground": "$color:start_window_background", // 应用启动时的背景色 "exported": true, "skills": [ // 当前 Ability 负责的功能,跟 Ability 之前的跳转有关系 { // 表示项目入口 "entities": ["entity.system.home"], "actions": ["action.system.home"] } ] } ] } }
AbilityStage
onMemoryLevel
系统决定调整内存时的回调函数onConfigurationUpdated
系统发生全局配置变化时的回调函数
UIAbility
UIAbility生命周期
/src/main/ets/entryability/EntryAbility.ts
-
create
创建Ability
-
onWindowStageCreate
创建WindowStage舞台
-
onForeground
应用被切换至前台Visible
当前WindowStage
可见Active
当WindowStage
可见时,获取焦点InActive
当前WindowStage
失去焦点InVisible
当前WindowStage
不可见
-
onBackground
应用被切换至后台onWindowStageDestroy
WindowStage
被销毁
-
onDestroy
应用被销毁时
页面组件生命周期
aboutToAppear
页面组件实例创建以后,build
函数执行之前aboutToDisappear
组件被销毁之前onPageShow
页面展示之后执行onPageHide
页面隐藏之前执行onBackPress
返回上一页之前执行
onPageShow
、onPageHide
、onBackPress
都是属于页面生命周期函数,只能在@Entry
下使用,普通组件则无法使用
aboutToAppear
、aboutToDisappear
属于组件生命周期函数,可以在任何页面或组件中使用
UIAbility启动模式
修改启动模式
// entry/src/main/module.json5
// 默认 singleton
{
"abilities": [
{
"launchType": "singleton",
// "launchType": "multion",
// "launchType": "standard",
// "launchType": "specified",
}
]
}
-
Singleton
【单实例模式】 每一个UIAbility只存在唯一实例。是默认启动模式。任务列表中只会存在一个相同的UIAbility
-
multion
每次启动UIAbility都会创建一个新的实例。在任务列表中可能存在一个或多个相同的UIAbility,但内存中只会存在一份
-
standard
与multion
相似,但不同之处是,standard
在内存中会存在多个(API10中好像被去除了) -
specified
每个UIAbility实例可以设置Key标示,启动UIAbility时,需要指定key,存在key相同实例直接被拉起,不存在则创建新实例。实现在一个Ability
中唤醒另一个Ability
-
当前
UIAbility
调用startAbility
方法拉起目标UIAbility
import common from '@ohos.app.ability.common' import Want from '@ohos.app.ability.Want' // 获取上下文 private context = getContext(this) as common.UIAbilityContext // 指定要跳转到的 UIAbility 信息 let want: Want = { deviceId: '', // 设备信息,不传(空)表示本设备 bundleName: 'com.example.harmonyapp', // 包名(应用名) /AppScope/app.json5 的 bundleName abilityName: 'DocumentAbility', // 要调用的 UIAbility 名称 moduleName: 'entry', // 要调用的 UIAbility 在哪个模块 parameters: { // 参数 // getInstancekey:自定义方法,生成目标UIAbility实例的key,要唤醒的 Ability 实例的 key instanceKey: this.getInstanceKey() } } // 尝试唤醒目标 UIAbility 实例 this.context.startAbility(want)
-
在
AbilityStage
的生命周期回调中为目标UIAbility
实例生成 keymkdir harmonyapp/entry/src/main/ets/myabilitystage/MyAbilityStage.ts
import AbilityStage from '@ohos.app.ability.AbilityStage' import Want from '@ohos.app.ability.Want' export default class MyABilityStage extends AbilityStage { onAcceptWant(want: Want): string{ // 判读当前要拉去的是否为 DocumentAbility if (want.abilityName === 'DocumentAbility') { // 根据参数中的 instanceKey 参数拼接生成一个 key 值并返回 return `document_ability_${want.parameters.instanceKey}` } return '' } }
-
在
module.json5
配置文件中,通过srcEntry
参数指定AbilityStage
路径{ "module": { "abilities": [ { ... }, // 默认的 EntryAbility { "srcEntry": "./ets/myabilitystage.MyAbilityStage.ts", "launchType": "specified" } ] } }
-
ExtensionAbility组件
ExtensionAbility
组件:是基于特定场景(例如服务卡片、输入法等)提供的应用组件。每一个具体场景对应一个ExtensionAbilityType
,开发者只能使用系统已定义的类型。
各类型的ExtensionAlblity
组件均由相应的系统服务统一管理,例如InputMethodExtensonAblity
组件由输入法管理服务统一管理
目前EtensionAblity
组件类型有用于输入法场景的InputMethodExtensionAlbilfty
用于闲时任务场景的WorkSchedullerExtensionAbillity
HTTP数据请求
网络管理模块主要分为一下功能:
HTTP数据请求
通过HTTP
发起请求WebScoket连接
使用WebScoket
建立服务器与客户端的双向连接Scoket连接
通过Scoket
进行数据传输
HTTP数据请求
-
导入
http
模块import http from '@ohos.net.http'
-
使用
http
模块发送请求,单方法对象不可复用,只能发送一次请求,下次再请求时,需要再创建一个新的对象HttpRequestOptions
名称 类型 描述 method RequestMethod 请求方式,GET、 POST、 PUT、 DELETE等 extraData string Object 请求参数 header Object 请求头字段 connectTimeout number 连接超时时间,单位亳秒, 默认60000ms readTimeout number 读取超时间,单位亳秒, 默认60000ms HttpResponseOptions
名称 类型 描述 responseCode ResponseCode 响应状态码 header Object 响应头 cookies string 响应返回的cookies result string | Object 响应体,默认是JSON字符串 resultType HttpDataType 返回值类型 class dataInfo { id: number, name: string, ... } class httpModel { baseUrl: string = '请求连接' getData(): Promise<dataInfo> { return new Promise(resolve, reject) => { // 创建一个 http 请求对象 let httpRequest = http.createHttp() // 发起网络请求 httpRequest.request(baseUrl+'接口地址', { method: http.RequestMethod.POST, extraData: { 'param1': 'value1', ... } }) httpRequest.request(baseUrl+'接口地址', { method: http.RequestMethod.GET, extraData: { 'key1=value1&key2=value2...' } }).then((res: http.HttpResponse) => { // 处理响应结果 if (res.responseCode === 200) { // 请求成功 resolve(JSON.parse(res.data.toString())) } }).catch((error: Error) => { // 请求失败 reject() }) } } }
Axios使用
下载安装ohpm
-
解压工具包,执行初始化命令
# Windows init.bat # Mac/Linux ./init.sh
-
将
ohpm
配置到环境变量中# windows 环境,直接在我的电脑配置即可 # Linux 或Mac环境,其中OHPM的路经请替换为ohpm的安装路经 export OHPM_HOME=/home/xx/Downloads/ohpm #本处路径请替换为ohpm的安装路径 export PATH=${OHPM_HOME}/bin:${PATH}
ohpm -v
下载安装Axios
ohpm install @ohos/axios
开放网络权限
// entry/src/main/module.json5 "requestPermissions": [ // 申请网络权限,用于加载网络图片等 { "name": "ohos.permission.INTERNET", } ]
安装成功后,就会在 oh-package.json5
中的 description
选项中显示
{
"dependencies": {
"@ohos/axios": "^2.2.0"
}
}
导入使用
-
导入
import axios from '@ohos/axios'
-
发送请求并处理响应
baseUrl: string = '请求连接' axios.get(baseUrl+url, { params: {}, // GET请求参数 data: {} // POST请求参数 }).then(res => { if (res.code === 200) { } }).catch(error => { console.log('请求失败') })
class dataInfo { id: number, name: string, ... } class httpModel { baseUrl: string = '请求连接' getData(): Promise<dataInfo> { return new Promise(resolve, reject) => { // 创建一个 http 请求对象 let httpRequest = http.createHttp() // 发起网络请求 axios.get(baseUrl+'接口地址', { data: {} }).then((res: http.HttpResponse) => { // 处理响应结果 if (res.status === 200) { // 请求成功 resolve(res.data) } }).catch((error: Error) => { // 请求失败 reject() }) } } }
数据持久化
用户首选项(Preference)为应用提供Key-Va lue键值型的数据处理能力,支持应用持久化轻量级数据。
使用步骤
preferences 的 key 必须为 string 类型,同时要求非空,且长度不超过80字节
value 可以是 string、number、boolean等类型,但大小不能超过8192字节
preferences 的数据量建议不超过一万条
-
导入首选项模块
import dataPreference from '@ohos.data.preferences'
-
获取首选项实例,读取指定文件
// MyAppPreferences ---> Preferences实例名称 dataPreference.getPreferences(this.context, 'MyAppPreferences').then(preferences => { // 获取成功 }) .catch(error => { // 获取失败 })
-
数据操作
// 写入数据,如果数据已经存在,则会覆盖之前的数据;可以用使用 .has() 方法进行判断数据是否存在 preferences.put('key', value).then(() => preferences.flush()) // 刷到磁盘 .catch(error => {}) // 异常处理 // 删除数据 preferences.delete('key').then(() => {}).catch(error => {}) // 查询数据,defaultValue ---> 如果查不到,则返回 defaultValue preferences.get('key', 'defaultValue').then(() => {}).catch(error => {})
实战案例
-
封装 Preferences 工具包实例,因为在写入数据时 value 支持的类型有很多,因此 Preferences 提供了 preferences.ValueType,用来声明 value 的字段类型
import preferences from '@ohos.data.preferences'; class PreferencesUtil { prefMap: Map<string, preferences.Preferences> = new Map() // 存储 preferences /* 加载 preferences 实例 */ loadPreferences (context, name: string) { preferences.getPreferences(context, name).then(pref => { // 存储 preferences this.prefMap.set(name, pref) console.log('preferences=====>', `加载preferences${name}成功`) }).catch(error => { console.log('preferences=====>', `加载preferences${name}失败`, JSON.stringify(error)) }) } /* 同步加载 preferences 实例 */ async loadPreferencesSync (context, name: string) { try { const result = await preferences.getPreferences(context, name) this.prefMap.set(name, result) } catch (error) { console.log('preferences=====>', `加载preferences${name}失败`, JSON.stringify(error)) } } /* 新增 preferences 数据 */ async putPreferencesValue (name: string, key: string, value: preferences.ValueType) { // 判断当前数据是否存在 if (this.prefMap.has(name)) { try { const currentPref = this.prefMap.get(name) // 写入数据 await currentPref.put(key, value) // 刷数据 await currentPref.flush() console.error('preferences=====>', `保存preferences数据${name}:${key}=${value}成功`) } catch (error) { console.error('preferences=====>', `保存preferences数据${name}:${key}=${value}失败`, JSON.stringify(error)) } } else { console.error('preferences=====>', `当前preferences${name}尚未初始化`) } } /* 获取 preferences 数据 */ async getPreferencesValue (name: string, key: string, defaultValue: preferences.ValueType) { // 判断当前数据是否存在 if (this.prefMap.has(name)) { const currentPref = this.prefMap.get(name) // 读取数据 const result = await currentPref.get(key, defaultValue) console.error('preferences=====>', `保存preferences数据${name}:${key}=${result}成功`) return result } else { console.error('preferences=====>', `获取preferences${name}失败`) } } /* 删除 preferences 数据 */ async deletePreferencesValue (name: string, key: string) { // 判断当前数据是否存在 if (this.prefMap.has(name)) { const currentPref = this.prefMap.get(name) // 写入数据 await currentPref.delete(key) console.error('preferences=====>', `删除preferences数据${name}:${key}=${value}成功`) } else { console.error('preferences=====>', `删除preferences${name}失败`) } } } const preferencesUtil = new PreferencesUtil() export default preferencesUtil as PreferencesUtil
-
在应用启动时,创建 Preferences 实例
// entry/src/main/ets/entryability/EntryAbility.ts import UIAbility from '@ohos.app.ability.UIAbility'; import PreferencesUtil from '../common/util/PreferencesUtil' export default class EntryAbility extends UIAbility { onCreate(want, launchParam) { // 加载 Preferences 实例 PreferencesUtil.loadPreferencesSync(this.context, 'MyPreferences') } }
-
写入 Preferences 数据
import PreferencesUtil from '../../common/util/PreferencesUtil' Text('设置大字号').onClick(() => { PreferencesUtil.putPreferencesValue('MyPreferences', 'font_size', 20) })
-
获取 Preferences 数据,在获取数据时,因为要接收的数据是 number 类型,而 Preferences 返回的是 preferences.ValueType 多类型,因此要使用
as number
来指定接收的字段类型import PreferencesUtil from '../../common/util/PreferencesUtil' // 推荐在页面生命周期创建时获取 async aboutToAppear() { this.font_size = await PreferencesUtil.getPreferencesValue('MyPreferences', 'font_size', 16) as number }
关系型数据库
关系型数据库(RDB)是基于SQLite组件提供的本地数据库,用于管理应用中的结构化数据。例如:记账本、备忘录、任务管理等。
- 不需要通过网络进行请求,直接在应用内部实现
- 还具备关系型数据库的所有高级特性,例如:事务、存储过程等
- 因为不通过网络,所以响应性能好
初始化数据库
-
导入关系型数据库模块
import relationalStore from '@ohos.data.relationalStore';
-
初始化数据库表
// rdb配置 const config = { name: 'HarmonyApp.db', // 数据库文件名 securityLevel: relationalStore.SecurityLevel.S1 // 数据库安全级别 } // 初始化表的SQL const sql = `CREATE TABLE IF NOT EXISTS TASK ( ID INTEGER PRIMARY KEY, NAME TEXT NOT NULL, FINISHED bit)` // 获取rdb relationalStore.getRdbStore(this.context, config, (err, rdbStore) => { // 执行Sql,后续的所有增册改查都是使用rdbStore对象 rdbStore.executeSql(sql) })
-
增删改
在 HarmonyOS 中
rdbStore
的API中也提供了比较常见的 增、删、改、查的方法简化了对数据的ADBC操作表格实例
ID NAME FINISHED 行号 -1 1 任务1 false 0 2 任务2 false 1 3 任务3 true 2 -
新增
// 准备数据 const task = { id: 0, name: '任务1', is_finished: false } // 新增数据 this.rdnStore.insert(this.tableName, task)
-
修改
// 准备数据 const task = { is_finished: true } // 查间条件, RabPredicates就是条件谓词,相当于判断查询 const predicates = new relationalStore.RdbPredicates(this.tableName) // 判断ID是否对应 predicates.equalTo('ID', id) // 执行更新 this.rdbstore.update(task, predicates)
-
删除
// 查询条件 const predicates = new relationalStore.RdbPredicates(this.tableName) // 判断ID是否对应 predicates.equalTo('ID', id) // 执行更新 this.rdbstore.delete(predicates)
-
查询数据
// 查询条件 const predicates = new relationalStore.RdbPredicates(this.tableName) // 执行查询 ['ID', 'NAME', 'FINISHED'] 要查询的字段列表,返回的是 结果集数据 const result = await this.rdnStore.query(predicates, ['ID', 'NAME', 'FINISHED'])
-
解析结果集数据,使用
result.getColumnIndex
获取列的编号// 保存解析后的数据 let taskList: ang[] = [] // 循环遍历结果集,判断是否结果是否遍历到最后一行 while (!result.isAtLastRow) { // 指针移动到下一行数据 result.goToNextRow() // 根据字段名称获取字段index,从而获取字段的值 const id = result.getLong(result.getColumnIndex('ID')) const name = result.getString(result.getColumnIndex('NAME')) taskList.push(id, name) }
-
示例代码
-
封装数据库工具类
// entry/src/main/ets/common/util/TaskModel.ets import relationalStore from '@ohos.data.relationalStore'; // 任务类型 @Observed class TaskInfo { static id: number = 1 // 任务名称 name: string = `任务${Task.id++}` // 任务状态:是否完成 finished: boolean = false } class TaskModel { // 设置成员变量 private rdbStore: relationalStore.RdbStore private table_name: string = 'Task' /* 初始化数据库 */ initTaskDB (context) { // rdb配置 const config = { name: 'HarmonyApp.db', // 数据库文件名 securityLevel: relationalStore.SecurityLevel.S1 // 数据库安全级别 } // 初始化表的SQL const sql = `CREATE TABLE IF NOT EXISTS Task ( ID INTEGER PRIMARY KEY, NAME TEXT NOT NULL, FINISHED bit)` // 获取rdb relationalStore.getRdbStore(context, config, (error, rdbStore) => { if (error) { console.log('获取rdb失败') return false } // 执行Sql,后续的所有增册改查都是使用rdbStore对象 rdbStore.executeSql(sql) console.log('HarmonyAppDB', '创建Task表成功') // 保存到成员变量 this.rdbStore = rdbStore }) } /* 查询数据库数据 */ getTaskDB () { // 构建查询条件 const predicates = new relationalStore.RdbPredicates(this.table_name) // 执行查询 ['ID', 'NAME', 'FINISHED'] 要查询的字段列表 const result = await this.rdnStore.query(predicates, ['ID', 'NAME', 'FINISHED']) // 查询的结果返回的是结果集,因此需要定义一个数组,组装最终的查询结果 const taskList: TaskInfo[] = [] // 判断数据是否为最后一行 while (!result.isAtLastRow) { // 指针移动到下一行 result.goToNextRow() // 获取数据 let id = result.getLong(result.getColumnIndex('ID')) let name = result.getString(result.getColumnIndex('NAME')) // 因为在数据库中,true 和 false 被存储为 0 和 1,因此也需要以number类型获取 Boolean 数据 let finished = result.getLong(result.getColumnIndex('FINISHED')) // 将数据存储到数组中 taskList.push({ id, name, finished: !!finished }) } return taskList } /* 添加数据,数据添加后,会以 Promise 的方式返回一个新增后的 id 值 */ addTaskDB (name: string) { this.rdbStore.insert(this.table_name, { name, finished: false }) } /* 更新Task数据状态 */ updateTaskStatus (id: number, finished: boolean) { const data = { finished } // 构建查询条件 const predicates = new relationalStore.RdbPredicates(this.table_name) // 判断数据是否一致,或数据是否存在 predicates.equalTo('ID', id) // 执行更新操作,返回 Promise 对象 return this.rdbStore.update(data, predicates) } /** 根据 id 删除数据 */ deleteTaskData (id: number) { // 构建删除条件 const predicates = new relationalStore.RdbPredicates(this.table_name) predicates.equalTo('ID', id) // 执行删除操作,返回 Promise 对象 return this.rdbStore.delete(predicates) } } const taskModel = new TaskModel() export default taskModel as TaskModel;
在项目启动时,创建初始化数据库操作
需要注意的是
EntryAbility
是 TS文件,而TaskModel
工具类是 ETS文件ETS文件中可以引用 TS文件,但 TS文件中是不可以引用 ETS文件的
因此要将
EntryAbility
的文件后缀修改为 ETS 后缀// entry/src/main/ets/entryability/EntryAbility.ts import UIAbility from '@ohos.app.ability.UIAbility'; import TaskModel from '../common/util/TaskModel' export default class EntryAbility extends UIAbility { onCreate(want, launchParam) { // 初始化任务表数据库 TaskModel.initTaskDB(this.context) } }
基础通知
应用可以通过通知接口发送通知消息
注意
- 从API version 11开始不再维护
contentType
,建议使用notificationContentType
代替。- 从API version 11开始不再维护
slotType
,建议使用notificationSlotType
代替。
let request: notificationManager.NotificationRequest = { id: 10, content: { notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, normal: { title: '通知标题', text: '通知内容详情', additionalText: '通知附加内容' } }, deliveryTime: new Date().getTime(), showDeliveryTime: true, groupName: 'app', notificationSlotType: notificationManager.SlotType.SOCIAL_COMMUNICATION }
notificationManager.publish
的错误回调也需要注明
import Base from '@ohos.base'; notificationManager.publish(request).then(() => { console.log('发送通知成功') }).catch((reason: Base.BusinessError) => { // 不加上 Base.BusinessError 就会报错 console.log('发送通知失败:失败原因', JSON.stringify(reason)) })
使用步骤
-
导入
notification
模块import notificationManager from '@ohos.notificationManager'
-
发布通知
// 构建通知请求对象 let request: notificationManager.NotificationRequest = { id: 10, // 当前通知的唯一标识,多次通知采用相同的 id 时,后面的通知内容将会覆盖原来的通知内容 content: { // 通知内容 } } // 发布通知 notificationManager.publish(request).then(() => { console.log('发送通知成功') }).catch(reason => { console.log('发送通知失败:失败原因', JSON.stringify(reason)) })
-
取消通知
// 取消指定 id 的通知 notificationManager.cancel(10) // 取消当前应用的的所有通知 notificationManager.cancelAll()
通知请求参数
类型 | 说明 |
---|---|
NOTIFICATION_CONTENT_BASIC_TEXT | 普通文本型 |
NOTIFICATION_CONTENT_LONG_TEXT | 长文本型 |
NOTIFICATION_CONTENT_MULTILINE | 多行文本型 |
NOTIFICATION_CONTENT_PICTURE | 图片型 |
-
普通文本类型
let request: notificationManager.NotificationRequest = { id: 10, content: { contentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, normal: { title: '通知标题', text: '通知内容详情', additionalText: '通知附加内容' } } }
-
长文本类型
let request: notificationManager.NotificationRequest = { id: 10, content: { contentType: notificationManager.ContentType.NOTIFICATION_CONTENT_LONG_TEXT, longText: { title: '通知标题', text: '通知内容详情', additionalText: '通知附加内容', longText: '通知中的长文本内容', briefText: '通知概要总结', expandedTitle: '通知展开时的标题' } } }
-
多行文本类型
let request: notificationManager.NotificationRequest = { id: 10, content: { contentType: notificationManager.ContentType.NOTIFICATION_CONTENT_MULTILINE, multiLine: { title: '通知标题', text: '通知内容详情', additionalText: '通知附加内容', longText: '通知中的长文本内容', briefText: '通知概要总结', longTitle: '通知展开时的标题', lines: [ '第一行', '第二行', '第三行' ] } } }
-
图片类型
let request: notificationManager.NotificationRequest = { id: 10, content: { contentType: notificationManager.ContentType.NOTIFICATION_CONTENT_PICTURE, picture: { title: '通知标题', text: '通知内容详情', additionalText: '通知附加内容', briefText: '通知概要总结', expandedTitle: '通知展开后的标题', picture: this.pixel } } }
这里的图片需要使用像素图,需要先将图片加载出来,再将图片创建为一个
PixelMap
,因为PixelMap
是基于像素级别,可以对图像进行任意修改操作,PixelMap
构建方式相对固定import { image } from '@kit.ImageKit'; async aboutToAppear() { // 获取资源管理器 const rm = getContext(this).resourceManager; // 读取图片 const file = await rm.getMediaContent($r('app.media.header_back')) // 创建 PixelMap image.createImageSource(file.buffer).createPixelMap().then(value => this.pixel = value).catch(reason => { console.log('图片加载异常', JSON.stringify(reason)) }) }
NotificationRequest常用参数
-
deliveryTime
发送通知的时间 -
showDeliveryTime
是否展示发送通知的时间 -
groupName
分组名称,默认每个通知是各自不同的分组,如果设置 groupName 可以实现,如微信app一样,将通知集中到一个通知分组中 -
slotType
通道类型,通知展示的方式类型类型 说明 状态栏图标 提示音 横幅 SOCIAL_COMMUNICATION 社交类型 √ √ √ SERVICE_INFORMATION 服务类型 √ √ × CONTENT_INFORMATION 内容类型 √ × × OTHER_TYPES 其他 × × ×
let request: notificationManager.NotificationRequest = {
id: 10, // 当前通知的唯一标识,多次通知采用相同的 id 时,后面的通知内容将会覆盖原来的通知内容
content: {
// 通知内容
},
deliveryTime: new Date().getTime(),
showDeliveryTime: true,
groupName: 'app',
slotType: notificationManager.SlotType.SOCIAL_COMMUNICATION
}
进度条通知
进度条通知会展示一个动态的进度条,主要用于文件下载、长任务处理的进度显示。
使用步骤
-
判断当前系统是否支持进度条模版
this.isSupport = await notificationManage.isSupportTemplate('downloadTemplate') if (!this.isSupport) { return false }
-
定义通知请求
// 通知模版 let template = { name: 'downloadTemplate', data: { progressValue: this.progressValue, // 进度条当前进度 progressMaxValue: 100 // 进度条最大值 } } // 通知请求 let request: notificationManage.NotificationRequest = { id: 999, template: template, content: { notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, normal: { title: `${this.filename: this.state}`, text: '', additionalText: `${this.progressValue}` } } }
实例代码
import notificationManager from '@ohos.notificationManager'
import promptAction from '@ohos.promptAction'
import Base from '@ohos.base';
enum DownloadState {
NOT_BEGIN = '未开始',
DOWNLOADING = '下载中',
PAUSE = '已暂停',
FINISHED = '已完成',
}
@Entry
@Component
struct DownloadProgressPage {
// 下载进度
@State progressValue: number = 0
progressMaxValue: number = 100
// 任务状态
@State state: DownloadState = DownloadState.NOT_BEGIN
// 下载的文件名
filename: string = '圣诞星.mp4'
// 模拟下载的任务的id,存储计时器
taskId: number = -1
// 通知id
notificationId: number = 999
isSupport: boolean = false
async aboutToAppear(){
// 判断当前系统是否支持进度条模板
this.isSupport = await notificationManager.isSupportTemplate('downloadTemplate')
}
build() {
Row() {
Column() {
Progress({
value: this.progressValue,
total: 100
})
Row({ space: 5 }) {
Text(`${(this.progressValue * 0.43).toFixed(2)}MB`).fontSize(14).fontColor('#c1c2c1')
Blank()
if (this.state === DownloadState.NOT_BEGIN) {
Button('开始').downloadButton().onClick(() => this.handleClickDownload())
} else if (this.state === DownloadState.DOWNLOADING) {
Button('取消').downloadButton().backgroundColor('#d1d2d3').onClick(() => this.handleClickCancelDown())
Button('暂停').downloadButton().onClick(() => this.handleClickPauseDown())
} else if (this.state === DownloadState.PAUSE) {
Button('取消').downloadButton().backgroundColor('#d1d2d3').onClick(() => this.handleClickCancelDown())
Button('继续').downloadButton().onClick(() => this.handleClickDownload())
} else {
Button('打开').downloadButton().onClick(() => this.handleClickOpenDown())
}
}.width('100%')
}
.width('100%')
.padding(20)
}
.height('100%')
}
/** 点击开始下载 */
handleClickDownload () {
// 清理旧任务
this.taskId && clearInterval(this.taskId);
// 开启定时任务,模拟下载
this.taskId = setInterval(() => {
// 判断任务进度是否达到100
if(this.progressValue >= 100){
// 任务完成了,应该取消定时任务
clearInterval(this.taskId)
this.taskId = -1
// 并且标记任务状态为已完成
this.state = DownloadState.FINISHED
// 发送通知
this.publishDownloadNotification()
return
}
// 模拟任务进度变更
this.progressValue += 2
// 发送通知
this.publishDownloadNotification()
}, 500)
// 标记任务状态:下载中
this.state = DownloadState.DOWNLOADING
}
/** 点击暂停下载 */
handleClickPauseDown () {
// 取消定时任务
if(this.taskId > 0){
clearInterval(this.taskId);
this.taskId = -1
}
// 标记任务状态:已暂停
this.state = DownloadState.PAUSE
// 发送通知
this.publishDownloadNotification()
}
/** 点击取消下载 */
handleClickCancelDown () {
// 取消定时任务
if(this.taskId > 0){
clearInterval(this.taskId);
this.taskId = -1
}
// 清理下载任务进度
this.progressValue = 0
// 标记任务状态:未开始
this.state = DownloadState.NOT_BEGIN
// 取消通知
notificationManager.cancel(this.notificationId)
}
/** 点击打开下载文件 */
handleClickOpenDown () {
promptAction.showToast({
message: '功能未实现'
})
}
/** 发送进度条通知消息 */
publishDownloadNotification () {
// 判断当前系统是否支持进度条模板
if (!this.isSupport) return
let request: notificationManager.NotificationRequest = {
id: this.notificationId,
template: {
name: 'downloadTemplate',
data: {
progressValue: this.progressValue,
progressMaxValue: this.progressMaxValue
}
},
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: `${this.filename}: ${this.state}`,
text: '',
additionalText: this.progressValue + '%'
}
}
}
// 发送通知
notificationManager.publish(request)
.then(() => console.log('test', '通知发送成功'))
.catch((reason: Base.BusinessError) => console.log('test', '通知发送失败!', JSON.stringify(reason)))
}
}
// @Extend 设置继承自哪个组件
@Extend(Button) function downloadButton() {
.width(75).height(28).fontSize(14)
}
通知意图
我们可以给通知或其中的按钮设置的行为意图
want
,从而实现拉起应用组件或发布公共事件等能力。说白了,就是当手机收到通知消息时,通过点击当前通知进入到对应的应用
就像微信通知那样,当我们收到一条微信消息时,通过点击微信消息就可以直接进入到微信聊天页面。
使用说明
import wantAgent, { WantAgent } from '@ohos.app.ability.wantAgent'
// 创建意图行为信息
let wantInfo: wantAgent.WantAgentInfo = {
wants: [
{
deviceId: '', // 设备ID,为空代表时,代表是当前设备
bundleName: 'com.example.harmonyapp', // 应用唯一标识
abilityName: 'EntryAbility', // 拉起应用中的哪一个 Ability 页面
action: '',
entities: []
}
],
requestCode: 0, // 请求码
operationType: wantAgent.OperationType.START_ABILITY, // 操作类型,接下来要做什么,START_ABILITY 启动一个页面
wantAgentFlags: [wantAgent.WantAgentFlags.CONSTANT_FLAG] // 标识当前行为意图信息
}
// 创建意图行为实例
this.wantAgentInstance = await wantAgent.getWantAgent(wantInfo)
// 通知请求
let request: notificationManager.NotificationRequest = {
id: this.notificationId,
template: {},
wantAgent: this.wantAgentInstance,
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: '下载',
text: '',
additionalText: this.progressValue + '%'
}
}
}
实例案例
import notificationManager from '@ohos.notificationManager';
import wantAgent, { WantAgent } from '@ohos.app.ability.wantAgent';
import promptAction from '@ohos.promptAction';
import Base from '@ohos.base';
enum DownloadState {
NOT_BEGIN = '未开始',
DOWNLOADING = '下载中',
PAUSE = '已暂停',
FINISHED = '已完成',
}
@Entry
@Component
struct DownloadProgressPage {
// 下载进度
@State progressValue: number = 0
progressMaxValue: number = 100
// 任务状态
@State state: DownloadState = DownloadState.NOT_BEGIN
// 下载的文件名
filename: string = '圣诞星.mp4'
// 模拟下载的任务的id,存储计时器
taskId: number = -1
// 通知id
notificationId: number = 999
isSupport: boolean = false
wantAgentInstance?: WantAgent
async aboutToAppear(){
// 判断当前系统是否支持进度条模板
this.isSupport = await notificationManager.isSupportTemplate('downloadTemplate')
// 创建拉取当前应用的行为意图
// 创建wantInfo信息
let wantInfo: wantAgent.WantAgentInfo = {
// 设置意图
wants: [
{
bundleName: 'com.example.harmonyapp',
abilityName: 'EntryAbility',
}
],
requestCode: 0,
operationType: wantAgent.OperationType.START_ABILITY,
wantAgentFlags: [wantAgent.WantAgentFlags.CONSTANT_FLAG]
}
// 创建wantAgent实例
this.wantAgentInstance = await wantAgent.getWantAgent(wantInfo)
}
build() {
Row() {
Column() {
Progress({
value: this.progressValue,
total: 100
})
Row({ space: 5 }) {
Text(`${(this.progressValue * 0.43).toFixed(2)}MB`).fontSize(14).fontColor('#c1c2c1')
Blank()
if (this.state === DownloadState.NOT_BEGIN) {
Button('开始').downloadButton().onClick(() => this.handleClickDownload())
} else if (this.state === DownloadState.DOWNLOADING) {
Button('取消').downloadButton().backgroundColor('#d1d2d3').onClick(() => this.handleClickCancelDown())
Button('暂停').downloadButton().onClick(() => this.handleClickPauseDown())
} else if (this.state === DownloadState.PAUSE) {
Button('取消').downloadButton().backgroundColor('#d1d2d3').onClick(() => this.handleClickCancelDown())
Button('继续').downloadButton().onClick(() => this.handleClickDownload())
} else {
Button('打开').downloadButton().onClick(() => this.handleClickOpenDown())
}
}.width('100%')
}
.width('100%')
.padding(20)
}
.height('100%')
}
/** 点击开始下载 */
handleClickDownload () {
// 清理旧任务
this.taskId && clearInterval(this.taskId);
// 开启定时任务,模拟下载
this.taskId = setInterval(() => {
// 判断任务进度是否达到100
if(this.progressValue >= 100){
// 任务完成了,应该取消定时任务
clearInterval(this.taskId)
this.taskId = -1
// 并且标记任务状态为已完成
this.state = DownloadState.FINISHED
// 发送通知
this.publishDownloadNotification()
return
}
// 模拟任务进度变更
this.progressValue += 2
// 发送通知
this.publishDownloadNotification()
}, 500)
// 标记任务状态:下载中
this.state = DownloadState.DOWNLOADING
}
/** 点击暂停下载 */
handleClickPauseDown () {
// 取消定时任务
if(this.taskId > 0){
clearInterval(this.taskId);
this.taskId = -1
}
// 标记任务状态:已暂停
this.state = DownloadState.PAUSE
// 发送通知
this.publishDownloadNotification()
}
/** 点击取消下载 */
handleClickCancelDown () {
// 取消定时任务
if(this.taskId > 0){
clearInterval(this.taskId);
this.taskId = -1
}
// 清理下载任务进度
this.progressValue = 0
// 标记任务状态:未开始
this.state = DownloadState.NOT_BEGIN
// 取消通知
notificationManager.cancel(this.notificationId)
}
/** 点击打开下载文件 */
handleClickOpenDown () {
promptAction.showToast({
message: '功能未实现'
})
}
/** 发送进度条通知消息 */
publishDownloadNotification () {
// 判断当前系统是否支持进度条模板
if (!this.isSupport) return
let request: notificationManager.NotificationRequest = {
id: this.notificationId,
template: {
name: 'downloadTemplate',
data: {
progressValue: this.progressValue,
progressMaxValue: this.progressMaxValue
}
},
wantAgent: this.wantAgentInstance,
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: `${this.filename}: ${this.state}`,
text: '',
additionalText: this.progressValue + '%'
}
}
}
// 发送通知
notificationManager.publish(request)
.then(() => console.log('test', '通知发送成功'))
.catch((reason: Base.BusinessError) => console.log('test', '通知发送失败!', JSON.stringify(reason)))
}
}
@Extend(Button) function downloadButton() {
.width(75).height(28).fontSize(14)
}
欢迎页面
自定义弹窗
使用
@CustomDialog
装饰器声明弹窗组件,在使用@CustomDialog
时必须声明controller
成员变量
/**
* 用户隐私政策弹窗
*/
@CustomDialog
struct UserPrivacyDialog {
// 设置 controller 类型为自定义弹窗控制器类型,用来控制弹窗状态
controller: CustomDialogController
build() {
Column() {
Text('隐私政策弹窗').fontSize(20)
}
}
}
-
在页面中使用弹窗控制器组件
/** * 用户隐私政策弹窗 */ @CustomDialog export default struct UserPrivacyDialog { controller: CustomDialogController // 设置 controller 类型为自定义弹窗控制器类型,用来控制弹窗状态 cancel: () => void = () => {} confirm: () => void = () => {} build() { Column({ space: 10 }) { Text('隐私政策弹窗').lineHeight(30).fontSize(20).fontWeight(FontWeight.Bold) Text('隐私政策内容隐私政策内容隐私政策内容隐私政策内容隐私政策内容隐私政策内容隐私政策内容隐私政策内容').lineHeight(28).fontSize(20).margin(10) Row() { Button('同意').onClick(() => { this.confirm() this.controller.close() }) .margin(10) Button('不同意').onClick(() => { this.cancel() this.controller.close() }) .margin(10) .backgroundColor('#ffffff') .fontColor('#333333') .border({ style: BorderStyle.Solid, width: 1, radius: 30 }) } } .width('100%') .padding(10) } }
欢迎页面
将
EntryAbility
页面的入口改为欢迎页面的路由地址
import UserPrivacyDialog from '../components/UserPrivacyDialog';
import { common } from '@kit.AbilityKit';
import PreferencesUtil from '../../common/util/PreferencesUtil';
import router from '@ohos.router';
// 声明首选项KEY
const USER_PREF_KEY = 'userPrivacyKey'
@Entry
@Component
struct WelcomePage {
@State message: string = 'Welcome';
context = getContext(this) as common.UIAbilityContext
dialogController: CustomDialogController = new CustomDialogController({
// 自定义弹窗内容构造器
builder: UserPrivacyDialog({
confirm: (): void => this.handleClickConfirm(),
cancel: (): void => this.exitApp(),
}),
autoCancel: false, // 是否点击遮障层退出
alignment: DialogAlignment.Center // 弹窗在竖直方向上的对齐方式
})
handleClickConfirm () {
// 用户同意后,将同意存储首选项
PreferencesUtil.putPreferencesValue('MyPreferences', USER_PREF_KEY, true)
// 跳转首页
this.handleRouterToHome()
}
/** 退出应用 */
exitApp () {
this.context.terminateSelf()
}
// 默认开启弹窗
async aboutToAppear(): Promise<void> {
// 加载首选项数据
const is_agree = await PreferencesUtil.getPreferencesValue('MyPreferences', USER_PREF_KEY, false)
// 判断用户是否同意隐私政策
if (is_agree) {
// 用户同意隐私政策后,跳转首页
this.handleRouterToHome()
} else {
// 用户没有同意隐私政策,则直接弹窗
this.dialogController.open()
}
}
// 跳转到首页
handleRouterToHome () {
setTimeout(() => router.replaceUrl({ url: 'pages/basic/DownloadProgressPage' }), 1000)
}
build() {
Row() {
Column() {
Text(this.message)
.padding({ top: 10, right: 30, bottom: 10, left: 30 })
.fontSize(30)
.fontWeight(FontWeight.Bold)
.border({ style: BorderStyle.Solid, width: 2, color: '#3D3D3D', radius: 60 })
.onClick(() => {
// 利用弹窗控制器 open() 方法打开弹窗,使用 close() 方法关闭
this.dialogController.open()
})
}
.width('100%')
}
.height('100%')
.backgroundColor('#ffffff')
.backgroundImage($r('app.media.welcome'))
.backgroundImageSize({ width: '100%', height: 'auto' })
.layoutWeight(1)
}
}
ArkUI组件
List列表组件
ForEach循环渲染组件
参数
arr
数据源itemGeneratow
(必填)为子组件生成函数,为数组中的每个元素创建对应的组件keyGenerator
(选填)是数组项唯一键值生成函数,为数据源arr的每个数组项生成唯一且持久的键值。
Swiper轮播组件
基本用法
Swiper() { Image('https://images.pexels.com/photos/15804651/pexels-photo-15804651.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500').border({ radius: 20 })
Image('https://images.pexels.com/photos/26051261/pexels-photo-26051261.jpeg?auto=compress&cs=tinysrgb&w=300&lazy=load').border({ radius: 20 })
}
.width('100%')
.height(100)
常见属性
属性方法 | 参数 | 作用 | 默认值 |
---|---|---|---|
loop | boolean | 是否开启循环轮播 | true |
autoPlay | boolean | 是否自动播放 | false |
interval | number | 自动播放时间间隔 | 3000 |
vertical | boolean | 纵向滑动轮播 | false |
Swiper() {
Image('https://images.pexels.com/photos/15804651/pexels-photo-15804651.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500').border({ radius: 10 })
Image('https://images.pexels.com/photos/26051261/pexels-photo-26051261.jpeg?auto=compress&cs=tinysrgb&w=300&lazy=load').border({ radius: 10 })
}
.width(355)
.height(100)
.loop(true)
.autoPlay(true)
.interval(3000)
样式自定义
Swiper() {
Image('https://images.pexels.com/photos/15804651/pexels-photo-15804651.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500').border({ radius: 10 })
Image('https://images.pexels.com/photos/26051261/pexels-photo-26051261.jpeg?auto=compress&cs=tinysrgb&w=300&lazy=load').border({ radius: 10 })
}
.width(355)
.height(100)
.loop(true)
.autoPlay(true)
.interval(3000)
// .indicator(false)
.indicator(
Indicator.dot() // 小圆点
.itemWidth(6) // 圆点默认宽度
.itemHeight(6) // 圆点默认高度
.color(Color.White) // 圆点颜色
.selectedItemWidth(14) // 圆点选中宽度
.selectedItemHeight(6) // 圆点选中高度
.selectedColor('#278ff0') // 圆点选中颜色
)
Scroll滚动容器
WaterFlow瀑布流容器
瀑布流容器,由“行”和“列”分割的单元格所组成,通过容器自身的排列规则,将不同大小的“项目”自上而下, 如瀑布般紧密布局。
实例
WaterFlow () {
ForEach(this.dataSource, (item.number) => {
FlowItem () {
Column () {
Text('N' + item)
Image('res/waterFlowTest(' + item % 5 + ').jpg')
}
}
}), (item: string => item)
}
.columnsTemplate('1fr 1fr')
.columnGap(10)
.rowGap(5)
常用组件
布局
自适应布局
元素可以根据相对关系自动变化以适应外部容器变化的布局能力。当前开发框架提炼了七种自适应布局能力,这些布局可以独立使用,也可多种布局叠加使用。
拉伸能力
拉伸能力是指容器组件尺寸发生变化时,增加或减小的空间全部分配给容器组件内指定区域。本例中,页面由中间的内容区(包含一张图片)以及两侧的留白区组成。
拉伸能力可以使用Flex
组件的 flexGrow()
、flexShrink
属性
Row(){
Row ().width(150).flexGrow(0).flexShrink(1)
Image ($r('app.media.photo')).width(400).flexGrow(1).flexShrink(0)
Row ().width(150).flexGrow(0).flexShrink(1)
}
均分能力
均分能力是指容器组件尺寸发生变化时,增加或减小的空间均匀分配给容器组件内所有空白区域。本例中, 父容器尺寸变化过程中,图标及文字的尺寸不变,图标间的间距及图标离左右边缘的距离同时均等改变。
均分能力主要使用Row
组件的 Column
、justifyContent(FlexAlign.SpaceEvenly)
属性进行设置
Column () {
Row () {
ForEach(this.list, (item: number) => {...})
}
.width('100%')
// 均匀分配父容器主轴方向的剩余空间
.justifyContent(FlexAlign.SpaceEvenly)
Row () {
ForEach(this.list, (item: number) => {...})
}
.width('100%')
// 均匀分配父容器主轴方向的剩余空间
.justifyContent(FlexAlign.SpaceEvenly)
...
}.width('100%')
占比能力
占比能力是指子组件的宽高按照预设的比例,随父容器组件发生变化。
Row () {
Column () {...}.layoutWeight(1) // 设置子组件在父容器主轴方向的布局权重
Column () {...}.layoutWeight(1) // 设置子组件在父容器主轴方向的布局权重
Column () {...}.layoutWeight(1) // 设置子组件在父容器主轴方向的布局权重
}.width('100%')
缩放能力
缩放能力是指子组件的宽高按照预设的比例,随容器组件发生变化,且变化过程中子组件的宽高比不变
Column () {
Column () {
Image($r('app.media.photo')).width('100%').height('100%')
}.aspectRatio(1) // 固定宽高比
}
.width('100%')
.height('100%')
延伸能力
容器组件内的子组件,按照其在列表中的先后顺序,随容器组件尺寸变化显示或隐藏
Row () {
// 通过List组件实现超出隐藏
List ({ space: 10 }) {...}
.listDirection(Axis.Horizontal)
.width('100%')
}.width('100%')
隐藏能力
容器组件内的子组件,按照其预设的显示优先级,随容器组件尺寸变化显示或隐藏。相同显示优先级的子组件同时显示或隐藏。通过 displayPriority
实现
Row() {
Image($r("app.media.photo")).displayPriority(1) // 布局优先级
Image($r("app.media.photo")).displayPriority(2) // 布局优先级
Image($r("app.media.photo")).displayPriority(3) // 布局优先级
Image($r("app.media.photo")).displayPriority(2) // 布局优先级
Image($r("app.media.photo")).displayPriority(1) // 布局优先级
}.width('100%')
折行能力
容器组件尺寸发生变化时,如果布局方向尺寸不足以显示完整内容,自动换行。通过 FlexWrap.Wrap
实现
Column() {
// 通过 Flex组件wrap参数实现自适应折行
Flex({
wrap: FlexWrap.Wrap,
direction: FlexDirection.Row
}) {
ForEact(this.list, (item: Resource) => {
Image(item).width(183).height(183)
})
}.width('100%')
}
响应式布局
元素可以根据特定的特征(如窗口宽度、屏幕方向等)触发变化以适应外部容器变化的布局能力。响应式布局基于断点、媒体查询、栅格等能力实现。
断点
将窗口宽度划分为不同的范围(即断点),监听窗口尺寸变化,当断点改变时同步调整页面布局。
注意,断点支持自定义,取值范围可修改扩展
断点名称 | 取值范围(vp) |
---|---|
xs | [0, 320) |
sm | [320, 600) |
md | [600, 840) |
lg | [840, +∞] |
媒体查询
媒体查询:媒体查询提供了丰富的媒体特征监听能力,可以监听应用显示区域变化、横竖屏、深浅色、设备类型等等,即借助媒体查询能力,监听断点的变化。
栅格布局
根据设备的水平宽度,将不同的屏幕尺寸划分为不同数量的栅格,来实现屏幕的自适应。
- 通过
GridRow
栅格布局可以调节布局占栅格的数量(设置参数span)
、偏移量(设置参数offset)
,来实现栅格的适配。 - 可以修改断点的取值范围,支持启用最多6个断点
(设置breakpoints的value参数)
@Entry
@Component
struct LoginPage {
@State message: string = 'Hello World';
build() {
GridRow({
// 初始化设置对应栅格个数
columns: { sm: 4, md: 8, lg: 12 },
gutter: { x: '12vp' }
}) {
GridCol({
/**
* 小屏幕目:占 4 个栅格,偏移量为 0
* 中屏幕目(折叠屏):占 6 个栅格,偏移量为 1
* 大屏幕目(平板):占 8 个栅格,偏移量为 2
*/
span: { sm: 4, md: 6, lg: 8 },
offset: { sm: 0, md: 1, lg: 0 }
}) {
Column() {
Image($r('app.media.icon')).width(56).height(56)
Text('买好货上涂多多').margin({ top: 10 })
}
.width('100%')
.height('100%')
.padding({ top: 20, bottom: 15 })
}
}
.backgroundColor('#f2f2f2')
}
}
缩进场景
- 通过设置
GridCol
的span
属性分配组件所占栅格列数 - 通过设置
GridCol
的offset
、GridRow
的gutter
等属性改变间距,实现最佳效果
GridRow({ columns: { sm: 4, md: 8, lg: 12 }, gutter: 24 }) {
GridCol({
span: { sm: 4, md: 6, lg: 8 },
offset: {md: 1, lg: 0 } // 0 可以进行省略
}) {
Column() {...}.width('100%')
}
}
挪移布局场景
通过设置GridCol
的span
属性分配组件所占栅格列数
GridRow({ columns: { sm: 4, md: 8, lg: 12 }, gutter: 24 }) {
GridCol({ span: { sm: 4, md: 6, lg: 8 } }) {...}
GridCol({ span: { sm: 4, md: 6, lg: 8 } }) {...}
}
.onBreakpointChange((breakpoint: string) => {
this.currentBreakpoint = breakpoint
})
重复布局
通过设置GridCol
的span
属性分配组件所占栅格列数
GridRow({ columns: { sm: 4, md: 8, lg: 12 }, gutter: 24 }) {
ForEach(this.list, () => {
// 通过配置元素在不同断点下占的列数,实现不同的布局效果
GridCol({ span: { sm: 4, md: 6, lg: 6 } }) {...}
})
}
一次开发,多端部署
视觉风格
分层参数
为了保证各组件有相同风格的默认样式,或者为了保证HarmonyoS系统应用有统一的风格。UX
定义了一套系统资源,预置在系统中,开发者可以直接使用,称为分层参数。
使用了分层参数后,当系统切换深色模式时,字体和背景也可以自适应。
Column() {
Text('分层参数')
.fontColor($r('sys.color.ohos_id_color_text_primary'))
.fontSize($r('sys.float.ohos_id_text_size_headline3'))
}
.backgroundColor($r('sys.color.ohos_id_color_background'))
资源 | 正常模式 | 深色模式 |
---|---|---|
ohos_id_color_text_primary | 黑色 | 白色 |
ohos_id_text_size_headline3 | 60vp | 60vp |
ohos_id_color_background | 白色 | 黑色 |
SysCap机制介绍
Harmonyos使用SysCap
机制(即SystemCapability),可以帮助开发者仅关注设备的系统能力,而不用考虑成百上千种具体的设备类型,降低多设备应用开发难度。
canlUse接口
在编码阶段,开发者可以通过canIUse
接口,判断目标设备是否支持某系统能力,进而执行不同的业务逻辑。
通常当设备不支持某种能力时,运行到这部分代码后,给出友好弹窗,避免应用crash崩溃。
aboutToAppear(): void {
if (canIUse("SystemCapability.Communication.NFC.Core")) {
console.log("该设备支持SystemCapability.Communication.NFC.Core");
} else {
console.log("该设备不支持SystemCapability.Communication.NFC.Core");
}
}
三层架构规范
推荐在应用开发过程中使用如下的“三层工程结构”,以方便代码复用及提升开发效率。
common
(公共能力层):用于存放公共基础能力集合(如公共配置等)。其只可以被product
和features
依赖,不可以反向依赖。features
(基础特性层):用于存放相对独立的UI及逻辑实现等。其可以横向调用及依赖common
层,但不能反向依赖product
层。product
(产品定制层):用于针对不同设备形态进行功能和特性集成,作为应用主入口。product
层不可以横向调用。
/application
├── common # 可选。公共能力层, 编译为HAR包或HSP包
├── features # 可选。基础特性层
│ ├── feature1 # 子功能1,例如:手机蓝牙 编译为HAR包或HSP包或Feature类型的HAP包
│ ├── feature2 # 子功能2,例如:手机WI-FI 编译为HAR包或HSP包或Feature类型的HAP包
│ └── ...
└── products # 必选。产品定制层
├── wearable # 智能穿戴泛类目录, 编译为Entry类型的HAP包
├── default # 默认设备泛类目录, 编译为Entry类型的HAP包
└── ...
自由流转
多端协同
多端协同的情况有并发、协作、互补,并发主要就是镜像相同的内容到不同的显示屏幕
跨端迁移应用开发示例
申请权限并启用接续能力
- 如果仅使用want迁移数据,无需申请权限。
- 如果需要使用分布式文件或分布式对象迁移数据,需要申请权限
- 如果未申请
ohos.permission.DISTRIBUTED_DATASYNC
权限,虽然会在组网内设备上出现接续应用图标,但是由于数据无法传输,会导致接续失败。- 如果未将
continuable
配置为true
,则在用户应用程序在组网内设备的docker栏上不会出现图标,无法触发应用迁移。
- 数据迁移需要申请ohos.permission.DISTRIBUTED_DATASYNC权限,详见声明权限。
- 由于数据迁移使用权限需要用户授权,所以在应用首次启动时弹窗向用户申请授权,详见向用户申请授权。
在module.json5
文件的abilities
中,将continuable
标签配置为true
,表示该UIAbility
可被迁移。配置为false
的UIAbility
将被系统识别为无法迁移且该配置默认值为false
。
{
"module": {
// module其他属性已省略
...
"abilities": [
{
// 接续能力是否可用配置开关
"continuable": true, // 配置系统迁移能力
"launchType": "singleton" // 设置系统启动模式
// ability其他属性已省略
...
}
],
"requestPermissions": [
{
// 申请权限
"name": "ohos.permission.DISTRIBUTED_DATASYNC",
"reason": "$string:distributed_data_sync",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
}
]
}
}
import UIAbility from '@ohos.app.ability.UIAbility';
import { abilityAccessCtrl, bundleManager, Permissions, Want, AbilityConstant } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
this.checkPermissions()
}
/**
* 获取用户授权
*/
async checkPermissions(): Promise<void> {
const permissions: Array<Permissions> = ['ohos.permission.DISTRIBUTED_DATASYNC']
const accessManager = abilityAccessCtrl.createAtManager()
try {
const bundleFlags = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
const bundleInfo = await bundleManager.getBundleInfoForSelf(bundleFlags)
const tokenId = bundleInfo.appInfo.accessTokenId
// 获取用户授权状态,检查用户是否授权
const grantStatus = await accessManager.checkAccessToken(tokenId, permissions[0])
// 未获得授权,则需要向用户申请授权
if (abilityAccessCtrl.GrantStatus.PERMISSION_DENIED === grantStatus) {
accessManager.requestPermissionsFromUser(this.context, permissions)
}
} catch (error) {
console.log(`用户授权失败,失败信息为:${error}`)
}
}
}
// 源端:就是当前正在使用的设备,实现源端回调
import UIAbility from '@ohos.app.ability.UIAbility';
import { AbilityConstant } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult | Promise<AbilityConstant.OnContinueResult> {
return AbilityConstant.OnContinueResult.AGREE
}
}
// 对端:实现对端回调
import UIAbility from '@ohos.app.ability.UIAbility';
import { Want, AbilityConstant } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
this.checkPermissions()
// 判断是否被迁移
if (AbilityConstant.LaunchReason.CONTINUATION === launchParam.launchReason) {
// 如果是迁移,则需要手动出发页面刷新操作
this.context.restoreWindowStage(new LocalStorage())
}
}
}
分布式数据传输
组件迁移数据
部分组件支持分布式数据迁移,如:List
、Grid
、Scroll
、WaterFlow
等
同时还需要设置迁移组件的restoreId
,这样就能完成自动迁移
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
Column() {
List({ space: 20 }) {
ForEach(this.list, (item: number) => {
ListItem() {...}
}, (item: number) => (item.toString()))
}.restoreId(1)
}
}
}
}
页面栈迁移
系统默认迁移页面栈,如果不希望迁移页面栈,可以将 wantParam["ohos.extra.param.key.supportContinuePageStack"]
设置为false
无论是否需要迁移页面栈,在对端的onCreate
函数中,只要是迁移的场景就要主动触发页面恢复
import UIAbility from '@ohos.app.ability.UIAbility';
import { abilityAccessCtrl, bundleManager, Permissions, Want, AbilityConstant } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
this.checkPermissions()
}
onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult | Promise<AbilityConstant.OnContinueResult> {
// 配置迁移页面栈和页面状态
wantParam["ohos.extra.param.key.supportContinuePageStack"] = false
return AbilityConstant.OnContinueResult.AGREE
}
}
少量状态数据迁移
少量状态数据迁移一般是小于100KB的状态数据
// 源端:实现源端回调
onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult | Promise<AbilityConstant.OnContinueResult> {
let sessionId: string = AppStorage.get('sessionId') as string
wantParam['sessionId'] = AppStorage.Get<string>('sessionId')
}
// 对端:恢复数据
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
// 判断是否被迁移
if (AbilityConstant.LaunchReason.CONTINUATION === launchParam.launchReason) {
AppStorage.setOrCreate<string>('sessionId', want.parameters?.sessionId)
// 如果是迁移,则需要手动出发页面刷新操作
this.context.restoreWindowStage(new LocalStorage())
}
}
内存数据迁移
适用于临时数据,生命周期较短,通常保存在内存数据中的,可以封装成一个对象,使用分布式数据对象的方式进行迁移
onWindowStageCreate(windowStage: window.WindowStage) {
...
if (!this.localObject) {
// 创建分布式数据对象
let mailInfo: MailInfo = new MailInfo(undefined, undefined, undefined, undefined)
this.localObject = distributedDataObject.create(this.context, mailInfo)
}
...
}
onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult | Promise<AbilityConstant.OnContinueResult> {
try {
let sessionId: string = AppStorage.get('sessionId') as string
if (!sessionId) {
sessionId = distributedDataObject.genSessionId()
AppStorage.setOrCreate('sessionId', sessionId)
}
if (this.localObject) {
// 设置 sessionId,将 sessionId 与对象绑定
this.localObject.setSessionId(sessionId)
// 将数据保存到分布书
this.localObject['recipient'] = AppStorage.get('recipient');
this.localObject['sender'] = AppStorage.get('sender');
this.localobject['subject'] = AppStorage.get('subject');
this.localObject['emailContent'] = AppStorage.get('emaiLContent');
// 触发对象同步
this.localobject.save(wantParam.targetDevice as string);
wantParam.distributedSessionId = sessionId:
}
} catch(error) {
}
}
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
// 判断是否被迁移
if (AbilityConstant.LaunchReason.CONTINUATION === launchParam.launchReason) {
// 从 want 中获取 sessionId
let sessionId: string = AppStorage.get('sessionId') as string
if (!this.localObject) {
// 创建分布式数据对象
let mailInfo: MailInfo = new MailInfo(undefined, undefined, undefined, undefined)
this.localObject = distributedDataObject.create(this.context, mailInfo)
// 监听分布式数据对象的数据变更
this.localObject.on('change', this.changeCall)
}
if (sessionId && this.localObject) {
// 设置 sessionId,将 sessionId 与对象绑定
this.localObject.setSessionId(sessionId)
// 从分布式数据对象中恢复数据
AppStorage.setOrCreate('recipient', this.localObject['recipient']);
AppStorage.setOrCreate('sender', this.localObject['sender']);
AppStorage.setOrCreate('subject', this.localObject['subject']);
AppStorage.setOrCreate('emaiLContent', this.localObject['emaiLContent']);
}
this.context.restoreWindowStage(new LocalStorage())
}
}