mcpField
R
reanimated-dnd

reanimated-dnd

github.com/entropyconquers开发1.1k196 次安装32 周前更新
代码仓库

关于 reanimated-dnd

面向 React Native 开发者的拖放 UI 集成技能,提供组件和底层 hooks,用于拖放、列表与网格排序,并在 UI 线程运行动画。

主要功能

  1. 01拖放组件与区域
  2. 02纵向和横向可排序列表
  3. 03可插入或交换的网格排序
  4. 04轴向、边界和碰撞约束
  5. 05可测量动态高度的列表
  6. 06基于 Reanimated worklets 的动画

适用场景

  1. 01在界面区域间移动条目
  2. 02构建可重排的列表或网格
  3. 03限制拖动方向与范围
  4. 04处理不同高度的列表项

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/entropyconquers/react-native-reanimated-dnd --skill reanimated-dnd

安装后在 Claude Code 中输入/reanimated-dnd确认已加载。

详细介绍

react-native-reanimated-dnd 集成技能

版本: 2.0.0 类别: UI / 拖放 平台: React Native(需要 react-native-reanimated >=4.2.0、react-native-gesture-handler >=2.28.0、react-native-worklets >=0.7.0)


概述

react-native-reanimated-dnd 为 React Native 提供高性能拖放基础组件。它提供高层组件和底层钩子,用于:

  • 拖放:在放置区域之间移动项目
  • 可排序列表:可纵向和横向重新排序的列表
  • 可排序网格:支持插入或交换排序的二维网格
  • 约束:锁定轴向、限制拖动范围、碰撞检测
  • 动态高度:自动测量列表中高度可变的项目

所有动画都通过 Reanimated worklets 在 UI 线程上运行。


安装

npm install react-native-reanimated-dnd
# or
yarn add react-native-reanimated-dnd
对等依赖项(必须单独安装)
npm install react-native-reanimated react-native-gesture-handler react-native-worklets
必需的设置

使用 GestureHandlerRootView 包装应用根组件:

import { GestureHandlerRootView } from 'react-native-gesture-handler';

export default function App() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      {/* Your app content */}
    </GestureHandlerRootView>
  );
}

核心架构

DropProvider (context - required for Draggable/Droppable)
├── Draggable (items that can be picked up)
│   └── Draggable.Handle (optional restricted drag area)
└── Droppable (zones that accept drops)

Sortable (self-contained vertical/horizontal list)
├── SortableItem (individual reorderable item)
│   └── SortableItem.Handle (optional restricted drag area)

SortableGrid (self-contained 2D grid)
├── SortableGridItem (individual grid cell)
│   └── SortableGridItem.Handle (optional restricted drag area)

关键规则:所有数据项目都必须具有 id: string 属性,以便进行跟踪。


模式 1:基础拖放

使用 DropProvider + Draggable + Droppable 将项目移动到放置区域。

import {
  DropProvider,
  Draggable,
  Droppable,
} from 'react-native-reanimated-dnd';

function DragDropExample() {
  const [droppedItem, setDroppedItem] = useState<string | null>(null);

  return (
    <DropProvider>
      <View style={styles.items}>
        <Draggable data={{ id: '1', label: 'Item A' }}>
          <View style={styles.item}>
            <Text>Item A</Text>
          </View>
        </Draggable>

        <Draggable data={{ id: '2', label: 'Item B' }}>
          <View style={styles.item}>
            <Text>Item B</Text>
          </View>
        </Draggable>
      </View>

      <Droppable onDrop={(data) => setDroppedItem(data.label)}>
        <View style={styles.dropZone}>
          <Text>{droppedItem ?? 'Drop here'}</Text>
        </View>
      </Droppable>
    </DropProvider>
  );
}
Draggable 属性
属性类型默认值描述
dataTData必填传递给放置处理程序的数据载荷
draggableIdstring自动唯一标识符
dragDisabledbooleanfalse禁用拖动
preDragDelaynumber0拖动开始前的延迟时间,单位为毫秒
dragAxis"x" | "y" | "both""both"限制移动轴向
dragBoundsRefRefObject<View>-限制在某个视图内移动
collisionAlgorithm"center" | "intersect" | "contain""intersect"检测与可放置区域重叠的方式
animationFunction(toValue: number) => number-自定义返回动画
onDragStart(data: TData) => void-拖动开始时调用
onDragEnd(data: TData) => void-拖动结束时调用
onDragging({ x, y, tx, ty, itemData }) => void-实时更新位置
onStateChange(state: DraggableState) => void-状态转换回调
Droppable 属性
属性类型默认值描述
onDrop(data: TData) => void必填处理放置的项目
dropDisabledbooleanfalse禁用放置
capacitynumber1允许的最大项目数
dropAlignmentDropAlignment"center"放置项目的位置对齐方式
dropOffset{ x: number, y: number }-对齐后微调位置
activeStyleStyleProp<ViewStyle>-项目悬停时应用的样式
onActiveChange(isActive: boolean) => void-悬停状态变化时调用
droppableIdstring自动放置区域的唯一标识符
DropProvider 属性
属性类型描述
onDroppedItemsUpdate(items: DroppedItemsMap) => void跟踪所有区域中的项目
onDragging({ x, y, tx, ty, itemData }) => void全局拖动位置跟踪
onDragStart(data) => void任意拖动开始时调用
onDragEnd(data) => void任意拖动结束时调用
onLayoutUpdateComplete() => void布局更新完成时调用
DropAlignment 的取值

"center" | "top-left" | "top-center" | "top-right" | "center-left" | "center-right" | "bottom-left" | "bottom-center" | "bottom-right"


模式 2:拖动手柄

使用 Draggable.Handle 将可拖动区域限制为特定的手柄区域:

<Draggable data={{ id: '1', label: 'Card' }}>
  <View style={styles.card}>
    <Draggable.Handle>
      <View style={styles.handleBar}>
        <Text>Drag here</Text>
      </View>
    </Draggable.Handle>
    <View style={styles.cardContent}>
      <Text>This area does NOT initiate drag</Text>
    </View>
  </View>
</Draggable>

可排序项目也可以使用 SortableItem.Handle 采用相同模式。


模式 3:纵向可排序列表

使用 Sortable 组件创建可重新排序的列表:

import { Sortable, SortableItem } from 'react-native-reanimated-dnd';

interface Item {
  id: string;
  title: string;
}

const ITEM_HEIGHT = 60;

function SortableListExample() {
  const [items, setItems] = useState<Item[]>([
    { id: '1', title: 'First' },
    { id: '2', title: 'Second' },
    { id: '3', title: 'Third' },
  ]);

  const renderItem = useCallback(({ item, ...props }) => (
    <SortableItem
      key={item.id}
      id={item.id}
      data={item}
      onMove={(id, from, to) => {
        setItems(prev => {
          const next = [...prev];
          const [moved] = next.splice(from, 1);
          next.splice(to, 0, moved);
          return next;
        });
      }}
      {...props}
    >
      <View style={styles.listItem}>
        <Text>{item.title}</Text>
      </View>
    </SortableItem>
  ), []);

  return (
    <Sortable
      data={items}
      renderItem={renderItem}
      itemHeight={ITEM_HEIGHT}
    />
  );
}
Sortable 属性
属性类型默认值描述
dataTData[]必填项目(每项都必须包含 id: string)
renderItem(props) => ReactNode必填项目渲染器
direction"vertical" | "horizontal""vertical"列表方向
itemHeightnumber | number[] | (item, i) => number-项目高度(垂直布局时必填)
itemWidthnumber-项目宽度(水平布局时必填)
gapnumber0项目之间的间距(仅适用于水平布局)
paddingHorizontalnumber0容器内边距(仅适用于水平布局)
enableDynamicHeightsbooleanfalse自动测量项目高度
estimatedItemHeightnumber60未测量项目的备用高度
onHeightsMeasured(heights) => void-测量出高度后调用
useFlatListbooleantrue使用 FlatList 实现虚拟化
itemKeyExtractor(item, index) => stringitem.id自定义键提取函数
styleStyleProp<ViewStyle>-ScrollView/FlatList 样式
contentContainerStyleStyleProp<ViewStyle>-内容容器样式
SortableItem 属性
属性类型描述
idstring唯一项目标识符
dataT项目数据
onMove(id, from, to) => void位置变化时调用
onDragStart(id, position) => void开始拖动时调用
onDrop(id, position, allPositions?) => void放下项目时调用
onDragging(id, overItemId, yPosition) => void实时拖动位置
styleStyleProp<ViewStyle>容器样式
animatedStyleStyleProp<ViewStyle>动画样式

模式 4:水平可排序列表

import {
  Sortable,
  SortableItem,
  SortableDirection,
} from 'react-native-reanimated-dnd';

const ITEM_WIDTH = 120;

function HorizontalSortableExample() {
  const [items, setItems] = useState([
    { id: '1', label: 'Tag A' },
    { id: '2', label: 'Tag B' },
    { id: '3', label: 'Tag C' },
  ]);

  const renderItem = useCallback(({ item, ...props }) => (
    <SortableItem
      key={item.id}
      id={item.id}
      data={item}
      onMove={(id, from, to) => {
        setItems(prev => {
          const next = [...prev];
          const [moved] = next.splice(from, 1);
          next.splice(to, 0, moved);
          return next;
        });
      }}
      {...props}
    >
      <View style={styles.tag}>
        <Text>{item.label}</Text>
      </View>
    </SortableItem>
  ), []);

  return (
    <Sortable
      data={items}
      renderItem={renderItem}
      direction={SortableDirection.Horizontal}
      itemWidth={ITEM_WIDTH}
      gap={12}
      paddingHorizontal={12}
    />
  );
}

模式 5:可排序网格

import {
  SortableGrid,
  SortableGridItem,
  GridOrientation,
  GridStrategy,
} from 'react-native-reanimated-dnd';

function GridExample() {
  const [items, setItems] = useState([
    { id: '1', label: 'A' },
    { id: '2', label: 'B' },
    { id: '3', label: 'C' },
    { id: '4', label: 'D' },
    { id: '5', label: 'E' },
    { id: '6', label: 'F' },
  ]);

  const renderItem = useCallback(({ item, ...props }) => (
    <SortableGridItem
      key={item.id}
      id={item.id}
      data={item}
      onMove={(id, from, to) => {
        setItems(prev => {
          const next = [...prev];
          const [moved] = next.splice(from, 1);
          next.splice(to, 0, moved);
          return next;
        });
      }}
      {...props}
    >
      <View style={styles.gridCell}>
        <Text>{item.label}</Text>
      </View>
    </SortableGridItem>
  ), []);

  return (
    <SortableGrid
      data={items}
      renderItem={renderItem}
      dimensions={{
        columns: 3,
        itemWidth: 100,
        itemHeight: 100,
        rowGap: 8,
        columnGap: 8,
      }}
      orientation={GridOrientation.Vertical}
      strategy={GridStrategy.Insert}
    />
  );
}
SortableGrid 属性
属性类型默认值描述
dataTData[]必填网格项目(每项都必须包含 id: string)
renderItem(props) => ReactNode必填项目渲染器
dimensionsGridDimensions必填网格配置(见下文)
orientation"vertical" | "horizontal""vertical"网格方向
strategy"insert" | "swap""insert"项目重新排序方式:插入会移动其他项目,交换会互换两个项目
scrollEnabledbooleantrue启用滚动
itemKeyExtractor(item, index) => stringitem.id自定义键提取函数
styleStyleProp<ViewStyle>-ScrollView 样式
contentContainerStyleStyleProp<ViewStyle>-内容样式
GridDimensions
属性类型描述
columnsnumber列数(垂直方向)
rowsnumber行数(水平方向)
itemWidthnumber项目宽度
itemHeightnumber项目高度
rowGapnumber行间距
columnGapnumber列间距
SortableGridItem 属性
属性类型描述
idstring唯一标识符
dataT项目数据
activationDelaynumber开始拖动前的延迟时间(毫秒)
onMove(id, from, to) => void位置变化时调用
onDragStart(id, position) => void开始拖动时调用
onDrop(id, position, allPositions?) => void放下项目时调用
onDragging(id, overItemId, x, y) => void实时位置
isBeingRemovedboolean触发移除动画
styleStyleProp<ViewStyle>容器样式
animatedStyleStyleProp<ViewStyle>动画样式

模式 6:动态高度

适用于项目高度不一的列表:

<Sortable
  data={items}
  renderItem={renderItem}
  enableDynamicHeights
  estimatedItemHeight={80}
  onHeightsMeasured={(heights) => {
    // { [id]: measuredHeight }
  }}
/>

或者为每个项目指定明确的高度:

<Sortable
  data={items}
  renderItem={renderItem}
  itemHeight={(item, index) => item.expanded ? 120 : 60}
/>

// Or as an array:
<Sortable
  data={items}
  renderItem={renderItem}
  itemHeight={[60, 80, 120, 60, 100]}
/>

模式 7:轴向约束

将拖动锁定在单一轴向:

// Horizontal only
<Draggable data={data} dragAxis="x">
  <View>{/* content */}</View>
</Draggable>

// Vertical only
<Draggable data={data} dragAxis="y">
  <View>{/* content */}</View>
</Draggable>

模式 8:限制拖动范围

将拖动限制在容器内:

function BoundedExample() {
  const boundsRef = useRef<View>(null);

  return (
    <DropProvider>
      <View ref={boundsRef} style={styles.boundary}>
        <Draggable data={{ id: '1' }} dragBoundsRef={boundsRef}>
          <View style={styles.item}>
            <Text>Cannot escape boundary</Text>
          </View>
        </Draggable>
      </View>
    </DropProvider>
  );
}

结合轴向限制:

<Draggable data={data} dragBoundsRef={boundsRef} dragAxis="y">
  {/* Vertical movement only, within bounds */}
</Draggable>

模式 9:碰撞检测

三种算法控制可拖动项何时“激活”可放置区:

// Default: any overlap triggers
<Draggable data={data} collisionAlgorithm="intersect">

// Center point must be over the droppable
<Draggable data={data} collisionAlgorithm="center">

// Entire draggable must be inside the droppable
<Draggable data={data} collisionAlgorithm="contain">

模式 10:放置区容量

限制放置区可接受的项目数量:

<Droppable onDrop={handleDrop} capacity={1}>
  {/* Accepts exactly one item */}
</Droppable>

<Droppable onDrop={handleDrop} capacity={3}>
  {/* Accepts up to three items */}
</Droppable>

<Droppable onDrop={handleDrop} capacity={Infinity}>
  {/* Unlimited */}
</Droppable>

模式 11:跨放置区跟踪项目

使用 DropProvider.onDroppedItemsUpdate 跟踪每个放置区中的项目:

<DropProvider
  onDroppedItemsUpdate={(droppedItems) => {
    // droppedItems is: { [draggableId]: { droppableId, data } }
    setMapping(droppedItems);
  }}
>
  {/* Draggables and Droppables */}
</DropProvider>

通过 ref 以编程方式访问:

const providerRef = useRef<DropProviderRef>(null);

<DropProvider ref={providerRef}>
  {/* ... */}
</DropProvider>

// Later:
const items = providerRef.current?.getDroppedItems();
providerRef.current?.requestPositionUpdate();

模式 12:放置区的激活样式

当项目悬停在放置区上方时提供视觉反馈:

<Droppable
  onDrop={handleDrop}
  activeStyle={{
    borderColor: 'blue',
    borderWidth: 2,
    backgroundColor: 'rgba(0, 0, 255, 0.1)',
  }}
  onActiveChange={(isActive) => {
    // true when item is hovering, false when it leaves
  }}
>
  <View style={styles.zone}>
    <Text>Drop Zone</Text>
  </View>
</Droppable>

模式 13:自定义动画

为项目弹回或落位时提供自定义动画:

import { withSpring, withTiming, Easing } from 'react-native-reanimated';

// Spring animation
<Draggable
  data={data}
  animationFunction={(toValue) =>
    withSpring(toValue, { damping: 15, stiffness: 150 })
  }
>

// Timing animation with easing
<Draggable
  data={data}
  animationFunction={(toValue) =>
    withTiming(toValue, { duration: 300, easing: Easing.bezier(0.25, 0.1, 0.25, 1) })
  }
>

模式 14:拖动状态跟踪

监控拖动生命周期:

import { DraggableState } from 'react-native-reanimated-dnd';

<Draggable
  data={data}
  onStateChange={(state) => {
    // DraggableState.IDLE | DraggableState.DRAGGING | DraggableState.DROPPED
  }}
  onDragStart={(data) => { /* drag began */ }}
  onDragEnd={(data) => { /* drag ended */ }}
  onDragging={({ x, y, tx, ty, itemData }) => {
    // real-time position while dragging
  }}
>

Hooks API(底层)

如需完全控制,请使用 hooks,而不是组件:

useDraggable
import { useDraggable } from 'react-native-reanimated-dnd';
import { GestureDetector } from 'react-native-gesture-handler';
import Animated from 'react-native-reanimated';

function CustomDraggable({ data }) {
  const {
    animatedViewProps,  // { style, onLayout }
    gesture,            // pan gesture
    state,              // DraggableState
  } = useDraggable({
    data,
    onDragStart: (d) => {},
    onDragEnd: (d) => {},
    collisionAlgorithm: 'intersect',
  });

  return (
    <GestureDetector gesture={gesture}>
      <Animated.View {...animatedViewProps}>
        <Text>Custom Draggable</Text>
      </Animated.View>
    </GestureDetector>
  );
}
useDroppable
import { useDroppable } from 'react-native-reanimated-dnd';

function CustomDropZone() {
  const {
    viewProps,   // { onLayout, style? }
    isActive,    // boolean
  } = useDroppable({
    onDrop: (data) => {},
    capacity: 1,
    activeStyle: { borderColor: 'green' },
  });

  return (
    <Animated.View {...viewProps}>
      <Text>{isActive ? 'Release to drop' : 'Drop here'}</Text>
    </Animated.View>
  );
}
useSortableList + useSortable

用于自定义可排序列表实现:

import { useSortableList, useSortable } from 'react-native-reanimated-dnd';
import { GestureDetector } from 'react-native-gesture-handler';
import Animated from 'react-native-reanimated';

function CustomSortableList({ data }) {
  const {
    positions,
    scrollY,
    autoScroll,
    scrollViewRef,
    handleScroll,
    handleScrollEnd,
    contentHeight,
    getItemProps,
  } = useSortableList({ data, itemHeight: 60 });

  return (
    <Animated.ScrollView
      ref={scrollViewRef}
      onScroll={handleScroll}
      onMomentumScrollEnd={handleScrollEnd}
      style={{ height: 400 }}
      contentContainerStyle={{ height: contentHeight }}
    >
      {data.map((item, index) => (
        <CustomSortableItem
          key={item.id}
          item={item}
          {...getItemProps(item, index)}
        />
      ))}
    </Animated.ScrollView>
  );
}

function CustomSortableItem({ item, ...sortableProps }) {
  const {
    animatedStyle,
    panGestureHandler,
  } = useSortable({
    ...sortableProps,
    onMove: (id, from, to) => {},
  });

  return (
    <GestureDetector gesture={panGestureHandler}>
      <Animated.View style={animatedStyle}>
        <Text>{item.title}</Text>
      </Animated.View>
    </GestureDetector>
  );
}
useHorizontalSortableList + useHorizontalSortable

与垂直列表使用相同的模式,但适用于水平列表:

import {
  useHorizontalSortableList,
  useHorizontalSortable,
} from 'react-native-reanimated-dnd';

const {
  positions,
  scrollX,
  autoScroll,
  scrollViewRef,
  handleScroll,
  handleScrollEnd,
  contentWidth,
  getItemProps,
} = useHorizontalSortableList({
  data,
  itemWidth: 120,
  gap: 12,
  paddingHorizontal: 12,
});
useGridSortableList + useGridSortable

用于自定义网格实现:

import {
  useGridSortableList,
  useGridSortable,
  GridOrientation,
  GridStrategy,
} from 'react-native-reanimated-dnd';

const {
  positions,
  scrollY,
  scrollX,
  autoScrollDirection,
  scrollViewRef,
  handleScroll,
  handleScrollEnd,
  contentWidth,
  contentHeight,
  getItemProps,
} = useGridSortableList({
  data,
  dimensions: { columns: 3, itemWidth: 100, itemHeight: 100 },
  orientation: GridOrientation.Vertical,
  strategy: GridStrategy.Insert,
});

类型参考

// All sortable data must have an id
interface SortableData {
  id: string;
}

// Drag state enum
enum DraggableState {
  IDLE = "IDLE",
  DRAGGING = "DRAGGING",
  DROPPED = "DROPPED",
}

// Scroll directions
enum ScrollDirection { None, Up, Down }
enum HorizontalScrollDirection { None, Left, Right }

// List direction
enum SortableDirection { Vertical = "vertical", Horizontal = "horizontal" }

// Grid types
enum GridOrientation { Vertical = "vertical", Horizontal = "horizontal" }
enum GridStrategy { Insert = "insert", Swap = "swap" }
enum GridScrollDirection { None, Up, Down, Left, Right, UpLeft, UpRight, DownLeft, DownRight }

interface GridDimensions {
  columns?: number;
  rows?: number;
  itemWidth: number;
  itemHeight: number;
  rowGap?: number;
  columnGap?: number;
}

interface GridPosition {
  index: number;
  row: number;
  column: number;
  x: number;
  y: number;
}

// Collision detection
type CollisionAlgorithm = "center" | "intersect" | "contain";

// Drop alignment
type DropAlignment =
  | "center" | "top-left" | "top-center" | "top-right"
  | "center-left" | "center-right"
  | "bottom-left" | "bottom-center" | "bottom-right";

// Dropped items tracking
interface DroppedItemsMap<TData = unknown> {
  [draggableId: string]: {
    droppableId: string;
    data: TData;
  };
}

// DropProvider imperative handle
interface DropProviderRef {
  requestPositionUpdate: () => void;
  getDroppedItems: () => DroppedItemsMap;
}

// Grid positions shared value type
interface GridPositions {
  [id: string]: GridPosition;
}

常用示例

带拖动手柄的可排序列表
<SortableItem id={item.id} data={item} {...props}>
  <View style={styles.row}>
    <SortableItem.Handle>
      <View style={styles.handle}>
        <Text>|||</Text>
      </View>
    </SortableItem.Handle>
    <Text style={styles.content}>{item.title}</Text>
  </View>
</SortableItem>
支持交换重排的网格
<SortableGrid
  data={items}
  renderItem={renderItem}
  dimensions={{ columns: 4, itemWidth: 80, itemHeight: 80 }}
  strategy={GridStrategy.Swap}
/>
带拖动手柄的网格项
<SortableGridItem id={item.id} data={item} {...props}>
  <View style={styles.cell}>
    <SortableGridItem.Handle>
      <View style={styles.handle}>
        <Text>Drag</Text>
      </View>
    </SortableGridItem.Handle>
    <Text>{item.label}</Text>
  </View>
</SortableGridItem>
带激活延迟的网格(防止意外拖动)
<SortableGridItem
  id={item.id}
  data={item}
  activationDelay={300}
  {...props}
>
可拖动项的拖动前延迟
<Draggable data={data} preDragDelay={200}>
  {/* Must hold 200ms before drag activates */}
</Draggable>
带偏移量的放置对齐
<Droppable
  onDrop={handleDrop}
  dropAlignment="top-left"
  dropOffset={{ x: 10, y: 10 }}
>
向可排序列表动态添加项目

项目必须始终具有唯一的 id 字段。只需更新状态数组:

const addItem = () => {
  setItems(prev => [...prev, { id: String(Date.now()), title: 'New Item' }]);
};
带动画地从网格中移除项目
<SortableGridItem isBeingRemoved={item.removing} {...props}>

注意事项与禁止事项

数据要求
  1. 每个项目都必须具有 id: string —— 缺少 ID 或 ID 为 undefined 会导致重排异常和静默失败。在开发模式下会显示 console.error;在生产环境中则会静默失效。
  2. ID 必须唯一 —— 重复的 ID 会导致项目共用位置。该库在内部将 id 映射到 index;重复 ID 会互相覆盖。
  3. 不要修改项目对象 —— 更新状态时始终创建新的数组或对象。该库会在手势闭包中捕获数据,因此修改原始对象会导致回调中的数据过时。
上下文与包装
  1. Draggable/Droppable 必须有 DropProvider 祖先 —— 如果没有,放置操作会静默失败(不会崩溃,生产环境也不会报错)。始终将 Draggable/Droppable 用法包裹在 <DropProvider> 中。
  2. 不要用 DropProvider 包裹 Sortable 或 SortableGrid —— 它们会创建自己的内部 DropProvider。嵌套 Provider 会导致碰撞检测异常。
  3. 不要用 GestureHandlerRootView 包裹 Sortable 或 SortableGrid —— 它们会在内部自行包裹。重复包裹会导致手势冲突。对于 Draggable/Droppable 模式,只在应用根节点使用 GestureHandlerRootView。
  4. 手柄组件必须是其父组件的直接子组件 —— Draggable.Handle 必须位于 Draggable 内部,SortableItem.Handle 必须位于 SortableItem 内部,SortableGridItem.Handle 必须位于 SortableGridItem 内部。在其他位置使用时,它们会渲染子项,但不具备拖动功能(开发模式下会显示警告)。
必需属性(缺失时会抛出错误)
  1. 垂直 Sortable 必须设置 itemHeight —— 可以传入固定数值、数组或函数,也可以设置 enableDynamicHeights={true}。缺少此属性会抛出错误。
  2. 水平 Sortable 必须设置 itemWidth —— 不支持动态宽度模式。必须使用固定数值。
  3. 网格尺寸必须包含 itemWidth + itemHeight + columns(垂直)或 rows(水平) —— 缺少其中任何一项都会抛出错误。
状态管理
  1. onMove 必须更新你的状态数组 — 该库只会在视觉上为位置添加动画效果。如果你不在 onMove 中重新排序 data 数组,视觉顺序就会与数据顺序不一致。
// CORRECT
onMove={(id, from, to) => {
  setItems(prev => {
    const next = [...prev];
    const [moved] = next.splice(from, 1);
    next.splice(to, 0, moved);
    return next;
  });
}}

// WRONG — visual reorder happens but data stays stale
onMove={(id, from, to) => {
  console.log('moved', id, from, to); // not updating state!
}}
  1. 不要在 onDragStart/onDragEnd 中从外部状态读取 data 属性 — 这些回调会在手势创建时捕获 data。如果数据对象在手势创建和回调触发之间发生变化,回调看到的就是旧版本。请改用传递给回调的 data 参数。

  2. Droppable 的默认容量是 1,而非无限 — 如果不设置 capacity,每个区域只能放置一个项目。要允许无限放置,请设置 capacity={Infinity}。

动画
  1. animationFunction 必须返回 Reanimated 动画值 — 它通过 worklet 在 UI 线程上运行。请始终返回 withSpring、withTiming 或其他 Reanimated 动画。返回普通数字会完全跳过动画(项目会瞬间跳到目标位置,不带过渡效果)。
// CORRECT — returns a Reanimated animation
animationFunction={(toValue) => withSpring(toValue, { damping: 15 })}

// CORRECT — timing with easing
animationFunction={(toValue) => withTiming(toValue, { duration: 200 })}

// BAD — returns plain number, item snaps with no animation
animationFunction={(toValue) => toValue}
  1. 默认动画是 withSpring(有弹性) — 默认情况下,项目会弹回原位。如果你想要更干脆的效果,请提供 withTiming:
animationFunction={(toValue) => withTiming(toValue, { duration: 200 })}
  1. Droppable 上的 activeStyle 不会应用动画 — 它会立即生效(不是过渡效果)。如果需要带动画的悬停反馈,请配合 Reanimated 的 useAnimatedStyle 使用 onActiveChange。
手势处理
  1. 可排序项目的长按激活时间硬编码为 200ms — 你无法为 SortableItem 或水平可排序项自定义此时间。只有 SortableGridItem 提供 activationDelay。这可以防止意外拖动,但也意味着可排序项目始终需要短暂按住才能拖动。

  2. Draggable 上的 preDragDelay={0} 会与 ScrollViews 冲突 — 延迟为 0 时,平移手势会立即激活并抢占滚动手势的触摸事件。如果 Draggable 位于 ScrollView 内,请使用 preDragDelay={100} 或更高的值。

  3. Droppable 区域重叠时,由先匹配者胜出 — 如果两个 Droppable 区域在空间上重叠,先注册的区域会胜出碰撞检测。这里没有“中心点最近”算法。请避免 Droppable 区域重叠。

  4. 如果 Draggable 大于 Droppable,contain 碰撞检测永远不会触发 — Draggable 必须完全位于 Droppable 内部。如果 Draggable 更大,请改用 "intersect" 或 "center"。

平台特定说明
  1. 拖动阴影仅适用于 iOS — 拖动项目时,该库会应用 shadowColor/shadowOpacity/shadowRadius 以提供视觉反馈。这些属性仅适用于 iOS。Android 上没有拖动阴影。如果需要 Android 端反馈,请通过 animatedStyle 应用你自己的 elevation。

  2. 在 Android 上,collapsable={false} 至关重要 — 该库会在内部为 Draggable/Droppable 视图设置此属性。如果你使用 hooks API(useDraggable/useDroppable)搭配自定义视图,则必须在你的 Animated.View 上设置 collapsable={false},否则 Android 会优化掉原生视图,导致测量结果为 0。

// When using hooks directly
<Animated.View collapsable={false} {...animatedViewProps}>
性能
  1. Sortable 会在数据数组变化时重新挂载整个列表 — Sortable 使用所有项目 ID 的哈希值作为 React key,因此任何数据变化(包括重新排序、添加和移除)都会强制完整重新挂载。这会重置滚动位置和所有动画状态。如果数据经常变化,可以考虑直接使用 hooks API(useSortableList + useSortable),以获得更多控制权。

  2. SortableGrid 不会在数据变化时重新挂载 — 与 Sortable 不同,SortableGrid 能更高效地处理数据变化,无需完整重新挂载。

  3. 保持 onDragging 处理函数轻量 — 它们每秒触发约 20 次(节流间隔为 50ms),并从 UI 线程桥接到 JS。在这些回调中进行繁重计算会导致卡顿。

  4. Sortable 的 backgroundColor: "white" 是硬编码的 — 内部 ScrollView/FlatList 的 backgroundColor: "white" 是硬编码的。对于深色模式,请通过 style 属性覆盖:

<Sortable
  data={items}
  renderItem={renderItem}
  itemHeight={60}
  style={{ backgroundColor: '#1a1a1a' }}
/>
动态高度
  1. 小于 1px 的动态高度变化会被忽略 — 该库会对高度进行取整,并忽略小于 1px 的变化,以防止无限重新渲染循环。亚像素高度调整会被丢弃。

  2. 初始动态高度只根据第一份数据快照计算一次 — 如果数据在初始渲染完成前发生变化,高度可能会过时,直到重新测量项目为止。


最佳实践

数据结构
// GOOD — simple, flat data with string IDs
const items = [
  { id: '1', title: 'Item 1' },
  { id: '2', title: 'Item 2' },
];

// GOOD — use stable unique IDs (not array indices)
const items = tasks.map(task => ({ ...task, id: task.uuid }));

// BAD — numeric IDs (must be strings)
const items = [{ id: 1, title: 'Item 1' }];

// BAD — using array index as ID (breaks on reorder)
const items = data.map((d, i) => ({ ...d, id: String(i) }));

// BAD — missing ID field
const items = [{ title: 'Item 1' }];
对 renderItem 使用记忆化

始终使用 useCallback 包装 renderItem,以避免不必要的重新渲染:

// GOOD
const renderItem = useCallback(({ item, ...props }) => (
  <SortableItem key={item.id} id={item.id} data={item} {...props}>
    <MyItemComponent item={item} />
  </SortableItem>
), []);

// BAD — creates new function on every render
const renderItem = ({ item, ...props }) => (
  <SortableItem key={item.id} id={item.id} data={item} {...props}>
    <MyItemComponent item={item} />
  </SortableItem>
);
展开 renderItem 属性

renderItem 回调会接收来自父级 Sortable/SortableGrid 的共享值和配置。始终将它们展开并传递给项目组件:

// GOOD — spread all props from renderItem
const renderItem = useCallback(({ item, ...props }) => (
  <SortableItem id={item.id} data={item} {...props}>
    {/* content */}
  </SortableItem>
), []);

// BAD — manually passing individual props (easy to miss required ones)
const renderItem = useCallback(({ item, positions, lowerBound }) => (
  <SortableItem id={item.id} data={item} positions={positions} lowerBound={lowerBound}>
    {/* missing autoScrollDirection, itemsCount, etc. */}
  </SortableItem>
), []);
对交互内容使用手柄

如果可排序项目包含按钮、输入框或其他交互元素,请使用手柄以避免拖动冲突:

// GOOD — only the handle initiates drag, buttons work normally
<SortableItem id={item.id} data={item} {...props}>
  <View style={styles.row}>
    <SortableItem.Handle>
      <View style={styles.dragHandle} />
    </SortableItem.Handle>
    <Text>{item.title}</Text>
    <Button onPress={onDelete} title="Delete" />
  </View>
</SortableItem>

// PROBLEMATIC — entire item is draggable, button taps may trigger drag
<SortableItem id={item.id} data={item} {...props}>
  <View style={styles.row}>
    <Text>{item.title}</Text>
    <Button onPress={onDelete} title="Delete" />
  </View>
</SortableItem>
网格激活延迟

对于项目可点击的网格,请使用 activationDelay 来区分点击和拖动:

<SortableGridItem
  id={item.id}
  data={item}
  activationDelay={250}
  {...props}
>
  <Pressable onPress={() => navigateTo(item)}>
    {/* Grid cell content */}
  </Pressable>
</SortableGridItem>
对复杂布局使用 Hooks API

如果需要完全控制容器(自定义滚动行为、嵌套列表、非标准布局),请使用 hooks,而不是组件 API:

// Hooks give you raw shared values and gesture handlers
const { positions, scrollY, autoScroll, scrollViewRef, handleScroll, contentHeight, getItemProps } =
  useSortableList({ data, itemHeight: 60 });

// You control the ScrollView entirely
<Animated.ScrollView
  ref={scrollViewRef}
  onScroll={handleScroll}
  contentContainerStyle={{ height: contentHeight }}
  // Add your own custom scroll props
  showsVerticalScrollIndicator={false}
  bounces={false}
>
  {data.map((item, index) => (
    <CustomItem key={item.id} item={item} {...getItemProps(item, index)} />
  ))}
</Animated.ScrollView>
避免嵌套 Sortable

请勿将 Sortable 嵌套在另一个 Sortable 或 SortableGrid 中。每个组件都会创建自己的 DropProvider 和 GestureHandlerRootView,嵌套的手势处理器会发生冲突。如果需要嵌套可重新排序的列表,请使用 hooks API,并共享单个手势根节点。

评论

支持 Markdown