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)指的是三类对象:
- 自身可滚动的
<Droppable />; - 拥有可滚动父级元素的
<Droppable />; 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 中:
| 配置项 | 默认值 | 含义 |
|---|---|---|
startFromPercentage | 0.25 | 拖拽项中心距容器边缘达到容器尺寸的 25% 时开始自动滚动 |
maxScrollAtPercentage | 0.05 | 距边缘 5% 时达到最大滚动速度,无需精确贴边 |
maxPixelScroll | 28 | 每动画帧(frame)最大滚动像素数 |
ease | (p) => Math.pow(p, 2) | 缓动函数,把 0~1 的进度映射为 0~1 的速度系数 |
durationDampening.stopDampeningAt | 1200(ms) | 从拖拽开始算起,速度阻尼结束的时间点 |
durationDampening.accelerateAt | 360(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 ~ 360ms(accelerateAt)内,无论距离多近,单次滚动都只有minScroll(1px); 360ms ~ 1200ms(stopDampeningAt)之间,速度按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)的执行顺序是:
- 若
state.isWindowScrollAllowed为真,先尝试滚动window(get-window-scroll-change.js); - 否则或窗口无法吸收该位移时,通过 get-best-scrollable-droppable.js 找到最佳可滚动列表:优先滚动当前拖拽悬停(dragged over)的列表;若未悬停在任何列表上,则寻找中心点所在的最近可滚动列表帧(已排除 disabled 或无 frame 的列表);
- 计算该列表的滚动值(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,一次跳跃滚动的执行流程如下:
- 读取
state.scrollJumpRequest(键盘移动时产生的滚动请求); - 优先滚动悬停的列表:调用
scrollDroppableAsMuchAsItCan,若列表无法吸收全部位移,返回剩余量; - 再由窗口吸收剩余量:调用
scrollWindowAsMuchAsItCan,同样只吸收窗口能承受的部分; - 剩余部分手动移动:把窗口也吸收不掉的位移通过
move(moveByOffset)直接平移拖拽项,保证其依然处于正确位置。
这种“先列表、再窗口、最后手动平移”的优先级设计,是为了避免拖拽项过早离开列表(源码注释: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_COMPLETE、DROP_ANIMATE或FLUSH时调用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),仅供参考