关于 reanimated-dnd
面向 React Native 开发者的拖放 UI 集成技能,提供组件和底层 hooks,用于拖放、列表与网格排序,并在 UI 线程运行动画。
主要功能
- 01拖放组件与区域
- 02纵向和横向可排序列表
- 03可插入或交换的网格排序
- 04轴向、边界和碰撞约束
- 05可测量动态高度的列表
- 06基于 Reanimated worklets 的动画
适用场景
- 01在界面区域间移动条目
- 02构建可重排的列表或网格
- 03限制拖动方向与范围
- 04处理不同高度的列表项
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ entropyconquers/ react-native-reanimated-dnd --skill reanimated-dnd安装后在 Claude Code 中输入/reanimated-dnd确认已加载。
详细介绍
react-native-reanimated-dnd 集成技能
版本: 2.0.0 类别: UI / 拖放 平台: React Native(需要 react-native-reanimated >=4.2.0、react-native-gesture-handler >=2.28.0、react-native-worklets >=0.7.0)
概述
react-native-reanimated-dnd 为 React Native 提供高性能拖放基础组件。它提供高层组件和底层钩子,用于:
- 拖放:在放置区域之间移动项目
- 可排序列表:可纵向和横向重新排序的列表
- 可排序网格:支持插入或交换排序的二维网格
- 约束:锁定轴向、限制拖动范围、碰撞检测
- 动态高度:自动测量列表中高度可变的项目
所有动画都通过 Reanimated worklets 在 UI 线程上运行。
安装
npm install react-native-reanimated-dnd
# or
yarn add react-native-reanimated-dnd
对等依赖项(必须单独安装)
npm install react-native-reanimated react-native-gesture-handler react-native-worklets
必需的设置
使用 GestureHandlerRootView 包装应用根组件:
import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
{/* Your app content */}
</GestureHandlerRootView>
);
}
核心架构
DropProvider (context - required for Draggable/Droppable)
├── Draggable (items that can be picked up)
│ └── Draggable.Handle (optional restricted drag area)
└── Droppable (zones that accept drops)
Sortable (self-contained vertical/horizontal list)
├── SortableItem (individual reorderable item)
│ └── SortableItem.Handle (optional restricted drag area)
SortableGrid (self-contained 2D grid)
├── SortableGridItem (individual grid cell)
│ └── SortableGridItem.Handle (optional restricted drag area)
关键规则:所有数据项目都必须具有 id: string 属性,以便进行跟踪。
模式 1:基础拖放
使用 DropProvider + Draggable + Droppable 将项目移动到放置区域。
import {
DropProvider,
Draggable,
Droppable,
} from 'react-native-reanimated-dnd';
function DragDropExample() {
const [droppedItem, setDroppedItem] = useState<string | null>(null);
return (
<DropProvider>
<View style={styles.items}>
<Draggable data={{ id: '1', label: 'Item A' }}>
<View style={styles.item}>
<Text>Item A</Text>
</View>
</Draggable>
<Draggable data={{ id: '2', label: 'Item B' }}>
<View style={styles.item}>
<Text>Item B</Text>
</View>
</Draggable>
</View>
<Droppable onDrop={(data) => setDroppedItem(data.label)}>
<View style={styles.dropZone}>
<Text>{droppedItem ?? 'Drop here'}</Text>
</View>
</Droppable>
</DropProvider>
);
}
Draggable 属性
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
data | TData | 必填 | 传递给放置处理程序的数据载荷 |
draggableId | string | 自动 | 唯一标识符 |
dragDisabled | boolean | false | 禁用拖动 |
preDragDelay | number | 0 | 拖动开始前的延迟时间,单位为毫秒 |
dragAxis | "x" | "y" | "both" | "both" | 限制移动轴向 |
dragBoundsRef | RefObject<View> | - | 限制在某个视图内移动 |
collisionAlgorithm | "center" | "intersect" | "contain" | "intersect" | 检测与可放置区域重叠的方式 |
animationFunction | (toValue: number) => number | - | 自定义返回动画 |
onDragStart | (data: TData) => void | - | 拖动开始时调用 |
onDragEnd | (data: TData) => void | - | 拖动结束时调用 |
onDragging | ({ x, y, tx, ty, itemData }) => void | - | 实时更新位置 |
onStateChange | (state: DraggableState) => void | - | 状态转换回调 |
Droppable 属性
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
onDrop | (data: TData) => void | 必填 | 处理放置的项目 |
dropDisabled | boolean | false | 禁用放置 |
capacity | number | 1 | 允许的最大项目数 |
dropAlignment | DropAlignment | "center" | 放置项目的位置对齐方式 |
dropOffset | { x: number, y: number } | - | 对齐后微调位置 |
activeStyle | StyleProp<ViewStyle> | - | 项目悬停时应用的样式 |
onActiveChange | (isActive: boolean) => void | - | 悬停状态变化时调用 |
droppableId | string | 自动 | 放置区域的唯一标识符 |
DropProvider 属性
| 属性 | 类型 | 描述 |
|---|---|---|
onDroppedItemsUpdate | (items: DroppedItemsMap) => void | 跟踪所有区域中的项目 |
onDragging | ({ x, y, tx, ty, itemData }) => void | 全局拖动位置跟踪 |
onDragStart | (data) => void | 任意拖动开始时调用 |
onDragEnd | (data) => void | 任意拖动结束时调用 |
onLayoutUpdateComplete | () => void | 布局更新完成时调用 |
DropAlignment 的取值
"center" | "top-left" | "top-center" | "top-right" | "center-left" | "center-right" | "bottom-left" | "bottom-center" | "bottom-right"
模式 2:拖动手柄
使用 Draggable.Handle 将可拖动区域限制为特定的手柄区域:
<Draggable data={{ id: '1', label: 'Card' }}>
<View style={styles.card}>
<Draggable.Handle>
<View style={styles.handleBar}>
<Text>Drag here</Text>
</View>
</Draggable.Handle>
<View style={styles.cardContent}>
<Text>This area does NOT initiate drag</Text>
</View>
</View>
</Draggable>
可排序项目也可以使用 SortableItem.Handle 采用相同模式。
模式 3:纵向可排序列表
使用 Sortable 组件创建可重新排序的列表:
import { Sortable, SortableItem } from 'react-native-reanimated-dnd';
interface Item {
id: string;
title: string;
}
const ITEM_HEIGHT = 60;
function SortableListExample() {
const [items, setItems] = useState<Item[]>([
{ id: '1', title: 'First' },
{ id: '2', title: 'Second' },
{ id: '3', title: 'Third' },
]);
const renderItem = useCallback(({ item, ...props }) => (
<SortableItem
key={item.id}
id={item.id}
data={item}
onMove={(id, from, to) => {
setItems(prev => {
const next = [...prev];
const [moved] = next.splice(from, 1);
next.splice(to, 0, moved);
return next;
});
}}
{...props}
>
<View style={styles.listItem}>
<Text>{item.title}</Text>
</View>
</SortableItem>
), []);
return (
<Sortable
data={items}
renderItem={renderItem}
itemHeight={ITEM_HEIGHT}
/>
);
}
Sortable 属性
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
data | TData[] | 必填 | 项目(每项都必须包含 id: string) |
renderItem | (props) => ReactNode | 必填 | 项目渲染器 |
direction | "vertical" | "horizontal" | "vertical" | 列表方向 |
itemHeight | number | number[] | (item, i) => number | - | 项目高度(垂直布局时必填) |
itemWidth | number | - | 项目宽度(水平布局时必填) |
gap | number | 0 | 项目之间的间距(仅适用于水平布局) |
paddingHorizontal | number | 0 | 容器内边距(仅适用于水平布局) |
enableDynamicHeights | boolean | false | 自动测量项目高度 |
estimatedItemHeight | number | 60 | 未测量项目的备用高度 |
onHeightsMeasured | (heights) => void | - | 测量出高度后调用 |
useFlatList | boolean | true | 使用 FlatList 实现虚拟化 |
itemKeyExtractor | (item, index) => string | item.id | 自定义键提取函数 |
style | StyleProp<ViewStyle> | - | ScrollView/FlatList 样式 |
contentContainerStyle | StyleProp<ViewStyle> | - | 内容容器样式 |
SortableItem 属性
| 属性 | 类型 | 描述 |
|---|---|---|
id | string | 唯一项目标识符 |
data | T | 项目数据 |
onMove | (id, from, to) => void | 位置变化时调用 |
onDragStart | (id, position) => void | 开始拖动时调用 |
onDrop | (id, position, allPositions?) => void | 放下项目时调用 |
onDragging | (id, overItemId, yPosition) => void | 实时拖动位置 |
style | StyleProp<ViewStyle> | 容器样式 |
animatedStyle | StyleProp<ViewStyle> | 动画样式 |
模式 4:水平可排序列表
import {
Sortable,
SortableItem,
SortableDirection,
} from 'react-native-reanimated-dnd';
const ITEM_WIDTH = 120;
function HorizontalSortableExample() {
const [items, setItems] = useState([
{ id: '1', label: 'Tag A' },
{ id: '2', label: 'Tag B' },
{ id: '3', label: 'Tag C' },
]);
const renderItem = useCallback(({ item, ...props }) => (
<SortableItem
key={item.id}
id={item.id}
data={item}
onMove={(id, from, to) => {
setItems(prev => {
const next = [...prev];
const [moved] = next.splice(from, 1);
next.splice(to, 0, moved);
return next;
});
}}
{...props}
>
<View style={styles.tag}>
<Text>{item.label}</Text>
</View>
</SortableItem>
), []);
return (
<Sortable
data={items}
renderItem={renderItem}
direction={SortableDirection.Horizontal}
itemWidth={ITEM_WIDTH}
gap={12}
paddingHorizontal={12}
/>
);
}
模式 5:可排序网格
import {
SortableGrid,
SortableGridItem,
GridOrientation,
GridStrategy,
} from 'react-native-reanimated-dnd';
function GridExample() {
const [items, setItems] = useState([
{ id: '1', label: 'A' },
{ id: '2', label: 'B' },
{ id: '3', label: 'C' },
{ id: '4', label: 'D' },
{ id: '5', label: 'E' },
{ id: '6', label: 'F' },
]);
const renderItem = useCallback(({ item, ...props }) => (
<SortableGridItem
key={item.id}
id={item.id}
data={item}
onMove={(id, from, to) => {
setItems(prev => {
const next = [...prev];
const [moved] = next.splice(from, 1);
next.splice(to, 0, moved);
return next;
});
}}
{...props}
>
<View style={styles.gridCell}>
<Text>{item.label}</Text>
</View>
</SortableGridItem>
), []);
return (
<SortableGrid
data={items}
renderItem={renderItem}
dimensions={{
columns: 3,
itemWidth: 100,
itemHeight: 100,
rowGap: 8,
columnGap: 8,
}}
orientation={GridOrientation.Vertical}
strategy={GridStrategy.Insert}
/>
);
}
SortableGrid 属性
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
data | TData[] | 必填 | 网格项目(每项都必须包含 id: string) |
renderItem | (props) => ReactNode | 必填 | 项目渲染器 |
dimensions | GridDimensions | 必填 | 网格配置(见下文) |
orientation | "vertical" | "horizontal" | "vertical" | 网格方向 |
strategy | "insert" | "swap" | "insert" | 项目重新排序方式:插入会移动其他项目,交换会互换两个项目 |
scrollEnabled | boolean | true | 启用滚动 |
itemKeyExtractor | (item, index) => string | item.id | 自定义键提取函数 |
style | StyleProp<ViewStyle> | - | ScrollView 样式 |
contentContainerStyle | StyleProp<ViewStyle> | - | 内容样式 |
GridDimensions
| 属性 | 类型 | 描述 |
|---|---|---|
columns | number | 列数(垂直方向) |
rows | number | 行数(水平方向) |
itemWidth | number | 项目宽度 |
itemHeight | number | 项目高度 |
rowGap | number | 行间距 |
columnGap | number | 列间距 |
SortableGridItem 属性
| 属性 | 类型 | 描述 |
|---|---|---|
id | string | 唯一标识符 |
data | T | 项目数据 |
activationDelay | number | 开始拖动前的延迟时间(毫秒) |
onMove | (id, from, to) => void | 位置变化时调用 |
onDragStart | (id, position) => void | 开始拖动时调用 |
onDrop | (id, position, allPositions?) => void | 放下项目时调用 |
onDragging | (id, overItemId, x, y) => void | 实时位置 |
isBeingRemoved | boolean | 触发移除动画 |
style | StyleProp<ViewStyle> | 容器样式 |
animatedStyle | StyleProp<ViewStyle> | 动画样式 |
模式 6:动态高度
适用于项目高度不一的列表:
<Sortable
data={items}
renderItem={renderItem}
enableDynamicHeights
estimatedItemHeight={80}
onHeightsMeasured={(heights) => {
// { [id]: measuredHeight }
}}
/>
或者为每个项目指定明确的高度:
<Sortable
data={items}
renderItem={renderItem}
itemHeight={(item, index) => item.expanded ? 120 : 60}
/>
// Or as an array:
<Sortable
data={items}
renderItem={renderItem}
itemHeight={[60, 80, 120, 60, 100]}
/>
模式 7:轴向约束
将拖动锁定在单一轴向:
// Horizontal only
<Draggable data={data} dragAxis="x">
<View>{/* content */}</View>
</Draggable>
// Vertical only
<Draggable data={data} dragAxis="y">
<View>{/* content */}</View>
</Draggable>
模式 8:限制拖动范围
将拖动限制在容器内:
function BoundedExample() {
const boundsRef = useRef<View>(null);
return (
<DropProvider>
<View ref={boundsRef} style={styles.boundary}>
<Draggable data={{ id: '1' }} dragBoundsRef={boundsRef}>
<View style={styles.item}>
<Text>Cannot escape boundary</Text>
</View>
</Draggable>
</View>
</DropProvider>
);
}
结合轴向限制:
<Draggable data={data} dragBoundsRef={boundsRef} dragAxis="y">
{/* Vertical movement only, within bounds */}
</Draggable>
模式 9:碰撞检测
三种算法控制可拖动项何时“激活”可放置区:
// Default: any overlap triggers
<Draggable data={data} collisionAlgorithm="intersect">
// Center point must be over the droppable
<Draggable data={data} collisionAlgorithm="center">
// Entire draggable must be inside the droppable
<Draggable data={data} collisionAlgorithm="contain">
模式 10:放置区容量
限制放置区可接受的项目数量:
<Droppable onDrop={handleDrop} capacity={1}>
{/* Accepts exactly one item */}
</Droppable>
<Droppable onDrop={handleDrop} capacity={3}>
{/* Accepts up to three items */}
</Droppable>
<Droppable onDrop={handleDrop} capacity={Infinity}>
{/* Unlimited */}
</Droppable>
模式 11:跨放置区跟踪项目
使用 DropProvider.onDroppedItemsUpdate 跟踪每个放置区中的项目:
<DropProvider
onDroppedItemsUpdate={(droppedItems) => {
// droppedItems is: { [draggableId]: { droppableId, data } }
setMapping(droppedItems);
}}
>
{/* Draggables and Droppables */}
</DropProvider>
通过 ref 以编程方式访问:
const providerRef = useRef<DropProviderRef>(null);
<DropProvider ref={providerRef}>
{/* ... */}
</DropProvider>
// Later:
const items = providerRef.current?.getDroppedItems();
providerRef.current?.requestPositionUpdate();
模式 12:放置区的激活样式
当项目悬停在放置区上方时提供视觉反馈:
<Droppable
onDrop={handleDrop}
activeStyle={{
borderColor: 'blue',
borderWidth: 2,
backgroundColor: 'rgba(0, 0, 255, 0.1)',
}}
onActiveChange={(isActive) => {
// true when item is hovering, false when it leaves
}}
>
<View style={styles.zone}>
<Text>Drop Zone</Text>
</View>
</Droppable>
模式 13:自定义动画
为项目弹回或落位时提供自定义动画:
import { withSpring, withTiming, Easing } from 'react-native-reanimated';
// Spring animation
<Draggable
data={data}
animationFunction={(toValue) =>
withSpring(toValue, { damping: 15, stiffness: 150 })
}
>
// Timing animation with easing
<Draggable
data={data}
animationFunction={(toValue) =>
withTiming(toValue, { duration: 300, easing: Easing.bezier(0.25, 0.1, 0.25, 1) })
}
>
模式 14:拖动状态跟踪
监控拖动生命周期:
import { DraggableState } from 'react-native-reanimated-dnd';
<Draggable
data={data}
onStateChange={(state) => {
// DraggableState.IDLE | DraggableState.DRAGGING | DraggableState.DROPPED
}}
onDragStart={(data) => { /* drag began */ }}
onDragEnd={(data) => { /* drag ended */ }}
onDragging={({ x, y, tx, ty, itemData }) => {
// real-time position while dragging
}}
>
Hooks API(底层)
如需完全控制,请使用 hooks,而不是组件:
useDraggable
import { useDraggable } from 'react-native-reanimated-dnd';
import { GestureDetector } from 'react-native-gesture-handler';
import Animated from 'react-native-reanimated';
function CustomDraggable({ data }) {
const {
animatedViewProps, // { style, onLayout }
gesture, // pan gesture
state, // DraggableState
} = useDraggable({
data,
onDragStart: (d) => {},
onDragEnd: (d) => {},
collisionAlgorithm: 'intersect',
});
return (
<GestureDetector gesture={gesture}>
<Animated.View {...animatedViewProps}>
<Text>Custom Draggable</Text>
</Animated.View>
</GestureDetector>
);
}
useDroppable
import { useDroppable } from 'react-native-reanimated-dnd';
function CustomDropZone() {
const {
viewProps, // { onLayout, style? }
isActive, // boolean
} = useDroppable({
onDrop: (data) => {},
capacity: 1,
activeStyle: { borderColor: 'green' },
});
return (
<Animated.View {...viewProps}>
<Text>{isActive ? 'Release to drop' : 'Drop here'}</Text>
</Animated.View>
);
}
useSortableList + useSortable
用于自定义可排序列表实现:
import { useSortableList, useSortable } from 'react-native-reanimated-dnd';
import { GestureDetector } from 'react-native-gesture-handler';
import Animated from 'react-native-reanimated';
function CustomSortableList({ data }) {
const {
positions,
scrollY,
autoScroll,
scrollViewRef,
handleScroll,
handleScrollEnd,
contentHeight,
getItemProps,
} = useSortableList({ data, itemHeight: 60 });
return (
<Animated.ScrollView
ref={scrollViewRef}
onScroll={handleScroll}
onMomentumScrollEnd={handleScrollEnd}
style={{ height: 400 }}
contentContainerStyle={{ height: contentHeight }}
>
{data.map((item, index) => (
<CustomSortableItem
key={item.id}
item={item}
{...getItemProps(item, index)}
/>
))}
</Animated.ScrollView>
);
}
function CustomSortableItem({ item, ...sortableProps }) {
const {
animatedStyle,
panGestureHandler,
} = useSortable({
...sortableProps,
onMove: (id, from, to) => {},
});
return (
<GestureDetector gesture={panGestureHandler}>
<Animated.View style={animatedStyle}>
<Text>{item.title}</Text>
</Animated.View>
</GestureDetector>
);
}
useHorizontalSortableList + useHorizontalSortable
与垂直列表使用相同的模式,但适用于水平列表:
import {
useHorizontalSortableList,
useHorizontalSortable,
} from 'react-native-reanimated-dnd';
const {
positions,
scrollX,
autoScroll,
scrollViewRef,
handleScroll,
handleScrollEnd,
contentWidth,
getItemProps,
} = useHorizontalSortableList({
data,
itemWidth: 120,
gap: 12,
paddingHorizontal: 12,
});
useGridSortableList + useGridSortable
用于自定义网格实现:
import {
useGridSortableList,
useGridSortable,
GridOrientation,
GridStrategy,
} from 'react-native-reanimated-dnd';
const {
positions,
scrollY,
scrollX,
autoScrollDirection,
scrollViewRef,
handleScroll,
handleScrollEnd,
contentWidth,
contentHeight,
getItemProps,
} = useGridSortableList({
data,
dimensions: { columns: 3, itemWidth: 100, itemHeight: 100 },
orientation: GridOrientation.Vertical,
strategy: GridStrategy.Insert,
});
类型参考
// All sortable data must have an id
interface SortableData {
id: string;
}
// Drag state enum
enum DraggableState {
IDLE = "IDLE",
DRAGGING = "DRAGGING",
DROPPED = "DROPPED",
}
// Scroll directions
enum ScrollDirection { None, Up, Down }
enum HorizontalScrollDirection { None, Left, Right }
// List direction
enum SortableDirection { Vertical = "vertical", Horizontal = "horizontal" }
// Grid types
enum GridOrientation { Vertical = "vertical", Horizontal = "horizontal" }
enum GridStrategy { Insert = "insert", Swap = "swap" }
enum GridScrollDirection { None, Up, Down, Left, Right, UpLeft, UpRight, DownLeft, DownRight }
interface GridDimensions {
columns?: number;
rows?: number;
itemWidth: number;
itemHeight: number;
rowGap?: number;
columnGap?: number;
}
interface GridPosition {
index: number;
row: number;
column: number;
x: number;
y: number;
}
// Collision detection
type CollisionAlgorithm = "center" | "intersect" | "contain";
// Drop alignment
type DropAlignment =
| "center" | "top-left" | "top-center" | "top-right"
| "center-left" | "center-right"
| "bottom-left" | "bottom-center" | "bottom-right";
// Dropped items tracking
interface DroppedItemsMap<TData = unknown> {
[draggableId: string]: {
droppableId: string;
data: TData;
};
}
// DropProvider imperative handle
interface DropProviderRef {
requestPositionUpdate: () => void;
getDroppedItems: () => DroppedItemsMap;
}
// Grid positions shared value type
interface GridPositions {
[id: string]: GridPosition;
}
常用示例
带拖动手柄的可排序列表
<SortableItem id={item.id} data={item} {...props}>
<View style={styles.row}>
<SortableItem.Handle>
<View style={styles.handle}>
<Text>|||</Text>
</View>
</SortableItem.Handle>
<Text style={styles.content}>{item.title}</Text>
</View>
</SortableItem>
支持交换重排的网格
<SortableGrid
data={items}
renderItem={renderItem}
dimensions={{ columns: 4, itemWidth: 80, itemHeight: 80 }}
strategy={GridStrategy.Swap}
/>
带拖动手柄的网格项
<SortableGridItem id={item.id} data={item} {...props}>
<View style={styles.cell}>
<SortableGridItem.Handle>
<View style={styles.handle}>
<Text>Drag</Text>
</View>
</SortableGridItem.Handle>
<Text>{item.label}</Text>
</View>
</SortableGridItem>
带激活延迟的网格(防止意外拖动)
<SortableGridItem
id={item.id}
data={item}
activationDelay={300}
{...props}
>
可拖动项的拖动前延迟
<Draggable data={data} preDragDelay={200}>
{/* Must hold 200ms before drag activates */}
</Draggable>
带偏移量的放置对齐
<Droppable
onDrop={handleDrop}
dropAlignment="top-left"
dropOffset={{ x: 10, y: 10 }}
>
向可排序列表动态添加项目
项目必须始终具有唯一的 id 字段。只需更新状态数组:
const addItem = () => {
setItems(prev => [...prev, { id: String(Date.now()), title: 'New Item' }]);
};
带动画地从网格中移除项目
<SortableGridItem isBeingRemoved={item.removing} {...props}>
注意事项与禁止事项
数据要求
- 每个项目都必须具有
id: string—— 缺少 ID 或 ID 为 undefined 会导致重排异常和静默失败。在开发模式下会显示console.error;在生产环境中则会静默失效。 - ID 必须唯一 —— 重复的 ID 会导致项目共用位置。该库在内部将
id映射到index;重复 ID 会互相覆盖。 - 不要修改项目对象 —— 更新状态时始终创建新的数组或对象。该库会在手势闭包中捕获数据,因此修改原始对象会导致回调中的数据过时。
上下文与包装
- Draggable/Droppable 必须有 DropProvider 祖先 —— 如果没有,放置操作会静默失败(不会崩溃,生产环境也不会报错)。始终将 Draggable/Droppable 用法包裹在
<DropProvider>中。 - 不要用 DropProvider 包裹 Sortable 或 SortableGrid —— 它们会创建自己的内部 DropProvider。嵌套 Provider 会导致碰撞检测异常。
- 不要用 GestureHandlerRootView 包裹 Sortable 或 SortableGrid —— 它们会在内部自行包裹。重复包裹会导致手势冲突。对于 Draggable/Droppable 模式,只在应用根节点使用
GestureHandlerRootView。 - 手柄组件必须是其父组件的直接子组件 ——
Draggable.Handle必须位于Draggable内部,SortableItem.Handle必须位于SortableItem内部,SortableGridItem.Handle必须位于SortableGridItem内部。在其他位置使用时,它们会渲染子项,但不具备拖动功能(开发模式下会显示警告)。
必需属性(缺失时会抛出错误)
- 垂直 Sortable 必须设置
itemHeight—— 可以传入固定数值、数组或函数,也可以设置enableDynamicHeights={true}。缺少此属性会抛出错误。 - 水平 Sortable 必须设置
itemWidth—— 不支持动态宽度模式。必须使用固定数值。 - 网格尺寸必须包含
itemWidth+itemHeight+columns(垂直)或rows(水平) —— 缺少其中任何一项都会抛出错误。
状态管理
onMove必须更新你的状态数组 — 该库只会在视觉上为位置添加动画效果。如果你不在onMove中重新排序data数组,视觉顺序就会与数据顺序不一致。
// CORRECT
onMove={(id, from, to) => {
setItems(prev => {
const next = [...prev];
const [moved] = next.splice(from, 1);
next.splice(to, 0, moved);
return next;
});
}}
// WRONG — visual reorder happens but data stays stale
onMove={(id, from, to) => {
console.log('moved', id, from, to); // not updating state!
}}
-
不要在
onDragStart/onDragEnd中从外部状态读取data属性 — 这些回调会在手势创建时捕获data。如果数据对象在手势创建和回调触发之间发生变化,回调看到的就是旧版本。请改用传递给回调的data参数。 -
Droppable 的默认容量是 1,而非无限 — 如果不设置
capacity,每个区域只能放置一个项目。要允许无限放置,请设置capacity={Infinity}。
动画
animationFunction必须返回 Reanimated 动画值 — 它通过 worklet 在 UI 线程上运行。请始终返回withSpring、withTiming或其他 Reanimated 动画。返回普通数字会完全跳过动画(项目会瞬间跳到目标位置,不带过渡效果)。
// CORRECT — returns a Reanimated animation
animationFunction={(toValue) => withSpring(toValue, { damping: 15 })}
// CORRECT — timing with easing
animationFunction={(toValue) => withTiming(toValue, { duration: 200 })}
// BAD — returns plain number, item snaps with no animation
animationFunction={(toValue) => toValue}
- 默认动画是
withSpring(有弹性) — 默认情况下,项目会弹回原位。如果你想要更干脆的效果,请提供withTiming:
animationFunction={(toValue) => withTiming(toValue, { duration: 200 })}
- Droppable 上的
activeStyle不会应用动画 — 它会立即生效(不是过渡效果)。如果需要带动画的悬停反馈,请配合 Reanimated 的useAnimatedStyle使用onActiveChange。
手势处理
-
可排序项目的长按激活时间硬编码为 200ms — 你无法为
SortableItem或水平可排序项自定义此时间。只有SortableGridItem提供activationDelay。这可以防止意外拖动,但也意味着可排序项目始终需要短暂按住才能拖动。 -
Draggable 上的
preDragDelay={0}会与 ScrollViews 冲突 — 延迟为 0 时,平移手势会立即激活并抢占滚动手势的触摸事件。如果 Draggable 位于 ScrollView 内,请使用preDragDelay={100}或更高的值。 -
Droppable 区域重叠时,由先匹配者胜出 — 如果两个 Droppable 区域在空间上重叠,先注册的区域会胜出碰撞检测。这里没有“中心点最近”算法。请避免 Droppable 区域重叠。
-
如果 Draggable 大于 Droppable,
contain碰撞检测永远不会触发 — Draggable 必须完全位于 Droppable 内部。如果 Draggable 更大,请改用"intersect"或"center"。
平台特定说明
-
拖动阴影仅适用于 iOS — 拖动项目时,该库会应用
shadowColor/shadowOpacity/shadowRadius以提供视觉反馈。这些属性仅适用于 iOS。Android 上没有拖动阴影。如果需要 Android 端反馈,请通过animatedStyle应用你自己的elevation。 -
在 Android 上,
collapsable={false}至关重要 — 该库会在内部为 Draggable/Droppable 视图设置此属性。如果你使用 hooks API(useDraggable/useDroppable)搭配自定义视图,则必须在你的Animated.View上设置collapsable={false},否则 Android 会优化掉原生视图,导致测量结果为 0。
// When using hooks directly
<Animated.View collapsable={false} {...animatedViewProps}>
性能
-
Sortable会在数据数组变化时重新挂载整个列表 —Sortable使用所有项目 ID 的哈希值作为 React key,因此任何数据变化(包括重新排序、添加和移除)都会强制完整重新挂载。这会重置滚动位置和所有动画状态。如果数据经常变化,可以考虑直接使用 hooks API(useSortableList+useSortable),以获得更多控制权。 -
SortableGrid不会在数据变化时重新挂载 — 与 Sortable 不同,SortableGrid 能更高效地处理数据变化,无需完整重新挂载。 -
保持
onDragging处理函数轻量 — 它们每秒触发约 20 次(节流间隔为 50ms),并从 UI 线程桥接到 JS。在这些回调中进行繁重计算会导致卡顿。 -
Sortable 的
backgroundColor: "white"是硬编码的 — 内部 ScrollView/FlatList 的backgroundColor: "white"是硬编码的。对于深色模式,请通过style属性覆盖:
<Sortable
data={items}
renderItem={renderItem}
itemHeight={60}
style={{ backgroundColor: '#1a1a1a' }}
/>
动态高度
-
小于 1px 的动态高度变化会被忽略 — 该库会对高度进行取整,并忽略小于 1px 的变化,以防止无限重新渲染循环。亚像素高度调整会被丢弃。
-
初始动态高度只根据第一份数据快照计算一次 — 如果数据在初始渲染完成前发生变化,高度可能会过时,直到重新测量项目为止。
最佳实践
数据结构
// GOOD — simple, flat data with string IDs
const items = [
{ id: '1', title: 'Item 1' },
{ id: '2', title: 'Item 2' },
];
// GOOD — use stable unique IDs (not array indices)
const items = tasks.map(task => ({ ...task, id: task.uuid }));
// BAD — numeric IDs (must be strings)
const items = [{ id: 1, title: 'Item 1' }];
// BAD — using array index as ID (breaks on reorder)
const items = data.map((d, i) => ({ ...d, id: String(i) }));
// BAD — missing ID field
const items = [{ title: 'Item 1' }];
对 renderItem 使用记忆化
始终使用 useCallback 包装 renderItem,以避免不必要的重新渲染:
// GOOD
const renderItem = useCallback(({ item, ...props }) => (
<SortableItem key={item.id} id={item.id} data={item} {...props}>
<MyItemComponent item={item} />
</SortableItem>
), []);
// BAD — creates new function on every render
const renderItem = ({ item, ...props }) => (
<SortableItem key={item.id} id={item.id} data={item} {...props}>
<MyItemComponent item={item} />
</SortableItem>
);
展开 renderItem 属性
renderItem 回调会接收来自父级 Sortable/SortableGrid 的共享值和配置。始终将它们展开并传递给项目组件:
// GOOD — spread all props from renderItem
const renderItem = useCallback(({ item, ...props }) => (
<SortableItem id={item.id} data={item} {...props}>
{/* content */}
</SortableItem>
), []);
// BAD — manually passing individual props (easy to miss required ones)
const renderItem = useCallback(({ item, positions, lowerBound }) => (
<SortableItem id={item.id} data={item} positions={positions} lowerBound={lowerBound}>
{/* missing autoScrollDirection, itemsCount, etc. */}
</SortableItem>
), []);
对交互内容使用手柄
如果可排序项目包含按钮、输入框或其他交互元素,请使用手柄以避免拖动冲突:
// GOOD — only the handle initiates drag, buttons work normally
<SortableItem id={item.id} data={item} {...props}>
<View style={styles.row}>
<SortableItem.Handle>
<View style={styles.dragHandle} />
</SortableItem.Handle>
<Text>{item.title}</Text>
<Button onPress={onDelete} title="Delete" />
</View>
</SortableItem>
// PROBLEMATIC — entire item is draggable, button taps may trigger drag
<SortableItem id={item.id} data={item} {...props}>
<View style={styles.row}>
<Text>{item.title}</Text>
<Button onPress={onDelete} title="Delete" />
</View>
</SortableItem>
网格激活延迟
对于项目可点击的网格,请使用 activationDelay 来区分点击和拖动:
<SortableGridItem
id={item.id}
data={item}
activationDelay={250}
{...props}
>
<Pressable onPress={() => navigateTo(item)}>
{/* Grid cell content */}
</Pressable>
</SortableGridItem>
对复杂布局使用 Hooks API
如果需要完全控制容器(自定义滚动行为、嵌套列表、非标准布局),请使用 hooks,而不是组件 API:
// Hooks give you raw shared values and gesture handlers
const { positions, scrollY, autoScroll, scrollViewRef, handleScroll, contentHeight, getItemProps } =
useSortableList({ data, itemHeight: 60 });
// You control the ScrollView entirely
<Animated.ScrollView
ref={scrollViewRef}
onScroll={handleScroll}
contentContainerStyle={{ height: contentHeight }}
// Add your own custom scroll props
showsVerticalScrollIndicator={false}
bounces={false}
>
{data.map((item, index) => (
<CustomItem key={item.id} item={item} {...getItemProps(item, index)} />
))}
</Animated.ScrollView>
避免嵌套 Sortable
请勿将 Sortable 嵌套在另一个 Sortable 或 SortableGrid 中。每个组件都会创建自己的 DropProvider 和 GestureHandlerRootView,嵌套的手势处理器会发生冲突。如果需要嵌套可重新排序的列表,请使用 hooks API,并共享单个手势根节点。