本篇为 dnd-kit 官方文档 中 DndContext 一章的对照翻译,如有错误,欢迎指正。
对于碰撞检测知之甚少,有关章节请大佬们指正。
原文章节目录跳转
DndContext
应用结构 Application structure
Context provider
为了让你的 「Droppable」 和 「Draggable」 组件彼此互动,你需要确保在 React 组件树中使用它们的部分在同一个父 <DndContext>
组件中。 <DndContext>
provider 利用 「React Context API」 在 draggable、droppable 组件和 hooks 之间共享数据。
React context 提供了一种通过组件树传递数据的方法,而不需要在每一级手动传递 props。
因此,使用 Draggable
、useDroppable
或 DragOverlay
的组件将需要嵌套在一个 DndContext
provider 中。
它们不需要是直接的后代,但确实需要有一个父 <DndContext>
provider 在树上的某个地方。
import React from 'react'
import { DndContext } from '@dnd-kit/core'
function App() {
return (
<DndContext>
{/* */}
{/* Components that use `useDraggable`, `useDroppable` */}
</DndContext>
)
}
Nesting 嵌套
您还可以将 <DndContext>
provider 嵌套在其他 <DndContext>
provider 中,以实现相互独立的嵌套式 draggable/droppable 界面。
import React from 'react'
import { DndContext } from '@dnd-kit/core'
function App() {
return (
<DndContext>
{/* Components that use `useDraggable`, `useDroppable` */}
<DndContext>
{/* ... */}
<DndContext>{/* ... */}</DndContext>
</DndContext>
</DndContext>
)
}
在嵌套 DndContext
providers 时,请记住,useDroppable
和 useDraggable
只能访问该上下文中的其他 draggable 和 droppable 的节点。
如果多个 DndContext
provider 在监听同一事件,那么事件将由包含被该事件激活的传感器的第一个 DndContext
捕获,类似于事件在 DOM 中的冒泡方式。
Props
interface Props {
announcements?: Announcements;
autoScroll?: boolean;
cancelDrop?: CancelDrop;
children?: React.ReactNode;
collisionDetection?: CollisionDetection;
layoutMeasuring?: Partial<LayoutMeasuring>;
modifiers?: Modifiers;
screenReaderInstructions?: ScreenReaderInstructions;
sensors?: SensorDescriptor<any>[];
onDragStart?(event: DragStartEvent): void;
onDragMove?(event: DragMoveEvent): void;
onDragOver?(event: DragOverEvent): void;
onDragEnd?(event: DragEndEvent): void;
onDragCancel?(): void;
}
Event handlers 事件处理
正如你从上面的 props 列表中看到的,有许多由 <DndContext>
发出的不同事件,你可以监听并决定如何处理。
你可以监听的主要事件有
onDragStart
当符合该传感器激活条件的拖动事件发生时触发,同时触发的还有被拾起的可拖动元素的唯一标识。
onDragMove
当 draggable 项被移动时启动。取决于被激活的传感器,例如,当指针被移动或键盘移动键被按下时。
onDragOver
当一个 draggable 项被移动到一个 droppable 容器上时发生触发,同时还有该 droppable 容器的唯一标识符。
onDragEnd
在 draggable 项完成后触发的事件
该事件包含有关活动的 draggable ID 的信息,以及可拖动项目是否被放在上方的信息。
如果可拖动项目被 dropped 时没有检测到碰撞 ,over
属性将为空。如果检测到碰撞,over
属性将包含被投放的可拖动项的 ID。
重要的是要理解
onDragEnd
事件不会将可拖动项目移入可拖动容器。相反,它提供了关于哪个可拖动项目被 dropped 以及它被 dropped 时是否在可拖动容器上的信息。
应由
DndContext
的消费端来决定如何处理该信息以及如何对其作出反应,例如,通过更新(或不更新)其内部状态来响应该事件,从而使项目在不同的父级 droppable 中被声明性地呈现。
onDragCancel
当拖动操作被取消时启动,例如,如果用户在拖动可拖动的项目时按下 escape 键。
Accessibility 可访问性
欲了解更多有关可拖拽组件的可访问性的细节和最佳实践,请阅读可访问性部分。
Announcements
使用 announcements 属性来定制屏幕阅读器公告,当可拖动的项目被拾起、移动到 droppable 区域上并被 dropped 时,这些公告会在实时区域内公布。
默认的 announcements 是。
const defaultAnnouncements = {
onDragStart(id) {
return `Picked up draggable item ${id}.`
},
onDragOver(id, overId) {
if (overId) {
return `Draggable item ${id} was moved over droppable area ${overId}.`
}
return `Draggable item ${id} is no longer over a droppable area.`
},
onDragEnd(id, overId) {
if (overId) {
return `Draggable item was dropped over droppable area ${overId}`
}
return `Draggable item ${id} was dropped.`
},
onDragCancel(id) {
return `Dragging was cancelled. Draggable item ${id} was dropped.`
},
}
虽然这些默认公告是合理的,涵盖大多数简单的用例,但你最了解你的应用程序,我们强烈建议你定制这些公告,以提供一个更适合您应用的屏幕阅读器体验。
Screen reader instructions 屏幕阅读器说明
使用 screenReaderInstructions
来定制当焦点被移动时读给屏幕阅读器的指令。
Autoscroll 自动滚动
使用可选的布尔值属性 autoScroll
来暂时或永久地禁用在此 DndContext
中使用的所有传感器的自动滚动功能。
也可以使用传感器的静态属性 autoScrollEnabled
在单个传感器的基础上禁用自动滚动功能。例如,键盘传感器在内部管理滚动,因此其静态属性 autoScrollEnabled
设置为 false
。
Collision detection 碰撞检测
使用 collisionDetection
属性来定制用于检测 DndContext
provider 内 draggable 节点和 droppable 区域之间碰撞的碰撞检测算法。
默认的碰撞检测算法是矩形相交算法。
内置的碰撞检测算法有。
- Rectangle intersection 矩形相交
- Closest center 最接近的中心
- Closest corners 最接近的角
你也可以建立自定义的碰撞检测算法或对现有的算法进行组合。
要了解更多,请阅读碰撞检测指南。
Sensors 传感器
传感器是一个抽象概念,用于检测不同的输入方法,以便启动拖动操作、响应移动以及结束或取消操作。
DndContext
使用的默认传感器是指针和键盘传感器。
要了解如何定制传感器或如何将不同的传感器传递给 DndContext,请阅读传感器指南。
Modifiers 修改器
修改器让你动态地修改由传感器检测到的运动坐标。它们可以用于广泛的使用情况,例如。
- 将运动限制在一个单一的轴上
- 将运动限制在可拖动节点容器的矩形边界内
- 将运动限制在可拖动节点的滚动容器矩形边界内
- 施加阻力或钳制运动
要了解更多关于如何使用修改器的信息,请阅读修改器指南。
Layout measuring 布局测量
使用实例。
你可以通过使用 layoutMeasuring
属性来配置 DndContext
何时以及多长时间应该测量它的 droppable 元素。
frequency
参数控制布局应该被测量的频率。默认情况下,布局测量被设置为 optimized
,它只测量基于 strategy
的布局。
指定以下策略之一:
-
LayoutMeasuringStrategy.WhileDragging
: 默认行为,只在拖动开始后测量 droppable 元素。 -
LayoutMeasuringStrategy.BeforeDragging
: 在拖动开始前和拖动结束后立即测量可移动的元素。 -
LayoutMeasuringStrategy.Always
: 在拖动开始前、拖动开始后以及拖动结束后,测量可移动的元素。
Example usage:
import { DndContext, LayoutMeasuringStrategy } from '@dnd-kit/core'
;<DndContext layoutMeasuring={{ strategy: LayoutMeasuringStrategy.Always }} />
useDndContext
对于高级用例,例如,如果你在 @dnd-kit/core
的基础上构建你自己的预设,你可能希望能够访问 useDraggable
和 useDroppable
所能访问的 <DndContext>
的内部环境。
import { useDndContext } from '@dnd-kit/core'
function CustomPreset() {
const dndContext = useDndContext()
}
如果你认为你建立的预设对其他人有用,请在 dnd-kit
资源库中开辟一个 PR 进行讨论。
useDndMonitor
useDndMonitor
hook 可用于被 DndContext
provider 包裹的组件中,以监控发生在该 DndContext
中的不同拖放事件。
import { DndContext, useDndMonitor } from '@dnd-kit/core'
function App() {
return (
<DndContext>
<Component />
</DndContext>
)
}
function Component() {
// Monitor drag and drop events that happen on the parent `DndContext` provider
useDndMonitor({
onDragStart(event) {},
onDragMove(event) {},
onDragOver(event) {},
onDragEnd(event) {},
onDragCancel(event) {},
})
}
Collision detection algorithms 碰撞检测算法
如果你熟悉 2D 游戏的构建方式,你可能已经遇到了碰撞检测算法的概念。
碰撞检测的一个较简单的形式是在两个轴对齐的矩形之间--意味着没有旋转的矩形。这种形式的碰撞检测通常被称为轴对齐的边界盒(AABB)。
内置的碰撞检测算法假定有一个矩形的边界框。
一个元素的边界框是最小的可能的矩形(与该元素的用户坐标系的轴对齐),它完全包围着它和它的后代。
– Source: MDN
这意味着,即使可拖动或可下拉的节点看起来是圆形或三角形的,它们的边界框仍将是矩形的。
如果你想使用矩形以外的其他形状来检测碰撞,请建立你自己的碰撞检测算法custom collision detection algorithm.
Rectangle intersection 矩形相交
默认情况下,DndContext
使用矩形相交的碰撞检测算法。
该算法的工作原理是确保矩形的 4 个边之间没有任何间隙。任何缝隙都意味着不存在碰撞。
这意味着,为了使可拖动的项目被认为是在可拖动的区域上,两个矩形之间需要有一个交点。
Closest center 最接近的中心
虽然矩形相交算法非常适合大多数拖放用例,但它是严格的,因为它要求可拖动和可放下的边界矩形直接接触并相交。
对于某些用例,如可排序的列表,建议使用一个更宽容的碰撞检测算法。
顾名思义,最接近中心的算法会找到「droppable 容器中心」与当前「draggable 项的矩形中心」最接近的那个容器。
Closest corners 最接近的角
与最靠近中心的算法一样,最靠近角落的算法不要求 「draggable 矩形」和 「droppable 矩形」相交。
相反,它测量当前 draggable 项的四个角与每个 droppable 容器的四个角之间的距离,以找到最近的一个。
距离是从 draggable 项的左上角到 droppable 边界矩形的左上角,右上角到右上角,左下角到左下角,以及右下角到右下角。
什么时候应该使用最接近角的算法而不是最接近中心的算法?
在大多数情况下,最接近中心的算法效果很好,而且通常是可排序列表的推荐默认算法,因为它提供了比矩形相交算法更宽松的条件。
一般来说,最靠近中心的算法和最靠近角落的算法会产生相同的结果。然而,当构建 droppable 容器堆叠在一起的界面时,例如在构建看板时,最接近中心的算法有时会返回整个看板列的下层 droppable 区域,而不是该列中的 droppable 区域。
在这些情况下,最接近角的算法是首选,它将产生与人眼预测更一致的结果。
指针内碰撞检测算法
顾名思义,指针内碰撞检测算法只在指针包含在其他 droppable 容器的边界矩形内时才会记录碰撞。
这种碰撞检测算法非常适用于高精度的拖放界面。
顾名思义,这种碰撞检测算法只适用于基于指针的传感器。出于这个原因,如果你打算使用
pointerWithin
碰撞检测算法,我们建议你使用碰撞检测算法的组合,以便你可以回落到键盘传感器的不同碰撞检测算法。
自定义碰撞检测算法
在高级用例中,如果开箱即用的碰撞检测算法不适合你,你可能想建立自己的碰撞检测算法。
你可以从头开始写一个新的碰撞检测算法,或者将两个或更多现有的碰撞检测算法组合起来。
现有算法的组合
有时,你不需要从头开始建立自定义的碰撞检测算法。相反,你可以组合现有的碰撞算法来增强它们。
一个常见的例子是使用 pointerWithin
碰撞检测算法时。正如其名称所示,这种碰撞检测算法依赖于指针坐标,因此在使用其他传感器(如键盘传感器)时不起作用。这也是一个非常高精度的碰撞检测算法,所以当 pointerWithin
算法没有返回任何碰撞时,回落到一个更宽容的碰撞检测算法有时会有帮助。
import { pointerWithin, rectIntersection } from '@dnd-kit/core'
function customCollisionDetectionAlgorithm(args) {
// First, let's see if there are any collisions with the pointer
const pointerCollisions = pointerWithin(args)
// Collision detection algorithms return an array of collisions
if (pointerCollisions.length > 0) {
return pointerCollisions
}
// If there are no collisions with the pointer, return rectangle intersections
return rectIntersection(args)
}
另一个对现有算法进行组合的例子是,如果你想让你的一些 droppable 容器具有与其他容器不同的碰撞检测算法,那么你就可以使用这种算法。
例如,如果你正在建立一个也支持将物品移到垃圾桶的可分类列表,你可能想把最接近中心(closestCenter)和矩形相交(rectangleIntersection)的碰撞检测算法都组合起来。
从实现的角度来看,上面的例子中描述的自定义相交算法看起来像。
import { closestCorners, rectIntersection } from '@dnd-kit/core'
function customCollisionDetectionAlgorithm({ droppableContainers, ...args }) {
// First, let's see if the `trash` droppable rect is intersecting
const rectIntersectionCollisions = rectIntersection({
...args,
droppableContainers: droppableContainers.filter(({ id }) => id === 'trash'),
})
// Collision detection algorithms return an array of collisions
if (rectIntersectionCollisions.length > 0) {
// The trash is intersecting, return early
return rectIntersectionCollisions
}
// Compute other collisions
return closestCorners({
...args,
droppableContainers: droppableContainers.filter(({ id }) => id !== 'trash'),
})
}
构建自定义碰撞检测算法
对于高级用例或检测非矩形或非轴对齐的形状之间的碰撞,你会想建立自己的碰撞检测算法。
下面是一个检测圆形而不是矩形之间碰撞的例子。
/**
* Sort collisions in descending order (from greatest to smallest value)
*/
export function sortCollisionsDesc(
{data: {value: a}},
{data: {value: b}}
) {
return b - a;
}
function getCircleIntersection(entry, target) {
// Abstracted the logic to calculate the radius for simplicity
var circle1 = {radius: 20, x: entry.offsetLeft, y: entry.offsetTop};
var circle2 = {radius: 12, x: target.offsetLeft, y: target.offsetTop};
var dx = circle1.x - circle2.x;
var dy = circle1.y - circle2.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < circle1.radius + circle2.radius) {
return distance;
}
return 0;
}
/**
* Returns the circle that has the greatest intersection area
*/
function circleIntersection({
collisionRect,
droppableRects,
droppableContainers,
}) => {
const collisions = [];
for (const droppableContainer of droppableContainers) {
const {id} = droppableContainer;
const rect = droppableRects.get(id);
if (rect) {
const intersectionRatio = getCircleIntersection(rect, collisionRect);
if (intersectionRatio > 0) {
collisions.push({
id,
data: {droppableContainer, value: intersectionRatio},
});
}
}
}
return collisions.sort(sortCollisionsDesc);
};
要了解更多,请参考内置碰撞检测算法的实现。