HarmonyOS7 互动卡片架构深度解析
互动卡片(Live Form / LiveCard)为服务卡片带来了质的飞跃——从"信息的静态展示"进化到了"可交互、可动效、可感知"的全新维度。本文以开源项目 LiveCard(包名 com.example.livecard)为蓝本,从 LiveFormExtensionAbility 生命周期出发,深入剖析互动卡片的整体架构设计。
LiveCard 项目包含四种互动卡片:睡眠卡片(三叶草动画 + 睡眠健康数据)、快递卡片(陀螺仪驱动的憨憨动画)、运动卡片(卡路里燃烧动画与状态切换)、音乐卡片(专辑封面同步动画 + 音乐播放)。全文代码均取自该实战项目。

互动卡片 vs 传统卡片
在 module.json5 中,我们可以同时声明两种卡片类型的 ExtensionAbility:
[entry/src/main/module.json5]
{
"extensionAbilities": [
{
"name": "EntryFormAbility",
"srcEntry": "./ets/entryformability/EntryFormAbility.ets",
"type": "form", // 传统卡片
"metadata": [
{
"name": "ohos.extension.form",
"resource": "$profile:form_config"
}
]
},
{
"name": "SleepLiveCardAbility",
"srcEntry": "./ets/livecardability/SleepLiveCardAbility.ets",
"type": "liveForm" // 互动卡片
}
]
}
传统卡片对应 FormExtensionAbility,而互动卡片对应 LiveFormExtensionAbility。二者最核心的差异体现在三个层面:
生命周期模型:传统卡片通过 onAddForm、onUpdateForm、onFormEvent 等回调与卡片交互,本质上是一种"事件驱动"的轻量模型。而互动卡片通过 onLiveFormCreate 获得一个 UIExtensionContentSession,拥有独立的 UI 渲染能力,本质上是一个"UIExtension"的增强形态。
渲染能力:传统卡片的 UI 由 ArkTS widget 页面渲染,不支持帧动画、传感器数据、实时媒体播放等能力。互动卡片则可以运行完整的帧动画(如睡眠卡片的憨憨动画)、集成陀螺仪传感器(如快递卡片)、使用 AVPlayer 播放音乐等。
通信方式:传统卡片通过 postCardAction 向指定的 Ability 发送事件,接收方在 onFormEvent 中处理。互动卡片除了支持 postCardAction,还能通过 LocalStorage 与主 Ability 共享数据,并通过 callee.on('cardAction', ...) 注册方法调用处理。
从代码结构上看,传统卡片的 UI 页面位于 widget/pages/ 目录下,而互动卡片的 UI 页面位于 livecardability/pages/ 目录下——这不仅仅是路径的差异,更代表了两种完全不同的运行环境。
LiveFormExtensionAbility 生命周期
onLiveFormCreate 的同步调用约束
LiveFormExtensionAbility 的核心生命周期方法是 onLiveFormCreate,它在用户将卡片添加到桌面时被调用。该方法接收两个参数:LiveFormInfo(携带卡片元数据)和 UIExtensionContentSession(用于加载卡片 UI)。
在 LiveCard 项目中,我们可以看到同步与异步两种写法:
[entry/src/main/ets/livecardability/SleepLiveCardAbility.ets]
export class SleepLiveCardAbility extends LiveFormExtensionAbility {
onLiveFormCreate(liveFormInfo: LiveFormInfo, session: UIExtensionContentSession): void {
let storage: LocalStorage = new LocalStorage();
storage.setOrCreate('context', this.context);
storage.setOrCreate('session', session);
let formId: string = liveFormInfo.formId;
storage.setOrCreate('formId', formId);
let borderRadius: number = liveFormInfo.borderRadius;
storage.setOrCreate('borderRadius', borderRadius);
let formRect: formInfo.Rect = liveFormInfo.rect;
storage.setOrCreate('formRect', formRect);
try {
session.loadContent('livecardability/pages/SleepLiveCard', storage);
} catch (error) {
Logger.error(TAG, `loadContent catch error, code: ${error.code}, message: ${error.message}`);
}
}
}
而音乐卡片则是异步版本:
[entry/src/main/ets/livecardability/MusicLiveCardAbility.ets]
export class MusicLiveCardAbility extends LiveFormExtensionAbility {
async onLiveFormCreate(liveFormInfo: LiveFormInfo, session: UIExtensionContentSession): Promise<void> {
// ... 初始化 LocalStorage ...
session.loadContent('livecardability/pages/MusicLiveCard', storage);
// 异步数据加载
let actionData = MusicFileStore.getTriggerAction(this.context);
let songRdbHelper = SongRdbHelper.getInstance(this.context);
let initSongs: SongItem[] = await songRdbHelper.queryAllSongs();
// ... 继续初始化 ...
}
}
这里有一个重要的设计考虑:虽然 onLiveFormCreate 可以声明为 async,但 session.loadContent() 的调用应该尽早执行,不应被 await 阻塞。音乐卡片的做法是正确的——先调用 loadContent 加载 UI,再执行异步数据初始化,数据通过 LocalStorage 传递给卡片组件。
LiveFormInfo 的数据结构
LiveFormInfo 包含了互动卡片运行所需的核心参数:
- formId:卡片的唯一标识,后续所有
formProvider.updateForm()操作都需要它 - borderRadius:卡片的圆角值,用于与卡片 UI 保持一致
- rect:
formInfo.Rect类型,包含width、height、left、top,标识卡片在桌面的位置和尺寸
这些参数通过 LocalStorage 传递到卡片组件中,使得组件能够自适应桌面布局。
sceneAnimationParams 机制
abilityName 的配置与系统激活流程
在 form_config.json 中,每个卡片的配置都包含 sceneAnimationParams 字段:
[entry/src/main/resources/base/profile/form_config.json]
{
"forms": [
{
"name": "SleepCard",
"src": "./ets/widget/pages/SleepCard.ets",
"isDynamic": true,
"sceneAnimationParams": {
"abilityName": "SleepLiveCardAbility"
}
},
{
"name": "DeliveryCard",
"sceneAnimationParams": {
"abilityName": "DeliveryLiveCardAbility",
"triggerTypes": ["shake"]
}
}
]
}
sceneAnimationParams 是传统卡片与互动卡片之间的"桥梁"。它的工作流程如下:
- 用户将卡片添加到桌面,系统首先加载传统卡片(
widget/pages/SleepCard.ets),这是一个普通的 Form 卡片 - 当用户点击卡片或满足触发条件时,系统检测
sceneAnimationParams.abilityName配置 - 系统激活对应的
LiveFormExtensionAbility(即SleepLiveCardAbility) onLiveFormCreate被调用,互动卡片 UI 通过UIExtensionContentSession加载- 此时桌面上的卡片从"静态 widget"无缝切换为"互动卡片"
这个机制实现了一种"渐进式增强"的卡片体验——用户看到的是同一张卡片,但系统在后台完成了从轻量 widget 到全功能 UIExtension 的升级。
对于快递卡片,triggerTypes 还包含 "shake",这意味着除了点击触发外,系统还可以通过"摇一摇"传感器事件来激活互动状态。
多卡片协作架构
LiveCard 项目中存在四种完全不同的互动卡片,它们如何共享同一个主 Ability 却又各自独立运行?答案藏在架构设计中。
单 Ability + 多 Extension 模式
┌─────────────────────────────────────────────┐
│ EntryAbility │
│ (LiveCardAbility / UIAbility) │
│ │
│ ├─ CardActionHandler (callee 注册) │
│ ├─ MediaService (全局音乐服务) │
│ └─ FormUtils (数据管理) │
├─────────────────────────────────────────────┤
│ │
│ ExtensionAbilities (各自独立进程上下文) │
│ │
│ SleepLiveCardAbility ─── SleepLiveCard.ets │
│ DeliveryLiveCardAbility ─── DeliveryLiveCard│
│ ExerciseLiveCardAbility ─── ExerciseLiveCard│
│ MusicLiveCardAbility ─── MusicLiveCard │
└─────────────────────────────────────────────┘
每个 LiveFormExtensionAbility 都是独立的 Extension,拥有自己的 Context。它们通过 LocalStorage 与各自的卡片页面通信,互不干扰。
跨进程通信的三条路径
卡片与主 Ability 之间的通信主要通过以下三种方式:
路径一:postCardAction + CALL 模式
[entry/src/main/ets/utils/ActionUtils.ets]
public playByAction(component: object, type: PlayActionType, formId: string): void {
postCardAction(component, {
action: FormCarAction.CALL,
abilityName: ENTRY_ABILITY, // "LiveCardAbility"
params: {
method: 'cardAction',
actionType: CardActionType.PLAY_ACTION,
playActionType: type,
formId: formId,
},
});
}
主 Ability 通过 this.callee.on('cardAction', CardActionHandler.getHandler()) 注册处理方法,CardActionHandler 根据 actionType 分发到不同的处理逻辑。
路径二:postCardAction + MESSAGE 模式
public requestOverFlow(component: object, widthRatio: number, heightRatio: number, duration: number): void {
postCardAction(component, {
action: FormCarAction.MESSAGE,
abilityName: ENTRY_FORM_ABILITY, // "EntryFormAbility"
params: {
message: 'requestOverflow',
widthRatio: widthRatio,
heightRatio: heightRatio,
duration: duration
},
});
}
MESSAGE 消息发送到 EntryFormAbility,在 onFormEvent 回调中处理。这主要用于触发卡片的"溢出动画"(overflow animation)。
路径三:formProvider.updateForm 推送数据
public async updateExerciseCardState(context: Context, state: number): Promise<void> {
let formList: FormInfo[] = await FormRdbHelper.getInstance(context).queryFormByName('ExerciseCard');
formList.forEach((formInfo) => {
class ExerciseUpdateData {
public currentState: number = state;
public calories: number = 0;
}
this.updateForm(formInfo.formId, new ExerciseUpdateData());
});
}
主 Ability 通过 formProvider.updateForm() 主动向所有同名卡片推送数据更新。卡片组件上使用 @LocalStorageProp 或 @LocalStorageLink 装饰器接收这些数据。
数据持久化的统一入口
虽然每种卡片各有独立的数据存储(ExerciseFileStore、MusicFileStore、FormRdbHelper),但所有数据操作都通过 FormUtils 这个统一入口类对外暴露。主页面、卡片、Extension Ability 都通过 FormUtils 来读写数据,保持了数据层的整洁。
关键配置解读
form_config.json 中的关键配置项
isDynamic: true
启用卡片的动态能力。这是互动卡片的基础开关,设置为 true 后,卡片才能接收 formProvider.updateForm() 的更新。
supportDimensions: ["2*4"]
定义卡片支持的网格尺寸。睡眠卡片和音乐卡片使用 2*4(两列四行),快递卡片和运动卡片使用 2*2。这决定了卡片在桌面的布局占比。
defaultDimension: "2*4"
卡片首次添加到桌面时的默认尺寸。
updateEnabled: false
禁用系统的自动定时更新机制。LiveCard 项目中的卡片通过 formProvider.updateForm() 手动触发数据更新,因此不需要系统级的定时刷新。
sceneAnimationParams.abilityName
如前所述,这是连接传统 widget 与互动卡片 Extension 的桥梁,指定了激活哪一个 LiveFormExtensionAbility。
module.json5 中的关键配置项
type: "liveForm"
这是将 ExtensionAbility 标记为互动卡片类型的关键。没有这个字段,系统不会将其识别为 LiveForm,onLiveFormCreate 也不会被调用。
"metadata" 的差异:传统卡片需要 metadata 字段指定 ohos.extension.form 资源配置,而互动卡片不需要——因为互动卡片的配置直接在 form_config.json 中通过 sceneAnimationParams 引用。
@Entry({ useSharedStorage: true })
互动卡片页面组件上的装饰器参数。useSharedStorage: true 表示使用共享存储,这是与 LiveFormExtensionAbility 共享 LocalStorage 数据的前提。
源码走读:SleepLiveCardAbility 初始化流程
最后,我们以睡眠卡片为例,完整走通一条初始化链路。
Step 1:传统卡片加载
用户添加卡片,系统加载 widget/pages/SleepCard.ets。这个页面是静态的,显示憨憨和三叶草的静态图片,通过 isSleep 状态切换"睡觉"和"起床"两种样式。
用户点击卡片时,调用 ActionUtils.requestOverFlow() 通过 MESSAGE 通知 EntryFormAbility:
.onClick(() => {
if (this.isSleep) {
ActionUtils.requestOverFlow(this, LiveCardScale.SLEEP_WIDTH,
LiveCardScale.SLEEP_HEIGHT, LIVE_CARD_DURATION);
} else {
ActionUtils.jumpAppPage(this, 'SleepReport');
}
})
Step 2:系统激活 LiveFormExtensionAbility
EntryFormAbility.onFormEvent 接收到 requestOverflow 消息后,调用 formProvider.requestOverflow() 触发溢出动画。同时,因为 form_config.json 中 SleepCard 配置了 sceneAnimationParams.abilityName: "SleepLiveCardAbility",系统自动激活 SleepLiveCardAbility。
Step 3:onLiveFormCreate 执行
onLiveFormCreate(liveFormInfo: LiveFormInfo, session: UIExtensionContentSession): void {
// 1. 创建 LocalStorage,建立数据通道
let storage: LocalStorage = new LocalStorage();
storage.setOrCreate('context', this.context);
storage.setOrCreate('session', session);
// 2. 从 LiveFormInfo 提取卡片元数据
let formId: string = liveFormInfo.formId;
storage.setOrCreate('formId', formId);
let borderRadius: number = liveFormInfo.borderRadius;
storage.setOrCreate('borderRadius', borderRadius);
let formRect: formInfo.Rect = liveFormInfo.rect;
storage.setOrCreate('formRect', formRect);
// 3. 加载互动卡片 UI 页面
session.loadContent('livecardability/pages/SleepLiveCard', storage);
}
Step 4:互动卡片 UI 启动
SleepLiveCard 组件通过 @LocalStorageProp 从 LocalStorage 中获取 formId、formRect、borderRadius。
在 aboutToAppear 中初始化帧动画的累积时间表,并在 1 秒后通过 formProvider.updateForm() 通知传统卡片更新状态:
aboutToAppear(): void {
this.initCumulativeTimes();
setTimeout(() => {
let formMsg: formBindingData.FormBindingData = formBindingData.createFormBindingData({
isSleep: false
});
formProvider.updateForm(this.formId, formMsg);
}, 1000);
}
Step 5:帧动画驱动
卡片通过 setInterval 每 16ms(约 60fps)更新一帧,根据经过时间计算当前应显示的帧索引。帧列表通过 FrameItem 的 weight 属性支持"停留时长权重",使某些关键帧可以多停留一段时间,形成更自然的动画节奏。
startImageSync(): void {
this.animStartTime = Date.now();
this.imageSyncTimer = setInterval(() => {
const elapsed = Date.now() - this.animStartTime;
// 根据经过时间计算帧索引
const newHanhanFrame = this.getFrameByElapsed(elapsed, this.hanhanCumulativeTime, ...);
const newCloverFrame = this.getFrameByElapsed(elapsed, this.cloverCumulativeTime, ...);
// 更新状态,驱动 UI 刷新
this.currentHanhanFrameIndex = newHanhanFrame;
this.currentCloverFrameIndex = newCloverFrame;
}, 16);
}
当动画播放完毕(达到 LIVE_CARD_DURATION = 3500ms),憨憨从"睡眠"状态切换为"起床"状态,同时通过 formProvider.updateForm() 反向同步传统卡片的状态——实现了互动卡片与传统卡片的数据联动。
总结
互动卡片的架构设计呈现了 HarmonyOS 服务卡片从"静态展示"到"动态交互"的演进路径。通过 LiveFormExtensionAbility + UIExtensionContentSession 的组合,开发者获得了完整的 UI 渲染能力;而 sceneAnimationParams 机制则巧妙地解决了传统 widget 与互动卡片之间的衔接过渡问题。LiveCard 项目展示的四种卡片各具特色,在共享同一套架构基础的同时,通过 postCardAction 通信、formProvider.updateForm 数据推送、以及 callee 方法注册等机制,实现了丰富的交互体验。
更多推荐

所有评论(0)