1. 锁屏卡片开发:先搞清楚你要做的是哪一种
先说结论:鸿蒙里说“锁屏卡片开发”,大多数人其实指的是两种完全不同的东西。一种是服务卡片在锁屏场景下的展示与刷新策略,另一种是锁屏通知卡片(比如媒体播放、通话、快捷开关)的原生定制。前者是应用开发者最常碰到的,后者更多是系统厂商或深度定制场景才会涉及,但两者的底层机制、调试手段、权限模型都有交集。
我最早接触这个需求,是产品经理丢过来一句话:“锁屏上要有个卡片,能显示订单状态,点一下能跳转。”当时我第一反应是——这不就是个桌面服务卡片嘛,锁屏上显示不就行了?结果真上手才发现,锁屏场景和桌面场景完全是两套逻辑:锁屏有安全校验、有省电策略、有系统窗口层级的限制,稍不注意卡片就“卡死”在锁屏上,既不刷新也不响应。
所以这篇我打算把锁屏卡片开发的完整链路拆开讲:从卡片形态选型、FormExtensionAbility的生命周期管理,到锁屏场景下的刷新与跳转适配,再到真机调试里那些文档里不会写的坑。适合已经在做鸿蒙应用开发、想往系统级交互场景深入的同学,也适合那些刚被“锁屏卡片”需求砸中、需要快速搞清楚从哪下手的开发者。
先说一个核心认知:桌面卡片和锁屏卡片在鸿蒙里不是两个独立框架,而是同一套服务卡片机制在不同窗口场景下的表现。区别在于锁屏状态下系统对卡片的能力做了收缩——比如定时刷新的频率会被压低、点击事件需要先通过锁屏校验、部分数据接口在锁屏态不可用。理解了这个前提,后面所有适配工作都围绕“如何在受限环境里保住核心体验”来展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 卡片机制选型与架构设计
2.1 服务卡片的核心链路:谁在渲染、谁在传数据
鸿蒙的服务卡片(Service Widget)不是应用往锁屏上扔一张静态图,而是一套完整的跨进程渲染机制。应用侧的代码通过 FormExtensionAbility 提供卡片的生命周期回调,卡片实际渲染则由系统的卡片框架(FormManagerService)负责,应用和卡片之间通过 FormProvider 和 FormBindingData 来传递数据。
这套架构有一个关键点:卡片并不跑在你的应用进程里。应用进程挂了,卡片依然可以显示上一次的数据;反过来,卡片要更新数据,也不能直接改UI,而是要通过 FormProvider.updateForm() 通知系统拉取新数据。这个设计思路很像Web开发里的“服务端渲染 + 客户端水合”——卡片是那个静态壳,应用是那个推送数据的服务端。
所以选型的时候第一个决策就是:你的锁屏卡片是实时性要求高(比如订单状态、媒体播放进度),还是低频数据展示(比如天气、日程、备忘录)。实时性要求高的,必须走定时刷新 + 数据回调双通道;低频展示的,用 onAddForm 时一次性绑定数据就行,不需要做复杂的刷新策略。
这里有个很多新手搞混的点:锁屏卡片和桌面卡片用的是同一套 FormExtensionAbility,但配置会多几样东西。如果目标是锁屏场景,卡片配置里要显式声明支持锁屏展示的模块,同时要处理锁屏态下的“亮屏刷新”与“息屏降载”。
2.2 卡片尺寸与样式设计:锁屏空间比你想的更紧张
桌面卡片有1x2、2x2、2x4、4x4等标准栅格,锁屏卡片则通常只能以横向窄条或居中圆角卡片形式出现。这是我在设计阶段吃过的亏——一开始直接复用桌面卡片的2x4布局,结果锁屏上一显示,上下都被系统时间、快捷入口遮住,可点击区域小得可怜。
锁屏布局的核心约束有三个:一是安全区,锁屏顶部有状态栏和系统时间,底部有快捷操作区,卡片只能落在中间的安全区间;二是像素密度,锁屏场景的显示分辨率可能和桌面不同,卡片要用 vp(虚拟像素)而不是 px 来适配;三是交互深度,锁屏卡片尽量只做一级交互,点一下进应用或解锁,不要设计多层级菜单。
我在实际项目中定下的设计规范是:锁屏卡片高度不超过屏幕高度的 25%,左右边距不少于 16vp,内容以“一行主信息 + 一行辅助信息 + 一个主操作按钮”为上限。超过这个信息量,锁屏上根本看不清楚,用户体验反而更差。
为什么锁屏卡片不能做成和桌面一样的信息密度? 原因在交互心理学:锁屏场景下用户的注意力是碎片化的,通常只有一两秒的扫视时间。卡片要做的是“一眼看到关键信息”,而不是“提供完整业务功能”。这是锁屏卡片与桌面卡片的本质差异,也是产品设计上最容易扯皮的地方。
2.3 工程结构:模块划分与依赖关系
工程上,锁屏卡片功能的代码不要和应用主模块耦合太深。我的做法是拆一个独立的 card 模块,里面只放 FormExtensionAbility、卡片数据处理器、卡片路由表。主应用通过接口层向卡片模块提供数据,卡片模块不反向依赖主应用的内部实现。这样做的直接好处是:卡片模块可以单独编译、单独调试,不跑主应用也能在卡片的预览器里看效果。
同时,卡片模块要配置独立的路由表。锁屏卡片的点击事件会通过 router 能力拉起应用页面,这里的跳转目标必须在配置里显式声明,否则锁屏状态下点击会静默失败。这是我从线上反馈里排查出来的问题——桌面点卡片能跳转,锁屏点卡片没反应,查了半天才发现跳转的页面没有在卡片的路由配置里注册。
3. 核心细节解析与实操要点
3.1 FormExtensionAbility 生命周期:onAddForm 之外的五个回调
服务卡片的生命周期远不止 onAddForm 一个。完整来看,一个卡片实例从创建到销毁会经过:
- onAddForm:卡片被添加到桌面/锁屏时触发,返回 FormBindingData 作为初始数据。
- onUpdateForm:到了刷新周期(默认30分钟一条)或主动请求刷新时触发。
- onFormEvent:卡片上有交互事件(比如点击按钮)时通过 postCardAction 触发。
- onRemoveForm:卡片被移除时触发,做资源清理。
- onAcquireFormState:查询卡片可用状态,比如判断当前是否支持添加。
- onShareForm:卡片分享相关,锁屏场景用得少,但桌面卡片很常见。
我自己在锁屏项目里踩过一个很经典的坑:onUpdateForm 在锁屏状态下不会严格按30分钟触发。原因我在前面提过——锁屏态有省电策略,系统会把后台刷新频率降到极低,甚至暂停。表现就是:解锁后卡片数据“唰”地跳成最新,但锁屏期间一直是旧数据。
要解决这个问题,不能只依赖定时刷新,必须加主动推送通道。卡片所在应用在前台时,可以通过 FormProvider.updateForm() 主动推送数据;应用在后台时,可以用后台任务(长时任务、延迟任务)来兜底,但要注意申请权限和时机限制。我的经验是:锁屏卡片最稳定的更新方式是“亮屏时刷新 + 关键事件即时推送”,而不是依赖系统定时刷新。
3.2 FormBindingData 与卡片数据格式:JSON 只是个壳
FormBindingData 接收的核心数据结构是 FormData,本质上是一个 key-value 的 JSON 对象。卡片侧通过 $xxx 语法绑定这些字段。这个机制本身不复杂,但实际用起来有几个隐蔽的坑。
第一个坑是类型精度。FormData 里的数字字段,在跨进程序列化后,浮点数精度可能丢失。如果卡片要显示金额、百分比这类数据,建议在组装 FormData 时先转成字符串再塞进去,避免“显示金额少了一分钱”这种被用户骂死的问题。
第二个坑是数据嵌套。FormData 虽然支持嵌套对象,但卡片模板里的绑定语法对深层嵌套支持有限。我的建议是:在卡片数据处理器里把嵌套结构压平成单层字段,比如把 { "user": { "name": "张三" } } 压成 { "user_name": "张三" },这样模板绑定简单,也减少跨进程序列化的开销。
第三个坑是图片数据。卡片要显示图片时,FormData 里放的是图片的 媒体库URI 或 内存图片句柄,不能直接放二进制大字段。用媒体库URI时要注意授予卡片读取权限,否则锁屏上会显示一个空白占位。我在锁屏卡片项目里就遇到过:桌面卡片图片正常,锁屏卡片图片不显示,排查后发现是锁屏态下文件描述符的访问权限被收紧了。
3.3 锁屏场景的适配:安全限制、刷新降载与点击校验
锁屏卡片和桌面卡片最大的差异,在于系统对锁屏态做了三件“额外的事”:
第一件是安全校验。锁屏状态下,卡片的点击事件不会直接跳转页面,而是先触发解锁流程。这里的关键是:如果你的卡片设计了“点击卡片直接进应用某个页面”的交互,在锁屏态下用户点击后,系统会先让用户解锁,解锁成功后才会拉起目标页面。这个流程本身是合理的,但要注意不要在卡片的点击事件里做需要立即返回结果的操作,比如“点击后马上显示一个Toast”,这在锁屏态会失败或延迟到解锁后才显示。
第二件是刷新降载。前面提到锁屏态会降低刷新频率,但更隐蔽的是——锁屏态下 onUpdateForm 里的网络请求可能直接失败。因为系统对锁屏态的应用网络访问有收紧策略,尤其是非前台应用的网络请求会被延迟。我的做法是:在 onUpdateForm 里优先读本地缓存,再尝试网络更新,而不是一上来就请求网络,否则锁屏场景下卡片很容易“刷不出数据”。
第三件是窗口层级约束。锁屏卡片的窗口层级在系统里是有明确限制的,普通应用创建的卡片窗口不能覆盖系统关键信息区(时间、状态栏、快捷入口)。在代码层面,这意味着卡片布局不要尝试用绝对定位“贴边”或“顶到屏幕顶部”,需要用安全区域的 padding 来保证内容不被系统元素遮挡。
4. 实操过程与核心环节实现
4.1 卡片module配置:从零搭一个可运行的锁屏卡片
如果你是从零开始接锁屏卡片需求,第一步不是写代码,而是在工程里加一个 card 模块。我在 DevEco Studio 里的操作流程是:
新建一个 Module,类型选 Form(或者用 Empty Ability 模板后手动补 FormExtensionAbility)。模块会生成一个 FormExtensionAbility 的子类和一个 form_config.json 配置文件。这里有几个关键字段要说明:
json复制{
"forms": [
{
"name": "LockScreenCard",
"displayName": "锁屏订单卡片",
"description": "锁屏展示订单状态",
"src": "./js/pages/index/pages.js",
"uiSyntax": "arkts",
"window": {
"designWidth": 720,
"autoDesignWidth": true
},
"isDefault": true,
"updateEnabled": true,
"scheduledUpdateTime": "10:30",
"updateDuration": 1,
"defaultDimension": "2*2",
"supportDimensions": ["2*2"]
}
]
}
注意 updateDuration 这个字段——它代表卡片定时刷新的频率档位,单位是小时,最小是1。也就是系统定时刷新最短也要1小时一条,但正如前面说的,锁屏态下实际刷新间隔会更长。所以这个字段别把它当成实时刷新的保证,它只是一个“最低频率”的约定。
然后需要在 module.json5 里注册 FormExtensionAbility:
json复制{
"extensionAbilities": [
{
"name": "LockScreenFormAbility",
"srcEntry": "./ets/forms/LockScreenFormAbility.ets",
"label": "$string:lock_screen_form_label",
"description": "$string:lock_screen_form_desc",
"type": "form",
"metadata": [
{
"name": "ohos.extension.form",
"resource": "$profile:form_config"
}
]
}
]
}
这里有一个容易漏掉的配置:metadata 里的 resource 一定要指向 form_config.json 对应的 profile 资源。如果这个配置缺失,卡片会在添加时直接报错“Form bind failed”,而且日志里只会给一句很模糊的错误信息,新手很难定位。
4.2 卡片页面:用 ArkTS 写一个响应锁屏数据的卡片
卡片的 UI 页面是独立的 ArkTS 文件,它不依赖主应用的页面栈,而是通过 postCardAction 和 LocalStorage 与 FormExtensionAbility 通信。
一个最简的锁屏卡片页面长这样:
typescript复制let storage = new LocalStorage();
@Entry
@Component
struct LockScreenCard {
@LocalStorageProp('title') title: string = '默认标题';
@LocalStorageProp('status') status: string = '待配送';
@LocalStorageProp('buttonText') buttonText: string = '查看详情';
build() {
Column({ space: 8 }) {
Text(this.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(this.status)
.fontSize(14)
.fontColor('#666666')
.maxLines(1)
Button(this.buttonText)
.width('100%')
.height(32)
.fontSize(14)
.onClick(() => {
postCardAction(this, {
action: 'router',
abilityName: 'OrderDetailAbility',
params: { orderId: this.orderId }
});
})
}
.padding(16)
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
.borderRadius(16)
}
}
这里重点说下 postCardAction 的三种 action 区别:
- router:拉起应用内的 Ability,适合“点击卡片进详情页”。
- message:把事件传给 FormExtensionAbility 的 onFormEvent 处理,适合“在卡片上做小交互,比如切换显示模式”。
- call:拉起应用并执行指定方法,适合跨应用的复杂调度,但在锁屏场景会被安全策略限制,不建议优先选。
锁屏场景下,router 和 message 是主要交互方式。我在模拟器上验证过,锁屏态点击卡片,系统会先拉起锁屏校验,校验通过后才执行 action 对应的跳转或消息回调。这里要注意的是,不要在按钮的 onClick 里直接写业务逻辑,而是通过 postCardAction 把事件抛出,由 FormExtensionAbility 或页面侧统一处理。
4.3 FormExtensionAbility 实现:数据组装、刷新与事件回调
FormExtensionAbility 是实现卡片数据链路的核心,我通常这样写:
typescript复制import { FormExtensionAbility, formBindingData, formProvider } from '@kit.FormKit';
import { Want } from '@kit.AbilityKit';
export default class LockScreenFormAbility extends FormExtensionAbility {
onAddForm(want: Want) {
const formData = {
title: '订单状态',
status: '配送中,预计今天18:00送达',
buttonText: '查看详情',
orderId: '20240215001'
};
return formBindingData.createFormBindingData(formData);
}
onUpdateForm(formId: string) {
// 优先读本地缓存,避免锁屏态网络请求失败
const cachedData = this.loadLocalCache(formId);
if (cachedData) {
formProvider.updateForm(formId, formBindingData.createFormBindingData(cachedData));
}
// 尝试网络更新,失败时静默,不影响已展示的数据
this.tryNetworkRefresh(formId);
}
onFormEvent(formId: string, message: string) {
if (message === 'toggleStatus') {
// 切换卡片显示模式
const newData = { ... };
formProvider.updateForm(formId, formBindingData.createFormBindingData(newData));
}
}
onRemoveForm(formId: string) {
// 清理缓存、取消网络任务
}
}
这里面有个细节值得展开:onAddForm 返回的 FormBindingData 决定了卡片首次渲染的数据。如果这个数据组装得慢(比如要从数据库查订单),用户添加卡片时会看到一个白屏或加载态,体验很差。我的优化手段是:onAddForm 里先返回一个带“骨架屏”数据的 FormBindingData,比如 title 显示“加载中”,然后立刻触发一次异步刷新,用真实数据覆盖。这样用户感知到的是“卡片先出来,内容随后更新”,而不是“点了添加半天没反应”。
另外一个经验是:onUpdateForm 里的网络请求务必做超时和重试控制。系统对卡片的刷新回调是有时间预算的,如果在这个回调里执行了耗时的同步操作,会拖慢系统卡片框架,甚至导致“卡片更新失败”的系统级提醒。我一般把网络请求包一层 async 函数,3秒超时,失败不重试,保持回调轻量。
4.4 真机验证:锁屏卡片的添加、预览与刷新测试
锁屏卡片的验证比桌面卡片麻烦,因为模拟器对锁屏卡片的支持是有限的。我踩过一个大坑:在 DevEco Studio 模拟器上添加卡片到锁屏,显示一切正常,但换到真机上锁屏卡片直接不出现。后来排查发现,模拟器的锁屏场景走的是简化逻辑,对卡片的窗口层级和刷新策略没有真机那么严格。
真机验证的关键步骤我梳理一下:
- 应用签名后安装到真机,确保卡片模块没有因签名问题被系统拒载。
- 在桌面上添加一次卡片,确认卡片基本渲染正常。
- 长按卡片或通过卡片预览器,选择“添加到锁屏”(不同系统版本入口不同,一般是通过卡片编辑菜单)。
- 按电源键锁屏,观察锁屏界面上卡片是否显示。
- 模拟数据变化:在应用里修改订单状态,回到桌面看卡片是否更新,再锁屏看锁屏卡片是否同步更新。
最后一步特别重要——锁屏卡片的“数据新鲜度”是一个典型的联调问题。我在项目里经常会遇到:桌面卡片已经显示最新数据了,锁屏卡片还是旧的。原因通常是锁屏态下拉起后台任务受限,数据推送没有触达。遇到这种情况,先别急着改代码,要看日志里的卡片更新记录,确认是“系统没有触发刷新”还是“触发了但数据源拿到的还是旧数据”,前者是机制问题,后者是数据链路问题。
5. 常见问题与排查技巧实录
5.1 “卡片在锁屏上不显示”的五种可能
这个是我被问得最多的一个问题,实际排查下来,原因五花八门,我见过的案例至少有五种:
| 现象 | 可能原因 | 排查方式 |
|---|---|---|
| 锁屏完全没有卡片入口 | 系统版本不支持锁屏卡片,或卡片未配置锁屏展示能力 | 确认系统版本,检查 forms 配置里是否声明锁屏场景 |
| 卡片在桌面正常,锁屏空白 | 卡片窗口高度异常,被系统裁剪掉 | 检查卡片布局的 vp 尺寸,不要使用固定 px |
| 锁屏能显示,但点不了 | 点击事件被锁屏安全策略拦截 | 检查是否使用 postCardAction,避免直接跳转 |
| 锁屏卡片显示的是旧数据 | 锁屏态刷新被降载 | 检查是否依赖定时刷新,增加主动推送通道 |
| 锁屏卡片出现,但样式错乱 | 卡片的 designWidth 与锁屏场景不一致 | 检查 form_config.json 的 window 配置,开 autoDesignWidth |
排查时我习惯用一个笨但有效的办法:把卡片先放到桌面上调试,完全正常后再切锁屏场景。这样能把“卡片本身的问题”和“锁屏场景的问题”分开,避免一次面对太多变量。
5.2 postCardAction 在锁屏态失效的排查
postCardAction 的 action 在锁屏态生效,但有个前提——目标页面必须在应用中配置为可被卡片拉起。如果你的跳转目标是一个普通页面,桌面场景点卡片能进去,锁屏场景却死活进不去,八成是路由配置的问题。
检查清单:
- 目标 Ability 是否在 module.json5 的 abilities 数组里声明,且 exported 字段为 true。
- 卡片的 router action 里传的 abilityName 是否与声明一致,注意大小写和模块名。
- 如果目标页面需要传参,params 里的 key 是否与页面接收参数的 key 一致。
- 锁屏态跳转前是否会先触发解锁,避免在卡片点击事件里做耗时操作。
我自己的坑是:在卡片里用了 router 跳转,但给目标 Ability 的 exported 配成了 false。桌面场景下跳转成功了(可能因为系统有兜底),锁屏场景却直接失败。这个配置在文档里写得比较隐蔽,非告警类错误很容易忽略。
5.3 刷新频率与省电策略的平衡
锁屏卡片如果刷新太频繁,用户会骂“耗电”;刷新太低频,用户会骂“数据不准”。这个度怎么拿捏,我在实践中总结了一套方法:
第一,区分数据优先级。订单状态、物流进度这类用户主动关注的数据,用“事件驱动”刷新——应用侧在数据变化时主动推送一条更新到卡片;天气、日期这类通用信息,用“系统定时刷新”兜底即可。
第二,充分利用 updateDuration 的档位。如果业务上不要求实时,直接配 1 小时一条;如果要求实时,不要试图把 updateDuration 配得更小,而是把更新机制切到主动推送。
第三,排查时关注 Battery Optimization 的影响。有相当一部分“锁屏卡片不更新”的线上反馈,不是代码问题,而是用户开启了省电模式,系统把后台刷新任务挂起了。这时可以引导用户关闭该应用的省电限制,但不要在产品里承诺“一定会实时更新”。
5.4 锁屏卡片的内存与性能优化
锁屏卡片从系统角度看,是优先级不低的界面元素,但它的运行环境比桌面卡片更抠内存。我在性能优化上做过几件事:
- 图片缓存策略:锁屏卡片要显示图片时,图片尺寸要压缩到卡片实际渲染大小的2倍以内,别用原图。一张原图2MB,锁屏卡片渲染一张72x72的缩略图,白白浪费性能。
- 减少卡片上的动画:锁屏卡片不要做持续循环动画,一方面耗电,另一方面系统可能直接冻结动画帧率。
- onRemoveForm 里清理资源:移除卡片时,要取消未完成的网络请求、释放图片缓存。我见过一个项目,因为 onRemoveForm 没做清理,用户反复添加/移除卡片会导致内存持续上涨。
6. 从锁屏卡片到系统级交互的延伸思考
做完锁屏卡片之后,你会发现这套机制的价值不止于“锁屏显示一张卡片”。服务卡片的本质是把应用的核心信息和交互前置到系统最表层,而锁屏是手机系统里用户触达频率最高的界面之一。从商业角度讲,锁屏卡片是应用争夺用户注意力的高价值入口;从技术角度讲,它也是验证你对自己应用“原子化服务能力”理解深度的试金石。
我个人实践中比较受益的一个思考方式:把“锁屏卡片”当成“应用在锁屏场景下的最小可用产品”来设计。不要试图把完整业务搬上锁屏,而是提炼用户在这个场景下最需要的那一个信息、那一个动作。订单类应用,锁屏就显示“最新状态 + 查看详情”;音乐类应用,锁屏就显示“封面 + 播放/暂停”;天气类应用,锁屏就显示“当前温度 + 天气图标”。少即是多。
另外,锁屏卡片开发里积累的调试经验——比如跨进程数据传参、权限模型排查、系统级省电策略适配——这些能力在后续做 HarmonyOS 的桌面卡片、实况窗(Live View)、服务流转等功能时都能迁移复用。我建议你把这一套走通之后,做一个小工具把卡片的数据链路日志可视化,后面排查问题会快很多。
最后再分享一个小技巧:开发期可以在代码里临时加一个 Debug 入口,把 FormBindingData 的组装结果实时打印到日志里,这样锁定数据问题时不用反复解锁锁屏、盯卡片的UI变化,直接看数据层对不对。真机上线前记得把这个 Debug 入口去掉,打印日志在锁屏高频场景下会影响性能和隐私安全。
