一、概述
① 基本概念
- 服务卡片(以下简称“卡片”)是 FA 的一种界面展示形式,将 FA 的重要信息或操作前置到卡片,以达到服务直达,减少体验层级的目的。
- 卡片常用于嵌入到其他应用(当前只支持系统应用)中作为其界面的一部分显示,并支持拉起页面,发送消息等基础的交互功能。卡片使用方负责显示卡片。
- 服务卡片如下图所示:
- 卡片使用方:显示卡片内容的宿主应用,控制卡片在宿主中展示的位置。
- 卡片管理服务:用于管理系统中所添加卡片的常驻代理服务,包括卡片对象的管理与使用,以及卡片周期性刷新等。
- 卡片提供方:提供卡片显示内容的 HarmonyOS 应用或原子化服务,控制卡片的显示内容、控件布局以及控件点击事件。
- 卡片使用方和提供方不要求常驻运行,在需要添加/删除/请求更新卡片时,卡片管理服务会拉起卡片提供方获取卡片信息。
- 卡片提供方控制卡片实际显示的内容、控件布局以及控件点击事件。
② 运作机制
- 卡片管理服务包含以下模块:
-
- 周期性刷新:在卡片添加后,根据卡片的刷新策略启动定时任务周期性触发卡片的刷新。
-
- 卡片缓存管理:在卡片添加到卡片管理服务后,对卡片的视图信息进行缓存,以便下次获取卡片时可以直接返回缓存数据,降低时延。
-
- 卡片生命周期管理:对于卡片切换到后台或者被遮挡时,暂停卡片的刷新;以及卡片的升级/卸载场景下对卡片数据的更新和清理。
-
- 卡片使用方对象管理:对卡片使用方的 RPC 对象进行管理,用于使用方请求进行校验以及对卡片更新后的回调处理。
-
- 通信适配层:负责与卡片使用方和提供方进行 RPC 通信。
- 卡片提供方包含以下模块:
-
- 卡片服务:由卡片提供方开发者实现,开发者实现 onCreateForm、onUpdateForm 和 onDeleteForm 处理创建卡片、更新卡片以及删除卡片等请求,提供相应的卡片服务。
-
- 卡片提供方实例管理模块:由卡片提供方开发者实现,负责对卡片管理服务分配的卡片实例进行持久化管理。
-
- 通信适配层:由 HarmonyOS SDK 提供,负责与卡片管理服务通信,用于将卡片的更新数据主动推送到卡片管理服务。
二、API 说明
- 卡片提供方接口功能:
类名 | 接口名 | 描述 |
---|---|---|
Ability | ProviderFormInfo onCreateForm(Intent intent) | 卡片提供方接收创建卡片通知接口 |
void onUpdateForm(long formId) | 卡片提供方接收更新卡片通知接口 | |
void onDeleteForm(long formId) | 卡片提供方接收删除卡片通知接口 | |
void onTriggerFormEvent(long formId, String message) | 卡片提供方处理卡片事件接口(JS卡片使用) | |
boolean updateForm(long formId, ComponentProvider component) | 卡片提供方主动更新卡片(Java卡片使用) | |
boolean updateForm(long formId, FormBindingData formBindingData) | 卡片提供方主动更新卡片(JS卡片使用),仅更新formBindingData中携带的信息,卡片中其余信息保持不变 | |
void onCastTempForm(long formId) | 卡片提供方接收临时卡片转常态卡片通知 | |
void onEventNotify(Map < Long, Integer > formEvents) | 卡片提供方接收到事件通知,其中Ability.FORM_VISIBLE表示卡片可见通知, Ability.FORM_INVISIBLE表示卡片不可见通知 | |
FormState onAcquireFormState(Intent intent) | 卡片提供方接收查询卡片状态通知接口,默认返回卡片初始状态 | |
ProviderFormInfo | ProviderFormInfo(int resId, Context context) | Java卡片返回对象构造函数 |
ProviderFormInfo() | JS卡片返回对象构造函数 | |
void mergeActions(ComponentProvider componentProviderActions) | 在提供方侧调用该接口,将开发者在ComponentProvider中设置的actions配置数据合并到当前对象中 | |
void setJsBindingData(FormBindingData data) | 设置JS卡片的内容信息(JS卡片使用) |
- onEventNotify 仅系统应用才会回调,其他接口回调时机如下图:
- 卡片管理服务不负责保持卡片的活跃状态(设置了定时更新的除外),当使用方作出相应的请求时,管理服务会拉起提供方并回调相应接口。
三、Java 卡片与 JS 卡片选型
- Java/JS 卡片场景能力差异如下表所示:
场景 | Java卡片 | JS卡片 | 支持的版本 |
---|---|---|---|
实时刷新(类似时钟) | Java使用ComponentProvider做实时刷新代价比较大 | JS可以做到端侧刷新,但是需要定制化组件 | HarmonyOS 2.0及以上 |
开发方式 | Java UI在卡片提供方需要同时对数据和组件进行处理,生成ComponentProvider远端渲染 | JS卡片在使用方加载渲染,提供方只要处理数据、组件和逻辑分离 | HarmonyOS 2.0及以上 |
组件支持 | Text、Image、DirectionalLayout、PositionLayout、DependentLayout | div、list、list-item、swiper、stack、image、text、span、progress、button(定制:chart 、clock、calendar) | HarmonyOS 2.0及以上 |
卡片内动效 | 不支持 | 暂不开放 | HarmonyOS 2.0及以上 |
阴影模糊 | 不支持 | 支持 | HarmonyOS 2.0及以上 |
动态适应布局 | 不支持 | 支持 | HarmonyOS 2.0及以上 |
自定义卡片跳转页面 | 不支持 | 支持 | HarmonyOS 2.0及以上 |
- 综上所述,JS 卡片比 Java 卡片支持的控件和能力都更丰富:
-
- Java 卡片:适合作为一个直达入口,没有复杂的页面和事件。
-
- JS 卡片:适合有复杂界面的卡片。
- 对于同一个 Page ability,在 config.json 中最多支持配置 16 张卡片。
三、创建服务卡片
① 卡片目录结构
- JS 服务卡片(entry/src/main/js/Component)的典型开发目录结构如下:
- 目录结构中文件分类如下:
-
- .hml 结尾的 HML 模板文件,这个文件用来描述卡片页面的模板布局结构;
-
- .css 结尾的 CSS 样式文件,这个文件用于描述页面样式;
-
- .json 结尾的 JSON 文件,这个文件用于配置卡片中使用的变量 action 事件。
- 各个文件夹的作用:
-
- pages 目录用于存放卡片模板页面;
-
- common 目录用于存放公共资源文件,比如:图片资源;
-
- resources 目录用于存放资源配置文件,比如:多分辨率加载配置文件;
-
- i18n 目录用于配置不同语言场景资源内容,比如应用文本词条,图片路径等资源。
② 卡片创建
- 对于创建新工程,可以在工程向导中勾选“Show in Service Center”,该参数表示是否在服务中心露出。如果 Project Type 为 Service,则会同步创建一个 22 的服务卡片模板,同时还会创建入口卡片;如果 Project Type 为 Application,则只会创建一个 22 的服务卡片模板:
- 卡片创建完成后,会在工程目录下生成 EntryCard 目录:
- 在该目录下,每个拥有 EntryCard 的模块,都会生成一个和模块名相同的文件夹,同时还会默认生成一张 2x2 的快照型 EntryCard 图片(png 格式)。
- 可以将其替换为提前设计好的 2x2 快照图:将新的快照图拷贝到上图目录下,删除默认图片,新图片命名遵循格式“卡片名称-2x2.png”。
- 在已有工程中添加新模块,也可以添加服务卡片和 EntryCard,只需在创建模块时,勾选“Show in Service Center”即可。创建出来的服务卡片和 EntryCard,同创建新工程生成的一致。
- 在已有工程中,只添加 EntryCard,只能通过手工方式,按照上图中的 EntryCard 目录创建对应的文件夹和图片。
- 在已有工程中,新添加服务卡片,可以通过如下方法进行创建:
-
- 打开一个工程,创建服务卡片模板,创建方法包括如下两种方式:
-
-
- 选择模块(如entry模块)下的任意文件,点击菜单栏 File > New > Service Widget创建服务卡片;
-
-
-
- 选择模块(如entry模块)下的任意文件,点击右键 > New > Service Widget创建服务卡片。
-
-
- 在 Choose a template for your service widget 界面中,选择需要创建的卡片模板,点击 Next:
-
- 在 Configure Your Service Widget 界面中,配置卡片的基本信息,包括:
Service Widget Name:卡片的名称,在同一个 FA 中,卡片名称不能重复,且只能包含数字、字母和下划线。
- 在 Configure Your Service Widget 界面中,配置卡片的基本信息,包括:
-
-
- Description:卡片的描述信息;
-
-
-
- Select Ability/New Ability:选择一个挂靠服务卡片的 Page Ability,或者创建一个新的 Page Ability;
-
-
-
- Type:卡片的开发语言类型;
-
-
-
- JS Component Name:Type 选择 JS 时需要设置卡片的 JS Component 名称;
-
-
-
- Support Dimensions:选择卡片的规格,部分卡片支持同时设置多种规格;
-
-
- 点击 Finish 完成卡片的创建。创建完成后,工具会自动创建出服务卡片的布局文件,并在 config.json 文件中写入服务卡片的属性字段:
-
- 卡片创建完成后,请根据 Java 卡片开发或 JS 卡片开发,完成服务卡片的开发。
四、JS 卡片开发
① 使用 hml+css+json 开发 JS 卡片页面
- 使用 DevEco Studio 创建卡片工程
-
- 创建成功后,在 config.json 的 module 中会生成 js 模块,用于对应卡片的 js 相关资源,配置示例如下:
"js": [
{
"name": "card",
"pages": [
"pages/index/index"
],
"window": {
"designWidth": 720,
"autoDesignWidth": true
},
"type": "form"
}
]
-
- config.json 文件“abilities”配置 forms 模块细节如下:
"forms": [
{
"name": "Form_Js",
"description": "form_description",
"type": "JS",
"jsComponentName": "card",
"formConfigAbility": "ability://com.huawei.demo.SecondFormAbility",
"colorMode": "auto",
"isDefault": true,
"updateEnabled": true,
"scheduledUpdateTime": "10:30",
"updateDuration": 1,
"defaultDimension": "2*2",
"supportDimensions": [
"2*2",
"2*4",
"4*4"
],
"metaData": {
"customizeData": [
{
"name": "originWidgetName",
"value": "com.huawei.weather.testWidget"
}
]
}
}
]
-
- 配置文件中,应注意如下配置:
-
-
- “js”模块中的 name 字段要与“forms”模块中的 jsComponentName 字段的值一致,为 js 资源的实例名。
-
-
-
- “forms”模块中的 name 为卡片名,即在 onCreateForm 中根据 AbilitySlice.PARAM_FORM_NAME_KEY 可取到的值。
-
-
-
- 卡片的 Ability 中还需要配置"visible": true 和"formsEnabled": true。
-
-
-
- 定时刷新和定点刷新都配置的情况下,定时刷新优先。
-
-
-
- defaultDimension 是默认规格,必须设置。
-
-
- forms 对象的内部结构说明:
属性名称 | 子属性名称 | 含义 | 数据类型 | 是否可缺省 |
---|---|---|---|---|
name | - | 表示卡片的类名,字符串最大长度为127字节 | 字符串 | 否 |
description | - | 表示卡片的描述,取值可以是描述性内容,也可以是对描述性内容的资源索引,以支持多语言。字符串最大长度为255字节 | 字符串 | 可缺省,缺省为空 |
isDefault | - | 表示该卡片是否为默认卡片,每个Ability有且只有一个默认卡片 true:默认卡片 false:非默认卡片 | 布尔值 | 否 |
type | - | 表示卡片的类型,取值范围如下: Java:Java卡片 JS:JS卡片 | 字符串 | 否 |
colorMode | - | 表示卡片的主题样式,取值范围如下: auto:自适应 dark:深色主题 light:浅色主题 | 字符串 | 可缺省,缺省值为“auto” |
supportDimensions | - | 表示卡片支持的外观规格,取值范围: 1*2:表示1行2列的二宫格 2*2:表示2行2列的四宫格 2*4:表示2行4列的八宫格 4*4:表示4行4列的十六宫格 | 字符串数组 | 否 |
defaultDimension | - | 表示卡片的默认外观规格,取值必须在该卡片supportDimensions配置的列表中 | 字符串 | 否 |
landscapeLayouts | - | 表示卡片外观规格对应的横向布局文件,与supportDimensions中的规格一一对应,仅当卡片类型为Java卡片时,需要配置该标签 | 字符串数组 | 否 |
portraitLayouts | - | 表示卡片外观规格对应的竖向布局文件,与supportDimensions中的规格一一对应,仅当卡片类型为Java卡片时,需要配置该标签 | 字符串数组 | 否 |
updateEnabled | - | 表示卡片是否支持周期性刷新,取值范围: true:表示支持周期性刷新,可以在定时刷新(updateDuration)和定点刷新(scheduledUpdateTime)两种方式任选其一,优先选择定时刷新 false:表示不支持周期性刷新 | 布尔类型 | 否 |
scheduledUpdateTime | - | 表示卡片的定点刷新的时刻,采用24小时制,精确到分钟 | 字符串 | 可缺省,缺省值为“0:0” |
updateDuration | - | 表示卡片定时刷新的更新周期,单位为30分钟,取值为自然数: 当取值为0时,表示该参数不生效;当取值为正整数N时,表示刷新周期为30*N分钟 | 数值 | 可缺省,缺省值为“0” |
formConfigAbility | - | 表示卡片的配置跳转链接,采用URI格式 | 字符串 | 可缺省,缺省值为空 |
jsComponentName | - | 表示JS卡片的Component名称,字符串最大长度为127字节,仅当卡片类型为JS卡片时,需要配置该标签 | 字符串 | 否 |
metaData | - | 表示卡片的自定义信息,包含customizeData数组标签 | 对象 | 可缺省,缺省值为空 |
customizeData | - | 表示自定义的卡片信息 | 对象数组 | 可缺省,缺省值为空 |
name | 表示数据项的键名称,字符串最大长度为255字节 | 字符串 | 可缺省,缺省值为空 | |
value | 表示数据项的值,字符串最大长度为255字节 | 字符串 | 可缺省,缺省值为空 |
- 创建一个 FormAbility,覆写卡片相关回调函数
-
- 回调函数
-
-
- onCreateForm(Intent intent)
-
-
-
- onUpdateForm(long formId)
-
-
-
- onDeleteForm(long formId)
-
-
-
- onCastTempForm(long formId)
-
-
-
- onEventNotify(Map<Long, Integer> formEvents)
-
-
-
- onTriggerFormEvent(long formId, String message)
-
-
-
- onAcquireFormState(Intent intent)
-
-
- 当卡片使用方请求获取卡片时,卡片提供方会被拉起并调用 onCreateForm(Intent intent) 回调,intent 中会带有卡片 ID、卡片名称和卡片外观规格信息,可按需获取使用。
-
- 开发 JS 卡片时,FormAbility 可以继承 AceAbility 或 Ability,继承 Ability 时,需在 onStart() 方法中额外设置路由信息。
-
- FormAbility 继承 AceAbility 的代码示例:
public class FormAbility extends AceAbility {
......
public static long formId = -1;
@Override
public void onStart(Intent intent) {
super.onStart(intent);
}
@Override
protected ProviderFormInfo onCreateForm(Intent intent) {
long formId = intent.getLongParam(AbilitySlice.PARAM_FORM_IDENTITY_KEY, 0);
String formName = intent.getStringParam(AbilitySlice.PARAM_FORM_NAME_KEY);
int specificationId = intent.getIntParam(AbilitySlice.PARAM_FORM_DIMENSION_KEY, 0);
boolean tempFlag = intent.getBooleanParam(AbilitySlice.PARAM_FORM_TEMPORARY_KEY, false);
HiLog.info(LABEL_LOG, "onCreateForm: " + formId + " " + formName + " " + specificationId);
FormBindingData formBindingData = new FormBindingData("{\"temperature\": \"60°\"}");
ProviderFormInfo formInfo = new ProviderFormInfo();
formInfo.setJsBindingData(formBindingData);
return formInfo;
}
@Override
protected void onDeleteForm(long formId) {
// 删除卡片实例数据
super.onDeleteForm(formId);
......
}
@Override
protected void onUpdateForm(long formId) {
// 若卡片支持定时更新/定点更新/卡片使用方主动请求更新功能,则提供方需要覆写该方法以支持数据更新
super.onUpdateForm(formId);
......
}
@Override
protected void onTriggerFormEvent(long formId, String message) {
// 若卡片支持触发事件,则需要覆写该方法并实现对事件的触发
super.onTriggerFormEvent(formId, message);
......
}
@Override
protected void onCastTempForm(long formId) {
//使用方将临时卡片转换为常态卡片触发,提供方需要做相应的处理
super.onCastTempForm (formId);
......
}
@Override
protected void onEventNotify(Map<Long, Integer> formEvents) {
//使用方发起可见或者不可见通知触发,提供方需要做相应的处理
super.onEventNotify(formEvents);
......
}
@Override
protected FormState onAcquireFormState(Intent intent) {
ElementName elementName = intent.getElement();
if (elementName == null) {
HiLog.info(LABEL_LOG, "onAcquireFormState bundleName and abilityName are not set in intent");
return FormState.UNKNOWN;
}
String bundleName = elementName.getBundleName();
String abilityName = elementName.getAbilityName();
String moduleName = intent.getStringParam(AbilitySlice.PARAM_MODULE_NAME_KEY);
String formName = intent.getStringParam(AbilitySlice.PARAM_FORM_NAME_KEY);
int specificationId = intent.getIntParam(AbilitySlice.PARAM_FORM_DIMENSION_KEY, 0);
if ("form_name2".equals(formName)) {
return FormState.DEFAULT;
}
return FormState.READY;
}
}
-
- FormAbility 继承 Ability 的代码示例:
public class FormAbility extends Ability {
......
public static long formId = -1;
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setMainRoute(FormAbilitySlice.class.getName()); //设置路由
}
@Override
protected ProviderFormInfo onCreateForm(Intent intent) {
long formId = intent.getLongParam(AbilitySlice.PARAM_FORM_IDENTITY_KEY, 0);
String formName = intent.getStringParam(AbilitySlice.PARAM_FORM_NAME_KEY);
int specificationId = intent.getIntParam(AbilitySlice.PARAM_FORM_DIMENSION_KEY, 0);
boolean tempFlag = intent.getBooleanParam(AbilitySlice.PARAM_FORM_TEMPORARY_KEY, false);
HiLog.info(LABEL_LOG, "onCreateForm: " + formId + " " + formName + " " + specificationId);
FormBindingData formBindingData = new FormBindingData("{\"temperature\": \"60°\"}");
ProviderFormInfo formInfo = new ProviderFormInfo();
formInfo.setJsBindingData(formBindingData);
return formInfo;
}
@Override
protected void onDeleteForm(long formId) {
// 删除卡片实例数据
super.onDeleteForm(formId);
......
}
@Override
protected void onUpdateForm(long formId) {
// 若卡片支持定时更新/定点更新/卡片使用方主动请求更新功能,则提供方需要覆写该方法以支持数据更新
super.onUpdateForm(formId);
......
}
@Override
protected void onTriggerFormEvent(long formId, String message) {
// 若卡片支持触发事件,则需要覆写该方法并实现对事件的触发
super.onTriggerFormEvent(formId, message);
......
}
@Override
protected void onCastTempForm(long formId) {
//使用方将临时卡片转换为常态卡片触发,提供方需要做相应的处理
super.onCastTempForm (formId);
......
}
@Override
protected void onEventNotify(Map<Long, Integer> formEvents) {
//使用方发起可见或者不可见通知触发,提供方需要做相应的处理
super.onEventNotify(formEvents);
......
}
@Override
protected FormState onAcquireFormState(Intent intent) {
ElementName elementName = intent.getElement();
if (elementName == null) {
HiLog.info(LABEL_LOG, "onAcquireFormState bundleName and abilityName are not set in intent");
return FormState.UNKNOWN;
}
String bundleName = elementName.getBundleName();
String abilityName = elementName.getAbilityName();
String moduleName = intent.getStringParam(AbilitySlice.PARAM_MODULE_NAME_KEY);
String formName = intent.getStringParam(AbilitySlice.PARAM_FORM_NAME_KEY);
int specificationId = intent.getIntParam(AbilitySlice.PARAM_FORM_DIMENSION_KEY, 0);
if ("form_name2".equals(formName)) {
return FormState.DEFAULT;
}
return FormState.READY;
}
}
- 卡片信息持久化
-
- 因大部分卡片提供方都不是常驻服务,只有在需要使用时才会被拉起获取卡片信息,且卡片管理服务支持对卡片进行多实例管理,卡片 ID 对应实例 ID,因此若卡片提供方支持对卡片数据进行配置,则需要对卡片的业务数据按照卡片 ID 进行持久化管理,以便在后续获取、更新以及拉起时能获取到正确的卡片业务数据。且需要适配 onDeleteForm(long formId) 卡片删除通知接口,在其中实现卡片实例数据的删除。
-
- 常态卡片:卡片使用方会持久化的卡片;
-
- 临时卡片:卡片使用方不会持久化的卡片;
-
- 需要注意的是,卡片使用方在请求卡片时传递给提供方应用的 Intent 数据中存在临时标记字段,表示此次请求的卡片是否为临时卡片,由于临时卡片的数据具有非持久化的特殊性,某些场景比如卡片服务框架死亡重启,此时临时卡片数据在卡片管理服务中已经删除,且对应的卡片 ID 不会通知到提供方,所以卡片提供方需要自己负责清理长时间未删除的临时卡片数据。同时对应的卡片使用方可能会将之前请求的临时卡片转换为常态卡片。如果转换成功,卡片提供方也需要对对应的临时卡片 ID 进行处理,把卡片提供方记录的临时卡片数据转换为常态卡片数据,防止提供方在清理长时间未删除的临时卡片时,把已经转换为常态卡片的临时卡片信息删除,导致卡片信息丢失。
@Override
protected ProviderFormInfo onCreateForm(Intent intent) {
long formId = intent.getIntParam(AbilitySlice.PARAM_FORM_IDENTITY_KEY, -1L);
String formName = params.getStringParam(AbilitySlice.PARAM_FORM_NAME_KEY);
int specificationId = params.getIntParam(AbilitySlice.PARAM_FORM_DIMENSION_KEY, 0);
boolean tempFlag = params.getBooleanParam(AbilitySlice.PARAM_FORM_TEMPORARY_KEY, false);
HiLog.info(LABEL_LOG, "onCreateForm: " + formId + " " + formName + " " + specificationId);
.......
// 由开发人员自行实现,将创建的卡片信息持久化,以便在下次获取/更新该卡片实例时进行使用
storeFormInfo(formId, formName, specificationId, formData);
......
HiLog.info(LABEL_LOG, "onCreateForm finish.......");
return formInfo;
}
@Override
protected void onDeleteForm(long formId) {
super.onDeleteForm(formId);
// 由开发人员自行实现,删除卡片实例数据
deleteFormInfo(formId);
......
}
@Override
protected void onCastTempForm(long formId) {
// 使用方将临时卡片转换为常态卡片触发,提供方需要做相应的处理
super.onCastTempForm (formId);
......
}
- 卡片数据交互
-
- 当卡片应用需要更新数据时(如触发了定时更新或定点更新),卡片应用获取最新数据,并调用 updateForm 接口更新卡片。示例如下:
@Override
protected void onUpdateForm(long formId) {
super.onUpdateForm(formId);
ZSONObject zsonObject = new ZSONObject();
zsonObject.put("temperature", "90°");
FormBindingData formBindingData = new FormBindingData(zsonObject);
// 调用updateForm接口去更新对应的卡片,仅更新入参中携带的数据信息,其他信息保持不变
if (!updateForm(formId, formBindingData)) {
// err process
}
}
- 开发 JS 卡片页面
-
- JS 卡片页面与普通 FA 类似通过 hml+css+json 开发,示例如下:
hml:
<div class="container">
<stack class="stack_container">
<image class = "img" src="common/clouds.png"></image>
<div style="flex-direction: column;">
<text class="txt_city" onclick="messageEvent">{{city}}</text>
<text class="txt_temperature" onclick="routerEvent">{{temperature}}</text>
</div>
</stack>
</div>
css:
.container {
flex-direction: column;
justify-content: center;
align-items: center;
}
.stack_container {
width: 100%;
height: 100%;
background-image: url("/common/weather-background-day.png");
background-size: cover;
}
...
json:
{
"data": {
"temperature": "35°",
"city": "hangzhou"
},
"actions": {
"routerEvent": {
"action": "router",
"abilityName": "com.example.myapplication.FormAbility",
"params": {
"message": "weather"
}
},
"messageEvent": {
"action": "message",
"params": {
"message": "weather update"
}
}
}
}
- 开发 JS 卡片事件和 action
-
- JS 卡片支持为组件设置 action,包括 router 事件和 message 事件,其中 router 事件用于应用跳转,message 事件用于卡片开发人员自定义点击事件。
-
- 关键步骤说明如下:
-
-
- 在 hml 中为组件设置 onclick 属性,其值对应到 json 文件的 actions 字段中。
-
-
-
- 若设置 router 事件,则
-
-
-
-
- action 属性值为"router";
-
-
-
-
-
- abilityName 为卡片提供方应用的跳转目标 Ability 名;
-
-
-
-
-
- params中的值按需填写,其值在使用时通过 intent.getStringParam(“params”) 获取即可;
-
-
-
-
- 若设置 message 事件,则 action 属性值为"message",params 为 json 格式的值。
-
hml:
<text class="txt_city" onclick="messageEvent">{{city}}</text>
<text class="txt_temperature" onclick="routerEvent">{{temperature}}</text>
json:
{
"actions": {
"routerEvent": {
"action": "router",
"abilityName": "com.example.myapplication.FormAbility",
"params": {
"message": "weather"
}
},
"messageEvent": {
"action": "message",
"params": {
"message": "test date",
}
}
}
}
- 当点击组件触发 message 事件时,卡片应用的 onTriggerFormEvent 方法被触发, params 属性的值将作为参数被传入,解析使用即可。
- message 事件由于是自定义,也可以在 message 事件中实现跳转到其他 Ability 的能力。但是,在这种情况下,卡片使用方定义的动效是不生效的。宿主侧定义的动效仅在 router 事件的跳转中生效。
- 如果想要保证动效,使用 routerEvent。routerEvent 配置跳转链接时,只能配置到卡片提供方自己的 ability 中。
② 通过内存图片方式使用 image 组件
- 在卡片上如果想要显示网络的图片资源、数据库中查询读取的图片资源等图片资源,可以通过 image 组件提供的内存图片能力。
- 获取图片数据:内存图片使用 byte[] 格式的图片数据,可以来自多个途径:比如网络的图片资源、数据库中查询读取的图片资源、本地图片打开后获得的图片资源等。
- 调用 FormBindingData 的 addImageData 接口传入数据:
-
- 首先创建一个 ZSONObject,将{imageSrc,memory:// + picName}的键值对添加到 ZSONObject 中,其中,imageSrc 是 image 组件 src 属性关联的变量(比如在 js 文件中, image 组件的写法是
),picName 是共享内存的图片名,该命名可以自定义,但是要保证图片格式的后缀名正确。
- 首先创建一个 ZSONObject,将{imageSrc,memory:// + picName}的键值对添加到 ZSONObject 中,其中,imageSrc 是 image 组件 src 属性关联的变量(比如在 js 文件中, image 组件的写法是
ZSONObject zsonObject = new ZSONObject();
zsonObject.put("imageSrc", "memory://logo.png");
-
- 使用该 ZSONObject 去创建一个 FormBindingData。
-
- 调用 FormBindingData 的 addImageData 接口添加数据:addImageData(“logo.png”, bytes),其中,"logo.png"为 picName,必须和第一步里面添加到 ZSONObject 中的键值对的picName一致,否则1中的共享图片路径(“memory://logo.png”)将读取不到这里添加进去的图片数据。
-
- 如果是在卡片的 onCreateForm 生命周期去更新共享内存图片数据,则只需创建 ProviderFormInfo,然后将 FormBindingData 设置给 ProviderFormInfo中,返回 ProviderFormInfo 即可。
-
- 如果是在卡片的其他生命周期去更新共享内存图片数据,直接调用 updateForm 去更新指定卡片即可,代码示例在 onCreateForm 中使用。
<!-- xxx.hml -->
<image src="{{imageSrc}}"></image>
// xxx.xxx.FormAbility
@Override
protected ProviderFormInfo onCreateForm(Intent intent) {
IntentParams params = intent.getParams();
if (params == null) {
return null;
}
formId = (int) params.getParam(AbilitySlice.PARAM_FORM_ID_KEY);
String formName = (String) params.getParam(AbilitySlice.PARAM_FORM_NAME_KEY);
int specificationId = (int) params.getParam(AbilitySlice.PARAM_FORM_DIMENSION_KEY);
// ********************* memory image usage section *********************
// Step1: Create [ZSONObject] and set [picName] of [imageSrc] to it
// [imageSrc] is the variable we bind to the [src] attribute of image component in js
// such as <image src="{{imageSrc}}"></image>
ZSONObject zsonObject = new ZSONObject();
zsonObject.put("imageSrc", "memory://logo.png");
// Step2: Construct a [FormbindingData] using the [ZSONObject] we just created
FormBindingData formBindingData = new FormBindingData(zsonObject);
// Step3: Add image data to [FormBindingData] via interface: [addImageData]
formBindingData.addImageData("logo.png", bytes);
// ********************* memory image usage section *********************
ProviderFormInfo formInfo = new ProviderFormInfo();
formInfo.setJsBindingData(formBindingData);
return formInfo;
}
五、Java 卡片开发
① 使用 DevEco Studio 创建卡片工程
- 卡片应用是一款特殊的元能力服务,其配置文件 config.json 中声明以下几项,系统能够识别该应用为一款卡片应用,并与系统进行绑定。
- config.json 文件 “abilities” 配置 forms 模块细节如下:
"forms": [
{
"name": "Form_Java",
"description": "form_description",
"type": "Java",
"colorMode": "auto",
"isDefault": true,
"updateEnabled": true,
"scheduledUpdateTime": "10:30",
"updateDuration": 1,
"defaultDimension": "2*2",
"formVisibleNotify": true,
"supportDimensions": [
"1*2",
"2*2",
"2*4",
"4*4"
],
"landscapeLayouts": [
"$layout:form_ability_layout_1_2",
"$layout:form_ability_layout_2_2",
"$layout:form_ability_layout_2_4",
"$layout:form_ability_layout_4_4"
],
"portraitLayouts": [
"$layout:form_ability_layout_1_2",
"$layout:form_ability_layout_2_2",
"$layout:form_ability_layout_2_4",
"$layout:form_ability_layout_4_4"
],
"formConfigAbility": "ability://SecondFormAbility",
"metaData": {
"customizeData": [
{
"name": "originWidgetName",
"value": "com.huawei.weather.testWidget"
}
]
}
}
]
- "forms"模块中的 name 为卡片名,即在 onCreateForm 中根据 AbilitySlice.PARAM_FORM_NAME_KEY 可取到的值。
- 在卡片所在的"abilities"中还需要配置"visible": true 和"formsEnabled": true。
② 创建一个 FormAbility,覆写卡片相关回调函数
- 相关函数:
-
- onCreateForm(Intent intent)
-
- onUpdateForm(long formId)
-
- onDeleteForm(long formId)
-
- onCastTempForm(long formId)
-
- onEventNotify(Map<Long, Integer> formEvents)
-
- onAcquireFormState(Intent intent)
- 在 onCreateForm(Intent intent) 中,当卡片使用方请求获取卡片时,卡片提供方会被拉起并调用 onCreateForm(Intent intent) 回调,intent 中会带有卡片 ID,卡片名称,临时卡片标记和卡片外观规格信息,分别通过 AbilitySlice.PARAM_FORM_IDENTITY_KEY、AbilitySlice.PARAM_FORM_NAME_KEY、AbilitySlice.PARAM_FORM_TEMORARY_KEY和AbilitySlice.PARAM_FORM_DIMENSION_KEY 按需获取。
- 提供方可以通过 AbilitySlice.PARAM_FORM_CUSTOMIZE_KEY 获取卡片使用方设置的自定义数据。
public class FormAbility extends Ability {
......
@Override
public void onStart(Intent intent) {
super.onStart(intent);
......
}
@Override
protected ProviderFormInfo onCreateForm(Intent intent) {
long formId = intent.getLongParam(AbilitySlice.PARAM_FORM_IDENTITY_KEY, 0);
String formName = intent.getStringParam(AbilitySlice.PARAM_FORM_NAME_KEY);
int specificationId = intent.getIntParam(AbilitySlice.PARAM_FORM_DIMENSION_KEY, 0);
boolean tempFlag = intent.getBooleanParam(AbilitySlice.PARAM_FORM_TEMPORARY_KEY, false);
// 获取自定义数据
IntentParams intentParams = intent.getParam(AbilitySlice.PARAM_FORM_CUSTOMIZE_KEY);
HiLog.info(LABEL_LOG, "onCreateForm: " + formId + " " + formName + " " + specificationId);
// 开发者需要根据卡片的名称以及外观规格获取对应的xml布局并构造卡片对象,此处ResourceTable.Layout_form_ability_layout_2_2仅为示例
ProviderFormInfo formInfo = new ProviderFormInfo(ResourceTable.Layout_form_ability_layout_2_2, this);
// 获取卡片信息
String formData = getInitFormData(formName, specificationId);
ComponentProvider componentProvider = new ComponentProvider();
componentProvider.setText(ResourceTable.Id_title, "formData-" + formData);
formInfo.mergeActions(componentProvider);
......
HiLog.info(LABEL_LOG, "onCreateForm finish.......");
return formInfo;
}
@Override
protected void onDeleteForm(long formId) {
super.onDeleteForm(formId);
// 删除卡片实例数据,需要由开发者实现
deleteFormInfo(formId);
......
}
@Override
// 若卡片支持定时更新/定点更新/卡片使用方主动请求更新功能,则提供方需要覆写该方法以支持数据更新
protected void onUpdateForm(long formId) {
super.onUpdateForm(formId);
// 更新卡片信息,由开发者实现
......
}
@Override
protected void onCastTempForm(long formId) {
// 使用方将临时卡片转换为常态卡片触发,提供方需要做相应的处理,将数据持久化。
super.onCastTempForm (formId);
......
}
@Override
protected void onEventNotify(Map<Long, Integer> formEvents) {
// 使用方发起可见或者不可见通知触发,提供方需要做相应的处理,比如卡片可见时刷新卡片,仅系统应用能收到该回调。
super.onEventNotify(formEvents);
......
}
@Override
protected FormState onAcquireFormState(Intent intent) {
ElementName elementName = intent.getElement();
if (elementName == null) {
HiLog.info(LABEL_LOG, "onAcquireFormState bundleName and abilityName are not set in intent");
return FormState.UNKNOWN;
}
String bundleName = elementName.getBundleName();
String abilityName = elementName.getAbilityName();
String moduleName = intent.getStringParam(AbilitySlice.PARAM_MODULE_NAME_KEY);
String formName = intent.getStringParam(AbilitySlice.PARAM_FORM_NAME_KEY);
int specificationId = intent.getIntParam(AbilitySlice.PARAM_FORM_DIMENSION_KEY, 0);
if ("form_name2".equals(formName)) {
return FormState.DEFAULT;
}
return FormState.READY;
}
}
③ 卡片信息持久化
- 因大部分卡片提供方都不是常驻服务,只有在需要使用时才会被拉起获取卡片信息。且卡片管理服务支持对卡片进行多实例管理,卡片 ID 对应实例 ID,因此若卡片提供方支持对卡片数据进行配置,则需要提供方对卡片的业务数据按照卡片 ID 进行持久化管理,以便在后续获取、更新以及拉起时能获取到正确的卡片业务数据。
- 同时,需要适配 onDeleteForm(int formId) 卡片删除通知接口,在其中实现卡片实例数据的删除。和 JS 卡片相同,需要注意卡片使用方在请求卡片时传递给提供方应用的 Intent 数据中存在临时标记字段,表示此次请求的卡片是否为临时卡片,由于临时卡片的数据具有非持久化的特殊性,某些场景比如卡片服务框架死亡重启,此时临时卡片数据在卡片管理服务中已经删除,且对应的卡片 ID 不会通知到提供方,所以卡片提供方需要自己负责清理长时间未删除的临时卡片数据。同时对应的卡片使用方可能会将之前请求的临时卡片转换为常态卡片。如果转换成功,卡片提供方也需要对对应的临时卡片 ID 进行处理,把卡片提供方记录的临时卡片数据转换为常态卡片数据,防止提供方在清理长时间未删除的临时卡片时,把已经转换为常态卡片的临时卡片信息删除,导致卡片信息丢失。
@Override
protected ProviderFormInfo onCreateForm(Intent intent) {
long formId = intent.getLongParam(AbilitySlice.PARAM_FORM_ID_KEY, -1L);
String formName = intent.getStringParam(AbilitySlice.PARAM_FORM_NAME_KEY);
int specificationId = intent.getIntParam(AbilitySlice.PARAM_FORM_DIMENSION_KEY, 0);
boolean tempFlag = params.getBooleanParam(AbilitySlice.PARAM_FORM_TEMPORARY_KEY, false);
HiLog.info(LABEL_LOG, "onCreateForm: " + formId + " " + formName + " " + specificationId);
.......
// 将创建的卡片信息持久化,以便在下次获取/更新该卡片实例时进行使用,该方法需要由开发者实现。
storeFormInfo(formId, formName, specificationId, formData);
......
HiLog.info(LABEL_LOG, "onCreateForm finish.......");
return formInfo;
}
@Override
protected void onDeleteForm(long formId) {
super.onDeleteForm(formId);
// 由开发人员自行实现,删除卡片实例数据
deleteFormInfo(formId);
......
}
@Override
protected void onCastTempForm(long formId) {
// 使用方将临时卡片转换为常态卡片触发,提供方需要做相应的处理
super.onCastTempForm (formId);
......
}
④ 卡片数据更新
- 当需要卡片提供方更新数据时(如触发了定时更新、定点更新或者卡片使用方主动请求更新),卡片提供方获取最新数据,并调用 updateForm 接口更新卡片。示例如下:
@Override
protected void onUpdateForm(long formId) {
super.onUpdateForm(formId);
ComponentProvider componentProvider = new ComponentProvider(ResourceTable.Layout_form_ability_layout_2_2, this);
// 获取卡片实例需要更新的卡片数据,需要由开发者实现
String formData = getUpdateFormData(formId);
componentProvider.setText(ResourceTable.Id_title, "update formData-" + formData);
updateForm(formId, componentProvider);
......
}
- 卡片使用方点击拉起卡片页面,会在 onStart(Intent intent) 中携带 formId(通过 AbilitySlice.PARAM_FORM_IDENTITY_KEY 获取),若需要在 AbilitySlice 中更新,也可以使用 updateForm 接口进行更新,示例如下:
public class FormAbilitySlice extends AbilitySlice {
......
@Override
public void onStart(Intent intent) {
super.onStart(intent);
......
Button button = new Button(this);
button.setText("Update form data");
button.setClickedListener(component -> {
......
if (intent.hasParameter(AbilitySlice.PARAM_FORM_IDENTITY_KEY)) {
int formId = intent.getIntParam(AbilitySlice.PARAM_FORM_ID_KEY, -1);
ComponentProvider componentProvider = new ComponentProvider(ResourceTable.Layout_form_ability_layout_2_2, context);
String formData = getUpdateFormData(formId);
componentProvider.setText(ResourceTable.Id_modifylayout, "update formData-" + formData);
getAbility().updateForm(formId, componentProvider);
}
});
......
}
}
⑤ Java 卡片控制事件
- Java 卡片当前通过 IntentAgent 能力支持对卡片控制设置事件,例如可以使用 START_ABILITY、START_SERVICE 这两类能力,在点击整张卡片时,跳转到提供卡片的 ability。(注:Intent 中支持自定义参数的传递,支持的类型有 int/long/String/List)
- 示例如下:
@Override
protected ProviderFormInfo onCreateForm(Intent intent) {
......
ProviderFormInfo formInfo = new ProviderFormInfo(ResourceTable.Layout_form_ability_layout_2_2, this);
ComponentProvider componentProvider = new ComponentProvider();
// 针对title控件设置事件
componentProvider.setIntentAgent(ResourceTable.Id_title, startAbilityIntentAgent());
formInfo.mergeActions(componentProvider);
......
return formInfo;
}
// 设置触发的事件为系统预置的HarmonyOS betaApp应用
private IntentAgent startAbilityIntentAgent() {
Intent intent = new Intent();
Operation operation = new Intent.OperationBuilder()
.withDeviceId("")
.withBundleName("com.huawei.ohos.betaapp.link")
.withAbilityName("com.huawei.ohos.betaapp.link.MainAbility")
.build();
intent.setOperation(operation);
List<Intent> intentList = new ArrayList<>();
intentList.add(intent);
List<Flags> flags = new ArrayList<>();
flags.add(Flags.UPDATE_PRESENT_FLAG);
IntentAgentInfo paramsInfo = new IntentAgentInfo(200, IntentAgentConstant.OperationType.START_ABILITY, flags, intentList, null);
IntentAgent intentAgent = IntentAgentHelper.getIntentAgent(this, paramsInfo);
return intentAgent;
}
⑥ 开发 Java 卡片布局
- 在使用 DevEco Studio 创建模块时会生成对应的 Java UI xml 布局文件,需要注意设置ohos:remote=“true”。
- 以下是天气卡片 xml 布局示例,供参考:
<?xml version="1.0" encoding="utf-8"?>
<DependentLayout xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:width="match_parent"
ohos:height="match_parent"
ohos:id="$+id:background"
ohos:orientation="vertical"
ohos:background_element="$media:weather"
ohos:remote="true">
<Text
ohos:id="$+id:title"
ohos:text="天气1"
ohos:text_size="39px"
ohos:text_color="#b0c4de"
ohos:top_margin="42px"
ohos:left_margin="20px"
ohos:width="match_content"
ohos:height="match_content"/>
<Text
ohos:id="$+id:temperature"
ohos:text="35°"
ohos:text_size="100px"
ohos:text_color="#b0c4de"
ohos:top_margin="25px"
ohos:left_margin="20px"
ohos:below="$id:title"
ohos:width="match_content"
ohos:height="match_content"/>
<Text
ohos:id="$+id:location"
ohos:text="上海"
ohos:text_size="39px"
ohos:text_color="#b0c4de"
ohos:top_margin="24px"
ohos:left_margin="20px"
ohos:below="$id:temperature"
ohos:width="match_content"
ohos:height="match_content"/>
<Text
ohos:id="$+id:textView4"
ohos:text="9月4号 星期五"
ohos:text_size="39px"
ohos:text_color="#b0c4de"
ohos:top_margin="10px"
ohos:left_margin="20px"
ohos:below="$id:location"
ohos:width="match_content"
ohos:height="match_content"/>
<Text
ohos:id="$+id:textView5"
ohos:text="多云"
ohos:text_size="39px"
ohos:text_color="#b0c4de"
ohos:top_margin="10px"
ohos:left_margin="150px"
ohos:below="$id:location"
ohos:end_of="$id:textView4"
ohos:align_parent_end="true"
ohos:width="match_content"
ohos:height="match_content"/>
<Image
ohos:id="$+id:imageView"
ohos:width="160px"
ohos:height="150px"
ohos:top_margin="20px"
ohos:left_margin="150px"
ohos:below="$id:title"
ohos:end_of="$id:temperature"
ohos:image_src="$media:clouds"/>
</DependentLayout>
四、其他可选功能
① 卡片编辑功能
- 卡片提供方提供一个卡片的编辑页面,允许从卡片跳转至编辑页面。
- 卡片提供方在配置文件 config.json 中添加参数“formConfigAbility”,配置参数信息的规则如下:
-
- 配置该参数的值,导航到编辑页面的 Page ability,格式如下:ability://单个ability名字
-
- 如果不配置“formConfigAbility”参数,则无卡片编辑功能。
② 卡片背景模糊
- 卡片可以在 config.json 中声明是否支持背景模糊。声明方式如下:
config.json 的 metaData 中,在 customizeData 中增加一个 name 为 needBlurBackgroundForLauncher 的字符串类型的属性,value 为“true”表示支持, 否则为不支持。
"forms": [
{
"name": "Form_JS_DEMO",
"description": "it is js form",
"type": "JS",
"jsComponentName": "card",
"colorMode": "auto",
"isDefault": true,
"updateEnabled": true,
"scheduledUpdateTime": "10:30",
"defaultDimension": "2*2",
"supportDimensions": [
"2*2",
"2*4"
],
"metaData": {
"customizeData": [
{
"name": "needBlurBackgroundForLauncher",
"value": "true"
}
]
}
}
]
- 卡片开发者可以从 onCreateForm 的回调中,获取是否支持背景模糊:
protected ProviderFormInfo onCreateForm(Intent intent) {
......
// 1. 获取自定义数据
IntentParams intentParams = intent.getParam(AbilitySlice.PARAM_FORM_CUSTOMIZE_KEY);
// 2. 从intentParams中获取是否支持背景模糊
boolean isSupport = (boolean) intentParams.getParam("fa_card_background_blur_support");
......
}
- 背景模糊限制:
-
- 当前仅 JS 卡片支持背景模糊;
-
- 部分机型受性能限制,不支持卡片背景模糊。