关于元素拖动时的参考线
这个也是我之前的工作内容,想起来就记一下吧。
我们当时做的是一个低代码平台,能放进编辑器的实体组件(弹窗就不算)都是可以拖动的,有拖动那就会有对齐的问题,对不齐可难受了,所以也就会有参考线这个功能,辅助对齐。
这个功能我想分成几篇来写,因为功能上虽然不复杂,但可以“整活”的地方很多,这一篇就写基本实现吧。
拖动
首先是拖动部分,这部分我不打算自己做了,直接用 react-draggable
就好,他提供了相关的组件,我们只需要提供 onDrag
和 onStop
函数即可:
const initialData = [
{ id: 1, x: 50, y: 50, w: 200, h: 50, color: 'red' },
{ id: 2, x: 100, y: 100, w: 50, h: 100, color: 'green' },
{ id: 3, x: 30, y: 40, w: 30, h: 40, color: 'blue' }
// 更多元素...
];
const threshold = 10; // 参考线阈值
function App (props) {
const [data, setData] = React.useState(initialData);
const handleDrag = (id, x, y, isDragging) => {
let newData = data.map(item => item.id === id ? { ...item, x, y } : item);
setData(newData);
};
return (
<div className="container">
<div>
{data.map(item => (
<Draggable
key={item.id}
position={{ x: item.x, y: item.y }}
onDrag={(e, data) => handleDrag(item.id, data.x, data.y, true)}
onStop={(e, data) => handleDrag(item.id, data.x, data.y, false)}
>
<div
style={{
position: 'absolute',
width: item.w,
height: item.h,
border: `1px solid ${item.color}`
}}
>Drag me!</div>
</Draggable>
))}
</div>
</div>
)
}
这个应该不用解析吧,组件的位置和尺寸信息都放在 initialData
里,实际业务中也是这么干的,两方面吧:
组件状态要序列化同步到服务器,所以会把组件状态都保存成 JSON
如果用 DOM 自己的尺寸、位置属性,访问时会导致 layout,可能影响性能
参考线
先从思路上讲,在拖拽某个组件时,会通过 onDrag 去更新组件的位置,同时去和其他静止的组件做比较,如果两个组件过于接近(小于一个阈值),那么就把接触的位置记录下来画上参考线,同时可能有多条参考线。
所以不可避免地要拿所有组件的位置、尺寸去多次比较,在拖拽停止后,再把参考线全部清除即可。
const initialData = [
{ id: 1, x: 50, y: 50, w: 200, h: 50, color: 'red' },
{ id: 2, x: 100, y: 100, w: 50, h: 100, color: 'green' },
{ id: 3, x: 30, y: 40, w: 30, h: 40, color: 'blue' }
// 更多元素...
];
const threshold = 10; // 对齐线阈值
function App (props) {
const [data, setData] = React.useState(initialData);
const [lines, setLines] = React.useState([]); // 对齐线数据
const handleDrag = (id, x, y, isDragging) => {
const curData = data.find(item => item.id === id)
const otherData = data.filter(item => item.id !== id);
let newData = data.map(item => item.id === id ? { ...item, x, y } : item);
setData(newData);
// 如果正在拖拽,检测对齐线
if (isDragging) {
const newLines = []
otherData.forEach(item => {
// 左边对齐
if (Math.abs(x - item.x) <= threshold) {
newLines.push({ x1: item.x, y1: 0, x2: item.x, y2: window.innerHeight });
}
if (Math.abs(x - (item.x + item.w)) <= threshold) {
newLines.push({ x1: item.x + item.w, y1: 0, x2: item.x + item.w, y2: window.innerHeight });
}
// 右边对齐
if (Math.abs((x + curData.w) - item.x) <= threshold) {
newLines.push({ x1: item.x, y1: 0, x2: item.x, y2: window.innerHeight });
}
if (Math.abs((x + curData.w) - (item.x + item.w)) <= threshold) {
newLines.push({ x1: item.x + item.w, y1: 0, x2: item.x + item.w, y2: window.innerHeight });
}
// 上边对齐
if (Math.abs(y - item.y) <= threshold) {
newLines.push({ x1: 0, y1: item.y, x2: window.innerWidth, y2: item.y });
}
if (Math.abs(y - (item.y + item.h)) <= threshold) {
newLines.push({ x1: 0, y1: item.y + item.h, x2: window.innerWidth, y2: item.y + item.h });
}
// 下边对齐
if (Math.abs((y + curData.h) - item.y) <= threshold) {
newLines.push({ x1: 0, y1: item.y + item.h, x2: window.innerWidth, y2: item.y + item.h });
}
if (Math.abs((y + curData.h) - (item.y + item.h)) <= threshold) {
newLines.push({ x1: 0, y1: item.y + item.h, x2: window.innerWidth, y2: item.y + item.h });
}
})
setLines(newLines)
} else {
// 如果拖拽结束,清空对齐线
setLines([]);
}
};
return (
<div className="container">
<div>
{data.map(item => (
<Draggable
key={item.id}
position={{ x: item.x, y: item.y }}
onDrag={(e, data) => handleDrag(item.id, data.x, data.y, true)}
onStop={(e, data) => handleDrag(item.id, data.x, data.y, false)}
>
<div
style={{
position: 'absolute',
width: item.w,
height: item.h,
border: `1px solid ${item.color}`
}}
>Drag me!</div>
</Draggable>
))}
</div>
<svg style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' }}>
{lines.map((line, index) => (
<line
key={index}
x1={line.x1}
y1={line.y1}
x2={line.x2}
y2={line.y2}
stroke="red"
strokeWidth={0.5}
/>
))}
</svg>
</div>
)
}
这里我将被拖拽的组件作为 curData,剩余的静止组件作为 otherData,Math.abs(x - item.x) <= threshold
诸如此类的就是用来判断组件是否接近的,有满足条件的边时,就往 newLines
里添加参考线,最后把他们画出来。
吸附
吸附其实和参考线的判断一样,换一种说法,既然都画出参考线了,就表示当前的组件应该被吸附。
const initialData = [
{ id: 1, x: 50, y: 50, w: 200, h: 50, color: 'red' },
{ id: 2, x: 100, y: 100, w: 50, h: 100, color: 'green' },
{ id: 3, x: 30, y: 40, w: 30, h: 40, color: 'blue' }
// 更多元素...
];
const threshold = 10; // 对齐线阈值
function App (props) {
const [data, setData] = React.useState(initialData);
const [lines, setLines] = React.useState([]); // 对齐线数据
const handleDrag = (id, x, y, isDragging) => {
const curData = data.find(item => item.id === id)
const otherData = data.filter(item => item.id !== id);
let newData = data.map(item => item.id === id ? { ...item, x, y } : item);
setData(newData);
// 如果正在拖拽,检测对齐线
if (isDragging) {
const newLines = []
otherData.forEach(item => {
// 左边对齐
if (Math.abs(x - item.x) <= threshold) {
newLines.push({ x1: item.x, y1: 0, x2: item.x, y2: window.innerHeight });
}
if (Math.abs(x - (item.x + item.w)) <= threshold) {
newLines.push({ x1: item.x + item.w, y1: 0, x2: item.x + item.w, y2: window.innerHeight });
}
// 右边对齐
if (Math.abs((x + curData.w) - item.x) <= threshold) {
newLines.push({ x1: item.x, y1: 0, x2: item.x, y2: window.innerHeight });
}
if (Math.abs((x + curData.w) - (item.x + item.w)) <= threshold) {
newLines.push({ x1: item.x + item.w, y1: 0, x2: item.x + item.w, y2: window.innerHeight });
}
// 上边对齐
if (Math.abs(y - item.y) <= threshold) {
newLines.push({ x1: 0, y1: item.y, x2: window.innerWidth, y2: item.y });
}
if (Math.abs(y - (item.y + item.h)) <= threshold) {
newLines.push({ x1: 0, y1: item.y + item.h, x2: window.innerWidth, y2: item.y + item.h });
}
// 下边对齐
if (Math.abs((y + curData.h) - item.y) <= threshold) {
newLines.push({ x1: 0, y1: item.y + item.h, x2: window.innerWidth, y2: item.y + item.h });
}
if (Math.abs((y + curData.h) - (item.y + item.h)) <= threshold) {
newLines.push({ x1: 0, y1: item.y + item.h, x2: window.innerWidth, y2: item.y + item.h });
}
})
setLines(newLines)
} else {
if (lines.length) {
const newLines = []
otherData.forEach(item => {
// 左边对齐
if (Math.abs(item.x - x) <= threshold) {
x = item.x;
}
if (Math.abs(item.x - (x + curData.w)) <= threshold) {
x = item.x - curData.w;
}
// 右边对齐
if (Math.abs((item.x + item.w) - x) <= threshold) {
x = item.x + item.w;
}
if (Math.abs((item.x + item.w) - (x + curData.w)) <= threshold) {
x = item.x + item.w - curData.w;
}
// 上边对齐
if (Math.abs(item.y - y) <= threshold) {
y = item.y;
}
if (Math.abs(item.y - (y + curData.h)) <= threshold) {
y = item.y - curData.h;
}
// 下边对齐
if (Math.abs((item.y + item.h) - y) <= threshold) {
y = item.y + item.h;
}
if (Math.abs((item.y + item.h) - (y + curData.h)) <= threshold) {
y = item.y + item.h - curData.h;
}
})
newData = data.map(item => item.id === id ? { ...item, x, y } : item);
setData(newData);
}
// 如果拖拽结束,清空对齐线
setLines([]);
}
};
return (
<div className="container">
<div>
{data.map(item => (
<Draggable
key={item.id}
position={{ x: item.x, y: item.y }}
onDrag={(e, data) => handleDrag(item.id, data.x, data.y, true)}
onStop={(e, data) => handleDrag(item.id, data.x, data.y, false)}
>
<div
style={{
position: 'absolute',
width: item.w,
height: item.h,
border: `1px solid ${item.color}`
}}
>Drag me!</div>
</Draggable>
))}
</div>
<svg style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' }}>
{lines.map((line, index) => (
<line
key={index}
x1={line.x1}
y1={line.y1}
x2={line.x2}
y2={line.y2}
stroke="red"
strokeWidth={0.5}
/>
))}
</svg>
</div>
)
}
触发参考线后,只需要把当前组件的位置移动过去就好了,当然了,最好的方式是判断最接近的参考线再移动,我这里没做到这一步。