HarmonyOS实战—服务卡片初体验

简介: 最近看到很多博客网站上出现了HarmonyOS的征文活动,看到那些精美的奖品让我也安耐不住开了,当然奖品的诱惑当然是抵挡不住我对技术的狂热追求,对于开发者而言技术没有顶峰没有终点。那么今天给大家做一个卡片服务开发的经验分享,如果有什么地方说的不对的请各位开发者进行指正,如果有什么问题也可以发私信或者直接在帖子中留言,我也会及时回复大家。

最近看到很多博客网站上出现了HarmonyOS的征文活动,看到那些精美的奖品让我也安耐不住开了,当然奖品的诱惑当然是抵挡不住我对技术的狂热追求,对于开发者而言技术没有顶峰没有终点。那么今天给大家做一个卡片服务开发的经验分享,如果有什么地方说的不对的请各位开发者进行指正,如果有什么问题也可以发私信或者直接在帖子中留言,我也会及时回复大家。


一、什么是服务卡片

服务卡片(以下简称“卡片”)是FA的一种界面展示形式,将FA的重要信息或操作直接放置到卡片中,用户通过直接操作卡片就可以达到应用的使用体验,这样做大大减少了应用的使用层级性。


卡片常用于嵌入到其他应用中作为其界面的一部分显示(也可以使用原子化服务将应用保存到服务中心中,这种方式不需要安装应用),并支持拉起页面,发送消息等基础的交互功能。


原子化服务在下个帖子中介绍。


示例如下图所示。



20210623102327988.png

为了开发者能够便于理解,官方将服务卡片分为三方面:


卡片使用方

显示卡片内容的宿主应用,控制卡片在宿主中展示的位置。


卡片管理服务

用于管理系统中所添加卡片的常驻代理服务,包括卡片对象的管理与使用,以及卡片周期性刷新等。


卡片提供方

提供卡片显示内容的HarmonyOS应用或原子化服务,控制卡片的显示内容、控件布局以及控件点击事件。


卡片使用方和提供方不要求常驻运行,在需要添加/删除/请求更新卡片时,卡片管理服务会拉起卡片提供方获取卡片信息。


二、服务卡片的运作机制

文字描述滞后,先上图。(图片由官网提供,此处借用一下)


75b0e6908a616e0a90c8b224699b1114.png


从图中可以清楚的看到服务卡片的整体通信层都是由"RPC"负责,通过通信适配层构成了数据发送接收的通道。


卡片管理服务


周期性刷新:在卡片添加后,根据卡片的刷新策略启动定时任务周期性触发卡片的刷新。

卡片缓存管理:在卡片添加到卡片管理服务后,对卡片的视图信息进行缓存,以便下次获取卡片时可以直接返回缓存数据,降低时延。

卡片生命周期管理:对于卡片切换到后台或者被遮挡时,暂停卡片的刷新;以及卡片的升级/卸载场景下对卡片数据的更新和清理。

卡片使用方对象管理:对卡片使用方的RPC对象进行管理,用于使用方请求进行校验以及对卡片更新后的回调处理。

卡片提供方


创建卡片实例并实现onCreateForm、onUpdateForm和onDeleteForm处理创建卡片、更新卡片以及删除卡片等请求,提供相应的卡片服务。

三、服务卡片的实现(java方式)

第一步、下载、安装、配置 DevEco Studio

点击查看

第二步、运行DevEco Studio创建新项目

因为要使用java语言编写,所以此处要选择支持Java语言的空页面。


20210623105021931.png


选择成功进入新建项目配置界面。

20210623105702849.png



点击Finish进入项目。



20210623105937388.png

第三步、添加卡片模板

创建模板:右击“Entry”→“New”→“Server Widget”


20210623110229403.png

20210623110659146.png



设置模板


20210623111156599.png


第四步、查看卡片服务配置

项目目录


20210623112458674.png


由图可见:添加卡片模板后在原来项目中增加一个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();
    }
}


配置文件

20210623112548734.png


属性名称

子属性名称

含义

数据类型

name


表示卡片的类名。字符串最大长度为127字节。

字符串

description


表示卡片的描述。取值可以是描述性内容,也可以是对描述性内容的资源索引,以支持多语言。字符串最大长度为255字节。

字符串

isDefault


表示该卡片是否为默认卡片,每个Ability有且只有一个默认卡片。

  • true:默认卡片。
  • false:非默认卡片。

布尔值

type


表示卡片的主题样式,取值范围如下:

  • auto:自适应。
  • dark:深色主题。
  • light:浅色主题。

字符串

colorMode


表示卡片的主题样式,取值范围如下:

  • auto:自适应。
  • dark:深色主题。
  • light:浅色主题。

字符串

supportDimensions


表示卡片支持的外观规格,取值范围:

  • 1*2:表示1行2列的二宫格。
  • 2*2:表示2行2列的四宫格。
  • 2*4:表示2行4列的八宫格。
  • 4*4:表示4行4列的十六宫格。

字符串数组

defaultDimension


表示卡片的默认外观规格,取值必须在该卡片supportDimensions配置的列表中。

字符串

landscapeLayouts


表示卡片外观规格对应的横向布局文件,与supportDimensions中的规格一一对应。

仅当卡片类型为Java卡片时,需要配置该标签。

字符串数组

portraitLayouts


表示卡片外观规格对应的竖向布局文件,与supportDimensions中的规格一一对应。

仅当卡片类型为Java卡片时,需要配置该标签。

字符串数组

updateEnabled


表示卡片是否支持周期性刷新,取值范围:

  • true:表示支持周期性刷新,可以在定时刷新(updateDuration)和定点刷新(scheduledUpdateTime)两种方式任选其一,优先选择定时刷新。
  • false:表示不支持周期性刷新。

布尔类型

scheduledUpdateTime


表示卡片的定点刷新的时刻,采用24小时制,精确到分钟。

字符串

updateDuration


表示卡片定时刷新的更新周期,单位为30分钟,取值为自然数。

  • 当取值为0时,表示该参数不生效。
  • 当取值为正整数N时,表示刷新周期为30*N分钟。

数值

formConfigAbility


表示卡片的配置跳转链接,采用URI格式。

字符串

jsComponentName


表示JS卡片的Component名称。字符串最大长度为127字节。

仅当卡片类型为JS卡片时,需要配置该标签。

字符串

metaData


表示卡片的自定义信息,包含customizeData数组标签。

对象

customizeData


表示自定义的卡片信息。

对象数组


name


表示数据项的键名称。字符串最大长度为255字节。

字符串


value

表示数据项的值。字符串最大长度为255字节。

字符串


第五步、编写卡片界面

20210623120305441.png


分别修改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>


第六步、运行程序、查看效果

点击图标上划,出现卡片。

20210623125841576.png




点击右上角图钉按钮,将卡片放在屏幕中。



20210623125939859.png


长按应用,出现服务卡片。


20210623130040506.png



点击服务卡片选择界面,上下滑动可选择卡片内容。


20210623130143965.png



点击添加到桌面,则将卡片添加到桌面中。


20210623130326930.png


至此,卡片服务应用就以全部开发完成,后续会对卡片内部进行相关编写。使其进行动态的刷新及动态的获取数据。


跳转项目地址


喜欢的请为作者点个喜欢或者赞赏~


相关文章
|
1月前
|
存储 调度 数据安全/隐私保护
鸿蒙Flutter实战:13-鸿蒙应用打包上架流程
鸿蒙应用打包上架流程包括创建应用、打包签名和上传应用。首先,在AppGallery Connect中创建项目、APP ID和元服务。接着,使用Deveco进行手动签名,生成.p12和.csr文件,并在AppGallery Connect中上传CSR文件获取证书。最后,配置签名并打包生成.app文件,上传至应用市场。常见问题包括检查签名配置文件是否正确。参考资料:[应用/服务签名](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide-signing-V5)。
58 3
鸿蒙Flutter实战:13-鸿蒙应用打包上架流程
|
1月前
|
开发工具 芯片 开发者
鸿蒙Flutter实战:12-使用模拟器开发调试
本文介绍了如何在 M 系列芯片的 Mac 电脑上使用模拟器进行鸿蒙 Flutter 开发和调试。主要内容包括:创建 Flutter 项目、签名、创建模拟器、运行 Flutter 项目以及常见问题的解决方法。适用于希望在鸿蒙系统上开发 Flutter 应用的开发者。
46 2
鸿蒙Flutter实战:12-使用模拟器开发调试
|
1月前
|
UED
<大厂实战经验> Flutter&鸿蒙next 中使用 initState 和 mounted 处理异步请求的详细解析
在 Flutter 开发中,处理异步请求是常见需求。本文详细介绍了如何在 `initState` 中触发异步请求,并使用 `mounted` 属性确保在适当时机更新 UI。通过示例代码,展示了如何安全地进行异步操作和处理异常,避免在组件卸载后更新 UI 的问题。希望本文能帮助你更好地理解和应用 Flutter 中的异步处理。
66 3
|
1月前
|
安全 测试技术 数据安全/隐私保护
原生鸿蒙应用市场开发者服务的技术解析:从集成到应用发布的完整体验
原生鸿蒙应用市场开发者服务的技术解析:从集成到应用发布的完整体验
|
1月前
|
JavaScript API 开发工具
<大厂实战场景> ~ Flutter&鸿蒙next 解析后端返回的 HTML 数据详解
本文介绍了如何在 Flutter 中解析后端返回的 HTML 数据。首先解释了 HTML 解析的概念,然后详细介绍了使用 `http` 和 `html` 库的步骤,包括添加依赖、获取 HTML 数据、解析 HTML 内容和在 Flutter UI 中显示解析结果。通过具体的代码示例,展示了如何从 URL 获取 HTML 并提取特定信息,如链接列表。希望本文能帮助你在 Flutter 应用中更好地处理 HTML 数据。
109 1
|
1月前
|
存储 监控 安全
开发者的黄金时代:原生鸿蒙应用市场的全生命周期服务
2024年10月22日,华为发布了HarmonyOS NEXT,标志着鸿蒙生态进入商用发展阶段。原生鸿蒙应用市场全面焕新,不仅在UI设计、互动体验和隐私安全机制上进行了重塑,还为开发者和用户提供了从开发到分发的全生命周期服务。通过统一上架、多端分发、隐私合规保障等措施,原生鸿蒙应用市场助力开发者实现高效、安全的应用开发与分发,为全球数亿鸿蒙用户带来更流畅、更安全的使用体验。
|
1月前
|
移动开发 资源调度 IDE
鸿蒙Taro实战:01-搭建开发环境
本文介绍了如何使用 Taro 4.x 框架搭建鸿蒙应用开发环境。主要内容包括:下载并配置 DevEco IDE,创建鸿蒙项目,安装 Taro 4.x,初始化 Taro 项目,配置鸿蒙插件和编译配置,修改 `package.json`,运行 Taro 和鸿蒙项目。通过本文,读者可以快速上手鸿蒙应用开发。
|
1月前
|
JSON Dart 数据格式
<大厂实战场景> ~ flutter&鸿蒙next处理后端返回来的数据的转义问题
在 Flutter 应用开发中,处理后端返回的数据是常见任务,尤其涉及转义字符时。本文详细探讨了如何使用 Dart 的 `dart:convert` 库解析包含转义字符的 JSON 数据,并提供了示例代码和常见问题的解决方案,帮助开发者有效处理数据转义问题。
127 0
|
23天前
|
Android开发
鸿蒙开发:自定义一个简单的标题栏
本身就是一个很简单的标题栏组件,没有什么过多的技术含量,有一点需要注意,当使用沉浸式的时候,注意标题栏的位置,需要避让状态栏。
鸿蒙开发:自定义一个简单的标题栏
|
23天前
|
API
鸿蒙开发:切换至基于rcp的网络请求
本文的内容主要是把之前基于http封装的库,修改为当前的Remote Communication Kit(远场通信服务),无非就是通信的方式变了,其他都大差不差。
鸿蒙开发:切换至基于rcp的网络请求