一、前言

经过计算器和待办事项两个项目的实践,我们已经熟练掌握了 ArkUI 的页面布局、状态管理、列表渲染和事件处理。今天我们将挑战一个更有趣的项目——番茄计时器(Pomodoro Timer)

番茄工作法是弗朗西斯科·西里洛在 1980 年代创立的时间管理方法,核心规则很简单:专注工作 25 分钟,休息 5 分钟,每完成 4 个循环后进行一次 15-30 分钟的长休息。这种节奏交替工作与休息,既保证了深度专注的效率,又避免了长时间工作的疲劳。

我们将从零开发一个完整的番茄计时器应用,包含倒计时、三种模式切换、自动循环、番茄计数和进度展示。这个项目将带你接触 ArkUI 的定时器 API、生命周期管理、@Builder 组件复用等新的知识点。相比计算器和待办事项,番茄计时器的状态管理更复杂——因为它涉及"正在倒计时"这种随时间变化的状态。


二、项目准备

2.1 开发环境要求

  • IDE:DevEco Studio(推荐最新版本)
  • SDK:API 23 或以上
  • 语言/框架:ArkTS + ArkUI

2.2 新建项目

打开 DevEco Studio,点击 File → New → Create Project,选择 Empty Ability 模板:

配置项 推荐值
项目名称 PomodoroTimer
包名 com.example.pomodorotimer
Compile SDK API 23
模块名称 entry

点击 Finish 完成创建,等待项目同步完成。


三、功能设计

3.1 界面布局

┌─────────────────────────────┐
│                             │
│     🍅 番茄计时器           │  ← 顶部标题
│                             │
│        ┌───────────┐        │
│        │   25:00   │        │  ← 倒计时数字(大号等宽字体)
│        │  🍅 专注中 │       │  ← 当前模式标签
│        └───────────┘        │
│                             │
│    ┌──────────────────┐     │
│    │  ▶️ 开始    ↺     │     │  ← 控制按钮(开始/暂停 + 重置)
│    └──────────────────┘     │
│                             │
│   ┌──────┐ ┌────┐ ┌────┐   │
│   │专注  │ │短休│ │长休│   │  ← 模式切换按钮组
│   └──────┘ └────┘ └────┘   │
│                             │
│      🔁 已完成 3 个番茄      │  ← 底部统计
│      🍅🍅🍅                │  ← 进度图标
│                             │
└─────────────────────────────┘

3.2 三种模式

番茄计时器有三种工作模式,每种有独立的时长和颜色:

模式 默认时长 颜色 图标 说明
专注 (focus) 25 分钟 橙色 #FF9F0A 🍅 核心工作时段
短休 (shortBreak) 5 分钟 绿色 #34C759 短暂休息
长休 (longBreak) 15 分钟 蓝色 #5AC8FA 🌙 每 4 个循环后

3.3 功能清单

功能 说明
倒计时 从设定时间逐秒减少,显示 MM:SS 格式
开始/暂停 点击按钮在两种状态间切换
模式切换 手动点击标签切换三种模式
自动循环 专注结束自动切短休,4 次后切长休
番茄计数 每次专注完成计数 +1
重置 停止计时并重置当前模式的剩余时间
时间到提示 显示闪烁的 ⏰ 提示文字
进度图标 每完成一个番茄显示一个 🍅

四、核心代码实现

4.1 常量定义

private readonly FOCUS_TIME: number = 25 * 60     // 专注 25 分钟
private readonly SHORT_BREAK: number = 5 * 60      // 短休 5 分钟
private readonly LONG_BREAK: number = 15 * 60      // 长休 15 分钟
private readonly MAX_CYCLES: number = 4             // 4 个循环后长休

所有时间以为单位存储。25 × 60 = 1500 秒,5 × 60 = 300 秒,15 × 60 = 900 秒。为什么不用分钟?因为倒计时每秒递减一次,用秒可以直接 -- 操作,不需要做单位转换的额外运算。

4.2 状态变量

@State displayTime: string = '25:00'        // 界面显示的倒计时字符串
@State currentMode: string = 'focus'         // 当前模式: focus/shortBreak/longBreak
@State isRunning: boolean = false             // 是否正在倒计时
@State completedCycles: number = 0            // 已完成的番茄周期数
@State isTimeUp: boolean = false              // 时间是否已到

private remainingSeconds: number = 25 * 60    // 剩余秒数(逻辑变量)
private timerId: number = -1                   // 定时器 ID,用于销毁

注意 remainingSecondstimerId 没有被 @State 装饰。timerId 是内部引用,不需要驱动 UI;remainingSeconds 每秒变化一次,如果我们直接把它声明为 @State,那每次 setInterval 回调时都会触发变更检测和 UI 重算,性能开销太大。我们的策略是:让 remainingSeconds 在定时器回调中递减,然后手动把格式化后的字符串赋值给 displayTime(唯一 @State),这样每秒只触发一次渲染。

4.3 时间格式化

private formatTime(seconds: number): string {
  const mins = Math.floor(seconds / 60)
  const secs = seconds % 60
  return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
}

padStart(2, '0') 确保分钟和秒始终显示两位数字。比如 5 分 3 秒显示为 05:03 而非 5:3。这在倒计时显示中非常重要——数字跳变时保持位数一致,视觉上更稳定。

4.4 模式配置映射

private getModeTime(mode: string): number {
  if (mode === 'focus') return this.FOCUS_TIME
  if (mode === 'shortBreak') return this.SHORT_BREAK
  return this.LONG_BREAK
}

private getModeLabel(mode: string): string {
  if (mode === 'focus') return '🍅 专注中'
  if (mode === 'shortBreak') return '☕ 短休中'
  return '🌙 长休中'
}

private getModeColor(mode: string): string {
  if (mode === 'focus') return '#FF9F0A'
  if (mode === 'shortBreak') return '#34C759'
  return '#5AC8FA'
}

这三个映射函数将模式标识符(字符串)分别映射为:时长、标签文字、主题色。这样做的好处是:如果需要增加或修改模式,只需在这里改三处,不需要满篇搜索硬编码。

4.5 开始/暂停

private toggleTimer(): void {
  if (this.isRunning) {
    this.pauseTimer()
  } else {
    this.startTimer()
  }
}

private startTimer(): void {
  this.isRunning = true
  this.isTimeUp = false

  this.timerId = setInterval(() => {
    this.remainingSeconds--

    if (this.remainingSeconds <= 0) {
      this.displayTime = '⏰ 时间到!'
      this.onTimeUp()
      return
    }

    this.displayTime = this.formatTime(this.remainingSeconds)
  }, 1000)
}

private pauseTimer(): void {
  this.isRunning = false
  if (this.timerId !== -1) {
    clearInterval(this.timerId)
    this.timerId = -1
  }
}

setInterval(callback, 1000) 创建一个每秒执行一次的定时器。每次回调中:

  1. 剩余秒数减一
  2. 如果剩余为 0,显示"时间到"并调用 onTimeUp
  3. 否则更新显示时间

关键细节setInterval 返回的 timerId 必须在暂停或销毁时调用 clearInterval 清除,否则定时器会持续运行,即使页面关闭或用户切换到了其他 App,定时器依然在后台消耗资源。aboutToDisappear 生命周期就是用来处理这个问题的。

4.6 时间到处理

private onTimeUp(): void {
  this.pauseTimer()
  this.isTimeUp = true

  if (this.currentMode === 'focus') {
    this.completedCycles++
    if (this.completedCycles % this.MAX_CYCLES === 0) {
      this.switchMode('longBreak')
    } else {
      this.switchMode('shortBreak')
    }
  } else {
    this.switchMode('focus')
  }
}

自动循环逻辑:

  • 专注结束 → 判断休息类型:如果已完成周期数是 4 的倍数(第 4、8、12…次),自动切换到长休;否则切换到短休
  • 休息结束 → 回到专注:无论短休还是长休结束,都自动回到专注模式

这样用户不需要手动操作,计时器会自动在专注和休息之间循环,保持了番茄工作法的完整节奏。

4.7 模式切换

private switchMode(mode: string): void {
  if (this.isRunning) this.pauseTimer()
  this.currentMode = mode
  this.remainingSeconds = this.getModeTime(mode)
  this.displayTime = this.formatTime(this.remainingSeconds)
  this.isTimeUp = false
}

private selectMode(mode: string): void {
  this.switchMode(mode)
  this.isRunning = false
}

switchMode 是内部方法,供自动切换和手动切换共用。selectMode 是用户手动点击模式标签时调用的方法,在切换完成后额外确保计时器保持暂停状态——因为用户在切换模式时通常想重新开始计时。

4.8 开始/暂停的防连点保护

倒计时相关的按钮有个容易被忽略的问题:快速连点。用户在紧张或急躁时可能快速多次点击开始按钮,导致多个 setInterval 同时运行,倒计时速度翻倍。

我们的代码通过 isRunning 状态做了天然防护:

private toggleTimer(): void {
  if (this.isRunning) {
    this.pauseTimer()
  } else {
    this.startTimer()
  }
}

startTimer() 只负责设置定时器,不需要检查 isRunning——因为 toggleTimer 已经保证了在 isRunning = true 时不会调用 startTimer。同理 pauseTimerisRunning = false 时也不会被调用。这个设计模式叫做状态门控(State Gating),用一个布尔变量严格控制两种互斥状态之间的切换。

4.9 重置

private resetTimer(): void {
  this.pauseTimer()
  this.remainingSeconds = this.getModeTime(this.currentMode)
  this.displayTime = this.formatTime(this.remainingSeconds)
  this.isTimeUp = false
}

重置将当前模式的时间恢复为初始值,但不切换模式。比如你正在短休模式下倒计时到 2:30,点重置后恢复为 5:00。

4.9 生命周期清理

aboutToDisappear(): void {
  this.pauseTimer()
}

当用户离开这个页面时(比如返回上一页或切换到其他 App),调用 aboutToDisappear 确保定时器被清除。这是防止内存泄漏的标准做法。

4.10 进度图标

private getCycleDots(): string[] {
  const dots: string[] = []
  for (let i = 0; i < this.completedCycles; i++) {
    dots.push('🍅')
  }
  return dots
}

根据已完成周期数生成对应数量的番茄 emoji 数组,用于在底部展示进度。每完成一个专注周期就多显示一个 🍅,成就感满满。

4.11 完整 UI 构建

build() {
  Column() {
    // ═══ 顶部标题 ═══
    Text('🍅 番茄计时器')
      .fontSize(28).fontWeight(FontWeight.Bold).fontColor(Color.White)
      .padding(20)

    // ═══ 倒计时显示区(圆形面板)═══
    Column() {
      Text(this.displayTime)
        .fontSize(72).fontWeight(FontWeight.Bold)
        .fontColor(this.isTimeUp ? '#FF3B30' : this.getModeColor(this.currentMode))
        .fontFamily('Courier New')

      Text(this.getModeLabel(this.currentMode))
        .fontSize(18).fontColor(this.getModeColor(this.currentMode))
        .margin({ top: 8 })
    }
    .width(240).height(240).backgroundColor('#1C1C1E')
    .borderRadius(120).justifyContent(FlexAlign.Center)

    // ═══ 控制按钮 ═══
    Row() {
      Button(this.isRunning ? '⏸️ 暂停' : '▶️ 开始')
        .fontSize(20).fontWeight(FontWeight.Bold)
        .width(160).height(56).borderRadius(28)
        .backgroundColor(this.getModeColor(this.currentMode))
        .fontColor(Color.White)
        .onClick(() => { this.toggleTimer() })

      Button('↺')
        .fontSize(24).width(56).height(56).borderRadius(28)
        .backgroundColor('#333333')
        .margin({ left: 12 })
        .onClick(() => { this.resetTimer() })
    }
    .margin({ top: 30 })

    // ═══ 模式切换标签 ═══
    Row() {
      this.modeButton('focus', '🍅 专注')
      this.modeButton('shortBreak', '☕ 短休')
      this.modeButton('longBreak', '🌙 长休')
    }
    .width('100%').padding({ left: 20, right: 20 })
    .margin({ top: 24 })

    // ═══ 底部统计 ═══
    Text(`🔁 已完成 ${this.completedCycles} 个番茄`)
      .fontSize(16).fontColor('#8E8E93')
      .margin({ top: 20 })

    if (this.completedCycles >= 1) {
      Row() {
        ForEach(this.getCycleDots(), (dot: string) => {
          Text(dot).fontSize(22).margin({ left: 4, right: 4 })
        })
      }
      .margin({ top: 8 })
    }
  }
  .width('100%').height('100%').backgroundColor('#000000')
  .justifyContent(FlexAlign.Start)
}

// @Builder 抽离的模式切换按钮
@Builder modeButton(mode: string, label: string) {
  Button(label)
    .fontSize(14).fontWeight(FontWeight.Medium)
    .height(40).borderRadius(20)
    .backgroundColor(this.currentMode === mode
      ? this.getModeColor(mode) : '#2C2C2E')
    .fontColor(this.currentMode === mode ? Color.White : '#8E8E93')
    .layoutWeight(1)
    .margin({ left: 4, right: 4 })
    .onClick(() => { this.selectMode(mode) })
}

UI 设计要点:

  1. 等宽字体fontFamily('Courier New') 让所有数字宽度一致。普通字体中 10 窄,倒计时数字跳动时会左右晃动;等宽字体消除了这个视觉问题
  2. @Builder:ArkUI 中抽离重复 UI 的利器。modeButton 方法创建可复用的标签按钮,通过参数控制颜色和文字。代码中三个标签只写了三行,实际按钮逻辑集中在一处
  3. 条件渲染底部进度if (this.completedCycles >= 1) 确保在没有完成任何番茄时不显示空行
  4. 大号圆形倒计时面板:240×240 像素,圆角 120 实现正圆形,视觉上更像一个钟表

五、运行与测试

5.1 替换代码

打开 entry/src/main/ets/pages/Index.ets,全选替换为上面提供的完整代码。

5.2 功能测试

# 测试操作 预期结果
1 启动 App 显示 25:00,模式标签"🍅 专注"高亮
2 点 ▶️ 开始 倒计时开始逐秒递减,按钮变为 ⏸️ 暂停
3 点 ⏸️ 暂停 倒计时暂停,按钮恢复 ▶️
4 点 ☕ 短休 切换到 05:00,短休标签高亮绿色
5 点 🌙 长休 切换到 15:00,长休标签高亮蓝色
6 等待专注结束 显示 ⏰ 时间到!,番茄数 +1
7 连续完成 4 个番茄 第 4 次自动切换到长休
8 点 ↺ 重置 回到当前模式的初始值
9 快速切模式 每次切换后时间重置为新模式的值
10 退出再打开 番茄数重置为 0(无持久化,建议后续加)

5.3 调试技巧

测试倒计时时,可以把专注时间临时改成 5 秒以便快速验证:

private readonly FOCUS_TIME: number = 5  // 改成 5 秒方便调试

验证完自动循环逻辑后再改回 25 × 60。


六、运行效果

在这里插入图片描述


从这三个项目的变化可以清楚看到鸿蒙开发的学习路径:计算器是静态交互(点一下动一下),待办 App 是数据管理(增删改查),计时器是动态过程(随时间变化)。每一步都比前一步更接近真实 App 的复杂度。

七、扩展思路

  1. 自定义时长:增加设置页面,用 Slider 滑动条让用户自定义专注/休息时长
  2. 系统通知:时间到时调用 Notification.requestPermission 发送系统推送通知
  3. 每日统计:将每天的完成番茄数用 @StorageLink 持久化,用图表展示
  4. 白噪音:集成简单的白噪音播放(雨声、篝火声),专注时自动播放
  5. 严格模式:开启后专注期间不可切换模式或暂停,倒逼专注

八、总结

本文从零开始完成了鸿蒙番茄计时器 App 的开发,核心知识点回顾:

  • 定时器setInterval / clearInterval 实现每秒倒计时
  • 生命周期aboutToDisappear 清理定时器,防止内存泄漏
  • @Builder:抽离可复用 UI 组件,减少重复代码
  • 状态管理:一个 @State(displayTime)驱动所有 UI 变化
  • 自动循环:专注→短休→专注→…→长休的完整工作流
  • 等宽字体fontFamily('Courier New') 让数字对齐
  • 条件渲染if (completedCycles >= 1) 控制进度显示

三个项目循序渐进:计算器训练事件处理和基本布局,待办 App 训练列表和数据持久化,番茄计时器训练定时器和复杂状态管理。完成本系列三篇文章的所有代码,你已经掌握了 ArkUI 开发的核心知识体系,从零搭建一个完整的鸿蒙工具类应用已经不在话下。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐