前言

早年在开发内部技术论坛时,为了实现一种流程的图片浏览体验,基于 Vue 开发了一个图片查看器组件,简单梳理了实现思路,希望能给大家提供一下帮助。

先来看看效果图:

vue拖拽组件_vue子组件修改父组件_vue 拖拽组件

从交互上来说内容很简单,点击页面上的图片,从图片当前的位置弹出图片浮动层,以便达到图片浏览的目的。

原理分析根据点击事件,取得点击的图片元素使当前图片元素不可见(通过 visibility 或者 opacity)创建遮照层在图片元素当前位置创建同等大小的图片元素创建动画将图片放大至合适的尺寸(更新位置、比例)

思路清晰了,实现起来也就不难了。

实现方案

因为最终目的就是在 Vue 项目中使用,所以以下方案就直接封装成 Vue 组件了。

图片查看器基本结构

图片查看器组件的视图结构很简单:


  
    
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; }, }, }; 复制代码

简单解析一下:

样式也相当简单vue拖拽组件,绘制一个半透明的遮照很简单的动画:


  .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;
    }
  }

复制代码

从原处弹出图片并放大

我们的图片查看器已经能把图片展示出来了,接下来使如何使查看器中的目标图片元素(.image)从源图片元素(el)中弹出来。

根据 Vue 数据驱动的思想,其本质就是通过应用起始数据和结束数据,来达到图片从原处弹出,放到至合适的尺寸的动画效果。这里通过维护一份维度数据dimensionvue拖拽组件,根据维度数据来计算目标图片元素的样式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包含了图片元素的以下信息:

数据

描述

size: { width, height }

图片实际宽高

position: { top, left }

图片绝对位置

scale

图片元素实际尺寸与图片自然尺寸的比值,用于后续图片缩放动画

translate: { x, y }

图片位移位置,默认为 0,用于后续图片缩放位移动画

获取图片元素的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的值:

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),等待浏览器重新渲染再执行:

const doubleRaf = (cb) => {
  requestAnimationFrame(() => {
    requestAnimationFrame(cb);
  });
};
复制代码

这样一来,图片放大的动画效果就出来了。

同理,当我们点击遮照层触发关闭图片浏览器时,应使图片缩小并回到原来的位置:


  
    
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'); }, }, }; 复制代码

现在,图片查看器组件部分的逻辑基本完成了。

封装为函数调用

为了让这个组件更加方便易用,我们将其封装成函数调用方式:

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 ==========

  
export default { methods: { onImageClick(e) { this.$showImage(e.target); }, }, }; 复制代码

总结

虽然功能比较简陋,但主要的图片浏览功能已经实现了,相比大多数的图片浏览插件,在用户体验方面要流畅很多,能让用户从视觉上有更加平滑的过度,提供更好的沉浸式浏览体验。

还有许多想法还没有实现,例如在浏览过程中拖拽图片移动、鼠标滚轮实现缩放、手势操作优化、移动端体验优化、多图片浏览等等,就留给大家去思考了。

欢迎大家留言讨论,祝工作顺利、生活愉快!

如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点star:不胜感激 !

限时特惠:本站每日持续更新海量设计资源,一年会员只需29.9元,全站资源免费下载
站长微信:ziyuanshu688