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

news2025/1/18 19:09:53

前言

提到元素拖拽,通常都会先想到用 HTML5 的拖拽放置 (Drag 和 Drop) 来实现,它提供了一套完整的事件机制,看起来似乎是首选的解决方案,但实际却不是那么美好,主要是它的样式太过简陋,无法实现更高级的用户体验:

其实这是浏览器默认的一种拖拽效果,随便拖拽任意的图片都会产生(包括文字):

笔者因为之前有个小项目需要经常参考稿定设计,一直有留意其元素拖拽的效果(如下图),所以接下来我将以这种效果为蓝本,使用原生 JS 实现一个富有动感的 自定义拖拽 效果,话不多说直接开摸。

实现原理

首先说下思路,我们需要知道鼠标的三个事件,分别是 mousedownmousemovemouseup ,当点击按下的时候,克隆一个绝对定位的元素,并标识下"拖拽中"的状态,接着在 mousemove 中就可以判断应该执行的具体方法,从而让元素随着鼠标移动起来。

在监听事件的 event 对象中,有几个参数是比较重要的:clientXclientY 标识的鼠标当前横坐标和纵坐标,offsetXoffsetY 表示相对偏移量,可以在 mousedown 鼠标按下时记录初始坐标,在 mouseup 鼠标抬起时判断是否在目标区域中,如果是则用鼠标获取到的当前的偏移量 - 初始坐标得到元素实际在目标区域中的位置。

基础界面

先简单实现一个两栏布局界面,并应用上一些 CSS 效果:

<div id="app"><div class="slide"><div id="list"><img class="item" src="......." /><img.........</div></div><div class="content"></div>
</div> 
#app {width: 100vw;height: 100vh;display: flex;
}
.active {cursor: grabbing;
}

.slide {width: 260px;height: 100%;overflow: scroll;border-right: 1px solid rgba(0,0,0,.15);#list {user-select: none;.item {background: rgba(0,0,0,.15);width: 120px;display: inline-block;break-inside: avoid;margin-bottom: 4px;}.item:hover {cursor: grab;filter: brightness(90%);}.item:active {cursor: grabbing;}}.grid {column-count: 2;column-gap: 0px;}
}
.slide::-webkit-scrollbar {display: none; /* Chrome Safari */
}

#content {position: relative;flex: 1;height: 100%;margin-left: 45px;background: rgba(0,0,0,.07);.item {position: absolute;transform-origin: top left;}
} 

利用滤镜 filter: brightness(90%); 调节明亮度可以快速实现一个鼠标覆盖的动态效果,无需额外制作遮罩:

使用伪类激活 cursorgrabgrabbing 可以设置抓取动作的图标:

实现元素抓取

利用事件委托机制为选择列表添加 mousedown 事件监听,实现抓取的原理是在鼠标按下时克隆按下的元素,并把克隆出来的元素设置成绝对定位,让它"浮"起来:

let dragging = false
let cloneEl = null // 克隆元素
let initial = {} // 初始化数据记录
......
// 选中了元素
cloneEl = e.target.cloneNode(true) // 克隆元素
cloneEl.classList.add('flutter') // 使其浮动
e.target.parentElement.appendChild(cloneEl) // 加入到列表中
dragging = true // 标记拖动开始

// TODO: 初始化克隆元素的定位并记录,方便后面移动时计算位置
........ 
.flutter {position: absolute;z-index: 9999;pointer-events: none;
} 

将鼠标的坐标设置为克隆元素的绝对定位值(lefttop),就会像下图所示这样,此时减去 offset 偏移量,就能让克隆元素覆盖在本体上面。

初始化的值需要记录起来方便后续计算,同时我们用 dragging 变量标记了状态(拖动中),接下来配合移动鼠标的监听事件就能将元素“抓”起来了:

// 鼠标移动
window.addEventListener("mousemove", (e) => {if (dragging && cloneEl) {// TODO: 处理元素的移动:改变 left top 定位// x 轴(left)计算方法:e.clientX - initial.offsetX// y 轴(top)计算方法:e.clientY - initial.offsetY}
}) 

上面只是实现了元素的拖动,但是"克隆"的效果实在太明显了,为了让元素看起来更像是拖出来的而不是复制出来的,我们还要让本体隐藏,同时DOM结构不能丢失,这时只需在按下拖动时给本体元素设置个 opacity: 0,结束时再改回透明度1就能搞定。

虽然到这功能就算实现了,但实际效果还是有点僵硬,参考稿定设计中的元素放开时会固定回到一个位置,然后再收回去,这个过渡又有点鬼畜,不够流畅。其实只需让元素回退过程有一个自然地动画就行,transition 就能实现:

.is_return {transition: all 0.3s;
} 
// 鼠标抬起
window.addEventListener("mouseup", (e) => {dragging = falseif (cloneEl) {cloneEl.classList.add('is_return') // 加上过渡动画changeStyle(......) // 设置回元素的初始位置setTimeout(() => {cloneEl.remove() // 移除元素}, 300)}
}) 

最终我在动作结束时给克隆元素添加了过渡属性,然后直接设置回初始坐标让克隆元素回到它的出生地点,用定时器在过渡动画持续的相同时间后移除克隆元素,这样就有了一个平滑稳定的回退动画。

性能优化

由于在改变元素状态的过程中需要频繁进行多个 CSS 操作,为降低回流重绘的成本,最好将多个操作合并起来处理,这里利用了 cssText 来实现:

// 改变漂浮元素:x、y、缩放倍率
function moveFlutter(x, y, d = 0) {const scale = d ? initial.width + d < initial.fakeSize ? `transform: scale(${(initial.width + d) / initial.width})` : null : nullconst options = [`left: ${x}px`, `top: ${y}px`]scale && options.push(scale)// 将CSS处理成数组,然后丢进DOM操作方法中一次执行changeStyle(options)
}
// 合并多个操作
function changeStyle(arr) {const original = cloneEl.style.cssText.split(';')original.pop()cloneEl.style.cssText = original.concat(arr).join(';') + ';'
} 

实现拖拽放大

放大我们可以使用 transform: scale 来实现,只需要将拖动位置之间的距离当做变化系数(假设为d),那么scale变化数值即为(元素宽度 + d)/元素宽度,而放大的最终倍数必定为 图片实际宽度/元素的宽度,只要判断不超过这个边界就可以。(这个图片实际宽高在真实业务场景中建议在上传资源时就记录在数据库,这里我是模拟的随机一个原图尺寸)。

两点间距离计算公式为 (x1−x2)2+(y1−y2)2\sqrt{(x_1-x_2)^2 + (y_1-y_2)^2}(x1​−x2​)2+(y1​−y2​)2​,代码实现:

// 计算两点之间距离
function distance({ clientX, clientY }) {const { clientX: x, clientY: y } = initial // 获取初始的坐标const b = clientX - x;const a = clientY - y;return Math.sqrt(Math.pow(b, 2) + Math.pow(a, 2))
}

window.addEventListener("mousemove", (e) => {if (dragging && cloneEl) {const d = distance(e) // 计算距离moveFlutter(e.clientX - initial.offsetX, e.clientY - initial.offsetY, d)}
})
function moveFlutter(x, y, d = 0) {let scale = ''// 如果距离大于0,且宽度+距离小于实际宽度if( d && initial.width + d <= initial.fakeSize ) {scale = `transform: scale(${(initial.width + d) / initial.width})`}// TODO ... changeStyle ...
} 

效果演示,GIF稍微掉帧:

注意元素都要设置 transform-origin: top left; 改变缩放原点到左上角,否则默认(中心为原点)的转换会发生比较明显的偏移。

实现放置

其实拖拽放置有点像是"复制"与"粘贴",前面我们实现了复制,放置主要就是将元素粘贴到画布当中,流程步骤如下:

1.如果鼠标在目标区域,拷贝元素到画布中,如果不在画布中,执行倒退动画
2.删除元素

// 完成处理
function done(x, y) {if (!cloneEl) { return }const newEl = cloneEl.cloneNode(true)newEl.classList.remove('flutter')newEl.src = cloneEl.getAttribute('raw') // 设置原图地址newEl.style.cssText = `left: ${x - initial.offsetX}px; top: ${y - initial.offsetY}px;`document.getElementById('content').appendChild(newEl)// TODO: 元素移除
} 

判断是否在画布内抬起很简单,往画布上绑定mouseup监听事件即可,克隆的新元素必须删除无用的属性和class,此时设置元素的lefttop即可将元素放置进画布中,关键点在于画布内的target有可能是错的,因为如果鼠标抬起的区域已经放置了元素,那么相对偏移量就得我们自己计算了,使用getBoundingClientRect方法获取画布本身相对于视窗的偏移,鼠标坐标减去画布本身的偏移就是元素在画布中的位置了。

document.getElementById('content').addEventListener("mouseup", (e) => {if (e.target.id !== 'content') {const lostX = e.x - document.getElementById('content').getBoundingClientRect().leftconst lostY = e.y - document.getElementById('content').getBoundingClientRect().topdone(lostX, lostY)} else { done(e.offsetX, e.offsetY) }
}) 

只贴了部分关键代码,完整代码建议 在码上掘金中查看。

边界判断

如果不对边界情况进行处理可能会导致拖动时发生意外的中断,无法正确回收克隆元素。

// 鼠标离开了视窗
document.addEventListener("mouseleave", (e) => {end()
})
// 用户可能离开了浏览器
window.onblur = () => {end()
} 

体验优化

参考稿定设计中元素拖拽是直接赋值原图的,原图大小通常无法控制,免不了需要加载时间,造成卡顿空白的问题,在网络不够快时体验尤其尴尬:

我的优化思路是利用浏览器加载过同一张图片就会优先读缓存的机制,先用一个Image加载原图,等其加载完毕再把拖拽元素的src改成原图,这样浏览器会"自动"帮我们优化这个过程,只需要注意一点,由于这是个异步任务,所以一定要做好对应标记,不然手速快的时候控制不好触发顺序。

function simulate(url, flag) {cloneEl.setAttribute('raw', url)const image = new Image()image.src = urlimage.onload = function () {// 异步任务,克隆节点可能已不存在,flag标记是否拖动的还是当前目标cloneEl && initial.flag === flag && (cloneEl.src = url)}
} 

效果演示,故意加大了图片的分辨率差异:

最后

最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/93360.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

spring boot实现文件上传和下载

文章目录 spring boot实现文件上传 1.文件上传 2.文件下载 1.文件上传 1.代码如下&#xff1a; package com.example.demo; import org.omg.PortableInterceptor.ServerRequestInfo; import org.springframework.stereotype.Controller; import org.springframework.we…

k8s 综合部署练习(针对负载均衡+外网访问web)

部署需求 &#xff08;1&#xff09;Kubernetes 区域可采用 Kubeadm 方式进行安装。 &#xff08;2&#xff09;要求在 Kubernetes 环境中&#xff0c;通过yaml文件的方式&#xff0c;创建Deployment控制器和4个Nginx Pod副本&#xff0c;并进入容器中修改index.html文件内容&…

【12月13日更新半决赛预测】用二元泊松模型预测2022年世界杯淘汰赛结果

用二元泊松模型预测2022年世界杯淘汰赛结果 网上有很多文章用双泊松&#xff08;Double Poisson&#xff09;模型来预测世界杯比赛结果。但是双泊松模型有一个严重的缺陷&#xff0c;那就是它假设比赛中两队的比分是条件独立的。而我们都知道&#xff0c;在对抗性比赛中&…

2022年拼多多元旦节会搞活动吗?拼多多有节日活动吗?

2022年拼多多元旦节会搞活动吗?拼多多有节日活动吗? 离春节只有一个月半的时间了&#xff0c;而且双十二也已经过了。要想等平台的活动&#xff0c;就只有元旦节和年货节了。有小伙伴想知道&#xff0c;拼多多元旦节会搞活动吗? 一、拼多多有节日活动吗? 拼多多购物节主要…

数据服务门槛再提升,这个“TOP1玩家”凭何再度领军?

在人工智能领域&#xff0c;数据的重要程度正在迅速提升。 根据ML大牛吴恩达提出的著名二八定律&#xff1a;80%数据20%模型更好的AI。他认为&#xff0c;一个机器学习团队80%的工作应该放在数据准备上&#xff0c;确保数据质量是最重要的工作&#xff0c;每个人都知道应该如此…

什么是元数据

元数据 元数据是描述数据的数据&#xff0c;关于数据的组织、数据域及其关系&#xff0c;本质上是关于数据的信息。元数据以数字化方式描述企业的数据、流程和应用程序&#xff0c;为企业数字资产的内容提供了上下文&#xff0c;使得数据更容易理解、查找、管理和使用。 元数据…

计算机毕设Python+Vue校园跳蚤平台(程序+LW+部署)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Nginx教程(1)

文章目录1.1 简介1.2 常见的服务器1.3 反向代理1.4 Nginx的安装与运行1.5 Nginx的进程模型1.6 Nginx处理Web请求机制解析1.7 Nginx核心配置文件nginx.conf解析1.8 Nginx常用命令解析1.9 Nginx日志切割1.10 Nginx为静态资源提供服务1.11 使用Gzip压缩提高请求效率1.12 location匹…

React源码分析(二)渲染机制

准备工作 为了方便讲解&#xff0c;假设我们有下面这样一段代码&#xff1a; function App(){const [count, setCount] useState(0)useEffect(() > {setCount(1)}, [])const handleClick () > setCount(count > count)return (<div>勇敢牛牛, <sp…

java计算机毕业设计基于安卓Android的急救服务APP

项目介绍 随着信息技术和网络技术的飞速发展,人类已进入全新信息化时代,传统管理技术已无法高效,便捷地管理信息。为了迎合时代需求,优化管理效率,各种各样的管理系统应运而生,各行各业相继进入信息管理时代,急救服务系统就是信息时代变革中的产物之一。 任何系统都要遵循系统设…

Android9.0 Fiddler 模拟器抓包

目录 一、生成Fiddler证书并安装 二、制作证书 三、adb的配置 四、安装证书到Android手机 五、抓包 六、总结 一、生成Fiddler证书并安装 1.到官网下载fiddler插件 https://www.telerik.com/fiddler/add-onshttps://www.telerik.com/fiddler/add-ons 2.官网插件工具很多&a…

TCP/IP HTTP WebSocket Socket 路由

最近在写一个上位机&#xff0c;需要将采集到的数据上传到云平台&#xff0c;然后就考虑到使用WebSocket实现&#xff0c;但是WebSocket和Socket有啥区别&#xff0c;这两个东西分别是个啥&#xff0c;咱也不清楚&#xff0c;然后就查资料&#xff0c;发现有好多之前想了解但是…

Java面试基础篇-IO

UNIX提供5种I/O模型 var code “7cfcb088-556d-478a-b21d-12b255236dbd” BIO模型 在进程空间调用recvfrom时被阻塞,直到有数据才返回。 NIO模型 调用recvfrom时先返回EWOULDBLOCK错误&#xff0c;然后轮询是否有数据。 I/O复用 linux提供select/poll&#xff0c;其支…

Minecraft 1.19.2 Forge模组开发 07.拼图建筑(jigsaw)

如果你看过之前的Minecraft 1.19.2建筑生成的话&#xff0c;想必会更好理解这篇教程。 效果演示效果演示效果演示 1.我们本期准备生成的建筑分为4块&#xff0c;所以首先需要用4个结构方块将整个建筑包括起来&#xff1a; 2.之后我们需要用指令拿出拼图方块: give p minecraf…

数据结构与算法——Java实现递归、迷宫回溯问题、八皇后问题

目录 一、递归 1.1 介绍递归 二、迷宫回溯问题 2.1 代码实现 三、八皇后问题 3.1 基本介绍 3.2 分析思路 3.3 代码实现 一、递归 1.1 介绍递归 简单的说&#xff1a;递归就是方法自己调用自己&#xff0c;每次传入不同的变量。 递归有助于编程者解决复杂的问题&#x…

Efficient Zero-shot Event Extraction with Context-Definition Alignment论文解读

Efficient Zero-shot Event Extraction with Context-Definition Alignment code&#xff1a;tencent-ailab/ZED: This is the repository for EMNLP 2022 paper “Efficient Zero-shot Event Extraction with Context-Definition Alignment” (github.com) paper&#xff1a;…

【手把手】分布式定时任务调度解析之Elastic-Job

1、这货怎么没怎么听过 经常使用Quartz或者Spring Task的小伙伴们&#xff0c;或多或少都会遇到几个痛点&#xff0c;比如&#xff1a; 1、不敢轻易跟着应用服务多节点部署&#xff0c;可能会重复多次执行而引发系统逻辑的错误&#xff1b; 2、Quartz的集群仅仅只是用来HA&…

业主应该重视装修中的“道”而不是“术”!极家精工装修好不好!

业主应该重视装修中的“道”而不是“术”&#xff01;极家精工装修好不好&#xff01;看了很多业主问了很多关于装修中很琐碎的事儿&#xff0c;比如“装修流程”、“装修应该注意什么”、“装修哪些必须要重视”、“某某材料和某某材料相比哪个好”、“家里装了什么是你最不后…

Lua中的基本数据类型

Lua中的数据类型一、Lua基本数据类型1.1、nil1.2、boolean1.3、number1.4、string1.5、function1.6、table二、Lua 通用数据结构的实现总结后言Lua是一门动态类型的脚本语言&#xff0c;这意味着同一个变量可以在不同时刻指向不同类型的数据。Lua代码中 一般采用一下两种做法相…

Dubbo-admin+Zookeeper 的环境搭建实操与 Could-not-extract-archive 报错踩坑

$ brew install zookeeper > Downloading https://homebrew.bintray.com/bottles/zookeeper-3.4.13.mojave.bottle.tar.gz ...先来看dubbo-admin的安装&#xff1b;我们先找到它在apache下的官方GitHub&#xff0c;官方也有相关介绍&#xff0c;中英文版都有(毕竟原本是中国…