最近搞了个鸿蒙的元服务卡片,目标很简单:让用户不用点开App,在桌面上就能一眼看清外卖到哪了,还能一键联系骑手。 官方概念吹得再响,不如一行代码实在。开整!

一、卡片是啥?桌面上的“信息小窗口”

想象你手机桌面上有个固定的小区域(卡片),它就干一件事:实时显示你当前外卖订单最关键的信息,比如:

  • 状态: 商家已接单 / 骑手取餐中 / 配送中 (离你500米) / 已送达

  • 预计时间: 12:15送达

  • 简单操作: 一个大大的【联系骑手】按钮

用户点一下卡片,可能直接跳转到App里更详细的页面(或者直接打电话),但核心信息在桌面就搞定了。这就是元服务卡片的价值——服务直达桌面。

二、开撸代码:核心步骤拆解

鸿蒙卡片开发主要在 FA模型 或 Stage模型 下搞,这里以FA模型为例(概念相对简单点)。核心是几个文件:

  1. Java 逻辑代码 (比如 MyCardAbilitySlice.java): 处理卡片的数据加载、更新和交互。

  2. resources/base/layout 下的 xml 布局文件 (比如 widget_card.xml): 定义卡片长啥样。

  3. resources/base/profile 下的 form_config.json: 声明卡片支持的尺寸、类型等。

卡片布局 (widget_card.xml) 

<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
    xmlns:ohos="http://schemas.huawei.com/res/ohos"
    ohos:width="match_parent"
    ohos:height="match_parent"
    ohos:orientation="vertical"
    ohos:padding="8vp"> <!-- 简单内边距 -->

    <!-- 状态行:文字 + 可能的小图标 -->
    <Text
        ohos:id="$+id:tvStatus"
        ohos:width="match_content"
        ohos:height="match_content"
        ohos:text="状态:骑手取餐中"
        ohos:text_size="16fp"
        ohos:text_color="#FF333333" />

    <!-- 预计送达时间 -->
    <Text
        ohos:id="$+id:tvEta"
        ohos:width="match_content"
        ohos:height="match_content"
        ohos:text="预计送达:12:15"
        ohos:layout_margin_top="4vp"
        ohos:text_size="14fp"
        ohos:text_color="#666666" />

    <!-- 关键!动态距离/位置信息 -->
    <Text
        ohos:id="$+id:tvDistance"
        ohos:width="match_content"
        ohos:height="match_content"
        ohos:text="骑手距离:约500米"
        ohos:layout_margin_top="4vp"
        ohos:text_size="14fp"
        ohos:text_color="#2196F3" /> <!-- 用个醒目的蓝色 -->

    <!-- 大大的联系骑手按钮 -->
    <Button
        ohos:id="$+id:btnCallRider"
        ohos:width="match_parent"
        ohos:height="35vp"
        ohos:text="联系骑手"
        ohos:background_element="#FF4081" <!-- 粉色按钮醒目 -->
        ohos:text_color="#FFFFFF"
        ohos:layout_margin_top="12vp"
        ohos:clickable="true" /> <!-- 记住要设置可点击! -->

</DirectionalLayout>

说明: 这就是个简单的垂直布局,放了几个文本和一个按钮。注意给每个需要动态更新的控件设置好 id,后面代码里要用。

卡片逻辑 (MyCardAbilitySlice.java) - 处理数据和更新

public class MyCardAbilitySlice extends FormAbilitySlice {

    // 定义卡片的ID
    private static final int CARD_ID = 100; // 随便写个唯一ID

    @Override
    protected void onStart(Intent intent) {
        super.onStart(intent);
        // 1. 设置布局
        setUIContent(ResourceTable.Layout_widget_card);

        // 2. 找到布局里的控件
        Text tvStatus = (Text) findComponentById(ResourceTable.Id_tvStatus);
        Text tvEta = (Text) findComponentById(ResourceTable.Id_tvEta);
        Text tvDistance = (Text) findComponentById(ResourceTable.Id_tvDistance);
        Button btnCallRider = (Button) findComponentById(ResourceTable.Id_btnCallRider);

        // 3. 【模拟】获取外卖数据!真实项目这里是从网络或本地数据库取
        // 假设我们有个方法 fetchLatestDeliveryData() 返回一个对象
        DeliveryData deliveryData = fetchLatestDeliveryData();

        // 4. 把数据塞到控件上显示
        tvStatus.setText("状态:" + deliveryData.getStatus());
        tvEta.setText("预计送达:" + deliveryData.getFormattedEta());
        tvDistance.setText("骑手距离:" + deliveryData.getDistance() + "米");

        // 5. 处理按钮点击 - 联系骑手
        btnCallRider.setClickedListener(component -> {
            // 真实场景:唤起电话拨号盘,拨打骑手电话
            // 这里简单用Toast模拟
            getUITaskDispatcher().asyncDispatch(() -> {
                new ToastDialog(getContext())
                    .setText("正在呼叫骑手: " + deliveryData.getRiderPhone())
                    .show();
            });
            // 实际代码可能是:
            // Intent callIntent = new Intent(Intent.ACTION_DIAL);
            // callIntent.setUri(Uri.parse("tel:" + deliveryData.getRiderPhone()));
            // startAbility(callIntent);
        });

        // 6. 【关键】定时更新!卡片需要自己刷新数据
        // 这里用个简单定时器模拟,真实项目用系统提供的更新机制或Push
        getUITaskDispatcher().delayDispatch(this::updateCardData, 30000); // 30秒更新一次
    }

    // 模拟获取外卖数据的方法
    private DeliveryData fetchLatestDeliveryData() {
        // 这里应该是网络请求或数据库查询...
        // 返回一个假数据
        DeliveryData data = new DeliveryData();
        data.setStatus("配送中");
        data.setEta(System.currentTimeMillis() + 1800000); // 半小时后
        data.setDistance(500); // 500米
        data.setRiderPhone("13800138000");
        return data;
    }

    // 更新卡片数据的方法 (逻辑和上面类似,需要重新获取数据并更新UI)
    private void updateCardData() {
        DeliveryData newData = fetchLatestDeliveryData();
        // ... 更新UI控件显示 newData ...
        // 再次设置定时更新
        getUITaskDispatcher().delayDispatch(this::updateCardData, 30000);
    }

    // ... 其他生命周期方法,比如onInactive, onActive等,根据需要处理卡片可见性变化 ...
}

// 简单的外卖数据Bean
class DeliveryData {
    private String status;
    private long eta; // 预计送达时间戳
    private int distance; // 距离(米)
    private String riderPhone;

    // getters and setters...
    public String getFormattedEta() {
        // 把时间戳转换成 "HH:mm" 格式
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");
        return sdf.format(new Date(eta));
    }
}

这里有几个着重解释下 :

  1. 找控件: findComponentById 用之前XML里定义的id找到对应的UI组件。

  2. 数据获取: fetchLatestDeliveryData() 是模拟的!真实项目这里最复杂:可能需要调用网络API,访问本地数据库,或者接收来自App主模块的事件通知。卡片本身是相对独立的。

  3. 数据绑定: 把获取到的数据设置到对应的Text、Button等控件上。

  4. 按钮事件: setClickedListener 处理用户点击“联系骑手”。这里模拟了Toast,真实场景是调用系统电话能力 (ACTION_DIAL)。

  5. 定时更新: 这是卡片能“动”起来的关键!这里用了简单的 delayDispatch 模拟定时循环(30秒一次)。生产环境强烈建议:

    • 使用 updateForm() 方法结合系统提供的更新机制。

    • 利用Push推送(比如通过华为推送服务)在订单状态变化时实时触发卡片更新,体验更好。

  6. FormAbilitySlice: 这是开发卡片的核心类,继承它。

卡片配置 (form_config.json) 

{
  "forms": [
    {
      "name": "DeliveryTrackerCard", // 你卡片的名称
      "description": "外卖进度跟踪卡片", // 描述
      "src": "./js/widget/pages/card/index", // Stage模型常用,FA模型主要靠上面的Java
      "window": {
        "designWidth": 720, // 设计基准宽度
        "autoDesignWidth": true // 是否自动适配宽度
      },
      "colorMode": "auto", // 颜色模式自动
      "isDefault": true, // 是否是默认卡片
      "updateEnabled": true, // 允许更新!重要!
      "scheduledUpdateTime": "10:30", // 每天固定更新时间(可配合定时更新)
      "updateDuration": 1, // 定时更新周期(单位小时),配合上面用
      "defaultDimension": "2*2", // 默认支持的尺寸 2行2列
      "supportDimensions": [ // 支持的其他尺寸
        "2*2",
        "2*4",
        "4*4"
      ],
      "type": "Java" // 卡片类型,这里是Java卡片
    }
  ]
}

 关键配置:

  • updateEnabled必须为 true,否则你的定时更新或Push更新都无效!

  • scheduledUpdateTime 和 updateDuration: 系统级别的定时更新策略,作为你代码内定时更新的补充或兜底。

  • supportDimensions: 声明你的卡片支持哪些尺寸布局,你可能需要为不同尺寸提供不同的布局XML。

三、踩坑实录 & 最佳实践

  1. 数据从哪里来? 这是最大的坑!卡片本身资源受限。

    • 理想方案: App主进程通过 FormProvider 的 onUpdateForm 主动推送数据给卡片。或者用公共的 DataAbility/分布式数据 共享数据。

    • 折中方案: 卡片自己调用网络API(注意权限和后台网络限制)或读本地数据库(需要和App约定好存储位置和格式)。

    • 简单模拟: 像上面代码那样写死或本地模拟(仅用于演示)。

  2. 更新频率控制: 卡片刷新太频繁耗电!太慢信息不准。

    • 利用系统定时更新 (scheduledUpdateTime + updateDuration) 做兜底(比如每小时一次)。

    • 关键状态变化(如商家接单->骑手取餐->配送中->送达)一定要用Push实时推!体验提升巨大。

    • 代码内定时更新 (delayDispatch 或 Timer) 可用于中间状态的微调(如距离变化),但间隔不宜过短(>1分钟)。

  3. 卡片尺寸适配: 用户可能选2*2或2*4。

    • 在 resources/base/layout 下为不同尺寸创建不同的布局文件,如 widget_card_2x2.xmlwidget_card_2x4.xml

    • 在 form_config.json 的对应卡片配置的 supportDimensions 里声明支持的尺寸。

    • 在 MyCardAbilitySlice 的 onStart 里,根据 intent 携带的卡片尺寸信息 (ohos.extra.param.key.form_dimension),动态加载不同的布局 (setUIContent)。

  4. 按钮跳转: 点击按钮想打开App的某个页面?

    • 使用 Intent 指定目标Ability (比如 ".MainAbility") 和可能的参数 (setParam)。

    • 调用 startAbility(intent)。确保目标Ability的配置正确。

  5. 性能!性能!性能! 卡片资源有限。布局简单点,逻辑轻量点,网络请求优化点。别在卡片里做耗时操作!

四、效果 & 真香时刻

折腾完这些,当你在手机桌面上长按 -> 服务卡片 -> 找到你的“外卖进度卡片” -> 添加到桌面。看着它静静躺在那里,实时显示着你的外卖距离,点一下按钮就能打电话给骑手... 那种“信息随手可得”的畅快感,就是元服务卡片最大的魅力!

总结一下核心:

  • 布局XML: 画个简单好看的皮囊。

  • Java逻辑: 定时/实时拿数据,塞到皮囊里,处理按钮点击。

  • 配置JSON: 告诉系统你的卡片叫啥、多大、能不能更新。

  • 数据通道: 搞定数据从哪里来(最难也最关键!)。

  • 尺寸适配: 让它在不同大小的卡片位置都好看。

代码虽然简化了,但流程和关键点都在这了。

搞起吧,把服务直接“钉”在用户桌面上,体验提升肉眼可见!

 

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐