Encapsulate the image viewer component of Vue floating popup

file

This article was first published at: https://github.com/bigo-frontend/blog/ Welcome to follow and repost.

Encapsulate the image viewer component of Vue floating popup

foreword

When developing internal technical forums in the early years, in order to realize a picture browsing experience of a process, a picture viewer component was developed based on Vue, and the implementation ideas were simply sorted out, hoping to provide some help to everyone.

Let’s take a look at the renderings first:

1221.gif

In terms of interaction, the content is very simple. Click the picture on the page, and the picture floating layer will pop up from the current position of the picture, so as to achieve the purpose of picture browsing.

Principle analysis

  1. According to the click event, get the clicked image element
  2. Make the current image element invisible (via visibility or opacity)
  3. Create a shade layer
  4. Creates an image element of the same size as the image element's current position
  5. Create an animation to enlarge the image to the appropriate size (update position, scale)

Once the thinking is clear, it is not difficult to realize it.

Implementation plan

Because the ultimate goal is to use it in Vue projects, the following solutions are directly packaged into Vue components.

Image viewer basic structure

The view structure of the image viewer component is simple:

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


Simple analysis:

  • transition : The outer layer nests the transition component, which is very convenient for us to do the animation effect of image displacement and scaling in the future
  • .image-viewer : The root element is used to place image elements and also acts as a shading layer
  • .image : The floating image displayed after clicking on the image is this element, and all subsequent operations are performed on this image
  • show(el) : This method will be called after clicking on the picture, passing the picture element into the component, and displaying the picture viewer

The style is also quite simple, drawing a semi-transparent shading animation is very simple:

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

pop image from place and zoom in

Our picture viewer has been able to display pictures, and how to make the viewer pop up 目标图片元素(.image)from源图片元素(el)

According to Vue's data-driven thinking, its essence is to achieve the animation effect that the picture pops up from the original place and is placed to a suitable size through the application 起始数据of and . 结束数据Here, by maintaining a piece of dimension data dimension, the style of the target image element is calculated according to the dimension data 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); // 从源图片获取维度数据
    },
  },
};

This dimensioncontains the following information for the image element:

data describe
size: { width, height } The actual width and height of the picture
position: { top, left } Image absolute position
scale The ratio of the actual size of the image element to the natural size of the image, used for subsequent image scaling animations
translate: { x, y } Image displacement position, the default is 0, used for subsequent image zoom displacement animation

The method to get the image element 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 },
  };
};

Now, we have overlaid a picture of the same size as the source picture, and then enlarged the picture to the appropriate size according to the screen size.

We just need to modify showpart of the logic so that it updates dimensionthe value at the next moment:

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,
          },
        };
      });
    },
  },
};

Used here doubleRaf(ie Double RequestAnimationFrame ), wait for the browser to re-render before executing:

const doubleRaf = (cb) => {
  requestAnimationFrame(() => {
    requestAnimationFrame(cb);
  });
};

In this way, the animation effect of zooming in on the picture will come out.

In the same way, when we click on the shade layer to trigger the closing of the image browser, the image should be reduced and returned to its original position:

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

Now, the logic of the image viewer component part is basically completed.

Encapsulated as a function call

In order to make this component more convenient and easy to use, we encapsulate it into a function call:

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;

At this point, the encapsulation of the component is also completed, and it can be used happily anywhere:

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

Summarize

Although the function is relatively simple, the main picture browsing function has been realized. Compared with most picture browsing plug-ins, the user experience is much smoother, allowing users to have a smoother visual transition and provide better immersive experience. browsing experience.

There are still many ideas that have not been realized, such as dragging and dropping pictures during browsing, zooming with the mouse wheel, optimization of gesture operations, optimization of mobile terminal experience, multi-picture browsing, etc., which are left to everyone to think about.

Welcome everyone to leave a message to discuss, I wish you a smooth work and a happy life!

I am the front end of bigo, see you in the next issue.

Guess you like

Origin blog.csdn.net/yeyeye0525/article/details/120645862