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:number0;privatetimerIds:number[][];constructor(listener:BreathGuideListener){this.listenerlistener;}}这里有两个关键状态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)|nullnull):void{consttimerIdsetTimeout(():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.cycleId1;constidthis.cycleId;this.schedule(id,吸气,4 秒 · 鼻腔缓慢充盈,0,():void{this.schedule(id,屏息,7 秒 · 温和守中,4000,():void{this.schedule(id,呼气,8 秒 · 放松肩背,7000,():void{this.start();});});});}流程是立即进入吸气。4 秒后进入屏息。7 秒后进入呼气。8 秒后重新开始下一轮。7. 停止逻辑stop():void{this.cycleId1;for(consttimerIdofthis.timerIds){clearTimeout(timerId);}this.timerIds[];}cycleId 1和clearTimeout()是双保险已经注册但未触发的定时器会被清理。即使某个定时器刚好触发也会因为 id 不一致而退出。8. 页面如何使用在Index.ets中页面保存当前帧StateprivatebreathLabel:string吸气;StateprivatebreathHint:string4-7-8 呼吸 · 跟随圆环;privatebreathAnimator:BreathGuideAnimator|nullnull;创建控制器privateensureBreathAnimator():BreathGuideAnimator{if(this.breathAnimatornull){this.breathAnimatornewBreathGuideAnimator((frame:BreathGuideFrame):void{this.breathLabelframe.label;this.breathHintframe.hint;});}returnthis.breathAnimator;}启动和停止privatestartBreathGuide():void{this.ensureBreathAnimator().start();}privatestopBreathGuide():void{this.breathAnimator?.stop();}页面消失时释放aboutToDisappear():void{this.stopBreathGuide();this.breathAnimatornull;}9. 控制流程图startstop old timerscycleId plus oneemit 吸气4 秒后 emit 屏息7 秒后 emit 呼气8 秒后 restartstopcycleId plus oneclear all timerIds10. 这个写法的价值这个控制器看起来很小但它体现了一个重要思路节奏逻辑不应该绑死在 UI 组件里。这样做以后同一套呼吸节奏可以用于疗愈页和全屏呼吸弹层。页面切换时能可靠停止。想调整为 3-5-7 或其他节奏只改控制器。UI 可以自由变化不影响计时逻辑。对于 HarmonyOS 应用来说把这类“有生命周期、有定时器、有复用需求”的逻辑从页面中抽出去是提升代码质量的有效办法。9. 为什么 cycleId 是关键只用 clearTimeout() 并不能覆盖所有边界情况。如果定时器刚好已经进入回调清理可能来不及。所以项目用 cycleId 作为逻辑取消标记s if (id ! this.cycleId) { return; }这是一种很常见的异步防抖思路每一轮任务都有自己的 id过期任务即使执行也不能修改当前状态。10. 呼吸训练和 UI 解耦控制器只发出 frames 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 写成配置 sinterface 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 }];