图片放大预览是种很常见的场景和功能,通常移动网站首页的轮播 banner
,商品详情页的商品图片等位置都会用到此功能css
像这种经常使用的场景功能确定是有人早就写好插件了的,因此遇到这种场景,通常都遵循如下三步:html
photo
、preview
、carousel
、photoSwipe
等关键字npm install
之这种作法没毛病,有现成的轮子可用固然拿来主义,由于项目用的是 vue
,因此我在网上找了一圈 基于 vue
的放大预览组件库,结果令我有点意外,图片放大预览的库的数量明显比不上轮播组件库,而且更使人 智熄 的是,这些少得可怜的组件库中,其中一大半都是基于 PhotoSwipe 这个开源库进行的二次封装,除此以外,能用于实际生产的预览组件库(image gallery
)……好像没有(也多是我见识短浅),这种状况不只体如今 vue
库上,其余框架乃至是原生的相关库都是如此vue
虽然说不提倡重复造轮子,但轮子太少没有选择的余地也有点说不过去, PhotoSwipe 用起来很顺手,功能也很齐全,足以应对实际生产环境中的绝大部分场景git
但与此同时,也就表明它代码体积会比较大,引入的冗余代码会比较多,因而,抱着精简代码以及顺便丰富放大预览插件家族的想法,决定本身造个轮子github
先看下最终实现效果:npm
或者你想本身体验一下,这里也有个写好的 Demobash
我已经将此功能打包成了一个
npm package
,可直接下载安装使用,包括样式在内的代码体积压缩后不到22KB
,Gzipped以后不到8KB
,源码 已上传框架
滑动形式的选型与 造轮子之图片轮播组件(swiper)中的同样,就很少说了post
数据处理和 造轮子之图片轮播组件(swiper) 中的第一种方法同样,就很少说了:性能
<VueActivePreview :urlList="urlList" />
复制代码
此组件的 touch
事件比较复杂,而且涉及到不一样 touch
事件之间的交互,因此稍微麻烦点,不过只要条理清晰,考虑清晰,仍是能够解决的
单指滑动的主体逻辑与 造轮子之图片轮播组件(swiper)的相差很少,都是计算手指滑动的距离,经过不断改变 translate
的值进行位移
支持对单个图片的缩放操做,原理其实很简单,经过计算在起始时与滑动过程当中双指间距离的比例,就能够获得图片的缩放比例
获取双指间距离:
getDistance (p1, p2) {
return Math.sqrt(Math.pow(p2.clientX - p1.clientX, 2) + Math.pow(p2.clientY - p1.clientY, 2))
}
复制代码
获取图片缩放比例:
this.scaleValue = this.getDistance(targetTouch1, targetTouch2) / doubleTransferInfo.startDistance
复制代码
经过改变 transform: scale(scaleValue)
,就能够实现图片的即时缩放
不过,这个时候有个问题,那就是 CSS3 scale
的缩放中心坐标,默认是 50% 50% 0
,也就是元素的中心位置,因此若是在不设置 transform-origin
的状况下,直接设置 scale
,那么图片也能够正常缩放,但缩放的结果却并不必定是所想要的
好比,双指中心坐标是 (10, 56)
,按照正常习惯,当进行放大时,整张图片应该是以这个点为中心进行放大,而不该该是图片的中心的位置
有两种方法能够解决这个问题
transform-origin
直接将双指之间的中心坐标设置为 transform-origin
,而后进行缩放便可,这是最简单的方法
因此须要动态设置 transform-origin
const targetTouch1 = e.touches[0]
const targetTouch2 = e.touches[1]
this.transOriginX = (targetTouch1.clientX + targetTouch2.clientX) / 2 - this.left
this.transOriginY = (targetTouch1.clientY + targetTouch2.clientY) / 2 - this.top
复制代码
transform-origin
的改变,其实就是改变了图片的位置状态,无需关心 transform-origin
到底应该是什么,直接默认图片的中心位置就是每次图片缩放的 transform-origin
,而后在图片缩放的过程当中,动态地修正图片的位置,抵消 transform-origin
带来的影响,就可保持视觉上的统一
例如,图片的默认 transform-origin
是 (100, 100)
,若是以此为中心放大两倍,那么结束放大后,图片的左上角相比于原始状态向左偏移了 100
个单位,可是如今双指的起始中心坐标是 (0, 0)
(只是个假设,为了方便计算说明),并将此设置为 transform-rogin
的话,放大两倍后,图片的左上角相比于原始状态向左将偏移 0
个单位,也就是没有任何偏移
因此,当缩放中心是 (0, 0)
时,在不改变 transform-origin
的状况下,要想保持视觉上的统一,必须在图片放大的过程当中,将图片进行持续地右移,保证在每一帧中移动的距离都能抵消由于 transform-origin
带来的差距,直到最终移出 100
个单位
由于考虑到后面图片的位置坐标还有其余地方须要用到,并且直接设置 transform-origin
的方式更简单方便,因此这里我选择了第一种方法
but
,很快我就发现,我仍是想得太简单了
假设如今手指离开屏幕后,图片以 (10, 56)
为缩放中心放大了 2
倍,而后双指再次放在屏幕上,这个时候双指的中心坐标为 (70, 88)
,这个时候按照上面说的,就须要动态地将 transform-origin
由以前的 (10, 56)
改成 (70, 88)
,可是若是真的改了,你就会发现,图片马上产生了跳动
这是由于在第二次双指触摸屏幕以前,图片放大两倍的状态是基于 transform-origin(10, 56)
,如今改变了 transform-origin
,那就至关因而改变了图片的放大基点,图片的状态必然会改变
难道要换成第二种方法?可是总感受频繁地修改 left/top
的值有点不太对劲,并且这种方法的计算方式也比较复杂,担忧影响性能
仔细想了下,也是能够解决的
之因此第二次缩放会产生跳动,就在因而改变了第一次结束后的状态,由于这个状态并非固定的,此时图片的 scale
和 transform-origin
都是被动态修改过的,只要能把这个状态给固定下来,固定为默认状态的值,那不就好了吗?
至于如何固定这个状态,其实也是很简单的
对于一个尺寸为 100*100
的图片,以 (10, 10)
为 transform-origin
放大 2
倍,则放大后的图片尺寸为 200*200
,左上角偏移量为 (-10, -10)
SO
在第一次缩放结束后,当即将图片的宽高设置为 200*200
,而且给个 left: -10; top: -10
的偏移量,而后就能够将 scale
与 transform-origin
恢复到默认状态了,这个时候的图片状态也就至关因而没有使用任何 transform
属性
那么第二次缩放的时候,初始状态就以当前这个 200*200
的图片为起始状态而非是一开始的 100*100
,这样一来,就无需关心状态问题了,由于每一次缩放都是一个全新的状态
直观示例:
transform: scale(2); => width: 200; height: 200;
transform-origin: 10 10; => left: -10; top: -10;
复制代码
代码示例:
this.left = left
this.top = top
this.currentW = currentW
this.currentH = currentH
this.scaleValue = 1
复制代码
单个图片缩放后,为了容许用户更自由地查看图片的每一个细节,容许对缩放后的图片进行滑动查看
这个功能的主体逻辑仍是比较简单的,经过监听 touch
事件,计算获得每一帧间的 move
距离,动态位移图片位置便可
不过为了更贴近实际的物理交互,达到更好的用户体验,添加了一个惯性滑动的能力,即当用户在滑动图片的过程当中结束触摸时,图片还会继续往前滑动必定的距离
这个场景有两种解决方案
在触摸结束的瞬间,以当前速度为条件,计算出图片应该滑动多少距离才停下来,并设置一个速度逐渐下降的 transition
动画
规定一个速度递减的系数,每一帧的速度都在前一帧的基础上,以这个系数为前提进行递减,直到最后停下来
综合考虑了一下,第一种的方式可能更加节约性能,可是不太好模拟出那种物理惯性的感受,数值不太好计算,相比于节约的那一点性能来讲,性价比不高
第二种方式更加容易控制,因此选择了第二种方式
主要是借助了 requestAnimationFrame
这个 API
(已经对不兼容此 API
的设备作了降级处理):
rafHandler = raf(() => {
speedX *= 0.9
speedY *= 0.9
// ...
if (Math.abs(speedX) < 1) speedX = 0
if (Math.abs(speedY) < 1) speedY = 0
if (speedX !== 0 || speedY !== 0) {
this.frictionMove(speedX, speedY)
} else {
// ...
}
})
复制代码
其实这个组件的主体逻辑仍是蛮清晰的,没什么太多的道道,可是须要考虑的状况太多,并且还有三种不一样状况下 touch
事件的交互与判断,全部的状况综合在一块儿仍是蛮伤脑筋的,五分之一不到的时间用来写主体逻辑,剩下的时间全耗在 if...else
上了,等我把这个轮子写完,我也算是明白为什么这个场景的轮子那么少了,由于真的脑阔疼,不是功能逻辑的疼,功能逻辑写起来毕竟还有点意思,而是 if...else
的疼