import React, { useState, useRef, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
PanResponder,
TouchableOpacity,
Animated,
StatusBar,
Dimensions,
NativeSyntheticEvent,
GestureResponderEvent
} from 'react-native';
// 手势类型定义
type Gesture = 'tap' | 'doubleTap' | 'longPress' | 'swipe' | 'pinch';
// 滑动方向
type SwipeDir = '' | 'Up' | 'Down' | 'Left' | 'Right';
// 手势状态接口
interface GestureState {
active: boolean;
value?: any;
}
// 验证状态
type ValidationStatus = 'pending' | 'success' | 'failure';
// 手势组合顺序
const REQUIRED_GESTURES: Gesture[] = ['longPress', 'doubleTap', 'swipe', 'pinch'];
// 长按优化配置(降低识别门槛)
const GESTURE_CONFIG = {
longPressThreshold: 900, // 长按时间延长至800ms(更容易触发)
longPressMoveTolerance: 10, // 长按允许的最大移动距离(10px内视为静止)
doubleTapTimeWindow: 600, // 双击时间窗口
doubleTapDistanceThreshold: 70,
tapDelay: 400, // 单击延迟(降低灵敏度)
swipeThreshold: 15, // 滑动触发阈值
swipeDirThreshold: 25,
};
const CompleteGestureValidator: React.FC = () => {
// 基础配置
const { width } = Dimensions.get('window');
const statusBarHeight = StatusBar.currentHeight || 0;
// 手势状态管理
const [gestures, setGestures] = useState<Record<Gesture, GestureState>>({
tap: { active: false },
doubleTap: { active: false },
longPress: { active: false },
swipe: { active: false, value: { direction: '', distance: 0 } },
pinch: { active: false, value: { scale: 1 } }
});
// 验证状态管理
const [currentStep, setCurrentStep] = useState(0);
const [validationStatus, setValidationStatus] = useState<ValidationStatus>('pending');
const [message, setMessage] = useState("请按顺序完成手势: 长按 → 双击 → 滑动 → 捏合");
// 动画效果
const bgAnim = useRef(new Animated.Value(0)).current;
const scaleAnim = useRef(new Animated.Value(1)).current;
// 手势识别引用(新增长按相关状态)
const panResponder = useRef<PanResponder.PanResponderInstance>(PanResponder.create({}));
const pressTimer = useRef<NodeJS.Timeout | null>(null);
const tapTimer = useRef<NodeJS.Timeout | null>(null);
const lastTap = useRef(0);
const tapCount = useRef(0);
const lastTapPosition = useRef<{x: number, y: number} | null>(null);
const pressStartPosition = useRef<{x: number, y: number} | null>(null); // 记录长按开始位置
const isSwiping = useRef(false);
const isPinching = useRef(false);
const isLongPressing = useRef(false); // 标记长按状态
const hasSwiped = useRef(false);
const initialDist = useRef(0);
const initialScale = useRef(1);
// 安全获取触摸点
const getTouches = (evt: NativeSyntheticEvent<GestureResponderEvent>) => {
const touches = evt.nativeEvent.touches;
const result: Array<{pageX: number, pageY: number}> = [];
if (!touches) return result;
try {
if (Array.isArray(touches)) {
touches.forEach(t => result.push({pageX: t.pageX, pageY: t.pageY}));
} else if (touches.length !== undefined) {
for (let i = 0; i < touches.length; i++) {
if (typeof touches.item === 'function') {
const t = touches.item(i);
result.push({pageX: t.pageX, pageY: t.pageY});
} else {
// @ts-ignore
const t = touches[i];
if (t && t.pageX !== undefined) {
result.push({pageX: t.pageX, pageY: t.pageY});
}
}
}
}
} catch (e) {
console.log('触摸点获取失败:', e);
}
return result;
};
// 计算两点距离
const getDistance = (touches: Array<{pageX: number, pageY: number}>) => {
if (touches.length < 2) return 0;
const dx = touches[0].pageX - touches[1].pageX;
const dy = touches[0].pageY - touches[1].pageY;
return Math.sqrt(dx * dx + dy * dy);
};
// 计算两点间直线距离
const getPointDistance = (p1: {x: number, y: number}, p2: {x: number, y: number}) => {
const dx = p1.x - p2.x;
const dy = p1.y - p2.y;
return Math.sqrt(dx * dx + dy * dy);
};
// 确定滑动方向
const getSwipeDir = (dx: number, dy: number): SwipeDir => {
const threshold = GESTURE_CONFIG.swipeDirThreshold;
const absDx = Math.abs(dx);
const absDy = Math.abs(dy);
if (Math.max(absDx, absDy) < threshold) return '';
return absDx > absDy
? dx > 0 ? 'Right' : 'Left'
: dy > 0 ? 'Down' : 'Up';
};
// 获取手势名称
const getGestureName = (gesture: Gesture): string => {
const names: Record<Gesture, string> = {
tap: "单击",
doubleTap: "双击",
longPress: "长按",
swipe: "滑动",
pinch: "捏合"
};
return names[gesture];
};
// 激活手势
const activateGesture = (type: Gesture, value?: any) => {
if (type === 'longPress') {
isLongPressing.current = true; // 标记长按激活
}
// 初始化所有手势为未激活状态
const newGestures: Record<Gesture, GestureState> = {
tap: { active: false },
doubleTap: { active: false },
longPress: { active: false },
swipe: { active: false, value: { direction: '', distance: 0 } },
pinch: { active: false, value: { scale: 1 } }
};
newGestures[type] = { active: true, value };
setGestures(newGestures);
// 视觉反馈
Animated.sequence([
Animated.timing(bgAnim, { toValue: 1, duration: 150, useNativeDriver: false }),
Animated.timing(bgAnim, { toValue: 0, duration: 300, useNativeDriver: false })
]).start();
Animated.sequence([
Animated.timing(scaleAnim, { toValue: 1.05, duration: 150, useNativeDriver: true }),
Animated.timing(scaleAnim, { toValue: 1, duration: 150, useNativeDriver: true })
]).start();
// 检查手势是否符合当前步骤要求
const expectedGesture = REQUIRED_GESTURES[currentStep];
if (type === expectedGesture) {
const nextStep = currentStep + 1;
setCurrentStep(nextStep);
if (nextStep < REQUIRED_GESTURES.length) {
setMessage(`成功!下一步: ${getGestureName(REQUIRED_GESTURES[nextStep])}`);
} else {
setValidationStatus('success');
setMessage("恭喜!所有手势验证通过!");
}
} else {
// 识别失败:自动重置
setValidationStatus('failure');
setMessage("手势识别失败");
setTimeout(resetAll, 1500);
}
// 自动重置当前手势状态
setTimeout(() => {
setGestures(prev => ({
...prev,
[type]: type === 'swipe'
? { active: false, value: { direction: '', distance: 0 } }
: type === 'pinch'
? { active: false, value: { scale: 1 } }
: { active: false }
}));
if (type === 'longPress') {
isLongPressing.current = false;
}
}, 1500);
};
// 处理点击
const handleTap = (evt: GestureResponderEvent) => {
// 长按状态下不处理单击/双击
if (isLongPressing.current || isSwiping.current || isPinching.current ||
hasSwiped.current || validationStatus !== 'pending') {
resetTapState();
return;
}
const currentPosition = {
x: evt.nativeEvent.pageX,
y: evt.nativeEvent.pageY
};
const now = Date.now();
const timeSinceLast = now - lastTap.current;
if (tapTimer.current) clearTimeout(tapTimer.current);
// 双击检测
if (
timeSinceLast < GESTURE_CONFIG.doubleTapTimeWindow &&
tapCount.current === 1 &&
lastTapPosition.current &&
getPointDistance(lastTapPosition.current, currentPosition) < GESTURE_CONFIG.doubleTapDistanceThreshold
) {
activateGesture('doubleTap');
resetTapState();
return;
}
// 单击检测
tapCount.current = 1;
lastTap.current = now;
lastTapPosition.current = currentPosition;
tapTimer.current = setTimeout(() => {
activateGesture('tap');
resetTapState();
}, GESTURE_CONFIG.tapDelay);
};
// 重置点击相关状态
const resetTapState = () => {
tapCount.current = 0;
lastTap.current = 0;
lastTapPosition.current = null;
if (tapTimer.current) {
clearTimeout(tapTimer.current);
tapTimer.current = null;
}
};
// 重置所有状态
const resetAll = () => {
setGestures({
tap: { active: false },
doubleTap: { active: false },
longPress: { active: false },
swipe: { active: false, value: { direction: '', distance: 0 } },
pinch: { active: false, value: { scale: 1 } }
});
setCurrentStep(0);
setValidationStatus('pending');
setMessage("请按顺序完成手势: 长按 → 双击 → 滑动 → 捏合");
// 清除所有定时器
if (pressTimer.current) clearTimeout(pressTimer.current);
resetTapState();
// 重置所有状态标记
hasSwiped.current = false;
isSwiping.current = false;
isPinching.current = false;
isLongPressing.current = false;
pressStartPosition.current = null;
};
// 初始化手势识别(重点优化长按逻辑)
useEffect(() => {
panResponder.current = PanResponder.create({
onStartShouldSetPanResponder: () => validationStatus === 'pending',
// 提高滑动触发门槛,轻微移动不视为滑动
onMoveShouldSetPanResponder: (_, gs) => {
return validationStatus === 'pending' &&
(Math.abs(gs.dx) > GESTURE_CONFIG.longPressMoveTolerance ||
Math.abs(gs.dy) > GESTURE_CONFIG.longPressMoveTolerance);
},
// 触摸开始:记录长按起始位置
onPanResponderStart: (evt) => {
if (validationStatus !== 'pending') return;
const touches = getTouches(evt);
hasSwiped.current = false;
isLongPressing.current = false;
resetTapState();
// 记录长按起始位置(单指操作时)
if (touches.length === 1) {
pressStartPosition.current = {
x: touches[0].pageX,
y: touches[0].pageY
};
}
// 长按检测(延长至800ms)
pressTimer.current = setTimeout(() => {
// 长按触发条件:单指、未滑动、未捏合、移动在容忍范围内
if (!isSwiping.current && !isPinching.current &&
touches.length === 1 && pressStartPosition.current) {
activateGesture('longPress');
}
}, GESTURE_CONFIG.longPressThreshold);
// 捏合初始化
if (touches.length === 2) {
isPinching.current = true;
initialDist.current = getDistance(touches);
initialScale.current = gestures.pinch.value?.scale || 1;
if (pressTimer.current) {
clearTimeout(pressTimer.current);
pressTimer.current = null;
}
}
},
// 触摸移动:优化长按取消逻辑
onPanResponderMove: (evt, gs) => {
if (validationStatus !== 'pending') return;
const touches = getTouches(evt);
// 处理捏合
if (touches.length === 2) {
isPinching.current = true;
isSwiping.current = false;
isLongPressing.current = false;
if (pressTimer.current) {
clearTimeout(pressTimer.current);
pressTimer.current = null;
}
const currentDist = getDistance(touches);
if (initialDist.current > 0 && currentDist > 0) {
const scale = (currentDist / initialDist.current) * initialScale.current;
const clamped = Math.max(0.5, Math.min(2, scale));
activateGesture('pinch', { scale: clamped });
}
}
// 处理滑动(只有明显滑动才取消长按)
else if (touches.length === 1) {
const { dx, dy } = gs;
const dist = Math.sqrt(dx * dx + dy * dy);
// 检查是否超过长按允许的移动距离
if (pressStartPosition.current &&
dist > GESTURE_CONFIG.longPressMoveTolerance) {
isSwiping.current = true;
}
// 只有超过滑动阈值才触发滑动并取消长按
if (dist > GESTURE_CONFIG.swipeThreshold) {
hasSwiped.current = true;
isLongPressing.current = false;
if (pressTimer.current) {
clearTimeout(pressTimer.current);
pressTimer.current = null;
}
const dir = getSwipeDir(dx, dy);
if (dir) {
activateGesture('swipe', { direction: dir, distance: Math.round(dist) });
}
}
}
},
// 触摸结束:清理状态
onPanResponderRelease: () => {
if (pressTimer.current) {
clearTimeout(pressTimer.current);
pressTimer.current = null;
}
// 延迟重置状态,避免快速操作冲突
setTimeout(() => {
isSwiping.current = false;
isPinching.current = false;
isLongPressing.current = false;
}, 200);
},
// 触摸中断:强制重置
onPanResponderTerminate: () => {
if (validationStatus === 'pending') {
resetAll();
}
}
});
}, [gestures.pinch.value?.scale, validationStatus]);
// 动画颜色
const bgColor = bgAnim.interpolate({
inputRange: [0, 1],
outputRange: ['#fff', validationStatus === 'failure' ? '#fff0f0' : '#e6f7ff']
});
const borderColor = bgAnim.interpolate({
inputRange: [0, 1],
outputRange: [
validationStatus === 'failure' ? '#ff4d4f' :
validationStatus === 'success' ? '#52c41a' : '#ccc',
validationStatus === 'failure' ? '#ff4d4f' :
validationStatus === 'success' ? '#52c41a' : '#1890ff'
]
});
// 进度条颜色
const progressColor = validationStatus === 'failure' ? '#ff4d4f' :
validationStatus === 'success' ? '#52c41a' : '#1890ff';
return (
<View style={[styles.container, { paddingTop: statusBarHeight }]}>
<View style={styles.header}>
<Text style={styles.title}>手势组合验证</Text>
<Text style={styles.subtitle}>按指定顺序完成所有手势以通过验证</Text>
</View>
{/* 进度指示器 */}
<View style={styles.progressContainer}>
<View style={styles.progressBarBackground}>
<View
style={[
styles.progressBarFill,
{
width: `${(currentStep / REQUIRED_GESTURES.length) * 100}%`,
backgroundColor: progressColor
}
]}
/>
</View>
<View style={styles.progressSteps}>
{REQUIRED_GESTURES.map((gesture, index) => (
<View
key={gesture}
style={[
styles.progressStep,
{
backgroundColor: index < currentStep
? progressColor
: index === currentStep && validationStatus === 'failure'
? '#ff4d4f'
: '#ccc',
borderColor: index < currentStep ? progressColor : '#ccc'
}
]}
>
<Text style={styles.stepNumber}>{index + 1}</Text>
</View>
))}
</View>
</View>
{/* 提示信息 */}
<View style={[
styles.messageBox,
validationStatus === 'failure' ? styles.failureMessageBox :
validationStatus === 'success' ? styles.successMessageBox : styles.defaultMessageBox
]}>
<Text style={styles.messageText}>{message}</Text>
</View>
{/* 手势操作区 */}
<Animated.View
style={[
styles.area,
{
backgroundColor: bgColor,
borderColor,
transform: [{ scale: scaleAnim }],
opacity: validationStatus !== 'pending' ? 0.7 : 1
}
]}
onTouchEnd={handleTap}
{...panResponder.current.panHandlers}
>
<Text style={styles.areaText}>操作区域</Text>
<Text style={styles.areaHint}>
{validationStatus === 'pending'
? `当前需要: ${getGestureName(REQUIRED_GESTURES[currentStep])}`
: validationStatus === 'success' ? '验证成功!' : '验证失败!'
}
</Text>
{/* 缩放演示元素 */}
<Animated.View
style={[
styles.scaleBox,
{ transform: [{ scale: gestures.pinch.active ? gestures.pinch.value?.scale : 1 }] }
]}
>
<Text style={styles.scaleText}>缩放我</Text>
</Animated.View>
</Animated.View>
{/* 结果展示 */}
<View style={styles.resultBox}>
<Text style={styles.resultTitle}>当前识别: {
Object.entries(gestures).find(([_, state]) => state.active)
? getGestureName((Object.entries(gestures).find(([_, state]) => state.active) as [Gesture, GestureState])[0])
: '无'
}</Text>
{validationStatus === 'success' && (
<TouchableOpacity style={styles.resetBtn} onPress={resetAll}>
<Text style={styles.resetText}>重新开始</Text>
</TouchableOpacity>
)}
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
header: {
padding: 16,
backgroundColor: '#1890ff',
alignItems: 'center',
},
title: {
color: '#fff',
fontSize: 20,
fontWeight: 'bold',
marginBottom: 4,
},
subtitle: {
color: 'rgba(255,255,255,0.8)',
fontSize: 14,
},
progressContainer: {
paddingHorizontal: 20,
marginBottom: 10,
},
progressBarBackground: {
height: 6,
backgroundColor: '#e8e8e8',
borderRadius: 3,
overflow: 'hidden',
},
progressBarFill: {
height: '100%',
borderRadius: 3,
transitionProperty: 'width',
transitionDuration: '300ms',
},
progressSteps: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 8,
paddingHorizontal: 10,
},
progressStep: {
width: 24,
height: 24,
borderRadius: 12,
borderWidth: 2,
justifyContent: 'center',
alignItems: 'center',
},
stepNumber: {
color: '#fff',
fontSize: 12,
fontWeight: 'bold',
},
messageBox: {
marginHorizontal: 20,
padding: 12,
borderRadius: 6,
marginBottom: 15,
},
defaultMessageBox: {
backgroundColor: '#e6f7ff',
},
successMessageBox: {
backgroundColor: '#f6ffed',
},
failureMessageBox: {
backgroundColor: '#fff2f0',
},
messageText: {
fontSize: 15,
textAlign: 'center',
},
area: {
flex: 1,
margin: 20,
borderRadius: 12,
borderWidth: 2,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
backgroundColor: '#fff',
},
areaText: {
fontSize: 20,
color: '#333',
fontWeight: '600',
marginBottom: 10,
},
areaHint: {
fontSize: 15,
color: '#666',
marginBottom: 30,
},
scaleBox: {
width: 120,
height: 120,
backgroundColor: '#722ed1',
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
},
scaleText: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
},
resultBox: {
margin: 20,
padding: 16,
backgroundColor: '#fff',
borderRadius: 8,
borderWidth: 1,
borderColor: '#eee',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
resultTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#333',
},
resetBtn: {
paddingVertical: 8,
paddingHorizontal: 16,
backgroundColor: '#1890ff',
borderRadius: 6,
alignItems: 'center',
},
resetText: {
color: '#fff',
fontSize: 14,
},
});
export default CompleteGestureValidator;
为什么识别不到长按,什么原因
最新发布