关于元素拖动时的参考线

关于元素拖动时的参考线

·

6 min read

Table of contents

这个也是我之前的工作内容,想起来就记一下吧。

我们当时做的是一个低代码平台,能放进编辑器的实体组件(弹窗就不算)都是可以拖动的,有拖动那就会有对齐的问题,对不齐可难受了,所以也就会有参考线这个功能,辅助对齐。

这个功能我想分成几篇来写,因为功能上虽然不复杂,但可以“整活”的地方很多,这一篇就写基本实现吧。

拖动

首先是拖动部分,这部分我不打算自己做了,直接用 react-draggable 就好,他提供了相关的组件,我们只需要提供 onDragonStop 函数即可:

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 里,实际业务中也是这么干的,两方面吧:

  1. 组件状态要序列化同步到服务器,所以会把组件状态都保存成 JSON

  2. 如果用 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>
  )
}

触发参考线后,只需要把当前组件的位置移动过去就好了,当然了,最好的方式是判断最接近的参考线再移动,我这里没做到这一步。