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)
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线程上运行。
bash
npm install react-native-reanimated-dnd
bash
npm install react-native-reanimated-dnd
yarn add react-native-reanimated-dnd
yarn add react-native-reanimated-dnd
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
Wrap your app root with
:
tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
{/* Your app content */}
</GestureHandlerRootView>
);
}
tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
{/* 你的应用内容 */}
</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)
Key rule: All data items MUST have an
property for tracking.
DropProvider(上下文 - Draggable/Droppable 必需)
├── Draggable(可被拖拽的项目)
│ └── Draggable.Handle(可选的拖拽限制区域)
└── Droppable(接受放置的区域)
Sortable(独立的垂直/水平列表)
├── SortableItem(单个可重新排序的项目)
│ └── SortableItem.Handle(可选的拖拽限制区域)
SortableGrid(独立的2D网格)
├── SortableGridItem(单个网格单元)
│ └── SortableGridItem.Handle(可选的拖拽限制区域)
Pattern 1: Basic Drag & Drop
模式1:基础拖拽功能
Use
+
+
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>
);
}
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 属性
| Prop | Type | Default | Description |
|---|
| | required | Payload passed to drop handlers |
| | auto | Unique identifier |
| | | Disable dragging |
| | | Delay in ms before drag starts |
| | | Constrain movement axis |
| | - | Constrain within a view |
| "center" | "intersect" | "contain"
| | How to detect overlap with droppables |
| (toValue: number) => number
| - | Custom return animation |
| | - | Called when drag begins |
| | - | Called when drag ends |
| ({ x, y, tx, ty, itemData }) => void
| - | Real-time position updates |
| (state: DraggableState) => void
| - | State transition callback |
| 属性 | 类型 | 默认值 | 描述 |
|---|
| | 必填 | 传递给放置处理函数的负载数据 |
| | 自动生成 | 唯一标识符 |
| | | 禁用拖拽 |
| | | 拖拽开始前的延迟(毫秒) |
| | | 限制移动的轴 |
| | - | 限制拖拽在指定视图内 |
| "center" | "intersect" | "contain"
| | 检测与放置区域重叠的算法 |
| (toValue: number) => number
| - | 自定义返回动画 |
| | - | 拖拽开始时触发 |
| | - | 拖拽结束时触发 |
| ({ x, y, tx, ty, itemData }) => void
| - | 实时位置更新 |
| (state: DraggableState) => void
| - | 状态变化回调 |
Droppable Props
Droppable 属性
| Prop | Type | Default | Description |
|---|
| | required | Handle dropped items |
| | | Disable dropping |
| | | Max items allowed |
| | | Position alignment for dropped items |
| | - | Fine-tune position after alignment |
| | - | Style applied when item hovers over |
| (isActive: boolean) => void
| - | Called when hover state changes |
| | auto | Unique identifier for the drop zone |
| 属性 | 类型 | 默认值 | 描述 |
|---|
| | 必填 | 处理放置的项目 |
| | | 禁用放置 |
| | | 允许放置的最大项目数 |
| | | 放置项目的位置对齐方式 |
| | - | 对齐后微调位置 |
| | - | 项目悬停时应用的样式 |
| (isActive: boolean) => void
| - | 悬停状态变化时触发 |
| | 自动生成 | 放置区域的唯一标识符 |
DropProvider Props
DropProvider 属性
| Prop | Type | Description |
|---|
| (items: DroppedItemsMap) => void
| Track items across all zones |
| ({ x, y, tx, ty, itemData }) => void
| Global drag position tracking |
| | Any drag begins |
| | Any drag ends |
| | Called when layout updates finish |
| 属性 | 类型 | 描述 |
|---|
| (items: DroppedItemsMap) => void
| 跟踪所有放置区域中的项目 |
| ({ x, y, tx, ty, itemData }) => void
| 全局拖拽位置跟踪 |
| | 任意拖拽开始时触发 |
| | 任意拖拽结束时触发 |
| | 布局更新完成时触发 |
DropAlignment values
DropAlignment 可选值
Pattern 2: Drag Handles
模式2:拖拽手柄
Restrict the draggable area to a specific handle region using
:
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
.
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>
Pattern 3: Vertical Sortable List
模式3:垂直可排序列表
Use the
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}
/>
);
}
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 属性
| Prop | Type | Default | Description |
|---|
| | required | Items (each must have ) |
| | required | Item renderer |
| "vertical" | "horizontal"
| | List direction |
| number | number[] | (item, i) => number
| - | Item height (required for vertical) |
| | - | Item width (required for horizontal) |
| | | Gap between items (horizontal only) |
| | | Container padding (horizontal only) |
| | | Auto-measure item heights |
| | | Fallback height for unmeasured items |
| | - | Called with measured heights |
| | | Use FlatList for virtualization |
| | | Custom key extraction function |
| | - | ScrollView/FlatList style |
| | - | Content container style |
| 属性 | 类型 | 默认值 | 描述 |
|---|
| | 必填 | 项目列表(每个项目必须包含 ) |
| | 必填 | 项目渲染函数 |
| "vertical" | "horizontal"
| | 列表方向 |
| number | number[] | (item, i) => number
| - | 项目高度(垂直列表必填) |
| | - | 项目宽度(水平列表必填) |
| | | 项目间的间距(仅水平列表) |
| | | 容器水平内边距(仅水平列表) |
| | | 自动测量项目高度 |
| | | 未测量项目的 fallback 高度 |
| | - | 获取测量后的高度时触发 |
| | | 使用FlatList实现虚拟化 |
| | | 自定义键提取函数 |
| | - | ScrollView/FlatList样式 |
| | - | 内容容器样式 |
SortableItem Props
SortableItem 属性
| Prop | Type | Description |
|---|
| | Unique item identifier |
| | Item data |
| | Called on position change |
| | Drag started |
| (id, position, allPositions?) => void
| Item dropped |
| (id, overItemId, yPosition) => void
| Real-time drag position |
| | Container style |
| | Animated style |
| 属性 | 类型 | 描述 |
|---|
| | 项目唯一标识符 |
| | 项目数据 |
| | 位置变化时触发 |
| | 拖拽开始时触发 |
| (id, position, allPositions?) => void
| 项目放置时触发 |
| (id, overItemId, yPosition) => void
| 拖拽时实时位置更新 |
| | 容器样式 |
| | 动画样式 |
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 属性
| Prop | Type | Default | Description |
|---|
| | required | Grid items (each must have ) |
| | required | Item renderer |
| | required | Grid configuration (see below) |
| "vertical" | "horizontal"
| | Grid orientation |
| | | How items reorder: insert shifts others, swap exchanges two |
| | | Enable scrolling |
| | | Custom key extraction |
| | - | ScrollView style |
| | - | Content style |
| 属性 | 类型 | 默认值 | 描述 |
|---|
| | 必填 | 网格项目(每个项目必须包含 ) |
| | 必填 | 项目渲染函数 |
| | 必填 | 网格配置(见下文) |
| "vertical" | "horizontal"
| | 网格方向 |
| | | 项目重排方式:insert会移动其他项目,swap会交换两个项目 |
| | | 启用滚动 |
| | | 自定义键提取函数 |
| | - | ScrollView样式 |
| | - | 内容容器样式 |
GridDimensions
GridDimensions
| Prop | Type | Description |
|---|
| | Columns (vertical orientation) |
| | Rows (horizontal orientation) |
| | Item width |
| | Item height |
| | Gap between rows |
| | Gap between columns |
| 属性 | 类型 | 描述 |
|---|
| | 列数(垂直方向) |
| | 行数(水平方向) |
| | 项目宽度 |
| | 项目高度 |
| | 行间距 |
| | 列间距 |
SortableGridItem Props
SortableGridItem 属性
| Prop | Type | Description |
|---|
| | Unique identifier |
| | Item data |
| | Delay in ms before drag starts |
| | Position changed |
| | Drag started |
| (id, position, allPositions?) => void
| Item dropped |
| (id, overItemId, x, y) => void
| Real-time position |
| | Trigger removal animation |
| | Container style |
| | Animated style |
| 属性 | 类型 | 描述 |
|---|
| | 唯一标识符 |
| | 项目数据 |
| | 拖拽开始前的延迟(毫秒) |
| | 位置变化时触发 |
| | 拖拽开始时触发 |
| (id, position, allPositions?) => void
| 项目放置时触发 |
| (id, overItemId, x, y) => void
| 拖拽时实时位置更新 |
| | 触发移除动画 |
| | 容器样式 |
| | 动画样式 |
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:
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>
);
}
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,
});
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;
}
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
fields. Just update the state array:
tsx
const addItem = () => {
setItems(prev => [...prev, { id: String(Date.now()), title: 'New Item' }]);
};
项目必须始终具有唯一的
字段。只需更新状态数组即可:
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
注意事项与禁忌
- Every item MUST have — Missing or undefined IDs cause broken reordering and silent failures. In dev mode you get a ; in production it silently breaks.
- IDs MUST be unique — Duplicate IDs cause items to share positions. The library maps internally; duplicates overwrite each other.
- 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.
- 每个项目必须包含 — 缺少或未定义ID会导致排序失败和静默错误。开发模式下会有提示;生产环境中会静默失效。
- ID必须唯一 — 重复ID会导致项目共享位置。库内部通过映射,重复ID会相互覆盖。
- 不要修改项目对象 — 更新状态时始终创建新数组/对象。库会在手势闭包中捕获数据,修改原始对象会导致回调中使用过时数据。
- Draggable/Droppable REQUIRE a DropProvider ancestor — Without it, drops silently fail (no crash, no error in production). Always wrap Draggable/Droppable usage in a .
- Do NOT wrap Sortable or SortableGrid in DropProvider — They create their own internal DropProvider. Nesting providers causes broken collision detection.
- Do NOT wrap Sortable or SortableGrid in GestureHandlerRootView — They wrap themselves internally. Double-wrapping causes gesture conflicts. Only use at the app root for Draggable/Droppable patterns.
- Handle components MUST be direct descendants of their parent — must be inside , inside , inside . Used outside, they render children but have no drag functionality (with a dev-mode warning).
- Draggable/Droppable必须有DropProvider祖先组件 — 没有的话,放置操作会静默失败(不会崩溃,生产环境中无错误提示)。使用Draggable/Droppable时始终用包裹。
- 不要在DropProvider中包裹Sortable或SortableGrid — 它们会创建自己的内部DropProvider。嵌套提供商会导致碰撞检测失效。
- 不要在GestureHandlerRootView中包裹Sortable或SortableGrid — 它们内部已自行包裹。双重包裹会导致手势冲突。仅在应用根节点为Draggable/Droppable模式使用。
- 手柄组件必须是父组件的直接子元素 — 必须在内部,必须在内部,必须在内部。在外部使用时,只会渲染子元素但无拖拽功能(开发模式下会有警告)。
Required Props (Will Throw if Missing)
必填属性(缺失会抛出错误)
- is required for vertical Sortable — Either pass a fixed number, an array, a function, or set
enableDynamicHeights={true}
. Missing this throws an error.
- is required for horizontal Sortable — No dynamic width mode exists. Must be a fixed number.
- Grid dimensions must include + + (vertical) or (horizontal) — Missing any of these throws an error.
- 垂直Sortable必须设置 — 可以传入固定数值、数组、函数,或设置
enableDynamicHeights={true}
。缺失该属性会抛出错误。
- 水平Sortable必须设置 — 没有动态宽度模式,必须传入固定数值。
- 网格尺寸必须包含 + + (垂直方向)或(水平方向) — 缺失任何一项会抛出错误。
- MUST update your state array — The library only animates positions visually. If you don't reorder your array in , 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!
}}
-
Do NOT read props inside / from external state — These callbacks capture
at gesture creation time. If the data object changes between gesture creation and callback firing, the callback sees the stale version. Use the
argument passed to the callback instead.
-
Default Droppable capacity is 1, NOT infinite — If you don't set
, only one item can be dropped per zone. Set
for unlimited.
- 必须更新状态数组 — 库仅负责视觉上的位置动画。如果不在中重新排序数组,视觉顺序和数据顺序会不一致。
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); // 没有更新状态!
}}
-
不要在/中从外部状态读取属性 — 这些回调会在手势创建时捕获
。如果在手势创建和回调触发之间数据对象发生变化,回调会使用过时版本。请使用回调传入的
参数。
-
默认Droppable容量为1,而非无限 — 如果不设置
,每个区域只能放置一个项目。设置
以支持无限放置。
- MUST return a Reanimated animation value — It runs on the UI thread via worklets. Always return , , 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}
- Default animation is (bouncy) — Items bounce back to position by default. If you want a snappy feel, provide :
tsx
animationFunction={(toValue) => withTiming(toValue, { duration: 200 })}
- on Droppable is NOT animated — It applies instantly (not a transition). If you need animated hover feedback, use with Reanimated's instead.
- 必须返回Reanimated动画值 — 它通过工作线程在UI线程上运行。始终返回、或其他Reanimated动画。返回普通数值会跳过动画(项目会立即跳转,无过渡效果)。
tsx
// 正确做法 — 返回Reanimated动画
animationFunction={(toValue) => withSpring(toValue, { damping: 15 })}
// 正确做法 — 带缓动的计时动画
animationFunction={(toValue) => withTiming(toValue, { duration: 200 })}
// 错误做法 — 返回普通数值,项目会立即跳转无动画
animationFunction={(toValue) => toValue}
- 默认动画是(有弹跳效果) — 项目默认会弹回到位置。如果想要干脆的效果,请使用:
tsx
animationFunction={(toValue) => withTiming(toValue, { duration: 200 })}
- Droppable的不是动画样式 — 它会立即应用(无过渡效果)。如果需要动画悬停反馈,请使用结合Reanimated的。
-
Sortable items have a hardcoded 200ms long-press activation — You cannot customize this for
or horizontal sortable. Only
exposes
. This prevents accidental drags but means sortable items always require a brief hold.
-
on Draggable conflicts with ScrollViews — With zero delay, the pan gesture activates immediately and steals touch from scroll gestures. Use
or higher if your Draggable is inside a ScrollView.
-
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.
-
collision never triggers if draggable is larger than droppable — The entire draggable must fit inside the droppable. If the draggable is bigger, use
or
instead.
-
可排序项目有硬编码的200ms长按激活时间 — 无法为
或水平可排序组件自定义该时间。只有
暴露了
属性。这可以防止误拖拽,但意味着可排序项目始终需要短暂按住才能拖拽。
-
Draggable的与ScrollView冲突 — 延迟为0时,平移手势会立即激活并抢占滚动手势的触摸事件。如果Draggable在ScrollView内部,请使用
或更高值。
-
重叠的放置区域:第一个匹配的获胜 — 如果两个放置区域在空间上重叠,先注册的区域会在碰撞检测中获胜。没有“最近中心点”算法。请避免重叠的放置区域。
-
如果可拖拽项目大于放置区域,碰撞算法永远不会触发 — 整个可拖拽项目必须完全在放置区域内。如果可拖拽项目更大,请使用
或
算法。
-
Drag shadow is iOS-only — When an item is being dragged, the library applies
/
/
for visual feedback. These are iOS-only properties. On Android, there is no drag shadow. If you need Android feedback, apply your own
via
.
-
is critical on Android — The library sets this internally on Draggable/Droppable views. If you use the hooks API (
/
) with custom views, you MUST set
on your
, or Android will optimize away the native view and measurements return zeros.
tsx
// When using hooks directly
<Animated.View collapsable={false} {...animatedViewProps}>
-
拖拽阴影仅在iOS上可用 — 当项目被拖拽时,库会应用
/
/
提供视觉反馈。这些是iOS专属属性。在Android上没有拖拽阴影。如果需要Android反馈,请通过
应用自己的
。
-
在Android上至关重要 — 库会在Draggable/Droppable视图内部设置该属性。如果使用hooks API(
/
)结合自定义视图,必须在
上设置
,否则Android会优化掉原生视图,导致测量结果为0。
tsx
// 直接使用hooks时
<Animated.View collapsable={false} {...animatedViewProps}>
-
Sortable remounts the entire list when the data array changes —
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 (
+
) directly for more control.
-
does NOT remount on data changes — Unlike Sortable, SortableGrid handles data changes more efficiently without full remounts.
-
Keep handlers lightweight — They fire ~20 times/second (50ms throttle) and bridge from the UI thread to JS. Heavy computation in these callbacks causes jank.
-
Sortable has a hardcoded — The internal ScrollView/FlatList has
hardcoded. For dark mode, override it via the
prop:
tsx
<Sortable
data={items}
renderItem={renderItem}
itemHeight={60}
style={{ backgroundColor: '#1a1a1a' }}
/>
-
当数据数组变化时,Sortable会重新挂载整个列表 —
使用所有项目ID的哈希值作为React键,任何数据变化(包括排序、添加和移除)都会强制完全重新挂载。这会重置滚动位置和所有动画状态。对于频繁的数据变化,建议直接使用hooks API(
+
)以获得更多控制。
-
SortableGrid不会在数据变化时重新挂载 — 与Sortable不同,SortableGrid能更高效地处理数据变化,无需完全重新挂载。
-
保持处理函数轻量化 — 它们每秒触发约20次(50ms节流),并从UI线程桥接到JS线程。这些回调中的繁重计算会导致卡顿。
-
Sortable有硬编码的 — 内部ScrollView/FlatList的
被硬编码为白色。对于深色模式,请通过
属性覆盖:
tsx
<Sortable
data={items}
renderItem={renderItem}
itemHeight={60}
style={{ backgroundColor: '#1a1a1a' }}
/>
-
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.
-
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.
-
小于1px的动态高度变化会被忽略 — 库会对高度取整,忽略小于1px的变化以防止无限重渲染循环。亚像素高度调整会被丢弃。
-
初始动态高度从第一个数据快照计算一次 — 如果在初始渲染完成前数据发生变化,高度可能会过时,直到项目被重新测量。
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
your
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>
);
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
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>
), []);
回调会从父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
to distinguish taps from drags:
tsx
<SortableGridItem
id={item.id}
data={item}
activationDelay={250}
{...props}
>
<Pressable onPress={() => navigateTo(item)}>
{/* Grid cell content */}
</Pressable>
</SortableGridItem>
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
inside another
or
. Each creates its own
and
, and nested gesture handlers will conflict. If you need nested reorderable lists, use the hooks API with a single shared gesture root.
不要在另一个
或
中嵌套
。每个组件都会创建自己的
和
,嵌套手势处理函数会冲突。如果需要嵌套可排序列表,请使用hooks API并使用单个共享手势根组件。