如何响应式重定位基于useGesture的可拖拽React组件?
拖拽React组件的响应式吸附与代码优化
核心解决思路
- 锚点标识替代坐标存储:不再直接存组件的x/y坐标,而是记录当前吸附的锚点(如
bottom-center),窗口缩放时根据锚点重新计算坐标,确保位置对应。 - 抽离独立逻辑函数:把锚点与坐标的转换、拖拽结束的吸附判断抽成单独函数,提升代码可读性和可维护性。
- 监听窗口resize事件:窗口尺寸变化时,根据当前锚点和组件尺寸重新计算坐标并更新组件位置。
完整优化代码
import { useRef, useState, useEffect } from 'react'; import { useDrag } from '@use-gesture/react'; import { useSpring } from '@react-spring/web'; // 定义所有预设吸附锚点类型 type AnchorPosition = | 'top-left' | 'top-center' | 'top-right' | 'middle-left' | 'center' | 'middle-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'; // 根据锚点计算组件对应坐标 const getPositionFromAnchor = ( anchor: AnchorPosition, windowWidth: number, windowHeight: number, elementWidth: number, elementHeight: number, offset = 10 // 边缘偏移量 ) => { switch (anchor) { case 'top-left': return { x: offset, y: offset }; case 'top-center': return { x: windowWidth / 2 - elementWidth / 2, y: offset }; case 'top-right': return { x: windowWidth - elementWidth - offset, y: offset }; case 'middle-left': return { x: offset, y: windowHeight / 2 - elementHeight / 2 }; case 'center': return { x: windowWidth / 2 - elementWidth / 2, y: windowHeight / 2 - elementHeight / 2 }; case 'middle-right': return { x: windowWidth - elementWidth - offset, y: windowHeight / 2 - elementHeight / 2 }; case 'bottom-left': return { x: offset, y: windowHeight - elementHeight - offset }; case 'bottom-center': return { x: windowWidth / 2 - elementWidth / 2, y: windowHeight - elementHeight - offset }; case 'bottom-right': return { x: windowWidth - elementWidth - offset, y: windowHeight - elementHeight - offset }; default: return { x: offset, y: offset }; } }; // 根据当前位置判断应该吸附的锚点 const getAnchorFromPosition = ( elementX: number, elementY: number, elementWidth: number, elementHeight: number, windowWidth: number, windowHeight: number ) => { const centerX = elementX + elementWidth / 2; const centerY = elementY + elementHeight / 2; // X轴锚点判断(三分视口) const xAnchor = centerX < windowWidth / 3 ? 'left' : centerX > windowWidth * 2 / 3 ? 'right' : 'center'; // Y轴锚点判断(三分视口) const yAnchor = centerY < windowHeight / 3 ? 'top' : centerY > windowHeight * 2 / 3 ? 'bottom' : 'middle'; return `${yAnchor}-${xAnchor}` as AnchorPosition; }; export function usePip(ref: React.RefObject<HTMLElement>) { const [currentAnchor, setCurrentAnchor] = useState<AnchorPosition>('bottom-center'); const [elementSize, setElementSize] = useState({ width: 0, height: 0 }); const [windowSize, setWindowSize] = useState({ width: typeof window !== 'undefined' ? window.innerWidth : 0, height: typeof window !== 'undefined' ? window.innerHeight : 0 }); // 组件挂载时获取自身尺寸 useEffect(() => { if (ref.current) { const rect = ref.current.getBoundingClientRect(); setElementSize({ width: rect.width, height: rect.height }); } }, [ref]); // 监听窗口resize事件 useEffect(() => { const handleResize = () => { setWindowSize({ width: window.innerWidth, height: window.innerHeight }); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); // 初始化spring动画状态 const [styles, setStyles] = useSpring(() => getPositionFromAnchor(currentAnchor, windowSize.width, windowSize.height, elementSize.width, elementSize.height) ); // 锚点或窗口尺寸变化时更新位置 useEffect(() => { if (elementSize.width && elementSize.height) { setStyles(getPositionFromAnchor(currentAnchor, windowSize.width, windowSize.height, elementSize.width, elementSize.height)); } }, [currentAnchor, windowSize, elementSize]); // 拖拽逻辑绑定 const bind = useDrag(({ down, movement: [x, y], last }) => { if (!ref.current) return; const rect = ref.current.getBoundingClientRect(); const currentX = down ? x : styles.x.get(); const currentY = down ? y : styles.y.get(); // 拖拽结束时判断并切换到对应锚点 if (last) { const anchor = getAnchorFromPosition(currentX, currentY, rect.width, rect.height, window.innerWidth, window.innerHeight); setCurrentAnchor(anchor); setStyles(getPositionFromAnchor(anchor, window.innerWidth, window.innerHeight, rect.width, rect.height)); return; } // 拖拽过程中实时更新位置 setStyles({ x: currentX, y: currentY, immediate: down }); }, { initial: () => [styles.x.get(), styles.y.get()] }); return [ bind, { ...styles, position: 'fixed' as const, left: 0, top: 0, zIndex: 99999 } ]; }
关键优化说明
- 锚点驱动的响应式:通过
currentAnchor记录当前吸附位置,窗口缩放时只需重新计算对应坐标,彻底解决位置偏移问题。 - 无硬编码尺寸:组件挂载时自动获取自身宽高,适配不同大小的拖拽组件,避免固定数值导致的适配问题。
- 完整9点位覆盖:通过三分视口的判断逻辑,精准匹配四角、四边中点及中心的所有预设吸附位置。
- 逻辑解耦:锚点与坐标的转换逻辑抽离为独立函数,后续修改点位规则只需调整对应函数,无需改动核心逻辑。
内容的提问来源于stack exchange,提问作者jamesrogers
相关产品推荐
相关产品推荐

