在「幻兽契约」这类原生养成应用中,把角色搬到 HarmonyOS 桌面卡片后,难点不只是“看起来像”。项目采用 ArkTS/ArkUI V2,内置自然、火、水三系幻兽,共用养成规则和存档。桌面卡片需要同时处理主题资源、透明背板、点击消息幂等、LiveForm 摇一摇动效和 V1/V2 状态交界。下面按实践中遇到的问题拆解。
一、主题资源与透明背板:先统一物种和深浅色
三系幻兽分别对应草木绿、暖橙和水蓝。项目把“物种”和“深浅色模式”拆成两个维度:物种决定视觉主题,颜色模式决定同一主题下的资源。入口组件用 @Provider 提供当前物种,子组件用 @Consumer 获取;base、dark 资源目录保持相同资源名。领养前用预览选择,领养后用存档物种,桌面快照也携带物种和主题偏好,保证应用与卡片一致。- @Provider() elementSpecies: string = '';
- @Monitor('store.snapshot', 'previewSpeciesId')
- onElementSpeciesChanged(): void {
- this.elementSpecies = this.store.snapshot?.pet.speciesId ?? this.previewSpeciesId;
- }
复制代码 配色可读性方面,项目按 W3C 相对亮度和对比度方法计算主按钮纯色背景与文字色,六组配色均高于普通文本常用的 4.5:1 阈值。插画和壁纸上的文字仍需实机观察,纯色配对计算不能替代实际画面。
透明背板是另一个关键。页面背景透明之外,还要配合系统背板透明能力:申请开放能力、使用对应手动签名 Profile、确认开关启用、更新 Profile 并重新签名安装。项目为卡片单独增加配置,保留普通样式,关键字段包括 transparencyEnabled、defaultDimension 和 supportDimensions。背板透明从 API 22 起提供;卡片 V2 状态接收方式要求 API 23 及以上。两种样式共用页面时,创建时把 cardStyle 记在卡片实例上,刷新和播放动效都沿用,否则去掉的白底可能在状态切换后又回来。- {
- "name": "yaqi_spirit",
- "src": "./ets/widget/pages/YaqiCard.ets",
- "uiSyntax": "arkts",
- "isDynamic": true,
- "transparencyEnabled": true,
- "defaultDimension": "2*2",
- "supportDimensions": ["2*2"]
- }
复制代码 透明后原来给文字兜底的背景也消失。项目保留气泡自身底色,把文字收在气泡里,周围继续透出壁纸。官方要求非透明区域至少占卡片面积 10%,透明卡片仍要让人看出内容和可操作入口。对幻兽卡片来说,几个气泡既是照料入口,也是桌面上的位置标记。
二、点击与存档:先解耦意图,再做幂等
卡片按钮不应直接结算存档,只负责把意图送出去:- postCardAction(this, {
- action: 'message',
- abilityName: 'YaqiFormAbility',
- params: { action: action, token: this.token }
- });
复制代码 token 关联这张卡片当时的存档 ID、修订号和食品 ID。提供方收到消息后生成 form_<token>_<action> 命令编号,同一快照上的同一动作即使消息来两遍,也能识别为同一笔操作。
有命令编号还不够。应用与卡片可能在不同进程,如果两边先读到“还有一份”再轮流写入,食物仍可能被处理两次。项目把读档也放进同一把 OS 文件独占锁,让重读、校验、结算、提交和确认都在持锁期间完成。消息进入后,判断顺序很关键:- if (command.saveId !== snapshot.saveId) {
- return this.result(command.commandId, 'rejected', 'STALE_SAVE');
- }
- const receipt = snapshot.commandReceipts.find(
- (item): boolean => item.commandId === command.commandId
- );
- if (receipt) {
- return this.result(command.commandId, 'duplicate');
- }
- if (command.expectedRevision !== snapshot.revision) {
- return this.result(command.commandId, 'rejected', 'REVISION_CONFLICT');
- }
复制代码 这里最值得停一下的是回执位置。第一下喂食成功,存档已换版本;第二条相同消息再来,如果先比较修订号,界面上就会让用户刷新重试。把成功回执放在版本检查前面,才能认出“这一口已经喂过了”,返回 duplicate,不再扣一次。项目每张卡片保留最近 16 条操作票据,每份存档最多保留 256 条命令回执;超出保留范围的旧请求仍要经过票据和版本检查,不能为了按钮继续响应就换成新版本执行。
版本变化也不一定表示喂过了。用户离开一段时间后回来喂食,程序可能先结算离线状态:点击时存档第 7 版,结算后第 8 版,喂完才是第 9 版。若进程停在第 8 版,时间已算过、食物还在背包,原请求仍指向第 7 版且没有喂食成功回执;重试应返回版本冲突并刷新卡片,把是否喂食的选择留给用户眼前这一版。相反,食物确实扣了但调用方没收到确认时,重试同一条命令应通过回执认出已完成操作。写入时项目做临时文件校验、备份、替换和读回确认;拿不准是否保存成功就再读一次,仍确认不了则画面保留上一次确认状态。手指落下时气泡可以马上缩一下,“点到了”立即回应,“喂好了”等存档确认;后续动画即使没播出来,也不能撤销已经喂掉的那一口。
三、摇一摇动效:配置之外还要补接线
普通卡片负责平时展示,短时动效由 LiveFormExtensionAbility 加载另一张激活页面。场景动效能力从 API 20 起提供,实际支持还取决于机型、宿主和系统状态。页面没加载、摇晃请求没送到,桌面上都可能表现为“幻兽一动不动”,因此排查要拆开:先确认页面如何拿到状态,再追摇一摇调用链。
项目使用 ArkUI V2,激活页需要随场景变化的 @ObservedV2 对象,但这里还会经过 V1 的 LocalStorage。最终做法是把对象留在 liveForm 进程的映射表里,通过字符串键关联页面与对象:- const model = new LiveSceneModel();
- model.sessionKey = `${info.formId}:${Date.now()}`;
- liveScenes.set(model.sessionKey, model);
- const storage = new LocalStorage();
- storage.setOrCreate('sceneKey', model.sessionKey);
- session.loadContent('liveformability/pages/YaqiLivePage', storage);
复制代码 页面拿到 sceneKey 后,再从同进程的 liveScenes 找到模型。这和卡片通过 updateForm 按变量名接收刷新是两条通道,也不意味着对象可以跨进程共享。页面已结束而异步读档刚返回时,不能让旧场景又活过来:先同步加载页面,再异步读取快照;返回时检查会话是否还在,销毁或替换场景时清理映射和定时器。项目设置 700ms 页面就绪保护和 1800ms 场景结束计时,让加载和退场有上限,这两个时间是项目自身选择。
摇一摇配置看起来很短:- {
- "sceneAnimationParams": {
- "abilityName": "YaqiLiveFormAbility",
- "triggerTypes": ["shake"]
- }
- }
复制代码 triggerTypes 从 API 26.0.0 起支持,摇晃由系统识别,应用不需要为这条路径注册加速度传感器。但配完第一次实测仍没动:日志显示桌面识别了摇晃,采集窗口内却没有完整应用激活记录。对照华为 LiveCard 示例后发现,onUpdateForm 做了数据刷新,却没有继续请求动效。补上后处理顺序为:先刷新,再看实例是否就绪、发布是否受理、动效是否开启、距上次是否已过 3 秒,满足条件才请求场景。已知节点复用回调仍然只刷新。
这条链最终接成“系统识别摇晃 → onUpdateForm → 场景请求受理 → LiveForm 创建 → 播放准入”。需要注意的是,当前公开的 FormUpdateReason 没有独立摇一摇原因值,不能只看更新原因就认定用户摇了手机;实际日志里其他宿主更新也曾创建场景。项目加了冷却和开关控制频次,但解决不了来源识别问题。因此没有顺带接入喂食或安抚:普通更新可能照常结算时间,激活页准备的是已提交存档;摇动后让幻兽回应即可,背包食物不应跟着少。
四、动画衔接与快照复用:避免过期状态回灌
触发理顺后,动画细节才显眼。常态与激活态的图像中心或容器比例只要不同,切换时幻兽就会挪位置。项目把透明样式两侧图像容器统一为 58%,并继续沿用实例里的 cardStyle,衔接是否自然仍要靠设备连续观察。
喂食姿态还遇到过回归:卡片刚发布吃东西的样子,重新构建激活页快照时又被基础姿态覆盖。直接沿用旧快照会把过期数据一起带过去。项目只复用动作图片,并给它很短有效期:最近 2.5 秒内已受理发布,卡片、存档版本、物种和成体阶段都能对上,才允许接着用;其余字段重新读取,条件不满足就回到基础姿态。这样刚才的喂食动作能接着播,吃完后也能回到平时样子。
总结
这类桌面卡片实践的核心不是单独某个 API,而是把 UI 意图、存档结算、动效播放拆成可验证的链路:透明背板保证视觉,主题资源保证一致性,命令编号和回执解决重复点击,文件锁保护跨进程读写,LiveForm 与 V1/V2 状态交界需要明确生命周期和清理。用户不会在点“喂食”前想事务和回执,但开发时这些细节决定了桌面上的幻兽是否可靠。 |