图片是使用的绝对定位+transition:all实现的移动和过渡效果 图片包裹层使用了vue的transition组件,蒙层的过渡是在 fade-enter-active中实现的
<template>
<div>
<img
src="../assets/imgs/douyin.png"
@click="changeVisible(true)"
class="origin-image"
alt=""
ref="originImage"
/>
<transition name="fade">
<div class="image-viewer" v-if="visible" @click="changeVisible(false)">
<div class="image-box">
<img
:style="imageStyle"
src="../assets/imgs/douyin.png"
class="image"
alt=""
/>
</div>
</div>
</transition>
</div>
</template>
<script>
export default {
data() {
return {
visible: false,
rect: {},
imageStyle: "",
};
},
methods: {
changeVisible(v) {
this.visible = v;
this.rect = this.$refs.originImage.getBoundingClientRect();
this.setImageStyle();
},
setImageStyle() {
const r = this.rect;
// 方法1, 使用requestAnimationFrame
new Promise((resolve) => {
if (this.imageStyle) {
this.imageStyle = `top:${r.top}px; left: ${r.left}px;`;
resolve();
} else {
// 第一次渲染时没有过渡效果,不知道为什么啊。。
this.raq = window.requestAnimationFrame(() => {
this.imageStyle = `top:${r.top}px; left: ${r.left}px;`;
resolve();
});
}
}).then(() => {
this.raq = window.requestAnimationFrame((timestamp) => {
const top = (window.innerHeight - r.height) / 2;
const left = (window.innerWidth - r.width) / 2;
this.imageStyle = `width: ${r.width}px;height: ${r.height}px; top:${top}px; left: ${left}px;`;
if (window.requestIdleCallback) {
window.requestIdleCallback(() => {
window.cancelAnimationFrame(this.raq);
});
} else {
window.cancelAnimationFrame(this.raq);
}
});
});
// 方法2, 使用setTimeout
const r = this.rect;
this.imageStyle = `top:${r.top}px; left: ${r.left}px;`;
const t = setTimeout(() => {
const top = (window.innerHeight - r.height) / 2;
const left = (window.innerWidth - r.width) / 2;
this.imageStyle = `width: ${r.width}px;height: ${r.height}px; top:${top}px; left: ${left}px;`;
clearTimeout(t)
});
},
},
};
</script>
<style scoped>
.origin-image {
width: 200px;
height: 200px;
margin-left: 300px;
margin-top: 300px;
}
.image-viewer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
z-index: 1000;
}
.image-box {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
line-height: 0;
background-color: rgba(0, 0, 0, 0.8);
overflow: auto;
}
.image {
position: absolute;
transition: all 0.2s;
cursor: zoom-out;
}
.fade-enter {
opacity: 0;
}
.fade-enter-active {
transition: opacity 0.2s;
}
.fade-enter-to {
opacity: 1;
}
</style>
可以将图片预览抽离成单独组件,使用单例模式,使得页面中只有一个图片预览节点
解决方案:
<template>
<div>
<img
src="../assets/imgs/douyin.png"
@click="changeVisible(true)"
class="origin-image"
ref="originImage"
/>
<transition name="fade">
<div class="image-viewer" v-if="visible" @click="changeVisible(false)">
<div class="image-box">
<img
ref="image"
:style="imageStyle"
src="../assets/imgs/douyin.png"
class="image"
/>
</div>
</div>
</transition>
</div>
</template>
<script>
export default {
data() {
return {
visible: false,
rect: {},
imageStyle: "",
timeout: null,
};
},
methods: {
changeVisible(v) {
this.visible = v;
this.rect = this.$refs.originImage.getBoundingClientRect();
this.setImageStyle(v);
},
setImageStyle(v) {
const r = this.rect;
if (v) {
this.imageStyle = `top:${r.top}px; left: ${r.left}px;`;
this.timeout = setTimeout(() => {
const top = (window.innerHeight - r.height) / 2;
const left = (window.innerWidth - r.width) / 2;
this.imageStyle = `width: ${r.width}px; height: ${r.height}px; top:${top}px; left: ${left}px;`;
});
} else {
// 回到原来的地方就行了
// 这里之所以使用ref,而不像原来那样 this.imageStyle,是因为vue的渲染机制,
// v-if为false时,imageStyle无法绑定到节点上
this.$refs.image.style = `width: ${r.width}px; height: ${r.height}px; top:${r.top}px; left: ${r.left}px;`;
}
},
},
destroyed() {
clearTimeout(this.timeout);
},
};
</script>
<style scoped>
.origin-image {
width: 200px;
height: 200px;
margin-left: 300px;
margin-top: 300px;
}
.image-viewer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
z-index: 1000;
}
.image-box {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
line-height: 0;
background-color: rgba(0, 0, 0, 0.8);
overflow: auto;
}
.image {
position: absolute;
transition: all 0.2s;
cursor: zoom-out;
}
.fade-enter {
opacity: 0;
}
.fade-enter-active {
transition: opacity 0.2s;
}
.fade-enter-to {
opacity: 1;
}
.fade-leave {
opacity: 1;
}
.fade-leave-active {
transition: opacity 0.2s;
}
.fade-leave-to {
opacity: 0;
}
</style>
import React, { useEffect, useRef, useState } from 'react'
import styles from './index.module.less'
import { CSSTransition } from 'react-transition-group'
import douyinPng from '../../assets/douyin.png'
const ImageViewer = () => {
const [visible, setVisible] = useState(false)
const [rect, setRect] = useState({})
const [imageStyle, setImageStyle] = useState({})
const originImage = useRef()
const handleVisible = () => {
setVisible(!visible)
setRect(originImage.current.getBoundingClientRect())
}
useEffect(() => {
handleTransition(visible)
}, [visible])
const handleTransition = (visible) => {
if (visible) {
setImageStyle({
top: `${rect.top}px`,
left: `${rect.left}px`
})
const raq = window.requestAnimationFrame(() => {
const top = (window.innerHeight - rect.height) / 2
const left = (window.innerWidth - rect.width) / 2
setImageStyle({
width: `${rect.width}px`,
height: `${rect.height}px`,
top: `${top}px`,
left: `${left}px`
})
if (window.requestIdleCallback) {
window.requestIdleCallback(() => {
window.cancelAnimationFrame(raq);
});
} else {
window.cancelAnimationFrame(raq);
}
})
} else {
setImageStyle({
top: `${rect.top}px`,
left: `${rect.left}px`,
width: `${rect.width}px`,
height: `${rect.height}px`
})
}
}
return (
<>
<img src={douyinPng} ref={originImage} className={styles.originImage} onClick={() => handleVisible()} />
<CSSTransition in={visible} classNames={{
enter: styles['image-enter'],
enterActive: styles['image-enter-active'],
exit: styles['image-exit'],
exitActive: styles['image-exit-active'],
}} timeout={200} unmountOnExit mountOnEnter={false}>
<div className={styles.imageViewer}>
<div className={styles.imageBox} onClick={() => handleVisible()}>
<img src={douyinPng} style={imageStyle} className={styles.image} />
</div>
</div>
</CSSTransition>
</>
)
}
export default ImageViewer
.originImage {
width: 200px;
height: 200px;
margin-left: 300px;
margin-top: 300px;
cursor: zoom-in;
}
.imageViewer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
z-index: 1000;
}
.imageBox {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
line-height: 0;
background-color: rgba(0, 0, 0, 0.8);
overflow: auto;
}
.image {
position: absolute;
transition: all .2s;
cursor: zoom-out;
}
.image-enter {
opacity: 0;
}
.image-enter-active {
transition: opacity .2s;
opacity: 1;
}
.image-exit {
opacity: 1;
}
.image-exit-active {
transition: opacity .2s;
opacity: 0;
}
<template>
<transition name="fade" @afterLeave="$_onAfterLeave">
<div class="image-viewer" v-if="visible" @click="hide">
<div class="image-box">
<img ref="image" :style="imageStyle" :src="imageSrc" class="image" />
</div>
</div>
</transition>
</template>
<script>
export default {
name: "image-viewer",
data() {
return {
timeout: null,
imageStyle: "",
visible: false,
};
},
props: {
imageSrc: {
type: String,
default: "",
},
rect: {
type: DOMRect,
},
},
methods: {
setImageStyle(v) {
const r = this.rect;
if (v) {
this.imageStyle = `top:${r.top}px; left: ${r.left}px;`;
this.timeout = setTimeout(() => {
const top = (window.innerHeight - r.height) / 2;
const left = (window.innerWidth - r.width) / 2;
this.imageStyle = `width: ${r.width}px; height: ${r.height}px; top:${top}px; left: ${left}px;`;
});
} else {
this.$refs.image.style = `width: ${r.width}px; height: ${r.height}px; top:${r.top}px; left: ${r.left}px;`;
}
},
$_onAfterLeave() {
this.$emit("afterLeave",'xx');
},
show() {
this.visible = true;
this.setImageStyle(true);
},
hide() {
this.visible = false;
this.setImageStyle(false);
},
},
beforeDestroy() {
clearTimeout(this.timeout);
},
};
</script>
<style scoped>
.image-viewer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
z-index: 1000;
}
.image-box {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
line-height: 0;
background-color: rgba(0, 0, 0, 0.8);
overflow: auto;
}
.image {
position: absolute;
transition: all 0.2s;
cursor: zoom-out;
}
.fade-enter {
opacity: 0;
}
.fade-enter-active {
transition: opacity 0.2s;
}
.fade-enter-to {
opacity: 1;
}
.fade-leave {
opacity: 1;
}
.fade-leave-active {
transition: opacity 0.2s;
}
.fade-leave-to {
opacity: 0;
}
</style>
import Vue from 'vue'
import imageViewerOptions from './imageViewer.vue'
const ImageViewer = function ({
rect = new DOMRect(),
imageSrc = '',
parentNode = document.body
}) {
let vm = ImageViewer._instance
if(!vm) {
const ImageViwerConstructor = Vue.extend(imageViewerOptions)
vm = ImageViewer._instance = new ImageViwerConstructor({
propsData: {
rect,
imageSrc
}
}).$mount()
}
if(!vm.$el.parentNode) {
parentNode.appendChild(vm.$el)
}
vm.rect = rect
vm.imageSrc = imageSrc
vm.show()
return vm
}
ImageViewer._instance = null
/*
隐藏预览图片
*/
ImageViewer.hide = () => {
const ImageViwerConstructor = Vue.extend(imageViewerOptions)
if(ImageViewer._instance instanceof ImageViwerConstructor && ImageViewer._instance.visible) {
ImageViewer._instance.hide()
}
}
ImageViewer.component = imageViewerOptions
export default ImageViewer
<template>
<div>
<img
src="../assets/imgs/douyin.png"
@click="show($event)"
class="origin-image"
/>
</div>
</template>
<script>
import ImageViewer from "@/components/imageViewer";
export default {
methods: {
show(e) {
const rect = e.target.getBoundingClientRect();
const imageViewer = ImageViewer({
rect,
imageSrc: e.target.src,
});
imageViewer.$once("afterLeave", (res) => {
console.log(res);
});
},
},
};
</script>
<style scoped>
.origin-image {
width: 200px;
height: 200px;
margin-left: 300px;
margin-top: 300px;
}
</style>
基于Vue.extend做的扩展,这样子做的好处呢,就是可以做成插件,到处都可以用了,还有个好处就是,不用每次引入组件时都写在dom上,通过构造函数去挂载dom,单例模式也好实现多了。
import Vue from 'vue'
import imageViewerOptions from './imageViewer.vue'
const ImageViewer = function ({
rect = new DOMRect(),
imageSrc = '',
parentNode = document.body
}) {
let vm = ImageViewer._instance
if (!vm) {
const ImageViwerConstructor = Vue.extend(imageViewerOptions)
vm = ImageViewer._instance = new ImageViwerConstructor({
propsData: {
rect,
imageSrc
}
}).$mount()
}
if (!vm.$el.parentNode) {
parentNode.appendChild(vm.$el)
}
vm.rect = rect
vm.imageSrc = imageSrc
vm.show()
return vm
}
ImageViewer._instance = null
/*
隐藏预览图片
*/
ImageViewer.hide = () => {
const ImageViwerConstructor = Vue.extend(imageViewerOptions)
if (ImageViewer._instance instanceof ImageViwerConstructor && ImageViewer._instance.visible) {
ImageViewer._instance.hide()
}
}
ImageViewer.component = imageViewerOptions
// 扩展成插件
const ImageViewerPlugin = function (Vue) {
if (!Vue || ImageViewerPlugin.installed) {
return
}
// 1. 添加全局方法或 property
// Vue.myGlobalMethod = function () {
// // 逻辑...
// }
// 2. 添加全局资源
// Vue.directive('my-directive', {
// })
// 3. 注入组件选项
// Vue.mixin({
// created: function () {
// // 逻辑...
// }
// ...
// })
// 4. 添加实例方法
// Vue.prototype.$myMethod = function (methodOptions) {
// // 逻辑...
// }
Vue.prototype.$imageViewer = ImageViewer
}
if (typeof window !== 'undefined' && window.Vue) {
ImageViewerPlugin(window.Vue)
}
export {
ImageViewer
}
export default ImageViewerPlugin
main.js
中引入import ImageViewer from './components/imageViewer/index'
Vue.use(ImageViewer)
然后在组件中使用
this.$imageViewer(rect, imageSrc)
import { ImageViewer } from "@/components/imageViewer";
// 使用方式
ImageViewer(rect, imageSrc)
<template>
<div>
<img
src="../assets/imgs/douyin.png"
@click="show($event)"
class="origin-image"
/>
<image-viewer
:rect="rect"
ref="imageViewer"
:imageSrc="imageSrc"
></image-viewer>
</div>
</template>
<script>
import { ImageViewer } from "@/components/imageViewer";
export default {
data() {
return {
rect: new DOMRect(),
imageSrc: "",
};
},
components: {
[ImageViewer.component.name]: ImageViewer.component,
},
methods: {
show(e) {
this.rect = e.target.getBoundingClientRect();
this.imageSrc = e.target.src;
this.$nextTick(() => {
this.$refs.imageViewer.show();
});
},
},
};
</script>
<style scoped>
.origin-image {
width: 200px;
height: 200px;
margin-left: 300px;
margin-top: 300px;
}
</style>
由于这是初版,局限性较大,所以指令很简单
import Vue from 'vue'
import imageViewerOptions from './imageViewer.vue'
const ImageViewer = function ({
rect = new DOMRect(),
imageSrc = '',
parentNode = document.body
}) {
let vm = ImageViewer._instance
if (!vm) {
const ImageViwerConstructor = Vue.extend(imageViewerOptions)
vm = ImageViewer._instance = new ImageViwerConstructor({
propsData: {
rect,
imageSrc
}
}).$mount()
}
if (!vm.$el.parentNode) {
parentNode.appendChild(vm.$el)
}
vm.rect = rect
vm.imageSrc = imageSrc
vm.show()
return vm
}
ImageViewer._instance = null
/*
隐藏预览图片
*/
ImageViewer.hide = () => {
const ImageViwerConstructor = Vue.extend(imageViewerOptions)
if (ImageViewer._instance instanceof ImageViwerConstructor && ImageViewer._instance.visible) {
ImageViewer._instance.hide()
}
}
ImageViewer.component = imageViewerOptions
// 扩展成插件
const ImageViewerPlugin = function (Vue) {
if (!Vue || ImageViewerPlugin.installed) {
return
}
// 1. 添加全局方法或 property
// Vue.myGlobalMethod = function () {
// // 逻辑...
// }
// 2. 添加全局资源
// Vue.directive('my-directive', {
// })
// 3. 注入组件选项
// Vue.mixin({
// created: function () {
// // 逻辑...
// }
// ...
// })
// 4. 添加实例方法
// Vue.prototype.$myMethod = function (methodOptions) {
// // 逻辑...
// }
Vue.prototype.$imageViewer = ImageViewer
// 添加指令
const isVue2 = Vue.version.split('.')[0] === '2'
if (isVue2) {
const handleClick = (el) => {
ImageViewer({ rect: el.getBoundingClientRect(), imageSrc: el.src })
}
Vue.directive('viewer', {
bind: (el) => {
el.addEventListener('click', () => handleClick(el))
},
// update: () => {
// console.log('update')
// },
// componentUpdated: () => {
// console.log('componentUpdated')
// },
unbind: (el) => {
el.removeEventListener('click', () => handleClick(el))
}
})
}
}
if (typeof window !== 'undefined' && window.Vue) {
ImageViewerPlugin(window.Vue)
}
export {
ImageViewer
}
export default ImageViewerPlugin
这个版本把图片移动改成了 transform
的方式(也算是一种性能优化,transform会触发GPU) 由于 CSSTransition
比较特殊,有个 unmountOnExit
卸载dom,帮我节省了卸载dom的步骤,也不需要判断单例了
import React, { useState, useEffect } from 'react'
import styles from './index.module.less'
import { CSSTransition } from 'react-transition-group'
import ReactDom from 'react-dom'
let changeVisible
const ImageViewerDom = (props) => {
const { rect, imageSrc } = props
const [visible, setVisible] = useState(false)
const [imageStyle, setImageStyle] = useState({})
changeVisible = (v) => {
setVisible(v)
}
const handleTransition = (visible) => {
if (visible) {
setImageStyle({
transform: `translate3d(${rect.left}px, ${rect.top}px, 0)`,
width: `${rect.width}px`,
height: `${rect.height}px`
})
const raq = window.requestAnimationFrame(() => {
const width = rect.width * 2
const height = rect.height * 2
const top = (window.innerHeight - height) / 2
const left = (window.innerWidth - width) / 2
setImageStyle({
width: `${rect.width * 2}px`,
height: `${rect.height * 2}px`,
transform: `translate3d(${left}px, ${top}px, 0)`
})
if (window.requestIdleCallback) {
window.requestIdleCallback(() => {
window.cancelAnimationFrame(raq);
});
} else {
window.cancelAnimationFrame(raq);
}
})
} else {
setImageStyle({
transform: `translate3d(${rect.left}px, ${rect.top}px, 0)`,
width: `${rect.width}px`,
height: `${rect.height}px`
})
}
}
useEffect(() => {
handleTransition(visible)
}, [visible])
return (
<CSSTransition in={visible} classNames={{
enter: styles['image-enter'],
enterActive: styles['image-enter-active'],
exit: styles['image-exit'],
exitActive: styles['image-exit-active'],
}} timeout={200} unmountOnExit mountOnEnter={false}>
<div className={styles.imageViewer}>
<div className={styles.imageBox} onClick={() => changeVisible(false)}>
<img src={imageSrc} style={imageStyle} className={styles.image} />
</div>
</div>
</CSSTransition>
)
}
function imageViewer(props = {
rect: new DOMRect(),
imageSrc: ''
}) {
const parentNode = document.body
imageViewer.container = document.createElement('div')
let vm = ReactDom.createPortal(<ImageViewerDom {...props} />, parentNode)
ReactDom.render(vm, imageViewer.container)
}
const api = {
show(payload) {
const { rect, imageSrc } = payload
imageViewer({ rect, imageSrc })
changeVisible(true)
}
}
export default api
.imageViewer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
z-index: 1000;
}
.imageBox {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
line-height: 0;
background-color: rgba(0, 0, 0, 0.8);
overflow: auto;
}
.image {
position: absolute;
transition: all .2s;
cursor: zoom-out;
transform-origin: center;
}
.image-enter {
opacity: 0;
}
.image-enter-active {
transition: opacity .2s;
opacity: 1;
}
.image-exit {
opacity: 1;
}
.image-exit-active {
transition: opacity .2s;
opacity: 0;
}
import imageViewer from './components/imageViewer'
imageViewer.show({
rect: originImage.current.getBoundingClientRect(),
imageSrc: douyinPng
})