一、引言

番茄工作法是世界上最简单也最有效的时间管理方法之一:专注 25 分钟,休息 5 分钟,循环往复。不需要复杂的设置,不需要学习曲线——按一下按钮,开始计时,然后专注工作。

但从开发者的角度看,一个番茄钟涉及的技术点却不少:setInterval 定时器管理组件生命周期与 aboutToDisappear 清理状态机设计(空闲 → 运行 → 暂停 → 休息 → 完成)、自动模式切换(工作结束自动进入休息)、UI 状态同步(不同状态下显示不同的按钮组合)。

本文将用 ArkUI 从零构建一个完整的番茄钟计时器。功能包括:开始专注(25 分钟倒计时)、自动切换休息(5 分钟)、暂停和继续、重置、跳过休息、完成计数。全程深色主题,纯前端实现。

配色方面,吸取了之前的教训——暂停按钮的琥珀色底(#FAAD14)配合的是深色文字(#1a1a2e),而非白色文字。浅色背景下用深色字,这是保证可读性的基本原则。

阅读完本文,你将能够:

  • 正确使用 setInterval / clearInterval 实现倒计时
  • aboutToDisappear 中清理定时器,防止内存泄漏
  • 设计清晰的状态机来控制 UI 分支
  • 实现工作/休息的自动切换逻辑
  • 掌握时长选择器的交互设计

二、状态机设计

2.1 四种核心状态

番茄钟比习惯追踪器和记账本更复杂——它不是"数据显示 + 增删改查",而是一个有明确生命周期的时间驱动的状态机:

状态 isRunning isPaused isBreak 说明
空闲 false false false 初始状态,可选择时长并开始
运行中 true false false/true 倒计时进行中
已暂停 false true false/true 倒计时暂停,可继续或重置
休息中 true false true 工作时段结束,自动进入休息倒计时

sessions(已完成番茄数)和 secondsLeft(剩余秒数)是独立于状态机的数据字段——它们不决定 UI 分支,但驱动核心显示。

2.2 状态转移图

空闲 ──[点击开始]──→ 运行中(工作)
运行中(工作)──[倒计时结束]──→ 运行中(休息,自动)
运行中(休息)──[倒计时结束]──→ 空闲(sessions++)
运行中 ──[点击暂停]──→ 已暂停
已暂停 ──[点击继续]──→ 运行中
已暂停 ──[点击重置]──→ 空闲
运行中(休息)──[点击跳过]──→ 空闲(sessions++)

从空闲到运行中的转换很简单。但有两个自动转换值得特别注意:

  1. 工作 → 休息:工作在 25 分钟后自动结束,无需用户操作,立即开始 5 分钟休息倒计时。这种"无缝切换"减少了用户的操作步骤——他们不需要在听到闹钟后手动点"开始休息"。

  2. 休息 → 空闲:休息在 5 分钟后自动结束,番茄计数 +1,回到空闲状态。此时用户可以选择"再做一个番茄"或停止。
    在这里插入图片描述

三、定时器管理

3.1 核心 tick 逻辑

所有倒计时逻辑集中在一个 tick() 方法中:

tick(): void {
  if (this.secondsLeft > 0) {
    this.secondsLeft--;
    return;
  }
  // 倒计时结束
  clearInterval(this.timerId);
  this.timerId = -1;
  if (this.isBreak) {
    // 休息结束 → 回到空闲
    this.isBreak = false;
    this.totalSeconds = this.getWorkSeconds();
    this.secondsLeft = this.totalSeconds;
    this.isRunning = false;
    this.sessions++;
  } else {
    // 工作结束 → 自动进入休息
    this.isBreak = true;
    this.totalSeconds = 5 * 60;
    this.secondsLeft = this.totalSeconds;
    this.timerId = setInterval(() => { this.tick(); }, 1000);
  }
}

tick() 每秒执行一次(由 setInterval 驱动)。当 secondsLeft 降到 0 时,判断当前是工作还是休息——工作结束自动开始休息(重新设置 setInterval),休息结束回到空闲状态并递增 sessions

注意 this.tick()setInterval 中需要用箭头函数包装:() => { this.tick(); }。如果直接传 this.tickthis 会在回调中丢失上下文。

3.2 生命周期清理

setInterval 创建了一个持续运行的定时器。如果用户离开页面时定时器没有清理,它会继续在后台运行——消耗 CPU、浪费电量、可能导致状态异常。更严重的是,如果组件已被销毁但定时器的回调还在尝试更新 @State,会报错。

aboutToDisappear() 是清理定时器的正确时机:

aboutToDisappear(): void {
  if (this.timerId !== -1) {
    clearInterval(this.timerId);
    this.timerId = -1;
  }
}

这确保了无论用户是通过返回键、导航跳转、还是其他方式离开页面,定时器都会被清理。timerId 判断(!== -1)防止了对无效 ID 调用 clearInterval

3.3 暂停与恢复

暂停的核心操作是 clearInterval——停止定时器,但不重置状态:

pauseTimer(): void {
  clearInterval(this.timerId);
  this.timerId = -1;
  this.isRunning = false;
  this.isPaused = true;
  // secondsLeft 保持不变——恢复时从当前时间继续
}

恢复时重新创建 setInterval

resumeTimer(): void {
  this.isRunning = true;
  this.isPaused = false;
  this.timerId = setInterval(() => { this.tick(); }, 1000);
}

secondsLeft 在暂停期间保持不变,恢复后从暂停处继续——这正是用户期望的行为。

3.4 跳过休息

在休息倒计时期间,用户可以点击"跳过休息,开始下一轮"直接回到空闲状态:

skipBreak(): void {
  clearInterval(this.timerId);
  this.timerId = -1;
  this.isBreak = false;
  this.totalSeconds = this.getWorkSeconds();
  this.secondsLeft = this.totalSeconds;
  this.isRunning = false;
  this.isPaused = false;
  this.sessions++;  // 当前番茄也算完成
}

注意 sessions++——跳过休息意味着当前工作时段已完成,所以计数仍需递增。
在这里插入图片描述

四、UI 设计

4.1 极简深色界面

页面只有三个视觉元素:

  1. 顶部标题栏:番茄 emoji + “番茄钟” + 已完成计数
  2. 中央计时区:模式标签(专注/休息)→ 巨大数字倒计时(64 号字)→ 进度条 → 状态文字
  3. 底部控制区:按钮根据状态动态变化

没有多余的装饰、没有底部导航、没有卡片和边框——计时器的 UI 应该尽可能"安静",让用户将注意力集中在倒计时的数字上。

4.2 按钮三态

页面在不同状态下显示不同的按钮组合:

空闲状态(!isRunning && !isPaused):

  • ▶ 开始专注(蓝色大按钮,带投影)
  • 时长选择器(15/25/45 分钟胶囊)

运行状态(isRunning):

  • ⏸ 暂停(琥珀色底 + 深色文字)
  • ↺ 重置(半透明底 + 白色文字)

暂停状态(isPaused):

  • ▶ 继续(蓝色按钮)
  • ↺ 重置(半透明按钮)

三种按钮组合完全互斥——用户在任何时刻只看到两个按钮(或一个按钮 + 时长选择器)。这种"只在需要时才显示"的设计减少了视觉噪音,让用户更容易做出正确的操作。

4.3 暂停按钮的配色

暂停按钮用了琥珀色底(#FAAD14)配深色文字(#1a1a2e):

Button('⏸  暂停')
  .fontColor('#1a1a2e')   // 深色文字
  .backgroundColor('#FAAD14')  // 琥珀色底

这是吸取了之前教训后的设计选择。#FAAD14(RGB: 250, 173, 20)是一个中等亮度的暖色,WCAG 亮度值约为 0.65。白色文字(亮度 1.0)在它上面的对比度仅约 1.5:1——严重不足。而深色文字(#1a1a2e,亮度约 0.06)的对比度约为 10:1——远超 AA 标准。

设计中的黄金法则:不确认对比度时,选深色文字。在浅色 UI 中深色文字几乎总是安全的;在深色 UI 中白字在深色底上也是安全的。只有"浅色底 + 白字"和"深色底 + 深色字"这两种组合需要警惕——前者看不清,后者也看不清。

4.4 时长选择器

在空闲状态下,用户可以切换专注时长(15/25/45 分钟)。它是一个简单的胶囊按钮组:

this.minChip('15 分钟', 15)
this.minChip('25 分钟', 25)
this.minChip('45 分钟', 45)

选中态为蓝底白字,未选中态为半透明底灰字。点击后同时更新 totalSecondssecondsLeft——相当于"重置为新时长"。

时长选择器只在空闲状态下显示(!isRunning && !isPaused && !isBreak)。一旦开始计时,选择器消失——用户不能在计时进行中改变时长(这会导致倒计时逻辑混乱)。这种"只在安全状态下才可配置"的设计是所有计时器 App 的通用做法。
在这里插入图片描述

五、交互流程

5.1 完整番茄周期

一个完整的番茄周期:

  1. 用户打开页面,看到"准备开始",时长默认 25 分钟
  2. 用户可切换时长(15/25/45 分钟)
  3. 点击"开始专注"→ 倒计时开始,每秒更新
  4. 25 分钟后 → 自动切换"休息时间",5 分钟倒计时
  5. 休息倒计时中可点"跳过休息"直接进入下一轮
  6. 休息结束 → 已完成番茄数 +1,回到空闲状态
  7. 回到步骤 2,开始下一个番茄

4 个交互点:

  • 开始/暂停/继续:控制计时器运行状态
  • 重置:放弃当前计时回到初始状态
  • 切换时长:在空闲状态选择 15/25/45 分钟
  • 跳过休息:缩短休息时间直接开始下一轮

六、完整代码结构

PomodoroTimerPage
├── Column(根容器,深色底)
│   ├── Row(标题栏:🍅 番茄钟 + 已完成 N 个番茄)
│   └── Column(中央内容区,layoutWeight 1,居中)
│       ├── Text(模式标签:🎯 专注时间 / ☕ 休息时间)
│       ├── Text(大号倒计时 MM:SS,64号字)
│       ├── Row(进度条:200vp 宽,4vp 高,色块随进度增长)
│       ├── Text(状态文字:准备开始 / 计时中 / 已暂停)
│       ├── Row(时长选择器,仅空闲状态显示)
│       ├── Row(控制按钮区,按状态切换按钮组合)
│       │   ├── 空闲:▶ 开始专注
│       │   ├── 运行:⏸ 暂停 | ↺ 重置
│       │   └── 暂停:▶ 继续 | ↺ 重置
│       └── Button(跳过休息,仅休息中显示)

七、总结

本文从零构建了一个番茄钟计时器。与前两篇的习惯追踪和记账本不同,番茄钟是一个时间驱动的状态机——数据模型简单,但状态流转复杂。

核心要点回顾:

  1. 状态机设计:四个状态(空闲/运行/暂停/休息)驱动所有 UI 分支。每种状态对应一组特定的按钮和显示。状态转移在用户操作(开始/暂停/继续/重置)和时间事件(倒计时结束)的触发下执行。

  2. setInterval 生命周期tick() 每秒执行一次,倒计时结束时自动切换模式。aboutToDisappear 清理定时器防止内存泄漏。暂停 = clearInterval + 保持状态,恢复 = 重新 setInterval

  3. 自动模式切换:工作时段结束自动进入休息(重新启动定时器),休息结束回到空闲(递增计数 + 停止定时器)。用户不需要手动控制模式切换——系统自动处理。

  4. 按键配色:暂停按钮(琥珀色 #FAAD14)配深色文字(#1a1a2e)而非白色。这是基于上一篇反馈的改进——任何情况下都要检查文字与背景的对比度,"不确定时选深色字"是最简单的安全原则。

  5. 时长选择:只在空闲状态下可配置(防止计时中途改时长),选中态蓝底白字,点击后同步更新 totalSecondssecondsLeft

Logo

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

更多推荐