react-beautiful-dnd 自动滚动(Auto Scrolling)完全指南:鼠标、触摸与键盘拖动的滚动机制
2026/9/19 21:02:28 网站建设 项目流程

react-beautiful-dnd 自动滚动(Auto Scrolling)完全指南:鼠标、触摸与键盘拖动的滚动机制

【免费下载链接】react-beautiful-dndBeautiful and accessible drag and drop for lists with React项目地址: https://gitcode.com/gh_mirrors/re/react-beautiful-dnd

本篇指南围绕 react-beautiful-dnd 的自动滚动能力展开:当用户把一个<Draggable />拖到某个_容器_(可滚动的<Droppable />、拥有滚动父级的列表,或window本身)边缘时,库会自动滚动容器为拖拽项腾出空间。文章会先讲清鼠标/触摸输入下的流体滚动(fluid scrolling)触发与加速逻辑,再说明键盘拖动时的跳跃滚动(jump scrolling)如何与列表滚动、窗口滚动协同,最后结合源码与测试给出可验证的实现依据。读完你将理解自动滚动的阈值百分比、加速曲线、时间阻尼等核心参数,并能判断大尺寸拖拽项、iOS 抖动等边界场景的行为。

什么是自动滚动

在 react-beautiful-dnd 中,容器(container)指的是三类对象:

  1. 自身可滚动的<Droppable />
  2. 拥有可滚动父级元素的<Droppable />
  3. window窗口本身。

当被拖拽的<Draggable />靠近某个容器的边缘时,库会自动滚动该容器,从而让拖拽项可以继续前进到视野之外的位置。该特性适用于单列表、多列表(Board)等全部配置,并且对鼠标、触摸和键盘三种输入方式都生效。

自动滚动的完整实现位于 src/state/auto-scroller 目录,它被拆分为两个子模块:

  • fluid-scroller:面向鼠标与触摸的连续流体滚动;
  • jump-scroller:面向键盘拖动的单次跳跃滚动。

两个模块由 src/state/auto-scroller/index.js 统一调度:当state.movementMode === 'FLUID'(鼠标/触摸)时走fluidScroller;当movementMode === 'SNAP'(键盘)且存在state.scrollJumpRequest时走jumpScroller。滚动仅发生在DRAGGING阶段。

鼠标与触摸输入:流体自动滚动

触发与加速机制

对鼠标和触摸输入,自动滚动遵循以下规则:

  • <Draggable />中心点进入距离容器边缘的某个小范围内时,自动滚动开始;
  • 拖拽项越靠近容器边缘,滚动速度越快;
  • 速度增长使用缓动函数(easing function):越接近边缘,加速度呈指数级增长;
  • 在距离真正边缘还有一小段距离处就达到最大滚动速度,这样用户无需把鼠标精确对准边缘即可获得最高速度;
  • 上述逻辑适用于任何可滚动的边缘(上、下、左、右)。

该行为的触发条件是"拖拽项中心点"而非边缘,源码可见于 scroll.js:const center = state.current.page.borderBoxCenter;,同时subject取的是拖拽项的page.marginBox

基于百分比的阈值:与容器尺寸无关

自动滚动的触发距离不是固定像素,而是基于容器高度或宽度的一定百分比(纵向看高度、横向看宽度)。使用百分比而非原始像素值,可以保证无论容器大小和形状如何,体验都是一致的。

这些百分比定义在核心配置 src/state/auto-scroller/fluid-scroller/config.js 中:

配置项默认值含义
startFromPercentage0.25拖拽项中心距容器边缘达到容器尺寸的 25% 时开始自动滚动
maxScrollAtPercentage0.05距边缘 5% 时达到最大滚动速度,无需精确贴边
maxPixelScroll28每动画帧(frame)最大滚动像素数
ease(p) => Math.pow(p, 2)缓动函数,把 0~1 的进度映射为 0~1 的速度系数
durationDampening.stopDampeningAt1200(ms)从拖拽开始算起,速度阻尼结束的时间点
durationDampening.accelerateAt360(ms)开始加速减少阻尼的时间点

百分比阈值会在运行期转换为像素值,见 get-distance-thresholds.js:

const startScrollingFrom = container[axis.size] * config.startFromPercentage; const maxScrollValueAt = container[axis.size] * config.maxScrollAtPercentage;

即:一个 400px 高的列表,拖拽项中心距底边 100px(25%)开始滚动,距底边 20px(5%)时达到全速。

速度计算:从距离到每帧像素

单个轴上的滚动值由 get-value-from-distance.js 计算:

  • 距离超过startScrollingFrom:返回0(不滚动);
  • 距离小于等于maxScrollValueAt:返回config.maxPixelScroll(最大速度 28px/帧);
  • 恰好等于startScrollingFrom:返回minScroll(最小滚动值 1px);
  • 两者之间:先通过 get-percentage.js 求出当前位置在[maxScrollValueAt, startScrollingFrom]区间内的比例并取反,再用maxPixelScroll * ease(percentage)得出滚动值,最后Math.ceil向上取整。

方向判断在 get-scroll-on-axis/index.js 中完成:比较中心点到该轴起点与终点的距离,更靠近哪端就往哪端滚动。纵向与横向两个轴会分别计算,最终在 get-scroll/index.js 中合成{ x, y }位移。

时间阻尼(Time Dampening):拖拽刚开始时不“起飞”

为避免拖拽刚一开始就高速滚动导致用户失去控制,滚动速度还会被拖拽持续时间“阻尼”:

  • 拖拽开始后的0 ~ 360msaccelerateAt)内,无论距离多近,单次滚动都只有minScroll(1px);
  • 360ms ~ 1200msstopDampeningAt)之间,速度按proposedScroll * ease(progress)平滑爬升;
  • 超过1200ms后阻尼完全解除,恢复按距离计算的速度。

实现见 dampen-value-by-time.js,而“是否需要时间阻尼”的探测发生在 fluid-scroller 的start()阶段:先用假回调执行一次滚动计算,若判定确实需要滚动,则开启阻尼;相关代码在 fluid-scroller/index.js。

minScroll被定义为常量1(见 min-scroll.js),因为浏览器scroll事件只有位移至少 1px 时才会触发,从而驱动下一次自动滚动调用。

滚动目标的优先级与边界校验

每次滚动计算(scroll.js)的执行顺序是:

  1. state.isWindowScrollAllowed为真,先尝试滚动window(get-window-scroll-change.js);
  2. 否则或窗口无法吸收该位移时,通过 get-best-scrollable-droppable.js 找到最佳可滚动列表:优先滚动当前拖拽悬停(dragged over)的列表;若未悬停在任何列表上,则寻找中心点所在的最近可滚动列表帧(已排除 disabled 或无 frame 的列表);
  3. 计算该列表的滚动值(get-droppable-scroll-change.js),并经过 can-scroll.js 中的canScrollWindow/canScrollDroppable校验是否真的还能滚动。

值得注意:窗口优先于列表滚动。此外所有滚动调用都通过raf-schd调度到同一动画帧内合并执行,避免一帧内多次触发scroll事件(见 fluid-scroller/index.js 与测试 droppable-scrolling.spec.js 中的 “should throttle multiple scrolls into a single animation frame”)。

鼠标滚轮与触控板的手动滚动

除了自动滚动,鼠标与触摸输入还允许用户在拖拽过程中手动滚动window或某个<Droppable />,即直接使用鼠标滚轮或触控板手势滚动容器。这意味着拖拽长列表时,用户既可以把拖拽项甩向边缘触发自动滚动,也可以随时用滚轮精确调整视野。

窗口手动滚动的监听由 scroll-listener.js 中间件驱动:INITIAL_PUBLISH时启动监听,DROP_COMPLETE/DROP_ANIMATE/FLUSH时停止,收到窗口滚动事件后派发moveByWindowScrollaction 同步拖拽位置。

大拖拽项的限制:按轴禁用滚动

如果<Draggable />在某个轴上大于容器,则该轴方向不允许自动滚动:

  • 拖拽项高度大于容器高度 → 禁止纵向(垂直)自动滚动;
  • 拖拽项宽度大于容器宽度 → 禁止横向(水平)自动滚动;
  • 两个轴都超尺寸 → 完全禁止自动滚动。

例如,一个比窗口还高的拖拽项不会触发纵向自动滚动,但仍允许横向滚动。实现位于 adjust-for-size-limits.js,它把对应轴的滚动值归零;对应测试见 big-draggables.spec.js。

iOS 上的自动滚动抖动问题

在 iOS 浏览器(webkit)上,自动滚动时<Draggable />会出现明显的抖动。这是因为 webkit 引擎存在一个尚无解决方案的 bug(bug 编号 181954),作者曾长期尝试绕过但未能解决。若需要跟进或推动修复,可关注 webkit 官方 bug 跟踪(bugs.webkit.org上的 181954 号问题)。这是引擎层面的限制,与 react-beautiful-dnd 的实现无关。

键盘拖动:跳跃滚动(Jump Scrolling)

键盘拖动(SNAP模式)同样会正确更新滚动位置。为了把<Draggable />移动到正确的位置,库会把一次键盘移动指令产生的滚动需求拆解为:列表滚动 + 窗口滚动 + 手动位移的组合,确保拖拽项总能落到正确位置。

实现位于 jump-scroller.js,一次跳跃滚动的执行流程如下:

  1. 读取state.scrollJumpRequest(键盘移动时产生的滚动请求);
  2. 优先滚动悬停的列表:调用scrollDroppableAsMuchAsItCan,若列表无法吸收全部位移,返回剩余量;
  3. 再由窗口吸收剩余量:调用scrollWindowAsMuchAsItCan,同样只吸收窗口能承受的部分;
  4. 剩余部分手动移动:把窗口也吸收不掉的位移通过movemoveByOffset)直接平移拖拽项,保证其依然处于正确位置。

这种“先列表、再窗口、最后手动平移”的优先级设计,是为了避免拖拽项过早离开列表(源码注释:We scroll the droppable first if we can to avoid the draggable leaving the list)。窗口与列表各自的吸收能力由 can-scroll.js 中的getOverlap/canPartiallyScroll精确计算,相关边界情况(到达滚动边界、跨轴、往回滚动等)在 can-scroll.spec.js 中有完整覆盖。

对于有视觉障碍的用户,这意味着无需鼠标定位也能在大列表中准确移动条目,显著提升了无障碍体验。

自动滚动的生命周期:中间件如何驱动

自动滚动由 src/state/middleware/auto-scroll.js 中间件接入 Redux 状态流:

  • 收到INITIAL_PUBLISH(拖拽开始、尺寸发布完成)时,先放行 action 让状态进入DRAGGING,随后调用autoScroller.start(state)
  • 之后每个 action 经 reducer 处理后,都会调用autoScroller.scroll(state),用最新状态做一次滚动计算;
  • 收到DROP_COMPLETEDROP_ANIMATEFLUSH时调用autoScroller.stop()取消所有待执行的滚动调度。

因此自动滚动是状态驱动的:拖拽项位置每变化一次,就重新评估一次是否需要滚动、滚动多快,从而形成“越靠边越快”的连续反馈。

可继续深入阅读的源码与测试

  • 核心参数: src/state/auto-scroller/fluid-scroller/config.js
  • 流体滚动主流程: fluid-scroller/index.js → scroll.js → get-scroll/index.js
  • 键盘跳跃滚动: jump-scroller.js
  • 滚动能力校验: can-scroll.js
  • 调度入口: auto-scroller/index.js 与 middleware/auto-scroll.js
  • 测试用例: droppable-scrolling.spec.js、big-draggables.spec.js、choosing-the-right-scroller.spec.js、can-scroll.spec.js

若想了解自动滚动之外的整体接入方式(容器定义、window视口计算、滚动容器的探测),可继续阅读文档 droppable.md、how-we-detect-scroll-containers.md 与 how-we-use-dom-events.md。回到文档总览可查看 README.md。

【免费下载链接】react-beautiful-dndBeautiful and accessible drag and drop for lists with React项目地址: https://gitcode.com/gh_mirrors/re/react-beautiful-dnd

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询