【共创季稿事节】动图魔方技术拆解 04:HarmonyOS 6.1 实况窗与服务卡片实践:从功能接入到上架能力门控
SEO 信息
- SEO 标题:【共创季稿事节】动图魔方技术拆解 04:HarmonyOS 6.1 实况窗与服务卡片实践:从功能接入到上架能力门控
- SEO 摘要:基于 HarmonyOS NEXT / ArkTS 项目“动图魔方”,拆解一个 GIF 导出工具如何把后台长任务、服务卡片和实况窗能力接进真实工程:用
BackgroundTasksKit保住后台导出,用FormExtensionAbility+formProvider.updateForm推进度,用显式开关门控LiveViewKit,把“开发态无法启用”的平台约束写进代码结构而不是留成口头 TODO。 - 关键词:HarmonyOS, ArkTS, LiveViewKit, FormExtensionAbility, BackgroundTasksKit, 服务卡片, 实况窗, 后台任务, GIF 导出
- 文章封面:
https://i-blog.csdnimg.cn/direct/d1be1e1bcf6740588450ecd4489c5152.png - 投稿方向:HarmonyOS 6.1 创新特性适配实战
- 项目环境:HarmonyOS SDK
6.1.0(23)、ArkTS、DevEco Studio、GIFRubiksCube
“动图魔方”前 3 篇分别解决了首页沉浸光感、视频抽帧编码和本地优先素材链路,但真正把导出做成一个像样的工具,还差最后一层体验闭环:用户把 App 切到后台后,长任务不能突然失联,导出进度要有能看见、能点回 App、能在能力未批准时安全降级的承接面。
一、真实工程问题背景
很多工具类 App 在做导出体验时,容易只停留在页面内进度条。
这在桌面 Demo 里没问题,但一到真实手机场景,问题马上暴露出来:
- 用户导出大素材时会退到后台,前台页面进度条就失去意义了。
- 如果后台任务能力没接,系统很可能在切后台后回收执行链路。
- 如果直接上实况窗,开发阶段又会被“应用需先上架并申请能力”卡住。
- 如果没有一个显式门控,未获批能力就会变成随机报错点。
- 如果没有服务卡片这样的稳定降级面,导出中与待命态都缺少系统级承接入口。
所以第 04 篇要解决的不是“能不能调用一个新 API”,而是怎样把下面 4 件事串成一条可靠链路:
- 导出切后台后继续执行。
- 进度能被系统级 UI 承接。
- 未获批能力在开发态完全惰性。
- 失败或不支持时不影响 GIF 主流程。
二、目标与边界
当前这一版实现的目标很明确:
- 导出开始后申请后台长任务,避免退后台后直接中断。
- 已添加到桌面的服务卡片可以显示“进行中 / 百分比 / 阶段文案”。
- 实况窗能力在代码里预留接入点,但默认关闭,不对开发态撒谎。
- 导出结束、取消、异常时统一回收后台任务并把卡片恢复为待命态。
边界也必须说清楚:
- 当前项目没有把 Live View 当成开发期可验证能力。
LIVE_VIEW_CAPABILITY_GRANTED默认是false,不会在开发态调用任何liveViewManager接口。- 服务卡片是当前可落地、可验证、可降级的主承接面。
- 本文聚焦“导出期间的系统能力门控”,不展开 GIF 编码细节。
这也是这篇文章最有工程价值的地方:不是硬把一个平台限制包装成“已经做完”,而是把限制本身写进架构。
三、链路拆分:后台任务、实况窗、服务卡片如何串起来
项目里这条链路被拆成了 4 层:
| 层级 | 责任 | 对应文件 |
|---|---|---|
| 导出页面层 | 发起导出、接收进度、统一编排开始/更新/结束 | entry/src/main/ets/pages/Index.ets |
| 后台任务层 | 申请/释放后台长任务能力 | entry/src/main/ets/services/BackgroundRenderService.ets |
| 实况窗门控层 | 预留实况窗接入点并做显式开关控制 | entry/src/main/ets/services/LiveViewService.ets |
| 服务卡片层 | 维护 formId 注册表并把进度推送到所有卡片 | entry/src/main/ets/services/CardBridge.ets、entry/src/main/ets/entryformability/EntryFormAbility.ets、entry/src/main/ets/widget/pages/WidgetCard.ets |
这样拆的好处是:
- 页面层只负责“当前导出状态是什么”。
- 系统能力层各自封装,不把页面代码绑死到具体 Kit。
- Live View 能力获批前后,导出主流程都不用改动。
- 服务卡片和实况窗可以共用同一套进度语义。
四、关键实现
4.1 先把后台长任务接上,保证导出退后台不掉链
如果用户一边导出一边切到后台,最先要保住的不是 UI,而是执行权。
BackgroundRenderService 的做法很直接:通过 backgroundTaskManager.startBackgroundRunning() 申请 DATA_TRANSFER 模式后台任务,并用 WantAgent 指回当前 Ability。
export class BackgroundRenderService {
private static active: boolean = false;
static async start(context: common.UIAbilityContext): Promise<void> {
if (BackgroundRenderService.active) {
return;
}
try {
const info: wantAgent.WantAgentInfo = {
wants: [
{
bundleName: context.abilityInfo.bundleName,
abilityName: context.abilityInfo.name
}
],
operationType: wantAgent.OperationType.START_ABILITY,
requestCode: 0,
wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
};
const agent: WantAgent = await wantAgent.getWantAgent(info);
await backgroundTaskManager.startBackgroundRunning(
context,
backgroundTaskManager.BackgroundMode.DATA_TRANSFER,
agent
);
BackgroundRenderService.active = true;
} catch (err) {
}
}
}
这里有两个工程判断值得单独拎出来:
- 失败是允许的。没有把后台能力申请失败等同于导出失败。
active标记让重复点击导出时不会反复申请后台任务。
同时 entry/src/main/module.json5 里明确声明了权限:
"requestPermissions": [
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
}
]
这段配置本身就是本文的重要证据,因为它说明项目确实进入了“长任务可持续运行”的工程语境,而不只是页面上写了个进度条。
4.2 实况窗不冒进:用显式门控把平台限制变成代码结构
很多文章会把实况窗写成“接下来补一下就行”的泛泛 TODO,但这类能力最大的风险恰恰是平台前置条件不在代码里。
项目里的处理方式更保守,也更真实:
import { liveViewManager } from '@kit.LiveViewKit';
const LIVE_VIEW_CAPABILITY_GRANTED = false;
export class LiveViewService {
private static active: boolean = false;
private static async enabled(): Promise<boolean> {
try {
return await liveViewManager.isLiveViewEnabled();
} catch (err) {
return false;
}
}
static async start(title: string, percent: number): Promise<void> {
if (!LIVE_VIEW_CAPABILITY_GRANTED || LiveViewService.active) {
return;
}
if (!await LiveViewService.enabled()) {
return;
}
LiveViewService.active = true;
// TODO: 能力正式获批后在这里接 startLiveView
}
}
这段代码的价值不在于“已经点亮了实况窗”,而在于它明确表达了三件事:
- 未获批前,
LiveViewKit是完全惰性的。 - 就算系统接口存在,也不会因为误调用把导出主流程搞崩。
- 真正接入时只需补
start / update / stop,不必重写页面编排。
这就是我在 HarmonyOS 项目里很看重的一种写法:先把能力门槛写死,再谈体验增强。否则一上线前发现没有审批通过,整条链路都得回炉。
4.3 服务卡片用作稳定降级面,进度语义比视觉更重要
实况窗在开发态不可用,并不代表系统级进度承接无解。当前项目把服务卡片作为主承接面,这个选择比“等待未来能力开放”更务实。
CardBridge 的职责是维护已添加卡片的 formId,并在进度变化时主动刷新所有卡片:
export class CardBridge {
static buildBinding(active: boolean, percent: number, stage: string): formBindingData.FormBindingData {
const safe = percent < 0 ? 0 : (percent > 100 ? 100 : Math.round(percent));
return formBindingData.createFormBindingData({
active: active,
percent: safe,
stage: stage
});
}
static async pushAll(context: common.Context, active: boolean, percent: number, stage: string): Promise<void> {
try {
const ids = await CardBridge.getFormIds(context);
if (ids.length === 0) {
return;
}
const data = CardBridge.buildBinding(active, percent, stage);
for (let index = 0; index < ids.length; index++) {
try {
await formProvider.updateForm(ids[index], data);
} catch (err) {
}
}
} catch (err) {
}
}
}
这里最关键的不是 UI 长什么样,而是先把进度语义定稳:
active决定卡片处于导出中还是待命态。percent统一钳制到0..100。stage直接复用导出链路里的阶段文案。
一旦这 3 个字段稳定下来,服务卡片和未来实况窗都可以消费同一份数据,不会出现两套状态机。
4.4 FormExtensionAbility 负责注册卡片实例,主应用只管推状态
服务卡片最容易踩坑的点,是把“卡片生命周期”和“主应用进度更新”混成一块。
这里项目做了明确拆分:EntryFormAbility 只处理 formId 注册与初始态返回,实时进度由主应用主动推送。
export default class EntryFormAbility extends FormExtensionAbility {
onAddForm(want: Want): formBindingData.FormBindingData {
let formId = '';
if (want.parameters) {
const idValue = want.parameters['ohos.extra.param.key.form_identity'];
if (typeof idValue === 'string') {
formId = idValue;
}
}
if (formId.length > 0) {
CardBridge.addFormId(this.context, formId);
}
return CardBridge.buildBinding(false, 0, '待命中');
}
onRemoveForm(formId: string): void {
CardBridge.removeFormId(this.context, formId);
}
}
对应的 form_config.json 和 module.json5 也都有实配:
{
"name": "EntryFormAbility",
"srcEntry": "./ets/entryformability/EntryFormAbility.ets",
"type": "form",
"metadata": [
{
"name": "ohos.extension.form",
"resource": "$profile:form_config"
}
]
}
{
"forms": [
{
"name": "GifRubikWidget",
"src": "./ets/widget/pages/WidgetCard.ets",
"isDynamic": true,
"defaultDimension": "2*2"
}
]
}
这说明服务卡片不是概念设计,而是已经进入了模块注册与动态刷新配置阶段。
4.5 页面编排要统一开始、更新、结束,不要把系统能力散在各处
真正把整条链路串起来的是 Index.ets 的导出流程。
开始导出时,项目会先申请后台任务,再尝试启动实况窗,再把卡片置为“准备中”:
await BackgroundRenderService.start(ctx);
await LiveViewService.start('准备中', 0);
await CardBridge.pushAll(ctx, true, 0, '准备中');
导出进度回调时,页面并没有逐帧去刷新系统 UI,而是只在整数百分比变化时推送一次:
signal.onProgress = (done: number, total: number, stage: string) => {
const ratio = total > 0 ? done / total : 0;
this.exportProgress = ratio;
this.exportStage = stage;
const pct = Math.round(ratio * 100);
if (pct !== this.lastCardPercent) {
this.lastCardPercent = pct;
LiveViewService.update(stage, pct);
CardBridge.pushAll(ctx, true, pct, stage);
}
};
结束或取消时则统一回收:
await LiveViewService.stop();
await BackgroundRenderService.stop(ctx);
await CardBridge.pushAll(ctx, false, 0, '待命中');
这段编排看起来普通,但它解决了一个很实在的问题:系统 UI 更新频率必须被节流。否则编码阶段每处理一帧就刷一次卡片或实况窗,性能收益还没拿到,UI 层先被自己打爆了。
五、踩坑与修正
5.1 实况窗不是“有 API 就能上”,上架审批前必须接受不可用事实
这是这篇最重要的踩坑结论。
实况窗能力和普通页面组件不同,它不是 import 成功就代表可以验证。当前项目明确把“应用已上架并申请审批通过”当作前置条件,因此开发态默认不启用。
这不是保守,而是避免两个更坏的结果:
- 代码里混入一堆只在少数环境可跑的试探调用。
- 测试同学或后续维护者误以为功能“只是偶发失败”。
5.2 后台任务、卡片、实况窗都可能失败,所以主流程必须 try/catch 降级
这组系统能力没有一个适合用“失败即终止导出”来处理。
原因很简单:用户真正要的是 GIF 文件,不是系统能力全绿。
因此当前实现基本都遵循同一原则:
- 能力接上最好。
- 能力失败时静默跳过。
- 主流程只围绕导出结果成败判断。
这种设计对工具类 App 很关键。否则新增一个系统能力增强,反而把原本稳定的导出链路变脆弱了。
5.3 卡片更新语义要固定,别让不同入口各自产生自己的状态机
如果首页按钮、导出页、后台任务回调、卡片渲染各自产生一套“进行中/已完成/待命/异常”的命名,后面越接能力越乱。
当前项目提前把卡片数据压成 active + percent + stage 这 3 个字段,未来无论换卡片样式、补实况窗模板还是做通知栏兜底,都能复用这套状态表达。
这比先冲 UI 更值钱,因为它直接决定后续接能力时要不要重构。
六、截图与代码证据
6.1 导出页已经具备真实的长任务入口和进度承接场景

这张图说明导出不是抽象能力,而是已经进入真实页面流程:用户会从这里发起导出、调整参数并进入长任务阶段。后台任务、卡片、实况窗门控,服务的都是这个真实入口。
6.2 当前工程已经有稳定的首页入口与主流程承载面

这张图的意义在于证明“动图魔方”不是一个只留在实验页的系统能力样例,而是有完整主流程的工具 App。也正因为它是完整产品,导出体验才必须覆盖后台、卡片和能力门控,而不能只靠页面内提示。
6.3 module.json5、form_config 与服务实现共同构成了可验收证据
除了界面截图,这篇的另一类证据来自工程配置与代码本身:
module.json5已声明ohos.permission.KEEP_BACKGROUND_RUNNING。module.json5已注册EntryFormAbility,类型为form。form_config.json已声明GifRubikWidget动态卡片。LiveViewService.ets已把实况窗能力门控写进代码。Index.ets已把开始、更新、结束三段编排串通。
对于这类“开发态受平台约束”的能力,这比硬贴一张伪截图更有说服力,因为它反映的是可维护的工程结构。
七、工程复盘
做完这一轮之后,我对 HarmonyOS 系统能力接入有一个很明确的判断:
第一,系统能力不是越多越好,而是越可控越好。
第二,能力前置条件必须在代码里可见,而不是写在群消息或脑子里。
第三,工具类 App 的增强能力应该始终服从主流程,而不是反过来绑架主流程。
“动图魔方”这一版没有为了展示实况窗而伪造完成度,而是先用后台任务和服务卡片把真实可交付部分做稳,再给 Live View 留出无侵入接入口。这种节奏更像工程,而不是演示。
八、验收清单
| 验收项 | 结果 | 说明 |
|---|---|---|
| 后台任务权限已声明 | 通过 | module.json5 已声明 ohos.permission.KEEP_BACKGROUND_RUNNING |
| 导出开始会申请后台长任务 | 通过 | BackgroundRenderService.start() 已接入导出开始阶段 |
| 服务卡片 Extension 已注册 | 通过 | EntryFormAbility 已在 module.json5 中注册为 form |
| 卡片动态刷新链路已具备 | 通过 | CardBridge.pushAll() 使用 formProvider.updateForm() 主动推送 |
| 导出结束能统一回收状态 | 通过 | finally 中统一调用 stop() 与 pushAll(false, 0, '待命中') |
| 实况窗开发态安全降级 | 通过 | LIVE_VIEW_CAPABILITY_GRANTED = false,未获批时完全惰性 |
| 能力失败不影响 GIF 主流程 | 通过 | 后台任务、卡片、实况窗服务均采用 try/catch 降级 |
| 真实产品入口与页面场景存在 | 通过 | 已有首页与导出页真实截图作为证据 |
九、小结
第 04 篇真正想说明的是:HarmonyOS 的系统能力接入,重点从来不是“把 API 调起来”,而是把能力边界、平台前置条件、失败降级和产品主流程一起设计好。
在“动图魔方”里,后台任务负责保住执行权,服务卡片负责承担当前可落地的系统级进度面,实况窗则通过显式门控留好未来入口。这样即使实况窗审批还没到位,项目依然有一条完整、诚实、可维护的导出体验链路。
十、下一篇衔接
下一篇我会继续拆“动图魔方技术拆解 05”,主题转向 HarmonyOS 7.0 视角下的创作能力前瞻:为什么 GIF 工具会提前预留 3D 预览、多模态创作和跨设备体验的结构位置,以及这些能力在现阶段应该如何做出“不冒进但不堵死”的工程铺垫。
更多推荐

所有评论(0)