React.js的触摸支持拖放功能

17

如何在Facebook的react.js中实现支持触摸事件的拖放?

有一些关于react.js拖放的问题文章,但它们似乎都没有提到触摸事件,并且这些示例都无法在我的手机上运行。

总体而言,我想知道哪种方法最简单:尝试使用已经支持触摸的现有d&d库来实现此操作,但可能需要一些工作才能与react有效地协同工作;或者尝试使用任何react d&d示例并使其支持触摸(根据这个问题,这可能不是易事?)


你在React中启用了触摸事件吗? - Brett DeWoody
3个回答

8

react-motion (带有触摸事件)

我们尝试使用 "react-motion" 来拖动列表中的项目。当列表中的项目超过15-20个时,它会变得非常卡顿。(但是对于小型列表,它的效果很好,如演示所示)。请注意,移动设备比台式机慢得多。

关于react-motion的重要说明:在测试动画性能时,请不要忘记使用生产模式!

react-dnd (带有触摸事件)

第二个选项是 "react-dnd"。这是一个很棒的库。它是低级别的,但是相当容易理解如何使用它。但是一开始,由于没有触摸事件支持,"react-dnd"对我们来说不是一个选择。

后来,当Yahoo发布了react-dnd-touch-backend时,我们决定将我们的应用程序从"react-motion"切换到"react-dnd"。这解决了我们所有的性能问题。我们有一个包含50-70个项目的列表,它正常工作。

雅虎做得非常好,这个解决方案在我们的生产应用程序中运行良好。


我无法在React + ES6中使其正常工作。我可以渲染它,但与元素的任何交互都无法更新样式属性以实现有用性。 :[ - Brad Gunn
适合我的情况的完美库 - Vitalii Del Vorobioff
@BradGunn 我不确定你的情况是否与 ES6 有关。在使用 React ES6 类语法时唯一的问题是自动绑定。请检查你的 "this" 是否引用了正确的对象。 - koorchik
react-dnd-touch-backend 看起来不再得到大力支持。在使用列表时,https://github.com/atlassian/react-beautiful-dnd 可能是考虑触摸支持的选择。 - Kerem

6

你已经提到了react-dnd,我创建了PR,使触摸设备也能够进行拖放操作,你可以尝试一下。


是的,touchbackend现在可以使用了,并且已经集成到项目中并在网站上提及。但我注意到的一件事是,当我使用touchbackend时,在我的支持鼠标和触摸的笔记本电脑上,鼠标拖放不再起作用了。 - Flion
@Flion 你可以传递一个设置到TouchBackend来启用鼠标事件。 - Prashanth Chandra

2
我还没有找到任何答案。被接受的答案并不是一个真正的答案,但它指向了一个github库。我将尝试在这里使用仅react来完整回答。代码应该是自说明的,但提前几个词。我们需要使用大量的状态变量来在渲染之间保持状态,否则任何变量都会被重置。为了使过渡平滑,我使用useEffect钩子在完成渲染后更新位置。我在codesandbox中测试了这个,我包括链接here供任何人编辑代码和玩耍,只需fork它。它可以与MS Surface Book2 Pro和Android一起使用。它在iPhone IOS上有一个格式问题。对于Safari和Chrome都是如此。如果有人修复它那就太好了。现在我已经得到了我需要的并声称成功。以下是codesandbox.io中src下的文件:
App.js

import "./styles/index.pcss";

import "./styles/tailwind-pre-build.css";
import Photos from "./Photos.js";

export default function App() {
  return (
    <>
      <div className="flow-root bg-green-200">
        <div className="my-4 bg-blue-100 mb-20">
          Drag and Drop with touch screens
        </div>
      </div>
      <div className="flow-root bg-red-200">
        <div className="bg-blue-100">
          <Photos />
        </div>
      </div>
    </>
  );
}

Photos.js:

import React, { useState } from "react";

import "./styles/index.pcss";
import Image from "./image";

export default function Photos() {
  const [styleForNumber, setStyleForNumber] = useState({
    position: "relative",
    width: "58px",
    height: "58px"
  });

  const photosArray = [
    "https://spinelli.io/noderestshop/uploads/G.1natalie.1642116451444",
    "https://spinelli.io/noderestshop/uploads/G.2natalie.1642116452437",
    "https://spinelli.io/noderestshop/uploads/G.3natalie.1642116453418",
    "https://spinelli.io/noderestshop/uploads/G.4natalie.1642116454396",
    "https://spinelli.io/noderestshop/uploads/G.5natalie.1642116455384",
    "https://spinelli.io/noderestshop/uploads/G.6natalie.1642116456410",
    "https://spinelli.io/noderestshop/uploads/G.7natalie.1642116457466",
    "https://spinelli.io/noderestshop/uploads/G.8natalie.1642116458535",
    "https://spinelli.io/noderestshop/uploads/G.0natalie.1642116228246"
  ];

  return (
    <>
      <div
        className="w-1/2 bg-green-200"
        style={{
          display: "grid",
          gridTemplateColumns: "[first] 60px [second] 60px [third] 60px",
          gridTemplateRows: "60px 60px 60px",
          rowGap: "10px",
          columnGap: "20px",
          position: "relative",
          justifyContent: "center",
          placeItems: "center"
        }}
      >
        {photosArray.map((photo, i) => (
          <div
            className="relative z-1 h-full w-full flex flex-wrap content-center touch-none"
            key={i}
          >
            <div className="contents">
              <Image photo={photo} i={i} />
            </div>
          </div>
        ))}
      </div>
    </>
  );
}

Image.js:

import React, { useRef, useState, useEffect } from "react";

import "./styles/index.pcss";

export default function Image({ photo, i }) {
  const imgRef = useRef();
  const [top, setTop] = useState(0);
  const [left, setLeft] = useState(0);
  const [drag, setDrag] = useState(false);
  const [styleForImg, setStyleForImg] = useState({
    position: "absolute",
    width: "58px",
    height: "58px"
  });
  const [offsetTop, setOffsetTop] = useState(-40);
  const [offsetLeft, setOffsetLeft] = useState(0);
  const [xAtTouchPointStart, setXAtTouchPointStart] = useState(0);
  const [yAtTouchPointStart, setYAtTouchPointStart] = useState(0);

  useEffect(() => {
    if (drag) {
      setStyleForImg({
        position: "relative",
        width: "58px",
        height: "58px",
        top: top,
        left: left
      });
    } else {
      setStyleForImg({
        position: "relative",
        width: "58px",
        height: "58px"
      });
    }
    console.log("style: ", styleForImg);
  }, [drag, top, left]);

  const handleTouchStart = (e, i) => {
    e.preventDefault();
    let evt = typeof e.originalEvent === "undefined" ? e : e.originalEvent;
    let touch = evt.touches[0] || evt.changedTouches[0];
    const x = +touch.pageX;
    const y = +touch.pageY;
    console.log(
      "onTouchStart coordinates of icon @ start: X: " + x + " | Y: " + y
    );
    console.log("dragged from position n = ", i + 1);
    // get the mouse cursor position at startup:
    setXAtTouchPointStart(x);
    setYAtTouchPointStart(y);
    setDrag(true);
  };

  const handleTouchEnd = (e) => {
    // if (process.env.NODE_ENV === 'debug5' || process.env.NODE_ENV === 'development') {
    e.preventDefault();
    setDrag(false);
    console.log(
      new Date(),
      "onTouchEnd event, coordinates of icon @ end: X: " +
        e.changedTouches[0]?.clientX +
        " | Y: " +
        e.changedTouches[0]?.clientY +
        " | top: " +
        top +
        " | left: " +
        left
    );
  };

  const handleElementDrag = (e) => {
    e = e || window.event;
    e.preventDefault();
    let x = 0;
    let y = 0;

    //Get touch or click position
    //https://dev59.com/BVgQ5IYBdhLWcg3w7ofY#41993300
    if (
      e.type === "touchstart" ||
      e.type === "touchmove" ||
      e.type === "touchend" ||
      e.type === "touchcancel"
    ) {
      let evt = typeof e.originalEvent === "undefined" ? e : e.originalEvent;
      let touch = evt.touches[0] || evt.changedTouches[0];
      x = +touch.pageX; // X Coordinate relative to the viewport of the touch point
      y = +touch.pageY; // same for Y
    } else if (
      e.type === "mousedown" ||
      e.type === "mouseup" ||
      e.type === "mousemove" ||
      e.type === "mouseover" ||
      e.type === "mouseout" ||
      e.type === "mouseenter" ||
      e.type === "mouseleave"
    ) {
      x = +e.clientX;
      y = +e.clientY;
    }
    console.log("x: ", x, "y: ", y);
    // calculate the new cursor position:
    const xRelativeToStart = x - xAtTouchPointStart;
    console.log(
      "xRel = ",
      x,
      " - ",
      xAtTouchPointStart,
      " = ",
      xRelativeToStart
    );
    const yRelativeToStart = y - yAtTouchPointStart;
    console.log(
      "yRel = ",
      y,
      " - ",
      yAtTouchPointStart,
      " = ",
      yRelativeToStart
    );
    // setXAtTouchPointStart(x); // Reseting relative point to current touch point
    // setYAtTouchPointStart(y);
    // set the element's new position:
    setTop(yRelativeToStart + "px");
    setLeft(xRelativeToStart + "px");
    console.log("top: ", yRelativeToStart + "px");
    console.log("Left: ", xRelativeToStart + "px");
  };

  const handleDragEnd = (e) => {
    // if (process.env.NODE_ENV === 'debug5' || process.env.NODE_ENV === 'development') {
    console.log(
      new Date(),
      "Coordinates of icon @ end X: " + e.clientX + " | Y: " + e.clientY
    );
  };

  const handleDragStart = (e, i) => {
    // From https://dev59.com/0Ww15IYBdhLWcg3wcrdC#69109382
    e.stopPropagation(); // let child take the drag
    e.dataTransfer.dropEffect = "move";
    e.dataTransfer.effectAllowed = "move";
    console.log(
      "Coordinates of icon @ start: X: " + e.clientX + " | Y: " + e.clientY
    );
    // console.log ('event: ', e)
    console.log("dragged from position n = ", i + 1);
  };
  return (
    <img
      ref={imgRef}
      className="hover:border-none border-4 border-solid border-green-600 mb-4"
      src={photo}
      alt="placeholder"
      style={styleForImg}
      onDragStart={(e) => handleDragStart(e, i)}
      onDragEnd={handleDragEnd}
      onTouchStart={(e) => handleTouchStart(e, i)}
      onTouchEnd={handleTouchEnd}
      onTouchMove={handleElementDrag}
    ></img>
  );
}

index.js:

import { StrictMode } from "react";
import ReactDOM from "react-dom";
import "./styles/index.pcss";

import App from "./App";

const root = document.getElementById("root");
ReactDOM.render(
  <StrictMode>
    <App />
  </StrictMode>,
  root
);

styles.css:

.Main {
  font-family: sans-serif;
  text-align: center;
}

/styles/index.pcss:

@tailwind base;

@tailwind components;

@tailwind utilities;

我无法让Tailwind的网格工作,所以我使用了实际的CSS内联样式。不知道为什么它们在CodeSandbox中无法正常工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接