React MoveBoxComponent
npm i react-spring
import React, { useState, useRef, useEffect } from "react";
import { useSpring, animated } from "@react-spring/web";
interface IPosition {
x: number;
y: number;
}
interface IBoxSize {
width: number;
height: number;
}
// 检查是否应该将窗口贴边或中间移动
const checkSnapPosition = (
containerBoxSize: IBoxSize,
moveBoxSize: IBoxSize,
pos: IPosition,
snapMode: "always" | "smart" = "smart"
): IPosition => {
const sideMargin = 21; // 边缘的最小间距
const containerWidth = containerBoxSize.width;
const containerHeight = containerBoxSize.height;
const moveBoxWidth = moveBoxSize.width;
const moveBoxHeight = moveBoxSize.height;
// 计算安全边界
const maxX = containerWidth - moveBoxWidth - sideMargin;
const maxY = containerHeight - moveBoxHeight;
const posX = Math.min(Math.max(pos.x, sideMargin), maxX);
const posY = Math.min(Math.max(pos.y, 0), maxY);
// 始终贴边模式
if (snapMode === "always") {
const distanceToLeft = posX;
const distanceToRight = containerWidth - posX - moveBoxWidth;
const distanceToTop = posY;
const distanceToBottom = containerHeight - posY - moveBoxHeight;
const minDistance = Math.min(
distanceToLeft,
distanceToRight,
distanceToTop,
distanceToBottom
);
if (minDistance === distanceToLeft) return { x: sideMargin, y: posY };
if (minDistance === distanceToRight) return { x: maxX, y: posY };
if (minDistance === distanceToTop) return { x: posX, y: sideMargin };
return { x: posX, y: containerHeight - moveBoxHeight };
}
// 智能贴边模式
const SNAP_THRESHOLD = 100; // 中间区域阈值
const posCoefficient = containerHeight / containerWidth;
const isInMiddleX = posX > SNAP_THRESHOLD && posX < maxX - SNAP_THRESHOLD;
const isInMiddleY = posY > SNAP_THRESHOLD && posY < maxY - SNAP_THRESHOLD;
if (isInMiddleX && isInMiddleY) {
return { x: posX, y: posY };
}
const isLeftDown = posY / posX > posCoefficient;
const isRightDown = posY / (containerWidth - posX) > posCoefficient;
if (isLeftDown && isRightDown) {
return { x: posX, y: containerHeight - moveBoxHeight - sideMargin };
}
if (isLeftDown) return { x: sideMargin, y: posY };
if (isRightDown) return { x: maxX, y: posY };
return { x: posX, y: sideMargin };
};
function MoveBoxComponent() {
const [position, setPosition] = useState<IPosition>({ x: 0, y: 0 }); // moveBox 的位置
const isDragging = useRef<boolean>(false); // 是否正在拖动
const offset = useRef({ x: 0, y: 0 }); // 鼠标按下时的偏移量
const containerBoxSize = { width: 500, height: 500 }; // container的尺寸
const moveBoxSize = { width: 64, height: 64 }; // moveBox的尺寸
// 使用 react-spring 创建动画
const [springProps, api] = useSpring(() => ({}));
useEffect(() => {
initBoxPosition();
}, []);
const initBoxPosition = async () => {
const screenSize = {
width: window.screen.availWidth,
height: window.screen.availHeight,
};
// 初始位置设置在右下角贴边
const pos = {
x: screenSize.width,
y: screenSize.height,
};
const snapPosition = checkSnapPosition(containerBoxSize, moveBoxSize, pos);
// 更新位置
setPosition(snapPosition);
// 使用动画效果移动到初始位置
api.start({
from: { x: 0, y: 0 },
to: snapPosition,
config: { tension: 300, friction: 130 },
});
};
// 处理鼠标按下事件,初始化拖动
const handleMouseDown = (e: React.MouseEvent) => {
isDragging.current = true;
// 记录鼠标按下时的偏移
offset.current = {
x: e.clientX - position.x,
y: e.clientY - position.y,
};
// 禁用选中文本,防止拖动时文本被选择
document.body.style.userSelect = "none";
// 添加 mousemove 和 mouseup 事件监听
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
};
// 处理鼠标移动事件,更新 moveBox 的位置
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging.current) return;
const pos = {
x: e.clientX - offset.current.x,
y: e.clientY - offset.current.y,
};
// 更新位置
setPosition(pos);
// 使用动画效果
api.start({
from: pos,
to: pos,
config: { tension: 300, friction: 50 },
});
};
// 处理鼠标松开事件,停止拖动
const handleMouseUp = (e: MouseEvent) => {
isDragging.current = false;
const pos = {
x: e.clientX - offset.current.x,
y: e.clientY - offset.current.y,
};
// 检查是否需要贴边或吸附
const snapPosition = checkSnapPosition(containerBoxSize, moveBoxSize, pos);
// 更新位置
setPosition(snapPosition);
// 使用动画效果
api.start({
from: pos,
to: snapPosition,
config: { tension: 300, friction: 50 },
});
// 移除事件监听
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
// 恢复文本选择
document.body.style.userSelect = "auto";
};
const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
console.log("右键菜单");
};
const handleClick = () => {
console.log("单击事件");
};
const handleDoubleClick = () => {
console.log("双击事件");
};
return (
<div
id='container'
style={{
width: containerBoxSize.width,
height: containerBoxSize.height,
border: "1px solid #ccc",
margin: "100px auto",
position: "relative",
}}
>
{/* moveBox */}
<animated.div
style={{
width: moveBoxSize.width,
height: moveBoxSize.height,
borderRadius: "50%",
backgroundImage: "",
backgroundSize: "contain",
backgroundRepeat: "no-repeat",
backgroundPosition: "center",
boxShadow: "0px 0px 5px 1px rgba(0, 0, 0, 0.32)",
cursor: "pointer",
position: "absolute",
// top: position.y, // 无需动画时可放开注释,且注释 springProps
// left: position.x, // 无需动画时可放开注释,且注释 springProps
...springProps, // 使用动画效果
}}
onMouseDown={handleMouseDown}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
onContextMenu={handleContextMenu}
></animated.div>
</div>
);
}
export default MoveBoxComponent;
效果图如下:

本文来自博客园,作者:苏沐~,转载请注明原文链接:https://www.cnblogs.com/sumu80/p/18985095

浙公网安备 33010602011771号