news 2026/7/5 14:24:52

HarmonyOS律愈实战08:BreathGuideAnimator实现4-7-8呼吸引导

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HarmonyOS律愈实战08:BreathGuideAnimator实现4-7-8呼吸引导

ArkTS 动画控制实战:把 4-7-8 呼吸训练从 UI 中拆出去



1. 为什么要拆动画控制器

呼吸训练看起来只是几个文字变化:吸气、屏息、呼气。但如果直接在页面里写一堆setTimeout(),后期会遇到问题:

  • 页面切换时定时器不容易清理。
  • 多次启动会产生重复循环。
  • UI 逻辑和节奏逻辑混在一起。
  • 想复用到全屏呼吸页时很麻烦。

“律愈”把呼吸节奏封装为BreathGuideAnimator,页面只负责展示当前帧。

2. 帧模型

呼吸训练每个阶段可以抽象成一帧:

exportinterfaceBreathGuideFrame{label:string;hint:string;}exporttypeBreathGuideListener=(frame:BreathGuideFrame)=>void;

label是主文案,例如“吸气”;hint是辅助文案,例如“4 秒 · 鼻腔缓慢充盈”。

3. 控制器结构

exportclassBreathGuideAnimator{privatelistener:BreathGuideListener;privatecycleId:number=0;privatetimerIds:number[]=[];constructor(listener:BreathGuideListener){this.listener=listener;}}

这里有两个关键状态:

  • cycleId:用来识别当前循环,防止旧定时器继续回调。
  • timerIds:保存所有定时器,停止时统一清理。

4. 发出一帧

privateemit(label:string,hint:string):void{this.listener({label,hint});}

控制器不关心 UI 是 Text、Dialog 还是全屏遮罩,它只把当前状态通知出去。

5. 安排定时器

privateschedule(id:number,label:string,hint:string,delayMs:number,next:(()=>void)|null=null):void{consttimerId=setTimeout(():void=>{if(id!==this.cycleId){return;}this.emit(label,hint);if(next!==null){next();}},delayMs)asnumber;this.timerIds.push(timerId);}

这段代码最重要的是:

if(id!==this.cycleId){return;}

当用户切换页面或关闭呼吸训练后,旧定时器即使触发,也不会再更新 UI。

6. 4-7-8 节奏

启动逻辑如下:

start():void{this.stop();this.cycleId+=1;constid=this.cycleId;this.schedule(id,'吸气','4 秒 · 鼻腔缓慢充盈',0,():void=>{this.schedule(id,'屏息','7 秒 · 温和守中',4000,():void=>{this.schedule(id,'呼气','8 秒 · 放松肩背',7000,():void=>{this.start();});});});}

流程是:

  1. 立即进入吸气。
  2. 4 秒后进入屏息。
  3. 7 秒后进入呼气。
  4. 8 秒后重新开始下一轮。

7. 停止逻辑

stop():void{this.cycleId+=1;for(consttimerIdofthis.timerIds){clearTimeout(timerId);}this.timerIds=[];}

cycleId += 1clearTimeout()是双保险:

  • 已经注册但未触发的定时器会被清理。
  • 即使某个定时器刚好触发,也会因为 id 不一致而退出。

8. 页面如何使用

Index.ets中,页面保存当前帧:

@StateprivatebreathLabel:string='吸气';@StateprivatebreathHint:string='4-7-8 呼吸 · 跟随圆环';privatebreathAnimator:BreathGuideAnimator|null=null;

创建控制器:

privateensureBreathAnimator():BreathGuideAnimator{if(this.breathAnimator===null){this.breathAnimator=newBreathGuideAnimator((frame:BreathGuideFrame):void=>{this.breathLabel=frame.label;this.breathHint=frame.hint;});}returnthis.breathAnimator;}

启动和停止:

privatestartBreathGuide():void{this.ensureBreathAnimator().start();}privatestopBreathGuide():void{this.breathAnimator?.stop();}

页面消失时释放:

aboutToDisappear():void{this.stopBreathGuide();this.breathAnimator=null;}

9. 控制流程图

start

stop old timers

cycleId plus one

emit 吸气

4 秒后 emit 屏息

7 秒后 emit 呼气

8 秒后 restart

stop

cycleId plus one

clear all timerIds

10. 这个写法的价值

这个控制器看起来很小,但它体现了一个重要思路:节奏逻辑不应该绑死在 UI 组件里。

这样做以后:

  • 同一套呼吸节奏可以用于疗愈页和全屏呼吸弹层。
  • 页面切换时能可靠停止。
  • 想调整为 3-5-7 或其他节奏,只改控制器。
  • UI 可以自由变化,不影响计时逻辑。

对于 HarmonyOS 应用来说,把这类“有生命周期、有定时器、有复用需求”的逻辑从页面中抽出去,是提升代码质量的有效办法。

9. 为什么 cycleId 是关键

只用 clearTimeout() 并不能覆盖所有边界情况。如果定时器刚好已经进入回调,清理可能来不及。所以项目用 cycleId 作为逻辑取消标记:

s if (id !== this.cycleId) { return; }

这是一种很常见的异步防抖思路:每一轮任务都有自己的 id,过期任务即使执行,也不能修改当前状态。

10. 呼吸训练和 UI 解耦

控制器只发出 frame:

s this.listener({ label, hint });

页面怎么展示完全由 UI 决定。可以是普通文本:

s Text(this.breathLabel) Text(this.breathHint)

也可以是全屏弹层、Canvas 圆环、卡片动画。这个设计让呼吸训练成为一个可复用服务。

11. 页面启动停止策略

在 Index.ets 中,Tab 切换时判断是否启动呼吸:

s .onChange((idx: number) => { this.selectedTabIndex = idx; if (idx === 1 || this.fullBreathOpen) { this.startBreathGuide(); } else { this.stopBreathGuide(); } })

这段代码很适合讲生命周期:用户在疗愈页或全屏呼吸页时才需要计时器,其他页面不需要。

12. 全屏呼吸层的产品意义

律愈不仅在疗愈页显示呼吸提示,还提供 openFullBreath():

s private openFullBreath(): void { this.fullBreathOpen = true; this.startBreathGuide(); }

这让呼吸训练从辅助信息变成独立功能。文章中可以结合配图说明:同一个控制器可以服务两个 UI 场景。

13. 可扩展节奏配置

如果要支持更多呼吸法,可以把 4-7-8 写成配置:

` s
interface BreathPhase {
label: string;
hint: string;
durationMs: number;
}

const phases: BreathPhase[] = [
{ label: ‘吸气’, hint: ‘鼻腔缓慢充盈’, durationMs: 4000 },
{ label: ‘屏息’, hint: ‘温和守中’, durationMs: 7000 },
{ label: ‘呼气’, hint: ‘放松肩背’, durationMs: 8000 }
];
`

这可以作为文章最后的进阶方向。

7000 },
{ label: ‘呼气’, hint: ‘放松肩背’, durationMs: 8000 }
];
`

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/5 14:20:36

芝士算法(前缀和 1.0)

目录 一维数组前缀和 二维数组前缀和 寻找数组的中心下标 除了自身以外数组的乘积 一维数组前缀和 一维数组前缀和 public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextInt()) { // 注意 w…

作者头像 李华
网站建设 2026/7/5 14:19:50

收藏!小白程序员必看:Hermes Agent 双 Loop 源码深度解析

Hermes Agent 源码中存在两套 Agent Loop:AIAgent 和 HermesAgentLoop。AIAgent 面向用户交互,处理流式输出、重试、中断等复杂交互逻辑;HermesAgentLoop 面向 RL 训练,关注异步、并发和训练数据生成。两者拆分是为了适应不同场景…

作者头像 李华
网站建设 2026/7/5 14:14:45

ORB-SLAM3 mFeatVec

mFeatVec(Feature Vector,特征向量)的计算原理,简单来说就是:为当前帧图像中的每个特征点,找到它在视觉词汇树中对应的中间节点(叶子节点world ID 上溯4层的节点ID),并将…

作者头像 李华
网站建设 2026/7/5 14:13:05

Java面向对象课程设计:学生成绩管理系统

一、项目简介 本项目为Java面向对象课程设计,由三人小组协作开发学生成绩管理系统。项目遵循多层分层架构思想,基于MySQL实现数据持久化,依托JDBC完成程序与数据库交互,使用Swing搭建可视化GUI界面。项目全程采用Git协同开发&…

作者头像 李华
网站建设 2026/7/5 14:09:54

3步搞定Unity卡通渲染:从零到一掌握URP Toon Shader核心技巧

3步搞定Unity卡通渲染:从零到一掌握URP Toon Shader核心技巧 【免费下载链接】UnityURPToonLitShaderExample A very simple toon lit shader example, for you to learn writing custom lit shader in Unity URP 项目地址: https://gitcode.com/gh_mirrors/un/Un…

作者头像 李华