HarmonyOS实战—原子化服务之服务卡片前端开发
本文正在参与“有奖征文 | HarmonyOS征文大赛” 活动链接
原子化服务
- 前言
- 一、什么是原子化服务?
-
- 1.概述
- 2.基本要素
- 二、原子化服务之服务卡片
-
- 1.服务卡片概述
- 2.服务卡片的运作机制
- 三、服务卡片前端混合开发
-
- 1.使用DevEco Studio创建卡片工程。
- 2.创建一个FormAbility,覆写卡片相关回调函数。
- 3.卡片信息持久化。
- 4.卡片数据交互。
- 5.开发JS卡片页面。
- 6.卡片尺寸适配
- 总结
前言
7月12日,华为官方宣布,HarmonyOS 开发者日定于7月31日在杭州举办,在新技术演讲环节中出现了 “你绝对想不到的”HarmonyOS卡片游戏,瞬间成为了大家关注的焦点。那么什么是HarmonyOS卡片的原理是什么呢,今天我们一起来学习下HarmonyOS实战—原子化服务之服务卡片(结尾有小彩蛋哦)
一、什么是原子化服务?
1.概述
原子化服务是 HarmonyOS 提供的一种面向未来的服务提供方式,是有独立入口的、免安装的、可为用户提供一个或多个便捷服务的用户程序形态。原子化服务兼容性强,可供用户在合适的场景、合适的设备上便捷使用。
原子化服务具有随处可及、服务免安装直达、分布式流转等特性。日常生活中,用户可以通过扫描 HarmonyOS Connect 标签、“碰一碰”设备来快速启动原子化服务,也可以在设备的服务中心和桌面上轻松找到他。
2.基本要素
-
基础信息
每个原子化服务有独立的图标、名称、描述、快照。基础信息将根据场景在服务中心、系统设置等界面展示。 -
服务卡片
为了给用户提供便捷、智能的服务体验,每个原子化服务都需要开发至少一个服务卡片,每个应用可选配置服务卡片。卡片作为服务的轻量承载,需要做到易用可见、智能可选和多端可变。
二、原子化服务之服务卡片
1.服务卡片概述
- FA:Feature Ability,元服务,代表有界面的Ability,用于与用户进行交互。
- 服务卡片(以下简称“卡片”)是FA的一种界面展示形式,将FA的重要信息或操作前置到卡片,将原子化服务/应用的重要信息以卡片的形式展示在桌面,用户可通过快捷手势使用卡片,通过轻量交互行为实现服务直达、减少层级跳转的目的。卡片常用于嵌入到其他应用中作为其界面的一部分显示,并支持拉起页面,发送消息等基础的交互功能。卡片使用方负责显示卡片。
- 服务卡片的核心理念在于提供用户容易使用且一目了然的信息内容,将智慧化能力融入到服务卡片的体验中供用户选择使用,同时满足在不同终端设备上的展示和自适应。
2.服务卡片的运作机制
服务卡片分为三方面:
-
卡片使用方
显示卡片内容的宿主应用,控制卡片在宿主中展示的位置。 -
卡片管理服务
用于管理系统中所添加卡片的常驻代理服务,包括卡片对象的管理与使用,以及卡片周期性刷新等。 -
卡片提供方
提供卡片显示内容的HarmonyOS应用或原子化服务,控制卡片的显示内容、控件布局以及控件点击事件。说明 卡片使用方和提供方不要求常驻运行,在需要添加/删除/请求更新卡片时,卡片管理服务会拉起卡片提供方获取卡片信息。
三、服务卡片前端混合开发
1.使用DevEco Studio创建卡片工程。
目录结构
.hml结尾的HML模板文件,这个文件用来描述卡片页面的模板布局结构。.css结尾的CSS样式文件,这个文件用于描述页面样式。.json结尾的JSON文件,这个文件用于配置卡片中使用的变量action事件。
各个文件夹的作用:
pages目录用于存放卡片模板页面。common目录用于存放公共资源文件,比如:图片资源。resources目录用于存放资源配置文件,比如:多分辨率加载配置文件。i18n目录用于配置不同语言场景资源内容,比如应用文本词条,图片路径等资源。
说明
i18n和resources是开发保留文件夹,不可重命名。JS服务卡片不同于JS应用使用js文件处理数据逻辑,卡片是通过卡片提供方应用处理数据并传递给卡片进行显示,卡片和卡片提供方应用间通过json配置文件约定相应的数据和事件交互接口,故不包含JS应用上的js文件。
创建成功后,在config.json配置示例如下:
{ "app": { "bundleName": "com.huawei.player", "version": { "code": 1, "name": "1.0" }, "vendor": "example" } "module": { ... "js": [{ "name": "myJsForm", "pages": [ "pages/index/index" ], "window": { "autoDesignWidth": true }, "type": "form" // 可选[normal(默认缺省), form] , 使能form类型 } ], "abilities": [{ ... "forms": [ { "name": "$string: form_name", //卡片名称,用于标识区分卡片 "isDefault": true, //是否为默认卡片,每个ability有且只能有一个默认卡片 "description": "$string: form_description", //卡片功能简介,不超过256个字符 "colorMode": "auto", //String类型,取值为auto、dark、light,标识支持的色调主题。 "supportDimensions":["1*2","2*2","2*4","4*4"], //卡片外观规格,一个卡片可以有多个规格。 "defaultDimension": "2*2", //缺省展现外观,不可缺省,取值必须在supportDimensions配置的列表中 "updateEnabled": true, //是否允许定时刷新 "scheduledUpdateTime": "10:30", //定点更新,采用24小时计数,精确到分钟 "updateDuration": 1, //更新频率;单位为30分钟的倍数。 "type": "JS", // Form类型可选 [Java, JS] "JsComponentName": "myJsForm" // 仅JS卡片时需要指定,需要和声明的JS Component名字对应 } ], "formsEnabled": true } ] } }]
说明
配置文件中,应注意如下配置:
- name、pages、window、type等标签配置需在配置文件中的“js”标签中完成设置
- pages列表中仅包含一个页面。
- 页面文件名不能使用组件名称,比如:text.hml、button.hml等。
- “js”模块中的name字段要与“forms”模块中的jsComponentName字段的值一致,为js资源的实例名。
- “forms”模块中的name为卡片名,即在onCreateForm中根据AbilitySlice.PARAM_FORM_NAME_KEY可取到的值。
- 卡片的Ability中还需要配置"visible": true和"formsEnabled": true。
- 定时刷新和定点刷新都配置的情况下,定时刷新优先。
- defaultDimension是默认规格,必须设置。
2.创建一个FormAbility,覆写卡片相关回调函数。
onCreateForm(Intent intent)onUpdateForm(long formId)onDeleteForm(long formId)onCastTempForm(long formId)onEventNotify(Map 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 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 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; }}
3.卡片信息持久化。
因大部分卡片提供方都不是常驻服务,只有在需要使用时才会被拉起获取卡片信息,且卡片管理服务支持对卡片进行多实例管理,卡片ID对应实例ID,因此若卡片提供方支持对卡片数据进行配置,则需要对卡片的业务数据按照卡片ID进行持久化管理,以便在后续获取、更新以及拉起时能获取到正确的卡片业务数据。且需要适配onDeleteForm(long formId)卡片删除通知接口,在其中实现卡片实例数据的删除。
-
常态卡片:卡片使用方会持久化的卡片;
-
临时卡片:卡片使用方不会持久化的卡片;
需要注意的是,卡片使用方在请求卡片时传递给提供方应用的Intent数据中存在临时标记字段,表示此次请求的卡片是否为临时卡片,由于临时卡片的数据具有非持久化的特殊性,某些场景比如卡片服务框架死亡重启,此时临时卡片数据在卡片管理服务中已经删除,且对应的卡片ID不会通知到提供方,所以卡片提供方需要自己负责清理长时间未删除的临时卡片数据。同时对应的卡片使用方可能会将之前请求的临时卡片转换为常态卡片。如果转换成功,卡片提供方也需要对对应的临时卡片ID进行处理,把卡片提供方记录的临时卡片数据转换为常态卡片数据,防止提供方在清理长时间未删除的临时卡片时,把已经转换为常态卡片的临时卡片信息删除,导致卡片信息丢失。
@Overrideprotected 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;}@Overrideprotected void onDeleteForm(long formId) { super.onDeleteForm(formId); // 由开发人员自行实现,删除卡片实例数据 deleteFormInfo(formId); ......}@Overrideprotected void onCastTempForm(long formId) { // 使用方将临时卡片转换为常态卡片触发,提供方需要做相应的处理 super.onCastTempForm (formId); ......}
4.卡片数据交互。
当卡片应用需要更新数据时(如触发了定时更新或定点更新),卡片应用获取最新数据,并调用updateForm接口更新卡片。示例如下:
@Overrideprotected 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 }}
5.开发JS卡片页面。
示例如下:
{{city}} {{temperature}} 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,包括router事件和message事件,其中router事件用于应用跳转,message事件用于卡片开发人员自定义点击事件。关键步骤说明如下:
在hml中为组件设置onclick属性,其值对应到json文件的actions字段中。
若设置router事件,则
action属性值为"router";
abilityName为卡片提供方应用的跳转目标Ability名;
params中的值按需填写,其值在使用时通过intent.getStringParam(“params”)获取即可;
若设置message事件,则action属性值为"message",params为json格式的值。
示例如下:
hml:{{city}}{{temperature}}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中。
6.卡片尺寸适配
window用于定义与显示窗口相关的配置。对于卡片尺寸适配问题,有2种配置方法,建议使用autoDesignWidth:
-
指定卡片designWidth 150px(2×2),所有与大小相关的样式(例如width、font-size)均以designWidth和实际卡片宽度的比例进行缩放,例如在designWidth为150时,如果设置width为100px时,在卡片实际宽度为300物理像素时,width实际渲染像素为200物理像素。
-
设置autoDesignWidth为true,此时designWidth字段将会被忽略,渲染组件和布局时按屏幕密度进行缩放。屏幕逻辑宽度由设备宽度和屏幕密度自动计算得出,在不同设备上可能不同,请使用相对布局来适配多种设备。例如:在466*466分辨率,320dpi的设备上,屏幕密度为2(以160dpi为基准),1px等于渲染出的2物理像素。
组件样式中类型的默认值,按屏幕密度进行计算和绘制, 如:在屏幕密度为2(以160dpi为基准)的设备上,默认为1px时,设备上实际渲染出2物理像素。 autoDesignWidth、designWidth的设置不影响默认值计算方式和绘制结果。
总结
7月12日,华为官方宣布,HarmonyOS 开发者日定于7月31日在杭州举办,在新技术演讲环节中出现了 “你绝对想不到的”HarmonyOS卡片游戏,瞬间成为了大家关注的焦点。而HarmonyOS卡片游戏就是依赖于原子化服务之服务卡片。所以作为开发者,心动不如行动。马上开始做个demo吧!
小编我有幸参加HarmonyOS 7月31日开发者日。届时我将给大家继续讲解。需要现场直播或者申请现场的可以关注我私信。我会一一回复的