如何在react-native web和移动端实现拖放?

问题描述 投票:0回答:1

我想在网络上使用 React Native 进行拖放,但我找不到任何可用于使其在网络和移动设备上工作的库。我发现的大多数软件包仅适用于移动设备,例如:react-native-drax、rn-dnd-kanban。如果我可以使用一个包来创建可在网络和移动设备上运行的拖放功能,请告诉我。

我已经制作了一个具有拖放功能的应用程序,带有react-native-gesture-handler ~2.16.1和react-native-reanimated ~3.10.1。但我所做的有一个问题,它只能在网络上运行,并且性能很差,消耗大量 CPU。

这是我正在编写的代码。

import React, { useEffect, useState, useRef } from "react";
import { StyleSheet, Text, View, FlatList, Dimensions } from "react-native";
import Animated, {
  runOnJS,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
  withTiming,
} from "react-native-reanimated";
import {
  Gesture,
  GestureDetector,
  GestureHandlerRootView,
  ScrollView,
} from "react-native-gesture-handler";
import { generateNewId } from "@/utils/index";

interface Card {
  id: string;
  title: string;
}

interface Column {
  id: string;
  title: string;
  cards: Card[];
}

interface Position {
  x: number;
  y: number;
  width: number;
  height: number;
}

interface DragContext {
  type: "card" | "column";
  columnId: string;
  cardId?: string;
  cardTitle?: string;
}

const initialColumns: Column[] = [
  {
    id: "b1c1",
    title: "To Do",
    cards: [
      { id: "b1c1c1", title: "Workspace - Board" },
      { id: "b1c1c2", title: "Workspace - Priority" },
    ],
  },
  {
    id: "b1c2",
    title: "In Progress",
    cards: [{ id: "b1c2c1", title: "Workspace - Drag And Drop" }],
  },
  { id: "b1c3", title: "Done", cards: [] },
  {
    id: "b1c4",
    title: "Upcoming",
    cards: [{ id: "b1c4c1", title: "Workspace - Backends" }],
  },
  {
    id: "b1c5",
    title: "Pending",
    cards: [{ id: "b1c5c1", title: "Workspace - Assignee" }],
  },
];

function Box({
  children,
  style,
  id,
  activeBox,
  setActiveBox,
  columnId,
  title,
  isColumn = false,
  onDragStart,
  onDragMove,
  onDragEnd,
  measureRef,
  dragHandleRef,
  isDragging,
}: {
  children: React.ReactNode;
  style: any;
  id: string;
  activeBox: string;
  setActiveBox: (id: string) => void;
  columnId?: string;
  title?: string;
  isColumn?: boolean;
  onDragStart?: (context: DragContext) => void;
  onDragMove?: (x: number, y: number) => void;
  onDragEnd?: (x: number, y: number) => void;
  measureRef?: (ref: View) => void;
  dragHandleRef?: React.RefObject<View>;
  isDragging?: boolean;
}) {
  const pressed = useSharedValue(false);
  const offset = useSharedValue({ x: 0, y: 0 });
  const boxRef = useRef<View>(null);
  const startPosition = useSharedValue({ x: 0, y: 0 });

  useEffect(() => {
    if (boxRef.current && measureRef) {
      measureRef(boxRef.current);
    }
  }, [measureRef]);

  const pan = Gesture.Pan()
    .minDistance(1)
    .onBegin((event) => {
      // Only start drag if touching drag handle for columns
      if (isColumn && dragHandleRef?.current) {
        dragHandleRef.current.measure((x, y, width, height, pageX, pageY) => {
          const isTouchInHandle =
            event.absoluteX >= pageX &&
            event.absoluteX <= pageX + width &&
            event.absoluteY >= pageY &&
            event.absoluteY <= pageY + height;

          if (!isTouchInHandle) return;
        });
      }

      pressed.value = true;
      startPosition.value = { x: event.absoluteX, y: event.absoluteY };
      runOnJS(setActiveBox)(id);
      if (onDragStart) {
        runOnJS(onDragStart)({
          type: isColumn ? "column" : "card",
          columnId: isColumn ? id : columnId || "",
          cardId: isColumn ? undefined : id,
          cardTitle: isColumn ? undefined : title,
        });
      }
    })
    .onChange((event) => {
      offset.value = {
        x: event.translationX,
        y: event.translationY,
      };
      if (onDragMove) {
        runOnJS(onDragMove)(event.absoluteX, event.absoluteY);
      }
    })
    .onFinalize((event) => {
      if (onDragEnd) {
        runOnJS(onDragEnd)(event.absoluteX, event.absoluteY);
      }
      offset.value = withSpring({ x: 0, y: 0 });
      pressed.value = false;
      if (activeBox === id) runOnJS(setActiveBox)("");
    });

  const animatedStyles = useAnimatedStyle(() => ({
    transform: [
      { translateX: offset.value.x },
      { translateY: offset.value.y },
      { scale: withTiming(pressed.value ? 1.1 : 1) },
    ],
    backgroundColor: pressed.value ? "#FFE04B" : style.backgroundColor,
    borderRadius: withTiming(pressed.value ? 25 : 10, { duration: 200 }),
    zIndex: pressed.value ? 999 : 1,
    opacity: isDragging ? 0.5 : 1,
  }));

  return (
    <View>
      <Animated.View ref={boxRef} style={[style, animatedStyles]}>
        <GestureDetector gesture={pan}>
          <View ref={dragHandleRef} style={styles.dragHandle}>
            <Text style={styles.dragHandleText}>:::</Text>
          </View>
        </GestureDetector>
        <View>{children}</View>
      </Animated.View>
    </View>
  );
}

export default function App() {
  const [activeBox, setActiveBox] = useState("");
  const [columns, setColumns] = useState(initialColumns);
  const [columnMeasurements, setColumnMeasurements] = useState<
    Map<string, Position>
  >(new Map());
  const [cardMeasurements, setCardMeasurements] = useState<
    Map<string, Position>
  >(new Map());
  const [dragContext, setDragContext] = useState<DragContext | null>(null);
  const [hoveredColumn, setHoveredColumn] = useState<string | null>(null);
  const [hoveredCard, setHoveredCard] = useState<string | null>(null);
  const columnHandleRefs = useRef<Map<string, React.RefObject<View>>>(
    new Map()
  );

  // Initialize refs for column handles
  useEffect(() => {
    columns.forEach((column) => {
      if (!columnHandleRefs.current.has(column.id)) {
        columnHandleRefs.current.set(column.id, React.createRef<View>());
      }
    });
  }, [columns]);

  const measureColumn = (columnId: string) => (ref: View) => {
    ref.measure((x, y, width, height, pageX, pageY) => {
      setColumnMeasurements((prev) =>
        new Map(prev).set(columnId, {
          x: pageX,
          y: pageY,
          width,
          height,
        })
      );
    });
  };

  const measureCard = (cardId: string) => (ref: View) => {
    ref.measure((x, y, width, height, pageX, pageY) => {
      setCardMeasurements((prev) =>
        new Map(prev).set(cardId, {
          x: pageX,
          y: pageY,
          width,
          height,
        })
      );
    });
  };

  const handleDragStart = (context: DragContext) => {
    setDragContext(context);
    console.log("Started dragging:", context);
  };

  const handleDragMove = (x: number, y: number) => {
    if (!dragContext) return;

    // Reset hoveredCard and hoveredColumn flags initially
    let isHoveringCard = false;
    let isHoveringColumn = false;

    // Handle column movement
    columnMeasurements.forEach((position, columnId) => {
      if (
        x >= position.x &&
        x <= position.x + position.width &&
        y >= position.y &&
        y <= position.y + position.height
      ) {
        isHoveringColumn = true; // Set flag if hovering over a column
        if (hoveredColumn !== columnId) {
          setHoveredColumn(columnId);
          console.log(`Hovering over column: ${columnId}`);
        }
      }
    });
    // If not hovering over any column, reset hoveredColumn
    if (!isHoveringColumn) {
      setHoveredColumn(null);
    }

    // Handle card movement
    cardMeasurements.forEach((position, cardId) => {
      if (
        cardId !== dragContext.cardId &&
        x >= position.x &&
        x <= position.x + position.width &&
        y >= position.y &&
        y <= position.y + position.height
      ) {
        isHoveringCard = true; // Set flag if hovering over a card
        if (hoveredCard !== cardId) {
          setHoveredCard(cardId);
          console.log(`Hovering over card: ${cardId}`);
        }
      }
    });
    // If not hovering over any card, reset hoveredCard
    if (!isHoveringCard) {
      setHoveredCard(null);
    }
  };

  function handleDragEnd() {
    if (!dragContext) return;

    // Check if the drag ended over a column
    if (dragContext.type === "card" && hoveredColumn) {
      setColumns((prevColumns) =>
        prevColumns.map((column) => {
          if (column.id === hoveredColumn) {
            let updatedCards = column.cards.filter(
              (card) => card.id !== dragContext.cardId
            );

            const targetIndex = hoveredCard
              ? updatedCards.findIndex((card) => card.id === hoveredCard)
              : updatedCards.length;

            updatedCards.splice(targetIndex, 0, {
              id: dragContext.cardId!,
              title: dragContext.cardTitle!,
            });

            return { ...column, cards: updatedCards };
          } else if (column.id === dragContext.columnId) {
            // Remove card from its original column
            return {
              ...column,
              cards: column.cards.filter(
                (card) => card.id !== dragContext.cardId
              ),
            };
          }
          return column;
        })
      );
    }
    //     // Reset states
    setDragContext(null);
    setHoveredColumn(null);
    setHoveredCard(null);
    return;
  }

  return (
    <GestureHandlerRootView style={styles.container}>
      <ScrollView horizontal style={styles.scrollView}>
        {columns.map((column) => (
          <Box
            key={column.id}
            id={column.id}
            activeBox={activeBox}
            setActiveBox={setActiveBox}
            style={[
              styles.columnBox,
              hoveredColumn === column.id && styles.columnBoxHovered,
            ]}
            isColumn={true}
            onDragStart={handleDragStart}
            onDragMove={handleDragMove}
            onDragEnd={handleDragEnd}
            measureRef={measureColumn(column.id)}
            dragHandleRef={columnHandleRefs.current.get(column.id)}
          >
            <Text style={styles.columnTitle}>{column.title}</Text>
            {column.cards.map((card) => (
              <Box
                key={card.id}
                id={card.id}
                title={card.title}
                activeBox={activeBox}
                setActiveBox={setActiveBox}
                columnId={column.id}
                style={[
                  styles.card,
                  hoveredCard === card.id && styles.cardHovered,
                ]}
                onDragStart={handleDragStart}
                onDragMove={handleDragMove}
                onDragEnd={handleDragEnd}
                measureRef={measureCard(card.id)}
                isDragging={dragContext?.cardId === card.id}
              >
                <Text style={styles.cardTitle}>{card.title}</Text>
              </Box>
            ))}
          </Box>
        ))}
      </ScrollView>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  scrollView: {
    flex: 1,
  },
  columnBox: {
    padding: 10,
    backgroundColor: "#e0e0e0e0",
    borderRadius: 8,
    width: 280,
    margin: 10,
    minHeight: 400,
  },
  columnBoxHovered: {
    backgroundColor: "#e0e0e0",
    borderColor: "#2196F3",
    borderWidth: 2,
  },
  columnTitle: {
    fontSize: 18,
    fontWeight: "600",
    marginBottom: 10,
    color: "#333",
  },
  card: {
    marginVertical: 5,
    padding: 15,
    backgroundColor: "white",
    borderRadius: 8,
    width: "100%",
    height: 100,
    shadowColor: "#000",
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.1,
    shadowRadius: 3,
    elevation: 3,
  },
  cardHovered: {
    backgroundColor: "#f8f8f8",
    borderColor: "#2196F3",
    borderWidth: 2,
  },
  cardTitle: {
    fontSize: 16,
    color: "#333",
  },
  dragHandle: {
    padding: 5,
    alignItems: "center",
    justifyContent: "center",
  },
  dragHandleText: {
    fontSize: 16,
    color: "#999",
  },
});
react-native drag-and-drop react-native-reanimated react-native-gesture-handler
1个回答
0
投票

你看过https://github.com/react-grid-layout/react-grid-layout吗?

它支持拖放,还支持使用 React 在 Web 上调整大小,我相信有些人已经通过添加按住切换来启用拖动与滚动来使其在移动界面上运行良好。

我还有一个网络演示模板这里

希望这有帮助!

© www.soinside.com 2019 - 2024. All rights reserved.