一、应用概述

间隔计时器(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 根据当前阶段从 workTimerestTime 重新取值。如果翻转后进入运动阶段,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() 的逻辑简洁明了:清除定时器并将运行状态置为 falsereset() 则彻底回到初始状态——运动阶段、剩余时间取 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 表单输入的核心组件,在本例中展示了几个重要特性:

  1. 双向数据绑定:通过 text 属性初始化值,onChange 回调更新状态
  2. 输入类型约束InputType.Number 限制键盘类型,避免用户输入非数字字符
  3. 样式定制backgroundColorborderRadiustextAlign 均可独立设置

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 实时视觉反馈

应用通过三种方式提供即时反馈:

  1. 阶段文字与颜色:运动阶段显示红色「💪 运动」,休息阶段显示绿色「☕ 休息」。红/绿对比强烈,用户余光即可识别当前阶段。
  2. 巨大的数字倒计时:72px 字号让倒计时数字占据视觉主体,即使在远处也能看清。
  3. 组数统计:「已完成 X 组」的提示让用户清楚地了解训练进度。

5.2 参数配置的安全设计

  • 运行时禁止影响当前倒计时:只有停止状态下的修改才会同步到 remaining
  • 输入容错处理parseInt(v) || 30 确保空值或非法输入不会导致崩溃
  • 最小可行输入:只暴露「秒」级别的配置,避免「分/秒」双输入带来的复杂性

5.3 按钮布局的最佳实践

三个操作按钮中,开始/暂停始终保持在固定位置(第一个按钮),重置在第二个。这种布局让肌肉记忆得以形成——用户在多次使用后可以盲操作。

六、可扩展思路

  1. 预设训练模板:内置 Tabata(20s 运动/10s 休息 × 8 组)、EMOM 等常见训练模板。
  2. 音频/振动提醒:阶段切换时通过 @ohos.vibrator 振动或 @ohos.multimedia.audio 播放提示音。
  3. 训练历史记录:将每次训练的数据(组数、时长)持久化到本地存储。
  4. 后台计时:利用 @ohos.backgroundTaskManager 实现应用切换到后台后依然精准计时。
  5. 自定义配色主题:允许用户为运动和休息阶段选择自己喜欢的颜色。

七、总结

间隔计时器虽然只有 97 行代码,却是一个功能完备的交互式工具应用。它展示了 ArkTS 开发中的几个关键技能:

  • 多状态管理:使用多个 @State 变量协同管理复杂的应用状态
  • 定时器的控制与切换setInterval 的启动、暂停、重置模式
  • 条件渲染if/elsebuild() 中的灵活运用
  • 用户输入处理TextInput 的配置与输入容错
  • 计算属性:getter 简化数据显示逻辑

这些技能的组合,可以轻松扩展到倒计时器、番茄钟、秒表等各类计时工具中。下一期我们将从时间维度转向健康维度,探讨一个温暖的应用——喝水提醒(Water Reminder),看看如何通过可视化水杯动画,帮助用户养成良好的饮水习惯。


本文是 HarmonyOS 应用开发实战系列第 32 期,更多内容请关注专栏。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐