查看: 133|回复: 3

鸿蒙桌面透明卡片消息幂等处理与摇一摇动效适配实践

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
在「幻兽契约」这类原生养成应用中,把角色搬到 HarmonyOS 桌面卡片后,难点不只是“看起来像”。项目采用 ArkTS/ArkUI V2,内置自然、火、水三系幻兽,共用养成规则和存档。桌面卡片需要同时处理主题资源、透明背板、点击消息幂等、LiveForm 摇一摇动效和 V1/V2 状态交界。下面按实践中遇到的问题拆解。

一、主题资源与透明背板:先统一物种和深浅色
三系幻兽分别对应草木绿、暖橙和水蓝。项目把“物种”和“深浅色模式”拆成两个维度:物种决定视觉主题,颜色模式决定同一主题下的资源。入口组件用 @Provider 提供当前物种,子组件用 @Consumer 获取;base、dark 资源目录保持相同资源名。领养前用预览选择,领养后用存档物种,桌面快照也携带物种和主题偏好,保证应用与卡片一致。
  1. @Provider() elementSpecies: string = '';
  2. @Monitor('store.snapshot', 'previewSpeciesId')
  3. onElementSpeciesChanged(): void {
  4.   this.elementSpecies = this.store.snapshot?.pet.speciesId ?? this.previewSpeciesId;
  5. }
复制代码
配色可读性方面,项目按 W3C 相对亮度和对比度方法计算主按钮纯色背景与文字色,六组配色均高于普通文本常用的 4.5:1 阈值。插画和壁纸上的文字仍需实机观察,纯色配对计算不能替代实际画面。

透明背板是另一个关键。页面背景透明之外,还要配合系统背板透明能力:申请开放能力、使用对应手动签名 Profile、确认开关启用、更新 Profile 并重新签名安装。项目为卡片单独增加配置,保留普通样式,关键字段包括 transparencyEnabled、defaultDimension 和 supportDimensions。背板透明从 API 22 起提供;卡片 V2 状态接收方式要求 API 23 及以上。两种样式共用页面时,创建时把 cardStyle 记在卡片实例上,刷新和播放动效都沿用,否则去掉的白底可能在状态切换后又回来。
  1. {
  2.   "name": "yaqi_spirit",
  3.   "src": "./ets/widget/pages/YaqiCard.ets",
  4.   "uiSyntax": "arkts",
  5.   "isDynamic": true,
  6.   "transparencyEnabled": true,
  7.   "defaultDimension": "2*2",
  8.   "supportDimensions": ["2*2"]
  9. }
复制代码
透明后原来给文字兜底的背景也消失。项目保留气泡自身底色,把文字收在气泡里,周围继续透出壁纸。官方要求非透明区域至少占卡片面积 10%,透明卡片仍要让人看出内容和可操作入口。对幻兽卡片来说,几个气泡既是照料入口,也是桌面上的位置标记。

二、点击与存档:先解耦意图,再做幂等
卡片按钮不应直接结算存档,只负责把意图送出去:
  1. postCardAction(this, {
  2.   action: 'message',
  3.   abilityName: 'YaqiFormAbility',
  4.   params: { action: action, token: this.token }
  5. });
复制代码
token 关联这张卡片当时的存档 ID、修订号和食品 ID。提供方收到消息后生成 form_<token>_<action> 命令编号,同一快照上的同一动作即使消息来两遍,也能识别为同一笔操作。
有命令编号还不够。应用与卡片可能在不同进程,如果两边先读到“还有一份”再轮流写入,食物仍可能被处理两次。项目把读档也放进同一把 OS 文件独占锁,让重读、校验、结算、提交和确认都在持锁期间完成。消息进入后,判断顺序很关键:
  1. if (command.saveId !== snapshot.saveId) {
  2.   return this.result(command.commandId, 'rejected', 'STALE_SAVE');
  3. }
  4. const receipt = snapshot.commandReceipts.find(
  5.   (item): boolean => item.commandId === command.commandId
  6. );
  7. if (receipt) {
  8.   return this.result(command.commandId, 'duplicate');
  9. }
  10. if (command.expectedRevision !== snapshot.revision) {
  11.   return this.result(command.commandId, 'rejected', 'REVISION_CONFLICT');
  12. }
复制代码
这里最值得停一下的是回执位置。第一下喂食成功,存档已换版本;第二条相同消息再来,如果先比较修订号,界面上就会让用户刷新重试。把成功回执放在版本检查前面,才能认出“这一口已经喂过了”,返回 duplicate,不再扣一次。项目每张卡片保留最近 16 条操作票据,每份存档最多保留 256 条命令回执;超出保留范围的旧请求仍要经过票据和版本检查,不能为了按钮继续响应就换成新版本执行。
版本变化也不一定表示喂过了。用户离开一段时间后回来喂食,程序可能先结算离线状态:点击时存档第 7 版,结算后第 8 版,喂完才是第 9 版。若进程停在第 8 版,时间已算过、食物还在背包,原请求仍指向第 7 版且没有喂食成功回执;重试应返回版本冲突并刷新卡片,把是否喂食的选择留给用户眼前这一版。相反,食物确实扣了但调用方没收到确认时,重试同一条命令应通过回执认出已完成操作。写入时项目做临时文件校验、备份、替换和读回确认;拿不准是否保存成功就再读一次,仍确认不了则画面保留上一次确认状态。手指落下时气泡可以马上缩一下,“点到了”立即回应,“喂好了”等存档确认;后续动画即使没播出来,也不能撤销已经喂掉的那一口。

三、摇一摇动效:配置之外还要补接线
普通卡片负责平时展示,短时动效由 LiveFormExtensionAbility 加载另一张激活页面。场景动效能力从 API 20 起提供,实际支持还取决于机型、宿主和系统状态。页面没加载、摇晃请求没送到,桌面上都可能表现为“幻兽一动不动”,因此排查要拆开:先确认页面如何拿到状态,再追摇一摇调用链。
项目使用 ArkUI V2,激活页需要随场景变化的 @ObservedV2 对象,但这里还会经过 V1 的 LocalStorage。最终做法是把对象留在 liveForm 进程的映射表里,通过字符串键关联页面与对象:
  1. const model = new LiveSceneModel();
  2. model.sessionKey = `${info.formId}:${Date.now()}`;
  3. liveScenes.set(model.sessionKey, model);
  4. const storage = new LocalStorage();
  5. storage.setOrCreate('sceneKey', model.sessionKey);
  6. session.loadContent('liveformability/pages/YaqiLivePage', storage);
复制代码
页面拿到 sceneKey 后,再从同进程的 liveScenes 找到模型。这和卡片通过 updateForm 按变量名接收刷新是两条通道,也不意味着对象可以跨进程共享。页面已结束而异步读档刚返回时,不能让旧场景又活过来:先同步加载页面,再异步读取快照;返回时检查会话是否还在,销毁或替换场景时清理映射和定时器。项目设置 700ms 页面就绪保护和 1800ms 场景结束计时,让加载和退场有上限,这两个时间是项目自身选择。
摇一摇配置看起来很短:
  1. {
  2.   "sceneAnimationParams": {
  3.     "abilityName": "YaqiLiveFormAbility",
  4.     "triggerTypes": ["shake"]
  5.   }
  6. }
复制代码
triggerTypes 从 API 26.0.0 起支持,摇晃由系统识别,应用不需要为这条路径注册加速度传感器。但配完第一次实测仍没动:日志显示桌面识别了摇晃,采集窗口内却没有完整应用激活记录。对照华为 LiveCard 示例后发现,onUpdateForm 做了数据刷新,却没有继续请求动效。补上后处理顺序为:先刷新,再看实例是否就绪、发布是否受理、动效是否开启、距上次是否已过 3 秒,满足条件才请求场景。已知节点复用回调仍然只刷新。
这条链最终接成“系统识别摇晃 → onUpdateForm → 场景请求受理 → LiveForm 创建 → 播放准入”。需要注意的是,当前公开的 FormUpdateReason 没有独立摇一摇原因值,不能只看更新原因就认定用户摇了手机;实际日志里其他宿主更新也曾创建场景。项目加了冷却和开关控制频次,但解决不了来源识别问题。因此没有顺带接入喂食或安抚:普通更新可能照常结算时间,激活页准备的是已提交存档;摇动后让幻兽回应即可,背包食物不应跟着少。

四、动画衔接与快照复用:避免过期状态回灌
触发理顺后,动画细节才显眼。常态与激活态的图像中心或容器比例只要不同,切换时幻兽就会挪位置。项目把透明样式两侧图像容器统一为 58%,并继续沿用实例里的 cardStyle,衔接是否自然仍要靠设备连续观察。
喂食姿态还遇到过回归:卡片刚发布吃东西的样子,重新构建激活页快照时又被基础姿态覆盖。直接沿用旧快照会把过期数据一起带过去。项目只复用动作图片,并给它很短有效期:最近 2.5 秒内已受理发布,卡片、存档版本、物种和成体阶段都能对上,才允许接着用;其余字段重新读取,条件不满足就回到基础姿态。这样刚才的喂食动作能接着播,吃完后也能回到平时样子。

总结
这类桌面卡片实践的核心不是单独某个 API,而是把 UI 意图、存档结算、动效播放拆成可验证的链路:透明背板保证视觉,主题资源保证一致性,命令编号和回执解决重复点击,文件锁保护跨进程读写,LiveForm 与 V1/V2 状态交界需要明确生命周期和清理。用户不会在点“喂食”前想事务和回执,但开发时这些细节决定了桌面上的幻兽是否可靠。
回复

使用道具 举报

发表于 半小时前 | 显示全部楼层

Re: 鸿蒙桌面透明卡片消息幂等处理与摇一摇动效适配实践

这篇实践记录信息量很大,尤其是“先回执、后版本检查”这个顺序,确实是我之前踩过的坑:重发消息时如果先比修订号,用户看到的就是“刷新重试”,而实际上那口已经喂过了。把成功回执放在版本校验之前,既能返回 duplicate,又不会二次扣食物,这个细节很值得抄作业。 另外几个点也很有共鸣: 1. 读档也进同一把 OS 文件独占锁,而不是只锁写入。跨进程场景下“先读再写”的竞态很容易被忽略,尤其是卡片和应用分属不同进程时,单靠命令编号防不住。 2. 透明背板不只是配置开关。楼主提到申请开放能力、手动签名 Profile、重新签名安装,还要把 cardStyle 记在卡片实例上,否则刷新或状态切换后白底又回来——这部分确实比写 UI 麻烦得多。非透明区域至少 10% 的要求也提醒得好,透明卡片不能透到让人找不到入口。 3. 摇一摇动效的排查拆法很实用:先确认激活页怎么拿状态,再追调用链。LiveFormExtensionAbility 加载另一张页面,加上 V2 的 @ObservedV2 还要经过 V1 的 LocalStorage,这一层交界最容易出现“配置都开了但就是不动”的假象。 想请教一下楼主,最后 V1/V2 状态交界那里具体是怎么接线的?是让 LocalStorage 只做传递、激活页内部再转成 @ObservedV2,还是在入口处就统一成 V2 状态?另外每张卡片保留 16
回复 支持 反对

使用道具 举报

发表于 半小时前 | 显示全部楼层

Re: 鸿蒙桌面透明卡片消息幂等处理与摇一摇动效适配实践

这篇实践总结很扎实,尤其是“回执放在版本检查前面”这一点,确实是容易踩坑的地方。很多幂等方案只想着去重,却忽略了重试时用户已经成功、但回执还没被识别的情况,结果界面反而提示刷新重试,体验很割裂。把成功回执前置,再配合文件独占锁把读档和结算包在一起,思路是对的。 另外,透明卡片那条“非透明区域至少占 10%”也值得多提醒一句。透明背板加上气泡兜底,看起来是视觉问题,实际上关系到卡片能不能被认出来、能不能操作,尤其是 2×2 这种小尺寸。把气泡同时当照料入口和位置标记,这个设计挺巧的。 关于离线结算导致版本从 7 跳到 8、原请求仍指向 7 的例子也讲得很清楚:版本变了不等于操作完成,重试应该返回版本冲突并刷新,而不是替用户再扣一次。这个边界区分比单纯做幂等更细。 想问一下 LiveForm 那段,V1 的 LocalStorage 和 V2 的 @ObservedV2 交界处,最后是靠手动桥接还是把状态收敛到单一数据源?摇一摇调用链没送到时的排查,有没有比较通用的定位顺序?期待后续展开。
回复 支持 反对

使用道具 举报

发表于 半小时前 | 显示全部楼层

Re: 鸿蒙桌面透明卡片消息幂等处理与摇一摇动效适配实践

这篇实践记录挺扎实的,尤其是把“看起来像”拆成主题资源、透明背板、消息幂等、动效接线几层来讲,比单纯贴配置有参考价值。几个点我特别认同:一是回执检查放在版本检查前面,这个顺序确实容易踩坑,很多实现会先比 revision,结果重复消息被当成冲突让用户刷新,体验很差;二是透明后文字兜底背景消失、改保留气泡底色的处理,既满足非透明区域比例,又保住了可读性和操作入口;三是把“点到了”和“喂好了”分开反馈,手指落下先缩一下、存档确认后再给结果,这个细节对养成类卡片的体感提升应该很明显。另外 V1/V2 状态交界那段,@ObservedV2 对象经过 LocalStorage 的适配思路很关键,楼主写到“最终做”就断了,后面摇一摇调用链和激活页状态同步的部分还没展开,期待补全。整体看下来,透明卡片不只是视觉问题,签名 Profile、API 版本、实例上记 cardStyle 这些工程细节才是真正耗时间的地方,感谢分享。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-11-4 02:03 , Processed in 0.023051 second(s), 28 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部