如何为Facebook的react.js实现支持触摸事件的拖放?
有几个关于react.js 拖放的questions、articles 和libraries,但它们似乎都没有提到触摸事件,而且演示也没有在我的手机上运行。
总的来说,我想知道什么是最简单的:尝试使用现有的 d&d 库来实现这一点,这些库已经支持触摸,但可能需要一些工作才能与 React 正确配合。或者尝试使用任何 React d&d 示例,并使它们与触摸一起工作(看到这个问题,这可能不是微不足道的?)
我们尝试了“react-motion”来拖动列表中的项目。如果项目超过 15-20 个,它就会变得非常滞后。 (但是对于小列表,效果很好,就像这个demo)。请注意,移动设备比台式机慢得多。
关于react-motion的重要说明:在测试动画性能时不要忘记使用生产模式!
第二个选项是“react-dnd”。这是一个很棒的图书馆。它的级别较低,但是很容易理解如何使用它。但起初,“react-dnd”不是我们的选择,因为不支持触摸事件。
后来,当雅虎发布了react-dnd-touch-backend时,我们决定将我们的应用程序从“react-motion”切换到“react-dnd”。这解决了我们所有的性能问题。我们列出了 50-70 项,它按预期工作。
雅虎做了非常好的工作,该解决方案适用于我们的生产应用程序。
我还没有找到任何答案。接受的答案并不是真正的答案,但它指向一个 github 库。我将尝试仅使用 React 在这里包含一个完整的答案。
这样,代码应该是不言自明的,但是提前说几句话。我们需要使用大量状态变量来保持渲染之间的状态,否则任何变量都会被重置。为了使过渡平滑,我使用 useEffect 钩子在渲染完成后更新位置。我在codesandbox中对此进行了测试,我在here中添加了链接,供任何人编辑代码并使用它,只需分叉即可。它适用于 MS Surface Book2 Pro 和安卓。 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>
</>
);
}
照片.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>
</>
);
}
图像.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://stackoverflow.com/a/41993300/5078983
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://stackoverflow.com/a/69109382/15355839
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
);
样式.css:
.Main {
font-family: sans-serif;
text-align: center;
}
/styles/index.pcss:
@tailwind base;
@tailwind components;
@tailwind utilities;
我无法让顺风网格工作,所以我使用了实际的 css 内联样式。不知道为什么他们没有在codesandbox中。