前言
这次的标题有点长,主要是想要表述的功能点有点多;
简单做一下需求描述
产品要求在商品详情页的头部轮播图部分,可以单击预览大图,同时在预览界面可以双指放大缩小图片并且可以移动查看图片,双击放大,单击还原,左右滑动可以切换预览的图片,非放大情况下单击退出预览(类似于淘宝现在的商详图片预览);要求微信小程序和H5中都实现该功能,时间1.5天;
需求分析
- 轮播图片点击唤起预览界面(这部分功能已经很早实现了,不做过多的解释),界面中可以定制别的内容;
- 预览图片双指缩放
- 预览图片放大之后可以拖动查看图片
- 双击放大
- 单击还原
- 滑动切换图片
- 单击关闭预览图片,同时索引定位到预览的位置
简单思路
图片点击预览图片这个功能是之前就有的,这次其实是加入了放大缩小手势等,想着直接用uni-app的uni.previewImage它支持图片预览,双击放大,拖动,轮播,而且底层是native的性能很棒,很丝滑;不支持关闭预览定位索引,不支持预览界面定制别的内容,因此没办法直接放弃了;
于是打算原生手写一个,尝试之后发现H5能用,但是很卡顿,小程序没法看;
最后想到了可以用uni-app的movable-area和movable-view,开发一个可以拖动的区域,配合swiper就可以了;正好看了一下uni.previewImage的实现源码,发现在H5端也是用这几个组件实现的源码位置,于是决定参照源码开发一下;
代码
<div :class="['img-preview', modal ? 'slide-down-to-up-opacity' : 'slide-up-to-down-opacity']">
<swiper class="swiper-container" :current="current" :disable-touch="disableTouch" @change="handleChangeSlide">
<swiper-item v-for="(img, idx) in picList" :key="idx" :class="{'swiper-slide': true}">
<movable-area scale-area class="movable-area">
<movable-view
direction="all"
:animation="false"
:scale-min="1"
:scale-max="2"
:damping="30"
:scale-value="img.scale"
:scale="true"
:inertia="false"
:out-of-bounds="false"
:class="{'movable-view':true}"
@touchmove="handleTouchmove($event, idx)"
@click.stop="handleMovableClick($event, idx)"
@scale.stop="handleOnScale($event, idx)"
>
<img
:key="award ? img.productImageSpecial : img.picture"
:src="award ? img.productImageSpecial : img.picture"
mode="widthFix"
:class="{'preview-img': true}"
/>
</movable-view>
</movable-area>
</swiper-item>
</swiper>
<div v-if="picList && picList.length > 1" class="product-align-single">
<div class="product-align-dots">
<div v-for="(item, idx) in picList" :key="idx" :class="{'product-align-dot': true, 'product-align-dot-active': idx === current}"></div>
</div>
</div>
</div>
export default {
name: 'ImgPreview',
props: {
// 显示与隐藏
value: {
type: Boolean,
value: false
},
imgList: {
type: Array,
default() {
return []
}
},
initIndex: {
type: Number,
default: 0
},
fullscreen: {
type: Boolean,
default: true
},
award: {
type: Boolean,
default: false
}
},
emits:['close','change-slide'],
data () {
return {
modal: this.value,
current: this.initIndex,
arrowIcon: 'https://static1.keepcdn.com/infra-cms/2023/3/7/17/35/553246736447566b58312f38753731477849327742542f44796c385238397273617968664475477a4f6c4d3d/48x48_e33efe885c6a5df9403962315de3681bad220cd2.png',
scale: 1,
lastTapTime: 0, // 记录上一次点击时间
clickTimer: null,
clickDelay: 300,
disableTouch: false,
picList: []
}
},
watch: {
value: {
handler(val) {
this.modal = val
if (val) {
this.picList = []
this.imgList.forEach(item => {
this.picList.push({
...item,
scale: 1
})
})
}
},
immediate:true
},
},
methods: {
handleOnScale(event, index) {
const { scale, x, y } = event.detail
let item = this.picList[index]
item.scale = scale
this.$set(this.picList, index, item)
this.$forceUpdate()
},
handleTouchmove(event, index) {
this.disableTouch = true
let item = this.picList[index]
if (item.scale !== 1) {
this.disableTouch = true
} else {
this.disableTouch = false
}
},
handleMovableClick(e, index) {
console.log(e, '<===========================')
// 判断双击事件
let curTime = e.timeStamp
if (this.lastTapTime > 0) {
if (curTime - this.lastTapTime < this.clickDelay) {
this.lastTapTime = curTime
clearTimeout(this.clickTimer)
// 双击
return this.handleMovableDbClick(e, index)
}
}
this.lastTapTime = curTime;
clearTimeout(this.clickTimer);
this.clickTimer = setTimeout(() => {
// 单击
this.handleMovableOnClick(e, index)
}, this.clickDelay)
},
// 图片单击事件(关闭预览)
handleMovableOnClick(e, index) {
this.modal = false
setTimeout(() => {
this.$emit('close', false)
}, 100)
},
// 图片双击事件
handleMovableDbClick(e, index) {
let item = this.picList[index]
item.scale = item.scale < 2 ? 2 : 1
this.$set(this.picList, index, item)
this.$forceUpdate()
},
handleChangeSlide(event) {
this.current = event.detail?.current || 0
this.$emit('change-slide', this.current)
this.resetScale(this.current)
},
resetScale(index) {
this.picList.forEach((element, idx) => {
if (idx !== index) {
element.scale = 1
}
})
this.$forceUpdate()
}
}
}
<style lang="less" scoped>
.img-preview {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
opacity: 0;
}
.img-preview-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 1);
z-index: 1;
}
.movable-area {
height: 100%;
width: 100%;
overflow: hidden;
}
.movable-view {
height: 100%;
width: 100%;
}
.img-preview-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 1);
z-index: 1;
}
.preivew-swiper{
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
// padding-top: calc((100vh - 100vw) * 0.356);
position: relative;
z-index: 2;
}
.preivew-swiper-fullscreen {
padding-top: calc((100vh - 100vw) * 0.5);
}
.swiper-container {
width: 100%;
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 2;
}
.swiper-wrapper,
.swiper-slide {
width: 100% !important;
height: 100%;
display: flex;
align-items: center;
}
.swiper-slide-single {
height: 133.34vw;
}
.preview-img {
width: 100%;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
max-height: 100%;
max-width: 100%;
}
</style>
css不太全,截取了一部分;
主要是movable-view组件的一些属性配置和事件触发;这里有一点需要注意就是在图片放大的情况下移动图片或触发swiper的滑动,这里就出现了一个问题我搞了半天但是还是没有解决;
怎么阻止swiper手动切换
阻止冒泡事件,event.stopPropagation();
uniapp中禁止 event.preventDefault();event.stopPropagation();
要想阻止冒泡事件只能用事件修饰符;显然事件修饰符不能根据条件修改,这个路不通;
swiper有没有什么可以禁止滑动的属性呢?有的!
disable-touch
查了一下swiper果然有个属性disable-touch;很开心,终于可以根据条件阻止swiper滑动了,当在movable-view中touchmove且scale!==1的时候disable-touch设为true,反之为false;
但是当在小程序中测试时,发现这个属性并不管用,后来发现该属性在小程序中只有初始化时有用,不能做到动态变更;
swiper-item添加touchmove
网上很火的解决方案都在21年左右的,但是尝试了一下行不通,不好用!
写一个伪类,用一个蒙层盖住swiper
.swiper {
position: relative;
&:after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
}
}
这个方法很好用,之前在别的需求中用过,盖住之后拖动肯定就不滑动了,但是现在的需求显然不能这么用,因为movable-view在swiper中需要拖动;
最后效果
小程序的swiper阻止切换没有实现,同时该组件在小程序端明显卡段,动画不流畅,也没有native那种回弹的效果,跟产品商量了一下也对比了一下决定来个AB实验;
- 小程序端直接用uni.previewImageAPI,毕竟用户就是想放大看看图片,没必要做那么多嵌套,动画流畅,体验敢强最重要;至于关闭定位索引和在弹框slot别的内容这些暂时在小程序端先不做;
- H5端用自己写的组件如上,因为uni.previewImage在H5端的效果一般,并且不能双击放大,其余的动画流畅度和性能都一样;
- 暂时先这样了,也没有过多的人力去研究这个H5的动画,也没必要做个引擎之类的;
参考
- 移动端单指移动和双指缩放的实现
- uniapp(移动端)图片双指缩放、单指拖动、双击缩放
- dcloudio/uni-app
- uniapp使用 movable-area movable-view 实现图片双指缩放、鼠标单击缩小双击放大、图片及标记点功能
- 微信小程序swiper禁止用户滑动
- 苛学加/previewImage
如果有需要增加图片旋转或者长按事件等可以参考这个,可以结合一下看看;就到这里吧;预览图有同学需要可以找我要,我看见就会回复!拜拜~~~