原生滑动进度条(react+ts)

// 拖拽进度条

import { ProgressModuleBox } from "./style"

import React, { useRef } from 'react';
import { useLayoutEffect } from 'react';
import { useHeightSize, useWidthSize } from "../../../../../resources/useFun/size";
import { useState } from 'react';


const ProgressModule = () => {

    const progressStyle = {
        width: useWidthSize(500),
        height: useHeightSize(60),
        borderRadius: useHeightSize(4),
        backgroundColor: '#E2E2E2',
    }

    const fontSize = {
        fontSize: useWidthSize(14)
    }

    const sliderRef = useRef<HTMLDivElement>(null);
    const [newProgress, setNewProgress] = useState<number>(0)

    useLayoutEffect(() => {
        const slider = sliderRef.current;
        let isDragging = false;

        const handleDragStart = () => {
            isDragging = true;
        };

        const handleDrag = (event: MouseEvent) => {
            if (!isDragging) return;

            const sliderWidth = slider!.offsetWidth;
            const offsetX = event.clientX - slider!.getBoundingClientRect().left;
            const newProgress = Math.round((offsetX / sliderWidth) * 100);
            slider!.style.backgroundSize = `${newProgress}% 100%`;
            slider!.style.background = `linear-gradient(to right, #3588FE, #7FB4FF ${newProgress}%, #eee ${newProgress}%, #eee 100%)`;
            console.log(newProgress);
            setNewProgress(newProgress)

        };

        const handleDragEnd = () => {
            isDragging = false;
        };

        slider!.addEventListener('mousedown', handleDragStart);
        slider!.addEventListener('mousemove', handleDrag);
        slider!.addEventListener('mouseup', handleDragEnd);

        return () => {
            slider!.removeEventListener('mousedown', handleDragStart);
            slider!.removeEventListener('mousemove', handleDrag);
            slider!.removeEventListener('mouseup', handleDragEnd);
        };
    }, []);


    return <ProgressModuleBox>
        <div
            className="native-slider"
            ref={sliderRef}
            style={progressStyle}
        ><span className="span_pict" style={{ lineHeight: useHeightSize(60), marginRight: useWidthSize(10) }}>{newProgress}%</span></div>
        <div style={fontSize} className="ke_du">
            <span>0</span>
            <span>20</span>
            <span>40</span>
            <span>60</span>
            <span>80</span>
            <span>100</span>
        </div>

    </ProgressModuleBox>
}

export default ProgressModule
// 样式

import styled from "styled-components";

export const ProgressModuleBox = styled.div`

.native-slider{
 
}

.span_pict{
    display: flex;
    justify-content: end;
    align-items: center;
}

.ke_du{
    display: flex;
    justify-content: space-between;
    span {
        margin-top: 6px;
        color: #CCCCCC;
    }
}

`

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; 为什么识别不到长按,什么原因
最新发布
08-17
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值