目录


一、项目结构概览

项目采用多模块架构,帮助模块 help 是一个 HAR(静态共享包),被入口 HAP products/default 依赖:

GuardEHome/
├── features/
│   └── help/                            # HAR 模块(被 HAP 依赖)
│       └── src/main/ets/
│           ├── HelpPage.ets              # 【改造】添加 @StorageProp 监听卡片信号
│           ├── view/
│           │   ├── SOSView.ets           # SOS 按钮(长按 1.5s 触发)
│           │   └── DistressView.ets      # 紧急状态页(红黑闪烁)
│           └── constants/
│               └── CommonConstants.ets
│
├── products/
│   └── default/                         # Entry HAP 模块(卡片代码写在此处)
│       └── src/main/
│           ├── ets/
│           │   ├── defaultability/
│           │   │   └── DefaultAbility.ets    # 【改造】拦截卡片 Want 参数
│           │   ├── soswidgetability/
│           │   │   └── SOSWidgetAbility.ets  # 【新增】FormExtensionAbility
│           │   └── pages/
│           │       ├── Index.ets
│           │       └── SOSWidgetPage.ets     # 【新增】卡片 UI 页面
│           ├── module.json5                  # 【改造】注册 extensionAbilities
│           └── resources/
│               └── base/
│                   ├── element/
│                   │   └── string.json       # 【改造】添加卡片名称/描述
│                   └── profile/
│                       ├── main_pages.json   # 【改造】注册卡片页面路由
│                       └── form_config.json  # 【新增】卡片元信息配置

关键约束FormExtensionAbility 必须存在于 HAP 中,无法写在 HAR 里。所以卡片相关的 Ability 和页面需要放到 products/default 模块,通过 AppStoragehelp 模块的 HelpPage 通信。


二、实现思路

桌面卡片 (SOSWidgetPage.ets)
       │
       │ postCardAction('router', { target: 'sos' })
       ▼
DefaultAbility (onCreate / onNewWant)
       │
       │ AppStorage.setOrCreate('sosWidgetTrigger', true)
       ▼
HelpPage (监听 @StorageProp 变化)
       │
       │ currentView = DISTRESS_VIEW
       ▼
DistressView(红黑闪烁 → 紧急呼救)

整个链路最核心的设计是:

  1. 卡片侧:ArkTS 卡片通过 postCardAction 向 App 发送 router 事件,附带参数 target: 'sos'
  2. Ability 侧:不论是冷启动(onCreate)还是热启动(onNewWant),只要收到 target === 'sos',就将标记写入 AppStorage
  3. UI 侧HelpPage 通过 @StorageProp('sosWidgetTrigger') 绑定该标记,一旦检测为 true 就切换到 DistressView

三、实操步骤

3.1 创建卡片 UI 页面

卡片页面是一个标准的 @Entry + @Component 组件,布局上以大红色背景突出 SOS 警示感。

// products/default/src/main/ets/pages/SOSWidgetPage.ets
@Entry
@Component
struct SOSWidgetPage {
  build() {
    // 整体容器:红色背景,居中排列
    Column() {
      // 大号 SOS 文字,加粗白色
      Text('SOS')
        .fontSize(36)
        .fontWeight(FontWeight.Bold)
        .fontColor(Color.White)
      // 副标题提示
      Text('紧急呼救')
        .fontSize(12)
        .fontColor(Color.White)
        .margin({ top: 4 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FF1744') // 醒目的警示红
    .borderRadius(16)
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    // 点击卡片 → 打开 App 并传递 SOS 触发信号
    .onClick(() => {
      postCardAction(this, {
        'action': 'router',        // router 类型:打开指定 Ability
        'abilityName': 'DefaultAbility', // 目标 Ability 名称
        'moduleName': 'default',         // 目标模块名称
        'params': {
          'target': 'sos'          // 自定义参数,Ability 端据此识别
        }
      });
    })
  }
}

关键 APIpostCardAction 是 ArkTS 卡片专有 API,支持 routermessagecall 三种 action。此处用 router 打开 App 主 Ability 并传递参数。


3.2 创建 FormExtensionAbility

FormExtensionAbility 是卡片生命周期的管理者。虽然本例的卡片 UI 不需要动态更新数据,但仍需提供骨架实现。

// products/default/src/main/ets/soswidgetability/SOSWidgetAbility.ets
import { FormExtensionAbility, formBindingData } from '@kit.FormKit';
import { Want } from '@kit.AbilityKit';

export default class SOSWidgetAbility extends FormExtensionAbility {
  /**
   * 用户将卡片添加到桌面时调用
   * @param want 包含卡片配置信息的 Want
   * @returns 表单绑定数据
   */
  onAddForm(want: Want): formBindingData.FormBindingData {
    // 本例为静态卡片,无需传递初始数据
    return formBindingData.createFormBindingData({});
  }

  /**
   * 用户从桌面移除卡片时调用
   */
  onRemoveForm(formId: string): void {
    console.info('SOSWidgetAbility onRemoveForm');
  }

  /**
   * 卡片接收到 message 事件时调用
   */
  onFormEvent(formId: string, message: string): void {
    console.info('SOSWidgetAbility onFormEvent');
  }
}

3.3 配置 form_config.json

form_config.json 描述卡片的元信息:名称、尺寸、页面路径等。注意这是一个独立的配置文件,需要和 main_pages.json 并列放在 profile 目录下。

// products/default/src/main/resources/base/profile/form_config.json
{
  "forms": [
    {
      "name": "SOSWidget",                          // 卡片唯一标识名
      "uiSyntax": "arkts",                          // 必须指定为 arkts
      "displayName": "$string:sos_widget_name",     // 卡片显示名称(引用字符串资源)
      "description": "$string:sos_widget_desc",     // 卡片描述
      "src": "./ets/pages/SOSWidgetPage.ets",       // 卡片 UI 页面路径(相对于 src/main/)
      "window": {
        "designWidth": 720,                         // 设计稿宽度
        "autoDesignWidth": true                     // 自动适配设备宽度
      },
      "colorMode": "auto",                          // 自适应深色/浅色模式
      "isDefault": true,                            // 设为默认卡片
      "updateEnabled": false,                       // 不需要定时更新
      "scheduledUpdateTime": "10:30",
      "updateDuration": 0,                          // 0 表示不自动更新
      "defaultDimension": "2*2",                    // 默认尺寸 2×2 栅格
      "supportDimensions": [                        // 支持的尺寸列表
        "2*2"
      ]
    }
  ]
}

重点src 路径是相对于模块根目录 src/main/ 的,必须是 ./ets/pages/xxx.ets 格式。


3.4 在 module.json5 中注册

在 entry 模块的 module.json5 中添加 extensionAbilities 节点,将 SOSWidgetAbility 注册为 form 类型扩展能力。

// products/default/src/main/module.json5
{
  "module": {
    "name": "default",
    "type": "entry",
    // ... 其他配置 ...
    "extensionAbilities": [
      {
        "name": "DefaultBackupAbility",
        "srcEntry": "./ets/defaultbackupability/DefaultBackupAbility.ets",
        "type": "backup",
        "exported": false,
        "metadata": [
          {
            "name": "ohos.extension.backup",
            "resource": "$profile:backup_config"
          }
        ]
      },
      // 👇 新增:卡片扩展能力
      {
        "name": "SOSWidgetAbility",                            // 类名
        "srcEntry": "./ets/soswidgetability/SOSWidgetAbility.ets", // 入口文件
        "type": "form",                                       // 类型固定为 form
        "exported": false,
        "metadata": [
          {
            "name": "ohos.extension.form",                    // 固定 metadata 名称
            "resource": "$profile:form_config"                // 引用 form_config.json
          }
        ]
      }
    ]
  }
}

3.5 将卡片页面注册到 main_pages.json

卡片页面也属于页面路由,必须在 main_pages.json 中注册,否则构建系统会提示 “Form referenced in the config was not found”。

// products/default/src/main/resources/base/profile/main_pages.json
{
  "src": [
    "pages/Index",
    "pages/AdaptiveIndex",
    "pages/ResponsiveIndex",
    "pages/SystemCapabilitiesIndex",
    "pages/SOSWidgetPage"
  ]
}

3.6 添加字符串资源

在 string.json 中添加卡片相关的国际化字符串。

// products/default/src/main/resources/base/element/string.json
{
  "string": [
    // ... 已有配置 ...
    {
      "name": "sos_widget_name",
      "value": "SOS 紧急呼救"
    },
    {
      "name": "sos_widget_desc",
      "value": "一键触发 SOS 紧急呼救"
    }
  ]
}

3.7 处理卡片点击事件——改造 Ability

当用户点击桌面卡片时,系统会通过 postCardAction 发送一个 WantDefaultAbility。无论 App 处于冷启动还是热启动状态,我们都需要拦截这个参数并传递给 UI。

// products/default/src/main/ets/defaultability/DefaultAbility.ets
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

const DOMAIN = 0x0000;

export default class DefaultAbility extends UIAbility {
  /**
   * 冷启动:App 进程不存在时首次创建
   * @param want 启动参数(包含卡片传来的 target)
   */
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    try {
      this.context.getApplicationContext()
        .setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
    } catch (err) {
      hilog.error(DOMAIN, 'testTag', 'setColorMode failed: %{public}s', JSON.stringify(err));
    }
    // 检查是否由 SOS 卡片触发
    this.checkWidgetTrigger(want);
  }

  /**
   * 热启动:App 已在后台运行,重新被唤起
   * @param want 新的启动参数
   */
  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    this.checkWidgetTrigger(want);
  }

  /**
   * 统一处理卡片触发的 SOS 信号
   * 将信号写入 AppStorage,HelpPage 通过 @StorageProp 监听
   */
  private checkWidgetTrigger(want: Want): void {
    const target: string = want?.parameters?.target as string;
    if (target === 'sos') {
      // AppStorage 是 ArkUI 全局状态存储,可在 Ability 和 Component 间共享
      AppStorage.setOrCreate('sosWidgetTrigger', true);
    }
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(DOMAIN, 'testTag', 'loadContent failed: %{public}s', JSON.stringify(err));
        return;
      }
    });
  }

  // ... 其他生命周期方法保持不变 ...
}

核心思路AppStorage 是 ArkUI 内置的全局键值存储,不区分模块边界。在 Ability 中写入、在 HAR 模块的 Component 中读取,完美解决跨模块通信问题。


3.8 监听触发信号——改造 HelpPage

HelpPage 原本通过 currentView 状态控制视图切换。我们通过 @StorageProp 绑定 sosWidgetTrigger,当该值变为 true 时自动跳转到 DistressView

// features/help/src/main/ets/HelpPage.ets
import { ViewConstants } from './constants/CommonConstants';
import { EmergencyContact } from './model/EmergencyContact';
import { EmergencyContactViewModel } from './viewmodel/EmergencyContactViewModel';
import { SOSView } from './view/SOSView';
import { DistressView } from './view/DistressView';
import { ContactListView } from './view/ContactListView';
import { ContactEditView } from './view/ContactEditView';
import { EmergencyCallView } from './view/EmergencyCallView';
import { window } from '@kit.ArkUI';

@Component
export struct HelpPage {
  @State currentView: number = ViewConstants.HUB_VIEW;
  @State contactList: EmergencyContact[] = [];
  @State editContactId: number = -1;
  @State isEditing: boolean = false;
  @State topPadding: number = 24;
  private contactViewModel: EmergencyContactViewModel = new EmergencyContactViewModel();

  // 👇 @StorageProp 绑定 AppStorage 中的卡片触发标记
  // @Watch 在标记值变化时自动回调
  @StorageProp('sosWidgetTrigger') @Watch('onWidgetTrigger') sosWidgetTrigger: boolean = false;

  /**
   * @Watch 回调:当 AppStorage 中 sosWidgetTrigger 变为 true 时触发
   * 适用于 App 已在运行、用户点击卡片的热启动场景
   */
  onWidgetTrigger(): void {
    if (this.sosWidgetTrigger) {
      this.currentView = ViewConstants.DISTRESS_VIEW;        // 切换到紧急状态页
      AppStorage.setOrCreate('sosWidgetTrigger', false);      // 重置标记,防止重复触发
    }
  }

  aboutToAppear(): void {
    // 👇 冷启动场景:组件初次创建时检查标记的初始值
    if (this.sosWidgetTrigger) {
      this.currentView = ViewConstants.DISTRESS_VIEW;
      AppStorage.setOrCreate('sosWidgetTrigger', false);
    }
    // 获取状态栏高度,适配安全区域
    window.getLastWindow(getContext(this)).then((data: window.Window) => {
      let avoidArea: window.AvoidArea =
        data.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
      this.topPadding = this.getUIContext().px2vp(avoidArea.topRect.height + 8);
    }).catch(() => {
      this.topPadding = 36;
    });
  }

  // ... 其余方法(build、HubContent、FeatureCard 等)保持不变 ...
}

注意@Watch 只在值变化后回调,首次初始化时不会触发。所以冷启动场景需要在 aboutToAppear 中额外检查一次初始值。


四、编译与运行

4.1 执行构建

# 在 DevEco Studio 中点击 Build,或使用命令行
hvigorw --mode project -p product=default assembleApp -p buildMode=debug --no-daemon

4.2 部署到模拟器

# 通过 DevEco Studio 或 hdc 安装
hdc install products/default/build/default/outputs/default/default-default-signed.hap

4.3 添加桌面卡片

  1. 长按模拟器桌面空白处 → 选择"服务卡片"
  2. 找到 “SOS 紧急呼救” 卡片
  3. 拖动到桌面
  4. 点击卡片 → App 启动并直接进入 SOS 紧急呼救状态

五、踩坑记录

❌ 坑1:form.src 路径格式

问题:构建时报 Form referenced in the config ... was not found

原因form_config.jsonsrc 字段的路径必须是相对于 src/main/ 目录的完整路径,且需要包含 ./ets/ 前缀和 .ets 后缀。

正确写法

"src": "./ets/pages/SOSWidgetPage.ets"

❌ 坑2:@Entry 组件未注册到 main_pages.json

问题:即使 form_config.json 路径正确,构建仍找不到页面。

原因:所有页面(包括卡片页面)都必须在 main_pages.jsonsrc 数组中注册。

解决:将 "pages/SOSWidgetPage" 追加到 main_pages.json

❌ 坑3:AppStorage 的导入方式

问题:ArkTS 检查提示 @kit.ArkUI 没有 AppStorage 导出。

原因AppStorage 是 ArkUI 框架的内置全局对象,无需显式 import,直接使用即可。

// ❌ 错误
import { AppStorage } from '@kit.ArkUI';

// ✅ 正确
// 无需 import,直接使用
AppStorage.setOrCreate('key', value);

❌ 坑4:@Watch 在冷启动时不触发

问题@StorageProp 绑定后,@Watch 回调只监听到后续变化,组件初始化时的已有值不会触发。

解决:在 aboutToAppear 中额外检查一次初始值,覆盖冷启动场景。


六、总结

本文从实战角度记录了为 HarmonyOS App 添加 ArkTS 桌面卡片的完整流程。核心收获如下:

知识点 说明
ArkTS 卡片架构 @Entry 卡片页面 + FormExtensionAbility + form_config.json 三者缺一不可
跨模块通信 AppStorage 是 HAR 与 HAP 之间共享状态的零成本方案
冷/热启动兼容 onCreate 处理冷启动,onNewWant 处理热启动,aboutToAppear + @Watch 双保险接收信号
路由配置 卡片页面需同时注册到 form_config.jsonsrc 字段)和 main_pages.json

桌面卡片的本质是系统级快捷入口。将 SOS 呼救功能做成桌面卡片后,用户从点击到触发紧急状态仅需 1 步 + 0 等待,极大提升了紧急场景下的可用性。

掌握了上述模式,你也可以为自己的 App 快速添加天气卡片、快捷操作卡片、信息摘要卡片等各类 ArkTS 桌面卡片。

Logo

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

更多推荐