import { useRef, useCallback, useState } from "react"; /** * 触摸点信息 */ interface TouchPoint { x: number; y: number; timestamp: number; } /** * 滑动方向 */ export type SwipeDirection = "left" | "right" | "up" | "down"; /** * 手势事件回调参数 */ export interface TouchEventData { /** 起始位置 */ startPoint: TouchPoint; /** 当前位置 */ currentPoint: TouchPoint; /** 移动距离 X */ deltaX: number; /** 移动距离 Y */ deltaY: number; /** 移动的总距离 */ distance: number; /** 移动速度(像素/毫秒) */ velocity: number; /** 移动方向 */ direction: SwipeDirection | null; /** 原始事件 */ event: React.TouchEvent | React.MouseEvent; } /** * useTouch 配置选项 */ export interface UseTouchOptions { /** 是否启用,默认 true */ enabled?: boolean; // ========== 滑动配置 ========== /** 触发滑动的最小距离(像素),默认 10 */ swipeThreshold?: number; /** 触发快速滑动的最小速度(像素/毫秒),默认 0.5 */ swipeVelocityThreshold?: number; // ========== 长按配置 ========== /** 触发长按的时间(毫秒),默认 500 */ longPressDelay?: number; /** 长按时允许的最大移动距离(像素),默认 10 */ longPressMoveThreshold?: number; // ========== 双击配置 ========== /** 双击的最大时间间隔(毫秒),默认 300 */ doubleTapDelay?: number; /** 双击的最大移动距离(像素),默认 10 */ doubleTapMoveThreshold?: number; // ========== 事件回调 ========== /** 触摸开始 */ onTouchStart?: (data: TouchEventData) => void; /** 触摸移动 */ onTouchMove?: (data: TouchEventData) => void; /** 触摸结束 */ onTouchEnd?: (data: TouchEventData) => void; /** 点击 */ onTap?: (data: TouchEventData) => void; /** 双击 */ onDoubleTap?: (data: TouchEventData) => void; /** 长按 */ onLongPress?: (data: TouchEventData) => void; /** 滑动 */ onSwipe?: (data: TouchEventData) => void; /** 向左滑动 */ onSwipeLeft?: (data: TouchEventData) => void; /** 向右滑动 */ onSwipeRight?: (data: TouchEventData) => void; /** 向上滑动 */ onSwipeUp?: (data: TouchEventData) => void; /** 向下滑动 */ onSwipeDown?: (data: TouchEventData) => void; // ========== 其他配置 ========== /** 是否阻止默认行为,默认 false */ preventDefault?: boolean; /** 是否阻止事件冒泡,默认 false */ stopPropagation?: boolean; } /** * useTouch 返回值 */ export interface UseTouchReturn { /** 是否正在触摸 */ isTouching: boolean; /** 触摸事件处理器 */ handlers: { onTouchStart: (e: React.TouchEvent) => void; onTouchMove: (e: React.TouchEvent) => void; onTouchEnd: (e: React.TouchEvent) => void; onMouseDown: (e: React.MouseEvent) => void; }; /** 当前触摸数据 */ touchData: TouchEventData | null; } /** * 计算两点之间的距离 */ function getDistance(p1: TouchPoint, p2: TouchPoint): number { const dx = p2.x - p1.x; const dy = p2.y - p1.y; return Math.sqrt(dx * dx + dy * dy); } /** * 计算滑动方向 */ function getDirection(deltaX: number, deltaY: number): SwipeDirection | null { const absDeltaX = Math.abs(deltaX); const absDeltaY = Math.abs(deltaY); if (absDeltaX < 10 && absDeltaY < 10) { return null; } if (absDeltaX > absDeltaY) { return deltaX > 0 ? "right" : "left"; } else { return deltaY > 0 ? "down" : "up"; } } /** * 灵活的触摸手势处理 Hook * * @example * ```tsx * const { handlers, isTouching } = useTouch({ * onSwipeLeft: () => console.log('向左滑动'), * onSwipeRight: () => console.log('向右滑动'), * onTap: () => console.log('点击'), * onDoubleTap: () => console.log('双击'), * onLongPress: () => console.log('长按'), * }); * * return
可触摸区域
; * ``` */ export function useTouch(options: UseTouchOptions = {}): UseTouchReturn { const { enabled = true, swipeThreshold = 10, swipeVelocityThreshold = 0.5, longPressDelay = 500, longPressMoveThreshold = 10, doubleTapDelay = 300, doubleTapMoveThreshold = 10, onTouchStart, onTouchMove, onTouchEnd, onTap, onDoubleTap, onLongPress, onSwipe, onSwipeLeft, onSwipeRight, onSwipeUp, onSwipeDown, preventDefault = false, stopPropagation = false, } = options; // 状态 const [isTouching, setIsTouching] = useState(false); const [touchData, setTouchData] = useState(null); // 引用 const startPointRef = useRef(null); const currentPointRef = useRef(null); const lastTapTimeRef = useRef(0); const longPressTimerRef = useRef(null); const isLongPressTriggeredRef = useRef(false); const isMouseDownRef = useRef(false); /** * 创建触摸点 */ const createTouchPoint = useCallback( (x: number, y: number): TouchPoint => ({ x, y, timestamp: Date.now(), }), [] ); /** * 创建触摸事件数据 */ const createTouchEventData = useCallback( ( startPoint: TouchPoint, currentPoint: TouchPoint, event: React.TouchEvent | React.MouseEvent ): TouchEventData => { const deltaX = currentPoint.x - startPoint.x; const deltaY = currentPoint.y - startPoint.y; const distance = getDistance(startPoint, currentPoint); const deltaTime = currentPoint.timestamp - startPoint.timestamp; const velocity = deltaTime > 0 ? distance / deltaTime : 0; const direction = getDirection(deltaX, deltaY); return { startPoint, currentPoint, deltaX, deltaY, distance, velocity, direction, event, }; }, [] ); /** * 清除长按定时器 */ const clearLongPressTimer = useCallback(() => { if (longPressTimerRef.current) { clearTimeout(longPressTimerRef.current); longPressTimerRef.current = null; } }, []); /** * 处理触摸开始 */ const handleStart = useCallback( (x: number, y: number, event: React.TouchEvent | React.MouseEvent) => { if (!enabled) return; if (preventDefault) event.preventDefault(); if (stopPropagation) event.stopPropagation(); const point = createTouchPoint(x, y); startPointRef.current = point; currentPointRef.current = point; isLongPressTriggeredRef.current = false; setIsTouching(true); const data = createTouchEventData(point, point, event); setTouchData(data); onTouchStart?.(data); // 设置长按定时器 if (onLongPress) { clearLongPressTimer(); longPressTimerRef.current = setTimeout(() => { if (startPointRef.current && currentPointRef.current) { const distance = getDistance( startPointRef.current, currentPointRef.current ); if (distance < longPressMoveThreshold) { isLongPressTriggeredRef.current = true; const longPressData = createTouchEventData( startPointRef.current, currentPointRef.current, event ); onLongPress(longPressData); } } }, longPressDelay); } }, [ enabled, preventDefault, stopPropagation, createTouchPoint, createTouchEventData, onTouchStart, onLongPress, longPressDelay, longPressMoveThreshold, clearLongPressTimer, ] ); /** * 处理触摸移动 */ const handleMove = useCallback( (x: number, y: number, event: React.TouchEvent | React.MouseEvent) => { if (!enabled || !startPointRef.current) return; if (preventDefault) event.preventDefault(); if (stopPropagation) event.stopPropagation(); const point = createTouchPoint(x, y); currentPointRef.current = point; const data = createTouchEventData(startPointRef.current, point, event); setTouchData(data); onTouchMove?.(data); // 如果移动距离超过阈值,取消长按 if (data.distance > longPressMoveThreshold) { clearLongPressTimer(); } }, [ enabled, preventDefault, stopPropagation, createTouchPoint, createTouchEventData, onTouchMove, longPressMoveThreshold, clearLongPressTimer, ] ); /** * 处理触摸结束 */ const handleEnd = useCallback( (event: React.TouchEvent | React.MouseEvent) => { if (!enabled || !startPointRef.current || !currentPointRef.current) return; if (preventDefault) event.preventDefault(); if (stopPropagation) event.stopPropagation(); clearLongPressTimer(); const data = createTouchEventData( startPointRef.current, currentPointRef.current, event ); onTouchEnd?.(data); // 判断手势类型 const { distance, velocity, direction } = data; // 长按已触发,不处理其他手势 if (isLongPressTriggeredRef.current) { setIsTouching(false); setTouchData(null); startPointRef.current = null; currentPointRef.current = null; return; } // 滑动手势 if (distance > swipeThreshold || velocity > swipeVelocityThreshold) { onSwipe?.(data); if (direction === "left") onSwipeLeft?.(data); if (direction === "right") onSwipeRight?.(data); if (direction === "up") onSwipeUp?.(data); if (direction === "down") onSwipeDown?.(data); } // 点击或双击 else if (distance < doubleTapMoveThreshold) { const now = Date.now(); const timeSinceLastTap = now - lastTapTimeRef.current; if (timeSinceLastTap < doubleTapDelay && onDoubleTap) { // 双击 onDoubleTap(data); lastTapTimeRef.current = 0; // 重置,避免三击触发两次双击 } else { // 单击 onTap?.(data); lastTapTimeRef.current = now; } } setIsTouching(false); setTouchData(null); startPointRef.current = null; currentPointRef.current = null; }, [ enabled, preventDefault, stopPropagation, createTouchEventData, onTouchEnd, onSwipe, onSwipeLeft, onSwipeRight, onSwipeUp, onSwipeDown, onTap, onDoubleTap, swipeThreshold, swipeVelocityThreshold, doubleTapMoveThreshold, doubleTapDelay, clearLongPressTimer, ] ); // 触摸事件处理器 const handleTouchStart = useCallback( (e: React.TouchEvent) => { if (e.touches.length === 1) { const touch = e.touches[0]; handleStart(touch?.clientX || 0, touch?.clientY || 0, e); } }, [handleStart] ); const handleTouchMove = useCallback( (e: React.TouchEvent) => { if (e.touches.length === 1) { const touch = e.touches[0]; handleMove(touch?.clientX || 0, touch?.clientY || 0, e); } }, [handleMove] ); const handleTouchEnd = useCallback( (e: React.TouchEvent) => { handleEnd(e); }, [handleEnd] ); // 鼠标事件处理器(桌面端支持) const handleMouseDown = useCallback( (e: React.MouseEvent) => { isMouseDownRef.current = true; handleStart(e.clientX, e.clientY, e); const handleMouseMove = (event: MouseEvent) => { if (isMouseDownRef.current) { handleMove( event.clientX, event.clientY, e as unknown as React.MouseEvent ); } }; const handleMouseUp = (event: MouseEvent) => { if (isMouseDownRef.current) { isMouseDownRef.current = false; handleEnd(e as unknown as React.MouseEvent); window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); } }; window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mouseup", handleMouseUp); }, [handleStart, handleMove, handleEnd] ); return { isTouching, handlers: { onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, onMouseDown: handleMouseDown, }, touchData, }; }