【青训营】- 图解拖拽核心原理

925 阅读6分钟

青训营项目难点总结—— 图解拖拽核心原理

概述

虽然市面上有很多类似的插件,但是为了研究原理打算还是是自己实现一下,希望通过此次实战可以弄懂实现原理 首先大概先了解一下定位相关的原理,为了方便理解,我花了一些时间做了张图示,标识了关键的位置信息都代表了什么 拖拽坐标图解.png

上图可以很清楚的知道当前元素距离左和上的坐标

样式实现

窗口使用position:fixed进行定位,通过left,top控制位置,widthheight控制元素宽高

拖拽移动js原理

实现步骤:

  • 求鼠标点击距离div元素边缘的距离 deltaX
  • 鼠标位置 减去 deltaX 求得 div元素距离浏览器左边的距离left

分析:

由于是点击div内任意位置都可以拖动位置,但是div的坐标是由left决定的,left是div边缘到浏览器窗口左边的距离, 所以要先计算出点击事件 距离div左边的距离,这个距离是鼠标相对div的相对距离,移动中保持不变,然后用e.clientX减去这个距离,就得出移动之后left的距离,因为e.clientX是鼠标当前位置距离浏览器窗口左边的距离。同样的原理可以计算出top的距离

避免拖动任意元素都移动

判断e.target的class是否是标题栏或者body,如果不是则不进行拖动

问:为什么要给document绑定mousemove而不是当前元素?

因为如果给当前元素绑定mousemove则鼠标只能在当前元素移动会生效,如果鼠标超出当前元素则会失效

核心代码如下:

   el.onmousedown = function (e) {
        var disx = e.clientX - el.offsetLeft
        var disy = e.clientY - el.offsetTop
        // console.log(e.target)
        // console.log(e.target.className)
        const className = e.target.className
        if (["window-body body", "bar", "title"].includes(className)) {
          document.onmousemove = function (e) {
            const end = new Date().getTime()
            if (end - start > 100) {
              el.style.left = e.clientX - disx + "px"
              el.style.top = e.clientY - disy + "px"
            }
          }
          document.onmouseup = function () {
            document.onmousemove = document.onmouseup = null
          }
        }
      }

拖拽调整宽高js原理:

样式定义:

设置一个热区用来拖拽,这个热区要超过div元素本身,露出可拖拽的边 并设置透明,所以他的上下左右值都要设置为负值

实现步骤:

  • 判断点击的位置(上下左右还是四个角)
  • 根据方向更改鼠标样式
  • 根据方向结合不同不同的算法,计算上下左右如何调整坐标和宽高

分析原理

判断方向

通过position = getBoundingClientRect获取div的上下左右边距离浏览器左和上的距离,然后通过event.clientX获取当前点击位置距离左侧和距离浏览器上部 的距离,通过判断对比这两个值的大小就可以判断出来,比如e.clientX如果小于position.left,那么点击的就是左侧,如果e,clientX大于position.right那么就是点的右边,以此类推,可以判断出鼠标点击的各个方向,如果是对角,比如左上角,那么就是clientX要大于left并且clientY要小于top 代码如下:

	function getDirect(disX, disY, position) {
          let direct
          let horizenDirect
          let verticalDirect
          const { left, right, top, bottom } = position
          if (disX <= left) {
            horizenDirect = "left"
          } else if (disX >= right) {
            horizenDirect = "right"
          }

          if (disY <= top) {
            verticalDirect = "top"
          } else if (disY >= bottom) {
            verticalDirect = "bottom"
          }

          if (verticalDirect == "top" && horizenDirect == "left") {
            direct = "left-top"
          } else if (verticalDirect === "top" && horizenDirect === "right") {
            direct = "right-top"
          } else if (verticalDirect === "bottom" && horizenDirect === "left") {
            direct = "left-bottom"
          } else if (verticalDirect === "bottom" && horizenDirect == "right") {
            direct = "right-bottom"
          } else if (verticalDirect) {
            direct = verticalDirect
          } else if (horizenDirect) {
            direct = horizenDirect
          }
          return direct
        }

设置鼠标样式

实现步骤:
  • 监听mouseenter事件
  • 通过之前判断的方向,更改鼠标的cursor样式

代码如下:

 	function updateCursor(direct) {
          switch (direct) {
            case "left":
              el.style.cursor = "w-resize"
              break
            case "right":
              el.style.cursor = "e-resize"
              break
            case "bottom":
              el.style.cursor = "s-resize"
              break
            case "top":
              el.style.cursor = "n-resize"
              break
            case "right-bottom":
              el.style.cursor = "se-resize"
              break
            case "left-bottom":
              el.style.cursor = "sw-resize"
              break
            case "right-top":
              el.style.cursor = "ne-resize"
              break
            case "left-top":
              el.style.cursor = "nw-resize"
              break
            default:
              el.style.cursor = "auto"
          }
        }
        el.onmouseenter = function (e) {
          e.stopPropagation()
          const disX = e.clientX // 获取鼠标按下时光标x的值
          const disY = e.clientY // 获取鼠标按下时光标Y的值
          const position = win.getBoundingClientRect() //父级元素坐标
          const direct = getDirect(disX, disY, position) //获取方向
          updateCursor(direct)
        }

根据方向动态计算每个方向的横纵坐标和宽高度

分析原理:

就哪从拖动左边举例,首先拖动左边,从左向右拖动,则div的宽度应该减少,并且div的横坐标应该是增加的,此时纵坐标不变,高度也不变化,所以宽度减少的值就是拖动产生的变量deltaX,由于宽度应该是减少的,所以这个deltaX应该为负值再加上原本的宽度就是拖拽之后的宽度,而移动的距离恰好也是这个deltaX,但是由于横坐标是增加的,所以增加的值应该是-deltaX,最后加上原本的横坐标就是移动之后的横坐标。纵坐标的原理与之类似不再赘述。如果从右边往左拖动,则宽度减少,高度不变,但是div左侧的坐标并没有移动,所以left不应该变

代码如下:

  //宽高限制
        const limit = {
          w: 200,
          h: 200
        } 	
	 el.onmousedown = function (e) {
          e.stopPropagation()

          const disX = e.clientX // 获取鼠标按下时光标x的值
          const disY = e.clientY // 获取鼠标按下时光标Y的值
          const disW = win.offsetWidth // 获取拖拽前div的宽
          const disH = win.offsetHeight // 获取拖拽前div的高

          const position = win.getBoundingClientRect() //父级元素坐标
          const direct = getDirect(disX, disY, position) //获取方向
          updateCursor(direct) //更新鼠标指针

          document.onmousemove = (d) => {
            d.stopPropagation()
            let w, h
            let x, y
            switch (direct) {
              case "left":
                w = disX - d.clientX + disW
                w = w < limit.w ? limit.w : w //宽度限制
                h = disH
                x = position.left + d.clientX - disX //左侧移动
                break
              case "right":
                w = d.clientX - disX + disW
                w = w < limit.w ? limit.w : w //宽度限制
                h = disH
                break
              case "bottom":
                w = disW
                h = d.clientY - disY + disH
                break
              case "top":
                w = disW
                h = disY - d.clientY + disH
                h = h < limit.h ? limit.h : h //高度限制
                y = position.top + d.clientY - disY
                break

              case "left-top":
                x = position.left + d.clientX - disX
                y = position.top + d.clientY - disY
                w = disX - d.clientX + disW
                h = disY - d.clientY + disH
                w = w < limit.w ? limit.w : w //宽度限制
                h = h < limit.h ? limit.h : h //高度限制
                break
              case "left-bottom":
                x = position.left + d.clientX - disX //左侧移动
                w = disX - d.clientX + disW
                h = d.clientY - disY + disH
                w = w < limit.w ? limit.w : w //宽度限制

                break
              case "right-top":
                w = d.clientX - disX + disW
                h = disY - d.clientY + disH
                y = position.top + d.clientY - disY
                w = w < limit.w ? limit.w : w //宽度限制
                h = h < limit.h ? limit.h : h //高度限制
                break
              case "right-bottom":
                w = d.clientX - disX + disW
                h = d.clientY - disY + disH
                w = w < limit.w ? limit.w : w //宽度限制
                h = h < limit.h ? limit.h : h //高度限制
                break
            }

            win.style.width = w + "px" // 拖拽后物体的宽
            win.style.height = h + "px" // 拖拽后物体的高
            if (x) {
              win.style.left = x + "px"
            }
            if (y) {
              win.style.top = y + "px"
            }
          }

总结:

我是采用vue指令实现,便于操作dom,使用起来感觉也较为方便,细节还是有优化的空间,比如性能优化,现在感觉拖拽有时候会卡顿,总的来说,通过这次集训的项目实战,对于之前有点模糊的事件位置相关知识点更加清晰了一些,也对拖拽的实现有了更清晰的认识。这算是此次实战的主要收获。

完整代码地址:

gitee.com/nabaonan/ma…

参考资料:

blog.csdn.net/bbsyi/artic… www.jb51.net/article/133… www.jianshu.com/p/824eb6f9d…