鸿蒙原生开发——从零构建番茄钟计时器
一、引言
番茄工作法是世界上最简单也最有效的时间管理方法之一:专注 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++)
从空闲到运行中的转换很简单。但有两个自动转换值得特别注意:
-
工作 → 休息:工作在 25 分钟后自动结束,无需用户操作,立即开始 5 分钟休息倒计时。这种"无缝切换"减少了用户的操作步骤——他们不需要在听到闹钟后手动点"开始休息"。
-
休息 → 空闲:休息在 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.tick,this 会在回调中丢失上下文。
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 极简深色界面
页面只有三个视觉元素:
- 顶部标题栏:番茄 emoji + “番茄钟” + 已完成计数
- 中央计时区:模式标签(专注/休息)→ 巨大数字倒计时(64 号字)→ 进度条 → 状态文字
- 底部控制区:按钮根据状态动态变化
没有多余的装饰、没有底部导航、没有卡片和边框——计时器的 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)
选中态为蓝底白字,未选中态为半透明底灰字。点击后同时更新 totalSeconds 和 secondsLeft——相当于"重置为新时长"。
时长选择器只在空闲状态下显示(!isRunning && !isPaused && !isBreak)。一旦开始计时,选择器消失——用户不能在计时进行中改变时长(这会导致倒计时逻辑混乱)。这种"只在安全状态下才可配置"的设计是所有计时器 App 的通用做法。
五、交互流程
5.1 完整番茄周期
一个完整的番茄周期:
- 用户打开页面,看到"准备开始",时长默认 25 分钟
- 用户可切换时长(15/25/45 分钟)
- 点击"开始专注"→ 倒计时开始,每秒更新
- 25 分钟后 → 自动切换"休息时间",5 分钟倒计时
- 休息倒计时中可点"跳过休息"直接进入下一轮
- 休息结束 → 已完成番茄数 +1,回到空闲状态
- 回到步骤 2,开始下一个番茄
4 个交互点:
- 开始/暂停/继续:控制计时器运行状态
- 重置:放弃当前计时回到初始状态
- 切换时长:在空闲状态选择 15/25/45 分钟
- 跳过休息:缩短休息时间直接开始下一轮
六、完整代码结构
PomodoroTimerPage
├── Column(根容器,深色底)
│ ├── Row(标题栏:🍅 番茄钟 + 已完成 N 个番茄)
│ └── Column(中央内容区,layoutWeight 1,居中)
│ ├── Text(模式标签:🎯 专注时间 / ☕ 休息时间)
│ ├── Text(大号倒计时 MM:SS,64号字)
│ ├── Row(进度条:200vp 宽,4vp 高,色块随进度增长)
│ ├── Text(状态文字:准备开始 / 计时中 / 已暂停)
│ ├── Row(时长选择器,仅空闲状态显示)
│ ├── Row(控制按钮区,按状态切换按钮组合)
│ │ ├── 空闲:▶ 开始专注
│ │ ├── 运行:⏸ 暂停 | ↺ 重置
│ │ └── 暂停:▶ 继续 | ↺ 重置
│ └── Button(跳过休息,仅休息中显示)
七、总结
本文从零构建了一个番茄钟计时器。与前两篇的习惯追踪和记账本不同,番茄钟是一个时间驱动的状态机——数据模型简单,但状态流转复杂。
核心要点回顾:
-
状态机设计:四个状态(空闲/运行/暂停/休息)驱动所有 UI 分支。每种状态对应一组特定的按钮和显示。状态转移在用户操作(开始/暂停/继续/重置)和时间事件(倒计时结束)的触发下执行。
-
setInterval 生命周期:
tick()每秒执行一次,倒计时结束时自动切换模式。aboutToDisappear清理定时器防止内存泄漏。暂停 =clearInterval+ 保持状态,恢复 = 重新setInterval。 -
自动模式切换:工作时段结束自动进入休息(重新启动定时器),休息结束回到空闲(递增计数 + 停止定时器)。用户不需要手动控制模式切换——系统自动处理。
-
按键配色:暂停按钮(琥珀色
#FAAD14)配深色文字(#1a1a2e)而非白色。这是基于上一篇反馈的改进——任何情况下都要检查文字与背景的对比度,"不确定时选深色字"是最简单的安全原则。 -
时长选择:只在空闲状态下可配置(防止计时中途改时长),选中态蓝底白字,点击后同步更新
totalSeconds和secondsLeft。
更多推荐

所有评论(0)