一、应用概述
间隔计时器(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 (休息秒数) │ └── TextInput2.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 期,更多内容请关注专栏。