React Native for OpenHarmony 实战:秒表实现

秒表和倒计时虽然都是计时工具,但使用场景完全不同。秒表用于测量时间间隔,比如跑步计时、烹饪计时等。今天我们用 React Native 实现一个带有计次功能和动态效果的秒表。
功能需求
这个秒表需要实现:
- 精确到毫秒的计时显示
- 开始、停止、重置功能
- 计次功能,记录多个时间点
- 运行时有动态视觉反馈
和倒计时不同,秒表是从 0 开始往上计数,而且需要更高的精度。
状态和动画值定义
import React, { useState, useRef, useEffect } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, ScrollView, Animated } from 'react-native';
export const Stopwatch: React.FC = () => {
const [time, setTime] = useState(0);
const [isRunning, setIsRunning] = useState(false);
const [laps, setLaps] = useState<number[]>([]);
const intervalRef = useRef<any>(null);
const pulseAnim = useRef(new Animated.Value(1)).current;
const glowAnim = useRef(new Animated.Value(0)).current;
const ringRotation = useRef(new Animated.Value(0)).current;
状态变量:
time存储当前计时的毫秒数isRunning标记是否正在计时laps存储计次记录的数组
动画值:
pulseAnim控制计时器的脉冲效果glowAnim控制发光效果ringRotation控制外圈的旋转
鸿蒙 ArkTS 对比:状态定义
@Entry
@Component
struct Stopwatch {
@State time: number = 0
@State isRunning: boolean = false
@State laps: number[] = []
@State pulseScale: number = 1
@State glowOpacity: number = 0
@State ringAngle: number = 0
private intervalId: number = -1
ArkTS 的状态定义更简洁,动画值直接用数字类型。在 React Native 中需要用 Animated.Value 包装,这是两个框架在动画实现上的主要区别。
动画效果控制
运行状态变化时启动或停止动画:
useEffect(() => {
if (isRunning) {
Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, { toValue: 1.05, duration: 500, useNativeDriver: true }),
Animated.timing(pulseAnim, { toValue: 1, duration: 500, useNativeDriver: true }),
])
).start();
Animated.timing(glowAnim, { toValue: 1, duration: 300, useNativeDriver: false }).start();
Animated.loop(
Animated.timing(ringRotation, { toValue: 1, duration: 3000, useNativeDriver: true })
).start();
} else {
pulseAnim.setValue(1);
Animated.timing(glowAnim, { toValue: 0, duration: 300, useNativeDriver: false }).start();
ringRotation.setValue(0);
}
}, [isRunning]);
运行时启动三个动画:
- 脉冲动画:计时器圆圈轻微放大缩小,1 秒一个周期
- 发光动画:绿色光晕渐显
- 旋转动画:外圈虚线环 3 秒转一圈
停止时重置所有动画值,发光效果渐隐。
鸿蒙 ArkTS 对比:动画控制
startAnimations() {
// 脉冲动画
animateTo({
duration: 500,
iterations: -1,
playMode: PlayMode.Alternate,
curve: Curve.EaseInOut
}, () => {
this.pulseScale = 1.05
})
// 旋转动画
animateTo({
duration: 3000,
iterations: -1,
curve: Curve.Linear
}, () => {
this.ringAngle = 360
})
}
stopAnimations() {
this.pulseScale = 1
this.ringAngle = 0
}
ArkTS 用 playMode: PlayMode.Alternate 实现往复动画,React Native 用 Animated.sequence 串联两个方向的动画。
核心计时逻辑
const start = () => {
setIsRunning(true);
intervalRef.current = setInterval(() => {
setTime(prev => prev + 10);
}, 10);
};
const stop = () => {
setIsRunning(false);
clearInterval(intervalRef.current);
};
const reset = () => {
stop();
setTime(0);
setLaps([]);
};
const lap = () => setLaps([time, ...laps]);
秒表的定时器间隔是 10 毫秒,每次增加 10。这样可以显示到百分之一秒的精度。
lap 函数把当前时间添加到数组开头,这样最新的记录会显示在最上面。用展开运算符 [time, ...laps] 创建新数组,保证 React 能检测到状态变化。
时间格式化
const formatTime = (ms: number) => {
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
const centiseconds = Math.floor((ms % 1000) / 10);
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
};
把毫秒数转换成 MM:SS.CC 格式:
- 分钟:总毫秒数除以 60000
- 秒:余数除以 1000
- 百分秒:余数除以 10
padStart(2, '0') 确保每个部分都是两位数字。
鸿蒙 ArkTS 对比:时间格式化
formatTime(ms: number): string {
let minutes = Math.floor(ms / 60000)
let seconds = Math.floor((ms % 60000) / 1000)
let centiseconds = Math.floor((ms % 1000) / 10)
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`
}
格式化逻辑完全一样,这是纯 JavaScript 代码,在任何平台都能运行。
动画插值
const spin = ringRotation.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
});
const glowColor = glowAnim.interpolate({
inputRange: [0, 1],
outputRange: ['rgba(0, 255, 136, 0)', 'rgba(0, 255, 136, 0.5)'],
});
interpolate 把动画值映射成实际的样式值。旋转角度从 0 到 360 度,发光颜色从透明到半透明绿色。
界面渲染:计时器区域
return (
<View style={styles.container}>
<View style={styles.timerContainer}>
<Animated.View style={[styles.outerRing, { transform: [{ rotate: spin }] }]}>
<View style={styles.ringDot} />
</Animated.View>
<Animated.View style={[styles.glowRing, { shadowColor: glowAnim.interpolate({
inputRange: [0, 1], outputRange: ['transparent', '#00ff88']
}), shadowOpacity: glowAnim }]}>
<Animated.View style={[styles.timerCircle, { transform: [{ scale: pulseAnim }] }]}>
<Text style={styles.timer}>{formatTime(time)}</Text>
<Text style={styles.msLabel}>毫秒精度</Text>
</Animated.View>
</Animated.View>
</View>
计时器区域有三层:
- 外圈虚线环,带一个小圆点,整体旋转
- 发光层,控制阴影颜色和透明度
- 内圈计时器,显示时间,带脉冲缩放
这种分层设计让每个动画效果独立控制,互不干扰。
控制按钮
<View style={styles.btnRow}>
{!isRunning ? (
<TouchableOpacity style={[styles.btn, styles.btnStart]} onPress={start}>
<View style={styles.btn3D}>
<Text style={styles.btnText}>▶ 开始</Text>
</View>
</TouchableOpacity>
) : (
<TouchableOpacity style={[styles.btn, styles.btnStop]} onPress={stop}>
<View style={styles.btn3D}>
<Text style={styles.btnText}>⏸ 停止</Text>
</View>
</TouchableOpacity>
)}
<TouchableOpacity style={[styles.btn, styles.btnLap]} onPress={isRunning ? lap : reset}>
<View style={styles.btn3D}>
<Text style={styles.btnText}>{isRunning ? '🏁 计次' : '↺ 重置'}</Text>
</View>
</TouchableOpacity>
</View>
左边按钮根据运行状态切换开始/停止,右边按钮在运行时是计次,停止时是重置。这种设计减少了按钮数量,界面更简洁。
鸿蒙 ArkTS 对比:条件渲染
Row() {
if (!this.isRunning) {
Button('▶ 开始')
.onClick(() => this.start())
} else {
Button('⏸ 停止')
.onClick(() => this.stop())
}
Button(this.isRunning ? '🏁 计次' : '↺ 重置')
.onClick(() => {
if (this.isRunning) {
this.lap()
} else {
this.reset()
}
})
}
ArkTS 在 build 方法中直接使用 if 语句进行条件渲染,React Native 用三元表达式。两种方式都能实现相同的效果。
计次记录列表
<ScrollView style={styles.laps} showsVerticalScrollIndicator={false}>
{laps.map((lapTime, i) => (
<Animated.View key={i} style={styles.lapItem}>
<View style={styles.lapBadge}>
<Text style={styles.lapNumber}>{laps.length - i}</Text>
</View>
<Text style={styles.lapTime}>{formatTime(lapTime)}</Text>
</Animated.View>
))}
</ScrollView>
</View>
);
};
计次记录用 ScrollView 包裹,支持滚动查看。每条记录显示序号和时间,序号用 laps.length - i 计算,让最新的记录显示为最大的数字。
showsVerticalScrollIndicator={false} 隐藏滚动条,界面更干净。
样式定义:容器和计时器
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#0d1117', padding: 20 },
timerContainer: { alignItems: 'center', justifyContent: 'center', height: 280, marginVertical: 20 },
outerRing: {
position: 'absolute',
width: 260,
height: 260,
borderRadius: 130,
borderWidth: 2,
borderColor: '#00ff88',
borderStyle: 'dashed',
},
ringDot: {
position: 'absolute',
top: -6,
left: '50%',
marginLeft: -6,
width: 12,
height: 12,
borderRadius: 6,
backgroundColor: '#00ff88',
},
glowRing: { shadowOffset: { width: 0, height: 0 }, shadowRadius: 30, elevation: 20 },
timerCircle: {
width: 220,
height: 220,
borderRadius: 110,
backgroundColor: '#161b22',
justifyContent: 'center',
alignItems: 'center',
borderWidth: 4,
borderColor: '#30363d',
},
timer: { fontSize: 48, color: '#fff', fontWeight: '200', fontFamily: 'monospace' },
msLabel: { color: '#8b949e', fontSize: 12, marginTop: 8 },
背景色用 GitHub 风格的深色 #0d1117。外圈用虚线边框 borderStyle: 'dashed',配合绿色 #00ff88 形成科技感。
计时器字体用 fontFamily: 'monospace',等宽字体让数字变化时不会跳动。
按钮样式
btnRow: { flexDirection: 'row', justifyContent: 'center', marginBottom: 20 },
btn: { marginHorizontal: 15 },
btn3D: {
width: 100,
height: 100,
borderRadius: 50,
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.5,
shadowRadius: 10,
elevation: 10,
},
btnStart: {},
btnStop: {},
btnLap: {},
btnText: { color: '#fff', fontSize: 14, fontWeight: '600' },
按钮用圆形设计,100x100 的尺寸方便点击。阴影让按钮有立体感。预留了 btnStart、btnStop、btnLap 样式,可以根据需要添加不同的背景色。
计次记录样式
laps: { flex: 1, backgroundColor: '#161b22', borderRadius: 16, padding: 16 },
lapItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: '#30363d',
},
lapBadge: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: '#238636',
justifyContent: 'center',
alignItems: 'center',
marginRight: 16,
},
lapNumber: { color: '#fff', fontSize: 14, fontWeight: '600' },
lapTime: { color: '#fff', fontSize: 20, fontFamily: 'monospace' },
});
计次列表用卡片样式,每条记录之间用细线分隔。序号用绿色圆形徽章显示,和整体的绿色主题呼应。时间同样用等宽字体,保持一致性。
性能考虑
秒表的定时器每 10 毫秒触发一次,这个频率比较高。在 React Native 中,每次 setTime 都会触发组件重新渲染。为了保证性能:
- 动画使用
useNativeDriver: true,在原生线程执行 - 计次列表用
ScrollView而不是FlatList,因为数据量通常不大 - 时间格式化函数是纯函数,没有副作用
在 OpenHarmony 上,这些优化同样有效。如果发现性能问题,可以考虑用 requestAnimationFrame 替代 setInterval,或者降低更新频率到 100 毫秒。
小结
这个秒表展示了 React Native 中高频定时器和多重动画的配合使用。通过分层设计,每个动画效果独立控制,代码结构清晰。计次功能用数组存储,配合 map 渲染列表,是 React 中常见的数据驱动模式。
在 OpenHarmony 平台上,10 毫秒的定时器精度可以正常工作,动画效果也能流畅运行。如果需要更高的精度,可以考虑使用原生模块来实现计时逻辑。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)