图片浏览组件easy-preview

为什么会有这个组件

因为某天我在用电脑逛p站(pixiv)时,发现看图的效果不是那么令人满意,我希望看图时能全屏浏览,并且支持图片的放大缩小和拖拽,不知道是不是搜索关键字不对,逛了一圈发现莫得喜欢的轮子,刚好又有些思路而且闲着无聊,于是决定自己封装一个组件。最后的效果是这样的。好耶,2K曲面屏看纸片人老婆

截图半分钟,压图半小时

你也可以点这里在线查看效果(要用电脑端哦)

源码在这里,感谢这个项目让我想起了我的github账号(求一波star)

使用方法

注册

全局注册

1. npm install easy-preview
2. 在main.js中加入下面两句
    import EasyPreview from "easy-preview";
    Vue.use(EasyPreview);
3. 使用<EasyPreview>标签即可

局部注册

1. 使用npm install easy-preview安装插件
2. 在组件中注册EasyPreview
3. 使用<EasyPreview>标签即可
import EasyPreview from "easy-preview";
export default {
  name: 'app',
  components: {EasyPreview},
  data () {
    return {}
  }
}

使用

打开和隐藏的控制权交给组件内部

比较简单,只要传入一个img标签给插槽使用并传入img-src属性的值即可

<EasyPreview :img-src="imgSrc">
    <img :src="imgSrc" width="500" style="border-radius: 10px" alt="">
</EasyPreview>

控制权不交给组件的使用

这个时候要传入一个属性options,并将options.controlByUsers置为true,此时插槽会失效,需要传入一个额外的属性:show-preview控制显示和隐藏,此时点击右上角自带的关闭按钮改为触发自定义的clickCloseButton和click-close-button 事件(两个都会触发),你可以选择监听事件并修改传入的show-preview的值。

<img :src="imgSrc" alt="" width="500" style="border-radius: 10px" @click="onclick">
<EasyPreview :img-src="imgSrc" :options="options" :show-preview="showPreview"   @clickCloseButton="onClickCloseButton"></EasyPreview>


{
    methods : { 
        onclick() {
            this.showPreview = true
        }
        onClickCloseButton() {
            this.showPreview = false;
        }
    }
}

参数说明

提供的全部可传入参数

属性名含义默认值备注
imgSrc浏览时的图片链接""
options自定义选项null具体参数看下面
showPreview是否展示预览图false仅控制权不是组件内部时生效
clickCloseButton点击关闭按钮时会触发的自定义事件仅控制权不是组件内部时生效 ,需要绑定回调函数
click-close-button点击关闭按钮时会触发的自定义事件仅控制权不是组件内部时生效 ,需要绑定回调函数

PS:clickCloseButton绑定的事件执行时会被传入一个函数,执行这个函数可以把图片恢复初始状态,调用时可以传入一个延迟执行的时间,这个时间默认是500ms(如果你没有修改transition的时间的话,最好不要修改它)

onClickCloseButton(reset) {
    this.showPreview = false;
    reset(500);
},

options的几个可选项

属性名含义默认值备注
controlByUsers控制权是否交给组件外部false
showCloseButton是否显示右上角的关闭按钮true
showStatusExtraStyle展示状态时额外的样式""可以传入对象或者字符串,样式优先级为内联级
hideStatusExtraStyle隐藏状态时额外的样式""可以传入对象或者字符串,样式优先级为内联级
buttonExtraStyle右上的按钮没有hover时的额外样式""可以传入对象或者字符串,样式优先级为内联级
buttonHoverExtraStyle右上的按钮hover时的额外样式""可以传入对象或者字符串,样式优先级为内联级

实现思路

鼠标滚动缩放

鼠标滚动时先判断是上还是下,上是放大,下就是缩小,直接通过transform来放大缩小就行

放大时的处理

放大两次鼠标指着的位置(缩放中心transform-origin)不同,就要移动图片来保持放大后鼠标扔指着同个位置,就需要进行移动,移动的代码如下

this.magnification是现在的缩放倍数,this.prevOrigin.x 和 this.prevOrigin.y是上次的缩放中心

this.e 和 this.f 是图像水平和垂直方向的偏移量,对应transform: matrix(a, b, c, d, e, f) 的e和f

// 计算需要偏移的量
let moveX = (1 - this.magnification) * (originX - this.prevOrigin.x);
let moveY = (1 - this.magnification) * (originY - this.prevOrigin.y);
// 进行移动
this.e -= moveX;
this.f -= moveY;

当然也不是每次放大都会保证鼠标指着的位置不变,在图像视觉大小小于屏幕大小(准确来说是父容器大小,但是全屏时大小一致)时,要做到放大是两边同时等距放大,所以在代码里有下面的处理

// 如果图片的视觉大小小于wrapper的大小,就把transform-origin取到图片中央,强制等距放大
if (imgVisualHeight < wrapperHeight) {
    originY = imgHeight / 2;
}
if (imgVisualWidth < wrapperWidth) {
    originX = imgWidth / 2;
}

缩小时的处理

缩放倍率大于1.5

如果缩放倍率大于1.5,就尽可能保证缩小后鼠标扔指向相同的位置,但这并不是100%的,在缩放前要计算下次缩放后图片会不会出现图片比屏幕大,但是又有部分图片没有填充到图片的情况,代码如下

this.rate是放大或缩小一次的倍数,这里是1.05

// 如果现在的缩放倍率已经大于1.5
 if (this.magnification > 1.5) {
    // 计算让鼠标能指向同个位置的修正X和Y
    let moveX = (1 - this.magnification) * (originX - this.prevOrigin.x);
    let moveY = (1 - this.magnification) * (originY - this.prevOrigin.y);

    // 计算下次图片放缩后的位置
    const imgOffsetLeft = this.$refs.img.offsetLeft;
    const imgOffsetTop = this.$refs.img.offsetTop;
    const magnification = this.magnification / this.rate;
    const x = this.originX;
    const y = this.originY;
    const e = this.e - moveX;
    const f = this.f - moveY;
    const nextImgVisualWidth = imgWidth * magnification;
    const nextImgVisualHeight = imgHeight * magnification;
    // 图片的视觉左边 / 顶边 / 右边 / 底边离wrapper左边 / 顶边 / 右边 / 底边的距离
    const left = (magnification - 1) * x - e - imgOffsetLeft;
    const top = (magnification - 1) * y - imgOffsetTop - f;
    const right = nextImgVisualWidth - left - wrapperWidth;
    const bottom = nextImgVisualHeight - top - wrapperHeight;

计算出图片四边离容器四边的距离后,就开始处理有空白的情况,限于篇幅仅展示x轴方向的处理

// 如果缩放后的图片比wrapper的宽,同时图片左边没有顶到wrapper的左边
if (nextImgVisualWidth > wrapperWidth && left < 0) {
    // 让图片的左边能顶到wrapper的左边
    moveX -= left;
} else if (nextImgVisualWidth > wrapperWidth && right < 0) {
    // 让图片的左边能顶到wrapper的右边
    moveX += right;
}

还有另外一种情况,就是尽管缩放倍数大于1.5,但图片的宽或高的某一边仍然不能占据完屏幕,这个时候也要保证缩小后两边的间隙相同,处理在下面

// 如果图片的大小已经小于wrapper的大小
// 要让图片两边的空白相同
if (nextImgVisualWidth < wrapperWidth) {
    // 计算要移动多少才能让两边的空白相同
    let average = (left + right) / 2;
    let diff = left - average;
    // 移动过去
    moveX -= diff;
}
if (nextImgVisualHeight < wrapperHeight) {
    let average = (top + bottom) / 2;
    let diff = top - average;
    moveY -= diff;
}

计算完要偏移的量就可以偏移了

// 修正位置
this.e -= moveX;
this.f -= moveY;
缩放倍率小于1.5

这时开始逐渐恢复图片,即将e和f归0,但是不能一下子归0,所以我们用下面的公式计算这次移动的多少

// 本次移动距离 = 还需移动的长度 ÷ 还能移动的次数
let moveX = -this.e / this.optionCount;
let moveY = -this.f / this.optionCount;

这样会显得比较"循序渐进",当然移动后可能出现上面说的,图片宽高大于屏幕宽高但该方向没占满屏幕,或者图片宽高小于屏幕宽高,移动后两边的间隙不同的情况,所以还是需要进行相同的处理,因为上面已经放过代码了,这里就不再说一次了。

鼠标拖动移动图片

估计不少人都做过拖拽移动吧,这里用的也是类似的原理,在mousedown中记录坐标,在mousemove中计算偏移,然后修改x和y方向的偏移,这里要说的就是一些边缘判断

在进行真正的移动前,要计算这么移动后会不会出现图片大于屏幕,但是又出现空白的情况,即,移动距离 = Math.min(图片的边缘屏幕边缘的距离, 鼠标离上一次位置的偏移量),这样就可以保证不会越界,如果图片大小已经小于屏幕大小,就不能在对应方向上移动了。
代码如下

// 水平移动的方向
let hd = translateX > 0 ? "right" : "left";
// 垂直移动方向
let vd = translateY > 0 ? "down" : "up";

.... // 省略了计算这四个值的代码
const left = (magnification - 1) * x - e - imgOffsetLeft;
const top = (magnification - 1) * y - imgOffsetTop - f;
const right = nextImgVisualWidth - left - wrapperWidth;
const bottom = nextImgVisualHeight - top - wrapperHeight;
// 判断图片能否向左 / 右 / 上 / 下 移动
// 判断的依据是往该方向移动后是否出现空隙
let leftAble = right > 0;
let rightAble = left > 0;
let upAble = bottom > 0;
let downAble = top > 0;

// 如果水平移动方向是左
if (hd === "left") {
    if (leftAble) {
        // 计算最多能偏移的大小, 超过这个大小会出现间隙
         translateX = -Math.min(Math.abs(translateX), right);
    } else {
        // 如果不能偏移就把偏移量置为0
        translateX = 0;
    }
}
// 其他几个方向的处理同理
......

完整代码(带注释)点 这里

点赞

发表评论

昵称和uid可以选填一个,填邮箱必填(留言回复后将会发邮件给你)
tips:输入uid可以快速获得你的昵称和头像

Title - Artist
0:00