今天我们用 React Native 实现一个反应速度测试工具,测量用户从看到绿色到点击的反应时间。
状态设计
tsx
import React, { useState, useRef, useEffect } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Animated } from 'react-native';
export const ReactionTest: React.FC = () => {
const [state, setState] = useState<'idle' | 'waiting' | 'ready' | 'result' | 'early'>('idle');
const [reactionTime, setReactionTime] = useState(0);
const [results, setResults] = useState<number[]>([]);
const startTimeRef = useRef(0);
const timeoutRef = useRef<any>(null);
const scaleAnim = useRef(new Animated.Value(1)).current;
const pulseAnim = useRef(new Animated.Value(1)).current;
const resultAnim = useRef(new Animated.Value(0)).current;
状态设计包含测试状态、反应时间、历史记录。
测试状态 :state 有 5 种状态:
'idle':空闲,未开始'waiting':等待中,红色背景,等待变绿'ready':准备好,绿色背景,可以点击'result':显示结果,蓝色背景'early':点击太早,橙色背景,红色还没变绿就点了
反应时间 :reactionTime 是当前测试的反应时间,单位毫秒。
历史记录 :results 数组存储最近 5 次的反应时间。
两个 ref:
startTimeRef:记录绿色出现的时间戳timeoutRef:记录延迟定时器的 ID,用于清除定时器
三个动画值:
scaleAnim:整体内容的缩放动画,点击时缩小再弹回pulseAnim:绿色状态的脉冲动画,循环缩放resultAnim:结果显示的缩放动画
为什么用 ref 存储时间戳和定时器 ID ?因为这些值不需要触发重新渲染。如果用 useState,每次更新都会重新渲染,浪费性能。ref 存储的值变化不会触发渲染,适合存储这类"幕后"数据。
脉冲动画
tsx
useEffect(() => {
if (state === 'ready') {
Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, { toValue: 1.05, duration: 200, useNativeDriver: true }),
Animated.timing(pulseAnim, { toValue: 1, duration: 200, useNativeDriver: true }),
])
).start();
} else {
pulseAnim.setValue(1);
}
}, [state]);
绿色状态时启动脉冲动画,其他状态停止。
条件判断 :如果状态是 'ready'(绿色),启动循环动画;否则重置动画值为 1。
循环动画:序列动画,从 1 到 1.05(200ms),再从 1.05 到 1(200ms),总共 400ms 一个周期。图标循环缩放,吸引用户注意。
为什么只在绿色状态脉冲?因为绿色是"可以点击"的信号,脉冲动画强调这个信号。其他状态不需要脉冲,避免干扰。
为什么缩放到 1.05?因为 5% 的缩放很微妙,像"呼吸"。如果缩放太大(比如 1.2),会太夸张,分散注意力。
开始测试
tsx
const start = () => {
setState('waiting');
scaleAnim.setValue(1);
const delay = Math.random() * 3000 + 2000;
timeoutRef.current = setTimeout(() => {
setState('ready');
startTimeRef.current = Date.now();
}, delay);
};
开始函数设置等待状态,随机延迟后变绿。
设置等待状态 :setState('waiting') 切换到等待状态,背景变红。
重置动画 :scaleAnim.setValue(1) 重置缩放动画为 1,确保动画从初始状态开始。
随机延迟 :Math.random() * 3000 + 2000
Math.random() * 3000:生成 0-3000 的随机数+ 2000:加上 2000,得到 2000-5000 的随机数- 延迟 2-5 秒后变绿
为什么用随机延迟?因为固定延迟让用户能预测变绿的时间,提前准备,测不出真实反应速度。随机延迟让用户无法预测,必须真正"看到绿色"才能反应。
为什么是 2-5 秒?因为 2 秒是最短等待时间,不会太快让用户措手不及;5 秒是最长等待时间,不会太久让用户不耐烦。
定时器回调:
setState('ready'):切换到准备状态,背景变绿startTimeRef.current = Date.now():记录变绿的时间戳
处理点击
tsx
const handlePress = () => {
Animated.sequence([
Animated.timing(scaleAnim, { toValue: 0.95, duration: 50, useNativeDriver: true }),
Animated.spring(scaleAnim, { toValue: 1, friction: 3, useNativeDriver: true }),
]).start();
if (state === 'idle' || state === 'result' || state === 'early') {
start();
} else if (state === 'waiting') {
clearTimeout(timeoutRef.current);
setState('early');
} else if (state === 'ready') {
const time = Date.now() - startTimeRef.current;
setReactionTime(time);
setResults([time, ...results.slice(0, 4)]);
setState('result');
resultAnim.setValue(0);
Animated.spring(resultAnim, { toValue: 1, friction: 4, useNativeDriver: true }).start();
}
};
点击处理函数根据当前状态执行不同操作。
点击动画:序列动画,内容缩小到 95%(50ms),再弹回到 100%。给用户即时反馈。
状态判断:
空闲、结果、太早状态 :调用 start() 开始新测试。这三种状态都是"可以开始"的状态。
等待状态:
clearTimeout(timeoutRef.current):清除定时器,停止变绿setState('early'):切换到太早状态,背景变橙色- 用户在红色时点击,说明点击太早
准备状态:
Date.now() - startTimeRef.current:当前时间减去变绿时间,得到反应时间setReactionTime(time):保存反应时间setResults([time, ...results.slice(0, 4)]):把新结果插入数组开头,保留前 4 个旧结果,总共 5 个setState('result'):切换到结果状态- 启动结果动画,从 0 缩放到 1
为什么保留 5 个历史记录?因为 5 个刚好,既能显示足够的统计信息(计算平均值),又不会占用太多内存。如果保留太多(比如 100 个),数组会很长;如果太少(比如 2 个),平均值不够准确。
统计计算
tsx
const getAverage = () => {
if (results.length === 0) return 0;
return Math.round(results.reduce((a, b) => a + b, 0) / results.length);
};
计算平均反应时间。
检查空数组:如果没有历史记录,返回 0。
计算平均值:
results.reduce((a, b) => a + b, 0):累加所有反应时间/ results.length:除以数量,得到平均值Math.round(...):四舍五入取整
颜色和消息
tsx
const getColor = () => {
switch (state) {
case 'waiting': return '#e74c3c';
case 'ready': return '#2ecc71';
case 'early': return '#f39c12';
default: return '#4A90D9';
}
};
const getMessage = () => {
switch (state) {
case 'idle': return '点击开始';
case 'waiting': return '等待绿色...';
case 'ready': return '点击!';
case 'early': return '太早了!';
case 'result': return `${reactionTime} ms`;
}
};
const getEmoji = () => {
if (state !== 'result') return '';
if (reactionTime < 200) return '🚀';
if (reactionTime < 300) return '👍';
if (reactionTime < 400) return '😊';
return '🐢';
};
三个函数根据状态返回背景色、消息、emoji。
背景色:
- 等待:红色(#e74c3c),表示"不能点"
- 准备:绿色(#2ecc71),表示"可以点"
- 太早:橙色(#f39c12),表示"警告"
- 其他:蓝色(#4A90D9),默认色
为什么用红绿色?因为红绿是交通信号灯的颜色,全世界通用。红色表示"停",绿色表示"行"。用户看到颜色就知道能不能点击,不需要思考。
消息文字:
- 空闲:提示"点击开始"
- 等待:提示"等待绿色..."
- 准备:提示"点击!"
- 太早:提示"太早了!"
- 结果:显示反应时间"X ms"
结果 emoji:根据反应时间显示不同的 emoji:
- 小于 200ms:🚀 火箭,超快
- 小于 300ms:👍 点赞,不错
- 小于 400ms:😊 笑脸,一般
- 大于等于 400ms:🐢 乌龟,有点慢
为什么用这些阈值?因为人类的平均反应时间是 200-300ms。小于 200ms 是非常快的,小于 300ms 是正常的,小于 400ms 是稍慢的,大于 400ms 是明显慢的。
界面渲染
tsx
return (
<TouchableOpacity
style={[styles.container, { backgroundColor: getColor() }]}
onPress={handlePress}
activeOpacity={1}
>
<Animated.View style={[styles.content, { transform: [{ scale: scaleAnim }] }]}>
<Animated.View style={{ transform: [{ scale: state === 'ready' ? pulseAnim : 1 }] }}>
<Text style={styles.emoji}>
{state === 'idle' && '👆'}
{state === 'waiting' && '⏳'}
{state === 'ready' && '🎯'}
{state === 'early' && '⚠️'}
{state === 'result' && getEmoji()}
</Text>
</Animated.View>
<Animated.Text style={[styles.message, state === 'result' && {
transform: [{ scale: resultAnim }],
}]}>
{getMessage()}
</Animated.Text>
{state === 'result' && (
<Text style={styles.hint}>
{reactionTime < 200 ? '超快!' : reactionTime < 300 ? '不错!' : reactionTime < 400 ? '一般' : '有点慢'}
</Text>
)}
{state === 'early' && <Text style={styles.hint}>点击重试</Text>}
</Animated.View>
整个屏幕是一个可点击区域,背景色根据状态变化。
容器:
TouchableOpacity让整个屏幕可点击backgroundColor: getColor():背景色根据状态变化activeOpacity={1}:点击时不改变透明度,因为已经有缩放动画
内容区域:应用缩放动画,点击时缩小再弹回。
图标:
- 应用脉冲动画(只在准备状态)
- 根据状态显示不同的 emoji
消息文字:
- 显示当前状态的消息
- 结果状态时应用缩放动画
提示文字:
- 结果状态:根据反应时间显示评价
- 太早状态:提示"点击重试"
统计信息
tsx
{results.length > 0 && (
<View style={styles.stats}>
<View style={styles.statItem}>
<Text style={styles.statLabel}>平均</Text>
<Text style={styles.statValue}>{getAverage()} ms</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statLabel}>最近</Text>
<Text style={styles.statValue}>{results[0]} ms</Text>
</View>
</View>
)}
</TouchableOpacity>
);
};
统计信息只在有历史记录时显示,固定在底部。
两个统计项:
- 平均反应时间:所有记录的平均值
- 最近反应时间:最新一次的结果
为什么显示平均和最近?因为平均值反映整体水平,最近值反映当前状态。用户可以看到自己的平均水平,也可以看到刚才的表现。
鸿蒙 ArkTS 对比:测试逻辑
typescript
@State state: string = 'idle'
@State reactionTime: number = 0
@State results: number[] = []
private startTime: number = 0
private timeoutId: number = -1
start() {
this.state = 'waiting'
const delay = Math.random() * 3000 + 2000
this.timeoutId = setTimeout(() => {
this.state = 'ready'
this.startTime = Date.now()
}, delay)
}
handlePress() {
if (this.state === 'idle' || this.state === 'result' || this.state === 'early') {
this.start()
} else if (this.state === 'waiting') {
clearTimeout(this.timeoutId)
this.state = 'early'
} else if (this.state === 'ready') {
const time = Date.now() - this.startTime
this.reactionTime = time
this.results = [time, ...this.results.slice(0, 4)]
this.state = 'result'
}
}
getAverage(): number {
if (this.results.length === 0) return 0
return Math.round(this.results.reduce((a, b) => a + b, 0) / this.results.length)
}
ArkTS 中的测试逻辑完全一样,核心是状态机、随机延迟、时间戳计算。Date.now()、setTimeout、Math.random() 都是标准 JavaScript API,跨平台通用。
样式定义
tsx
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
content: { alignItems: 'center' },
emoji: { fontSize: 80, marginBottom: 20 },
message: { fontSize: 56, color: '#fff', fontWeight: '700' },
hint: { fontSize: 24, color: '#fff', marginTop: 20, opacity: 0.9 },
stats: {
position: 'absolute',
bottom: 60,
flexDirection: 'row',
backgroundColor: 'rgba(0,0,0,0.3)',
borderRadius: 20,
padding: 20,
},
statItem: { alignItems: 'center', marginHorizontal: 20 },
statLabel: { color: '#fff', fontSize: 14, opacity: 0.8 },
statValue: { color: '#fff', fontSize: 24, fontWeight: '700', marginTop: 4 },
});
容器占满屏幕,居中对齐。图标字号 80,很大,醒目。消息字号 56,白色粗体。统计信息固定在底部,半透明黑色背景,白色文字。
小结
这个反应测试工具展示了状态机和时间戳的应用。用 5 种状态管理测试流程,用随机延迟防止预测,用时间戳精确测量反应时间。红绿背景色是直观的视觉信号,emoji 和评价让结果更有趣。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
