CREATED DAY:20220206

自定义拖拽和旋转

拖拽和旋转是低代码平台中最基本的两个功能,笔者在最近的业务中也深有体会,决定自己也封装两个用于之后的自用。

旋转

整体的功能是实现一个组件,内部嵌入其他dom内容,对应内容上方就会出现一个旋转icon,鼠标控制它就能控制对应的dom内容的旋转。

<ShapeWrap rotateAngle={0}>
  <img src="xxxxx" height="100" draggable={false}/>
</ShapeWrap>

具体实现上需要关注鼠标点击时刻,鼠标持续点住以及鼠标松开这三个阶段,相对应地,分别是mousedown,mousemove以及mouseup三个阶段。

首先分析逻辑,在鼠标点击下去的那一刻,需要设置旋转状态为true,旋转中心以及当前的鼠标点击位置。之后在鼠标持续拖动的时候,需要不断计算当前鼠标位置和初始鼠标点击位置相对于旋转中心的夹角,然后将夹角赋值给内部dom作为形变角度。当鼠标松开后,需要将旋转状态设置为false.

核心代码如下所示,

onRotateStart(e: React.MouseEvent<Element, MouseEvent>) {
    e.stopPropagation();
    const handleMouseMove = (e: MouseEvent) => {
      e.stopImmediatePropagation();
      if (this.state.rotating) {
        this.setTransform(
          this.state.currentDeg +
            this.getDeg(
              [this.state.downPoint[0] - this.state.rotateCenter[0], this.state.downPoint[1] - this.state.rotateCenter[1]],
              [e.clientX - this.state.rotateCenter[0], e.clientY - this.state.rotateCenter[1]],
            ),
        );
      }
    };
    const handleMouseStop = (e: MouseEvent) => {
      this.setState({
        rotating: false,
        currentDeg:
          this.state.currentDeg +
          this.getDeg(
            [this.state.downPoint[0] - this.state.rotateCenter[0], this.state.downPoint[1] - this.state.rotateCenter[1]],
            [e.clientX - this.state.rotateCenter[0], e.clientY - this.state.rotateCenter[1]],
          ),
      });
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseStop);
    };
    const { width, height, left, top } = (this.movableDom.current as Element).getBoundingClientRect();
    this.setState({
      rotating: true,
      downPoint: [e.clientX, e.clientY],
      rotateCenter: [left + width / 2, top + height / 2],
    });
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseStop);
  }

其中需要注意的是,在鼠标松开后,需要及时地将点击事件和移动事件取消。而onRotateStart则会在鼠标点击时被触发。其中的getDeg函数则是计算夹角的数学工具函数

export const getAngle = (vector1: number[], vector2: number[]) => {
  const dot = vector1[0] * vector2[0] + vector1[1] * vector2[1];
  const det = vector1[0] * vector2[1] - vector1[1] * vector2[0];
  const angle = (Math.atan2(det, dot) / Math.PI) * 180;

  return (angle + 360) % 360;
};

拖拽

拖拽的整体思路其实是与旋转相似的,也是同样关注mousedown,mousemove和mouseup三个事件。

核心代码如下所示:

onDragStart(e: React.MouseEvent<Element, MouseEvent>) {
    const handleMove = (e: MouseEvent) => {
      if (this.state.dragging) {
        this.setState({
          diffVector: [e.clientX - this.state.startPos[0], e.clientY - this.state.startPos[1]],
        });
        this.setTransform(this.state.currentDeg);
      }
    };
    const handleEnd = (e: MouseEvent) => {
      this.setState({
        dragging: false,
      });
      document.removeEventListener('mousemove', handleMove);
      document.removeEventListener('mouseup', handleEnd);
    };

    let [diffX, diffY] = this.state.diffVector;
    this.setState({
      dragging: true,
			//计算点击位置的原始坐标
      startPos: [e.clientX - diffX, e.clientY - diffY],
    });
    document.addEventListener('mousemove', handleMove);
    document.addEventListener('mouseup', handleEnd);
  }

几乎和旋转的实现一模一样,只是在拖动的过程中,从算夹角变成了算位移向量。其中需要注意的是参数startPos,这里的含义指的是鼠标点击在图片的某个点,其在原始位置上的坐标。因为每次拖动的时候,鼠标点击的位置都不相同,因此计算相对位移值时,都需要逆推出其之前的原始坐标。从而在鼠标移动过程中,能够计算出整体的相对位移向量变化。

二者融合

其实这两者在大多数场景下,往往都是共存的。因此我们可以做一个结合。

首先定义一个MovableShape的组件,其props类型和state类型如下所示

type PropsType = {};
type StateType = {
  dragging: boolean;
  rotating: boolean;
  startPos: number[];
  diffVector: number[];
  rotateCenter: number[];
  downPoint: number[];
  currentDeg: number;
};

其他的核心整体和之前上述描述一样,只是在dom层结合过程中注意一下。

render() {
    return (
      <div
        className="movable-shape"
        ref={this.movableDom}
        onMouseDown={(e) => {
          this.onDragStart(e);
        }}
      >
        <div
          className="rotate-svg"
          onMouseDown={(e) => {
            this.onRotateStart(e);
          }}
        >
          <RotateSvg />
        </div>
        {this.props.children}
      </div>
    );
  }

最后效果如下图所示

具体地详细代码可以看

GitHub - Hujianboo/doodle-movable