基于React Reanimated实现React Native类Trello拖拽逻辑示例请求
React Reanimated实现Trello式跨列拖拽示例
核心思路
基于React Reanimated + React Native Gesture Handler实现,核心是通过手势跟踪卡片位置,实时判断拖拽目标列/位置,在拖拽结束时更新数据源,并通过动画优化拖拽过程的视觉体验。
依赖准备
先确保安装必要依赖:
npm install react-reanimated react-native-gesture-handler
(需按照官方文档完成iOS/Android的原生配置)
完整示例代码
import React, { useState, useRef } from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { PanGestureHandler, useAnimatedGestureHandler, useAnimatedStyle, useSharedValue, withSpring } from 'react-reanimated'; // 初始数据 const initialColumns = [ { id: '4638e8e9-ff81-45ee-9ec0-c3fc5a2bd0f4', name: 'Philosophy', cards: [ { cardIndex: 0, name: 'A', id: 'ef207fe4-62ab-4285-a89e-d92c529a1421' }, { cardIndex: 1, name: 'B', id: '7fb99625-bda1-4366-bbac-205677331848' }, { cardIndex: 2, name: 'C', id: 'd305c776-caf7-4eb3-89fe-92aaecf9975e' }, ], }, { id: '1532323a-2747-4ed7-8df0-4ccc32ca6389', name: 'History', cards: [ { cardIndex: 0, name: 'F', id: '26d19ca5-76f6-4b56-a38e-a307035c7840' }, { cardIndex: 1, name: 'G', id: 'e65dc7c8-ec23-4ebb-b7fe-ebcc26a50070' }, ], }, ]; // 卡片组件 const DraggableCard = ({ card, columnId, onDragEnd, onDragUpdate }) => { const translateX = useSharedValue(0); const translateY = useSharedValue(0); const cardRef = useRef(null); const gestureHandler = useAnimatedGestureHandler({ onStart: (event, context) => { // 记录初始位置和卡片/列信息 context.startX = translateX.value; context.startY = translateY.value; context.cardId = card.id; context.columnId = columnId; context.cardIndex = card.cardIndex; // 获取卡片布局信息 cardRef.current?.measure((x, y, width, height, pageX, pageY) => { context.cardWidth = width; context.cardHeight = height; context.pageX = pageX; context.pageY = pageY; }); }, onActive: (event, context) => { // 更新拖拽位置 translateX.value = context.startX + event.translationX; translateY.value = context.startY + event.translationY; // 实时通知外部更新拖拽状态 onDragUpdate({ cardId: context.cardId, currentX: context.pageX + event.translationX, currentY: context.pageY + event.translationY, cardHeight: context.cardHeight, originalColumnId: context.columnId, originalIndex: context.cardIndex, }); }, onEnd: (event, context) => { // 重置动画值 translateX.value = withSpring(0); translateY.value = withSpring(0); // 通知外部处理拖拽结束逻辑 onDragEnd({ cardId: context.cardId, originalColumnId: context.columnId, originalIndex: context.cardIndex, }); }, }); const animatedStyle = useAnimatedStyle(() => { return { transform: [ { translateX: translateX.value }, { translateY: translateY.value }, ], zIndex: 100, }; }); return ( <PanGestureHandler onGestureEvent={gestureHandler}> <Animated.View ref={cardRef} style={[styles.card, animatedStyle]}> <Text>{card.name}</Text> </Animated.View> </PanGestureHandler> ); }; // 列组件 const Column = ({ column, placeholderIndex, onDragUpdate, onDragEnd }) => { return ( <View style={styles.column}> <Text style={styles.columnTitle}>{column.name}</Text> {column.cards.map((card, index) => { // 渲染占位符 if (index === placeholderIndex) { return <View key="placeholder" style={styles.placeholder} />; } return ( <DraggableCard key={card.id} card={card} columnId={column.id} onDragUpdate={onDragUpdate} onDragEnd={onDragEnd} /> ); })} {/* 列末尾的占位符 */} {placeholderIndex === column.cards.length && ( <View key="end-placeholder" style={styles.placeholder} /> )} </View> ); }; // 主组件 const TrelloBoard = () => { const [columns, setColumns] = useState(initialColumns); const [dragState, setDragState] = useState({ activeCardId: null, targetColumnId: null, targetIndex: -1, }); const columnRefs = useRef({}); // 初始化列的布局信息(实际项目可在组件挂载后调用) const initColumnLayouts = () => { columns.forEach(column => { columnRefs.current[column.id]?.measure((x, y, width, height, pageX, pageY) => { columnRefs.current[column.id] = { left: pageX, top: pageY, width, height }; }); }); }; // 处理拖拽过程中的位置判断 const handleDragUpdate = (dragInfo) => { let targetColumnId = null; let targetIndex = -1; // 遍历列,判断当前拖拽位置所属列和插入位置 Object.entries(columnRefs.current).forEach(([colId, layout]) => { if (dragInfo.currentX > layout.left && dragInfo.currentX < layout.left + layout.width) { targetColumnId = colId; const relativeY = dragInfo.currentY - layout.top; targetIndex = Math.floor(relativeY / dragInfo.cardHeight); targetIndex = Math.max(0, Math.min(targetIndex, columns.find(c => c.id === colId).cards.length)); } }); setDragState({ activeCardId: dragInfo.cardId, targetColumnId, targetIndex, }); }; // 处理拖拽结束,更新数据源 const handleDragEnd = (dragInfo) => { const { originalColumnId, originalIndex, cardId } = dragInfo; const { targetColumnId, targetIndex } = dragState; if (!targetColumnId || targetIndex === -1) return; // 1. 从原列移除卡片并更新索引 const updatedColumns = [...columns]; const originalColumn = updatedColumns.find(col => col.id === originalColumnId); const [movedCard] = originalColumn.cards.splice(originalIndex, 1); originalColumn.cards.forEach((card, idx) => card.cardIndex = idx); // 2. 添加到目标列并更新索引 const targetColumn = updatedColumns.find(col => col.id === targetColumnId); targetColumn.cards.splice(targetIndex, 0, { ...movedCard, cardIndex: targetIndex }); targetColumn.cards.forEach((card, idx) => card.cardIndex = idx); setColumns(updatedColumns); setDragState({ activeCardId: null, targetColumnId: null, targetIndex: -1 }); }; return ( <GestureHandlerRootView style={styles.container}> <View style={styles.board}> {columns.map((column) => ( <View key={column.id} ref={el => columnRefs.current[column.id] = el}> <Column column={column} placeholderIndex={dragState.targetColumnId === column.id ? dragState.targetIndex : -1} onDragUpdate={handleDragUpdate} onDragEnd={handleDragEnd} /> </View> ))} </View> </GestureHandlerRootView> ); }; const styles = StyleSheet.create({ container: { flex: 1, padding: 16, backgroundColor: '#f0f0f0', }, board: { flexDirection: 'row', gap: 16, flexWrap: 'wrap', }, column: { backgroundColor: '#ebecf0', borderRadius: 8, padding: 8, width: 280, }, columnTitle: { fontSize: 16, fontWeight: '600', marginBottom: 8, }, card: { backgroundColor: 'white', borderRadius: 4, padding: 12, marginBottom: 8, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2, elevation: 2, }, placeholder: { backgroundColor: '#dcdde1', borderRadius: 4, height: 48, marginBottom: 8, }, }); export default TrelloBoard;
关键逻辑说明
- 手势处理:用
PanGestureHandler跟踪拖拽动作,useAnimatedGestureHandler处理开始/移动/结束三个阶段的逻辑,实时更新卡片的动画位置。 - 位置判断:拖拽过程中通过卡片的实时坐标,结合列的布局信息判断其进入哪个列、应该插入到哪个位置,显示占位符提示。
- 数据源更新:拖拽结束时,修改
columns数组,将卡片从原列移除并插入到目标列的指定位置,同时更新所有相关卡片的cardIndex保证排序正确。 - 动画优化:用
withSpring实现卡片归位的弹性动画,提升交互体验。
注意事项
- 示例中通过
columnRefs获取列的真实布局信息,确保位置判断的准确性。 - 若卡片数量较多,建议使用
React.memo包裹DraggableCard和Column组件优化渲染性能。 - 可扩展添加拖拽时的卡片半透明效果、列边界高亮等细节,进一步提升交互体验。
内容的提问来源于stack exchange,提问作者Nik Shein
相关产品推荐
相关产品推荐

