【鸿蒙开发】第四十章 Form Kit(卡片开发服务)
目录
1 概述
1.1 卡片使用场景
1.2 服务卡片架构
1.3 亮点/特征
1.4 开发模式
1.5 与相关Kit的关系
1.6 约束限制
2 ArkTS卡片运行机制
2.1 实现原理
2.2 ArkTS卡片的优势
2.3 ArkTS卡片的约束
3 ArkTS卡片相关模块
4 ArkTS卡片开发指导
4.1 创建一个ArkTS卡片
4.2 配置卡片的配置文件
isDynamic标签
window标签
4.3 卡片生命周期管理
4.4 开发卡片页面
4.4.1 卡片页面能力说明
4.4.2 卡片使用动效能力
4.4.3 卡片使用自定义绘制能力
4.5 开发卡片事件
4.5.1 卡片事件能力说明
动态卡片事件能力说明
静态卡片事件能力说明
4.5.2 拉起卡片提供方的UIAbility(router事件)
开发步骤
4.5.3 拉起卡片提供方的UIAbility到后台(call事件)
开发步骤
4.5.4 通过message事件刷新卡片内容
4.5.5 通过router或call事件刷新卡片内容
通过router事件刷新卡片内容
通过call事件刷新卡片内容
4.6 卡片数据交互
4.6.1 卡片内容更新
4.6.2 卡片定时刷新
4.6.3 卡片定点刷新
4.6.4 刷新本地图片和网络图片
4.6.5 根据卡片状态刷新不同内容
1 概述
Form Kit(卡片开发框架)提供了一种在桌面、锁屏等系统入口嵌入显示应用信息的开发框架和API,可以将应用内用户关注的重要信息或常用操作抽取到服务卡片(以下简称“卡片”)上,通过将卡片添加到桌面上,以达到信息展示、服务直达的便捷体验效果。
1.1 卡片使用场景
- 支持设备类型:卡片可以在手机、平板等设备上使用。
- 支持开发卡片应用类型:应用和元服务内均支持开发卡片。
- 支持卡片使用位置:用户可以在桌面、锁屏等系统应用上添加使用,暂不支持在普通应用内嵌入显示卡片。
1.2 服务卡片架构
图1 服务卡片架构
卡片场景中涉及到的基本概念
卡片使用方:如上图中的桌面,作为显示卡片内容的宿主应用,用于与用户直接进行交互,完成卡片添加、删除、显示功能,并能控制卡片在宿主中具体展示的位置。
卡片提供方:提供卡片的应用或元服务,是卡片功能的具体实现者,需要设计实现卡片UI、数据更新、以及点击交互处理功能。
卡片管理服务:操作系统内管理整机卡片信息的系统服务,作为卡片提供方和使用方的桥梁,向使用方提供卡片信息查询、添加、删除等能力,同时向提供方提供卡片被添加、被删除、刷新、点击事件等通知能力。
卡片的常见使用步骤如下:
图2 卡片常见使用步骤
- 长按“桌面图标”,弹出操作菜单。
- 点击“卡片”选项,进入卡片预览界面。
- 点击“添加到桌面”按钮,即可在桌面上看到新添加的卡片。
1.3 亮点/特征
-
信息呈现:将应用/元服务的重要信息以卡片形式展示在桌面,同时支持信息定时更新能力,用户可以随时查看关注的信息。
-
服务直达:通过点击卡片内按钮,就可以实现功能快捷操作,也支持点击后跳转到应用/元服务对应功能页,实现功能服务一步直达的效果。
1.4 开发模式
应用运行模式选择
当前系统中应用开发模型支持Stage和FA两种方式,所以Form Kit也同时支持开发者使用Stage模型和FA模型来开发卡片应用,但更推荐使用Stage模型。
UI开发范式选择
- Stage模型支持两种卡片UI开发方式,可以基于声明式范式ArkTS语言开发卡片(简称ArkTS卡片)、也可以基于类Web范式JS语言开发卡片(简称JS卡片)。
- FA模型仅支持基于类Web范式JS语言开发JS卡片。
ArkTS卡片与JS卡片具备不同的实现原理及特征,在场景能力上的差异如下表所示:
类别 | JS卡片 | ArkTS卡片 |
---|---|---|
开发范式 | 类Web范式 | 声明式范式 |
组件能力 | 支持 | 支持 |
布局能力 | 支持 | 支持 |
事件能力 | 支持 | 支持 |
自定义动效 | 不支持 | 支持 |
自定义绘制 | 不支持 | 支持 |
逻辑代码执行 | 不支持 | 支持 |
1.5 与相关Kit的关系
- Ability Kit: Form Kit内部实现依赖Ability Kit提供的Extension基础能力,与Ability Kit存在生命周期调度交互。
- ArkUI: Form Kit卡片提供方在卡片页面中可以使用ArkUI提供的部分组件、事件、动效、状态管理等能力。
1.6 约束限制
UI能力约束
卡片尺寸大小有限,适合承载简洁明了的信息和交互操作,所以卡片开发中支持使用的UI控件范围、动效能力会有一定限制。
运动能力约束
卡片能为应用和元服务提供在桌面等入口常驻的信息显示和更新的能力,系统对于更新频次和卡片后台运行能力范围会有一定的限制。
2 ArkTS卡片运行机制
2.1 实现原理
图1 ArkTS卡片实现原理
卡片使用方:显示卡片内容的宿主应用,控制卡片在宿主中展示的位置,当前仅系统应用可以作为卡片使用方。
卡片提供方:提供卡片显示内容的应用,控制卡片的显示内容、控件布局以及控件点击事件。
卡片管理服务:用于管理系统中所添加卡片的常驻代理服务,提供formProvider的接口能力,同时提供卡片对象的管理与使用以及卡片周期性刷新等能力。
卡片渲染服务:用于管理卡片渲染实例,渲染实例与卡片使用方上的卡片组件一一绑定。卡片渲染服务运行卡片页面代码widgets.abc进行渲染,并将渲染后的数据发送至卡片使用方对应的卡片组件。
图2 ArkTS卡片渲染服务运行原理
与动态卡片相比,静态卡片整体的运行框架和渲染流程是一致的,主要区别在于,卡片渲染服务将卡片内容渲染完毕后,卡片使用方会使用最后一帧渲染的数据作为静态图片显示,其次卡片渲染服务中的卡片实例会释放该卡片的所有运行资源以节省内存。因此频繁的刷新会导致静态卡片运行时资源不断的创建和销毁,增加卡片功耗。
与JS卡片相比,ArkTS卡片支持在卡片中运行逻辑代码,为确保ArkTS卡片发生问题后不影响卡片使用方应用的使用,ArkTS卡片新增了卡片渲染服务用于运行卡片页面代码widgets.abc,卡片渲染服务由卡片管理服务管理。卡片使用方的每个卡片组件都对应了卡片渲染服务里的一个渲染实例,同一应用提供方的渲染实例运行在同一个ArkTS虚拟机运行环境中,不同应用提供方的渲染实例运行在不同的ArkTS虚拟机运行环境中,通过ArkTS虚拟机运行环境隔离不同应用提供方卡片之间的资源与状态。开发过程中需要注意的是globalThis对象的使用,相同应用提供方的卡片globalThis对象是同一个,不同应用提供方的卡片globalThis对象是不同的。
2.2 ArkTS卡片的优势
卡片作为应用的一个快捷入口,ArkTS卡片相较于JS卡片具备如下几点优势:
-
统一开发范式,提升开发体验和开发效率。
提供ArkTS卡片能力后,统一了卡片和页面的开发范式,页面的布局可以直接复用到卡片布局中,提升开发体验和开发效率。
图3 卡片工程结构对比
-
增强了卡片的能力,使卡片更加万能。
- 新增了动效的能力:ArkTS卡片开放了属性动画和显式动画的能力,使卡片的交互更加友好。
- 新增了自定义绘制的能力:ArkTS卡片开放了Canvas画布组件的能力,卡片可以使用自定义绘制的能力构建更多样的显示和交互效果。
- 允许卡片中运行逻辑代码:开放逻辑代码运行后很多业务逻辑可以在卡片内部自闭环,拓宽了卡片的业务适用场景。
2.3 ArkTS卡片的约束
ArkTS卡片相较于JS卡片具备了更加丰富的能力,但也增加了使用卡片进行恶意行为的风险。由于ArkTS卡片显示在使用方应用中,使用方应用一般为桌面应用,为确保桌面的使用体验以及功耗相关考虑,对ArkTS卡片的能力做了以下约束:
当导入模块时,仅支持导入标识“支持在ArkTS卡片中使用”的模块。
不支持导入共享包。
不支持使用native语言开发。
仅支持声明式范式的部分组件、事件、动效、数据管理、状态管理和API能力。
卡片的事件处理和使用方的事件处理是独立的,建议在使用方支持左右滑动的场景下卡片内容不要使用左右滑动功能的组件,以防手势冲突影响交互体验。
除此之外,当前ArkTS卡片还存在如下约束:
暂不支持极速预览。
暂不支持断点调试能力。
暂不支持Hot Reload热重载。
暂不支持setTimeOut。
3 ArkTS卡片相关模块
图1 ArkTS卡片相关模块
FormExtensionAbility:卡片扩展模块,提供卡片创建、销毁、刷新等生命周期回调。
FormExtensionContext:FormExtensionAbility的上下文环境,提供FormExtensionAbility具有的接口和能力。
formProvider:提供卡片提供方相关的接口能力,可通过该模块提供接口实现更新卡片、设置卡片更新时间、获取卡片信息、请求发布卡片等。
formInfo:提供了卡片信息和状态等相关类型和枚举。
formBindingData:提供卡片数据绑定的能力,包括FormBindingData对象的创建、相关信息的描述。
页面布局(WidgetCard.ets):基于ArkUI提供卡片UI开发能力。
- ArkTS卡片通用能力:提供了能在ArkTS卡片中使用的组件、属性和API。
- ArkTS卡片特有能力:postCardAction用于卡片内部和提供方应用间的交互,仅在卡片中可以调用。
卡片配置:包含FormExtensionAbility的配置和卡片的配置
- 在module.json5配置文件中的extensionAbilities标签下,配置FormExtensionAbility相关信息。
- 在resources/base/profile/目录下的form_config.json配置文件中,配置卡片(WidgetCard.ets)相关信息。
4 ArkTS卡片开发指导
4.1 创建一个ArkTS卡片
创建卡片当前有两种入口:
- 创建工程时,选择Application,可以在创建工程后右键新建卡片。
- 创建工程时,选择Atomic Service(元服务),也可以在创建工程后右键新建卡片。
在已有的应用工程中,可以通过右键新建ArkTS卡片,具体的操作方式如下。
-
右键新建卡片。
说明
在API 10及以上 Stage模型的工程中,在Service Widget菜单可直接选择创建动态或静态服务卡片。创建服务卡片后,也可以在卡片的form_config.json配置文件中,通过isDynamic参数修改卡片类型:isDynamic置空或赋值为"true",则该卡片为动态卡片;isDynamic赋值为"false",则该卡片为静态卡片。
-
根据实际业务场景,选择一个卡片模板。
-
在选择卡片的开发语言类型(Language)时,选择ArkTS选项,然后单击“Finish”,即可完成ArkTS卡片创建。
建议根据实际使用场景命名卡片名称,ArkTS卡片创建完成后,工程中会新增如下卡片相关文件:卡片生命周期管理文件(EntryFormAbility.ets)、卡片页面文件(WidgetCard.ets)和卡片配置文件(form_config.json)。
4.2 配置卡片的配置文件
卡片相关的配置文件主要包含FormExtensionAbility的配置和卡片的配置两部分。
-
卡片需要在module.json5配置文件中的extensionAbilities标签下,配置FormExtensionAbility相关信息。FormExtensionAbility需要填写metadata元信息标签,其中键名称为固定字符串“ohos.extension.form”,资源为卡片的具体配置信息的索引。
配置示例如下:
{
"module": {
// ...
"extensionAbilities": [
{
"name": "EntryFormAbility",
"srcEntry": "./ets/entryformability/EntryFormAbility.ets",
"label": "$string:EntryFormAbility_label",
"description": "$string:EntryFormAbility_desc",
"type": "form",
"metadata": [
{
"name": "ohos.extension.form",
"resource": "$profile:form_config"
}
]
}
]
}
}
2.
-
卡片的具体配置信息。在上述FormExtensionAbility的元信息(“metadata”配置项)中,可以指定卡片具体配置信息的资源索引。例如当resource指定为$profile:form_config时,会使用开发视图的resources/base/profile/目录下的form_config.json作为卡片profile配置文件。内部字段结构说明如下表所示。
表1 卡片form_config.json配置文件
属性名称 含义 数据类型 是否可缺省 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。 fontScaleFollowSystem 表示卡片使用方设置此卡片的字体是否支持跟随系统变化。
- true:支持跟随系统字体大小变化 。
- false:不支持跟随系统字体大小变化。
布尔类型 可缺省,缺省值为true。 supportShapes 表示卡片的显示形状,取值范围如下:
- rect:表示方形卡片。
- circle:表示圆形卡片。
字符串 可缺省,缺省值为“rect”。
isDynamic标签
此标签标识卡片是否为动态卡片(仅针对ArkTS卡片生效)。
卡片类型 | 支持的能力 | 适用场景 | 优缺点 |
---|---|---|---|
静态卡片 | 仅支持UI组件和布局能力。 | 主要用于展示静态信息(UI相对固定),仅可以通过FormLink组件跳转到指定的UIAbility。 | 功能简单但可以有效控制内存开销。 |
动态卡片 | 除了支持UI组件和布局能力,还支持通用事件能力和自定义动效能力。 | 用于有复杂业务逻辑和交互的场景。例如:卡片页面图片的刷新、卡片内容的刷新等。 | 功能丰富但内存开销较大。 |
window标签
此标签标识window对象的内部结构说明。
属性名称 | 含义 | 数据类型 | 是否可缺省 |
---|---|---|---|
designWidth | 标识页面设计基准宽度。以此为基准,根据实际设备宽度来缩放元素大小。 | 数值 | 可缺省,缺省值为720px。 |
autoDesignWidth | 标识页面设计基准宽度是否自动计算。当配置为true时,designWidth将会被忽略,设计基准宽度由设备宽度与屏幕密度计算得出。 | 布尔值 | 可缺省,缺省值为false。 |
配置示例如下:
{
"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",
"isDefault": true,
"updateEnabled": true,
"scheduledUpdateTime": "10:30",
"updateDuration": 1,
"defaultDimension": "2*2",
"supportDimensions": [
"2*2"
],
"formConfigAbility": "ability://EntryAbility",
"dataProxyEnabled": false,
"isDynamic": true,
"transparencyEnabled": false,
"metadata": []
}
]
}
4.3 卡片生命周期管理
创建ArkTS卡片,需实现FormExtensionAbility生命周期接口。
1. 在EntryFormAbility.ets中,导入相关模块。
import { formBindingData, FormExtensionAbility, formInfo, formProvider } from '@kit.FormKit';
import { Configuration, Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
2. 在EntryFormAbility.ets中,实现FormExtensionAbility生命周期接口,其中在onAddForm的入参want中可以通过FormParam取出卡片的相关信息。
const TAG: string = 'EntryFormAbility';
const DOMAIN_NUMBER: number = 0xFF00;
export default class EntryFormAbility extends FormExtensionAbility {
onAddForm(want: Want): formBindingData.FormBindingData {
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onAddForm');
hilog.info(DOMAIN_NUMBER, TAG, want.parameters?.[formInfo.FormParam.NAME_KEY] as string);
// ...
// 卡片使用方创建卡片时触发,提供方需要返回卡片数据绑定类
let obj: Record<string, string> = {
'title': 'titleOnAddForm',
'detail': 'detailOnAddForm'
};
let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj);
return formData;
}
onCastToNormalForm(formId: string): void {
// 卡片使用方将临时卡片转换为常态卡片触发,提供方需要做相应的处理。
// 1、临时卡、常态卡是卡片使用方的概念。
// 2、临时卡是短期存在的,在特定事件或用户行为后显示,完成后自动消失。
// 3、常态卡是持久存在的,在用户未进行清除或更改的情况下,会一直存在,平时开发的功能卡片属于常态卡。
// 4、目前手机上没有地方会使用临时卡。
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onCastToNormalForm');
}
onUpdateForm(formId: string): void {
// 若卡片支持定时更新/定点更新/卡片使用方主动请求更新功能,则提供方需要重写该方法以支持数据更新
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onUpdateForm');
let obj: Record<string, string> = {
'title': 'titleOnUpdateForm',
'detail': 'detailOnUpdateForm'
};
let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj);
formProvider.updateForm(formId, formData).catch((error: BusinessError) => {
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] updateForm, error:' + JSON.stringify(error));
});
}
onChangeFormVisibility(newStatus: Record<string, number>): void {
// 卡片使用方发起可见或者不可见通知触发,提供方需要做相应的处理,仅系统应用生效
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onChangeFormVisibility');
}
onFormEvent(formId: string, message: string): void {
// 若卡片支持触发事件,则需要重写该方法并实现对事件的触发
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onFormEvent');
// ...
}
onRemoveForm(formId: string): void {
// 删除卡片实例数据
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onRemoveForm');
// 删除之前持久化的卡片实例数据
// 此接口请根据实际情况实现,具体请参考:FormExtAbility Stage模型卡片实例
}
onConfigurationUpdate(config: Configuration) {
// 当前formExtensionAbility存活时更新系统配置信息时触发的回调。
// 需注意:formExtensionAbility创建后10秒内无操作将会被清理。
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onConfigurationUpdate:' + JSON.stringify(config));
}
onAcquireFormState(want: Want) {
// 卡片提供方接收查询卡片状态通知接口,默认返回卡片初始状态。
return formInfo.FormState.READY;
}
}
说明
FormExtensionAbility进程不能常驻后台,即在卡片生命周期回调函数中无法处理长时间的任务,在生命周期调度完成后会继续存在10秒,如10秒内没有新的生命周期回调触发则进程自动退出。针对可能需要10秒以上才能完成的业务逻辑,建议拉起主应用进行处理,处理完成后使用updateForm通知卡片进行刷新。
4.4 开发卡片页面
4.4.1 卡片页面能力说明
ArkTS卡片具备JS卡片的全量能力,并且新增了动效能力和自定义绘制的能力,支持声明式范式的部分组件、事件、动效、数据管理、状态管理能力。
对于支持在ArkTS卡片中使用的接口,会添加“卡片能力”的标记:从API version x开始,该接口支持在ArkTS卡片中使用。同时请留意卡片场景下的能力差异说明。
例如:以下说明表示CircleShape可在ArkTS卡片中使用。
4.4.2 卡片使用动效能力
ArkTS卡片开放了使用动画效果的能力,支持显式动画、属性动画、组件内转场能力。需要注意的是,ArkTS卡片使用动画效果时具有以下限制:
表1 动效参数限制
名称 | 参数说明 | 限制描述 |
---|---|---|
duration | 动画播放时长 | 限制最长的动效播放时长为1秒,当设置大于1秒的时间时,动效时长仍为1秒。 |
tempo | 动画播放速度 | 卡片中禁止设置此参数,使用默认值1。 |
delay | 动画延迟执行的时长 | 卡片中禁止设置此参数,使用默认值0。 |
iterations | 动画播放次数 | 卡片中禁止设置此参数,使用默认值1。 |
说明
静态卡片不支持使用动效能力。
以下示例代码实现了按钮旋转的动画效果:
@Entry
@Component
struct AnimationCard {
@State rotateAngle: number = 0;
build() {
Row() {
Button('change rotate angle')
.height('20%')
.width('90%')
.margin('5%')
.onClick(() => {
this.rotateAngle = (this.rotateAngle === 0 ? 90 : 0);
})
.rotate({ angle: this.rotateAngle })
.animation({
curve: Curve.EaseOut,
playMode: PlayMode.Normal,
})
}.height('100%').alignItems(VerticalAlign.Center)
}
}
4.4.3 卡片使用自定义绘制能力
ArkTS卡片开放了自定义绘制的能力,在卡片上可以通过Canvas组件创建一块画布,然后通过CanvasRenderingContext2D对象在画布上进行自定义图形的绘制,如下示例代码实现了在画布的中心绘制了一个笑脸。
@Entry
@Component
struct CustomCanvasDrawingCard {
private canvasWidth: number = 0;
private canvasHeight: number = 0;
// 初始化CanvasRenderingContext2D和RenderingContextSettings
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
build() {
Column() {
Row() {
Canvas(this.context)
.width('100%')
.height('100%')
.onReady(() => {
// 在onReady回调中获取画布的实际宽和高
this.canvasWidth = this.context.width;
this.canvasHeight = this.context.height;
// 绘制画布的背景
this.context.fillStyle = '#EEF0FF';
this.context.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
// 在画布的中心绘制一个圆
this.context.beginPath();
let radius = this.context.width / 3;
let circleX = this.context.width / 2;
let circleY = this.context.height / 2;
this.context.moveTo(circleX - radius, circleY);
this.context.arc(circleX, circleY, radius, 2 * Math.PI, 0, true);
this.context.closePath();
this.context.fillStyle = '#5A5FFF';
this.context.fill();
// 绘制笑脸的左眼
let leftR = radius / 13;
let leftX = circleX - (radius / 2.3);
let leftY = circleY - (radius / 4.5);
this.context.beginPath();
this.context.arc(leftX, leftY, leftR, 0, 2 * Math.PI, true);
this.context.closePath();
this.context.strokeStyle = '#FFFFFF';
this.context.lineWidth = 15;
this.context.stroke();
// 绘制笑脸的右眼
let rightR = radius / 13;
let rightX = circleX + (radius / 2.3);
let rightY = circleY - (radius / 4.5);
this.context.beginPath();
this.context.arc(rightX, rightY, rightR, 0, 2 * Math.PI, true);
this.context.closePath();
this.context.strokeStyle = '#FFFFFF';
this.context.lineWidth = 15;
this.context.stroke();
// 绘制笑脸的鼻子
let startX = circleX;
let startY = circleY - 20;
this.context.beginPath();
this.context.moveTo(startX, startY);
this.context.lineTo(startX - 8, startY + 40);
this.context.lineTo(startX + 8, startY + 40);
this.context.strokeStyle = '#FFFFFF';
this.context.lineWidth = 15;
this.context.lineCap = 'round';
this.context.lineJoin = 'round';
this.context.stroke();
// 绘制笑脸的嘴巴
let mouthR = radius / 2;
let mouthX = circleX;
let mouthY = circleY + 10;
this.context.beginPath();
this.context.arc(mouthX, mouthY, mouthR, Math.PI / 1.4, Math.PI / 3.4, true);
this.context.strokeStyle = '#FFFFFF';
this.context.lineWidth = 15;
this.context.stroke();
this.context.closePath();
})
}
}.height('100%').width('100%')
}
}
运行效果如下图所示。
4.5 开发卡片事件
4.5.1 卡片事件能力说明
动态卡片事件能力说明
动态卡片事件的主要使用场景如下:
- router事件:可以使用router事件跳转到指定UIAbility,并通过router事件刷新卡片内容。
- call事件:可以使用call事件拉起指定UIAbility到后台,并通过call事件刷新卡片内容。
- message事件:可以使用message拉起FormExtensionAbility,并通过FormExtensionAbility刷新卡片内容。
静态卡片事件能力说明
请参见FormLink
4.5.2 拉起卡片提供方的UIAbility(router事件)
在动态卡片中使用postCardAction接口的router能力,能够快速拉起动态卡片提供方应用的指定UIAbility(页面),因此UIAbility较多的应用往往会通过卡片提供不同的跳转按钮,实现一键直达的效果。例如相机卡片,卡片上提供拍照、录像等按钮,点击不同按钮将拉起相机应用的不同UIAbility,从而提高用户的体验。
开发步骤
-
创建动态卡片
在工程的 entry 模块中,新建名为WidgetEventRouterCard的ArkTs卡片。
-
构建ArkTs卡片页面代码布局
卡片页面布局中有两个按钮,点击其中一个按钮时调用postCardAction向指定UIAbility发送router事件,并在事件内定义需要传递的内容。
//src/main/ets/widgeteventroutercard/pages/WidgetEventRouterCard.ets
@Entry
@Component
struct WidgetEventRouterCard {
build() {
Column() {
Text($r('app.string.JumpLabel'))
.fontColor('#FFFFFF')
.opacity(0.9)
.fontSize(14)
.margin({ top: '8%', left: '10%' })
Row() {
Column() {
Button() {
Text($r('app.string.ButtonA_label'))
.fontColor('#45A6F4')
.fontSize(12)
}
.width(120)
.height(32)
.margin({ top: '20%' })
.backgroundColor('#FFFFFF')
.borderRadius(16)
.onClick(() => {
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility',
params: { targetPage: 'funA' }
});
})
Button() {
Text($r('app.string.ButtonB_label'))
.fontColor('#45A6F4')
.fontSize(12)
}
.width(120)
.height(32)
.margin({ top: '8%', bottom: '15vp' })
.backgroundColor('#FFFFFF')
.borderRadius(16)
.onClick(() => {
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility',
params: { targetPage: 'funB' }
});
})
}
}.width('100%').height('80%')
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Start)
.backgroundImage($r('app.media.CardEvent'))
.backgroundImageSize(ImageSize.Cover)
}
}
3. 处理router事件
在UIAbility中接收router事件并获取参数,根据传递的params不同,选择拉起不同的页面。
//src/main/ets/entryability/EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG: string = 'EntryAbility';
const DOMAIN_NUMBER: number = 0xFF00;
export default class EntryAbility extends UIAbility {
private selectPage: string = '';
private currentWindowStage: window.WindowStage | null = null;
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 获取router事件中传递的targetPage参数
hilog.info(DOMAIN_NUMBER, TAG, `Ability onCreate: ${JSON.stringify(want?.parameters)}`);
if (want?.parameters?.params) {
// want.parameters.params 对应 postCardAction() 中 params 内容
let params: Record<string, Object> = JSON.parse(want.parameters.params as string);
this.selectPage = params.targetPage as string;
hilog.info(DOMAIN_NUMBER, TAG, `onCreate selectPage: ${this.selectPage}`);
}
}
// 如果UIAbility已在后台运行,在收到Router事件后会触发onNewWant生命周期回调
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(DOMAIN_NUMBER, TAG, `Ability onNewWant: ${JSON.stringify(want?.parameters)}`);
if (want?.parameters?.params) {
// want.parameters.params 对应 postCardAction() 中 params 内容
let params: Record<string, Object> = JSON.parse(want.parameters.params as string);
this.selectPage = params.targetPage as string;
hilog.info(DOMAIN_NUMBER, TAG, `onNewWant selectPage: ${this.selectPage}`);
}
if (this.currentWindowStage !== null) {
this.onWindowStageCreate(this.currentWindowStage);
}
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
let targetPage: string;
// 根据传递的targetPage不同,选择拉起不同的页面
switch (this.selectPage) {
case 'funA':
targetPage = 'pages/FunA'; //与实际的UIAbility页面路径保持一致
break;
case 'funB':
targetPage = 'pages/FunB'; //与实际的UIAbility页面路径保持一致
break;
default:
targetPage = 'pages/Index'; //与实际的UIAbility页面路径保持一致
}
if (this.currentWindowStage === null) {
this.currentWindowStage = windowStage;
}
windowStage.loadContent(targetPage, (err, data) => {
if (err.code) {
hilog.error(DOMAIN_NUMBER, TAG, 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
});
}
}
4. 创建跳转后的UIAbility页面
在pages文件夹下新建FunA.ets和FunB.ets,构建页面布局。
//src/main/ets/pages/FunA.ets
@Entry
@Component
struct FunA {
@State message: string = 'Hello World';
build() {
RelativeContainer() {
Text(this.message)
.id('HelloWorld')
.fontSize(50)
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
}
.height('100%')
.width('100%')
}
}
5. 注册UIAbility页面
打开main_pages.json,将新建的FunA.ets和FunB.ets正确注册在src数组中。
//src/main/resources/base/profile/main_pages.json
{
"src": [
"pages/Index",
"pages/FunA",
"pages/FunB"
]
}
4.5.3 拉起卡片提供方的UIAbility到后台(call事件)
开发步骤
-
创建动态卡片
新建一个名为WidgetEventCallCardArkTs动态卡片。
-
页面布局代码实现
在卡片页面中布局两个按钮,点击其中一个按钮时调用postCardAction向指定UIAbility发送call事件,并在事件内定义需要调用的方法和传递的数据。需要注意的是,method参数为必选参数,且类型需要为string类型,用于触发UIAbility中对应的方法。
//src/main/ets/widgeteventcallcard/pages/WidgetEventCallCardCard.ets
@Entry
@Component
struct WidgetEventCallCard {
@LocalStorageProp('formId') formId: string = '12400633174999288';
build() {
Column() {
//...
Row() {
Column() {
Button() {
//...
}
//...
.onClick(() => {
postCardAction(this, {
action: 'call',
abilityName: 'WidgetEventCallEntryAbility', // 只能跳转到当前应用下的UIAbility,与module.json5中定义保持
params: {
formId: this.formId,
method: 'funA' // 在EntryAbility中调用的方法名
}
});
})
Button() {
//...
}
//...
.onClick(() => {
postCardAction(this, {
action: 'call',
abilityName: 'WidgetEventCallEntryAbility', // 只能跳转到当前应用下的UIAbility,与module.json5中定义保持
params: {
formId: this.formId,
method: 'funB', // 在EntryAbility中调用的方法名
num: 1 // 需要传递的其他参数
}
});
})
}
}.width('100%').height('80%')
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Center)
}
}
3. 创建指定的UIAbility
在UIAbility中接收call事件并获取参数,根据传递的method不同,执行不同的方法。其余数据可以通过readString方法获取。需要注意的是,UIAbility需要onCreate生命周期中监听所需的方法。
//src/main/ets/widgeteventcallcard/WidgetEventCallEntryAbility/WidgetEventCallEntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { rpc } from '@kit.IPCKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG: string = 'WidgetEventCallEntryAbility';
const DOMAIN_NUMBER: number = 0xFF00;
const CONST_NUMBER_1: number = 1;
const CONST_NUMBER_2: number = 2;
class MyParcelable implements rpc.Parcelable {
num: number;
str: string;
constructor(num: number, str: string) {
this.num = num;
this.str = str;
}
marshalling(messageSequence: rpc.MessageSequence): boolean {
messageSequence.writeInt(this.num);
messageSequence.writeString(this.str);
return true;
}
unmarshalling(messageSequence: rpc.MessageSequence): boolean {
this.num = messageSequence.readInt();
this.str = messageSequence.readString();
return true;
}
}
export default class WidgetEventCallEntryAbility extends UIAbility {
// 如果UIAbility第一次启动,在收到call事件后会触发onCreate生命周期回调
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
try {
// 监听call事件所需的方法
this.callee.on('funA', (data: rpc.MessageSequence) => {
// 获取call事件中传递的所有参数
hilog.info(DOMAIN_NUMBER, TAG, `FunACall param: ${JSON.stringify(data.readString())}`);
promptAction.showToast({
message: 'FunACall param:' + JSON.stringify(data.readString())
});
return new MyParcelable(CONST_NUMBER_1, 'aaa');
});
this.callee.on('funB', (data: rpc.MessageSequence) => {
// 获取call事件中传递的所有参数
hilog.info(DOMAIN_NUMBER, TAG, `FunBCall param: ${JSON.stringify(data.readString())}`);
promptAction.showToast({
message: 'FunBCall param:' + JSON.stringify(data.readString())
});
return new MyParcelable(CONST_NUMBER_2, 'bbb');
});
} catch (err) {
hilog.error(DOMAIN_NUMBER, TAG, `Failed to register callee on. Cause: ${JSON.stringify(err as BusinessError)}`);
}
}
// 进程退出时,解除监听
onDestroy(): void | Promise<void> {
try {
this.callee.off('funA');
this.callee.off('funB');
} catch (err) {
hilog.error(DOMAIN_NUMBER, TAG, `Failed to register callee off. Cause: ${JSON.stringify(err as BusinessError)}`);
}
}
}
4. 配置后台运行权限
call事件含有约束限制:提供方应用需要在module.json5顶层对象module下添加后台运行权限(ohos.permission.KEEP_BACKGROUND_RUNNING)。
//src/main/module.json5
"requestPermissions":[
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
}
]
5. 配置指定的UIAbility
在module.json5顶层对象module的abilities数组内添加WidgetEventCallEntryAbility对应的配置信息。
//src/main/module.json5
"abilities": [
{
"name": 'WidgetEventCallEntryAbility',
"srcEntry": './ets/widgeteventcallcard/WidgetEventCallEntryAbility/WidgetEventCallEntryAbility.ets',
"description": '$string:WidgetEventCallCard_desc',
"icon": "$media:app_icon",
"label": "$string:WidgetEventCallCard_label",
"startWindowIcon": "$media:app_icon",
"startWindowBackground": "$color:start_window_background"
}
]
4.5.4 通过message事件刷新卡片内容
在卡片页面中可以通过postCardAction接口触发message事件拉起FormExtensionAbility,然后由FormExtensionAbility刷新卡片内容。
- 在卡片页面通过注册Button的onClick点击事件回调,并在回调中调用postCardAction接口触发message事件拉起FormExtensionAbility。卡片页面中使用LocalStorageProp装饰需要刷新的卡片数据。
let storageUpdateByMsg = new LocalStorage();
@Entry(storageUpdateByMsg)
@Component
struct UpdateByMessageCard {
@LocalStorageProp('title') title: ResourceStr = $r('app.string.default_title');
@LocalStorageProp('detail') detail: ResourceStr = $r('app.string.DescriptionDefault');
build() {
Column() {
Column() {
Text(this.title)
.fontColor('#FFFFFF')
.opacity(0.9)
.fontSize(14)
.margin({ top: '8%', left: '10%' })
Text(this.detail)
.fontColor('#FFFFFF')
.opacity(0.6)
.fontSize(12)
.margin({ top: '5%', left: '10%' })
}.width('100%').height('50%')
.alignItems(HorizontalAlign.Start)
Row() {
Button() {
Text($r('app.string.update'))
.fontColor('#45A6F4')
.fontSize(12)
}
.width(120)
.height(32)
.margin({ top: '30%', bottom: '10%' })
.backgroundColor('#FFFFFF')
.borderRadius(16)
.onClick(() => {
postCardAction(this, {
action: 'message',
params: { msgTest: 'messageEvent' }
});
})
}.width('100%').height('40%')
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Start)
.backgroundImage($r('app.media.CardEvent'))
.backgroundImageSize(ImageSize.Cover)
}
}
- 在FormExtensionAbility的onFormEvent生命周期中调用updateForm接口刷新卡片。
import { BusinessError } from '@kit.BasicServicesKit';
import { formBindingData, FormExtensionAbility, formProvider } from '@kit.FormKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG: string = 'EntryFormAbility';
const DOMAIN_NUMBER: number = 0xFF00;
export default class EntryFormAbility extends FormExtensionAbility {
onFormEvent(formId: string, message: string): void {
// Called when a specified message event defined by the form provider is triggered.
hilog.info(DOMAIN_NUMBER, TAG, `FormAbility onFormEvent, formId = ${formId}, message: ${JSON.stringify(message)}`);
class FormDataClass {
title: string = 'Title Update.'; // 和卡片布局中对应
detail: string = 'Description update success.'; // 和卡片布局中对应
}
let formData = new FormDataClass();
let formInfo: formBindingData.FormBindingData = formBindingData.createFormBindingData(formData);
formProvider.updateForm(formId, formInfo).then(() => {
hilog.info(DOMAIN_NUMBER, TAG, 'FormAbility updateForm success.');
}).catch((error: BusinessError) => {
hilog.info(DOMAIN_NUMBER, TAG, `Operation updateForm failed. Cause: ${JSON.stringify(error)}`);
})
}
//...
}
运行效果如下图所示。
初始状态 | 点击刷新 |
---|---|
| |
4.5.5 通过router或call事件刷新卡片内容
使用router事件,点击卡片可拉起对应应用的UIAbility至前台,并刷新卡片。使用call事件,点击卡片可拉起对应应用的UIAbility至后台,并刷新卡片。在卡片页面中可以通过postCardAction接口触发router事件或者call事件拉起UIAbility,然后由UIAbility刷新卡片内容。
通过router事件刷新卡片内容
- 在卡片页面代码文件中,通过注册Button的onClick点击事件回调并在回调中调用postCardAction接口,触发router事件拉起UIAbility至前台。
let storageUpdateRouter = new LocalStorage();
@Entry(storageUpdateRouter)
@Component
struct WidgetUpdateRouterCard {
@LocalStorageProp('routerDetail') routerDetail: ResourceStr = $r('app.string.init');
build() {
Column() {
Column() {
Text(this.routerDetail)
.fontColor('#FFFFFF')
.opacity(0.9)
.fontSize(14)
.margin({ top: '8%', left: '10%', right: '10%' })
.textOverflow({ overflow: TextOverflow.Ellipsis })
.maxLines(2)
}.width('100%').height('50%')
.alignItems(HorizontalAlign.Start)
Row() {
Button() {
Text($r('app.string.JumpLabel'))
.fontColor('#45A6F4')
.fontSize(12)
}
.width(120)
.height(32)
.margin({ top: '30%', bottom: '10%' })
.backgroundColor('#FFFFFF')
.borderRadius(16)
.onClick(() => {
postCardAction(this, {
action: 'router',
abilityName: 'WidgetEventRouterEntryAbility', // 只能跳转到当前应用下的UIAbility
params: {
routerDetail: 'RouterFromCard',
}
});
})
}.width('100%').height('40%')
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Start)
.backgroundImage($r('app.media.CardEvent'))
.backgroundImageSize(ImageSize.Cover)
}
}
- 在UIAbility的onCreate或者onNewWant生命周期中可以通过入参want获取卡片的formID和传递过来的参数信息,然后调用updateForm接口刷新卡片。
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { formBindingData, formInfo, formProvider } from '@kit.FormKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG: string = 'WidgetEventRouterEntryAbility';
const DOMAIN_NUMBER: number = 0xFF00;
export default class WidgetEventRouterEntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.handleFormRouterEvent(want, 'onCreate');
}
handleFormRouterEvent(want: Want, source: string): void {
hilog.info(DOMAIN_NUMBER, TAG, `handleFormRouterEvent ${source}, Want: ${JSON.stringify(want)}`);
if (want.parameters && want.parameters[formInfo.FormParam.IDENTITY_KEY] !== undefined) {
let curFormId = want.parameters[formInfo.FormParam.IDENTITY_KEY].toString();
// want.parameters.params 对应 postCardAction() 中 params 内容
let message: string = (JSON.parse(want.parameters?.params as string))?.routerDetail;
hilog.info(DOMAIN_NUMBER, TAG, `UpdateForm formId: ${curFormId}, message: ${message}`);
let formData: Record<string, string> = {
'routerDetail': message + ' ' + source + ' UIAbility', // 和卡片布局中对应
};
let formMsg = formBindingData.createFormBindingData(formData);
formProvider.updateForm(curFormId, formMsg).then((data) => {
hilog.info(DOMAIN_NUMBER, TAG, 'updateForm success.', JSON.stringify(data));
}).catch((error: BusinessError) => {
hilog.info(DOMAIN_NUMBER, TAG, 'updateForm failed.', JSON.stringify(error));
});
}
}
// 如果UIAbility已在后台运行,在收到Router事件后会触发onNewWant生命周期回调
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(DOMAIN_NUMBER, TAG, 'onNewWant Want:', JSON.stringify(want));
this.handleFormRouterEvent(want, 'onNewWant');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(DOMAIN_NUMBER, TAG, '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err, data) => {
if (err.code) {
hilog.error(DOMAIN_NUMBER, TAG, 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
});
}
// ...
}
通过call事件刷新卡片内容
- 在卡片页面代码文件中,通过注册Button的onClick点击事件回调并在回调中调用postCardAction接口,触发call事件拉起UIAbility至后台。
let storageUpdateCall = new LocalStorage();
@Entry(storageUpdateCall)
@Component
struct WidgetUpdateCallCard {
@LocalStorageProp('formId') formId: string = '12400633174999288';
@LocalStorageProp('calleeDetail') calleeDetail: ResourceStr = $r('app.string.init');
build() {
Column() {
Column() {
Text(this.calleeDetail)
.fontColor('#FFFFFF')
.opacity(0.9)
.fontSize(14)
.margin({ top: '8%', left: '10%' })
}.width('100%').height('50%')
.alignItems(HorizontalAlign.Start)
Row() {
Button() {
Text($r('app.string.CalleeJumpLabel'))
.fontColor('#45A6F4')
.fontSize(12)
}
.width(120)
.height(32)
.margin({ top: '30%', bottom: '10%' })
.backgroundColor('#FFFFFF')
.borderRadius(16)
.onClick(() => {
postCardAction(this, {
action: 'call',
abilityName: 'WidgetCalleeEntryAbility', // 只能拉起当前应用下的UIAbility
params: {
method: 'funA',
formId: this.formId,
calleeDetail: 'CallFrom'
}
});
})
}.width('100%').height('40%')
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Start)
.backgroundImage($r('app.media.CardEvent'))
.backgroundImageSize(ImageSize.Cover)
}
}
- 在UIAbility的onCreate生命周期中监听call事件所需的方法,然后在对应方法中调用updateForm接口刷新卡片。
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { formBindingData, formProvider } from '@kit.FormKit';
import { rpc } from '@kit.IPCKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG: string = 'WidgetCalleeEntryAbility';
const DOMAIN_NUMBER: number = 0xFF00;
const MSG_SEND_METHOD: string = 'funA';
const CONST_NUMBER_1: number = 1;
class MyParcelable implements rpc.Parcelable {
num: number;
str: string;
constructor(num: number, str: string) {
this.num = num;
this.str = str;
};
marshalling(messageSequence: rpc.MessageSequence): boolean {
messageSequence.writeInt(this.num);
messageSequence.writeString(this.str);
return true;
};
unmarshalling(messageSequence: rpc.MessageSequence): boolean {
this.num = messageSequence.readInt();
this.str = messageSequence.readString();
return true;
};
}
// 在收到call事件后会触发callee监听的方法
let funACall = (data: rpc.MessageSequence): MyParcelable => {
// 获取call事件中传递的所有参数
let params: Record<string, string> = JSON.parse(data.readString());
if (params.formId !== undefined) {
let curFormId: string = params.formId;
let message: string = params.calleeDetail;
hilog.info(DOMAIN_NUMBER, TAG, `UpdateForm formId: ${curFormId}, message: ${message}`);
let formData: Record<string, string> = {
'calleeDetail': message
};
let formMsg: formBindingData.FormBindingData = formBindingData.createFormBindingData(formData);
formProvider.updateForm(curFormId, formMsg).then((data) => {
hilog.info(DOMAIN_NUMBER, TAG, `updateForm success. ${JSON.stringify(data)}`);
}).catch((error: BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG, `updateForm failed: ${JSON.stringify(error)}`);
});
}
return new MyParcelable(CONST_NUMBER_1, 'aaa');
};
export default class WidgetCalleeEntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
try {
// 监听call事件所需的方法
this.callee.on(MSG_SEND_METHOD, funACall);
} catch (error) {
hilog.error(DOMAIN_NUMBER, TAG, `${MSG_SEND_METHOD} register failed with error ${JSON.stringify(error)}`);
}
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(DOMAIN_NUMBER, TAG, '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err, data) => {
if (err.code) {
hilog.error(DOMAIN_NUMBER, TAG, 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
});
}
}
要拉起UIAbility至后台,需要在module.json5配置文件中,配置ohos.permission.KEEP_BACKGROUND_RUNNING权限。
"requestPermissions":[
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
}
]
4.6 卡片数据交互
4.6.1 卡片内容更新
ArkTS卡片框架为提供方提供了updateForm接口、为使用方提供了requestForm接口来实现主动触发卡片的页面刷新能力;另外卡片框架还会通过开发者声明的定时信息按需通知提供方进行卡片刷新。
卡片UI代码内通过LocalStorageProp可以获得提供方推送的需要刷新的卡片数据。
接口 | 是否系统能力 | 约束 |
---|---|---|
updateForm | 否 | 1. 提供方调用。 2. 提供方仅允许刷新自己的卡片,其他提供方的卡片无法刷新。 |
requestForm | 是 | 1. 使用方调用。 2. 仅允许刷新添加到当前使用方的卡片,添加到其他使用方的卡片无法刷新。 |
1. 提供方主动刷新卡片流程示意:
卡片提供方应用运行过程中,如果识别到有要更新卡片数据的诉求,可以主动通过formProvider提供的updateForm接口更新卡片。
2. 使用方主动请求更新卡片流程示意:
卡片使用方在运行过程中,如果检测到系统语言、深浅色有变化时,可以主动通过formHost提供的requestForm接口请求更新卡片,卡片管理服务会进而通知提供方完成卡片更新。
3. 卡片框架通知提供方定时更新卡片流程示意:
根据卡片提供方开发者提前配置声明的定时刷新信息,卡片管理服务会根据定时信息、卡片可见状态、刷新次数等因素综合判断是否需要通知提供方更新卡片。
4.6.2 卡片定时刷新
当前卡片框架提供了如下几种按时间刷新卡片的方式:
-
定时刷新:表示在一定时间间隔内调用onUpdateForm的生命周期回调函数自动刷新卡片内容。可以在form_config.json配置文件的updateDuration字段中进行设置。例如,可以将updateDuration字段的值设置为2,表示刷新时间设置为每小时一次。
{
"forms": [
{
"name": "UpdateDuration",
"description": "$string:widget_updateduration_desc",
"src": "./ets/updateduration/pages/UpdateDurationCard.ets",
"uiSyntax": "arkts",
"window": {
"designWidth": 720,
"autoDesignWidth": true
},
"colorMode": "auto",
"isDefault": true,
"updateEnabled": true,
"scheduledUpdateTime": "10:30",
"updateDuration": 2,
"defaultDimension": "2*2",
"supportDimensions": [
"2*2"
]
}
]
}
说明
在使用定时刷新时,需要在form_config.json配置文件中设置updateEnabled字段为true,以启用周期性刷新功能。
为减少卡片被动周期刷新进程启动次数,降低卡片刷新功耗,应用市场在安装应用时可以为该应用配置刷新周期,
也可以为已经安装的应用动态配置刷新周期,用来限制卡片刷新周期的时长,以达到降低周期刷新进程启动次数的目的。
● 当配置了updateDuration(定时刷新)后,若应用市场动态配置了该应用的刷新周期,
卡片框架会将form_config.json文件中配置的刷新周期与应用市场配置的刷新周期进行比较,取较长的刷新周期做为该卡片的定时刷新周期。
● 若应用市场未动态配置该应用的刷新周期,则以form_config.json文件中配置的刷新周期为准。
● 若该卡片取消定时刷新功能,该规则将无效。
● 卡片定时刷新的更新周期单位为30分钟。应用市场配置的刷新周期范围是1~336,即最短为半小时(1 * 30min)刷新一次,最长为一周(336 * 30min)刷新一次。
● 该规则从API11开始生效。若小于API11,则以form_config.json文件中配置的刷新周期为准。
- 下次刷新:表示指定卡片的下一次刷新时间。可以通过调用setFormNextRefreshTime接口来实现。最短刷新时间为5分钟。例如,可以在接口调用后的5分钟内刷新卡片内容。
import { FormExtensionAbility, formProvider } from '@kit.FormKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
const TAG: string = 'UpdateByTimeFormAbility';
const FIVE_MINUTE: number = 5;
const DOMAIN_NUMBER: number = 0xFF00;
export default class UpdateByTimeFormAbility extends FormExtensionAbility {
onFormEvent(formId: string, message: string): void {
// Called when a specified message event defined by the form provider is triggered.
hilog.info(DOMAIN_NUMBER, TAG, `FormAbility onFormEvent, formId = ${formId}, message: ${JSON.stringify(message)}`);
try {
// 设置过5分钟后更新卡片内容
formProvider.setFormNextRefreshTime(formId, FIVE_MINUTE, (err: BusinessError) => {
if (err) {
hilog.info(DOMAIN_NUMBER, TAG, `Failed to setFormNextRefreshTime. Code: ${err.code}, message: ${err.message}`);
return;
} else {
hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in setFormNextRefreshTiming.');
}
});
} catch (err) {
hilog.info(DOMAIN_NUMBER, TAG, `Failed to setFormNextRefreshTime. Code: ${(err as BusinessError).code}, message: ${(err as BusinessError).message}`);
}
}
// ...
}
约束限制:
- 定时刷新有配额限制,每张卡片每天最多通过定时方式触发刷新50次,定时刷新次数包含卡片配置项updateDuration和调用setFormNextRefreshTime方法两种方式,当达到50次配额后,无法通过定时方式再次触发刷新,刷新次数会在每天的0点重置。
- 当前定时刷新使用同一个计时器进行计时,因此卡片定时刷新的第一次刷新会有最多30分钟的偏差。比如第一张卡片A(每隔半小时刷新一次)在3点20分添加成功,定时器启动并每隔半小时触发一次事件,第二张卡片B(每隔半小时刷新一次)在3点40分添加成功,在3点50分定时器事件触发时,卡片A触发定时刷新,卡片B会在下次事件(4点20分)中才会触发。
- 定时刷新在卡片可见情况下才会触发,在卡片不可见时仅会记录刷新动作和刷新数据,待可见时统一刷新布局。
- 如果使能了卡片代理刷新,定时刷新和下次刷新不生效。
4.6.3 卡片定点刷新
- 定点刷新:表示在每天的某个特定时间点自动刷新卡片内容。可以在form_config.json配置文件中的scheduledUpdateTime字段中进行设置。例如,可以将刷新时间设置为每天的上午10点30分。
{
"forms": [
{
"name": "ScheduledUpdateTime",
"description": "$string:widget_scheupdatetime_desc",
"src": "./ets/scheduledupdatetime/pages/ScheduledUpdateTimeCard.ets",
"uiSyntax": "arkts",
"window": {
"designWidth": 720,
"autoDesignWidth": true
},
"colorMode": "auto",
"isDefault": true,
"updateEnabled": true,
"scheduledUpdateTime": "10:30",
"updateDuration": 0,
"defaultDimension": "2*2",
"supportDimensions": [
"2*2"
]
}
]
}
约束限制:
- 定点刷新在卡片可见情况下才会触发,在卡片不可见时仅会记录刷新动作和刷新数据,待可见时统一刷新布局。
4.6.4 刷新本地图片和网络图片
在卡片上通常需要展示本地图片或从网络上下载的图片,获取本地图片和网络图片需要通过FormExtensionAbility来实现,如下示例代码介绍了如何在卡片上显示本地图片和网络图片。
-
下载网络图片需要使用到网络能力,需要申请ohos.permission.INTERNET权限,配置方式请参见声明权限。
-
在EntryFormAbility中的onAddForm生命周期回调中实现本地文件的刷新。
import { Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo } from '@kit.CoreFileKit';
import { formBindingData, FormExtensionAbility } from '@kit.FormKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG: string = 'WgtImgUpdateEntryFormAbility';
const DOMAIN_NUMBER: number = 0xFF00;
export default class WgtImgUpdateEntryFormAbility extends FormExtensionAbility {
// 在添加卡片时,打开一个本地图片并将图片内容传递给卡片页面显示
onAddForm(want: Want): formBindingData.FormBindingData {
// 假设在当前卡片应用的tmp目录下有一个本地图片:head.PNG
let tempDir = this.context.getApplicationContext().tempDir;
hilog.info(DOMAIN_NUMBER, TAG, `tempDir: ${tempDir}`);
let imgMap: Record<string, number> = {};
try {
// 打开本地图片并获取其打开后的fd
let file = fileIo.openSync(tempDir + '/' + 'head.PNG');
imgMap['imgBear'] = file.fd;
} catch (e) {
hilog.error(DOMAIN_NUMBER, TAG, `openSync failed: ${JSON.stringify(e as BusinessError)}`);
}
class FormDataClass {
text: string = 'Image: Bear';
loaded: boolean = true;
// 卡片需要显示图片场景, 必须和下列字段formImages 中的key 'imgBear' 相同。
imgName: string = 'imgBear';
// 卡片需要显示图片场景, 必填字段(formImages 不可缺省或改名), 'imgBear' 对应 fd
formImages: Record<string, number> = imgMap;
}
let formData = new FormDataClass();
// 将fd封装在formData中并返回至卡片页面
return formBindingData.createFormBindingData(formData);
}
//...
}
3. 在EntryFormAbility中的onFormEvent生命周期回调中实现网络文件的刷新。
import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo } from '@kit.CoreFileKit';
import { formBindingData, FormExtensionAbility, formProvider } from '@kit.FormKit';
import { http } from '@kit.NetworkKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG: string = 'WgtImgUpdateEntryFormAbility';
const DOMAIN_NUMBER: number = 0xFF00;
export default class WgtImgUpdateEntryFormAbility extends FormExtensionAbility {
async onFormEvent(formId: string, message: string): Promise<void> {
let param: Record<string, string> = {
'text': '刷新中...'
};
let formInfo: formBindingData.FormBindingData = formBindingData.createFormBindingData(param);
formProvider.updateForm(formId, formInfo);
// 注意:FormExtensionAbility在触发生命周期回调时被拉起,仅能在后台存在5秒
// 建议下载能快速下载完成的小文件,如在5秒内未下载完成,则此次网络图片无法刷新至卡片页面上
let netFile = 'https://cn-assets.gitee.com/assets/mini_app-e5eee5a21c552b69ae6bf2cf87406b59.jpg'; // 需要在此处使用真实的网络图片下载链接
let tempDir = this.context.getApplicationContext().tempDir;
let fileName = 'file' + Date.now();
let tmpFile = tempDir + '/' + fileName;
let imgMap: Record<string, number> = {};
class FormDataClass {
text: string = 'Image: Bear' + fileName;
loaded: boolean = true;
// 卡片需要显示图片场景, 必须和下列字段formImages 中的key fileName 相同。
imgName: string = fileName;
// 卡片需要显示图片场景, 必填字段(formImages 不可缺省或改名), fileName 对应 fd
formImages: Record<string, number> = imgMap;
}
let httpRequest = http.createHttp()
let data = await httpRequest.request(netFile);
if (data?.responseCode == http.ResponseCode.OK) {
try {
let imgFile = fileIo.openSync(tmpFile, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
imgMap[fileName] = imgFile.fd;
try{
let writeLen: number = await fileIo.write(imgFile.fd, data.result as ArrayBuffer);
hilog.info(DOMAIN_NUMBER, TAG, "write data to file succeed and size is:" + writeLen);
hilog.info(DOMAIN_NUMBER, TAG, 'ArkTSCard download complete: %{public}s', tmpFile);
try {
let formData = new FormDataClass();
let formInfo = formBindingData.createFormBindingData(formData);
await formProvider.updateForm(formId, formInfo);
hilog.info(DOMAIN_NUMBER, TAG, '%{public}s', 'FormAbility updateForm success.');
} catch (error) {
hilog.error(DOMAIN_NUMBER, TAG, `FormAbility updateForm failed: ${JSON.stringify(error)}`);
}
} catch (err) {
hilog.error(DOMAIN_NUMBER, TAG, "write data to file failed with error message: " + err.message + ", error code: " + err.code);
} finally {
// 在fileIo.closeSync执行之前,确保formProvider.updateForm已执行完毕。
fileIo.closeSync(imgFile);
};
} catch (e) {
hilog.error(DOMAIN_NUMBER, TAG, `openSync failed: ${JSON.stringify(e as BusinessError)}`);
}
} else {
hilog.error(DOMAIN_NUMBER, TAG, `ArkTSCard download task failed`);
let param: Record<string, string> = {
'text': '刷新失败'
};
let formInfo: formBindingData.FormBindingData = formBindingData.createFormBindingData(param);
formProvider.updateForm(formId, formInfo);
}
httpRequest.destroy();
}
}
4. 在卡片页面通过backgroundImage属性展示EntryFormAbility传递过来的卡片内容。
let storageWidgetImageUpdate = new LocalStorage();
@Entry(storageWidgetImageUpdate)
@Component
struct WidgetImageUpdateCard {
@LocalStorageProp('text') text: ResourceStr = $r('app.string.loading');
@LocalStorageProp('loaded') loaded: boolean = false;
@LocalStorageProp('imgName') imgName: ResourceStr = $r('app.string.imgName');
build() {
Column() {
Column() {
Text(this.text)
.fontColor('#FFFFFF')
.opacity(0.9)
.fontSize(12)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.maxLines(1)
.margin({ top: '8%', left: '10%' })
}.width('100%').height('50%')
.alignItems(HorizontalAlign.Start)
Row() {
Button() {
Text($r('app.string.update'))
.fontColor('#45A6F4')
.fontSize(12)
}
.width(120)
.height(32)
.margin({ top: '30%', bottom: '10%' })
.backgroundColor('#FFFFFF')
.borderRadius(16)
.onClick(() => {
postCardAction(this, {
action: 'message',
params: {
info: 'refreshImage'
}
});
})
}.width('100%').height('40%')
.justifyContent(FlexAlign.Center)
}
.width('100%').height('100%')
.backgroundImage(this.loaded ? 'memory://' + this.imgName : $r('app.media.ImageDisp'))
.backgroundImageSize(ImageSize.Cover)
}
}
说明
Image组件通过入参(memory://fileName)中的(memory://)标识来进行远端内存图片显示,其中fileName需要和EntryFormAbility传递对象('formImages': {key: fd})中的key相对应。
Image组件通过传入的参数是否有变化来决定是否刷新图片,因此EntryFormAbility每次传递过来的imgName都需要不同,连续传递两个相同的imgName时,图片不会刷新。
在卡片上展示的图片,大小需要控制在2MB以内。
4.6.5 根据卡片状态刷新不同内容
相同的卡片可以添加到桌面上实现不同的功能,比如添加两张桌面的卡片,一张显示杭州的天气,一张显示北京的天气,设置每天早上7点触发定时刷新,卡片需要感知当前的配置是杭州还是北京,然后将对应城市的天气信息刷新到卡片上,以下示例介绍了如何根据卡片的状态动态选择需要刷新的内容。
-
卡片配置文件:配置每30分钟自动刷新。
{
"forms": [
{
"name": "WidgetUpdateByStatus",
"description": "$string:UpdateByStatusFormAbility_desc",
"src": "./ets/widgetupdatebystatus/pages/WidgetUpdateByStatusCard.ets",
"uiSyntax": "arkts",
"window": {
"designWidth": 720,
"autoDesignWidth": true
},
"colorMode": "auto",
"isDefault": true,
"updateEnabled": true,
"scheduledUpdateTime": "10:30",
"updateDuration": 1,
"defaultDimension": "2*2",
"supportDimensions": [
"2*2"
]
}
]
}
- 卡片页面:卡片具备不同的状态选择,在不同的状态下需要刷新不同的内容,因此在状态发生变化时通过postCardAction通知EntryFormAbility。
let storageUpdateByStatus = new LocalStorage();
@Entry(storageUpdateByStatus)
@Component
struct WidgetUpdateByStatusCard {
@LocalStorageProp('textA') textA: Resource = $r('app.string.to_be_refreshed');
@LocalStorageProp('textB') textB: Resource = $r('app.string.to_be_refreshed');
@State selectA: boolean = false;
@State selectB: boolean = false;
build() {
Column() {
Column() {
Row() {
Checkbox({ name: 'checkbox1', group: 'checkboxGroup' })
.padding(0)
.select(false)
.margin({ left: 26 })
.onChange((value: boolean) => {
this.selectA = value;
postCardAction(this, {
action: 'message',
params: {
selectA: JSON.stringify(value)
}
});
})
Text($r('app.string.status_a'))
.fontColor('#000000')
.opacity(0.9)
.fontSize(14)
.margin({ left: 8 })
}
.width('100%')
.padding(0)
.justifyContent(FlexAlign.Start)
Row() {
Checkbox({ name: 'checkbox2', group: 'checkboxGroup' })
.padding(0)
.select(false)
.margin({ left: 26 })
.onChange((value: boolean) => {
this.selectB = value;
postCardAction(this, {
action: 'message',
params: {
selectB: JSON.stringify(value)
}
});
})
Text($r('app.string.status_b'))
.fontColor('#000000')
.opacity(0.9)
.fontSize(14)
.margin({ left: 8 })
}
.width('100%')
.position({ y: 32 })
.padding(0)
.justifyContent(FlexAlign.Start)
}
.position({ y: 12 })
Column() {
Row() { // 选中状态A才会进行刷新的内容
Text($r('app.string.status_a'))
.fontColor('#000000')
.opacity(0.4)
.fontSize(12)
Text(this.textA)
.fontColor('#000000')
.opacity(0.4)
.fontSize(12)
}
.margin({ top: '12px', left: 26, right: '26px' })
Row() { // 选中状态B才会进行刷新的内容
Text($r('app.string.status_b'))
.fontColor('#000000')
.opacity(0.4)
.fontSize(12)
Text(this.textB)
.fontColor('#000000')
.opacity(0.4)
.fontSize(12)
}.margin({ top: '12px', bottom: '21px', left: 26, right: '26px' })
}
.margin({ top: 80 })
.width('100%')
.alignItems(HorizontalAlign.Start)
}.width('100%').height('100%')
.backgroundImage($r('app.media.CardUpdateByStatus'))
.backgroundImageSize(ImageSize.Cover)
}
}
- EntryFormAbility:将卡片的状态存储在本地数据库中,在刷新事件回调触发时,通过formId获取当前卡片的状态,然后根据卡片的状态选择不同的刷新内容。
import { Want } from '@kit.AbilityKit';
import { preferences } from '@kit.ArkData';
import { BusinessError } from '@kit.BasicServicesKit';
import { formBindingData, FormExtensionAbility, formInfo, formProvider } from '@kit.FormKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG: string = 'UpdateByStatusFormAbility';
const DOMAIN_NUMBER: number = 0xFF00;
export default class UpdateByStatusFormAbility extends FormExtensionAbility {
onAddForm(want: Want): formBindingData.FormBindingData {
let formId: string = '';
let isTempCard: boolean;
if (want.parameters) {
formId = want.parameters[formInfo.FormParam.IDENTITY_KEY].toString();
isTempCard = want.parameters[formInfo.FormParam.TEMPORARY_KEY] as boolean;
if (isTempCard === false) { // 如果为常态卡片,直接进行信息持久化
hilog.info(DOMAIN_NUMBER, TAG, 'Not temp card, init db for:' + formId);
let promise: Promise<preferences.Preferences> = preferences.getPreferences(this.context, 'myStore');
promise.then(async (storeDB: preferences.Preferences) => {
hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded to get preferences.');
await storeDB.put('A' + formId, 'false');
await storeDB.put('B' + formId, 'false');
await storeDB.flush();
}).catch((err: BusinessError) => {
hilog.info(DOMAIN_NUMBER, TAG, `Failed to get preferences. ${JSON.stringify(err)}`);
});
}
}
let formData: Record<string, Object | string> = {};
return formBindingData.createFormBindingData(formData);
}
onRemoveForm(formId: string): void {
hilog.info(DOMAIN_NUMBER, TAG, 'onRemoveForm, formId:' + formId);
let promise = preferences.getPreferences(this.context, 'myStore');
promise.then(async (storeDB) => {
hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded to get preferences.');
await storeDB.delete('A' + formId);
await storeDB.delete('B' + formId);
}).catch((err: BusinessError) => {
hilog.info(DOMAIN_NUMBER, TAG, `Failed to get preferences. ${JSON.stringify(err)}`);
});
}
// 如果在添加时为临时卡片,则建议转为常态卡片时进行信息持久化
onCastToNormalForm(formId: string): void {
hilog.info(DOMAIN_NUMBER, TAG, 'onCastToNormalForm, formId:' + formId);
let promise: Promise<preferences.Preferences> = preferences.getPreferences(this.context, 'myStore');
promise.then(async (storeDB: preferences.Preferences) => {
hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded to get preferences.');
await storeDB.put('A' + formId, 'false');
await storeDB.put('B' + formId, 'false');
await storeDB.flush();
}).catch((err: BusinessError) => {
hilog.info(DOMAIN_NUMBER, TAG, `Failed to get preferences. ${JSON.stringify(err)}`);
});
}
onUpdateForm(formId: string): void {
let promise: Promise<preferences.Preferences> = preferences.getPreferences(this.context, 'myStore');
promise.then(async (storeDB: preferences.Preferences) => {
hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded to get preferences from onUpdateForm.');
let stateA = await storeDB.get('A' + formId, 'false');
let stateB = await storeDB.get('B' + formId, 'false');
// A状态选中则更新textA
if (stateA === 'true') {
let param: Record<string, string> = {
'textA': 'AAA'
};
let formInfo: formBindingData.FormBindingData = formBindingData.createFormBindingData(param);
await formProvider.updateForm(formId, formInfo);
}
// B状态选中则更新textB
if (stateB === 'true') {
let param: Record<string, string> = {
'textB': 'BBB'
};
let formInfo: formBindingData.FormBindingData = formBindingData.createFormBindingData(param);
await formProvider.updateForm(formId, formInfo);
}
hilog.info(DOMAIN_NUMBER, TAG, `Update form success stateA:${stateA} stateB:${stateB}.`);
}).catch((err: BusinessError) => {
hilog.info(DOMAIN_NUMBER, TAG, `Failed to get preferences. ${JSON.stringify(err)}`);
});
}
onFormEvent(formId: string, message: string): void {
// 存放卡片状态
hilog.info(DOMAIN_NUMBER, TAG, 'onFormEvent formId:' + formId + 'msg:' + message);
let promise: Promise<preferences.Preferences> = preferences.getPreferences(this.context, 'myStore');
promise.then(async (storeDB: preferences.Preferences) => {
hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded to get preferences.');
let msg: Record<string, string> = JSON.parse(message);
if (msg.selectA !== undefined) {
hilog.info(DOMAIN_NUMBER, TAG, 'onFormEvent selectA info:' + msg.selectA);
await storeDB.put('A' + formId, msg.selectA);
}
if (msg.selectB !== undefined) {
hilog.info(DOMAIN_NUMBER, TAG, 'onFormEvent selectB info:' + msg.selectB);
await storeDB.put('B' + formId, msg.selectB);
}
await storeDB.flush();
}).catch((err: BusinessError) => {
hilog.info(DOMAIN_NUMBER, TAG, `Failed to get preferences. ${JSON.stringify(err)}`);
});
}
}
说明
通过本地数据库进行卡片信息的持久化时,建议先在onAddForm生命周期中通过TEMPORARY_KEY判断当前添加的卡片是否为常态卡片:如果是常态卡片,则直接进行卡片信息持久化;如果为临时卡片,则可以在卡片转为常态卡片(onCastToNormalForm)时进行持久化;同时需要在卡片销毁(onRemoveForm)时删除当前卡片存储的持久化信息,避免反复添加删除卡片导致数据库文件持续变大。