RN for OpenHarmony 小工具 App 实战:番茄钟实现
本文介绍了一个番茄钟小工具的实现,基于状态机设计模式,支持工作/短休/长休三种状态切换。核心功能包括倒计时运行与暂停、重置和自动模式转换,并通过进度水位、旋转刻度和呼吸脉冲动画增强专注氛围。技术实现上使用React Hooks管理状态,setInterval驱动计时,Animated API实现动效。代码组织清晰,通过duration和color表统一管理不同模式的参数,避免条件分支混乱。该组件已

这一篇我们实现一个「番茄钟」小工具:支持工作/短休/长休三种模式,倒计时运行与暂停、重置、自动在工作与休息之间切换,并在 UI 上用“进度水位 + 旋转刻度 + 呼吸脉冲”做出更强的专注氛围。
本文基于仓库真实代码:
src/pages/PomodoroTimer.tsxsrc/tools/index.tssrc/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);
}
逻辑解释:
- 完成一次 work:
sessions + 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
更多推荐


所有评论(0)