在这里插入图片描述

这一篇我们实现一个「番茄钟」小工具:支持工作/短休/长休三种模式,倒计时运行与暂停、重置、自动在工作与休息之间切换,并在 UI 上用“进度水位 + 旋转刻度 + 呼吸脉冲”做出更强的专注氛围。

本文基于仓库真实代码:

  • src/pages/PomodoroTimer.tsx
  • src/tools/index.ts
  • src/screens/ToolScreen.tsx

先讲清楚:这其实是一个小型状态机

番茄钟最容易写成“if else 乱飞”,但本质上它是一个状态机:

  • mode:当前模式(工作/短休/长休)
  • timeLeft:剩余秒数
  • isRunning:是否运行
  • sessions:完成的工作番茄数量(用于决定第 4 次切长休)

对应代码(文件:src/pages/PomodoroTimer.tsx):

const [mode, setMode] = useState<'work' | 'break' | 'longBreak'>('work');
const [timeLeft, setTimeLeft] = useState(25 * 60);
const [isRunning, setIsRunning] = useState(false);
const [sessions, setSessions] = useState(0);

为了让不同模式统一管理时长/配色,代码里用两个表:

const durations = { work: 25 * 60, break: 5 * 60, longBreak: 15 * 60 };
const colors = { work: '#ff6b6b', break: '#4ecdc4', longBreak: '#a29bfe' };

这会让后续:

  • 重置:durations[mode]
  • 切换模式:colors[mode]

变得非常顺滑。

工具箱接入:id=46 映射到 PomodoroTimer

1) 工具列表注册

文件:src/tools/index.ts

{ id: 46, name: '番茄钟', description: '番茄工作法计时器', icon: '🍅', component: 'PomodoroTimer' },

2) ToolScreen 里映射到页面组件

文件:src/screens/ToolScreen.tsx

PomodoroTimer: Pages.PomodoroTimer,

核心计时循环:用 setInterval 驱动 timeLeft

这个组件最“关键也最危险”的部分是计时循环。代码使用 intervalRef 保存定时器引用,避免重复创建。

const intervalRef = useRef<any>(null);

useEffect(() => {
  if (isRunning && timeLeft > 0) {
    intervalRef.current = setInterval(() => {
      setTimeLeft(t => t - 1);
    }, 1000);
  } else if (timeLeft === 0) {
    // ...切换模式逻辑
    setIsRunning(false);
  }
  return () => clearInterval(intervalRef.current);
}, [isRunning, timeLeft]);

这里有 3 个设计点:

  • 递减用函数式更新setTimeLeft(t => t - 1),避免闭包捕获旧值
  • timeLeft 归零在同一个 effect 里处理:倒计时结束立即切模式
  • 清理定时器return () => clearInterval(...),保证暂停/切页时不会“幽灵计时”

模式流转:工作结束后决定短休还是长休

倒计时归零时,会根据 mode 判断下一步:

if (timeLeft === 0) {
  if (mode === 'work') {
    const newSessions = sessions + 1;
    setSessions(newSessions);
    if (newSessions % 4 === 0) {
      setMode('longBreak');
      setTimeLeft(durations.longBreak);
    } else {
      setMode('break');
      setTimeLeft(durations.break);
    }
  } else {
    setMode('work');
    setTimeLeft(durations.work);
  }
  setIsRunning(false);
}

逻辑解释:

  • 完成一次 worksessions + 1
  • 每完成 4 个番茄:进入 longBreak(长休)
  • 其他情况:进入 break(短休)
  • 休息结束后统一回到 work

同时把 isRunning 置为 false,让用户确认后再开始下一段,避免突然自动跳转造成干扰。

用户操作:开始/暂停、重置、手动切模式

1) 开始/暂停

const toggleTimer = () => setIsRunning(!isRunning);

2) 重置(回到当前模式的初始时长)

const resetTimer = () => {
  setIsRunning(false);
  setTimeLeft(durations[mode]);
};

3) 切模式(并停止计时)

const switchMode = (newMode: typeof mode) => {
  setMode(newMode);
  setTimeLeft(durations[newMode]);
  setIsRunning(false);
};

这种设计的体验是“可预期”的:

  • 你切到短休/长休,时间会立即刷新
  • 同时暂停,避免用户误触导致计时在后台跑

时间展示:把秒变成 mm:ss

const formatTime = (seconds: number) => {
  const m = Math.floor(seconds / 60);
  const s = seconds % 60;
  return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
};

padStart(2, '0') 能保证显示为 05:09 这种固定宽度格式。

进度表达:用“水位高度”表达完成度

这里没有使用 SVG 圆环,而是用一个圆形容器 + overflow: 'hidden',通过填充高度模拟进度。

const progress = 1 - timeLeft / durations[mode];

<View style={styles.progressRing}>
  <View style={[styles.progressFill, { height: `${progress * 100}%` }]} />
</View>

对应样式:

progressRing: {
  position: 'absolute',
  width: 220,
  height: 220,
  borderRadius: 110,
  backgroundColor: 'rgba(0,0,0,0.2)',
  overflow: 'hidden',
  justifyContent: 'flex-end',
},
progressFill: { backgroundColor: 'rgba(255,255,255,0.3)', width: '100%' },

要点:

  • justifyContent: 'flex-end' 让填充从底部向上“涨水位”
  • overflow: 'hidden' 保证水位不会溢出圆形区域

动效:呼吸脉冲 + 旋转刻度

呼吸脉冲(运行时才开启)

useEffect(() => {
  if (isRunning) {
    Animated.loop(
      Animated.sequence([
        Animated.timing(pulseAnim, { toValue: 1.05, duration: 1000, useNativeDriver: true }),
        Animated.timing(pulseAnim, { toValue: 1, duration: 1000, useNativeDriver: true }),
      ])
    ).start();
  } else {
    pulseAnim.setValue(1);
  }
}, [isRunning]);

并应用到圆心:

<Animated.View style={[styles.timerCircle, { transform: [{ scale: pulseAnim }] }]}>
  <Text style={styles.timer}>{formatTime(timeLeft)}</Text>
  <Text style={styles.modeLabel}>...</Text>
</Animated.View>

旋转刻度(营造“时钟”感觉)

Animated.loop(
  Animated.timing(rotateAnim, { toValue: 1, duration: 60000, easing: Easing.linear, useNativeDriver: true })
).start();

const rotate = rotateAnim.interpolate({
  inputRange: [0, 1],
  outputRange: ['0deg', '360deg'],
});

刻度环渲染:

<Animated.View style={[styles.outerRing, { transform: [{ rotate }] }]}>
  {Array(12).fill(0).map((_, i) => (
    <View
      key={i}
      style={[
        styles.ringMark,
        { transform: [{ rotate: `${i * 30}deg` }, { translateY: -120 }] },
      ]}
    />
  ))}
</Animated.View>

这里用 12 个刻度(每 30° 一个),再通过 translateY: -120 把刻度“推到圆周上”。整体环再做旋转,就很像一个在走动的钟表。

UI 模式切换:三个 Tab + 不同背景色

<View style={[styles.container, { backgroundColor: colors[mode] }]}>
  <View style={styles.modes}>
    {(['work', 'break', 'longBreak'] as const).map(m => (
      <TouchableOpacity
        key={m}
        style={[styles.modeBtn, mode === m && styles.modeBtnActive]}
        onPress={() => switchMode(m)}
      >
        <Text style={[styles.modeText, mode === m && styles.modeTextActive]}>
          {m === 'work' ? '🍅 工作' : m === 'break' ? '☕ 休息' : '🌴 长休'}
        </Text>
      </TouchableOpacity>
    ))}
  </View>
  {/* ... */}
</View>

用颜色区分模式(红/绿/紫)能显著降低“我现在是在工作还是休息”的认知成本。

番茄统计:数字 + emoji 队列

<Text style={styles.statValue}>{sessions}</Text>

{Array(Math.min(sessions, 8)).fill(0).map((_, i) => (
  <Text key={i} style={styles.tomato}>🍅</Text>
))}
{sessions > 8 && <Text style={styles.tomatoMore}>+{sessions - 8}</Text>}

这是一种“低成本但高反馈”的统计展示:

  • sessions 提供精确数值
  • 🍅🍅🍅... 提供直观的成就感

小结

这个番茄钟的实现可以归纳为三层:

  • 状态层mode / timeLeft / isRunning / sessions 组成最小状态机
  • 驱动层setInterval 每秒递减,归零触发模式切换
  • 表现层:水位进度、刻度旋转、脉冲呼吸、模式配色

整体代码量不大,但把“计时逻辑”和“视觉反馈”分开组织后,维护起来会很稳定。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐