当前位置: 首页 > 科技观察

原生JS手写丝滑流畅的元素拖拽效果

时间:2023-03-14 00:47:12 科技观察

前言说到元素拖拽,通常人们首先想到的是使用HTML5的拖放(DragandDrop)来实现。它提供了一套完整的事件机制,看到它似乎是首选的解决方案,但实际上并没有那么漂亮,主要是它的样式过于简单,无法实现更高级的用户体验:这是浏览器默认的拖放效果,单击并拖动任何图片或文本将生成。因为之前有一个小项目,经常需要参考设计稿,一直在关注它的元素的拖拽效果(如下图),所以就以这个效果为蓝本来使用原生JS实现动态自定义拖拽效果,直接触摸即可。实现原理首先说说思路。我们需要知道鼠标的三个事件,分别是mousedown、mousemove、mouseup。当点击按下时,克隆出一个绝对定位的元素,并标记“拖放”状态,然后在mousemove中判断应该执行的具体方法,让元素随鼠标一起移动。在监听事件的事件对象中,有几个参数比较重要:clientX,clientY标识的鼠标的当前横坐标和纵坐标,offsetX和offsetY代表相对偏移量,可以记录鼠标按下时的初始坐标向下。判断mouseup鼠标抬起时是否在目标区域,如果是,则使用鼠标获取的当前偏移量-初始坐标获取元素在目标区域的实际位置。为了阅读体验,以下代码全部省略。完整的源码地址可以在文末查看,代码量不多。基础界面先简单实现一个两栏布局界面,并应用一些CSS效果:

#app{宽度:100vw;高度:100vh;显示:flex;}.active{cursor:grabbing;}.slide{width:260px;高度:100%;溢出:滚动;border-right:1pxsolidrgba(0,0,0,.15);#list{用户选择:无;.item{背景:rgba(0,0,0,.15);宽度:120px;显示:内联块;闯入:避免;底部边距:4px;}.item:hover{光标:抓取;滤镜:亮度(90%);}.item:active{光标:抓取;}}.grid{列数:2;列间距:0px;}}.slide::-webkit-scrollbar{显示:无;/*ChromeSafari*/}#content{position:relative;弹性:1;高度:100%;左边距:45px;背景:RGBA(0,0,0,.07);.item{位置:绝对;变换原点:左上;}}使用滤镜filter:brightness(90%);调整亮度可以快速实现鼠标叠加的动态效果,无需额外遮罩Cover:使用伪类激活抓取,抓取光标设置抓取动作的图标:实现元素抓取,使用事件委托机制实现将mousedown事件监控添加到选择列表中。抓取的原理是当鼠标按下时克隆元素,并将克隆的元素设置为绝对定位,使其“浮动”:letdragging=falseletcloneEl=null//克隆元素letinitial={}//初始化数据记录...//选择元素cloneEl=e.target.cloneNode(true)//克隆元素cloneEl.classList.add('flutter')//使其浮动e.target.parentElement.appendChild(cloneEl)//添加到列表dragging=true//标记拖动开始//TODO:.........flutter{position:absolute;z-指数:9999;pointer-events:none;}设置鼠标坐标为被克隆元素的绝对定位值(left,top),如下图,此时减去offset,克隆元素可以叠加在上面身体需要记录初始化值,方便后续计算。同时,我们使用dragging变量来标记状态(dragging),然后我们就可以通过移动鼠标的监听事件来“抓取”元素了://mousemovementwindow.addEventListener("mousemove",(e)=>{if(dragging&&cloneEl){//TODO://x轴(左)的计算方法:e.clientX-initial.offsetX//y轴(上)的计算方法:e.clientY-initial.offsetY}})上面只是实现了元素的拖动,但是“克隆”的效果太明显了。为了让元素看起来更像是拖出来的而不是复制出来的,我们还需要将本体隐藏起来,DOM结构不能丢失。这时候只需要在按住拖拽的时候给body元素设置一个opacity:0,最后再改回opacity1即可。至此虽然实现了功能,但实际效果还是有点生硬。当参考草案设计中的元素被释放时,它们将被固定回一个位置,然后收回。这个过渡有点诡异,不够流畅。其实只需要让元素以自然的动画回退,就可以实现过渡:.is_return{transition:all0.3s;}//Mouseupwindow.addEventListener("mouseup",(e)=>{dragging=falseif(cloneEl){cloneEl.classList.add('is_return')//添加过渡动画changeStyle(...)//设置回元素的初始位置setTimeout((){cloneEl.remove()//Removeelement},300)}})最后我在动作的最后给被克隆的元素添加了一个transition属性,然后直接设置回初始坐标,让被克隆的元素回到它的诞生状态location,使用定时器继续过渡动画,克隆的元素在同一时间后被移除,所以有一个流畅稳定的回退动画。性能优化由于在改变元素状态的过程中需要频繁执行多个CSS操作,为了降低回流重绘的成本,最好将多个操作组合在一起。这里使用cssText来实现://改变浮动元素:x,y,scalingfunctionmoveFlutter(x,y,d=0){constscale=d?initial.width+d{if(dragging&&cloneEl){constd=distance(e)//计算距离moveFlutter(e.clientX-initial.offsetX,e.clientY-initial.offsetY,d)}})functionmoveFlutter(x,y,d=0){letscale=''//如果distance大于0,width+distance小于实际宽度if(d&&initial.width+d<=initial.fakeSize){scale=`transform:scale(${(initial.width+d)/initial.width})`}//TODO...changeStyle...}效果演示:注意必须设置元素transform-origin:topleft;将缩放原点更改为左上角,否则默认(中心为原点)转换将有更明显的偏移。实现放置其实拖放有点像“复制”和“粘贴”。我们之前实现过复制,放置主要是将元素粘贴到画布中。处理步骤如下:如果鼠标在目标区域,将元素复制到画布中,如果不在画布中,则执行向后动画删除元素//完成处理函数done(x,y){if(!cloneEl){return}constnewEl=cloneEl.cloneNode(true)newEl.classList.remove('flutter')newEl.src=cloneEl.getAttribute('raw')//设置原图地址newEl.style.cssText=`left:${x-initial.offsetX}px;顶部:${y-initial.offsetY}px;`文档。getElementById('content').appendChild(newEl)//TODO:}判断是否在画布中引发很简单,给画布绑定mouseup监听事件即可,克隆的新元素必须删除无用的属性和类,这个元素可以通过同时设置元素的left和top来放置到画布中。关键是canvas中的target可能是错误的,因为如果鼠标抬起的区域已经放置了元素,那么相对偏移就得自己定了。算了,用getBoundingClientRect方法得到画布本身相对于窗口的偏移量,鼠标坐标减去画布本身的偏移量就是元素在画布中的位置。document.getElementById('content').addEventListener("mouseup",(e)=>{if(e.target.id!=='content'){constlostX=e.x-document.getElementById('content').getBoundingClientRect().leftconstlostY=e.y-document.getElementById('content').getBoundingClientRect().topdone(lostX,lostY)}else{done(e.offsetX,e.offsetY)}})仅发布部分关键代码,完整代码见文末。边界判断如果不处理边界条件,拖动过程中可能会出现意外中断,无法正确回收克隆的元素。//鼠标离开了窗口document.addEventListener("mouseleave",(e){end()})//用户可能离开了浏览器window.onblur=(){end()}体验优化指的是元素设计中直接拖放指定原图。原始图像的大小通常是不可控的。必然会占用一个loading时间,导致出现空白的问题。网速不够快的时候体验尤其尴尬:我的优化思路是用浏览器加载同一张图片。它会优先采用读取缓存的机制,先用一个Image加载原图,加载完成后将被拖动元素的src改为原图,这样浏览器就会“自动”优化这个过程对我们来说,只要注意一下,既然是异步任务,就必须做相应的标记,否则手速快的时候很难控制触发顺序。functionsimulate(url,flag){cloneEl.setAttribute('raw',url)constimage=newImage()image.src=urlimage.onload=function(){//异步任务,克隆节点可能不存在了,是否拖动flag标签还是当前目标,感谢阅读来到这里,希望对您有所帮助或启发!创建起来并不容易。如果觉得文章写的不错,可以点个赞收藏一下支持一下。也欢迎您关注。我会更新更多实用的前端知识和技能。我是一个无味的茶日。期待与你一起成长~相关链接[1]完整代码地址:https://juejin.cn/post/7145447742515445791/#heading-9[2]作者简介:https://book.palxp.com

猜你喜欢