# 鸿蒙 HarmonyOS 应用开发实战(第32期)|间隔计时器(Interval Timer)— 健身训练的精确节拍器
一、应用概述
间隔计时器(Interval Timer) 是一款为高强度间歇训练(HIIT)量身定制的计时工具。它让用户分别设置「运动时长」和「休息时长」,然后在运动与休息之间自动循环切换,并精确统计完成的组数。在现代健身场景中,Tabata、CrossFit 等训练方式都依赖精准的间歇计时,这款应用正是为此而生。
从技术角度看,该应用进一步深化了 setInterval 定时器的使用模式,引入了多状态切换和用户可配置参数两大新课题。与第 31 期模拟时钟的「只读」计时不同,间隔计时器是「交互式」的——用户可以随时开始、暂停、重置,并在运动中动态调整参数。
二、技术架构与组件树
2.1 整体架构
@Entry @Component struct index32
├── Column (根容器)
│ ├── Row (顶部导航栏)
│ │ ├── Text ("‹ 返回")
│ │ ├── Text ("⏱️ 间隔计时")
│ │ └── Blank
│ └── Column (主内容区, 居中)
│ ├── Text (当前阶段: "💪 运动" / "☕ 休息")
│ ├── Text (倒计时时间, fontSize=72)
│ ├── Text ("已完成 X 组")
│ ├── Row (按钮组)
│ │ ├── Button (开始 / 暂停, 条件渲染)
│ │ └── Button (重置)
│ └── Row (参数设置区)
│ ├── Column (运动秒数)
│ │ └── TextInput
│ ├── Text ("|" 分隔符)
│ └── Column (休息秒数)
│ └── TextInput
2.2 状态变量设计
@State workTime: number = 30; // 运动时长(秒)
@State restTime: number = 15; // 休息时长(秒)
@State remaining: number = 30; // 当前阶段剩余秒数
@State isWork: boolean = true; // 当前是否为运动阶段
@State isRunning: boolean = false; // 是否正在运行
@State cycles: number = 0; // 已完成组数
intervalId: number = -1; // 定时器 ID(非 @State)
六个 @State 变量各司其职:
| 变量 | 类型 | 作用 | 变更触发场景 |
|---|---|---|---|
workTime |
number | 运动阶段时长 | 用户修改输入框 |
restTime |
number | 休息阶段时长 | 用户修改输入框 |
remaining |
number | 倒计时数字 | 每秒减 1 |
isWork |
boolean | 阶段标识 | 倒计时归零时翻转 |
isRunning |
boolean | 运行状态 | 点击开始/暂停 |
cycles |
number | 完成组数 | 每完成一轮运动后 +1 |
其中 intervalId 之所以不标记 @State,是因为它不需要驱动 UI 刷新——定时器的存在与否由 isRunning 间接表达。
三、核心代码深度解析
3.1 定时器主循环
start() {
if (this.isRunning) return; // 防重复启动
this.isRunning = true;
this.intervalId = setInterval(() => {
if (this.remaining > 0) {
this.remaining--; // 倒计时减 1
} else {
this.isWork = !this.isWork; // 翻转阶段
this.remaining = this.isWork ? this.workTime : this.restTime; // 重置剩余时间
if (this.isWork) this.cycles++; // 进入运动阶段时组数 +1
}
}, 1000);
}
定时器逻辑是一个有限状态机:
┌─────────────┐
│ 运动阶段 │
│ (remaining>0)│
└──────┬──────┘
│ remaining == 0
▼
┌─────────────┐
│ 休息阶段 │
│ (remaining>0)│
└──────┬──────┘
│ remaining == 0
▼
┌─────────────┐
│ 运动阶段 │ ← cycles++
└─────────────┘
当 remaining 归零时,isWork 翻转,remaining 根据当前阶段从 workTime 或 restTime 重新取值。如果翻转后进入运动阶段,cycles 递增。
这里有一个精妙的细节:组数统计的时机。代码在 isWork 变为 true 时递增 cycles,而不是在休息结束时。这意味着「一组」的定义是「完成一次完整运动」,这更符合健身训练的直觉——你完成了多少组运动,而不是休息了多少次。
3.2 暂停与重置
pause() {
clearInterval(this.intervalId);
this.isRunning = false;
}
reset() {
clearInterval(this.intervalId);
this.isRunning = false;
this.isWork = true;
this.remaining = this.workTime;
this.cycles = 0;
}
pause() 的逻辑简洁明了:清除定时器并将运行状态置为 false。reset() 则彻底回到初始状态——运动阶段、剩余时间取 workTime、组数归零。
注意:调用 clearInterval 后再将 intervalId 重新赋值为 -1 不是必须的,因为每次 start() 时会重新赋值。但为了防止悬空引用,重置 intervalId 是一个好习惯。
3.3 时间格式化计算属性
get display(): string {
const m = Math.floor(this.remaining / 60);
const s = this.remaining % 60;
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
}
将剩余的秒数转换为 MM:SS 格式。Math.floor(remaining / 60) 计算分钟数,remaining % 60 计算秒数。padStart(2, '0') 确保单位数时补零(如 00:05 而不是 0:5)。
3.4 条件渲染切换按钮
Row({ space: 12 }) {
if (!this.isRunning) {
Button('开始').width(100).height(48).borderRadius(24)
.backgroundColor('#4CAF50')
.onClick(() => this.start())
} else {
Button('暂停').width(100).height(48).borderRadius(24)
.backgroundColor('#FF9800')
.onClick(() => this.pause())
}
Button('重置').width(100).height(48).borderRadius(24)
.backgroundColor('#9E9E9E')
.onClick(() => this.reset())
}
这是典型的条件渲染模式——同一位置根据状态显示不同的按钮:
- 未运行时 → 显示绿色「开始」按钮
- 运行时 → 显示橙色「暂停」按钮
- 始终显示灰色的「重置」按钮
颜色编码也遵循了通用 UI 惯例:绿色表示"行动"(开始),橙色表示"暂停",灰色表示"中性操作"(重置)。
3.5 用户可配置参数
Row() {
Column() {
Text('运动(秒)').fontSize(12).fontColor('#888')
TextInput({ text: this.workTime.toString() })
.width(70).height(36).backgroundColor('#fff').borderRadius(8)
.textAlign(TextAlign.Center)
.type(InputType.Number)
.onChange(v => {
this.workTime = parseInt(v) || 30;
if (!this.isRunning) { this.remaining = this.workTime; }
})
}
Text('|').fontSize(20).fontColor('#ddd').margin({ left: 16, right: 16 })
Column() {
Text('休息(秒)').fontSize(12).fontColor('#888')
TextInput({ text: this.restTime.toString() })
.width(70).height(36).backgroundColor('#fff').borderRadius(8)
.textAlign(TextAlign.Center)
.type(InputType.Number)
.onChange(v => { this.restTime = parseInt(v) || 15 })
}
}
TextInput 组件有几个关键属性:
- type: InputType.Number:限制输入为数字,自动弹出数字键盘
- textAlign: TextAlign.Center:文字居中显示
- onChange 回调:在用户编辑完成时触发
这里的逻辑有一个特别的设计:当修改 workTime 时,如果当前没有运行(!this.isRunning),则同步更新 remaining,让倒计时显示与设定值保持一致。如果正在运行,则只改 workTime,不影响当前倒计时——避免用户在运动中误修改导致训练节奏被打乱。
parseInt(v) || 30 是一个安全兜底:如果用户清空输入框导致 parseInt 返回 NaN(假值),则回退到默认值 30。
四、HarmonyOS 特性分析
4.1 TextInput 组件的深度使用
TextInput 是 HarmonyOS 表单输入的核心组件,在本例中展示了几个重要特性:
- 双向数据绑定:通过
text属性初始化值,onChange回调更新状态 - 输入类型约束:
InputType.Number限制键盘类型,避免用户输入非数字字符 - 样式定制:
backgroundColor、borderRadius、textAlign均可独立设置
4.2 条件渲染(if/else)
ArkTS 支持在 build() 函数中直接使用 if/else 进行条件渲染。这种声明式的条件渲染比传统的 visible/display 属性切换更符合直觉,且能彻底销毁/创建组件,避免隐藏组件带来的性能开销。
适用场景:
- 开始/暂停按钮切换
- 空状态提示(列表为空时显示占位符)
- 加载中/加载完成的状态切换
4.3 弹性布局权重
.layoutWeight(1).justifyContent(FlexAlign.Center)
layoutWeight(1) 让主 Column 占据父容器中除导航栏外的所有剩余空间,配合 justifyContent(FlexAlign.Center) 实现内容垂直居中。这是 HarmonyOS 中实现"垂直居中占满"的标准模式。
五、UI/UX 设计与最佳实践
5.1 实时视觉反馈
应用通过三种方式提供即时反馈:
- 阶段文字与颜色:运动阶段显示红色「💪 运动」,休息阶段显示绿色「☕ 休息」。红/绿对比强烈,用户余光即可识别当前阶段。
- 巨大的数字倒计时:72px 字号让倒计时数字占据视觉主体,即使在远处也能看清。
- 组数统计:「已完成 X 组」的提示让用户清楚地了解训练进度。
5.2 参数配置的安全设计
- 运行时禁止影响当前倒计时:只有停止状态下的修改才会同步到
remaining - 输入容错处理:
parseInt(v) || 30确保空值或非法输入不会导致崩溃 - 最小可行输入:只暴露「秒」级别的配置,避免「分/秒」双输入带来的复杂性
5.3 按钮布局的最佳实践
三个操作按钮中,开始/暂停始终保持在固定位置(第一个按钮),重置在第二个。这种布局让肌肉记忆得以形成——用户在多次使用后可以盲操作。
六、可扩展思路
- 预设训练模板:内置 Tabata(20s 运动/10s 休息 × 8 组)、EMOM 等常见训练模板。
- 音频/振动提醒:阶段切换时通过
@ohos.vibrator振动或@ohos.multimedia.audio播放提示音。 - 训练历史记录:将每次训练的数据(组数、时长)持久化到本地存储。
- 后台计时:利用
@ohos.backgroundTaskManager实现应用切换到后台后依然精准计时。 - 自定义配色主题:允许用户为运动和休息阶段选择自己喜欢的颜色。
七、总结
间隔计时器虽然只有 97 行代码,却是一个功能完备的交互式工具应用。它展示了 ArkTS 开发中的几个关键技能:
- 多状态管理:使用多个
@State变量协同管理复杂的应用状态 - 定时器的控制与切换:
setInterval的启动、暂停、重置模式 - 条件渲染:
if/else在build()中的灵活运用 - 用户输入处理:
TextInput的配置与输入容错 - 计算属性:getter 简化数据显示逻辑
这些技能的组合,可以轻松扩展到倒计时器、番茄钟、秒表等各类计时工具中。下一期我们将从时间维度转向健康维度,探讨一个温暖的应用——喝水提醒(Water Reminder),看看如何通过可视化水杯动画,帮助用户养成良好的饮水习惯。
本文是 HarmonyOS 应用开发实战系列第 32 期,更多内容请关注专栏。
更多推荐


所有评论(0)