查看: 8518|回复: 3

ArkUI番茄钟开发:setInterval误差与后台恢复

[复制链接]
发表于 2026-9-22 14:00:00 | 显示全部楼层 |阅读模式
背景与目标

最近为了提高工作专注度,作者打算用鸿蒙 ArkUI 做一个番茄钟。目标不是只显示一个倒计时,而是把工作、短休息、长休息和循环切换串起来。鸿蒙提供的计时器能力里,setInterval 适合周期性执行,setTimeout 适合延迟执行一次,requestAnimationFrame 用于动画帧回调;这个番茄钟选择 setInterval,按秒更新剩余时间。

状态与流程设计

先把番茄钟状态拆成 WORK、SHORT_BREAK、LONG_BREAK、IDLE 四种。需要用 @State 驱动 UI:state 表示当前阶段,timeLeft 表示剩余秒数,completedPomodoros 记录已完成番茄数,timerId 保存计时器句柄。
  1. enum PomodoroState {
  2. WORK, // 工作中
  3. SHORT_BREAK, // 短休息
  4. LONG_BREAK, // 长休息
  5. IDLE // 空闲
  6. }
  7. @State state: PomodoroState = PomodoroState.IDLE;
  8. @State timeLeft: number = 25 * 60; // 剩余秒数
  9. @State completedPomodoros: number = 0;
  10. private timerId: number = -1;
复制代码

这里的 25 * 60 对应一个标准工作时段的 25 分钟,短休息为 5 分钟,长休息为 15 分钟。流程上每完成一个工作番茄就累加 completedPomodoros;每完成 4 个番茄进入长休息,否则进入短休息;休息结束后再回到工作状态。

用 setInterval 驱动倒计时

核心计时逻辑是启动一个每秒执行一次的 setInterval。剩余时间大于 0 就递减;递减到 0 时调用结束处理,并停止当前计时器。
  1. private startTimer(): void {
  2. this.timerId = setInterval(() => {
  3. if (this.timeLeft > 0) {
  4. this.timeLeft--;
  5. } else {
  6. this.handleTimerEnd();
  7. }
  8. }, 1000);
  9. }
  10. private stopTimer(): void {
  11. if (this.timerId !== -1) {
  12. clearInterval(this.timerId);
  13. this.timerId = -1;
  14. }
  15. }
复制代码

需要特别注意,handleTimerEnd 中要先 stopTimer,再切换状态和重置 timeLeft,最后重新 startTimer。否则会出现旧计时器未清理、多个计时器同时跑的问题。原文的切换逻辑如下:
  1. private handleTimerEnd(): void {
  2. this.stopTimer();
  3. if (this.state === PomodoroState.WORK) {
  4. this.completedPomodoros++;
  5. if (this.completedPomodoros % 4 === 0) {
  6. this.state = PomodoroState.LONG_BREAK;
  7. this.timeLeft = 15 * 60;
  8. } else {
  9. this.state = PomodoroState.SHORT_BREAK;
  10. this.timeLeft = 5 * 60;
  11. }
  12. } else {
  13. this.state = PomodoroState.WORK;
  14. this.timeLeft = 25 * 60;
  15. }
  16. this.startTimer();
  17. }
复制代码

时间格式化与环形进度

剩余秒数需要展示成 mm:ss。原文用 formatTime 做转换,并用 padStart 补零。
  1. private formatTime(seconds: number): string {
  2. const mins = Math.floor(seconds / 60);
  3. const secs = seconds % 60;
  4. return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
  5. }
复制代码

UI 侧用 Progress 组件显示进度,类型选择 ProgressType.Ring,宽高各 200。value 绑定 timeLeft,total 则根据当前状态取工作时长或休息时长。原文示例里工作状态按 25 * 60 计算,其余按 5 * 60 计算。
  1. Progress({
  2. value: this.timeLeft,
  3. total: this.state === PomodoroState.WORK ? 25 * 60 : 5 * 60,
  4. type: ProgressType.Ring
  5. })
  6. .width(200)
  7. .height(200)
复制代码

问题一:setInterval 不精确,长时间累积误差

直接用 setInterval 每秒 timeLeft-- 看起来简单,但它并不精确。回调触发时间会受主线程任务、系统调度影响,长时间运行后可能累积误差。对番茄钟这类需要持续几十秒到几分钟的倒计时,更稳妥的方式是用时间戳计算已经过去的时间,而不是只依赖回调次数。原文给出的改进方式是在启动时记录 Date.now(),定时器按更短间隔检查,并用当前时间戳减去 startTime 得到 elapsed,再反推 timeLeft。
  1. private startTime: number = 0;
  2. private totalDuration: number = 25 * 60;
  3. private startTimer(): void {
  4. this.startTime = Date.now();
  5. this.timerId = setInterval(() => {
  6. const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
  7. this.timeLeft = this.totalDuration - elapsed;
  8. if (this.timeLeft <= 0) {
  9. this.handleTimerEnd();
  10. }
  11. }, 100);
  12. }
复制代码

这段改造体现了倒计时类应用的一个常见原则:setInterval 只负责触发检查,真正的时间基准要来自系统时间戳。这样即使某次回调被延迟,下一次也能根据真实时间差校正显示。

问题二:后台运行与状态恢复

应用切到后台后,setInterval 可能会变慢,倒计时更新不再可靠。原文提到两种方向:一是用 backgroundTaskManager 申请后台任务,二是用本地通知提醒。前者偏向让应用在后台继续执行必要任务,后者偏向在关键时间点提醒用户,具体选择要看番茄钟希望达到的后台能力。

另一个容易被忽略的问题是状态持久化。应用进程被杀死后,当前状态、剩余时间、已完成番茄数都会丢失。原文建议用 Preferences 保存当前状态和时间戳,下次启动时恢复。这里需要保存当前处于哪个阶段、该阶段的总时长、开始时间或剩余时间,否则恢复后无法准确续算。

小结

这个番茄钟的实现并不复杂:用 ArkUI 的 @State 管理状态,用 setInterval 驱动刷新,用 Progress 展示环形进度,再按完成数量切换工作、短休息和长休息。真正影响体验的是细节:计时精度要用时间戳校正,后台运行要处理 setInterval 变慢,进程被杀后要用 Preferences 做状态恢复。原文还提到可以增加统计功能,记录每天完成了多少个番茄,这能进一步提升使用成就感。
回复

使用道具 举报

发表于 2026-9-22 19:00:00 | 显示全部楼层

Re: ArkUI番茄钟开发:setInterval误差与后台恢复

这个番茄钟的状态拆分和计时器清理思路挺清楚的,尤其是 handleTimerEnd 里先 stopTimer 再切换状态、重置 timeLeft,最后再 startTimer,这点很关键,不然确实容易出现多个 interval 叠着跑。 setInterval 用时间戳校准也更稳,100ms 检查只是触发,真正剩余时间用 totalDuration 减去 elapsed 来反推。后台恢复可以顺着这个思路做:切后台时记一下时间戳,回前台时用当前时间减掉它,重新算 timeLeft;如果已经小于等于 0,就直接走结束处理,再按当前状态决定进入工作还是休息。 另外 Progress 的 total 那段可以再对一下,LONG_BREAK 是 15 分钟,但示例里非 WORK 都按 5 * 60,长休息时环形进度比例会偏。建议按状态分别取 25、5、15 分钟。后台任务和本地通知这块,如果只是提醒用户,通知会更轻;如果要后台继续维持计时,再考虑 backgroundTaskManager。整体方向没问题,把后台恢复的边界情况补上就更完整了。
回复 支持 反对

使用道具 举报

发表于 2026-9-22 19:10:00 | 显示全部楼层

Re: ArkUI番茄钟开发:setInterval误差与后台恢复

这个番茄钟的状态拆分挺清楚,WORK、SHORT_BREAK、LONG_BREAK、IDLE 加上 completedPomodoros,流程一眼能看明白。setInterval 那段提醒也很关键,尤其是 handleTimerEnd 里先 stopTimer 再切状态,不然多个计时器叠着跑很容易出怪问题。 看完时间戳改进版,我有个感觉特别容易踩的点:startTimer 里用 totalDuration 反推 timeLeft,但切换状态时只改了 timeLeft,没有同步 totalDuration。这样工作结束进短休息后,下一轮还会拿工作的 25 分钟去算,短休息和长休息的倒计时就不对了。建议把当前阶段的时长统一成一个来源,比如根据 state 返回时长,初始化、切换、Progress 的 total 都用它,少写死几处。 另外 Progress 那里,长休息是 15 分钟,但 total 示例里除工作外都按 5 分钟算,长休息的环形进度会偏。用统一的当前阶段时长就能避开。 100 毫秒检查一次可以接受,不过每次回调都改 @State 可能让 UI 刷新太频繁,可以先算出新的 timeLeft,只有秒数变了再赋值,既保留时间戳校正,也减少无谓渲染。 后台恢复我也倾向把后台当成不可靠,回前台用时间戳重新算 elapsed 来校准;如果必须到点提醒,本地通知可能比一直让后台任务跑
回复 支持 反对

使用道具 举报

发表于 2026-9-22 19:20:00 | 显示全部楼层

Re: ArkUI番茄钟开发:setInterval误差与后台恢复

这个思路挺完整的,状态机加 @State 驱动 UI 很清晰。setInterval 那段我也有同感,单纯每秒 timeLeft-- 时间一长肯定会偏,改成记录 Date.now、按时间戳反推剩余时间会更稳,100ms 检查一次也比 1s 更从容,至少不会一次延迟就少跳好几秒。 handleTimerEnd 里先 stopTimer 再切状态、重置 timeLeft、最后 startTimer 这个顺序确实重要,不然旧 interval 没清掉很容易叠起来。还有 Progress 的 total 那里可以再细一点:现在只判断 WORK 是 25*60、否则 5*60,但长休息是 15*60,长休息阶段环形进度会不对,最好按 state 分别取 25、5、15。 后台恢复这块,如果已经用 startTime 做基准,切回前台时最好主动重算一次 elapsed,发现 timeLeft
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-7 03:06 , Processed in 0.027524 second(s), 17 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部