reanimated-dnd
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesereact-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- 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- 拖拽功能:在不同放置区域间移动项目
- 可排序列表:垂直和水平可重新排序的列表
- 可排序网格:支持插入或交换排序的2D网格
- 约束设置:轴锁定、边界拖拽、碰撞检测
- 动态高度:自动测量列表中高度可变的项目
所有动画通过Reanimated工作线程在UI线程上运行。
Installation
安装
bash
npm install react-native-reanimated-dndbash
npm install react-native-reanimated-dndor
或
yarn add react-native-reanimated-dnd
undefinedyarn add react-native-reanimated-dnd
undefinedPeer dependencies (must be installed separately)
对等依赖(必须单独安装)
bash
npm install react-native-reanimated react-native-gesture-handler react-native-workletsbash
npm install react-native-reanimated react-native-gesture-handler react-native-workletsRequired setup
必要配置
Wrap your app root with :
GestureHandlerRootViewtsx
import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
{/* Your app content */}
</GestureHandlerRootView>
);
}用 包裹你的应用根组件:
GestureHandlerRootViewtsx
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 property for tracking.
id: stringDropProvider(上下文 - Draggable/Droppable 必需)
├── Draggable(可被拖拽的项目)
│ └── Draggable.Handle(可选的拖拽限制区域)
└── Droppable(接受放置的区域)
Sortable(独立的垂直/水平列表)
├── SortableItem(单个可重新排序的项目)
│ └── SortableItem.Handle(可选的拖拽限制区域)
SortableGrid(独立的2D网格)
├── SortableGridItem(单个网格单元)
│ └── SortableGridItem.Handle(可选的拖拽限制区域)关键规则:所有数据项目必须包含 属性以用于跟踪。
id: stringPattern 1: Basic Drag & Drop
模式1:基础拖拽功能
Use + + to move items into drop zones.
DropProviderDraggableDroppabletsx
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>
);
}使用 + + 实现项目向放置区域的移动。
DropProviderDraggableDroppabletsx
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 |
| | | How to detect overlap with droppables |
| | - | Custom return animation |
| | - | Called when drag begins |
| | - | Called when drag ends |
| | - | Real-time position updates |
| | - | State transition callback |
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| | 必填 | 传递给放置处理函数的负载数据 |
| | 自动生成 | 唯一标识符 |
| | | 禁用拖拽 |
| | | 拖拽开始前的延迟(毫秒) |
| | | 限制移动的轴 |
| | - | 限制拖拽在指定视图内 |
| | | 检测与放置区域重叠的算法 |
| | - | 自定义返回动画 |
| | - | 拖拽开始时触发 |
| | - | 拖拽结束时触发 |
| | - | 实时位置更新 |
| | - | 状态变化回调 |
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 |
| | - | Called when hover state changes |
| | auto | Unique identifier for the drop zone |
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| | 必填 | 处理放置的项目 |
| | | 禁用放置 |
| | | 允许放置的最大项目数 |
| | | 放置项目的位置对齐方式 |
| | - | 对齐后微调位置 |
| | - | 项目悬停时应用的样式 |
| | - | 悬停状态变化时触发 |
| | 自动生成 | 放置区域的唯一标识符 |
DropProvider Props
DropProvider 属性
| Prop | Type | Description |
|---|---|---|
| | Track items across all zones |
| | Global drag position tracking |
| | Any drag begins |
| | Any drag ends |
| | Called when layout updates finish |
| 属性 | 类型 | 描述 |
|---|---|---|
| | 跟踪所有放置区域中的项目 |
| | 全局拖拽位置跟踪 |
| | 任意拖拽开始时触发 |
| | 任意拖拽结束时触发 |
| | 布局更新完成时触发 |
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.Handletsx
<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.Handletsx
<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.HandlePattern 3: Vertical Sortable List
模式3:垂直可排序列表
Use the component for a reorderable list:
Sortabletsx
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}
/>
);
}使用 组件实现可重新排序的列表:
Sortabletsx
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 |
| | | List direction |
| | - | 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 |
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| | 必填 | 项目列表(每个项目必须包含 |
| | 必填 | 项目渲染函数 |
| | | 列表方向 |
| | - | 项目高度(垂直列表必填) |
| | - | 项目宽度(水平列表必填) |
| | | 项目间的间距(仅水平列表) |
| | | 容器水平内边距(仅水平列表) |
| | | 自动测量项目高度 |
| | | 未测量项目的 fallback 高度 |
| | - | 获取测量后的高度时触发 |
| | | 使用FlatList实现虚拟化 |
| | | 自定义键提取函数 |
| | - | ScrollView/FlatList样式 |
| | - | 内容容器样式 |
SortableItem Props
SortableItem 属性
| Prop | Type | Description |
|---|---|---|
| | Unique item identifier |
| | Item data |
| | Called on position change |
| | Drag started |
| | Item dropped |
| | Real-time drag position |
| | Container style |
| | Animated style |
| 属性 | 类型 | 描述 |
|---|---|---|
| | 项目唯一标识符 |
| | 项目数据 |
| | 位置变化时触发 |
| | 拖拽开始时触发 |
| | 项目放置时触发 |
| | 拖拽时实时位置更新 |
| | 容器样式 |
| | 动画样式 |
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) |
| | | Grid orientation |
| | | How items reorder: insert shifts others, swap exchanges two |
| | | Enable scrolling |
| | | Custom key extraction |
| | - | ScrollView style |
| | - | Content style |
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| | 必填 | 网格项目(每个项目必须包含 |
| | 必填 | 项目渲染函数 |
| | 必填 | 网格配置(见下文) |
| | | 网格方向 |
| | | 项目重排方式: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 |
| | Item dropped |
| | Real-time position |
| | Trigger removal animation |
| | Container style |
| | Animated style |
| 属性 | 类型 | 描述 |
|---|---|---|
| | 唯一标识符 |
| | 项目数据 |
| | 拖拽开始前的延迟(毫秒) |
| | 位置变化时触发 |
| | 拖拽开始时触发 |
| | 项目放置时触发 |
| | 拖拽时实时位置更新 |
| | 触发移除动画 |
| | 容器样式 |
| | 动画样式 |
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 to track which items are in which zones:
DropProvider.onDroppedItemsUpdatetsx
<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.onDroppedItemsUpdatetsx
<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 fields. Just update the state array:
idtsx
const addItem = () => {
setItems(prev => [...prev, { id: String(Date.now()), title: 'New Item' }]);
};项目必须始终具有唯一的字段。只需更新状态数组即可:
idtsx
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
数据要求
- Every item MUST have — Missing or undefined IDs cause broken reordering and silent failures. In dev mode you get a
id: string; in production it silently breaks.console.error - IDs MUST be unique — Duplicate IDs cause items to share positions. The library maps internally; duplicates overwrite each other.
id -> index - 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: string提示;生产环境中会静默失效。console.error - ID必须唯一 — 重复ID会导致项目共享位置。库内部通过映射,重复ID会相互覆盖。
id -> index - 不要修改项目对象 — 更新状态时始终创建新数组/对象。库会在手势闭包中捕获数据,修改原始对象会导致回调中使用过时数据。
Context & Wrapping
上下文与包裹
- 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> - 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.
GestureHandlerRootView - Handle components MUST be direct descendants of their parent — must be inside
Draggable.Handle,DraggableinsideSortableItem.Handle,SortableIteminsideSortableGridItem.Handle. Used outside, they render children but have no drag functionality (with a dev-mode warning).SortableGridItem
- Draggable/Droppable必须有DropProvider祖先组件 — 没有的话,放置操作会静默失败(不会崩溃,生产环境中无错误提示)。使用Draggable/Droppable时始终用包裹。
<DropProvider> - 不要在DropProvider中包裹Sortable或SortableGrid — 它们会创建自己的内部DropProvider。嵌套提供商会导致碰撞检测失效。
- 不要在GestureHandlerRootView中包裹Sortable或SortableGrid — 它们内部已自行包裹。双重包裹会导致手势冲突。仅在应用根节点为Draggable/Droppable模式使用。
GestureHandlerRootView - 手柄组件必须是父组件的直接子元素 — 必须在
Draggable.Handle内部,Draggable必须在SortableItem.Handle内部,SortableItem必须在SortableGridItem.Handle内部。在外部使用时,只会渲染子元素但无拖拽功能(开发模式下会有警告)。SortableGridItem
Required Props (Will Throw if Missing)
必填属性(缺失会抛出错误)
- is required for vertical Sortable — Either pass a fixed number, an array, a function, or set
itemHeight. Missing this throws an error.enableDynamicHeights={true} - is required for horizontal Sortable — No dynamic width mode exists. Must be a fixed number.
itemWidth - Grid dimensions must include +
itemWidth+itemHeight(vertical) orcolumns(horizontal) — Missing any of these throws an error.rows
- 垂直Sortable必须设置— 可以传入固定数值、数组、函数,或设置
itemHeight。缺失该属性会抛出错误。enableDynamicHeights={true} - 水平Sortable必须设置— 没有动态宽度模式,必须传入固定数值。
itemWidth - 网格尺寸必须包含+
itemWidth+itemHeight(垂直方向)或columns(水平方向) — 缺失任何一项会抛出错误。rows
State Management
状态管理
- MUST update your state array — The library only animates positions visually. If you don't reorder your
onMovearray indata, the visual order and data order will diverge.onMove
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 readprops inside
data/onDragStartfrom external state — These callbacks captureonDragEndat gesture creation time. If the data object changes between gesture creation and callback firing, the callback sees the stale version. Use thedataargument passed to the callback instead.data -
Default Droppable capacity is 1, NOT infinite — If you don't set, only one item can be dropped per zone. Set
capacityfor unlimited.capacity={Infinity}
- 必须更新状态数组 — 库仅负责视觉上的位置动画。如果不在
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); // 没有更新状态!
}}-
不要在/
onDragStart中从外部状态读取onDragEnd属性 — 这些回调会在手势创建时捕获data。如果在手势创建和回调触发之间数据对象发生变化,回调会使用过时版本。请使用回调传入的data参数。data -
默认Droppable容量为1,而非无限 — 如果不设置,每个区域只能放置一个项目。设置
capacity以支持无限放置。capacity={Infinity}
Animation
动画
- MUST return a Reanimated animation value — It runs on the UI thread via worklets. Always return
animationFunction,withSpring, or another Reanimated animation. Returning a plain number skips the animation entirely (the item snaps instantly with no transition).withTiming
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
withSpring:withTiming
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
activeStylewith Reanimated'sonActiveChangeinstead.useAnimatedStyle
- 必须返回Reanimated动画值 — 它通过工作线程在UI线程上运行。始终返回
animationFunction、withSpring或其他Reanimated动画。返回普通数值会跳过动画(项目会立即跳转,无过渡效果)。withTiming
tsx
// 正确做法 — 返回Reanimated动画
animationFunction={(toValue) => withSpring(toValue, { damping: 15 })}
// 正确做法 — 带缓动的计时动画
animationFunction={(toValue) => withTiming(toValue, { duration: 200 })}
// 错误做法 — 返回普通数值,项目会立即跳转无动画
animationFunction={(toValue) => toValue}- 默认动画是(有弹跳效果) — 项目默认会弹回到位置。如果想要干脆的效果,请使用
withSpring:withTiming
tsx
animationFunction={(toValue) => withTiming(toValue, { duration: 200 })}- Droppable的不是动画样式 — 它会立即应用(无过渡效果)。如果需要动画悬停反馈,请使用
activeStyle结合Reanimated的onActiveChange。useAnimatedStyle
Gesture Handling
手势处理
-
Sortable items have a hardcoded 200ms long-press activation — You cannot customize this foror horizontal sortable. Only
SortableItemexposesSortableGridItem. This prevents accidental drags but means sortable items always require a brief hold.activationDelay -
on Draggable conflicts with ScrollViews — With zero delay, the pan gesture activates immediately and steals touch from scroll gestures. Use
preDragDelay={0}or higher if your Draggable is inside a ScrollView.preDragDelay={100} -
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
containor"intersect"instead."center"
-
可排序项目有硬编码的200ms长按激活时间 — 无法为或水平可排序组件自定义该时间。只有
SortableItem暴露了SortableGridItem属性。这可以防止误拖拽,但意味着可排序项目始终需要短暂按住才能拖拽。activationDelay -
Draggable的与ScrollView冲突 — 延迟为0时,平移手势会立即激活并抢占滚动手势的触摸事件。如果Draggable在ScrollView内部,请使用
preDragDelay={0}或更高值。preDragDelay={100} -
重叠的放置区域:第一个匹配的获胜 — 如果两个放置区域在空间上重叠,先注册的区域会在碰撞检测中获胜。没有“最近中心点”算法。请避免重叠的放置区域。
-
如果可拖拽项目大于放置区域,碰撞算法永远不会触发 — 整个可拖拽项目必须完全在放置区域内。如果可拖拽项目更大,请使用
contain或"intersect"算法。"center"
Platform-Specific
平台特定
-
Drag shadow is iOS-only — When an item is being dragged, the library applies/
shadowColor/shadowOpacityfor visual feedback. These are iOS-only properties. On Android, there is no drag shadow. If you need Android feedback, apply your ownshadowRadiusviaelevation.animatedStyle -
is critical on Android — The library sets this internally on Draggable/Droppable views. If you use the hooks API (
collapsable={false}/useDraggable) with custom views, you MUST setuseDroppableon yourcollapsable={false}, or Android will optimize away the native view and measurements return zeros.Animated.View
tsx
// When using hooks directly
<Animated.View collapsable={false} {...animatedViewProps}>-
拖拽阴影仅在iOS上可用 — 当项目被拖拽时,库会应用/
shadowColor/shadowOpacity提供视觉反馈。这些是iOS专属属性。在Android上没有拖拽阴影。如果需要Android反馈,请通过shadowRadius应用自己的animatedStyle。elevation -
在Android上至关重要 — 库会在Draggable/Droppable视图内部设置该属性。如果使用hooks API(
collapsable={false}/useDraggable)结合自定义视图,必须在useDroppable上设置Animated.View,否则Android会优化掉原生视图,导致测量结果为0。collapsable={false}
tsx
// 直接使用hooks时
<Animated.View collapsable={false} {...animatedViewProps}>Performance
性能
-
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 (
Sortable+useSortableList) directly for more control.useSortable -
does NOT remount on data changes — Unlike Sortable, SortableGrid handles data changes more efficiently without full remounts.
SortableGrid -
Keephandlers lightweight — They fire ~20 times/second (50ms throttle) and bridge from the UI thread to JS. Heavy computation in these callbacks causes jank.
onDragging -
Sortable has a hardcoded— The internal ScrollView/FlatList has
backgroundColor: "white"hardcoded. For dark mode, override it via thebackgroundColor: "white"prop:style
tsx
<Sortable
data={items}
renderItem={renderItem}
itemHeight={60}
style={{ backgroundColor: '#1a1a1a' }}
/>-
当数据数组变化时,Sortable会重新挂载整个列表 —使用所有项目ID的哈希值作为React键,任何数据变化(包括排序、添加和移除)都会强制完全重新挂载。这会重置滚动位置和所有动画状态。对于频繁的数据变化,建议直接使用hooks API(
Sortable+useSortableList)以获得更多控制。useSortable -
SortableGrid不会在数据变化时重新挂载 — 与Sortable不同,SortableGrid能更高效地处理数据变化,无需完全重新挂载。
-
保持处理函数轻量化 — 它们每秒触发约20次(50ms节流),并从UI线程桥接到JS线程。这些回调中的繁重计算会导致卡顿。
onDragging -
Sortable有硬编码的— 内部ScrollView/FlatList的
backgroundColor: "white"被硬编码为白色。对于深色模式,请通过backgroundColor属性覆盖:style
tsx
<Sortable
data={items}
renderItem={renderItem}
itemHeight={60}
style={{ backgroundColor: '#1a1a1a' }}
/>Dynamic Heights
动态高度
-
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的变化以防止无限重渲染循环。亚像素高度调整会被丢弃。
-
初始动态高度从第一个数据快照计算一次 — 如果在初始渲染完成前数据发生变化,高度可能会过时,直到项目被重新测量。
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 your to avoid unnecessary re-renders:
useCallbackrenderItemtsx
// 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>
);始终使用包裹以避免不必要的重渲染:
useCallbackrenderItemtsx
// 推荐
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:
renderItemtsx
// 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>
), []);renderItemtsx
// 推荐 — 展开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:
activationDelaytsx
<SortableGridItem
id={item.id}
data={item}
activationDelay={250}
{...props}
>
<Pressable onPress={() => navigateTo(item)}>
{/* Grid cell content */}
</Pressable>
</SortableGridItem>对于项目可点击的网格,使用区分点击和拖拽:
activationDelaytsx
<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.
SortableSortableSortableGridDropProviderGestureHandlerRootView不要在另一个或中嵌套。每个组件都会创建自己的和,嵌套手势处理函数会冲突。如果需要嵌套可排序列表,请使用hooks API并使用单个共享手势根组件。
SortableSortableGridSortableDropProviderGestureHandlerRootView