You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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实现卡片归位的弹性动画,提升交互体验。

注意事项

  1. 示例中通过columnRefs获取列的真实布局信息,确保位置判断的准确性。
  2. 若卡片数量较多,建议使用React.memo包裹DraggableCard和Column组件优化渲染性能。
  3. 可扩展添加拖拽时的卡片半透明效果、列边界高亮等细节,进一步提升交互体验。

内容的提问来源于stack exchange,提问作者Nik Shein

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 15:00:02