HarmonyOS律愈实战08:BreathGuideAnimator实现4-7-8呼吸引导
ArkTS 动画控制实战:把 4-7-8 呼吸训练从 UI 中拆出去



1. 为什么要拆动画控制器
呼吸训练看起来只是几个文字变化:吸气、屏息、呼气。但如果直接在页面里写一堆 setTimeout(),后期会遇到问题:
- 页面切换时定时器不容易清理。
- 多次启动会产生重复循环。
- UI 逻辑和节奏逻辑混在一起。
- 想复用到全屏呼吸页时很麻烦。
“律愈”把呼吸节奏封装为 BreathGuideAnimator,页面只负责展示当前帧。
2. 帧模型
呼吸训练每个阶段可以抽象成一帧:
export interface BreathGuideFrame {
label: string;
hint: string;
}
export type BreathGuideListener = (frame: BreathGuideFrame) => void;
label 是主文案,例如“吸气”;hint 是辅助文案,例如“4 秒 · 鼻腔缓慢充盈”。
3. 控制器结构
export class BreathGuideAnimator {
private listener: BreathGuideListener;
private cycleId: number = 0;
private timerIds: number[] = [];
constructor(listener: BreathGuideListener) {
this.listener = listener;
}
}
这里有两个关键状态:
cycleId:用来识别当前循环,防止旧定时器继续回调。timerIds:保存所有定时器,停止时统一清理。
4. 发出一帧
private emit(label: string, hint: string): void {
this.listener({ label, hint });
}
控制器不关心 UI 是 Text、Dialog 还是全屏遮罩,它只把当前状态通知出去。
5. 安排定时器
private schedule(
id: number,
label: string,
hint: string,
delayMs: number,
next: (() => void) | null = null
): void {
const timerId = setTimeout((): void => {
if (id !== this.cycleId) {
return;
}
this.emit(label, hint);
if (next !== null) {
next();
}
}, delayMs) as number;
this.timerIds.push(timerId);
}
这段代码最重要的是:
if (id !== this.cycleId) {
return;
}
当用户切换页面或关闭呼吸训练后,旧定时器即使触发,也不会再更新 UI。
6. 4-7-8 节奏
启动逻辑如下:
start(): void {
this.stop();
this.cycleId += 1;
const id = this.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.cycleId += 1;
for (const timerId of this.timerIds) {
clearTimeout(timerId);
}
this.timerIds = [];
}
cycleId += 1 和 clearTimeout() 是双保险:
- 已经注册但未触发的定时器会被清理。
- 即使某个定时器刚好触发,也会因为 id 不一致而退出。
8. 页面如何使用
在 Index.ets 中,页面保存当前帧:
@State private breathLabel: string = '吸气';
@State private breathHint: string = '4-7-8 呼吸 · 跟随圆环';
private breathAnimator: BreathGuideAnimator | null = null;
创建控制器:
private ensureBreathAnimator(): BreathGuideAnimator {
if (this.breathAnimator === null) {
this.breathAnimator = new BreathGuideAnimator((frame: BreathGuideFrame): void => {
this.breathLabel = frame.label;
this.breathHint = frame.hint;
});
}
return this.breathAnimator;
}
启动和停止:
private startBreathGuide(): void {
this.ensureBreathAnimator().start();
}
private stopBreathGuide(): void {
this.breathAnimator?.stop();
}
页面消失时释放:
aboutToDisappear(): void {
this.stopBreathGuide();
this.breathAnimator = null;
}
9. 控制流程图
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 }
];
`
更多推荐



所有评论(0)