为桌面端实现类移动端的拖拽滚动效果
实现桌面端鼠标拖拽惯性滚动方案
针对你的横向滚动容器需求,以下是原生React实现的鼠标拖拽+惯性滚动方案,完全匹配移动端触摸滚动的流畅体验:
完整实现代码
React组件代码
import React, { useRef, useEffect } from 'react'; const App = () => { const tileViewRef = useRef(null); const isDragging = useRef(false); const startX = useRef(0); const startScrollLeft = useRef(0); const velocity = useRef(0); const lastX = useRef(0); const lastTime = useRef(0); const animationFrameId = useRef(null); // 鼠标按下事件:初始化拖拽状态 const handleMouseDown = (e) => { isDragging.current = true; startX.current = e.pageX; startScrollLeft.current = tileViewRef.current.scrollLeft; lastX.current = e.pageX; lastTime.current = performance.now(); // 停止正在进行的惯性动画 if (animationFrameId.current) cancelAnimationFrame(animationFrameId.current); e.preventDefault(); // 避免选中文本 }; // 鼠标移动事件:实时更新滚动位置并计算拖拽速度 const handleMouseMove = (e) => { if (!isDragging.current) return; const currentX = e.pageX; const currentTime = performance.now(); const deltaX = currentX - lastX.current; const deltaTime = currentTime - lastTime.current; // 计算当前拖拽速度(像素/毫秒) velocity.current = deltaX / deltaTime; // 更新容器滚动位置 tileViewRef.current.scrollLeft = startScrollLeft.current - (currentX - startX.current); lastX.current = currentX; lastTime.current = currentTime; }; // 鼠标松开/离开事件:启动惯性滚动 const endDrag = () => { if (!isDragging.current) return; isDragging.current = false; startInertialScroll(); }; // 惯性滚动逻辑:逐帧衰减速度直到停止 const startInertialScroll = () => { let currentVelocity = velocity.current; const deceleration = 0.005; // 减速系数,值越大减速越快 const animateScroll = () => { // 速度衰减 currentVelocity *= (1 - deceleration); // 速度过小则停止动画 if (Math.abs(currentVelocity) < 0.1) { animationFrameId.current = null; return; } // 按速度更新滚动位置(16ms近似一帧的时间) tileViewRef.current.scrollLeft -= currentVelocity * 16; animationFrameId.current = requestAnimationFrame(animateScroll); }; animationFrameId.current = requestAnimationFrame(animateScroll); }; // 绑定/解绑事件监听 useEffect(() => { const tileView = tileViewRef.current; if (!tileView) return; tileView.addEventListener('mousedown', handleMouseDown); document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', endDrag); document.addEventListener('mouseleave', endDrag); return () => { tileView.removeEventListener('mousedown', handleMouseDown); document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', endDrag); document.removeEventListener('mouseleave', endDrag); if (animationFrameId.current) cancelAnimationFrame(animationFrameId.current); }; }, []); // 生成列表数据 const data = Array.from({ length: 50 }, (_, i) => ({ key: i, value: `item ${i}` })); const renderItem = (x) => `hello${x.key}`; return ( <div className="tileView" ref={tileViewRef}> {data.map(x => ( <div key={x.key} className="item">{renderItem(x)}</div> ))} </div> ); }; export default App;
CSS样式优化
.tileView { display: flex; overflow-x: scroll; overflow-y: hidden; width: 600px; cursor: grab; /* 拖拽前光标样式 */ } .tileView:active { cursor: grabbing; /* 拖拽中光标样式 */ } .item { border: 1px solid gray; width: 100px; height: 100px; flex-shrink: 0; margin: 10px; display: flex; align-items: center; justify-content: center; }
核心原理说明
- 拖拽状态管理:通过
useRef保存拖拽过程中的关键数据(位置、速度、动画帧ID),避免组件重复渲染 - 速度计算:在鼠标移动时实时计算拖拽速度,为惯性滚动提供基础
- 惯性滚动模拟:鼠标松开后,通过
requestAnimationFrame逐帧衰减速度,模拟自然减速的滚动效果 - 事件清理:组件卸载时移除事件监听并清理动画帧,避免内存泄漏
关于你的疑问
直接让浏览器把鼠标事件识别为触摸事件兼容性较差,且无法精准控制惯性效果。上述原生实现方案更可靠,完全自定义的逻辑可以适配不同场景的需求,同时保持和移动端触摸滚动一致的流畅体验。
内容的提问来源于stack exchange,提问作者Sebastian Meckovski
相关产品推荐
相关产品推荐

