赞
踩
目录
服务卡片是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以创建一个工程。
连接远程设备:
开启远程设备:
打开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和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”中定义的。
由于该工程的类型是”原子化服务”,所以在桌面上并没有相应的图标。由于在创建工程时选择了”在服务中心进行展示”,因此,打开服务中心,就看到了相应的入口卡片:
上下滑动所有卡片,总共有5个:
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配合手机管家中的相关配置来达到目的。示例代码如下:
- 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时钟卡片
【本文正在参与"有奖征文|HarmoneyOS征文大赛"活动】
传送口:https://marketing.csdn.net/p/ad3879b53f4b8b31db27382b5fc65bbc
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。