【译】dnd-kit 中英文档对照-DndContext

1,994 阅读12分钟

本篇为 dnd-kit 官方文档DndContext 一章的对照翻译,如有错误,欢迎指正。

对于碰撞检测知之甚少,有关章节请大佬们指正。

原文章节目录跳转

DndContext

应用结构 Application structure

Context provider

为了让你的 「Droppable」「Draggable」 组件彼此互动,你需要确保在 React 组件树中使用它们的部分在同一个父 <DndContext> 组件中。 <DndContext> provider 利用 「React Context API」 在 draggable、droppable 组件和 hooks 之间共享数据。

React context 提供了一种通过组件树传递数据的方法,而不需要在每一级手动传递 props。

因此,使用 DraggableuseDroppableDragOverlay 的组件将需要嵌套在一个 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 时,请记住,useDroppableuseDraggable 只能访问该上下文中的其他 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 的基础上构建你自己的预设,你可能希望能够访问 useDraggableuseDroppable 所能访问的 <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);
};

要了解更多,请参考内置碰撞检测算法的实现。