You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为桌面端实现类移动端的拖拽滚动效果

实现桌面端鼠标拖拽惯性滚动方案

针对你的横向滚动容器需求,以下是原生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;
}

核心原理说明

  1. 拖拽状态管理:通过useRef保存拖拽过程中的关键数据(位置、速度、动画帧ID),避免组件重复渲染
  2. 速度计算:在鼠标移动时实时计算拖拽速度,为惯性滚动提供基础
  3. 惯性滚动模拟:鼠标松开后,通过requestAnimationFrame逐帧衰减速度,模拟自然减速的滚动效果
  4. 事件清理:组件卸载时移除事件监听并清理动画帧,避免内存泄漏

关于你的疑问

直接让浏览器把鼠标事件识别为触摸事件兼容性较差,且无法精准控制惯性效果。上述原生实现方案更可靠,完全自定义的逻辑可以适配不同场景的需求,同时保持和移动端触摸滚动一致的流畅体验。

内容的提问来源于stack exchange,提问作者Sebastian Meckovski

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 18:03:16