Form Kit(卡片开发服务)
鸿蒙应用中,Form / Card / Widget 都翻译为“卡片”
Form Kit(卡片开发服务)提供一种界面展示形式,可以将应用的重要信息或操作前置到服务卡片,以达到服务直达、减少跳转层级的体验效果。卡片常用于嵌入到其他系统应用(例如桌面)中作为其界面显示的一部分,并支持拉起页面、发送消息等基础的交互能力。
桌面上长按某个应用图标,弹出菜单中点击“服务卡片”,即可预览并添加该应用提供的服务卡片
卡片的约束
为防止卡片被恶意使用,Form Kit 要求卡片使用方应用只能是系统应用(一般是桌面应用) ,为确保使用体验以及控制功耗,系统对 ArkTS 卡片的能力做了以下约束:
- 当导入模块时,仅支持导入标识“支持在 ArkTS 卡片中使用”的模块。
- 不支持导入共享包。
- 不支持使用 native(C++) 语言开发。
- 仅支持声明式范式的部分组件、事件、动效、数据管理、状态管理和 API 能力。
- 卡片的事件处理和使用方的事件处理是独立的,建议在使用方支持左右滑动的场景下卡片内容不要使用左右滑动功能的组件,以防手势冲突影响交互体验。
- 暂不支持断点调试能力。
- 暂不支持 Hot Reload 热重载。
- 暂不支持 setTimeOut / setInterval()。
创建卡片
- 选择一个模块,比如我选择 entry,右键
- 新建-Service Widget-Static Widget/Dynamic Widget (静态/动态卡片)
- 然后选择卡片类型,填写卡片名字大小等等信息
- 注意:多个卡片共享一个 Ability name,所以多张卡片都是一个 Ability name
静态卡片:
以渲染后的最后一帧为静态图片呈现,不支持动画、动态数据、自定义事件,只能使用 FormLink 组件跳转到 提供方
动态卡片:
内容可以动态改变的卡片,支持动画、动态数据、自定义事件等,只是系统资源消耗要大于静态卡片
isDynamic
配置项赋值为 false 就是静态卡片,否则就是动态卡片
相关模块
比如我创建的卡片名字是 MyWidgetCard ,卡片的 Ability name 是 EntryFormAbility
下面的 4 个文件都是和卡片密切相关的
- src/main/ets/entryformability/EntryFormAbility.ets 卡片创建、销毁、刷新等生命周期回调
- src/main/ets/mywidget/pages/MyWidgetCard.ets 卡片业务代码
- src/main/resources/base/profile/form_config.json 配置卡片的细节设定
- src/main/module.json5 注册 FormExtensionAbiltiy
在 MyWidgetCard 上写卡片的业务代码时,样式按照在正常的组件逻辑即可,虽然卡片有
2*2
、2*4
等不同大小
相关进程
在真机调试时,需要注意卡片相关代码运行于不同的进程中,必需筛选正确的进程才能看到相关日志输出内容:
- UIAbility:运行于 App 主进程中,进程名就是当前应用的 bundleName
- FormExtensionAbility :运行在独立的卡片扩展进程中,进程名形如:bundleName:form
- WidgetCard :运行于系统进程中,进程名为 com.ohos.formrenderservice
卡片生命周期
EntryFormAbility.ets
export default class EntryFormAbility extends FormExtensionAbility {
// 使用方创建卡片时触发,提供方需要返回卡片数据绑定对象
onAddForm(want: Want) {
return formData;
}
// 使用方将临时卡片转换为常态卡片触发,提供方需要做相应的处理
onCastToNormalForm(formId: string) {}
// 若卡片支持定时更新/定点更新/卡片使用方主动请求更新功能,则提供方需要重写该方法以支持数据更新
onUpdateForm(formId: string) {}
// 需要配置formVisibleNotify为true,且为系统应用才会回调
onChangeFormVisibility(newStatus: Record<string, number>) {}
// 若卡片支持触发事件,则需要重写该方法并实现对事件的触发
onFormEvent(formId: string, message: string) {}
// 当对应的卡片删除时触发的回调,入参是被删除的卡片ID
onRemoveForm(formId: string) {}
// 当前formExtensionAbility存活时更新系统配置信息时触发的回调
onConfigurationUpdate(config: Configuration) {}
// 卡片提供方接收查询卡片状态通知接口,默认返回卡片初始状态
onAcquireFormState(want: Want) {}
}
注意:FormExtensionAbility 进程不能常驻后台,即在卡片生命周期回调函数中无法处理长时间的任务,在生命周期调度完成后会继续存在 10 秒,如 10 秒内没有新的生命周期回调触发则进程自动退出。
卡片的三种动作
卡片行为/事件 (这里只提供动作,不传递数据)
针对静态卡片,ArkTS 卡片提供了 FormLink 组件用于卡片内部和提供方应用间的交互。(不举例了)
针对动态卡片,ArkTS 卡片中提供了 postCardAction( ) 接口用于卡片内部和提供方应用间的交互,当前支持 router、message 和 call 三种类型的动作(action):
- router 动作:可以使用 router 事件跳转到指定 UIAbility(拉起窗口),并通过 router 事件刷新卡片内容。
- call 动作:可以使用 call 事件拉起指定 UIAbility 到后台(不会拉起窗口),并通过 call 事件刷新卡片内容。
- message 动作:可以使用 message 拉起 FormExtensionAbility(8 秒哥,8 秒后进程死去),并通过 FormExtensionAbility 刷新卡片内容。
router 动作
在卡片中使用 postCardAction 接口发起 router 动作,能够快速拉起卡片提供方应用的指定 UIAbility 或 Page,因此 UIAbility 或 Page 较多的应用往往会通过卡片提供不同的跳转按钮,实现一键直达的效果。
// src/main/ets/mywidget/pages/MyWidgetCard.ets 卡片的UI页面
@Entry
@Component
struct MyWidgetCard {
build() {
Button('拉起UIAbility-发起router动作').onClick(_ => {
postCardAction(this, {
action: 'router', //发起一个router动作,即拉起一个UIAbility到前台
abilityName: 'EntryAbility', //启动卡片提供方应用中的某个UIAbility
})
})
}
}
call 动作
可以借助卡片,实现和应用在前台时相同的功能。例如音乐卡片,卡片上提供播放、暂停等按钮,点击后将触发音乐应用的对应功能。在卡片中使用 postCardAction 接口的 call 动作,能够将卡片提供方应用的指定的 UIAbility 拉到后台。同时,call 动作提供了调用应用指定方法、传递数据的功能,使应用在后台运行时可以通过卡片上的按钮执行不同的功能。
// src/main/ets/mywidget/pages/MyWidgetCard.ets 卡片的UI页面
@Entry
@Component
struct MyWidgetCard {
build() {
Button('发起call动作').onClick(_ => {
postCardAction(this, {
action: 'call', //发起一个call动作,即调用一个UIAbility的方法,但保持UIAbility在后台运行
abilityName: 'EntryAbility', //启动卡片提供方应用中的某个UIAbility
params: {
method: 'play' //必需参数
}
})
})
}
}
// src/main/ets/entryability/EntryAbility.ets 卡片提供方UIAbility
import {
AbilityConstant,
ConfigurationConstant,
UIAbility,
Want,
} from "@kit.AbilityKit";
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 {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
//开始监听卡片发来的call动作, callee指代当前正在后台运行的UIAbility实例
this.callee.on("play", (dataFromCard: rpc.MessageSequence) => {
let data = dataFromCard.readString(); //读取卡片所在的进程发来的数据,默认都是JSON字符串
console.log("--读取到卡片发来的数据:", data);
return new MyParcelable();
});
}
}
message 动作
在卡片页面中可以通过 postCardAction 接口触发 message 事件拉起 FormExtensionAbility (8 秒哥) ,然后由 FormExtensionAbility 执行相关操作。
// src/main/ets/mywidget/pages/MyWidgetCard.ets 卡片的UI页面
@Entry
@Component
struct MyWidgetCard {
build() {
Button('发起message动作').onClick(_ => {
postCardAction(this, {
action: 'message', //发起一个message动作,即给指定的FormExtensionAbility发消息
// abilityName: 'EntryAbility', //每个卡片对应的FormExtensionAbility是固定的,可以省略不声明
})
})
}
}
刷新卡片内容
卡片刷新原理
用户点击卡片,发起三种动作之一,由 Ability 提供更新后的数据,从而实现卡片内容的“手动更新”;
也可以在卡片的配置文件中指定时间点或时间间隔,实现卡片内容的定点或定时“自动更新”。
卡片提供方可以调用 updateForm() 接口为卡片提供“卡片绑定数据”,卡片管理服务以 LocalStorage 形式转交给卡片,从而可以实现卡片的页面刷新效果。
let data = formBindingData.createFormBindingData( {k1: v1, k2: v2, ...} )
formProvider.updateForm( formId, data )
router 动作刷新卡片
在使用 postCardAction 接口发起 router 动作时,需要在 FormExtensionAbility 中的 onAddForm 生命周期回调中更新 formId:
效果:点击卡片后变化天气,分冷热启动显示不同值
// src/main/ets/mywidget/pages/MyWidgetCard.ets 卡片的UI页面
let storage = new LocalStorage()
@Entry(storage)
@Component
struct MyWidgetCard {
@LocalStorageProp('weather') weather: string = '晴'
build() {
Column() {
Text(this.weather)
Button('发起router动作').onClick(_ => {
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility',
//给目标UIAbility提供一些启动参数
params: {
location: 'Beijing', //自定义参数1
time: '2998/06/01', //自定义参数2
}
})
})
}
}
}
// src/main/ets/entryability/EntryAbility.ets 卡片提供方UIAbility
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
console.log(
"--EntryAbility.onCreate: 冷启动",
JSON.stringify(want.parameters)
);
if (want.parameters?.["formID"]) {
//如果是卡片拉起的UIAbility,则want中存在formID
let fid = String(Number(want.parameters["formID"]));
let data = formBindingData.createFormBindingData({
weather: "多云",
});
formProvider.updateForm(fid, data);
}
}
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
console.log(
"--EntryAbility.onNewWant: 热启动",
JSON.stringify(want.parameters)
);
if (want.parameters?.["formID"]) {
//如果是卡片拉起的UIAbility,则want中存在formID
let fid = String(Number(want.parameters["formID"]));
let data = formBindingData.createFormBindingData({
weather: "雷雨",
});
formProvider.updateForm(fid, data);
}
}
}
call 动作刷新卡片
在卡片中可以通过 postCardAction()接口触发 call 动作拉起 UIAbility 到后台,然后由 UIAbility 使用 updateForm( )方法携带数据刷新卡片内容。
formID 线路图
因为 formProvider.updateForm()需要卡片 id 才能干活
- call 获取 formID 需要从 FormExtensionAbibility 获取
- 获取到 formID 后传递给卡片的 UI 页面,写在 params 上
- 然后在把 formID 传递到卡片提供者的 UIAbility,有 id 就可以操作卡片了
来吧,分 3 步,效果是按钮显示 formID,点击刷新随机数
获取 formID 传递给卡片的 UI 页面
// src/main/ets/entryformability/EntryFormAbility.ets 卡片的Ability
export default class EntryFormAbility extends FormExtensionAbility {
onAddForm(want: Want) {
console.log(
"--FormExtensionAbility.onAddForm:一个卡片被添加到卡片使用方",
JSON.stringify(want)
);
let formId = want.parameters?.["ohos.extra.param.key.form_identity"];
// formProvider.setFormNextRefreshTime(String(formId), 5)
return formBindingData.createFormBindingData({ curFormId: formId });
}
}
卡片页面的构建和传递 formID 给卡片提供方 UIAbility
// src/main/ets/mywidget/pages/MyWidgetCard.ets 卡片页面
let storage = new LocalStorage()
@Entry(storage)
@Component
struct MyWidgetCard {
@LocalStorageProp('myName') myName: string = ''
@LocalStorageProp('curFormId') formID: string = ''
build() {
Column() {
Text('---' + this.myName)
Button(this.formID).onClick(_ => {
postCardAction(this, {
action: 'call',
abilityName: 'EntryAbility',
params: {
method: 'play',
curFormId: this.formID
}
})
})
}
}
}
// src/main/ets/entryability/EntryAbility.ets 卡片提供方UIAbility
class MyParcelable implements rpc.Parcelable {
marshalling(dataOut: rpc.MessageSequence): boolean {
return true; //数据序列化
}
unmarshalling(dataIn: rpc.MessageSequence): boolean {
return true; //数据反序列化
}
}
interface DataFromCard {
method: string;
curFormId: string;
}
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
console.log("--PlayerAbility.onCreate: 冷启动");
//开始监听卡片发来的call动作, callee指代当前正在后台运行的UIAbility实例
this.callee.on("play", (data) => {
let dataFromCard = JSON.parse(data.readString()) as DataFromCard;
//给卡片发送更新后的数据
let fid = dataFromCard.curFormId;
let dataToCard = formBindingData.createFormBindingData({
myName: Math.floor(Math.random() * 100).toString(),
});
formProvider.updateForm(fid, dataToCard);
return new MyParcelable(); //此处返回此对象仅仅为了满足TS语法检查
});
}
}
message 动作刷新卡片
在卡片页面中可以通过 postCardAction 接口触发 message 事件拉起 FormExtensionAbility,然后由 FormExtensionAbility 执行相关操作。
// src/main/ets/mywidget/pages/MyWidgetCard.ets 卡片页面
let storage = new LocalStorage()
@Entry(storage)
@Component
struct MyWidgetCard {
@LocalStorageProp('myCount') count: number = 0
build() {
Column() {
Button(this.count.toString()).onClick(_ => {
postCardAction(this, {
action: 'message',
// abilityName: 'EntryAbility',
})
})
}
}
}
// src/main/ets/entryformability/EntryFormAbility.ets 卡片的Ability
export default class EntryFormAbility extends FormExtensionAbility {
//卡片主动给FormExtensionAbility发来message动作
onFormEvent(formId: string, message: string) {
console.log(
"--FormExtensionAbility.onFormEvent:一个卡片发来一个事件",
formId,
message
);
let data = formBindingData.createFormBindingData({
//把普通的JS对象封装为“卡片绑定数据”对象
myCount: Math.floor(Math.random() * 100),
});
formProvider.updateForm(formId, data);
}
}
定时/定点刷新
除了前面的三个特定 action 可以手动更新卡片内容,卡片框架提供了如下三种按时间刷新卡片的方式:
定时刷新:卡片使用方在一定时间间隔内调用 requestForm()从而触发 onUpdateForm 的生命周期回调函数实现卡片内容的刷新。可以在
form_config.json
配置文件的updateDuration
字段中进行设置。例如,可以将刷新时间设置为每小时一次。定点刷新:表示在每天的某个特定时间点自动刷新卡片内容。可以在
form_config.json
配置文件中的scheduledUpdateTime
字段中进行设置。例如,可以将刷新时间设置为每天的上午 10 点 30 分。下次刷新:表示指定卡片的下一次刷新时间。可以通过调用
setFormNextRefreshTime()
接口来实现。最短刷新时间为 5 分钟。例如,可以在接口调用后的 5 分钟内刷新卡片内容。
定时刷新
至少要等半个小时才能看到效果,及其不方便
// src/main/ets/mywidget/pages/MyWidgetCard.ets
let storage = new LocalStorage()
@Entry(storage)
@Component
struct MyWidgetCard {
@LocalStorageProp('myTime') time1: string = ''
build() {
Column() {
Text('--' + this.time1)
}
}
}
// src/main/ets/entryformability/EntryFormAbility.ets
export default class EntryFormAbility extends FormExtensionAbility {
//卡片使用方根据form_config.json配置申请刷新卡片内容
onUpdateForm(formId: string) {
console.log(
"--FormExtensionAbility.onUpdateForm:一个卡片需要更新内容",
formId
);
let data = formBindingData.createFormBindingData({
myTime: new Date().toLocaleString(),
});
formProvider.updateForm(formId, data);
}
}
// src/main/resources/base/profile/form_config.json
{
"forms": [
{
"name": "widget",
"isDynamic": true, // 只有动态卡片才能更新内容
"updateEnabled": true, //启用卡片内容更新功能
"scheduledUpdateTime": "22:00", // 启用定时更新时,定点更新自动失效
"updateDuration": 1 // 定时更新时间间隔,取值范围1~336,此处的 1 代表 1*30分钟
}
]
}
定点刷新
这个设定时间后可以马上看到效果,除了 form_config.json 和定时刷新不一样,其他的都一样
// src/main/resources/base/profile/form_config.json
{
"forms": [
{
"name": "widget",
"isDynamic": true, // 只有动态卡片才能更新内容
"updateEnabled": true, // 启用卡片内容更新功能
"scheduledUpdateTime": "10:30", // 定点更新时间
"updateDuration": 0 // 定时更新时间间隔设定为0,才能启用定点更新
}
]
}
下次刷新
注意:
- 定时刷新有配额限制,每张卡片每天最多通过定时方式触发刷新 50 次,定时刷新包含卡片配置项 updateDuration 和调用 setFormNextRefreshTime()方法两种方式,当达到 50 次配额后,无法通过定时方式再次触发刷新,刷新次数会在每天的 0 点重置。
- 当前定时刷新使用同一个计时器进行计时,因此卡片定时刷新的第一次刷新会有最多 30 分钟的偏差。比如第一张卡片 A(每隔半小时刷新一次)在 3 点 20 分添加成功,定时器启动并每隔半小时触发一次事件,第二张卡片 B(每隔半小时刷新一次)在 3 点 40 分添加成功,在 3 点 50 分定时器事件触发时,卡片 A 触发定时刷新,卡片 B 会在下次事件(4 点 20 分)中才会触发。
- 定时刷新和定点刷新仅在屏幕亮屏情况下才会触发,在灭屏场景下仅会记录刷新动作,待亮屏时统一进行刷新。
// src/main/ets/entryformability/EntryFormAbility.ets
export default class EntryFormAbility extends FormExtensionAbility {
onFormEvent(formId: string, message: string): void {
formProvider.setFormNextRefreshTime(formId, 5);
}
}