查看: 143|回复: 0

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

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
背景与目标

最近为了提高工作专注度,作者打算用鸿蒙 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 做状态恢复。原文还提到可以增加统计功能,记录每天完成了多少个番茄,这能进一步提升使用成就感。
回复

使用道具 举报

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

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

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

官方核心成员

关注微信公众号

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

GMT+8, 2026-9-22 15:22 , Processed in 0.018930 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部