HarmonyOS Next 实战卡片开发 01
HarmonyOS Next 实战卡片开发 01
介绍
Form Kit(卡片开发服务)提供一种界面展示形式,可以将应用的重要信息或操作前置到服务卡片(以下简称“卡片”),以达到服务直达、减少跳转层级的体验效果。卡片常用于嵌入到其他应用(当前被嵌入方即卡片使用方只支持系统应用,例如桌面)中作为其界面显示的一部分,并支持拉起页面、发送消息等基础的交互能力。
如:
直达服务
要完成的案例
新建一个卡片
卡片的类型主要有两个:
- 静态卡片 如果界面需要频繁刷新,不建议使用静态卡片,因为每一次刷新,都会导致卡片实例创建和销毁
- 动态卡片 如果界面需要频繁刷新,建议使用动态卡片
最后,工程下会多出主要的三个文件,作用如下
新建一个卡片效果
卡片的配置
卡片的配置文件 在 entry/src/main/resources/base/profile/form_config.json
路径
我们实际开发中,可能需要尤为关注的主要有以下几个:
-
displayName 卡片对外显示的名称
-
description 卡片对外显示的描述
- supportDimensions 卡片支持的尺寸 ,支持多个
- defaultDimension 卡片默认显示的尺寸
- isDynamic 是否是动态卡片
- updateEnabled 是否允许 定时刷新或者定点刷新
- scheduledUpdateTime 定点刷新,精确到分钟。 如 10:30
- updateDuration 定时刷新,以 30 分钟为一个单位,如 1 表示 1 * 30 分钟
{
"forms": [
{
"name": "widget",
"displayName": "$string:widget_display_name",
"description": "$string:widget_desc",
"src": "./ets/widget/pages/WidgetCard.ets",
"uiSyntax": "arkts",
"window": {
"designWidth": 720,
"autoDesignWidth": true
},
"colorMode": "auto",
"isDynamic": true,
"isDefault": true,
"updateEnabled": false,
"scheduledUpdateTime": "10:30",
"updateDuration": 1,
"defaultDimension": "2*4",
"supportDimensions": [
"2*4"
]
}
]
}
对应的配置说明:
属性名称 | 含义 | 数据类型 | 是否可缺省 |
---|---|---|---|
name | 表示卡片的名称,字符串最大长度为 127 字节。 | 字符串 | 否 |
displayName | 表示卡片的显示名称。取值可以是名称内容,也可以是对名称内容的资源索引,以支持多语言。字符串最小长度为 1 字节,最大长度为 30 字节。 | 字符串 | 否 |
description | 表示卡片的描述。取值可以是描述性内容,也可以是对描述性内容的资源索引,以支持多语言。字符串最大长度为 255 字节。 | 字符串 | 可缺省,缺省为空。 |
src | 表示卡片对应的 UI 代码的完整路径。当为 ArkTS 卡片时,完整路径需要包含卡片文件的后缀,如:"./ets/widget/pages/WidgetCard.ets"。当为 JS 卡片时,完整路径无需包含卡片文件的后缀,如:"./js/widget/pages/WidgetCard" | 字符串 | 否 |
uiSyntax | 表示该卡片的类型,当前支持如下两种类型:- arkts:当前卡片为 ArkTS 卡片。- hml:当前卡片为 JS 卡片。 | 字符串 | 可缺省,缺省值为 hml |
window | 用于定义与显示窗口相关的配置。 | 对象 | 可缺省,缺省值见表 2。 |
isDefault | 表示该卡片是否为默认卡片,每个 UIAbility 有且只有一个默认卡片。- true:默认卡片。- false:非默认卡片。 | 布尔值 | 否 |
colorMode | 表示卡片的主题样式,取值范围如下:- auto:跟随系统的颜色模式值选取主题。- dark:深色主题。- light:浅色主题。 | 字符串 | 可缺省,缺省值为“auto”。 |
supportDimensions | 表示卡片支持的外观规格,取值范围:- 1 _ 2:表示 1 行 2 列的二宫格。- 2 _ 2:表示 2 行 2 列的四宫格。- 2 _ 4:表示 2 行 4 列的八宫格。- 4 _ 4:表示 4 行 4 列的十六宫格。- 1 _ 1:表示 1 行 1 列的圆形卡片。- 6 _ 4:表示 6 行 4 列的二十四宫格。 | 字符串数组 | 否 |
defaultDimension | 表示卡片的默认外观规格,取值必须在该卡片 supportDimensions 配置的列表中。 | 字符串 | 否 |
updateEnabled | 表示卡片是否支持周期性刷新(包含定时刷新和定点刷新),取值范围:- true:表示支持周期性刷新,可以在定时刷新(updateDuration)和定点刷新(scheduledUpdateTime)两种方式任选其一,当两者同时配置时,定时刷新优先生效。- false:表示不支持周期性刷新。 | 布尔类型 | 否 |
scheduledUpdateTime | 表示卡片的定点刷新的时刻,采用 24 小时制,精确到分钟。说明:updateDuration 参数优先级高于 scheduledUpdateTime,两者同时配置时,以 updateDuration 配置的刷新时间为准。 | 字符串 | 可缺省,缺省时不进行定点刷新。 |
updateDuration | 表示卡片定时刷新的更新周期,单位为 30 分钟,取值为自然数。当取值为 0 时,表示该参数不生效。当取值为正整数 N 时,表示刷新周期为 30*N 分钟。说明:updateDuration 参数优先级高于 scheduledUpdateTime,两者同时配置时,以 updateDuration 配置的刷新时间为准。 | 数值 | 可缺省,缺省值为“0”。 |
formConfigAbility | 表示卡片的配置跳转链接,采用 URI 格式。 | 字符串 | 可缺省,缺省值为空。 |
metadata | 表示卡片的自定义信息,参考Metadata数组标签。 | 对象 | 可缺省,缺省值为空。 |
dataProxyEnabled | 表示卡片是否支持卡片代理刷新,取值范围:- true:表示支持代理刷新。- false:表示不支持代理刷新。设置为 true 时,定时刷新和下次刷新不生效,但不影响定点刷新。 | 布尔类型 | 可缺省,缺省值为 false。 |
isDynamic | 表示此卡片是否为动态卡片(仅针对 ArkTS 卡片生效)。- true:为动态卡片 。- false:为静态卡片。 | 布尔类型 | 可缺省,缺省值为 true。 |
formVisibleNotify | 表示是否允许卡片使用卡片可见性通知(仅对系统应用的卡片生效)。 | 布尔类型 | 可缺省,缺省值为 false。 |
transparencyEnabled | 表示是否支持卡片使用方设置此卡片的背景透明度(仅对系统应用的 ArkTS 卡片生效。)。- true:支持设置背景透明度 。- false:不支持设置背景透明度。 | 布尔类型 | 可缺省,缺省值为 false。 |
fontScaleFollowSystem | 表示卡片使用方设置此卡片的字体是否支持跟随系统变化。- true:支持跟随系统字体大小变化 。- false:不支持跟随系统字体大小变化。 | 布尔类型 | 可缺省,缺省值为 true。 |
supportShapes | 表示卡片的显示形状,取值范围如下:- rect:表示方形卡片。- circle:表示圆形卡片。 | 字符串 | 可缺省,缺省值为“rect”。 |
表 2 window 对象的内部结构说明
属性名称 | 含义 | 数据类型 | 是否可缺省 |
---|---|---|---|
designWidth | 标识页面设计基准宽度。以此为基准,根据实际设备宽度来缩放元素大小。 | 数值 | 可缺省,缺省值为 720px。 |
autoDesignWidth | 标识页面设计基准宽度是否自动计算。当配置为 true 时,designWidth 将会被忽略,设计基准宽度由设备宽度与屏幕密度计算得出。 | 布尔值 | 可缺省,缺省值为 false。 |
卡片开发支持的能力
大部分情况下,页面支持的能力和卡片支持的能力大致一样。但是实际开发中,结合两方面来考量:
-
开发文档中,卡片可以使用的能力会有对应的文档说明
-
考虑到文档存在不完善的情况,以模拟器+真机实际测试为准
简单测试
entry/src/main/ets/widget/pages/WidgetCard.ets
主要的限制
-
当导入模块时,仅支持导入标识“支持在 ArkTS 卡片中使用”的模块。
-
不支持导入共享包。
-
不支持使用 native 语言开发。
-
仅支持声明式范式的部分组件、事件、动效、数据管理、状态管理和 API 能力。
-
卡片的事件处理和使用方的事件处理是独立的,建议在使用方支持左右滑动的场景下卡片内容不要使用左右滑动功能的组件,以防手势冲突影响交互体验。
-
暂不支持极速预览。
-
暂不支持断点调试能力。
-
暂不支持 Hot Reload 热重载。
-
暂不支持 setTimeOut。
卡片的生命周期
卡片的生命周期文件在
entry/src/main/ets/entryformability/EntryFormAbility.ets
支持以下生命周期:
描述 | 触发时机 |
---|---|
onAddForm | 卡片被创建时触发 |
onCastToNormalForm | 卡片转换成常态卡片时触发 |
onUpdateForm | 卡片被更新时触发(调用 updateForm 时) |
onChangeFormVisibility | 卡片可见性修改时触发 |
onFormEvent | 卡片发起特定事件时触发(message) |
onRemoveForm | 卡片被卸载时触发 |
onConfigurationUpdate | 当系统配置更新时调用 |
onAcquireFormState | 卡片状态发生改变时触发 |
onStop | 卡片进程退出时触发 |
import { formBindingData, FormExtensionAbility, formInfo } from "@kit.FormKit";
import { Want } from "@kit.AbilityKit";
export default class EntryFormAbility extends FormExtensionAbility {
onAddForm(want: Want) {
let formData = "";
return formBindingData.createFormBindingData(formData);
}
onCastToNormalForm(formId: string) {}
onUpdateForm(formId: string) {}
onFormEvent(formId: string, message: string) {}
onRemoveForm(formId: string) {}
onAcquireFormState(want: Want) {
return formInfo.FormState.READY;
}
}
卡片通信
实际场景中,因为卡片的运行是可以独立于应用或者页面。所以开发难点其实是在于卡片之间或者卡片和应用之间的通信。
为了方便理解,我们做出以下区分
- 应用或者页面
- 卡片
解释
-
卡片组件和卡片的Ability之间通过 message 和onAddForm通信
- 卡片 Ability 通过onAddForm中的返回值向卡片组件通信
- 卡片组件通过触发 message 事件向卡片Ability 通信
-
卡片组件和应用的 Ability 之间同router和call事件
- 卡片组件通过触发卡片组件通过触发call和router事件向 应用Ability 通信
- 应用通过updateForm向卡片组件通信
-
卡片通过LocalStorage装饰器接收数据
-
首选项可以在以上的任意地方进行通信
卡片 Ability 向卡片组件通信
卡片在创建时,会触发 onAddForm 生命周期,此时返回数据可以直接传递给卡片
另外卡片在被卸载时,会触发onRemoveForm生命周期
卡片 Ability
formBindingData:提供卡片数据绑定的能力,包括 FormBindingData 对象的创建、相关信息的描述
entry/src/main/ets/entryformability/EntryFormAbility.ets
import { formBindingData, FormExtensionAbility, formInfo } from "@kit.FormKit";
import { Want } from "@kit.AbilityKit";
export default class EntryFormAbility extends FormExtensionAbility {
onAddForm(want: Want) {
class FormData {
// 每一张卡片创建时都会被分配一个唯一的id
formId: string =
want.parameters!["ohos.extra.param.key.form_identity"].toString();
}
let formData = new FormData();
// 返回数据给卡片
return formBindingData.createFormBindingData(formData);
}
onCastToNormalForm(formId: string) {}
onUpdateForm(formId: string) {}
onFormEvent(formId: string, message: string) {}
onRemoveForm(formId: string) {}
onAcquireFormState(want: Want) {
return formInfo.FormState.READY;
}
}
卡片组件
卡片组件通过LocalStorage来接收onAddForm中返回的数据
const localStorage = new LocalStorage()
@Entry(localStorage)
@Component
struct WidgetCard {
// 接收onAddForm中返回的卡片Id
@LocalStorageProp("formId")
formId: string = "xxx"
build() {
Column() {
Button(this.formId)
}
.width("100%")
.height("100%")
.justifyContent(FlexAlign.Center)
}
}
效果
postCardAction
卡片主动向外通信都是通过触发postCardAction来实现的。postCardAction 支持三种事件类型
事件类型 | 描述 |
---|---|
router | 会拉起应用,前台会展示页面,会触发应用的 onCreate 和 onNewWant 生命周期 |
call | 会拉起应用,但是会在后台的形式运行。需要申请后台运行权限,可以进行比较耗时的任务 |
message | 不会拉起应用,一定时间内(10s)如果没有更新的事件,会被销毁,适合做不太耗时的任务 |
卡片组件向卡片 Ability 通信
卡片页面中可以通过postCardAction接口触发 message 事件拉起 FormExtensionAbility 中的onUpdateForm
onUpdateForm中通过 updateForm 来返回数据
卡片组件
记得要携带formId过去,因为返回数据时需要根据formId找到对应的卡片
entry/src/main/ets/widget/pages/WidgetCard.ets
const localStorage = new LocalStorage()
@Entry(localStorage)
@Component
struct WidgetCard {
// 接收onAddForm中返回的卡片Id
@LocalStorageProp("formId")
formId: string = "xxx"
@LocalStorageProp("num")
num: number = 100
build() {
Column() {
Button(this.formId)
Button("message" + this.num)
.onClick(() => {
postCardAction(this, {
action: 'message',
// 提交过去的参数
params: { num: this.num, aa: 200, formId: this.formId }
});
})
}
.width("100%")
.height("100%")
.justifyContent(FlexAlign.Center)
}
}
卡片 Ability
当卡片组件发起 message 事件时,我们可以通过onFormEvent监听到
entry/src/main/ets/entryformability/EntryFormAbility.ets
import {
formBindingData,
FormExtensionAbility,
formInfo,
formProvider,
} from "@kit.FormKit";
import { Want } from "@kit.AbilityKit";
export default class EntryFormAbility extends FormExtensionAbility {
onAddForm(want: Want) {
class FormData {
// 每一张卡片创建时都会被分配一个唯一的id
formId: string =
want.parameters!["ohos.extra.param.key.form_identity"].toString();
}
let formData = new FormData();
// 返回数据给卡片
return formBindingData.createFormBindingData(formData);
}
// ...
onFormEvent(formId: string, message: string) {
// 接收到卡片通过message事件传递的数据
// message {"num":100,"aa":200,"params":{"num":100,"aa":200},"action":"message"}
interface IData {
num: number;
aa: number;
}
interface IRes extends IData {
params: IData;
action: "message";
formId: string;
}
const params = JSON.parse(message) as IRes;
interface IRet {
num: number;
}
const data: IRet = {
num: params.num + 100,
};
const formInfo = formBindingData.createFormBindingData(data);
// 返回数据给对应的卡片
formProvider.updateForm(params.formId, formInfo);
}
}
效果
卡片组件发起向应用 EntryAbility 通信 router
router事件的特定是会拉起应用,前台会展示页面,会触发应用的onCreate和onNewWant生命周期
我们可以利用这个特性做唤起特定页面并且传递数据。
当触发router事件时,
- 如果应用没有在运行,便触发 onCreate事件
- 如果应用正在运行,便触发onNewWant事件
卡片组件
-
提前新建好两个页面 pageA 和 pageB
-
卡片组件新建两个按钮,实现拉起应用并且显示特定页面
entry/src/main/ets/widget/pages/WidgetCard.ets
const localStorage = new LocalStorage()
@Entry(localStorage)
@Component
struct WidgetCard {
// 接收onAddForm中返回的卡片Id
@LocalStorageProp("formId")
formId: string = "xxx"
@LocalStorageProp("num")
num: number = 100
build() {
Column() {
// Button(this.formId)
// Button("message" + this.num)
// .onClick(() => {
// postCardAction(this, {
// action: 'message',
// params: { num: this.num, aa: 200, formId: this.formId }
// });
// })
Button("A页面")
.onClick(() => {
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility', // 只能跳转到当前应用下的UIAbility
params: {
targetPage: 'pages/PageA',
}
});
})
Button("B页面")
.onClick(() => {
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility', // 只能跳转到当前应用下的UIAbility
params: {
targetPage: 'pages/PageB',
}
});
})
}
.width("100%")
.height("100%")
.justifyContent(FlexAlign.Center)
}
}
应用 EntryAbility
分布在应用的 onCreate 和 onNewWant 编写逻辑实现跳转页面
import { AbilityConstant, UIAbility, Want } from "@kit.AbilityKit";
import { hilog } from "@kit.PerformanceAnalysisKit";
import { router, window } from "@kit.ArkUI";
import { formInfo } from "@kit.FormKit";
export default class EntryAbility extends UIAbility {
// 要跳转的页面 默认是首页
targetPage: string = "pages/Index";
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 判断是否带有formId 因为我们直接点击图标,也会拉起应用,此时不会有formId
if (
want.parameters &&
want.parameters[formInfo.FormParam.IDENTITY_KEY] !== undefined
) {
// 获取卡片的formId
const formId =
want.parameters![formInfo.FormParam.IDENTITY_KEY].toString();
// 获取卡片传递过来的参数
interface IData {
targetPage: string;
}
const params: IData = JSON.parse(want.parameters?.params as string);
this.targetPage = params.targetPage;
// 我们也可以在这里通过 updateForm(卡片id,数据) 来返回内容给卡片
}
}
// 如果应用已经在运行,卡片的router事件不会再触发onCreate,会触发onNewWant
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
const formId = want.parameters![formInfo.FormParam.IDENTITY_KEY].toString();
// 获取卡片传递过来的参数
interface IData {
targetPage: string;
}
const params: IData = JSON.parse(want.parameters?.params as string);
this.targetPage = params.targetPage;
// 跳转页面
router.pushUrl({
url: this.targetPage,
});
// 我们也可以在这里通过 updateForm(卡片id,数据) 来返回内容给卡片
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// 跳转到对应的页面
windowStage.loadContent(this.targetPage, (err) => {
if (err.code) {
return;
}
});
}
onForeground(): void {
// Ability has brought to foreground
hilog.info(0x0000, "testTag", "%{public}s", "Ability onForeground");
}
onBackground(): void {
// Ability has back to background
hilog.info(0x0000, "testTag", "%{public}s", "Ability onBackground");
}
}
效果
此时实现的效果是,不管有没有启动过页面,我们都可以直接点击卡片跳转到对应的页面
卡片组件发起向应用 EntryAbility 通信 call
卡片还可以通过 postCardAction 的触发 call 事件,call 会拉起应用,但是会在后台的形式运行。需要申请后台运行权限,可以进行比较耗
时的任务
需要注意的是:
- 申请后台运行应用权限
entry/src/main/module.json5
{
"module": {
// ...
"requestPermissions": [
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
}
],
- 卡片组件触发 call 事件,参数中必须携带 method 属性,用来区分不同的方法
- 应用 EntryAbility 在 onCreate 中,通过 callee 来监听不同的 method 事件
卡片组件
卡片组件触发 call 事件,参数中必须携带 method 属性,用来区分不同的方法
const localStorage = new LocalStorage()
@Entry(localStorage)
@Component
struct WidgetCard {
// 接收onAddForm中返回的卡片Id
@LocalStorageProp("formId")
formId: string = "xxx"
@LocalStorageProp("num")
num: number = 100
build() {
Column() {
Button("call事件" + this.num)
.onClick(() => {
postCardAction(this, {
action: 'call',
abilityName: 'EntryAbility', // 只能跳转到当前应用下的UIAbility
params: {
// 如果事件类型是call,必须传递method属性,用来区分不同的事件
method: "inc",
formId: this.formId,
num: this.num,
}
});
})
}
.width("100%")
.height("100%")
.justifyContent(FlexAlign.Center)
}
}
应用 EntryAbility
entry/src/main/ets/entryability/EntryAbility.ets
应用 EntryAbility 在 onCreate 中,通过 callee 来监听不同的 method 事件。然后根据需求来处理业务
import { AbilityConstant, UIAbility, Want } from "@kit.AbilityKit";
import { hilog } from "@kit.PerformanceAnalysisKit";
import { router, window } from "@kit.ArkUI";
import { formBindingData, formInfo, formProvider } from "@kit.FormKit";
import { rpc } from "@kit.IPCKit";
// 占位 防止语法出错,暂无实际作用
class MyParcelable implements rpc.Parcelable {
marshalling(dataOut: rpc.MessageSequence): boolean {
return true;
}
unmarshalling(dataIn: rpc.MessageSequence): boolean {
return true;
}
}
export default class EntryAbility extends UIAbility {
// 要跳转的页面 默认是首页
targetPage: string = "pages/Index";
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 监听call事件中的特定方法
this.callee.on("inc", (data: rpc.MessageSequence) => {
// data中存放的是我们的参数
interface IRes {
formId: string;
num: number;
}
// 读取参数
const params = JSON.parse(data.readString() as string) as IRes;
interface IData {
num: number;
}
// 修改数据
const info: IData = {
num: params.num + 100,
};
// 响应数据
const dataInfo = formBindingData.createFormBindingData(info);
formProvider.updateForm(params.formId, dataInfo);
// 防止语法报错,暂无实际应用
return new MyParcelable();
});
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// 跳转到对应的页面
windowStage.loadContent(this.targetPage, (err) => {
if (err.code) {
return;
}
});
}
onForeground(): void {
// Ability has brought to foreground
hilog.info(0x0000, "testTag", "%{public}s", "Ability onForeground");
}
onBackground(): void {
// Ability has back to background
hilog.info(0x0000, "testTag", "%{public}s", "Ability onBackground");
}
}
效果
总结
本文主要介绍了 HarmonyOS Next 中的卡片开发,包括卡片的基本概念、类型、新建卡片、配置、支持的能力、生命周期、通信等方面的内容。
-
卡片概述
- Form Kit 提供将应用重要信息或操作前置到服务卡片的界面展示形式,可减少跳转层级,常用于嵌入系统应用(如桌面),支持拉起页面、发送消息等交互能力。
- 主要有静态卡片(不建议界面频繁刷新时使用)和动态卡片(适用于界面频繁刷新)。
-
卡片开发支持的能力
-
页面支持的能力与卡片大致相同,但实际开发需结合开发文档说明和模拟器及真机测试为准。
-
卡片开发存在诸多限制,如仅支持导入特定模块、不支持导入共享包、不支持 native 语言开发、仅支持声明式范式的
部分组件等,还暂不支持极速预览、断点调试、Hot Reload 热重载和 setTimeOut 等功能。
-
-
卡片的生命周期
卡片的生命周期文件为
EntryFormAbility.ets
,支持多个生命周期,如onAddForm
(卡片创建时触发)、onCastToNormalForm
(转换成常态卡片时触发)、onUpdateForm
(卡片更新时触发)等,每个生命周期有其特定的触发时机。 -
卡片通信
实际场景中,需区分应用或页面与卡片之间的通信。卡片组件和卡片 Ability 之间通过 message 和
onAddForm
通信;卡片组件和应用的 Ability 之间通过
router
和call
事件以及updateForm
通信;卡片通过LocalStorage
装饰器接收数据;首选项可在任意地方通信。