在鸿蒙上做呼吸动画,我以为最难的是 ArkTS 语法,结果最麻烦的是------我根本不知道用户的设备跑到哪一档了。
呼吸动画是「呼吸视界」这个 App 的核心体验:吸气时圆圈缓慢扩张,屏气时保持,呼气时收缩。这个动画一旦卡顿,「跟着 App 呼吸」的节奏就断了,用户能感觉到「哪里不对」,但不会告诉你是帧率问题。
先说一下这个 App 是干什么的,方便后面的技术背景理解。
产品背景:一个给自己做的呼吸训练工具
「呼吸视界」(iOS App Store ID: 6758613852)做的是结构化呼吸训练引导------4-7-8 呼吸法、盒式呼吸、Wim Hof 法这些。网上这些方法的文字说明很多,但照着文字练,你得自己数秒、记顺序,练着练着就分心了。
我做这个的起因很功利:开会前容易紧张,想找个东西帮我两分钟之内把状态重置一下。找了一圈没找到合适的,就自己写了。
App 有三块核心功能:带动画节奏的引导式练习、本地持久化的训练记录、以及一个课程进度系统(不只是单次练习,而是完整的训练计划)。iOS 版目前评分 5 分,样本量不大,但有个用户说「可以跟随练习呼吸,保持稳定的心情」------说实话这个反馈比我预期的更朴实,我自己用下来觉得更直接的感受是:开会前真的有用,两分钟够了。
鸿蒙版最近发布,把移植过程里踩的几个坑整理一下。
坑一:呼吸动画的 GPU 降级,我不知道该在哪个阈值切
呼吸动画用 GPU 渲染时效果最好,过渡顺滑,缩放曲线自然。但鸿蒙设备碎片化比 iOS 严重得多,中低端机上 GPU 渲染直接掉帧,整个动画变得一顿一顿的。
所以我做了一套自适应降级:检测到性能不足时切到 Canvas fallback 模式,同时把当前渲染质量分成 high、balanced、low 三档。
问题来了:切换阈值怎么定?
我的判断方式是盯 frameMs(单帧渲染耗时)和连续低帧计数:
typescript
// 连续低帧超过阈值时触发降级
if (frameMs > 22 && consecutiveLowFpsCount >= 3) {
// 22ms ≈ 45fps,低于此值且连续3帧 → 切 balanced
adaptRenderer('degrade quality -> balanced');
consecutiveLowFpsCount = 0;
}
if (frameMs > 33 && consecutiveLowFpsCount >= 3) {
// 33ms ≈ 30fps,连续3帧 → 切 canvas fallback
adaptRenderer('switch renderer -> canvas fallback');
}
这个阈值不是凭感觉拍的,是我把 hilog 日志抓出来跑脚本分析的结果。日志里会输出每帧的 fps、frameMs、tickMs 以及当前渲染质量档位,降级事件会打 BF_PERF_ADAPT 标签,比如 degrade quality -> balanced 或者 switch renderer -> canvas fallback。
对独立开发者来说这套日志分析挺重要------没有 QA、没有用户主动反馈卡顿,只能靠工具自己发现问题。我在没有真机的情况下,靠日志回放重现了好几个卡顿场景。
目前 Canvas 模式下动画过渡还是不如 GPU 顺滑,这个还在打磨,算是没解决干净的问题。
坑二:设计 Token 漏用------一个脚本比 code review 更可靠
App 的调性是「平静克制」,UI 上我比较在意所有间距、圆角、阴影、动画时长要统一。如果哪个地方直接写了魔法数字,整体质感就散了。
鸿蒙版我把所有设计 token 收进一个 Style.ets,导出四个命名空间:SPACE、RADIUS、SHADOW、MOTION。问题是开发过程中很容易手滑------改某个组件时直接写 borderRadius(8) 而不是 RADIUS.card,这种事我自己也干过。
所以我写了一个 check_design_foundation.py,逻辑很简单:用 path.read_text() 读取关键文件内容,用字符串匹配检查是否包含预期的 token 引用。比如检查 AppBackdrop.ets 里有没有 export struct AppBackdrop,检查 SheetBackground.ets 里有没有调用 AppBackdrop(,检查根页面 RootPage.ets 里有没有 AppBackdrop({。
不是正则匹配魔法数字(那个误报太多),而是检查「关键结构是否存在」------更像一个架构约束验证器。
真实案例:有一次我重构了背景组件 AppBackdrop,改了对外接口,但忘了更新 SheetBackground 里的调用方式,就是被这个脚本拦下来的。如果没有这个检查,这个问题可能得等到真机运行时才会发现。
我还把几个类似的脚本整合进一个 check_foundation_alignment.py,统一管理:设计 token 校验、按压反馈检查、页面过渡检查、i18n 对等检查、路由检查------提交前一起跑,哪个挂了去修哪个。独立开发没有 code review,这套东西算是自己给自己兜底。
坑三:i18n 漏 key,双语维护是个持续性的低级错误
App 支持中英双语,维护四个语言文件:strings_app_en.ets、strings_app_zh-Hans.ets 以及对应的 base 版本。每次加新功能往里填 key,英文填了忘了填中文,或者反过来,这种事经常发生。
check_i18n_keys.py 做的事很直白:把四个文件里的 key 全部提取出来做集合差运算,输出「哪些 key 在英文有但中文没有」以及反向的情况。
这个脚本帮我发现过好几次漏掉的 key,有时候漏的是边缘功能的文案,有时候是一个按钮标题------后者如果漏了,用户看到的就是 key 字符串本身,很难看。
课程进度系统:本地存储是主动选择,不是偷懒
ProgramProgressRecord 记录用户在某个训练计划里完成了哪些 session、当前在第几阶段。数据全部本地存储,没有云同步。
说实话云同步我也不想做。OAuth 接入、服务器费用、隐私合规、多端数据冲突处理......这一套对独立开发者来说投入产出比太低。用户的训练记录放本地就够了,鸿蒙的 Preferences 和 RelationalStore 用起来比我预期顺手,持久化这块没遇到太大麻烦。
自定义呼吸节奏的交互,我还没想明白
用户可以自由设置吸气、屏气、呼气各阶段的时长。这个功能的交互我试了三版:滑动条、数字步进器、转盘------感觉都差点意思。滑动条精度不够,步进器操作次数太多,转盘在小屏上很难操作。
这个目前还搁着,UI 做得比较简陋。如果你做过类似的时长输入控件------尤其是整数秒精度、范围大概 1-30 秒的场景------很想听听你用了什么方案。