造轮子之图片预览组件(preview)

图片放大预览是种很常见的场景和功能,通常移动网站首页的轮播 banner,商品详情页的商品图片等位置都会用到此功能css

像这种经常使用的场景功能确定是有人早就写好插件了的,因此遇到这种场景,通常都遵循如下三步:html

  • 打开冰箱 启动 Github
  • 搜索 photopreviewcarouselphotoSwipe等关键字
  • 找到想要的库,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事件比较复杂,而且涉及到不一样 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的值有点不太对劲,并且这种方法的计算方式也比较复杂,担忧影响性能

仔细想了下,也是能够解决的

之因此第二次缩放会产生跳动,就在因而改变了第一次结束后的状态,由于这个状态并非固定的,此时图片的 scaletransform-origin都是被动态修改过的,只要能把这个状态给固定下来,固定为默认状态的值,那不就好了吗?

至于如何固定这个状态,其实也是很简单的

对于一个尺寸为 100*100的图片,以 (10, 10)transform-origin放大 2倍,则放大后的图片尺寸为 200*200,左上角偏移量为 (-10, -10) SO 在第一次缩放结束后,当即将图片的宽高设置为 200*200,而且给个 left: -10; top: -10的偏移量,而后就能够将 scaletransform-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距离,动态位移图片位置便可

不过为了更贴近实际的物理交互,达到更好的用户体验,添加了一个惯性滑动的能力,即当用户在滑动图片的过程当中结束触摸时,图片还会继续往前滑动必定的距离

这个场景有两种解决方案

  • css 动画

在触摸结束的瞬间,以当前速度为条件,计算出图片应该滑动多少距离才停下来,并设置一个速度逐渐下降的 transition动画

  • js 动态动画

规定一个速度递减的系数,每一帧的速度都在前一帧的基础上,以这个系数为前提进行递减,直到最后停下来

综合考虑了一下,第一种的方式可能更加节约性能,可是不太好模拟出那种物理惯性的感受,数值不太好计算,相比于节约的那一点性能来讲,性价比不高

第二种方式更加容易控制,因此选择了第二种方式

主要是借助了 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的疼

源码已经放到 github上了,代码注释得也算是比较详细,感兴趣的能够参考下,若是有什么问题,欢迎提 issues

相关文章
相关标签/搜索