目录
一、时钟卡片简介:
二、开发环境搭建:
第一步:创建一个空的工程文件:
使用JS开发卡片:
标签”forms”中卡片的配置:
卡片的定点/定时刷新:
时钟卡片配置文件:
卡片布局:
创建卡片:
创建卡片数据库:
卡片应用初始化:
更新卡片:
前台service
卡片组件更新
一、时钟卡片简介:
服务卡片是FA的一种界面展示形式,将FA重要信息或操作前置到卡片,以此达到服务直达、减少层级体验的目的。
这里将介绍如何在HarmonyOS上开发一个时钟类FA卡片应用,该卡片包含2*2、2*4两种布局样式,卡片应用在桌面上的显示效果如下:
二、开发环境搭建:
安装和配置DevEco Studio,详情请参考我的入门篇博客:下载和安装软件
这次实战使用的是DevEco Studio版本为DevEco Studio 2.1 Beta4,使用的SDK版本为API Version 5
第一步:创建一个空的工程文件:
创建一个HarmonyOS的工程,然后选择模板Empty Ability(JS)或Empty Ability(Java),点击按钮Next,进入到工程配置界面:(这里,使用的是SDK版本为API Version 5)
其中,工程类型有两种:一种是”Service”,也就是原子化服务;另一种是”Application”,也就是传统的应用。此外,还可以选择”是否在服务中心进行展示”。我们将工程类型指定为”原子化服务”,并且选择”在服务中心进行展示”。按照上图进行配置之后,点击按钮Finish以创建一个工程。
连接远程设备:
开启远程设备:
使用JS开发卡片:
打开DevEco Studio,创建一个HarmonyOS的工程,然后选择模板Empty Ability(JS),点击按钮Next,进入到工程配置界面:
其中,将工程类型指定为传统应用,并且不选择”在服务中心进行展示”。按照上图进行配置之后,点击按钮Finish以创建一个工程。
如何在一个传统应用的工程中创建卡片呢?
在目录entry上点击右键,在弹出的菜单中选择New,然后在弹出的子菜单中点击Service Widget:
这里的Service Widget指的就是卡片。
在模板选择界面,选择基本的模板Grid Pattern,点击按钮Next,进入到卡片配置界面:
首先配置卡片的名称和描述;然后配置卡片关联的Page Ability;然后配置卡片的编程语言类型是JS;接下来配置卡片的JS组件名称;最后配置卡片支持的规格,其中,2*2的小尺寸是必须要支持的,我们再勾选一个1*2的微尺寸。点击按钮Finish以创建一个卡片。
再创建2*4的中尺寸和4*4的大尺寸:
DevEco Studio会自动帮我们生成了一些目录和文件:
- widget01和widget02是创建卡片时配置的JS组件名称
- index.hml中定义了卡片中包含哪些UI组件;index.css中定义了卡片中的UI组件样式
- index.json中定义了卡片中动态绑定的数据,此外,还可以定义click触发事件
widget01和widget01是创建卡片时配置的卡片名称;在MainAbility中添加了卡片的生命周期回调方法,如:onCreateForm()、onUpdateForm()、onTriggerFormEvent()、等。 还添加了FormControllerManager、FormController、以及两个以Impl结尾的实现类。
在config.json中自动添加了很多配置:
{
"app": {
"bundleName": "com.example.demo",
"vendor": "example",
"version": {
"code": 1000000,
"name": "1.0.0"
}
},
"deviceConfig": {},
"module": {
"package": "com.example.demo",
"name": ".MyApplication",
"mainAbility": "com.example.demo.MainAbility",
"deviceType": [
"phone"
],
"distro": {
"deliveryWithInstall": true,
"moduleName": "entry",
"moduleType": "entry",
"installationFree": true
},
"abilities": [
{
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
}
],
"name": "com.example.demo.MainAbility",
"icon": "$media:icon",
"description": "$string:mainability_description",
"label": "$string:entry_MainAbility",
"type": "page",
"launchType": "standard",
"formsEnabled": true,
"forms": [
{
"jsComponentName": "widget01",
"isDefault": true,
"scheduledUpdateTime": "10:30",
"defaultDimension": "2*2",
"name": "widget01",
"description": "This is a service widget",
"colorMode": "auto",
"type": "JS",
"supportDimensions": [
"1*2",
"2*2"
],
"updateEnabled": true,
"updateDuration": 1
},
{
"jsComponentName": "widget02",
"isDefault": false,
"scheduledUpdateTime": "10:30",
"defaultDimension": "2*2",
"name": "widget02",
"description": "This is a service widget",
"colorMode": "auto",
"type": "JS",
"supportDimensions": [
"2*2",
"2*4",
"4*4"
],
"updateEnabled": true,
"updateDuration": 1
}
]
}
],
"js": [
{
"pages": [
"pages/index/index"
],
"name": "default",
"window": {
"designWidth": 720,
"autoDesignWidth": true
}
},
{
"pages": [
"pages/index/index"
],
"name": "widget01",
"window": {
"designWidth": 720,
"autoDesignWidth": true
},
"type": "form"
},
{
"pages": [
"pages/index/index"
],
"name": "widget02",
"window": {
"designWidth": 720,
"autoDesignWidth": true
},
"type": "form"
}
]
}
}
MainAbility添加了标签”forms”,这里的form就是卡片的意思,和Service Widget是一回事儿。”forms”是一个数组,包含两个元素,分别表示我们创建的两个卡片。顺便提一下,在前面我们有讲到:“可以在config.json中为每个Page Ability配置0~16个卡片”,也就是说,数组”forms”中最多可以包含16个元素。上图的最下方还添加了一个标签”js”,”js”也是一个数组,包含三个元素,其中后两个元素就是两个卡片对应的js组件,”name”分别是”widget01”和”widget02”,这两个值就对应着上面的标签”forms”中”jsComponentName”的两个值。也就是说,上面的标签”forms”中卡片的js组件,是在下面的标签”js”中定义的。
标签”forms”中卡片的配置:
- “isDefault”表示该卡片是否为默认的上滑卡片,也就是用手指按下应用图标的同时往上滑时弹出的卡片。
- “scheduledUpdateTime”表示卡片定点刷新的时刻,采用24小时制,精确到分钟。
- “defaultDimension“表示卡片的默认尺寸规格,取值必须在下面的“supportDimensions“所配置的列表中。
- “colorMode“表示卡片的主题样式,默认值是”auto”,表示自适应,还可以取值为”dark”或”light”,分别表示深色主题和浅色主题。
- “supportDimensions“表示卡片支持的尺寸规格,也就是我们在创建卡片时配置的尺寸规格。
- “updateEnabled”表示卡片是否支持定时刷新或定点刷新,优先选择定时刷新。
- “updateDuration“表示卡片定时刷新的周期。当取值为0时,表示该参数不生效;当取值为正整数N时,表示刷新周期为30*N分钟。
由于该工程的类型是”原子化服务”,所以在桌面上并没有相应的图标。由于在创建工程时选择了”在服务中心进行展示”,因此,打开服务中心,就看到了相应的入口卡片:
上下滑动所有卡片,总共有5个:
- widget01的卡片有两个,尺寸分别是1*2和2*2
- widget02的卡片有三个,尺寸分别是2*2、2*4和4*4
widget1的2*2的卡片被设为了上滑卡片,这是因为:在config.json中,将”isDefault”设为了”true”,并且将”defaultDimension”设为了” 2*2”:
"forms": [
{
"jsComponentName": "widget",
"isDefault": true,
"scheduledUpdateTime": "10:30",
"defaultDimension": "2*2",
"name": "widget",
"description": "This is a service widget",
"colorMode": "auto",
"type": "JS",
"supportDimensions": [
"2*2"
],
"updateEnabled": true,
"updateDuration": 1
},
在桌面上长按应用的图标然后显示所有卡片的时候,MainAbility中卡片的生命周期方法onCreateForm()会被自动回调,方法onCreateForm()的实现:
@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();
}
因为总共有5个卡片,所以方法onCreateForm()会被回调5次!!!
方法bindFormData():
@Override
public ProviderFormInfo bindFormData() {
HiLog.info(TAG, "bind form data");
ZSONObject zsonObject = new ZSONObject();
ProviderFormInfo providerFormInfo = new ProviderFormInfo();
if (dimension == DIMENSION_1X2) {
zsonObject.put("mini", true);
}
if (dimension == DIMENSION_2X4) {
zsonObject.put("dim2X4", true);
}
providerFormInfo.setJsBindingData(new FormBindingData(zsonObject));
return providerFormInfo;
}
程序根据卡片的尺寸,分别设置了两个变量”mini”和”dim2X4”的值,这两个变量是子目录widget01中的index.json中的两个变量。
{
"data": {
"mini": false,
"dim2X4": false,
"miniTitle": "Title",
"title": "Title",
"content": "Introduction",
"detailTitle": "Title"
}
}
这里顺便说一下,在index.hml中,很多变量都使用两个花括号括了起来,这些变量的值是在程序的运行过程中动态确定的,这种技术称之为动态绑定。这些变量的初始值在index.json中的标签”data”中进行了定义。所以,在方法bindFormData()中,根据卡片的尺寸修改了两个动态绑定的变量的值。
所有卡片的标题都被修改了(尺寸最小的卡片除外,因为他本来就不显示标题)。
卡片的定点/定时刷新:
打开config.json,先将widget02对应的标签”updateDuration”修改为0,以关闭定时刷新。
对于标签“scheduledUpdateTime”设定的时刻,当到达之后,MainAbility中卡片的回调方法onUpdateForm()就会被自动调用:
将要刷新的数据存放在一个ZSONObject实例中,然后,将其封装在一个FormBindingData的实例bindingData中,最后,调用MainAbility的方法updateForm(),并将bindingData作为第二个实参。
@Override
public void updateFormData(long formId, Object... vars) {
HiLog.info(TAG, "update form data timing, default 30 minutes");
//刷新数据:
ZSONObject data = new ZSONObject();
data.put("title","更新操作");
FormBindingData bindingData = new FormBindingData(data);
try {
((MainAbility)context).updateForm(formId, bindingData);
} catch (FormException e) {
HiLog.error(TAG,e.toString());
}
}
打开config.json,将标签“scheduledUpdateTime”的值修改为当前时刻的一分钟之后:
运行工程,将widget02对应的卡片都添加到桌面上,当到达设定的定点时刻之后,卡片标题都刷新了:
时钟卡片配置文件:
"forms": [
{
"landscapeLayouts": [
"$layout:form_image_with_info_date_card_2_2",
"$layout:form_image_with_info_date_card_2_4"
],
"isDefault": true,
"scheduledUpdateTime": "10:30",
"defaultDimension": "2*2",
"name": "DateCard",
"description": "This is a service widget",
"colorMode": "auto",
"type": "Java",
"supportDimensions": [
"2*2",
"2*4"
],
"portraitLayouts": [
"$layout:form_image_with_info_date_card_2_2",
"$layout:form_image_with_info_date_card_2_4"
],
"updateEnabled": true,
"updateDuration": 1,
"formVisibleNotify": true
}
]
卡片布局:
以2*2布局为例,详细进行介绍。整个2*2卡片展示的内容从上到下分别为日期、时间、星期,整体由DependentLayout布局内嵌套四个DirectionalLayout构成,每个DirectionalLayout 内均使用Text组件进行展示,部分代码如下:
<?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:background_element="#6A9F99"
ohos:remote="true">
<DirectionalLayout
ohos:height="match_content"
ohos:width="match_parent"
ohos:orientation="vertical"
>
<Text...>
</DirectionalLayout>
<DirectionalLayout
ohos:id="$+id:title"
ohos:height="match_content"
ohos:width="match_parent"
ohos:alignment="horizontal_center"
ohos:orientation="horizontal"
ohos:top_margin="35fp"
>
<Text...>
<Text...>
<Text...>
</DirectionalLayout>
<DirectionalLayout
ohos:id="$+id:time"
ohos:height="match_content"
ohos:width="match_parent"
ohos:alignment="horizontal_center"
ohos:below="$id:title"
ohos:orientation="horizontal"
ohos:top_margin="0.5fp"
>
<Text...>
<Text...>
<Text...>
<Text...>
<Text...>
</DirectionalLayout>
<DirectionalLayout
ohos:height="match_content"
ohos:width="match_parent"
ohos:alignment="center"
ohos:below="$id:time"
ohos:margin="20fp"
ohos:orientation="horizontal"
>
<Text...>
<Text...>
<Text...>
<Text...>
<Text...>
<Text...>
<Text...>
</DirectionalLayout>
</DependentLayout>
创建卡片:
创建卡片数据库:
使用对象关系映射数据库来对卡片ID,卡片名字等信息进行存储,我们创建了一个数据库(FormDatabase)和一张表(Form)。
首先定义了数据库类FormDatabase.java,数据库包含"Form"表,版本号为 "1",示例代码如下:
@Database(
entities = {Form.class},
version = 1)
public abstract class FormDatabase extends OrmDatabase {}
定义实体类Form.java,对应数据库内的表名为"form",包含了卡片id"formId"(主键),卡片名称"formName"和卡片规格"dimension"三个字段,示例代码如下:
@Entity(tableName = "form")
public class Form extends OrmObject {
@PrimaryKey()
private Long formId;
private String formName;
private Integer dimension;
public Form(Long formId, String formName, Integer dimension) {
this.formId = formId;
this.formName = formName;
this.dimension = dimension;
}
// 自行添加字段的getter和setter方法或者参考完整代码
}
卡片应用初始化:
卡片程序安装启动后,会进入MainAbility,在onStart时,会首先启动卡片定时器服务TimerAbility,以便刷新时钟卡片,部分示例代码如下:
@Override
public void onStart(Intent intent) {
super.onStart(intent);
connect = helper.getOrmContext("FormDatabase", "FormDatabase.db", FormDatabase.class);
// 启动TimerAbility
Intent intentService = new Intent();
Operation operation =
new Intent.OperationBuilder()
.withDeviceId("")
.withBundleName("com.huawei.cookbooks")
.withAbilityName("com.huawei.cookbooks.TimerAbility")
.build();
intentService.setOperation(operation);
startAbility(intentService);
super.setMainRoute(ClockCardSlice.class.getName());
}
当卡片使用方请求获取卡片时,卡片提供方会被拉起并调用onCreateForm回调函数,完成卡片信息的初始化,在MainAbility中有如下示例代码:
@Override
protected ProviderFormInfo onCreateForm(Intent intent) {
if (intent == null) {
return new ProviderFormInfo();
}
// 获取卡片id
formId = INVALID_FORM_ID;
if (intent.hasParameter(AbilitySlice.PARAM_FORM_IDENTITY_KEY)) {
formId = intent.getLongParam(AbilitySlice.PARAM_FORM_IDENTITY_KEY, INVALID_FORM_ID);
} else {
return new ProviderFormInfo();
}
// 获取卡片名称
String formName = EMPTY_STRING;
if (intent.hasParameter(AbilitySlice.PARAM_FORM_NAME_KEY)) {
formName = intent.getStringParam(AbilitySlice.PARAM_FORM_NAME_KEY);
}
// 获取卡片规格
int dimension = DEFAULT_DIMENSION_2X2;
if (intent.hasParameter(AbilitySlice.PARAM_FORM_DIMENSION_KEY)) {
dimension = intent.getIntParam(AbilitySlice.PARAM_FORM_DIMENSION_KEY, DEFAULT_DIMENSION_2X2);
}
int layoutId = ResourceTable.Layout_form_image_with_info_date_card_2_2;
if (dimension == DEFAULT_DIMENSION_2X4) {
layoutId = ResourceTable.Layout_form_image_with_info_date_card_2_4;
}
formInfo = new ProviderFormInfo(layoutId, this);
// 存储卡片信息
Form form = new Form(formId, formName, dimension);
ComponentProvider componentProvider = ComponentProviderUtils.getComponentProvider(form, this);
formInfo.mergeActions(componentProvider);
if (connect == null) {
connect =
helper.getOrmContext("FormDatabase", "FormDatabase.db", FormDatabase.class);
}
try {
DatabaseUtils.insertForm(form, connect);
} catch (Exception e) {
DatabaseUtils.deleteFormData(form.getFormId(), connect);
}
return formInfo;
}
当卡片被删除时,需要重写onDeleteForm方法,根据卡片id删除卡片实例数据:
@Override
protected void onDeleteForm(long formId) {
super.onDeleteForm(formId);
// 删除数据库中的卡片信息
DatabaseUtils.deleteFormData(formId, connect);
}
更新卡片:
为了方便处理时钟卡片刷新的定时任务,我们创建了一个Service Ability,定时去更新卡片信息,在TimerAbility.java中有如下部分参考代码:
@Override
public void onStart(Intent intent) {
connect = helper.getOrmContext("FormDatabase", "FormDatabase.db", FormDatabase.class);
startTimer();
super.onStart(intent);
}
// 卡片更新定时器,每秒更新一次
private void startTimer() {
Timer timer = new Timer();
timer.schedule(
new TimerTask() {
@Override
public void run() {
updateForms();
notice();
}
},
0,SEND_PERIOD);
}
private void updateForms() {
// 从数据库中获取卡片信息
OrmPredicates ormPredicates = new OrmPredicates(Form.class);
List<Form> formList = connect.query(ormPredicates);
// 更新时分秒
if (formList.size() > 0) {
for (Form form : formList) {
// 遍历卡片列表更新卡片
ComponentProvider componentProvider = ComponentProviderUtils.getComponentProvider(form, this);
try {
Long updateFormId = form.getFormId();
updateForm(updateFormId, componentProvider);
} catch (FormException e) {
// 删除不存在的卡片
DatabaseUtils.deleteFormData(form.getFormId(), connect);
HiLog.error(LABEL_LOG, "onUpdateForm updateForm error");
}
}
}
}
前台service
为了保持service不被系统销毁,需要使用前台service配合手机管家中的相关配置来达到目的。示例代码如下:
private void notice() {
// 创建通知
NotificationRequest request = new NotificationRequest(NOTICE_ID);
request.setAlertOneTime(true);
NotificationRequest.NotificationNormalContent content = new NotificationRequest.NotificationNormalContent();
content.setText(DateUtils.getCurrentDate("yyyy-MM-dd HH:mm:ss"));
NotificationRequest.NotificationContent notificationContent = new NotificationRequest.NotificationContent(content);
request.setContent(notificationContent);
// 绑定通知
keepBackgroundRunning(NOTICE_ID, request);
}
卡片组件更新
有关卡片组件的更新,我们封装了ComponentProviderUtils这个类,在卡片更新时候,通过调用updateForm方法,传入参数formId和componentProvider,以达到日期、时间和星期实时更新的效果,部分代码和效果如下:
public static ComponentProvider getComponentProvider(Form form, Context context) {
int layoutId = ResourceTable.Layout_form_image_with_info_date_card_2_2;
if (form.getDimension() == DIM_VERSION) {
layoutId = ResourceTable.Layout_form_image_with_info_date_card_2_4;
}
ComponentProvider componentProvider = new ComponentProvider(layoutId, context);
setComponentProviderValue(componentProvider);
return componentProvider;
}
// 为时钟各个组件赋值
private static void setComponentProviderValue(ComponentProvider componentProvider) {
Calendar now = Calendar.getInstance();
int hour = now.get(Calendar.HOUR_OF_DAY);
int min = now.get(Calendar.MINUTE);
int second = now.get(Calendar.SECOND);
String hourString = int2String(hour);
String minString = int2String(min);
String secondString = int2String(second);
componentProvider.setText(ResourceTable.Id_date, DateUtils.getCurrentDate("yyyy-MM-dd"));
componentProvider.setText(ResourceTable.Id_hour, hourString);
componentProvider.setText(ResourceTable.Id_min, minString);
componentProvider.setText(ResourceTable.Id_sec, secondString);
// 获取当前星期
int weekDayId = getWeekDayId();
componentProvider.setTextColor(weekDayId, nowWeekColor);
// 将前一天的星期改回原色
int lastWeekId = getLastWeekDayId();
componentProvider.setTextColor(lastWeekId, primaryWeekColor);
}
参考官网文档:JAVA时钟卡片