最近看到很多博客网站上出现了HarmonyOS的征文活动,看到那些精美的奖品让我也安耐不住开了,当然奖品的诱惑当然是抵挡不住我对技术的狂热追求,对于开发者而言技术没有顶峰没有终点。那么今天给大家做一个卡片服务开发的经验分享,如果有什么地方说的不对的请各位开发者进行指正,如果有什么问题也可以发私信或者直接在帖子中留言,我也会及时回复大家。
一、什么是服务卡片
服务卡片(以下简称“卡片”)是FA的一种界面展示形式,将FA的重要信息或操作直接放置到卡片中,用户通过直接操作卡片就可以达到应用的使用体验,这样做大大减少了应用的使用层级性。
卡片常用于嵌入到其他应用中作为其界面的一部分显示(也可以使用原子化服务将应用保存到服务中心中,这种方式不需要安装应用),并支持拉起页面,发送消息等基础的交互功能。
原子化服务在下个帖子中介绍。
示例如下图所示。
为了开发者能够便于理解,官方将服务卡片分为三方面:
卡片使用方
显示卡片内容的宿主应用,控制卡片在宿主中展示的位置。
卡片管理服务
用于管理系统中所添加卡片的常驻代理服务,包括卡片对象的管理与使用,以及卡片周期性刷新等。
卡片提供方
提供卡片显示内容的HarmonyOS应用或原子化服务,控制卡片的显示内容、控件布局以及控件点击事件。
卡片使用方和提供方不要求常驻运行,在需要添加/删除/请求更新卡片时,卡片管理服务会拉起卡片提供方获取卡片信息。
二、服务卡片的运作机制
文字描述滞后,先上图。(图片由官网提供,此处借用一下)
从图中可以清楚的看到服务卡片的整体通信层都是由"RPC"负责,通过通信适配层构成了数据发送接收的通道。
卡片管理服务
周期性刷新:在卡片添加后,根据卡片的刷新策略启动定时任务周期性触发卡片的刷新。
卡片缓存管理:在卡片添加到卡片管理服务后,对卡片的视图信息进行缓存,以便下次获取卡片时可以直接返回缓存数据,降低时延。
卡片生命周期管理:对于卡片切换到后台或者被遮挡时,暂停卡片的刷新;以及卡片的升级/卸载场景下对卡片数据的更新和清理。
卡片使用方对象管理:对卡片使用方的RPC对象进行管理,用于使用方请求进行校验以及对卡片更新后的回调处理。
卡片提供方
创建卡片实例并实现onCreateForm、onUpdateForm和onDeleteForm处理创建卡片、更新卡片以及删除卡片等请求,提供相应的卡片服务。
三、服务卡片的实现(java方式)
第一步、下载、安装、配置 DevEco Studio
点击查看
第二步、运行DevEco Studio创建新项目
因为要使用java语言编写,所以此处要选择支持Java语言的空页面。
选择成功进入新建项目配置界面。
点击Finish进入项目。
第三步、添加卡片模板
创建模板:右击“Entry”→“New”→“Server Widget”
设置模板
第四步、查看卡片服务配置
项目目录
由图可见:添加卡片模板后在原来项目中增加一个widget文件夹,并在文件夹中出现了三个文件FormControllerManager.java、FormController.java、CardWidgetImpl.java。三个文件对应了卡片服务的运行机制,通过关系的对应可以清楚的了解到代码运行原理。
FormControllerManager.java(卡片控制器管理【卡片管理服务】)
public class FormControllerManager { private static final HiLogLabel TAG = new HiLogLabel(HiLog.DEBUG, 0x0, FormControllerManager.class.getName()); private static final String PACKAGE_PATH = "com.example.carddemo.widget"; private static final String SHARED_SP_NAME = "form_info_sp.xml"; private static final String FORM_NAME = "formName"; private static final String DIMENSION = "dimension"; private static FormControllerManager managerInstance = null; private final HashMap<Long, FormController> controllerHashMap = new HashMap<>(); private final Context context; private final Preferences preferences; /** * 初始化构造器 * * @param context instance of Context. */ private FormControllerManager(Context context) { this.context = context; DatabaseHelper databaseHelper = new DatabaseHelper(this.context.getApplicationContext()); preferences = databaseHelper.getPreferences(SHARED_SP_NAME); } /** * FormControllerManager 实例化 * * @param context instance of Context. * @return FormControllerManager instance. */ public static FormControllerManager getInstance(Context context) { if (managerInstance == null) { synchronized (FormControllerManager.class) { if (managerInstance == null) { managerInstance = new FormControllerManager(context); } } } return managerInstance; } /** * 通过构造器将传入的卡片信息进行封装,返回卡片服务 * * @param formId form id. * @param formName form name. * @param dimension form dimension * @return FormController form controller */ public FormController createFormController(long formId, String formName, int dimension) { synchronized (controllerHashMap) { if (formId < 0 || formName.isEmpty()) { return null; } HiLog.info(TAG, "saveFormId() formId: " + formId + ", formName: " + formName + ", preferences: " + preferences); if (preferences != null) { ZSONObject formObj = new ZSONObject(); formObj.put(FORM_NAME, formName); formObj.put(DIMENSION, dimension); preferences.putString(Long.toString(formId), ZSONObject.toZSONString(formObj)); preferences.flushSync(); } // Create controller instance. FormController controller = newInstance(formName, dimension, context); // Cache the controller. if (controller != null) { if (!controllerHashMap.containsKey(formId)) { controllerHashMap.put(formId, controller); } } return controller; } } /** * 获取卡片控制器实例 * * @param formId form id. * @return the instance of form controller. */ public FormController getController(long formId) { synchronized (controllerHashMap) { if (controllerHashMap.containsKey(formId)) { return controllerHashMap.get(formId); } Map<String, ?> forms = preferences.getAll(); String formIdString = Long.toString(formId); if (forms.containsKey(formIdString)) { ZSONObject formObj = ZSONObject.stringToZSON((String) forms.get(formIdString)); String formName = formObj.getString(FORM_NAME); int dimension = formObj.getIntValue(DIMENSION); FormController controller = newInstance(formName, dimension, context); controllerHashMap.put(formId, controller); } return controllerHashMap.get(formId); } } private FormController newInstance(String formName, int dimension, Context context) { FormController ctrInstance = null; if (formName == null || formName.isEmpty()) { HiLog.error(TAG, "newInstance() get empty form name"); return ctrInstance; } try { String className = PACKAGE_PATH + "." + formName.toLowerCase(Locale.ROOT) + "." + getClassNameByFormName(formName); Class<?> clazz = Class.forName(className); if (clazz != null) { Object controllerInstance = clazz.getConstructor(Context.class, String.class, Integer.class) .newInstance(context, formName, dimension); if (controllerInstance instanceof FormController) { ctrInstance = (FormController) controllerInstance; } } } catch (NoSuchMethodException | InstantiationException | IllegalArgumentException | InvocationTargetException | IllegalAccessException | ClassNotFoundException | SecurityException exception) { HiLog.error(TAG, "newInstance() get exception: " + exception.getMessage()); } return ctrInstance; } /** * 从数组中获取所有卡片id * * @return form id list */ public List<Long> getAllFormIdFromSharePreference() { List<Long> result = new ArrayList<>(); Map<String, ?> forms = preferences.getAll(); for (String formId : forms.keySet()) { result.add(Long.parseLong(formId)); } return result; } /** * 删除卡片服务 * * @param formId form id */ public void deleteFormController(long formId) { synchronized (controllerHashMap) { preferences.delete(Long.toString(formId)); preferences.flushSync(); controllerHashMap.remove(formId); } } private String getClassNameByFormName(String formName) { String[] strings = formName.split("_"); StringBuilder result = new StringBuilder(); for (String string : strings) { result.append(string); } char[] charResult = result.toString().toCharArray(); charResult[0] = (charResult[0] >= 'a' && charResult[0] <= 'z') ? (char) (charResult[0] - 32) : charResult[0]; return String.copyValueOf(charResult) + "Impl"; } }
FormController.java(卡片控制器【卡片提供方】)
public abstract class FormController { protected final Context context; protected final String formName; protected final int dimension; public FormController(Context context, String formName, Integer dimension) { this.context = context; this.formName = formName; this.dimension = dimension; } /** * 创建卡片信息提供者 */ public abstract ProviderFormInfo bindFormData(); /** * 更新卡片信息 */ public abstract void updateFormData(long formId, Object... vars); /** * 在接收服务小部件消息事件时调用 */ public abstract void onTriggerFormEvent(long formId, String message); /** * 获取路由的目标界面 */ public abstract Class<? extends AbilitySlice> getRoutePageSlice(Intent intent); }
CardWidgetImpl.java(卡片应用【卡片使用方】)
public class CardWidgetImpl extends FormController { public static final int DIMENSION_1X2 = 1; public static final int DIMENSION_2X4 = 3; private static final HiLogLabel TAG = new HiLogLabel(HiLog.DEBUG, 0x0, CardWidgetImpl.class.getName()); private static final int DEFAULT_DIMENSION_2X2 = 2; private static final Map<Integer, Integer> RESOURCE_ID_MAP = new HashMap<>(); static { RESOURCE_ID_MAP.put(DIMENSION_1X2, ResourceTable.Layout_form_grid_pattern_cardwidget_1_2); RESOURCE_ID_MAP.put(DEFAULT_DIMENSION_2X2, ResourceTable.Layout_form_grid_pattern_cardwidget_2_2); RESOURCE_ID_MAP.put(DIMENSION_2X4, ResourceTable.Layout_form_grid_pattern_cardwidget_2_4); } public CardWidgetImpl(Context context, String formName, Integer dimension) { super(context, formName, dimension); } //创建好卡片服务后,在界面展示卡片 @Override public ProviderFormInfo bindFormData() { HiLog.info(TAG, "bind form data when create form"); return new ProviderFormInfo(RESOURCE_ID_MAP.get(dimension), context); } //更新卡片信息 @Override public void updateFormData(long formId, Object... vars) { HiLog.info(TAG, "update form data timing, default 30 minutes"); } //卡片中内容手势触发方法 @Override public void onTriggerFormEvent(long formId, String message) { HiLog.info(TAG, "handle card click event."); } @Override public Class<? extends AbilitySlice> getRoutePageSlice(Intent intent) { HiLog.info(TAG, "get the default page to route when you click card."); return null; } }
MainAbility的变化,新增了一些方法。
public class MainAbility extends Ability { public static final int DEFAULT_DIMENSION_2X2 = 2; public static final int DIMENSION_1X2 = 1; public static final int DIMENSION_2X4 = 3; public static final int DIMENSION_4X4 = 4; private static final int INVALID_FORM_ID = -1; private static final HiLogLabel TAG = new HiLogLabel(HiLog.DEBUG, 0x0, MainAbility.class.getName()); private String topWidgetSlice; @Override public void onStart(Intent intent) { super.onStart(intent); super.setMainRoute(MainAbilitySlice.class.getName()); if (intentFromWidget(intent)) { topWidgetSlice = getRoutePageSlice(intent); if (topWidgetSlice != null) { setMainRoute(topWidgetSlice); } } stopAbility(intent); } //创建卡片信息并返回卡片内容 @Override protected ProviderFormInfo onCreateForm(Intent intent) { HiLog.info(TAG, "onCreateForm"); long formId = intent.getLongParam(AbilitySlice.PARAM_FORM_IDENTITY_KEY, INVALID_FORM_ID); String formName = intent.getStringParam(AbilitySlice.PARAM_FORM_NAME_KEY); int dimension = intent.getIntParam(AbilitySlice.PARAM_FORM_DIMENSION_KEY, DEFAULT_DIMENSION_2X2); HiLog.info(TAG, "onCreateForm: formId=" + formId + ",formName=" + formName); FormControllerManager formControllerManager = FormControllerManager.getInstance(this); FormController formController = formControllerManager.getController(formId); //通过调用布局Id将卡片布局与卡片信息进行绑定 formController = (formController == null) ? formControllerManager.createFormController(formId, formName, dimension) : formController; if (formController == null) { HiLog.error(TAG, "Get null controller. formId: " + formId + ", formName: " + formName); return null; } return formController.bindFormData(); } //更新卡片信息 @Override protected void onUpdateForm(long formId) { HiLog.info(TAG, "onUpdateForm"); super.onUpdateForm(formId); FormControllerManager formControllerManager = FormControllerManager.getInstance(this); FormController formController = formControllerManager.getController(formId); formController.updateFormData(formId); } //删除卡片 @Override protected void onDeleteForm(long formId) { HiLog.info(TAG, "onDeleteForm: formId=" + formId); super.onDeleteForm(formId); FormControllerManager formControllerManager = FormControllerManager.getInstance(this); formControllerManager.deleteFormController(formId); } //卡片手势触发 @Override protected void onTriggerFormEvent(long formId, String message) { HiLog.info(TAG, "onTriggerFormEvent: " + message); super.onTriggerFormEvent(formId, message); FormControllerManager formControllerManager = FormControllerManager.getInstance(this); FormController formController = formControllerManager.getController(formId); formController.onTriggerFormEvent(formId, message); } @Override public void onNewIntent(Intent intent) { if (intentFromWidget(intent)) { // Only response to it when starting from a service widget. String newWidgetSlice = getRoutePageSlice(intent); if (topWidgetSlice == null || !topWidgetSlice.equals(newWidgetSlice)) { topWidgetSlice = newWidgetSlice; restart(); } } } private boolean intentFromWidget(Intent intent) { long formId = intent.getLongParam(AbilitySlice.PARAM_FORM_IDENTITY_KEY, INVALID_FORM_ID); return formId != INVALID_FORM_ID; } //跳转至对应的卡片界面 private String getRoutePageSlice(Intent intent) { long formId = intent.getLongParam(AbilitySlice.PARAM_FORM_IDENTITY_KEY, INVALID_FORM_ID); if (formId == INVALID_FORM_ID) { return null; } FormControllerManager formControllerManager = FormControllerManager.getInstance(this); FormController formController = formControllerManager.getController(formId); if (formController == null) { return null; } Class<? extends AbilitySlice> clazz = formController.getRoutePageSlice(intent); if (clazz == null) { return null; } return clazz.getName(); } }
配置文件
属性名称 |
子属性名称 |
含义 |
数据类型 |
name |
表示卡片的类名。字符串最大长度为127字节。 |
字符串 |
|
description |
表示卡片的描述。取值可以是描述性内容,也可以是对描述性内容的资源索引,以支持多语言。字符串最大长度为255字节。 |
字符串 |
|
isDefault |
表示该卡片是否为默认卡片,每个Ability有且只有一个默认卡片。
|
布尔值 |
|
type |
表示卡片的主题样式,取值范围如下:
|
字符串 |
|
colorMode |
表示卡片的主题样式,取值范围如下:
|
字符串 |
|
supportDimensions |
表示卡片支持的外观规格,取值范围:
|
字符串数组 |
|
defaultDimension |
表示卡片的默认外观规格,取值必须在该卡片supportDimensions配置的列表中。 |
字符串 |
|
landscapeLayouts |
表示卡片外观规格对应的横向布局文件,与supportDimensions中的规格一一对应。 仅当卡片类型为Java卡片时,需要配置该标签。 |
字符串数组 |
|
portraitLayouts |
表示卡片外观规格对应的竖向布局文件,与supportDimensions中的规格一一对应。 仅当卡片类型为Java卡片时,需要配置该标签。 |
字符串数组 |
|
updateEnabled |
表示卡片是否支持周期性刷新,取值范围:
|
布尔类型 |
|
scheduledUpdateTime |
表示卡片的定点刷新的时刻,采用24小时制,精确到分钟。 |
字符串 |
|
updateDuration |
表示卡片定时刷新的更新周期,单位为30分钟,取值为自然数。
|
数值 |
|
formConfigAbility |
表示卡片的配置跳转链接,采用URI格式。 |
字符串 |
|
jsComponentName |
表示JS卡片的Component名称。字符串最大长度为127字节。 仅当卡片类型为JS卡片时,需要配置该标签。 |
字符串 |
|
metaData |
表示卡片的自定义信息,包含customizeData数组标签。 |
对象 |
|
customizeData |
表示自定义的卡片信息。 |
对象数组 |
|
name |
表示数据项的键名称。字符串最大长度为255字节。 |
字符串 |
|
value |
表示数据项的值。字符串最大长度为255字节。 |
字符串 |
第五步、编写卡片界面
分别修改form_grid_pattern_cardwidget_1_2、form_grid_pattern_cardwidget_2_2.xml和form_grid_pattern_cardwidget_2_4.xml文件。
form_grid_pattern_cardwidget_1_2.xml
<?xml version="1.0" encoding="utf-8"?> <DependentLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:height="match_parent" ohos:width="match_parent" ohos:remote="true"> <Image ohos:height="match_parent" ohos:width="match_parent" ohos:background_element="#1281f0" ohos:scale_mode="clip_center"/> <DirectionalLayout ohos:height="match_parent" ohos:width="match_parent" ohos:alignment="center" ohos:orientation="horizontal"> <Text ohos:id="$+id:tv_name" ohos:height="match_content" ohos:width="match_content" ohos:text="$string:cardwidget_title" ohos:text_color="#E5FFFFFF" ohos:text_size="16fp" ohos:text_weight="1200"/> <Text ohos:height="match_content" ohos:width="match_content" ohos:text="3350" ohos:text_color="#E5FFFFFF" ohos:text_size="16fp" ohos:text_weight="1200"/> </DirectionalLayout> </DependentLayout>
form_grid_pattern_cardwidget_2_2.xml
<?xml version="1.0" encoding="utf-8"?> <DirectionalLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:height="match_parent" ohos:width="match_parent" ohos:background_element="#FFFFFFFF" ohos:remote="true" ohos:orientation="vertical"> <DirectionalLayout ohos:id="$+id:tv_top" ohos:height="match_parent" ohos:width="match_parent" ohos:weight="1" ohos:orientation="horizontal"> <DirectionalLayout ohos:height="match_parent" ohos:width="match_parent" ohos:weight="1" ohos:orientation="vertical" ohos:alignment="center"> <Text ohos:height="match_content" ohos:width="match_content" ohos:text="消耗热量" ohos:top_margin="5vp" ohos:text_size="10fp" ohos:text_weight="500"> </Text> <Text ohos:height="60vp" ohos:width="60vp" ohos:text="2.1 Kcal" ohos:text_alignment="center" ohos:text_color="#1281f0" ohos:background_element="$media:aa_1" ohos:text_size="10fp" ohos:text_weight="500"/> </DirectionalLayout> <DirectionalLayout ohos:height="match_parent" ohos:width="match_parent" ohos:weight="1" ohos:orientation="vertical" ohos:alignment="center"> <Text ohos:height="match_content" ohos:width="match_content" ohos:text="实时心率" ohos:top_margin="5vp" ohos:text_size="10fp" ohos:text_weight="500"> </Text> <Text ohos:height="60vp" ohos:width="60vp" ohos:text="99/min" ohos:text_alignment="center" ohos:text_color="#1281f0" ohos:background_element="$media:aa_1" ohos:text_size="10fp" ohos:text_weight="500"/> </DirectionalLayout> </DirectionalLayout> <DirectionalLayout ohos:id="$+id:tv_blow" ohos:height="60vp" ohos:width="match_parent" ohos:align_parent_bottom="true" ohos:background_element="#1281f0" ohos:alignment="center" ohos:orientation="vertical"> <DirectionalLayout ohos:height="match_content" ohos:width="match_parent" ohos:alignment="center" ohos:orientation="horizontal"> <Text ohos:id="$+id:tv_name" ohos:height="match_content" ohos:width="match_content" ohos:text="$string:cardwidget_title" ohos:text_color="#E5FFFFFF" ohos:text_size="16fp" ohos:text_weight="600"/> <Text ohos:height="match_content" ohos:width="match_content" ohos:text="3350" ohos:text_color="#E5FFFFFF" ohos:text_size="16fp" ohos:text_weight="1000"/> </DirectionalLayout> <Text ohos:height="match_content" ohos:width="match_content" ohos:text="$string:cardwidget_introduction" ohos:text_color="#ffffff" ohos:text_size="14fp" ohos:text_weight="800" ohos:top_margin="7vp"/> </DirectionalLayout> </DirectionalLayout>
form_grid_pattern_cardwidget_2_4.xml
<?xml version="1.0" encoding="utf-8"?> <DirectionalLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:height="match_parent" ohos:width="match_parent" ohos:background_element="#FFFFFFFF" ohos:orientation="horizontal" ohos:remote="true"> <DirectionalLayout ohos:height="match_parent" ohos:width="match_parent" ohos:weight="1" ohos:orientation="vertical"> <DirectionalLayout ohos:height="match_parent" ohos:width="match_parent" ohos:weight="1" ohos:orientation="horizontal"> <DirectionalLayout ohos:height="match_parent" ohos:width="match_parent" ohos:weight="1" ohos:orientation="vertical" ohos:alignment="center"> <Text ohos:height="match_content" ohos:width="match_content" ohos:text="消耗热量" ohos:top_margin="5vp" ohos:text_size="10fp" ohos:text_weight="500"> </Text> <Text ohos:height="60vp" ohos:width="60vp" ohos:text="2.1 Kcal" ohos:text_alignment="center" ohos:text_color="#1281f0" ohos:background_element="$media:aa_1" ohos:text_size="10fp" ohos:text_weight="500"/> </DirectionalLayout> <DirectionalLayout ohos:height="match_parent" ohos:width="match_parent" ohos:weight="1" ohos:orientation="vertical" ohos:alignment="center"> <Text ohos:height="match_content" ohos:width="match_content" ohos:text="实时心率" ohos:top_margin="5vp" ohos:text_size="10fp" ohos:text_weight="500"> </Text> <Text ohos:height="60vp" ohos:width="60vp" ohos:text="99/min" ohos:text_alignment="center" ohos:text_color="#1281f0" ohos:background_element="$media:aa_1" ohos:text_size="10fp" ohos:text_weight="500"/> </DirectionalLayout> </DirectionalLayout> <DirectionalLayout ohos:height="60vp" ohos:width="match_parent" ohos:align_parent_bottom="true" ohos:background_element="#1281f0" ohos:alignment="center" ohos:orientation="vertical"> <DirectionalLayout ohos:height="match_content" ohos:width="match_parent" ohos:alignment="center" ohos:orientation="horizontal"> <Text ohos:height="match_content" ohos:width="match_content" ohos:text="$string:cardwidget_title" ohos:text_color="#E5FFFFFF" ohos:text_size="16fp" ohos:text_weight="600"/> <Text ohos:height="match_content" ohos:width="match_content" ohos:text="3350" ohos:text_color="#E5FFFFFF" ohos:text_size="16fp" ohos:text_weight="1000"/> </DirectionalLayout> <Text ohos:height="match_content" ohos:width="match_content" ohos:text="$string:cardwidget_introduction" ohos:text_color="#ffffff" ohos:text_size="14fp" ohos:text_weight="800" ohos:top_margin="7vp"/> </DirectionalLayout> </DirectionalLayout> <DirectionalLayout ohos:height="match_parent" ohos:width="match_parent" ohos:weight="1"> </DirectionalLayout> </DirectionalLayout>
第六步、运行程序、查看效果
点击图标上划,出现卡片。
点击右上角图钉按钮,将卡片放在屏幕中。
长按应用,出现服务卡片。
点击服务卡片选择界面,上下滑动可选择卡片内容。
点击添加到桌面,则将卡片添加到桌面中。
至此,卡片服务应用就以全部开发完成,后续会对卡片内部进行相关编写。使其进行动态的刷新及动态的获取数据。
跳转项目地址
喜欢的请为作者点个喜欢或者赞赏~