Open fanky-c opened 3 years ago
早年在开发内部技术论坛时,为了实现一种流程的图片浏览体验,基于 Vue 开发了一个图片查看器组件,简单梳理了实现思路,希望能给大家提供一下帮助。
先来看看效果图:
从交互上来说内容很简单,点击页面上的图片,从图片当前的位置弹出图片浮动层,以便达到图片浏览的目的。
思路清晰了,实现起来也就不难了。
因为最终目的就是在 Vue 项目中使用,所以以下方案就直接封装成 Vue 组件了。
图片查看器组件的视图结构很简单:
<template> <transition> <div v-if="visible" class="image-viewer"> <img class="image" :src="src" /> </div> </transition> </template> <script> export default { data() { return { el: null, // 鼠标点中的图片元素 visible: false, // 图片查看器是否可见 }; }, computed: { src() { return this.el?.src; }, }, methods: { show(el) { el.style.opacity = 0; // 隐藏源图片 this.el = el; this.visible = true; }, }, }; </script>
简单解析一下:
样式也相当简单,绘制一个半透明的遮照很简单的动画:
<style lang="less" scoped> .image-viewer { position: fixed; z-index: 99; top: 0; left: 0; height: 100%; width: 100%; background: rgba(0, 0, 0, 0.6); cursor: move; transition: background 0.3s; /* 渐入渐出的动画效果 */ &.v-enter, &.v-leave-to { background: rgba(0, 0, 0, 0); } .image { position: absolute; user-select: none; transform-origin: center; will-change: transform, top, left; } } </style>
我们的图片查看器已经能把图片展示出来了,接下来使如何使查看器中的目标图片元素(.image)从源图片元素(el)中弹出来。
目标图片元素(.image)
源图片元素(el)
根据 Vue 数据驱动的思想,其本质就是通过应用起始数据和结束数据,来达到图片从原处弹出,放到至合适的尺寸的动画效果。这里通过维护一份维度数据dimension,根据维度数据来计算目标图片元素的样式style。
起始数据
结束数据
dimension
style
export default { data() { return { // ... // 图片维度信息 dimension: null, }; }, computed: { // ... // 目标图片样式 style() { if (!this.dimension) return null; const { scale, size: { width, height }, position: { top, left }, translate: { x, y }, } = this.dimension; return { width: `${width}px`, height: `${height}px`, top: `${top}px`, left: `${left}px`, transform: `translate3d(${x}px, ${y}px, 0) scale(${scale})`, transition: 'transform 0.3s', }; }, }, methods: { show(el) { el.style.opacity = 0; this.el = el; this.visible = true; this.dimension = getDimension(el); // 从源图片获取维度数据 }, }, };
这里的dimension包含了图片元素的以下信息:
获取图片元素的dimension的方法:
const getDimension = (el) => { const { naturalWidth, naturalHeight } = el; const rect = el.getBoundingClientRect(); // 放大后的图片宽高 const height = clamp(naturalHeight, 0, window.innerHeight * 0.9); const width = naturalWidth * (height / naturalHeight); return { size: { width, height }, position: { left: rect.left + (rect.width - width) / 2, top: rect.top + (rect.height - height) / 2, }, scale: rect.height / height, translate: { x: 0, y: 0 }, }; };
现在,我们已经在源图片的为止覆盖了一张一样尺寸的图片,然后是根据屏幕大小将图片放大至合适的尺寸。
我们只需要修改show部分的逻辑,使其在下一刻更新dimension的值:
show
export default { // ... methods: { show(el) { el.style.opacity = 0; this.el = el; this.dimension = getDimension(el); this.visible = true; doubleRaf(() => { const { innerWidth, innerHeight } = window; const { size, position } = this.dimension; this.dimension = { ...this.dimension, // 修改比例为1,即放大之后的比例 scale: 1, // 计算位移,使图片保持居中 translate: { x: (innerWidth - size.width) / 2 - position.left, y: (innerHeight - size.height) / 2 - position.top, }, }; }); }, }, };
这里使用了doubleRaf(即Double RequestAnimationFrame),等待浏览器重新渲染再执行:
doubleRaf
const doubleRaf = (cb) => { requestAnimationFrame(() => { requestAnimationFrame(cb); }); };
这样一来,图片放大的动画效果就出来了。
同理,当我们点击遮照层触发关闭图片浏览器时,应使图片缩小并回到原来的位置:
<template> <transition @afterLeave="hidden"> <div v-if="visible" class="image-viewer" @mouseup="hide"> <img class="image" :style="style" :src="src" /> </div> </transition> </template> <script> export default { // ... methods: { // 隐藏 hide() { // 重新获取源图片的dimension this.dimension = getDimension(this.el); this.visible = false; }, // 完全隐藏之后 hidden() { this.el.style.opacity = null; document.body.style.overflow = this.bodyOverflow; this.$emit('hidden'); }, }, }; </script>
现在,图片查看器组件部分的逻辑基本完成了。
为了让这个组件更加方便易用,我们将其封装成函数调用方式:
import Vue from 'vue'; import ImageViewer from './ImageViewer.vue'; const ImageViewerConstructor = Vue.extend(ImageViewer); function showImage(el) { // 创建组件实例,并调用组件的show方法 let instance = new ImageViewerConstructor({ el: document.createElement('div'), mounted() { this.show(el); }, }); // 将组件根元素插入到body document.body.appendChild(instance.$el); // 销毁函数:移除根元素,销毁组件 function destroy() { if (instance && instance.$el) { document.body.removeChild(instance.$el); instance.$destroy(); instance = null; } } // 组件动画结束时,执行销毁函数 instance.$once('hidden', destroy); // 如果是在某个父元素调用了该方法,当父元素被销毁时(如切换路由),也执行销毁函数 if (this && '$on' in this) { ![preview](https://user-images.githubusercontent.com/8649710/122009053-46478400-cdec-11eb-986c-134763e15a5d.gif) ![preview](https://user-images.githubusercontent.com/8649710/122009110-55c6cd00-cdec-11eb-8fa2-6f4e9f479a1a.gif) this.$on('hook:destroyed', destroy); } } showImage.install = (VueClass) => { VueClass.prototype.$showImage = showImage; }; export default showImage;
到这里,组件的封装也完成了,可以在任何地方愉快地使用了:
// ========== main.js ========== import Vue from 'vue'; import VueImageViewer from '@bigo/vue-image-viewer'; Vue.use(VueImageViewer); // ========== App.vue ========== <template> <div class="app"> <img src="http://wiki.bigo.sg:8090/download/attachments/441943984/preview.gif?version=1&modificationDate=1622463742000&api=v2" /> </div> </template> <script> export default { methods: { onImageClick(e) { this.$showImage(e.target); }, }, }; </script>
虽然功能比较简陋,但主要的图片浏览功能已经实现了,相比大多数的图片浏览插件,在用户体验方面要流畅很多,能让用户从视觉上有更加平滑的过度,提供更好的沉浸式浏览体验。
还有许多想法还没有实现,例如在浏览过程中拖拽图片移动、鼠标滚轮实现缩放、手势操作优化、移动端体验优化、多图片浏览等等,就留给大家去思考了。
前言
早年在开发内部技术论坛时,为了实现一种流程的图片浏览体验,基于 Vue 开发了一个图片查看器组件,简单梳理了实现思路,希望能给大家提供一下帮助。
先来看看效果图:
从交互上来说内容很简单,点击页面上的图片,从图片当前的位置弹出图片浮动层,以便达到图片浏览的目的。
原理分析
思路清晰了,实现起来也就不难了。
实现方案
因为最终目的就是在 Vue 项目中使用,所以以下方案就直接封装成 Vue 组件了。
图片查看器基本结构
图片查看器组件的视图结构很简单:
简单解析一下:
样式也相当简单,绘制一个半透明的遮照很简单的动画:
从原处弹出图片并放大
我们的图片查看器已经能把图片展示出来了,接下来使如何使查看器中的
目标图片元素(.image)
从源图片元素(el)
中弹出来。根据 Vue 数据驱动的思想,其本质就是通过应用
起始数据
和结束数据
,来达到图片从原处弹出,放到至合适的尺寸的动画效果。这里通过维护一份维度数据dimension
,根据维度数据来计算目标图片元素的样式style
。这里的
dimension
包含了图片元素的以下信息:获取图片元素的
dimension
的方法:现在,我们已经在源图片的为止覆盖了一张一样尺寸的图片,然后是根据屏幕大小将图片放大至合适的尺寸。
我们只需要修改
show
部分的逻辑,使其在下一刻更新dimension
的值:这里使用了
doubleRaf
(即Double RequestAnimationFrame),等待浏览器重新渲染再执行:这样一来,图片放大的动画效果就出来了。
同理,当我们点击遮照层触发关闭图片浏览器时,应使图片缩小并回到原来的位置:
现在,图片查看器组件部分的逻辑基本完成了。
封装为函数调用
为了让这个组件更加方便易用,我们将其封装成函数调用方式:
到这里,组件的封装也完成了,可以在任何地方愉快地使用了:
总结
虽然功能比较简陋,但主要的图片浏览功能已经实现了,相比大多数的图片浏览插件,在用户体验方面要流畅很多,能让用户从视觉上有更加平滑的过度,提供更好的沉浸式浏览体验。
还有许多想法还没有实现,例如在浏览过程中拖拽图片移动、鼠标滚轮实现缩放、手势操作优化、移动端体验优化、多图片浏览等等,就留给大家去思考了。