携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
现有个需求,实现聊天图片预览功能,即实现图片缩放与移动。
注:使用的是vue3和ts,基于移动端
面对这个需求,涌入我脑中的第一个想法就是通过改变图片宽高来实现缩放,通过改变图片的中心点x轴与y轴的坐标来实现移动,想必很多人和我一样是这样的想法吧?No!这样写就很不优雅了,我的导师给了我一个思路。
实现
关于移动缩放,transform都包括,我们可以通过绑定图片的transform样式来实现缩放和移动,具体如下:
1. 移动
首先,创建一个数据结构用于存储图片在x轴与y轴上移动的数值
import { reactive } from "vue";
const d = reactive({
dx: 0,
dy: 0,
});
然后,将图片style的transfrom与我们图片移动值进行绑定
<img ref="innerImg" :style="{ transform }" class="img" src="https://pic.rmb.bdstatic.com/bjh/news/93833881c80bc8a31743a6a6fd3bcfe6.jpeg"/>
const transform = computed(
() => `translateX(${d.dx}px) translateY(${d.dy}px)`
);
然后就是监听图片移动事件,获取图片在x轴与y轴上移动的数值,关于怎么获取移动端手指触发相关事件,导师推荐我使用hammer.js。 那然后就是安装引入hammer.js了,不多赘述,监听hammer.js的pan移动事件来获取图片在x轴与y轴上移动的数值。
import Hammer from "hammerjs";
const innerImg = ref<HTMLImageElement>();
const previewImg = () => {
const mc = new Hammer(img.value, {
recognizers: [[Hammer.Pan], [Hammer.Pinch]],
});
//上一次移动结束时移动的x值与y值
let finalX = 0;
let finalY = 0;
// 移动图片
mc.on("pan", (ev) => {
//是否点击打开预览图片
//if (ready.value) {
//是否停止移动
if (!ev.isFinal) {
//deltaX为单次移动改变的x值,finalX为上一次移动结束后的总的改变的x值
//targetX为总的改变的x值,y值同理
let targetX = ev.deltaX + finalX;
let targetY = ev.deltaY + finalY;
} else {
// 处理第一次平移之后会从初始位置开始的问题
// 用finalX保存上一次停止时移动的总x值
finalX = d.dx;
finalY = d.dy;
}
//}
});
};
同时,要为图片添加transform移动样式,否则移动不平滑。
.img {
width: 100%;
transition: transform 0.1s linear;
}
然后,现在面临一个问题,图片会移出屏幕,要给图片限制边界,我所做的处理是至少图片有一部分留在屏幕内。 在以上基础上添加边界判断:
const imgWidth = innerImg.value?.width ?? 0;
const imgHeight = innerImg.value?.height ?? 0;
const top = innerImg.value?.offsetTop ?? 0;
let targetX = ev.deltaX + finalX;
let targetY = ev.deltaY + finalY;
// 做边界限制,图片不能全部移动至屏幕外
if ((targetX <= width && targetX >= 50 - imgWidth) && (targetY <= height - top && targetY >= 50 - imgHeight - top)) {
isBoundary = false;
d.dx = ev.deltaX + finalX
d.dy = ev.deltaY + finalY
return
}
//当图片超出时,如果下一次移动时往屏幕内侧,则允许其移动,
//否则,不可移动
if (targetX > width || targetX < 50 - imgWidth) {
if (Math.abs(ev.deltaX + d.dx) < Math.abs(d.dx)) {
d.dx = ev.deltaX + finalX;
d.dy = ev.deltaY + finalY;
}
}
if (targetY > height || targetY < 50 - imgHeight - top) {
if (Math.abs(ev.deltaY + d.dy) < Math.abs(d.dy)) {
d.dx = ev.deltaX + finalX;
d.dy = ev.deltaY + finalY;
}
}
当图片移动至屏幕边界,宽或高留50px在屏幕内
2. 缩放
首先,绑定transform的scale属性来实现缩放
const d = reactive({
dx: 0,
dy: 0,
scale: 1,
})
const transform = computed(() => `translateX(${d.dx}px) translateY(${d.dy}px) scale(${d.scale}) `)
然后,监听hammer.js的pinch双指缩放事件来获取缩放值
mc.on("pinch", (ev) => {
//判断缩放值是否大于0.1,太小的话忽略,否则有不必要的误触抖动
if (ready.value && d.scale * ev.scale >= 0.1) {
const top = innerImg.value?.offsetTop ?? 0;
if (ev.srcEvent.type === "pointermove") {
let temp = Number((finalScale * ev.scale).toFixed(2));
if (Math.abs(temp - d.scale) / d.scale > 0.05) {
d.scale = temp;
}
}
if (ev.srcEvent.type === "pointerup") {
// finalScale与finalX类似,存储上一次的缩放值
//否则scale会从1重新开始变化与渲染
finalScale = d.scale;
}
}
})
处理到达边界问题,到达边界后,不允许缩小,只允许放大,否则,图片可能缩小至屏幕外
let isBoundary = false; //是否到达屏幕边界
// 移动到边界时,将isBoundary置为true
if (targetX > width || targetX < 50 - imgWidth * d.scale) {
isBoundary = true
......
}
if (targetY > height || targetY < 50 - imgHeight * d.scale - top) {
isBoundary = true
......
}
if (!isBoundary) {
canScale = true;
} else {
if (ev.scale < 1) {
canScale = true;
} else {
canScale = false;
}
}
mc.on("pinch", (ev) => {
//判断缩放值是否大于0.1,太小的话忽略,否则有不必要的误触抖动
if (ready.value && d.scale * ev.scale >= 0.1) {
const top = innerImg.value?.offsetTop ?? 0;
if (ev.srcEvent.type === "pointermove") {
let temp = Number((finalScale * ev.scale).toFixed(2));
if (Math.abs(temp - d.scale) / d.scale > 0.05) {
if (canScale) {
d.scale = temp;
}
}
}
if (ev.srcEvent.type === "pointerup") {
// finalScale与finalX类似,存储上一次的缩放值
//否则scale会从1重新开始变化与渲染
finalScale = d.scale;
}
}
})
解决缩放与移动互相影响的问题
然后,我们就迎来了最重要的问题,移动与缩放会相互影响,当我们先缩放再移动,会发现我们可能会移出边界;或者缩放后再次缩放会移动位置,不是我们想要的效果。
1.移出边界
那先解决第一种,先缩放再移动会移出边界,问题是怎么造成的呢? 当我们缩放后,图片的宽高就会改变,但我们的用来判断边界的图片宽高值没有更新,我们需要根据图片的缩放值来设置边界值,即将图片宽高变为缩放后的图片宽高imgWidth * d.scale:
const imgWidth = innerImg.value?.width ?? 0;
const imgHeight = innerImg.value?.height ?? 0;
const top = innerImg.value?.offsetTop ?? 0;
let targetX = ev.deltaX + finalX;
let targetY = ev.deltaY + finalY;
// 做边界限制,图片不能全部移动至屏幕外
if ((targetX <= width && targetX >= 50 - imgWidth * d.scale) && (targetY <= height - top && targetY >= 50 - imgHeight * d.scale - top)) {
isBoundary = false;
d.dx = ev.deltaX + finalX
d.dy = ev.deltaY + finalY
return
}
//当图片超出时,如果下一次移动时往屏幕内侧,则允许其移动,
//否则,不可移动
if (targetX > width || targetX < 50 - imgWidth * d.scale) {
if (Math.abs(ev.deltaX + d.dx) < Math.abs(d.dx)) {
d.dx = ev.deltaX + finalX;
d.dy = ev.deltaY + finalY;
}
}
if (targetY > height || targetY < 50 - imgHeight * d.scale - top) {
if (Math.abs(ev.deltaY + d.dy) < Math.abs(d.dy)) {
d.dx = ev.deltaX + finalX;
d.dy = ev.deltaY + finalY;
}
}
2.缩放后位置改变
缩放后位置改变是因为缩放后没有更新d.dx与d.dy值,那想正确更新d.dx值与d.dy值,需要知道其改变的x轴与y轴的值,为此,进行了一番推演(不知道是不是弄复杂了,欢迎提出更简单的方法):
为了方便,同时也是为了实现手指放哪就以哪里为中心缩放哪里,将图片transform中心点设为(0, 0)(其默认transfrom中心点为图片中心点)。
<img ref="innerImg" :style="{ transform,transformOrigin:origin
}" class="img" src="https://pic.rmb.bdstatic.com/bjh/news/93833881c80bc8a31743a6a6fd3bcfe6.jpeg" />
const origin = computed(() => `0px 0px`)
如图,初始时图片宽度占满屏幕,点1是我们手指放下时初始中心点,hammer.js的pinch事件可以获取到该中心点数值(cx, cy),当我们放大时,该点就会移到点2,而为了让图片缩放不移动,我们要做的就是将图片根据从点2到点1的路径移回去,以此种方式实现手指放哪就以哪里为中心缩放哪里,所以在缩放结束后我们要在x轴上移动dx,在y轴上移动dy。
由图可知:
cx/(cx+dx)= (cx*s1)/(cx*s2)
s1为原缩放的scale,s2为放大后的scale
则dx=cx(s2-s1)/s1
然后,处理上一次可能移动过的情况,
cx = cx - d.dx(上一次移动的值)
然后我们就得到了缩放后应该移动的x轴的距离,
注意,y轴上距离上侧的距离包括图片距离屏幕上侧的offsetTop。
果然,数学是编程的基础,哭。
最后,图片缩放完成。
// 放大/缩小图片
mc.on("pinch", (ev) => {
if (ready.value && d.scale * ev.scale >= 0.1) {
const top = innerImg.value?.offsetTop ?? 0;
if (!isBoundary) {
canScale = true;
} else {
if (ev.scale < 1) {
canScale = true;
} else {
canScale = false;
}
}
if (ev.srcEvent.type === "pointermove") {
let temp = Number((finalScale * ev.scale).toFixed(2));
if (Math.abs(temp - d.scale) / d.scale > 0.05) {
const lx = ev.center.x - d.dx;
const ly = ev.center.y - top - d.dy;
const changeScaleRatio = (temp - d.scale) / d.scale;
// canScale限制其超出边界不可缩放
if (canScale) {
d.scale = temp;
d.dx -= lx * changeScaleRatio;
d.dy -= ly * changeScaleRatio;
// 解决缩放后立刻移动后产生的抖动
finalX = d.dx
finalY = d.dy
}
}
}
if (ev.srcEvent.type === "pointerup") {
finalScale = d.scale;
}
}
})
至此为止,图片预览已全部完成,代码有存在码上掘金。
最后,感谢导师的指导,如有问题,欢迎指出。