- 1、概述
- 1.1、其他基本概念
- 1.2、运作机制
- 2、来体验一把HarmoneyOS当下火热的服务卡片(Services Widget)吧。
- 3、项目代码
- 4、运行
1、概述
服务卡片是FA(Feature Ability)的一种界面展示形式,将FA的重要信息或操作前置到卡片,以达到服务直达,减少体验层级目的。
可能有些小伙伴还不太理解服务卡片是个什么东西,其实我们在手机上已经接触过很多了,不管你用的什么手机,只要你滑到负1屏就可以看到我们的服务卡片
服务卡片目前仅支持系统应用,用于展示部分信息,并支持拉起页面,发送消息等功能,卡片使用方负责显示卡片。就拿菜鸟裹裹来说,我们取快递都需要用到身份码,而打开身份码往往需要多次操作才能完成,这无疑增加了不必要的逻辑操作,浪费了用户的时间。为了提高用户的体验,我们完全可以将用户经常需要使用的功能单独增加一个入口,而这个入口就是我们的服务卡片。
1.1、其他基本概念
- 卡片使用方
显示卡片内容的宿主应用,控制卡片在宿主中展示的位置。 - 卡片管理服务
用于管理系统中所添加卡片的常驻代理服务,包括卡片对象的管理与使用,以及卡片周期性刷新等。 - 卡片提供方
- 提供卡片显示内容的HarmoneyOS应用或原子化服务,控制卡片的显示内容、控件布局以及控件点击事件。
说明:卡片使用方和提供方不要求常驻运行,在需要添加/删除/请求更新卡片时,卡片管理服务会拉起卡片提供方获取卡片信息。
1.2、运作机制
。
简单分析一下这个图,以添加为例,卡片使用方发起添加请求,由卡片管理服务接收,然后拉起卡片提供方获取卡片服务,并将该卡片服务对象返回给卡片使用方。
卡片管理服务包含以下模块:
- 周期性刷新
在卡片添加后,根据卡片的刷新策略启动定时任务周期性触发卡片的刷新。 - 卡片缓存管理
在卡片添加到卡片管理服务后,对卡片的视图信息进行缓存,以便下次获取卡片时可以直接返回缓存数据,降低延时。 - 卡片生命周期管理
对于卡片切换到后台或者被遮挡住时,暂停卡片的刷新;以及卡片的升级/卸载场景下对卡片数据的更新和清理。 - 卡片使用方对卡片管理
对卡片使用方的RPC对象进行管理,用于使用方请求进行校验以及对卡片更新后的回调处理。 - 通信适配层
负责与卡片使用方和提供方进行RPC通信。
卡片提供方包含以下模块:
- 卡片服务
由卡片提供方开发者实现,开发者实现onCreateForm、onUpdateForm和onDeleteForm处理创建卡片、删除卡片以及更新卡片等请求,提供相应的卡片服务。 - 卡片提供方实例管理模块
由卡片提供方开发者实现,负责对负责对卡片管理服务分配的卡片实例进行持久化管理 - 通信适配层
由HarmoneyOS SDK提供,负责与卡片管理服务通信,用于将卡片的更新数据主动推送到卡片管理服务。
2、来体验一把HarmoneyOS当下火热的服务卡片(Services Widget)吧。
首先,我们需要先创建一个项目,还没有安装软件以及不会创建项目的小伙伴请参考超详细的DevEcoStudio安装教程以及我的第一个HarmoneyOS程序。
当我们创建好项目之后是这样的。
项目创建好之后,我们需要为其添加卡片服务,可参考官方文档:java卡片开发指导
具体操作如下:
找到我们java代码的根目录,右键新建一个服务卡片(Services Widget)
完成之后点击Finish即可。
卡片创建完成之后,在代码的根目录下会自动创建一个widget的文件夹
3、项目代码
卡片提供方:为卡片使用方的请求提供不同的接口,当卡片使用方请求获取卡片时,卡片提供方会被拉起并调用onCreateForm(Intent intent)回调,intent中会带有卡片ID,卡片名称,临时卡片标记和卡片外观规格信息,分别通过以下方式获取:
- AbilitySlice.PARAM_FORM_IDENTITY_KEY 获取卡片ID
- AbilitySlice.PARAM_FORM_NAME_KEY 获取卡片名称
- AbilitySlice.PARAM_FORM_TEMORARY_KEY 获取临时卡片标记
- AbilitySlice.PARAM_FORM_DIMENSION_KEY 获取卡片外观规格信息
提供方还可以通过AbilitySlice.PARAM_FORM_CUSTOMIZE_KEY获取卡片使用方设置的自定义数据
package com.example.myapplication;
import com.example.myapplication.slice.MainAbilitySlice;
import com.example.myapplication.widget.controller.*;
import ohos.aafwk.ability.Ability;
import ohos.aafwk.ability.AbilitySlice;
import ohos.aafwk.ability.ProviderFormInfo;
import ohos.aafwk.content.Intent;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
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);
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();
}
}
FormController抽象类
package com.example.myapplication.widget.controller;
import ohos.aafwk.ability.AbilitySlice;
import ohos.aafwk.ability.ProviderFormInfo;
import ohos.aafwk.content.Intent;
import ohos.app.Context;
/**
* The api set for form controller.
*/
public abstract class FormController {
/**
* Context of ability
*/
protected final Context context;
/**
* The name of current form service widget
*/
protected final String formName;
/**
* The dimension of current form service widget
*/
protected final int dimension;
public FormController(Context context, String formName, Integer dimension) {
this.context = context;
this.formName = formName;
this.dimension = dimension;
}
/**
* Bind data for a form
*
* @return ProviderFormInfo
*/
public abstract ProviderFormInfo bindFormData();
/**
* Update form data
*
* @param formId the id of service widget to be updated
* @param vars the data to update for service widget, this parameter is optional
*/
public abstract void updateFormData(long formId, Object... vars);
/**
* Called when receive service widget message event
*
* @param formId form id
* @param message the message context sent by service widget message event
*/
public abstract void onTriggerFormEvent(long formId, String message);
/**
* Get the destination ability slice to route
*
* @param intent intent of current page slice
* @return the destination ability slice name to route
*/
public abstract Class<? extends AbilitySlice> getRoutePageSlice(Intent intent);
}
**FormControllerManager类 **
package com.example.myapplication.widget.controller;
import ohos.app.Context;
import ohos.data.DatabaseHelper;
import ohos.data.preferences.Preferences;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.utils.zson.ZSONObject;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
/**
* Form controller manager.
*/
public class FormControllerManager {
private static final HiLogLabel TAG = new HiLogLabel(HiLog.DEBUG, 0x0, FormControllerManager.class.getName());
private static final String PACKAGE_PATH = "com.example.myapplication.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;
/**
* Constructor with context.
*
* @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);
}
/**
* Singleton mode.
*
* @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;
}
/**
* Save the form id and form name.
*
* @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;
}
}
/**
* Get the form controller instance.
*
* @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;
}
/**
* Get all form id from the share preference
*
* @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;
}
/**
* Delete a form controller
*
* @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";
}
}
**WidgetImpl类,实现了FormController抽象类 **
package com.example.myapplication.widget.widget;
import com.example.myapplication.ResourceTable;
import com.example.myapplication.widget.controller.FormController;
import ohos.aafwk.ability.AbilitySlice;
import ohos.aafwk.ability.ProviderFormInfo;
import ohos.aafwk.content.Intent;
import ohos.app.Context;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import java.util.HashMap;
import java.util.Map;
public class WidgetImpl extends FormController {
private static final HiLogLabel TAG = new HiLogLabel(HiLog.DEBUG, 0x0, WidgetImpl.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(DEFAULT_DIMENSION_2X2, ResourceTable.Layout_form_image_with_information_widget_2_2);
}
public WidgetImpl(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;
}
}
4、运行
在应用图片处上滑,属于你的个人卡片就出现了,是不是很有趣呢😄