当前位置: 首页 > Web前端 > HTML5

原生拖拽太拉跨了,纯JS自己手写一个拖拽效果,纵享丝滑

时间:2023-04-05 00:26:44 HTML5

原生拖拽太牵强,纯JS手写一个拖拽效果,享受丝般顺滑提??供了一套完整的事件机制,貌似是首选方案,但是没有那么漂亮,主要是它的样式太简单实现更高级的用户体验:这是浏览器默认的拖拽效果,点击拖拽任何图片或文字都会产生。因为之前有一个小项目,经常需要参考设计稿,一直在关注它的元素的拖拽效果(如下图),所以就以这个效果为蓝本来使用原生JS实现动态自定义拖拽效果,直接触摸即可。实现原理首先说说思路。我们需要知道鼠标的三个事件,分别是mousedown、mousemove、mouseup。当点击按下时,克隆出一个绝对定位的元素,并标记“拖放”状态,然后在mousemove中判断应该执行的具体方法,让元素随鼠标一起移动。在监听事件的事件对象中,有几个参数比较重要:clientX、clientY标识鼠标当前的横坐标和纵坐标,offsetX和offsetY表示相对偏移量,可以记录鼠标按下时的初始坐标。当mouseup鼠标抬起时,判断是否在目标区域,如果是,则根据鼠标当前获得的偏移量-初始坐标,获取元素在目标区域的实际位置。为了阅读方便,以下代码全部省略。演示GIF可能会掉帧。您可以在本文随附的文章末尾查看完整的源代码。代码量不多。基础界面先简单实现一个两栏布局界面,并应用一些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{位置:绝对;变换原点:左上;}}使用filterfilter:brightness(90%);调整亮度可以快速实现鼠标叠加的动态效果,无需额外遮罩:使用伪类激活抓取和抓取光标并设置抓取动作的图标:实现元素抓取,使用事件委托机制作为选择将mousedown事件监听添加到列表中。抓取的原理是在鼠标按下时克隆被按下的元素,并将被克隆的元素设置为绝对定位,使其“浮动”:letdragging=falseletcloneEl=null//克隆元素letinitial={}//初始化数据记录...//选中元素cloneEl=e.target.cloneNode(true)//克隆元素cloneEl.classList.add('flutter')//使其浮动e.target.parentElement.appendChild(cloneEl)//添加到列表中dragging=true//标记拖动的开始//TODO:初始化克隆元素的定位并记录下来,方便后面移动时计算位置.........flutter{位置:绝对;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...它的出生位置,并使用计时器为过渡设置动画。克隆的元素在相同的时间后被删除,因此有一个平滑稳定的回退动画。性能优化由于在改变元素状态的过程中需要频繁执行多个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...}效果演示,GIF略有掉帧:注意元素必须设置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:Elementremoval}判断是否在canvas中引发很简单,给canvas绑定mouseup监听事件即可,克隆出来的新元素必须delete无用attributes和class,此时设置元素的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()}Experience优化参考稿自定义设计中的元素拖拽直接赋值给原图。原图的大小通常是不可控的,必然需要加载时间,造成空白的问题。网速不够快的时候体验尤其尴尬:我的优化思路是利用浏览器加载传递同一张图片时先读取缓存的机制,先用一个Image加载原图,然后改src将被拖拽的元素加载到原图后,让浏览器“自动”帮我们优化这个过程,只是需要注意的是,由于这是一个异步任务,所以一定要进行相应的标记,否则很难控制手速快时的触发顺序。functionsimulate(url,flag){cloneEl.setAttribute('raw',url)constimage=newImage()image.src=urlimage.onload=function(){//异步任务,克隆节点可能不存在了,是否拖动flag标签还是当前目标地址以上就是文章的全部内容,感谢您阅读到这里,希望对您有所帮助或启发!创建起来并不容易。如果觉得文章写的不错,可以点个赞收藏一下支持一下。也欢迎您关注。我会持续更新实用的前端知识和技能。有一天我是茶无味(公众号:尝尝前端),期待与你一起成长~