鸿蒙原生开发——从零构建呼吸引导器
一、引言
呼吸是我们每时每刻都在做的事,但大多数人不会"正确"呼吸。现代人的呼吸倾向于短促、浅表,这种模式会激活交感神经系统,导致心率加快、血压升高、压力激素分泌增加。而深长、有节奏的呼吸可以激活副交感神经系统,产生相反的效果——降低心率、放松肌肉、减轻焦虑。
这就是呼吸引导器(Breathing Guide)存在的意义。它通过一个视觉动画(通常是扩缩的圆圈)来引导用户的呼吸节奏——圈扩大时吸气,圈收缩时呼气。用户不需要思考"该吸多久、该呼多久",只需要跟随动画。
从技术角度看,呼吸引导器的核心挑战有三个:
第一,平滑动画。一个从 120vp 扩大到 220vp 再缩回 120vp 的圆圈,需要在数秒内完成平滑过渡。不能一帧跳到位,需要用定时器逐帧更新尺寸。
第二,相位管理。不同的呼吸模式有不同的阶段(相位)。4-7-8 法有三个阶段(吸气 4 秒、屏息 7 秒、呼气 8 秒),盒式呼吸法有四个阶段(吸气 4 秒、屏息 4 秒、呼气 4 秒、屏息 4 秒)。每个阶段有不同的动画方向(扩大、保持、缩小)。
第三,相位过渡。一个阶段结束后自动进入下一阶段,最后一个阶段结束后自动回到第一个阶段并增加轮次计数。这个状态机需要在定时器回调中完成,不能有视觉卡顿。
本文将用 ArkUI 从零构建一个呼吸引导器。功能包括:三种呼吸模式(4-7-8 放松法 / 盒式呼吸法 / 简易 5 秒呼吸)、动画呼吸圈(大小渐变 + 颜色变化)、相位指示器(当前阶段高亮 + 进度条)、轮次统计、开始/暂停/重置控制。
阅读完本文,你将能够:
- 使用
setInterval实现逐帧动画引擎 - 设计多相位的状态转换系统
- 使用比例插值(0→1)驱动动画尺寸变化
- 构建呼吸引导器的完整 UI(动态圆圈 + 相位指示 + 模式选择)
二、呼吸模式的数据模型
2.1 阶段定义
每个呼吸模式由多个阶段(Phase)组成。每个阶段定义了名称、时长、动画起止比例和颜色:
interface PhaseDef {
label: string; // 阶段标签:"吸气" / "屏息" / "呼气"
duration: number; // 时长(秒)
startRatio: number; // 动画起始比例(0=最小圈, 1=最大圈)
endRatio: number; // 动画结束比例
color: string; // 圆圈颜色(青/蓝/紫)
}
startRatio 和 endRatio 是动画引擎的核心参数。它们定义了圆圈在该阶段内的尺寸变化:
- 吸气:
startRatio: 0, endRatio: 1→ 圈从小变大 - 屏息:
startRatio: 1, endRatio: 1→ 圈保持最大不变 - 呼气:
startRatio: 1, endRatio: 0→ 圈从大变回小 - 屏息(呼后):
startRatio: 0, endRatio: 0→ 圈保持最小不变
2.2 模式定义
模式是阶段的数组,外加元数据:
interface PatternDef {
name: string; // 模式名称
desc: string; // 简短描述
phases: PhaseDef[]; // 阶段列表
}
三种预设模式:
4-7-8 放松法(安德鲁·威尔医生推广的经典放松呼吸法):
{
name: '4-7-8', desc: '经典放松呼吸法',
phases: [
{ label: '吸气', duration: 4, startRatio: 0, endRatio: 1, color: '#4ECDC4' },
{ label: '屏息', duration: 7, startRatio: 1, endRatio: 1, color: '#1677FF' },
{ label: '呼气', duration: 8, startRatio: 1, endRatio: 0, color: '#7B68EE' },
],
}
三个阶段共计 19 秒完成一轮。4 秒吸气(圈扩张,青绿色)→ 7 秒屏息(圈保持最大,蓝色)→ 8 秒呼气(圈缩回最小,紫色)。
盒式呼吸法(四方呼吸,美国海豹突击队用于在高压环境下保持冷静):
{
name: '盒式', desc: '四方呼吸法',
phases: [
{ label: '吸气', duration: 4, startRatio: 0, endRatio: 1, color: '#4ECDC4' },
{ label: '屏息', duration: 4, startRatio: 1, endRatio: 1, color: '#1677FF' },
{ label: '呼气', duration: 4, startRatio: 1, endRatio: 0, color: '#7B68EE' },
{ label: '屏息', duration: 4, startRatio: 0, endRatio: 0, color: '#1677FF' },
],
}
四个阶段各 4 秒,形成 4×4 的"盒子"。吸气后屏息、呼气后屏息——这种对称的结构让呼吸者感到稳定和控制感。
简易 5 秒呼吸(最简单的模式,适合初学者):
{
name: '简易', desc: '5秒均匀呼吸',
phases: [
{ label: '吸气', duration: 5, startRatio: 0, endRatio: 1, color: '#4ECDC4' },
{ label: '呼气', duration: 5, startRatio: 1, endRatio: 0, color: '#7B68EE' },
],
}
两个阶段各 5 秒,没有屏息。最简单的吸-呼交替,适合呼吸练习的入门。
2.3 颜色语义
三种颜色对应三种呼吸动作,每个颜色都经过刻意选择:
| 颜色 | 色值 | 阶段 | 语义 |
|---|---|---|---|
| 青绿色 | #4ECDC4 |
吸气 | 新鲜、能量、吸入活力 |
| 蓝色 | #1677FF |
屏息 | 稳定、沉淀、保持平静 |
| 紫色 | #7B68EE |
呼气 | 释放、放松、排出压力 |
三种颜色都是冷色调——没有红色(紧张、危险)或黄色(对比度不足)。冷色调有助于触发副交感神经系统的放松反应,与呼吸引导的放松目标一致。
三、动画引擎
3.1 定时器驱动的逐帧动画
动画的核心是一个每 80 毫秒触发一次的 setInterval 定时器:
const TICK_MS: number = 80; // 80ms ≈ 12.5 fps
toggleRunning(): void {
if (this.isRunning) {
this.stopTimer();
this.isRunning = false;
} else {
this.isRunning = true;
this.timerId = setInterval(() => { this.tick(); }, TICK_MS);
}
}
选择 80ms 而非 16ms(60fps)的原因是:呼吸圈的动画速度很慢(4-8 秒完成一次变化),12.5fps 已经足够平滑。帧数过高会浪费 CPU 资源且用户感知不到差异。
aboutToDisappear() 中清除定时器,防止页面离开后继续运行:
aboutToDisappear(): void {
this.stopTimer();
}
3.2 tick 函数:逐帧更新
tick() 是动画引擎的心脏,每 80ms 执行一次,负责三件事:更新经过时间、计算圆圈尺寸、检测阶段切换:
tick(): void {
this.elapsedMs += TICK_MS; // ① 累加经过时间
const phase = this.currentPhase();
const phaseMs = phase.duration * 1000;
const progress = Math.min(1, this.elapsedMs / phaseMs); // ② 计算进度(0→1)
const ratio = phase.startRatio + (phase.endRatio - phase.startRatio) * progress; // ③ 插值
this.circleSize = MIN_SIZE + (MAX_SIZE - MIN_SIZE) * ratio; // ④ 更新尺寸
if (this.elapsedMs >= phaseMs) { // ⑤ 检测阶段切换
const pattern = this.currentPattern();
if (this.phaseIndex + 1 >= pattern.phases.length) {
this.phaseIndex = 0; // 回到第一阶段
this.totalCycles++; // 一轮完成
} else {
this.phaseIndex++; // 进入下一阶段
}
this.elapsedMs = 0; // 重置经过时间
}
}
逐行解析:
① elapsedMs += TICK_MS:追踪当前阶段内已经过了多少毫秒。这是所有计算的基准。
② progress = Math.min(1, elapsedMs / phaseMs):计算当前阶段的完成比例(0 到 1)。Math.min(1, ...) 确保在定时器延迟导致略微超过时长时不会出现 > 1 的值。
③ ratio = startRatio + (endRatio - startRatio) * progress:线性插值。在吸气阶段,ratio 从 0 平滑增长到 1(圈扩大);在屏息阶段,startRatio = endRatio = 1,ratio 始终为 1(圈不变);在呼气阶段,ratio 从 1 平滑减小到 0(圈缩小)。
④ circleSize = MIN_SIZE + (MAX_SIZE - MIN_SIZE) * ratio:将比例映射到实际像素值。MIN_SIZE = 120,MAX_SIZE = 220。当 ratio = 0 时 circleSize = 120vp(最小),ratio = 1 时 circleSize = 220vp(最大)。
⑤ 阶段切换检测:当 elapsedMs 超过阶段时长时,进入下一阶段或回到第一阶段(完成一轮)。elapsedMs 重置为 0。
3.3 进度条
除了圆圈动画,还有一个水平进度条显示当前阶段的完成程度:
cycleProgress(): number {
const phases = this.currentPattern().phases;
let totalMs = 0;
for (let i = 0; i < phases.length; i++) {
totalMs += phases[i].duration * 1000;
}
let doneMs = 0;
for (let i = 0; i < this.phaseIndex; i++) {
doneMs += phases[i].duration * 1000;
}
doneMs += this.elapsedMs;
return doneMs / totalMs;
}
cycleProgress() 返回整个呼吸轮次的完成比例(0→1),用于驱动进度条。与圆圈动画的"阶段内进度"不同,这是"跨阶段的全局进度"。
进度条的宽度使用百分比:
Row()
.width(`${this.cycleProgress() * 100}%`)
.height(3)
.borderRadius(2)
.backgroundColor(this.circleColor())
颜色跟随当前阶段颜色——吸气时为青绿,屏息时为蓝,呼气时为紫。进度条和圆圈使用相同的颜色,形成视觉统一。
四、状态管理
4.1 六个核心状态变量
@State patternIndex: number = 0; // 当前模式(0=4-7-8, 1=盒式, 2=简易)
@State phaseIndex: number = 0; // 当前阶段索引
@State elapsedMs: number = 0; // 当前阶段已过毫秒数
@State circleSize: number = MIN_SIZE; // 圆圈当前尺寸(vp)
@State totalCycles: number = 0; // 已完成轮数
@State isRunning: boolean = false; // 是否正在运行
这六个变量组合出呼吸引导器的完整状态。与计算器的状态机不同(四个变量组合出四个离散状态),呼吸引导器的状态是连续的——elapsedMs 和 circleSize 在持续变化。
4.2 模式切换
切换呼吸模式时,需要重置所有状态:
selectPattern(idx: number): void {
this.stopTimer();
this.patternIndex = idx;
this.phaseIndex = 0;
this.elapsedMs = 0;
this.circleSize = MIN_SIZE;
this.totalCycles = 0;
this.isRunning = false;
}
先停止定时器,再重置所有状态变量,确保新模式的动画从初始状态开始。
4.3 重置
重置操作将所有状态恢复到初始值:
reset(): void {
this.stopTimer();
this.isRunning = false;
this.phaseIndex = 0;
this.elapsedMs = 0;
this.circleSize = MIN_SIZE;
this.totalCycles = 0;
}
重置后圆圈回到最小尺寸、轮次归零、阶段回到第一个。与模式切换不同,重置不改变当前的呼吸模式。
4.4 阶段剩余时间
显示给用户的倒计时:
phaseRemaining(): number {
const total = this.currentPhase().duration * 1000;
const remain = total - this.elapsedMs;
return Math.ceil(remain / 1000);
}
使用 Math.ceil 向上取整。例如 6.2 秒剩余显示为 “7 秒” 而非 “6 秒”。这给用户一种"还有 1 秒缓冲"的心理感受,减少切换阶段的惊讶感。
五、UI 设计
5.1 整体布局
BreathingPage
├── 深色标题栏(52vp):🧘 呼吸引导 + "N 轮"
├── Scroll(可滚动内容区)
│ ├── 动画呼吸圈(120-220vp,动态尺寸 + 动态颜色 + 阴影)
│ │ └── 阶段标签 + 剩余秒数(白字居中)
│ ├── 全局进度条(3vp 高度,宽度百分比)
│ ├── 相位指示器(圆点 + 标签 + 时长)
│ ├── 模式选择器(3 个卡片并排)
│ └── 控制按钮(开始/暂停 + 重置)
5.2 动画呼吸圈
呼吸圈是整个页面最显眼的元素:
Column()
.width(this.circleSize)
.height(this.circleSize)
.borderRadius(this.circleSize / 2)
.backgroundColor(this.circleColor())
.shadow({ radius: 20, color: this.circleColor() + '44' })
borderRadius = circleSize / 2 确保始终是正圆形(宽度和高度的 borderRadius = 50%)。
.shadow() 添加与圆圈颜色匹配的柔和光晕('44' = 约 27% 不透明度)。光晕让圆圈有"发光"的质感,增强了呼吸引导的沉浸感——用户会觉得这个圈"有生命"。
圈内显示阶段标签(“吸气”/“屏息”/“呼气”)和剩余秒数。文字颜色为白色,在所有三种背景色(青绿/蓝/紫)上都清晰可读——这些颜色与白色的对比度均达到 4.5:1 以上。
5.3 相位指示器
呼吸圈下方的相位指示器显示当前模式的所有阶段:
● 吸气 ○ 屏息 ○ 呼气
4s 7s 8s
当前阶段用实心圆点 + 深色标签 + 阶段颜色高亮显示,已完成阶段用半透明圆点 + 浅色标签,未开始阶段用灰色圆点 + 浅灰标签。
ForEach(this.currentPattern().phases, (phase: PhaseDef, pi: number) => {
Column()
.width(10).height(10).borderRadius(5)
.backgroundColor(pi === this.phaseIndex ? phase.color :
(pi < this.phaseIndex ? phase.color + '44' : '#E0E0E8'))
})
三种状态的颜色区分让用户一眼就能看到"我现在在哪个阶段、已经完成了哪些阶段、接下来是什么"。
5.4 模式选择器
三个模式卡片并排,选中态蓝色实心 + 白字,未选中态浅灰:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 4-7-8 │ │ 盒式 │ │ 简易 │
│ 经典放松 │ │ 四方呼吸 │ │ 5秒均匀 │
│ 呼吸法 │ │ 法 │ │ 呼吸 │
└──────────┘ └──────────┘ └──────────┘
每张卡片包含模式名称(加粗)和简短描述(小字)。选中时整个卡片变蓝,与未选中卡片形成鲜明对比。
5.5 控制按钮
两个按钮并排:开始/暂停(主要操作,占 2/3 宽度)和重置(次要操作,占 1/3 宽度)。
开始前按钮显示 “▶ 开始”(蓝色),运行中显示 “⏸ 暂停”(品红色 #EB2F96)。品红色在这里替代了传统方案中可能使用的黄色或橙色,且白字对比度约 4.5:1。
暂停按钮使用品红而非红色,因为"暂停"不是危险操作——它是一种休息,而非停止。品红传达的是"温和的中断",红色则暗示"紧急停止"。
六、完整代码结构
BreathingPage
├── 数据定义
│ ├── PATTERNS[] — 三种呼吸模式及阶段数组
│ ├── MIN_SIZE / MAX_SIZE — 圆圈尺寸范围
│ └── TICK_MS — 动画帧间隔
├── 状态变量
│ ├── @State patternIndex / phaseIndex / elapsedMs
│ ├── @State circleSize / totalCycles / isRunning
│ └── private timerId(非响应式)
├── 动画引擎
│ ├── tick() — 逐帧更新(累加时间 → 计算插值 → 更新尺寸 → 检测阶段切换)
│ └── toggleRunning() / stopTimer() / reset()
├── 计算属性
│ ├── currentPattern() / currentPhase()
│ ├── phaseRemaining() — 当前阶段剩余秒数
│ └── cycleProgress() — 全局轮次进度(0→1)
├── 视图
│ ├── 标题栏(模式名 + 轮次计数)
│ ├── 呼吸圈(动态尺寸 + 动态颜色 + 阶段标签)
│ ├── 全局进度条(百分比宽度)
│ ├── 相位指示器(圆点序列)
│ ├── 模式选择器(3 卡片)
│ └── 控制按钮(开始/暂停 + 重置)
└── 生命周期
└── aboutToDisappear() — 清除定时器
七、总结
本文从零构建了一个呼吸引导器。与前九篇的功能型应用不同,呼吸引导器的核心是动画引擎 + 相位管理——没有 CRUD,没有数据持久化,只有一个扩缩圆圈的动画和一套精确的相位切换逻辑。
核心要点回顾:
-
三种呼吸模式的数据模型:
PatternDef包含多个PhaseDef,每个阶段用startRatio和endRatio定义动画方向(0 = 最小圈,1 = 最大圈)。三种模式覆盖了放松(4-7-8)、稳定(盒式)、入门(简易)三种场景。 -
80ms 定时器驱动的动画引擎:
tick()每 80ms 执行一次——累加 elapsedMs → 计算 phase progress → 线性插值 ratio → 更新 circleSize → 检测阶段切换。12.5fps 对于慢速呼吸动画已经足够平滑。 -
线性插值驱动尺寸变化:
ratio = startRatio + (endRatio - startRatio) * progress→circleSize = MIN_SIZE + (MAX_SIZE - MIN_SIZE) * ratio。这种分步计算让动画逻辑清晰可测试。 -
相位自动切换:
elapsedMs >= phaseMs时自动进入下一阶段。最后一个阶段结束后回到第一阶段,totalCycles++。整个状态机在tick()中完成,没有额外的切换延迟。 -
三色语义系统:青绿(吸气 = 新鲜能量)、蓝(屏息 = 稳定沉淀)、紫(呼气 = 释放放松)。全冷色调、无黄色、无红色,与呼吸引导的放松目标一致。
-
视觉反馈系统:动态圆圈(尺寸 + 颜色 + 光晕阴影)+ 全局进度条(百分比宽度 + 阶段颜色)+ 相位指示器(圆点三态)+ 阶段标签 + 剩余秒数。多重视觉反馈让用户不需要看文字就能跟随呼吸节奏。
呼吸引导器是一个"小而完整"的案例——200 行代码,一个定时器,一个圆圈,三种呼吸模式。但它涉及动画引擎设计、相位状态机、颜色心理学、视觉反馈系统等多个技术层面。它是移动健康类应用的基础模块,也是动画编程的一个优秀练习。
更多推荐




所有评论(0)