reanimated-dnd

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

react-native-reanimated-dnd Integration Skill

react-native-reanimated-dnd 集成指南

Version: 2.0.0 Category: UI / Drag and Drop Platform: React Native (requires react-native-reanimated >=4.2.0, react-native-gesture-handler >=2.28.0, react-native-worklets >=0.7.0)

版本: 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)

Overview

概述

react-native-reanimated-dnd
provides performant drag-and-drop primitives for React Native. It offers both high-level components and low-level hooks for:
  • Drag & Drop: Move items between drop zones
  • Sortable Lists: Vertical and horizontal reorderable lists
  • Sortable Grids: 2D grids with insert or swap reordering
  • Constraints: Axis locking, bounded dragging, collision detection
  • Dynamic Heights: Auto-measuring variable-height items in lists
All animations run on the UI thread via Reanimated worklets.

react-native-reanimated-dnd
为React Native提供高性能的拖拽原语。它提供了高级组件和低级钩子,用于:
  • 拖拽功能:在不同放置区域间移动项目
  • 可排序列表:垂直和水平可重新排序的列表
  • 可排序网格:支持插入或交换排序的2D网格
  • 约束设置:轴锁定、边界拖拽、碰撞检测
  • 动态高度:自动测量列表中高度可变的项目
所有动画通过Reanimated工作线程在UI线程上运行。

Installation

安装

bash
npm install react-native-reanimated-dnd
bash
npm install react-native-reanimated-dnd

or

yarn add react-native-reanimated-dnd
undefined
yarn add react-native-reanimated-dnd
undefined

Peer dependencies (must be installed separately)

对等依赖(必须单独安装)

bash
npm install react-native-reanimated react-native-gesture-handler react-native-worklets
bash
npm install react-native-reanimated react-native-gesture-handler react-native-worklets

Required setup

必要配置

Wrap your app root with
GestureHandlerRootView
:
tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler';

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

GestureHandlerRootView
包裹你的应用根组件:
tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler';

export default function App() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      {/* 你的应用内容 */}
    </GestureHandlerRootView>
  );
}

Core Architecture

核心架构

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)
Key rule: All data items MUST have an
id: string
property for tracking.

DropProvider(上下文 - Draggable/Droppable 必需)
├── Draggable(可被拖拽的项目)
│   └── Draggable.Handle(可选的拖拽限制区域)
└── Droppable(接受放置的区域)

Sortable(独立的垂直/水平列表)
├── SortableItem(单个可重新排序的项目)
│   └── SortableItem.Handle(可选的拖拽限制区域)

SortableGrid(独立的2D网格)
├── SortableGridItem(单个网格单元)
│   └── SortableGridItem.Handle(可选的拖拽限制区域)
关键规则:所有数据项目必须包含
id: string
属性以用于跟踪。

Pattern 1: Basic Drag & Drop

模式1:基础拖拽功能

Use
DropProvider
+
Draggable
+
Droppable
to move items into drop zones.
tsx
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>
  );
}
使用
DropProvider
+
Draggable
+
Droppable
实现项目向放置区域的移动。
tsx
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 ?? '拖拽到此处'}</Text>
        </View>
      </Droppable>
    </DropProvider>
  );
}

Draggable Props

Draggable 属性

PropTypeDefaultDescription
data
TData
requiredPayload passed to drop handlers
draggableId
string
autoUnique identifier
dragDisabled
boolean
false
Disable dragging
preDragDelay
number
0
Delay in ms before drag starts
dragAxis
"x" | "y" | "both"
"both"
Constrain movement axis
dragBoundsRef
RefObject<View>
-Constrain within a view
collisionAlgorithm
"center" | "intersect" | "contain"
"intersect"
How to detect overlap with droppables
animationFunction
(toValue: number) => number
-Custom return animation
onDragStart
(data: TData) => void
-Called when drag begins
onDragEnd
(data: TData) => void
-Called when drag ends
onDragging
({ x, y, tx, ty, itemData }) => void
-Real-time position updates
onStateChange
(state: DraggableState) => void
-State transition callback
属性类型默认值描述
data
TData
必填传递给放置处理函数的负载数据
draggableId
string
自动生成唯一标识符
dragDisabled
boolean
false
禁用拖拽
preDragDelay
number
0
拖拽开始前的延迟(毫秒)
dragAxis
"x" | "y" | "both"
"both"
限制移动的轴
dragBoundsRef
RefObject<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 Props

Droppable 属性

PropTypeDefaultDescription
onDrop
(data: TData) => void
requiredHandle dropped items
dropDisabled
boolean
false
Disable dropping
capacity
number
1
Max items allowed
dropAlignment
DropAlignment
"center"
Position alignment for dropped items
dropOffset
{ x: number, y: number }
-Fine-tune position after alignment
activeStyle
StyleProp<ViewStyle>
-Style applied when item hovers over
onActiveChange
(isActive: boolean) => void
-Called when hover state changes
droppableId
string
autoUnique identifier for the drop zone
属性类型默认值描述
onDrop
(data: TData) => void
必填处理放置的项目
dropDisabled
boolean
false
禁用放置
capacity
number
1
允许放置的最大项目数
dropAlignment
DropAlignment
"center"
放置项目的位置对齐方式
dropOffset
{ x: number, y: number }
-对齐后微调位置
activeStyle
StyleProp<ViewStyle>
-项目悬停时应用的样式
onActiveChange
(isActive: boolean) => void
-悬停状态变化时触发
droppableId
string
自动生成放置区域的唯一标识符

DropProvider Props

DropProvider 属性

PropTypeDescription
onDroppedItemsUpdate
(items: DroppedItemsMap) => void
Track items across all zones
onDragging
({ x, y, tx, ty, itemData }) => void
Global drag position tracking
onDragStart
(data) => void
Any drag begins
onDragEnd
(data) => void
Any drag ends
onLayoutUpdateComplete
() => void
Called when layout updates finish
属性类型描述
onDroppedItemsUpdate
(items: DroppedItemsMap) => void
跟踪所有放置区域中的项目
onDragging
({ x, y, tx, ty, itemData }) => void
全局拖拽位置跟踪
onDragStart
(data) => void
任意拖拽开始时触发
onDragEnd
(data) => void
任意拖拽结束时触发
onLayoutUpdateComplete
() => void
布局更新完成时触发

DropAlignment values

DropAlignment 可选值

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

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

Pattern 2: Drag Handles

模式2:拖拽手柄

Restrict the draggable area to a specific handle region using
Draggable.Handle
:
tsx
<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>
The same pattern works for sortable items with
SortableItem.Handle
.

使用
Draggable.Handle
将拖拽区域限制为特定的手柄区域:
tsx
<Draggable data={{ id: '1', label: 'Card' }}>
  <View style={styles.card}>
    <Draggable.Handle>
      <View style={styles.handleBar}>
        <Text>拖拽此处</Text>
      </View>
    </Draggable.Handle>
    <View style={styles.cardContent}>
      <Text>此区域无法触发拖拽</Text>
    </View>
  </View>
</Draggable>
相同模式也适用于可排序项目的
SortableItem.Handle

Pattern 3: Vertical Sortable List

模式3:垂直可排序列表

Use the
Sortable
component for a reorderable list:
tsx
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
组件实现可重新排序的列表:
tsx
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: '第一项' },
    { id: '2', title: '第二项' },
    { id: '3', title: '第三项' },
  ]);

  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 Props

Sortable 属性

PropTypeDefaultDescription
data
TData[]
requiredItems (each must have
id: string
)
renderItem
(props) => ReactNode
requiredItem renderer
direction
"vertical" | "horizontal"
"vertical"
List direction
itemHeight
number | number[] | (item, i) => number
-Item height (required for vertical)
itemWidth
number
-Item width (required for horizontal)
gap
number
0
Gap between items (horizontal only)
paddingHorizontal
number
0
Container padding (horizontal only)
enableDynamicHeights
boolean
false
Auto-measure item heights
estimatedItemHeight
number
60
Fallback height for unmeasured items
onHeightsMeasured
(heights) => void
-Called with measured heights
useFlatList
boolean
true
Use FlatList for virtualization
itemKeyExtractor
(item, index) => string
item.id
Custom key extraction function
style
StyleProp<ViewStyle>
-ScrollView/FlatList style
contentContainerStyle
StyleProp<ViewStyle>
-Content container style
属性类型默认值描述
data
TData[]
必填项目列表(每个项目必须包含
id: string
renderItem
(props) => ReactNode
必填项目渲染函数
direction
"vertical" | "horizontal"
"vertical"
列表方向
itemHeight
number | number[] | (item, i) => number
-项目高度(垂直列表必填)
itemWidth
number
-项目宽度(水平列表必填)
gap
number
0
项目间的间距(仅水平列表)
paddingHorizontal
number
0
容器水平内边距(仅水平列表)
enableDynamicHeights
boolean
false
自动测量项目高度
estimatedItemHeight
number
60
未测量项目的 fallback 高度
onHeightsMeasured
(heights) => void
-获取测量后的高度时触发
useFlatList
boolean
true
使用FlatList实现虚拟化
itemKeyExtractor
(item, index) => string
item.id
自定义键提取函数
style
StyleProp<ViewStyle>
-ScrollView/FlatList样式
contentContainerStyle
StyleProp<ViewStyle>
-内容容器样式

SortableItem Props

SortableItem 属性

PropTypeDescription
id
string
Unique item identifier
data
T
Item data
onMove
(id, from, to) => void
Called on position change
onDragStart
(id, position) => void
Drag started
onDrop
(id, position, allPositions?) => void
Item dropped
onDragging
(id, overItemId, yPosition) => void
Real-time drag position
style
StyleProp<ViewStyle>
Container style
animatedStyle
StyleProp<ViewStyle>
Animated style

属性类型描述
id
string
项目唯一标识符
data
T
项目数据
onMove
(id, from, to) => void
位置变化时触发
onDragStart
(id, position) => void
拖拽开始时触发
onDrop
(id, position, allPositions?) => void
项目放置时触发
onDragging
(id, overItemId, yPosition) => void
拖拽时实时位置更新
style
StyleProp<ViewStyle>
容器样式
animatedStyle
StyleProp<ViewStyle>
动画样式

Pattern 4: Horizontal Sortable List

模式4:水平可排序列表

tsx
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}
    />
  );
}

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

const ITEM_WIDTH = 120;

function HorizontalSortableExample() {
  const [items, setItems] = useState([
    { id: '1', label: '标签A' },
    { id: '2', label: '标签B' },
    { id: '3', label: '标签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}
    />
  );
}

Pattern 5: Sortable Grid

模式5:可排序网格

tsx
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}
    />
  );
}
tsx
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 Props

SortableGrid 属性

PropTypeDefaultDescription
data
TData[]
requiredGrid items (each must have
id: string
)
renderItem
(props) => ReactNode
requiredItem renderer
dimensions
GridDimensions
requiredGrid configuration (see below)
orientation
"vertical" | "horizontal"
"vertical"
Grid orientation
strategy
"insert" | "swap"
"insert"
How items reorder: insert shifts others, swap exchanges two
scrollEnabled
boolean
true
Enable scrolling
itemKeyExtractor
(item, index) => string
item.id
Custom key extraction
style
StyleProp<ViewStyle>
-ScrollView style
contentContainerStyle
StyleProp<ViewStyle>
-Content style
属性类型默认值描述
data
TData[]
必填网格项目(每个项目必须包含
id: string
renderItem
(props) => ReactNode
必填项目渲染函数
dimensions
GridDimensions
必填网格配置(见下文)
orientation
"vertical" | "horizontal"
"vertical"
网格方向
strategy
"insert" | "swap"
"insert"
项目重排方式:insert会移动其他项目,swap会交换两个项目
scrollEnabled
boolean
true
启用滚动
itemKeyExtractor
(item, index) => string
item.id
自定义键提取函数
style
StyleProp<ViewStyle>
-ScrollView样式
contentContainerStyle
StyleProp<ViewStyle>
-内容容器样式

GridDimensions

GridDimensions

PropTypeDescription
columns
number
Columns (vertical orientation)
rows
number
Rows (horizontal orientation)
itemWidth
number
Item width
itemHeight
number
Item height
rowGap
number
Gap between rows
columnGap
number
Gap between columns
属性类型描述
columns
number
列数(垂直方向)
rows
number
行数(水平方向)
itemWidth
number
项目宽度
itemHeight
number
项目高度
rowGap
number
行间距
columnGap
number
列间距

SortableGridItem Props

SortableGridItem 属性

PropTypeDescription
id
string
Unique identifier
data
T
Item data
activationDelay
number
Delay in ms before drag starts
onMove
(id, from, to) => void
Position changed
onDragStart
(id, position) => void
Drag started
onDrop
(id, position, allPositions?) => void
Item dropped
onDragging
(id, overItemId, x, y) => void
Real-time position
isBeingRemoved
boolean
Trigger removal animation
style
StyleProp<ViewStyle>
Container style
animatedStyle
StyleProp<ViewStyle>
Animated style

属性类型描述
id
string
唯一标识符
data
T
项目数据
activationDelay
number
拖拽开始前的延迟(毫秒)
onMove
(id, from, to) => void
位置变化时触发
onDragStart
(id, position) => void
拖拽开始时触发
onDrop
(id, position, allPositions?) => void
项目放置时触发
onDragging
(id, overItemId, x, y) => void
拖拽时实时位置更新
isBeingRemoved
boolean
触发移除动画
style
StyleProp<ViewStyle>
容器样式
animatedStyle
StyleProp<ViewStyle>
动画样式

Pattern 6: Dynamic Heights

模式6:动态高度

For lists where items have variable heights:
tsx
<Sortable
  data={items}
  renderItem={renderItem}
  enableDynamicHeights
  estimatedItemHeight={80}
  onHeightsMeasured={(heights) => {
    // { [id]: measuredHeight }
  }}
/>
Or provide explicit heights per item:
tsx
<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]}
/>

对于项目高度可变的列表:
tsx
<Sortable
  data={items}
  renderItem={renderItem}
  enableDynamicHeights
  estimatedItemHeight={80}
  onHeightsMeasured={(heights) => {
    // { [id]: measuredHeight }
  }}
/>
或者为每个项目指定明确的高度:
tsx
<Sortable
  data={items}
  renderItem={renderItem}
  itemHeight={(item, index) => item.expanded ? 120 : 60}
/>

// 或者传入数组:
<Sortable
  data={items}
  renderItem={renderItem}
  itemHeight={[60, 80, 120, 60, 100]}
/>

Pattern 7: Axis Constraints

模式7:轴约束

Lock dragging to a single axis:
tsx
// Horizontal only
<Draggable data={data} dragAxis="x">
  <View>{/* content */}</View>
</Draggable>

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

将拖拽限制为单个轴:
tsx
// 仅水平方向
<Draggable data={data} dragAxis="x">
  <View>{/* 内容 */}</View>
</Draggable>

// 仅垂直方向
<Draggable data={data} dragAxis="y">
  <View>{/* 内容 */}</View>
</Draggable>

Pattern 8: Bounded Dragging

模式8:边界拖拽

Constrain dragging within a container:
tsx
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>
  );
}
Combine with axis constraints:
tsx
<Draggable data={data} dragBoundsRef={boundsRef} dragAxis="y">
  {/* Vertical movement only, within bounds */}
</Draggable>

将拖拽限制在容器内:
tsx
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>无法拖出边界</Text>
          </View>
        </Draggable>
      </View>
    </DropProvider>
  );
}
结合轴约束:
tsx
<Draggable data={data} dragBoundsRef={boundsRef} dragAxis="y">
  {/* 仅垂直移动,且限制在边界内 */}
</Draggable>

Pattern 9: Collision Detection

模式9:碰撞检测

Three algorithms control when a draggable "activates" a droppable:
tsx
// 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">

三种算法控制可拖拽项目何时“激活”放置区域:
tsx
// 默认:有重叠即触发
<Draggable data={data} collisionAlgorithm="intersect">

// 中心点必须在放置区域上方
<Draggable data={data} collisionAlgorithm="center">

// 整个可拖拽项目必须在放置区域内
<Draggable data={data} collisionAlgorithm="contain">

Pattern 10: Drop Zone Capacity

模式10:放置区域容量

Limit how many items a zone accepts:
tsx
<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>

限制区域可接受的项目数量:
tsx
<Droppable onDrop={handleDrop} capacity={1}>
  {/* 仅接受一个项目 */}
</Droppable>

<Droppable onDrop={handleDrop} capacity={3}>
  {/* 最多接受三个项目 */}
</Droppable>

<Droppable onDrop={handleDrop} capacity={Infinity}>
  {/* 无限制 */}
</Droppable>

Pattern 11: Tracking Items Across Zones

模式11:跨区域跟踪项目

Use
DropProvider.onDroppedItemsUpdate
to track which items are in which zones:
tsx
<DropProvider
  onDroppedItemsUpdate={(droppedItems) => {
    // droppedItems is: { [draggableId]: { droppableId, data } }
    setMapping(droppedItems);
  }}
>
  {/* Draggables and Droppables */}
</DropProvider>
Access programmatically via ref:
tsx
const providerRef = useRef<DropProviderRef>(null);

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

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

使用
DropProvider.onDroppedItemsUpdate
跟踪哪些项目在哪个区域:
tsx
<DropProvider
  onDroppedItemsUpdate={(droppedItems) => {
    // droppedItems 格式:{ [draggableId]: { droppableId, data } }
    setMapping(droppedItems);
  }}
>
  {/* Draggable 和 Droppable 组件 */}
</DropProvider>
通过ref以编程方式访问:
tsx
const providerRef = useRef<DropProviderRef>(null);

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

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

Pattern 12: Active Styles on Drop Zones

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

Visual feedback when an item hovers over a drop zone:
tsx
<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>

当项目悬停在放置区域时提供视觉反馈:
tsx
<Droppable
  onDrop={handleDrop}
  activeStyle={{
    borderColor: 'blue',
    borderWidth: 2,
    backgroundColor: 'rgba(0, 0, 255, 0.1)',
  }}
  onActiveChange={(isActive) => {
    // 项目悬停时为true,离开时为false
  }}
>
  <View style={styles.zone}>
    <Text>放置区域</Text>
  </View>
</Droppable>

Pattern 13: Custom Animation

模式13:自定义动画

Provide a custom animation for when items snap back or into place:
tsx
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) })
  }
>

为项目回弹或归位提供自定义动画:
tsx
import { withSpring, withTiming, Easing } from 'react-native-reanimated';

// 弹簧动画
<Draggable
  data={data}
  animationFunction={(toValue) =>
    withSpring(toValue, { damping: 15, stiffness: 150 })
  }
>

// 带缓动的计时动画
<Draggable
  data={data}
  animationFunction={(toValue) =>
    withTiming(toValue, { duration: 300, easing: Easing.bezier(0.25, 0.1, 0.25, 1) })
  }
>

Pattern 14: Drag State Tracking

模式14:拖拽状态跟踪

Monitor drag lifecycle:
tsx
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
  }}
>

监控拖拽生命周期:
tsx
import { DraggableState } from 'react-native-reanimated-dnd';

<Draggable
  data={data}
  onStateChange={(state) => {
    // DraggableState.IDLE | DraggableState.DRAGGING | DraggableState.DROPPED
  }}
  onDragStart={(data) => { /* 拖拽开始 */ }}
  onDragEnd={(data) => { /* 拖拽结束 */ }}
  onDragging={({ x, y, tx, ty, itemData }) => {
    // 拖拽时的实时位置
  }}
>

Hooks API (Low-Level)

Hooks API(低级)

For full control, use hooks instead of components:
如需完全控制,可使用钩子而非组件:

useDraggable

useDraggable

tsx
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>
  );
}
tsx
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,            // 平移手势
    state,              // DraggableState
  } = useDraggable({
    data,
    onDragStart: (d) => {},
    onDragEnd: (d) => {},
    collisionAlgorithm: 'intersect',
  });

  return (
    <GestureDetector gesture={gesture}>
      <Animated.View {...animatedViewProps}>
        <Text>自定义可拖拽组件</Text>
      </Animated.View>
    </GestureDetector>
  );
}

useDroppable

useDroppable

tsx
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>
  );
}
tsx
import { useDroppable } from 'react-native-reanimated-dnd';

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

  return (
    <Animated.View {...viewProps}>
      <Text>{isActive ? '释放以放置' : '拖拽到此处'}</Text>
    </Animated.View>
  );
}

useSortableList + useSortable

useSortableList + useSortable

For custom sortable list implementations:
tsx
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>
  );
}
用于自定义可排序列表实现:
tsx
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

useHorizontalSortableList + useHorizontalSortable

Same pattern as vertical, but for horizontal lists:
tsx
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,
});
与垂直列表模式相同,但适用于水平列表:
tsx
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

useGridSortableList + useGridSortable

For custom grid implementations:
tsx
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,
});

用于自定义网格实现:
tsx
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,
});

Type Reference

类型参考

typescript
// 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;
}

typescript
// 所有可排序数据必须包含id
interface SortableData {
  id: string;
}

// 拖拽状态枚举
enum DraggableState {
  IDLE = "IDLE",
  DRAGGING = "DRAGGING",
  DROPPED = "DROPPED",
}

// 滚动方向
enum ScrollDirection { None, Up, Down }
enum HorizontalScrollDirection { None, Left, Right }

// 列表方向
enum SortableDirection { Vertical = "vertical", Horizontal = "horizontal" }

// 网格类型
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;
}

// 碰撞检测
type CollisionAlgorithm = "center" | "intersect" | "contain";

// 放置对齐方式
type DropAlignment =
  | "center" | "top-left" | "top-center" | "top-right"
  | "center-left" | "center-right"
  | "bottom-left" | "bottom-center" | "bottom-right";

// 已放置项目跟踪
interface DroppedItemsMap<TData = unknown> {
  [draggableId: string]: {
    droppableId: string;
    data: TData;
  };
}

// DropProvider 命令式句柄
interface DropProviderRef {
  requestPositionUpdate: () => void;
  getDroppedItems: () => DroppedItemsMap;
}

// 网格位置共享值类型
interface GridPositions {
  [id: string]: GridPosition;
}

Common Recipes

常见用法示例

Sortable list with handles

带手柄的可排序列表

tsx
<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>
tsx
<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>

Grid with swap reordering

交换排序的网格

tsx
<SortableGrid
  data={items}
  renderItem={renderItem}
  dimensions={{ columns: 4, itemWidth: 80, itemHeight: 80 }}
  strategy={GridStrategy.Swap}
/>
tsx
<SortableGrid
  data={items}
  renderItem={renderItem}
  dimensions={{ columns: 4, itemWidth: 80, itemHeight: 80 }}
  strategy={GridStrategy.Swap}
/>

Grid item with drag handle

带拖拽手柄的网格项目

tsx
<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>
tsx
<SortableGridItem id={item.id} data={item} {...props}>
  <View style={styles.cell}>
    <SortableGridItem.Handle>
      <View style={styles.handle}>
        <Text>拖拽</Text>
      </View>
    </SortableGridItem.Handle>
    <Text>{item.label}</Text>
  </View>
</SortableGridItem>

Grid with activation delay (prevent accidental drags)

带激活延迟的网格(防止误拖拽)

tsx
<SortableGridItem
  id={item.id}
  data={item}
  activationDelay={300}
  {...props}
>
tsx
<SortableGridItem
  id={item.id}
  data={item}
  activationDelay={300}
  {...props}
>

Pre-drag delay on draggable

可拖拽项目的预拖拽延迟

tsx
<Draggable data={data} preDragDelay={200}>
  {/* Must hold 200ms before drag activates */}
</Draggable>
tsx
<Draggable data={data} preDragDelay={200}>
  {/* 必须按住200ms才会触发拖拽 */}
</Draggable>

Drop alignment with offset

带偏移的放置对齐

tsx
<Droppable
  onDrop={handleDrop}
  dropAlignment="top-left"
  dropOffset={{ x: 10, y: 10 }}
>
tsx
<Droppable
  onDrop={handleDrop}
  dropAlignment="top-left"
  dropOffset={{ x: 10, y: 10 }}
>

Dynamically adding items to a sortable list

动态添加项目到可排序列表

Items must always have unique
id
fields. Just update the state array:
tsx
const addItem = () => {
  setItems(prev => [...prev, { id: String(Date.now()), title: 'New Item' }]);
};
项目必须始终具有唯一的
id
字段。只需更新状态数组即可:
tsx
const addItem = () => {
  setItems(prev => [...prev, { id: String(Date.now()), title: '新项目' }]);
};

Removing items from a grid with animation

带动画的网格项目移除

tsx
<SortableGridItem isBeingRemoved={item.removing} {...props}>

tsx
<SortableGridItem isBeingRemoved={item.removing} {...props}>

Gotchas & What NOT To Do

注意事项与禁忌

Data Requirements

数据要求

  1. Every item MUST have
    id: string
    — Missing or undefined IDs cause broken reordering and silent failures. In dev mode you get a
    console.error
    ; in production it silently breaks.
  2. IDs MUST be unique — Duplicate IDs cause items to share positions. The library maps
    id -> index
    internally; duplicates overwrite each other.
  3. Do NOT mutate item objects — Always create new arrays/objects when updating state. The library captures data in gesture closures, so mutating the original object leads to stale data in callbacks.
  1. 每个项目必须包含
    id: string
    — 缺少或未定义ID会导致排序失败和静默错误。开发模式下会有
    console.error
    提示;生产环境中会静默失效。
  2. ID必须唯一 — 重复ID会导致项目共享位置。库内部通过
    id -> index
    映射,重复ID会相互覆盖。
  3. 不要修改项目对象 — 更新状态时始终创建新数组/对象。库会在手势闭包中捕获数据,修改原始对象会导致回调中使用过时数据。

Context & Wrapping

上下文与包裹

  1. Draggable/Droppable REQUIRE a DropProvider ancestor — Without it, drops silently fail (no crash, no error in production). Always wrap Draggable/Droppable usage in a
    <DropProvider>
    .
  2. Do NOT wrap Sortable or SortableGrid in DropProvider — They create their own internal DropProvider. Nesting providers causes broken collision detection.
  3. Do NOT wrap Sortable or SortableGrid in GestureHandlerRootView — They wrap themselves internally. Double-wrapping causes gesture conflicts. Only use
    GestureHandlerRootView
    at the app root for Draggable/Droppable patterns.
  4. Handle components MUST be direct descendants of their parent
    Draggable.Handle
    must be inside
    Draggable
    ,
    SortableItem.Handle
    inside
    SortableItem
    ,
    SortableGridItem.Handle
    inside
    SortableGridItem
    . Used outside, they render children but have no drag functionality (with a dev-mode warning).
  1. Draggable/Droppable必须有DropProvider祖先组件 — 没有的话,放置操作会静默失败(不会崩溃,生产环境中无错误提示)。使用Draggable/Droppable时始终用
    <DropProvider>
    包裹。
  2. 不要在DropProvider中包裹Sortable或SortableGrid — 它们会创建自己的内部DropProvider。嵌套提供商会导致碰撞检测失效。
  3. 不要在GestureHandlerRootView中包裹Sortable或SortableGrid — 它们内部已自行包裹。双重包裹会导致手势冲突。仅在应用根节点为Draggable/Droppable模式使用
    GestureHandlerRootView
  4. 手柄组件必须是父组件的直接子元素
    Draggable.Handle
    必须在
    Draggable
    内部,
    SortableItem.Handle
    必须在
    SortableItem
    内部,
    SortableGridItem.Handle
    必须在
    SortableGridItem
    内部。在外部使用时,只会渲染子元素但无拖拽功能(开发模式下会有警告)。

Required Props (Will Throw if Missing)

必填属性(缺失会抛出错误)

  1. itemHeight
    is required for vertical Sortable
    — Either pass a fixed number, an array, a function, or set
    enableDynamicHeights={true}
    . Missing this throws an error.
  2. itemWidth
    is required for horizontal Sortable
    — No dynamic width mode exists. Must be a fixed number.
  3. Grid dimensions must include
    itemWidth
    +
    itemHeight
    +
    columns
    (vertical) or
    rows
    (horizontal)
    — Missing any of these throws an error.
  1. 垂直Sortable必须设置
    itemHeight
    — 可以传入固定数值、数组、函数,或设置
    enableDynamicHeights={true}
    。缺失该属性会抛出错误。
  2. 水平Sortable必须设置
    itemWidth
    — 没有动态宽度模式,必须传入固定数值。
  3. 网格尺寸必须包含
    itemWidth
    +
    itemHeight
    +
    columns
    (垂直方向)或
    rows
    (水平方向)
    — 缺失任何一项会抛出错误。

State Management

状态管理

  1. onMove
    MUST update your state array
    — The library only animates positions visually. If you don't reorder your
    data
    array in
    onMove
    , the visual order and data order will diverge.
tsx
// 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. Do NOT read
    data
    props inside
    onDragStart
    /
    onDragEnd
    from external state
    — These callbacks capture
    data
    at gesture creation time. If the data object changes between gesture creation and callback firing, the callback sees the stale version. Use the
    data
    argument passed to the callback instead.
  2. Default Droppable capacity is 1, NOT infinite — If you don't set
    capacity
    , only one item can be dropped per zone. Set
    capacity={Infinity}
    for unlimited.
  1. onMove
    必须更新状态数组
    — 库仅负责视觉上的位置动画。如果不在
    onMove
    中重新排序
    data
    数组,视觉顺序和数据顺序会不一致。
tsx
// 正确做法
onMove={(id, from, to) => {
  setItems(prev => {
    const next = [...prev];
    const [moved] = next.splice(from, 1);
    next.splice(to, 0, moved);
    return next;
  });
}}

// 错误做法 — 视觉上会重新排序,但数据会过时
onMove={(id, from, to) => {
  console.log('移动了', id, from, to); // 没有更新状态!
}}
  1. 不要在
    onDragStart
    /
    onDragEnd
    中从外部状态读取
    data
    属性
    — 这些回调会在手势创建时捕获
    data
    。如果在手势创建和回调触发之间数据对象发生变化,回调会使用过时版本。请使用回调传入的
    data
    参数。
  2. 默认Droppable容量为1,而非无限 — 如果不设置
    capacity
    ,每个区域只能放置一个项目。设置
    capacity={Infinity}
    以支持无限放置。

Animation

动画

  1. animationFunction
    MUST return a Reanimated animation value
    — It runs on the UI thread via worklets. Always return
    withSpring
    ,
    withTiming
    , or another Reanimated animation. Returning a plain number skips the animation entirely (the item snaps instantly with no transition).
tsx
// 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. Default animation is
    withSpring
    (bouncy)
    — Items bounce back to position by default. If you want a snappy feel, provide
    withTiming
    :
tsx
animationFunction={(toValue) => withTiming(toValue, { duration: 200 })}
  1. activeStyle
    on Droppable is NOT animated
    — It applies instantly (not a transition). If you need animated hover feedback, use
    onActiveChange
    with Reanimated's
    useAnimatedStyle
    instead.
  1. animationFunction
    必须返回Reanimated动画值
    — 它通过工作线程在UI线程上运行。始终返回
    withSpring
    withTiming
    或其他Reanimated动画。返回普通数值会跳过动画(项目会立即跳转,无过渡效果)。
tsx
// 正确做法 — 返回Reanimated动画
animationFunction={(toValue) => withSpring(toValue, { damping: 15 })}

// 正确做法 — 带缓动的计时动画
animationFunction={(toValue) => withTiming(toValue, { duration: 200 })}

// 错误做法 — 返回普通数值,项目会立即跳转无动画
animationFunction={(toValue) => toValue}
  1. 默认动画是
    withSpring
    (有弹跳效果)
    — 项目默认会弹回到位置。如果想要干脆的效果,请使用
    withTiming
tsx
animationFunction={(toValue) => withTiming(toValue, { duration: 200 })}
  1. Droppable的
    activeStyle
    不是动画样式
    — 它会立即应用(无过渡效果)。如果需要动画悬停反馈,请使用
    onActiveChange
    结合Reanimated的
    useAnimatedStyle

Gesture Handling

手势处理

  1. Sortable items have a hardcoded 200ms long-press activation — You cannot customize this for
    SortableItem
    or horizontal sortable. Only
    SortableGridItem
    exposes
    activationDelay
    . This prevents accidental drags but means sortable items always require a brief hold.
  2. preDragDelay={0}
    on Draggable conflicts with ScrollViews
    — With zero delay, the pan gesture activates immediately and steals touch from scroll gestures. Use
    preDragDelay={100}
    or higher if your Draggable is inside a ScrollView.
  3. Overlapping droppables: first-match wins — If two droppables overlap spatially, the one registered first wins collision detection. There is no "closest center" algorithm. Avoid overlapping droppables.
  4. contain
    collision never triggers if draggable is larger than droppable
    — The entire draggable must fit inside the droppable. If the draggable is bigger, use
    "intersect"
    or
    "center"
    instead.
  1. 可排序项目有硬编码的200ms长按激活时间 — 无法为
    SortableItem
    或水平可排序组件自定义该时间。只有
    SortableGridItem
    暴露了
    activationDelay
    属性。这可以防止误拖拽,但意味着可排序项目始终需要短暂按住才能拖拽。
  2. Draggable的
    preDragDelay={0}
    与ScrollView冲突
    — 延迟为0时,平移手势会立即激活并抢占滚动手势的触摸事件。如果Draggable在ScrollView内部,请使用
    preDragDelay={100}
    或更高值。
  3. 重叠的放置区域:第一个匹配的获胜 — 如果两个放置区域在空间上重叠,先注册的区域会在碰撞检测中获胜。没有“最近中心点”算法。请避免重叠的放置区域。
  4. 如果可拖拽项目大于放置区域,
    contain
    碰撞算法永远不会触发
    — 整个可拖拽项目必须完全在放置区域内。如果可拖拽项目更大,请使用
    "intersect"
    "center"
    算法。

Platform-Specific

平台特定

  1. Drag shadow is iOS-only — When an item is being dragged, the library applies
    shadowColor
    /
    shadowOpacity
    /
    shadowRadius
    for visual feedback. These are iOS-only properties. On Android, there is no drag shadow. If you need Android feedback, apply your own
    elevation
    via
    animatedStyle
    .
  2. collapsable={false}
    is critical on Android
    — The library sets this internally on Draggable/Droppable views. If you use the hooks API (
    useDraggable
    /
    useDroppable
    ) with custom views, you MUST set
    collapsable={false}
    on your
    Animated.View
    , or Android will optimize away the native view and measurements return zeros.
tsx
// When using hooks directly
<Animated.View collapsable={false} {...animatedViewProps}>
  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。
tsx
// 直接使用hooks时
<Animated.View collapsable={false} {...animatedViewProps}>

Performance

性能

  1. Sortable remounts the entire list when the data array changes
    Sortable
    uses a hash of all item IDs as a React key, forcing a full remount on any data change (including reorders, additions, and removals). This resets scroll position and all animation state. For frequent data changes, consider using the hooks API (
    useSortableList
    +
    useSortable
    ) directly for more control.
  2. SortableGrid
    does NOT remount on data changes
    — Unlike Sortable, SortableGrid handles data changes more efficiently without full remounts.
  3. Keep
    onDragging
    handlers lightweight
    — They fire ~20 times/second (50ms throttle) and bridge from the UI thread to JS. Heavy computation in these callbacks causes jank.
  4. Sortable has a hardcoded
    backgroundColor: "white"
    — The internal ScrollView/FlatList has
    backgroundColor: "white"
    hardcoded. For dark mode, override it via the
    style
    prop:
tsx
<Sortable
  data={items}
  renderItem={renderItem}
  itemHeight={60}
  style={{ backgroundColor: '#1a1a1a' }}
/>
  1. 当数据数组变化时,Sortable会重新挂载整个列表
    Sortable
    使用所有项目ID的哈希值作为React键,任何数据变化(包括排序、添加和移除)都会强制完全重新挂载。这会重置滚动位置和所有动画状态。对于频繁的数据变化,建议直接使用hooks API(
    useSortableList
    +
    useSortable
    )以获得更多控制。
  2. SortableGrid不会在数据变化时重新挂载 — 与Sortable不同,SortableGrid能更高效地处理数据变化,无需完全重新挂载。
  3. 保持
    onDragging
    处理函数轻量化
    — 它们每秒触发约20次(50ms节流),并从UI线程桥接到JS线程。这些回调中的繁重计算会导致卡顿。
  4. Sortable有硬编码的
    backgroundColor: "white"
    — 内部ScrollView/FlatList的
    backgroundColor
    被硬编码为白色。对于深色模式,请通过
    style
    属性覆盖:
tsx
<Sortable
  data={items}
  renderItem={renderItem}
  itemHeight={60}
  style={{ backgroundColor: '#1a1a1a' }}
/>

Dynamic Heights

动态高度

  1. Dynamic height changes under 1px are ignored — The library rounds heights and ignores changes smaller than 1px to prevent infinite re-render loops. Sub-pixel height adjustments are dropped.
  2. Initial dynamic heights are computed once from the first data snapshot — If data changes before the initial render completes, heights may be stale until items are re-measured.

  1. 小于1px的动态高度变化会被忽略 — 库会对高度取整,忽略小于1px的变化以防止无限重渲染循环。亚像素高度调整会被丢弃。
  2. 初始动态高度从第一个数据快照计算一次 — 如果在初始渲染完成前数据发生变化,高度可能会过时,直到项目被重新测量。

Best Practices

最佳实践

Data Structure

数据结构

tsx
// 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' }];
tsx
// 推荐 — 简单的扁平数据,带字符串ID
const items = [
  { id: '1', title: '项目1' },
  { id: '2', title: '项目2' },
];

// 推荐 — 使用稳定的唯一ID(不要用数组索引)
const items = tasks.map(task => ({ ...task, id: task.uuid }));

// 不推荐 — 数字ID(必须是字符串)
const items = [{ id: 1, title: '项目1' }];

// 不推荐 — 使用数组索引作为ID(排序后会失效)
const items = data.map((d, i) => ({ ...d, id: String(i) }));

// 不推荐 — 缺少ID字段
const items = [{ title: '项目1' }];

Memoize renderItem

记忆化renderItem

Always
useCallback
your
renderItem
to avoid unnecessary re-renders:
tsx
// 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>
);
始终使用
useCallback
包裹
renderItem
以避免不必要的重渲染:
tsx
// 推荐
const renderItem = useCallback(({ item, ...props }) => (
  <SortableItem key={item.id} id={item.id} data={item} {...props}>
    <MyItemComponent item={item} />
  </SortableItem>
), []);

// 不推荐 — 每次渲染都创建新函数
const renderItem = ({ item, ...props }) => (
  <SortableItem key={item.id} id={item.id} data={item} {...props}>
    <MyItemComponent item={item} />
  </SortableItem>
);

Spread renderItem props

展开renderItem属性

The
renderItem
callback receives shared values and configuration from the parent Sortable/SortableGrid. Always spread them onto the item component:
tsx
// 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>
), []);
renderItem
回调会从父Sortable/SortableGrid接收共享值和配置。始终将这些属性展开到项目组件上:
tsx
// 推荐 — 展开renderItem的所有属性
const renderItem = useCallback(({ item, ...props }) => (
  <SortableItem id={item.id} data={item} {...props}>
    {/* 内容 */}
  </SortableItem>
), []);

// 不推荐 — 手动传递单个属性(容易遗漏必填属性)
const renderItem = useCallback(({ item, positions, lowerBound }) => (
  <SortableItem id={item.id} data={item} positions={positions} lowerBound={lowerBound}>
    {/* 缺少autoScrollDirection、itemsCount等属性 */}
  </SortableItem>
), []);

Use Handles for Interactive Content

为交互式内容使用手柄

If your sortable items contain buttons, inputs, or other interactive elements, use handles to avoid drag conflicts:
tsx
// 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>
如果可排序项目包含按钮、输入框或其他交互式元素,请使用手柄以避免拖拽冲突:
tsx
// 推荐 — 只有手柄能触发拖拽,按钮可正常工作
<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="删除" />
  </View>
</SortableItem>

// 不推荐 — 整个项目都可拖拽,按钮点击可能触发拖拽
<SortableItem id={item.id} data={item} {...props}>
  <View style={styles.row}>
    <Text>{item.title}</Text>
    <Button onPress={onDelete} title="删除" />
  </View>
</SortableItem>

Grid Activation Delay

网格激活延迟

For grids where items are tappable, use
activationDelay
to distinguish taps from drags:
tsx
<SortableGridItem
  id={item.id}
  data={item}
  activationDelay={250}
  {...props}
>
  <Pressable onPress={() => navigateTo(item)}>
    {/* Grid cell content */}
  </Pressable>
</SortableGridItem>
对于项目可点击的网格,使用
activationDelay
区分点击和拖拽:
tsx
<SortableGridItem
  id={item.id}
  data={item}
  activationDelay={250}
  {...props}
>
  <Pressable onPress={() => navigateTo(item)}>
    {/* 网格单元内容 */}
  </Pressable>
</SortableGridItem>

Hooks API for Complex Layouts

复杂布局使用Hooks API

When you need full control over the container (custom scroll behavior, nested lists, non-standard layouts), use the hooks instead of the component API:
tsx
// 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>
当需要完全控制容器(自定义滚动行为、嵌套列表、非标准布局)时,使用hooks而非组件API:
tsx
// Hooks提供原始共享值和手势处理函数
const { positions, scrollY, autoScroll, scrollViewRef, handleScroll, contentHeight, getItemProps } =
  useSortableList({ data, itemHeight: 60 });

// 完全控制ScrollView
<Animated.ScrollView
  ref={scrollViewRef}
  onScroll={handleScroll}
  contentContainerStyle={{ height: contentHeight }}
  // 添加自定义滚动属性
  showsVerticalScrollIndicator={false}
  bounces={false}
>
  {data.map((item, index) => (
    <CustomItem key={item.id} item={item} {...getItemProps(item, index)} />
  ))}
</Animated.ScrollView>

Avoid Nesting Sortables

避免嵌套可排序组件

Do NOT nest
Sortable
inside another
Sortable
or
SortableGrid
. Each creates its own
DropProvider
and
GestureHandlerRootView
, and nested gesture handlers will conflict. If you need nested reorderable lists, use the hooks API with a single shared gesture root.
不要在另一个
Sortable
SortableGrid
中嵌套
Sortable
。每个组件都会创建自己的
DropProvider
GestureHandlerRootView
,嵌套手势处理函数会冲突。如果需要嵌套可排序列表,请使用hooks API并使用单个共享手势根组件。