教你实现图片的点击缩放和移动
为了方案的通用性,这回使用基本的html+js进行操作,vue和react使用方法类似,几乎不需要进行什么语法转换操作,注意一下点击事件在自己框架里的写法即可
随便来写一个简单的页面:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>
<p class="list">
文本内容1
</p>
<img onclick="imgClick(event)"
src="./img/img.JPG"
width="150"
height="150"
style="object-fit: cover;">
</div>
</body>
</html>
<style>
body {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
</style>
大概长这个样子:
一般项目中缩略图都是固定尺寸,但是原始尺寸不规则,防止缩小后伸缩异常,最好加上object-fit: cover;
属性,如果感兴趣可以参考文档
既然点击放大,那么我们首先来加上点击事件
点击事件
<img onclick="imgClick(event)">
<!-- 其余代码省略 -->
<script>
function imgClick(e) {
console.log(e);
}
</script>
点击事件对象继承了Event
,所以你可以通过e.target
获取dom上的信息
在进行图片放大的时候,我们最需要的就是img
标签上的src
图片路径,我们这里新创建图片放大的工具方法,并把图片的路径作为参数。
function imgClick(e) {
showImagePreview(e.target.src)
}
function showImagePreview(url) {
// 图片放大工具方法
}
那么点击放大的方法,实际就是生成两个dom元素,一个遮罩层还有一个不限制宽高的图片标签,按照这种思路,我们来写一个最基本的放大方法:
// 外部定义dom元素对象,方便后续缩放移动使用
let div = null
let img = null
function showImagePreview(url) {
div = document.createElement("div")
div.style = {
position: "fixed",
top: "0",
bottom: "0",
left: "0",
right: "0",
backgroundColor: "rgba(0,0,0,0.8)",
display: "flex",
justifyContent: "center",
alignItems: "center",
zIndex: "1000",
}
img = document.createElement("img")
img.src = url
img.style = {
position: "fixed",
}
div.appendChild(img)
// 遮罩层加一个点击事件,点击遮罩删除元素,回到初始画面
div.onclick = () => document.body.removeChild(div);
document.body.appendChild(div)
}
这里生成一个div
作为遮罩层,并添加了简单样式,然后又生成了img
标签,并将上面获取到的图片路径给它,最后把img
扔到遮罩上,遮罩再扔到body
上,现在点击已经可以实现图片放大效果了。如果你使用前端框架,那么上面也可以使用jsx
封装组件。
滚轮缩放图片
首先我们要知道鼠标滚轮事件是[wheel
](Element:滚轮事件 - Web API 接口参考 | MDN),在浏览器里鼠标滚轮事件一般都是有默认行为的,那就是对滚动条的操作,所以首要就是阻止默认事件的触发。
不过还要想一想,滚轮事件要加到遮罩上,还是加到图片上,如果加到图片上,你的鼠标必须要放到图片内部滚动才会触发,如果图片默认尺寸非常小,这操作无疑是非常难受的,所以我们把滚轮事件放到遮罩层上,从而在任意位置滚动都可以影响图片缩放。
// 缩放倍数,默认100%
let scale = 1
function showImagePreview(url) {
const div = document.createElement("div")
// ...
// img标签添加一个缩放的属性
img.style = {
position: "fixed",
transform: `scale(${scale})`
}
// ...
// 遮罩层添加滚轮事件
div.onwheel = (e) => zoom(e)
// 等同于下面这种写法
// div.addEventListener("wheel", (e) => zoom(e), { passive: false })
}
// 新建缩放操作方法
function zoom(wheelEvent) {
// 阻止滚轮的默认行为
wheelEvent.preventDefault()
// 根据滚轮事件对象的daltaY来判断向上滚动还是向下滚动
if (wheelEvent.deltaY > 0) {
scale = scale * 0.9
} else {
scale = scale * 1.1
}
// 对img的缩放重新设定
img.style.transform = `scale(${scale})`
}
deltaY
就是鼠标滚轮Y轴方向的滚动量,向上滚动为负数,向下滚动为正数
图片移动操作
拖动图片在遮罩上移动,显而易见,鼠标的mousedown
和mouseup
事件应该要在图片上触发,但是移动事件mousemove
需要在遮罩层上触发,到这里其实会有个问题,最上面的代码里我们有一个click
事件用来清除遮罩层返回初始页面,这里会和mouseup
事件冲突,导致移动完毕后松开鼠标,遮罩层会直接消失,不过我们暂时先注释掉上面的click事件,先来专心做移动操作。
移动操作的原理,就是通过移动事件来计算图片的left
和top
值,重新进行定位,所以我们不妨先来分析一下这两个值该怎么计算
在这个案例里,我们设定图片是屏幕居中,所以相对于遮罩层的原点就在中间位置,我们假定在1的位置移动到了2的位置,1相对浏览器左上角的距离clientX
和clientY
,以及相对于原点位置的left
和top
都是已知的,在移动到2的位置后,也可以根据鼠标的移动事件对象获取到最新为止的clientX
和clientY
,那么现在根据这些已知条件,来计算黄色矩形的边长,是不是很简单了,各位读者可以自己思考一下,亦可以直接看下面代码。
我们新创建函数
function showImagePreview(url) {
// ...
// 绑定两个事件
img.onmousedown = (e) => imgMouseDown(e)
img.onmouseup = (e) => imgMouseUP(e)
// ...
}
// 鼠标落下
function imgMouseDown(downEvent) {
// 阻止默认选中的行为
downEvent.preventDefault()
// 获取当前点击时刻的图片left和top值
const rect = window.getComputedStyle(img, null)
// 以下两种写法均可,仅做为演示
// 因为获取的值是例如 10px 这种的字符串,所以使用parseInt直接转为数字,方便后续计算
let leftNum = parseInt(rect.getPropertyValue("left"))
let topNum = parseInt(rect.top)
// 点击后为遮罩层绑定鼠标移动事件,注意是遮罩层的事件
div.onmousemove = (moveEvent) => {
// 移动后的client坐标 - 移动前的client坐标 + 移动前的left、top,就是最新的left和top
// 不要忘记加px单位,否则无法展示
img.style.top = moveEvent.clientY - downEvent.clientY + topNum + "px";
img.style.left = moveEvent.clientX - downEvent.clientX + leftNum + "px";
}
}
// 松开鼠标
function imgMouseUP(e) {
// 将遮罩层的鼠标移动事件清空,防止松开鼠标后图片依然跟随鼠标移动
div.onmousemove = null
}
关于getComputedStyle
可以参考Window.getComputedStyle() - Web API 接口参考 | MDN
这里也可以使用img.style.left
来获取,但是这种方法只能获取行内样式,很有可能会获取不到值
解决mouseup与click冲突
click
事件会在mousedown
和mouseup
后触发,也就是没有办法通过改变改变状态来控制click是否触发,而且这个click
是遮罩层的事件,阻止事件冒泡也没有什么作用。所以一般常见的方式,就是判断鼠标落下和抬起的时间差,时间差大于200ms
认为是拖动,反之则认为是点击,这样按下和抬起操作之后,click可以根据结果来判断是否执行,
所以我们在showImagePreview
函数里再为遮罩层绑定mousedown
和mouseup
,并修改click事件的写法:
// 记录初始点击时间
let startTime = 0
// 区分是否为点击
let isClick = true
function showImagePreview(url) {
//...
// 记录点击初始时间
div.onmousedown = (e) => startTime = e.timeStamp
// 遮罩层鼠标抬起,主要记录时间差是否大于200ms
div.onmouseup = (e) => divMouseUp(e)
// click最后触发,根据时间差的结果判断是否要触发
div.onclick = () => {
if (isClick) {
document.body.removeChild(div)
}
};
//...
}
function divMouseUp(e) {
// 时间差超过200ms不执行点击事件
if (e.timeStamp - startTime > 200) {
isClick = false
} else {
isClick = true
}
}
至此所有功能均实现完毕
下面是全部代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>
<p class="list">
文本内容1
</p>
<img onclick="imgClick(event)" src="./img/img.JPG" width="150" height="150" style="object-fit: cover;">
</div>
<!-- -->
</body>
</html>
<script>
function imgClick(e) {
showImagePreview(e.target.src)
}
// 遮罩层对象
let div = null
// 图片对象
let img = null
// 缩放倍数
let scale = 1
// 记录初始点击时间
let startTime = 0
// 区分是否为点击
let isClick = true
// 图片放大函数
function showImagePreview(url) {
// 创建遮罩
div = document.createElement("div")
div.style.position = "fixed";
div.style.top = "0";
div.style.bottom = "0";
div.style.left = "0";
div.style.right = "0";
div.style.backgroundColor = "rgba(0,0,0,0.8)";
div.style.display = "flex";
div.style.justifyContent = "center";
div.style.alignItems = "center";
div.style.zIndex = "1000";
// 遮罩层鼠标滚轮事件
div.onwheel = (e) => zoom(e)
// 记录点击初始时间
div.onmousedown = (e) => startTime = e.timeStamp
// 遮罩层鼠标抬起,主要记录时间差是否大于200ms
div.onmouseup = (e) => divMouseUp(e)
// click最后触发,根据时间差的结果判断是否要触发
div.onclick = () => {
if (isClick) {
document.body.removeChild(div)
}
};
// 创建图片
img = document.createElement("img")
img.src = url
img.style.position = "relative"
img.style.transform = `scale(${scale})`
// 图片移动操作
img.onmousedown = (e) => imgMouseDown(e)
img.onmouseup = (e) => imgMouseUP(e)
// div.addEventListener("wheel", (e) => zoom(e), { passive: false })
div.appendChild(img)
document.body.appendChild(div)
}
// 图片缩放操作函数
function zoom(wheelEvent) {
wheelEvent.preventDefault()
if (wheelEvent.deltaY > 0) {
scale = scale * 0.9
} else {
scale = scale * 1.1
}
img.style.transform = `scale(${scale})`
}
function imgMouseDown(downEvent) {
downEvent.preventDefault()
const rect = window.getComputedStyle(img, null)
let leftNum = parseInt(rect.getPropertyValue("left"))
let topNum = parseInt(rect.top)
div.onmousemove = (moveEvent) => {
img.style.top = moveEvent.clientY - downEvent.clientY + topNum + "px";
img.style.left = moveEvent.clientX - downEvent.clientX + leftNum + "px";
}
}
// 便于理解单独抽离
function imgMouseUP(e) {
div.onmousemove = null
}
function divMouseUp(e) {
if (e.timeStamp - startTime > 200) {
isClick = false
} else {
isClick = true
}
}
</script>
<style>
body {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
</style>