uni-app实现小程序、H5图片轮播预览、双指缩放、双击放大、单击还原、滑动切换功能

前言

这次的标题有点长,主要是想要表述的功能点有点多;

简单做一下需求描述

产品要求在商品详情页的头部轮播图部分,可以单击预览大图,同时在预览界面可以双指放大缩小图片并且可以移动查看图片,双击放大,单击还原,左右滑动可以切换预览的图片,非放大情况下单击退出预览(类似于淘宝现在的商详图片预览);要求微信小程序和H5中都实现该功能,时间1.5天;

需求分析

  • 轮播图片点击唤起预览界面(这部分功能已经很早实现了,不做过多的解释),界面中可以定制别的内容;
  • 预览图片双指缩放
  • 预览图片放大之后可以拖动查看图片
  • 双击放大
  • 单击还原
  • 滑动切换图片
  • 单击关闭预览图片,同时索引定位到预览的位置

简单思路

图片点击预览图片这个功能是之前就有的,这次其实是加入了放大缩小手势等,想着直接用uni-app的uni.previewImage它支持图片预览,双击放大,拖动,轮播,而且底层是native的性能很棒,很丝滑;不支持关闭预览定位索引,不支持预览界面定制别的内容,因此没办法直接放弃了;

于是打算原生手写一个,尝试之后发现H5能用,但是很卡顿,小程序没法看;

最后想到了可以用uni-app的movable-areamovable-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那种回弹的效果,跟产品商量了一下也对比了一下决定做个实验,

  • 小程序端直接用uni.previewImageAPI,毕竟用户就是想放大看看图片,没必要做那么多嵌套,动画流畅,体验敢强最重要;至于关闭定位索引和在弹框slot别的内容这些暂时在小程序端先不做;
  • H5端用自己写的组件如上,因为uni.previewImage在H5端的效果一般,并且不能双击放大,其余的动画流畅度和性能都一样;
  • 暂时先这样了,也没有过多的人力去研究这个H5的动画,也没必要做个引擎之类的;

参考

如果有需要增加图片旋转或者长按事件等可以参考这个,可以结合一下看看;就到这里吧;预览图有同学需要可以找我要,我看见就会回复!拜拜~~~

相关推荐
你也向往长安城吗4 分钟前
推荐一个三维导航库:three-pathfinding-3d
javascript·算法
karrigan13 分钟前
async/await 的优雅外衣下:Generator 的核心原理与 JavaScript 执行引擎的精细管理
javascript
wycode21 分钟前
Vue2实践(3)之用component做一个动态表单(二)
前端·javascript·vue.js
wycode1 小时前
Vue2实践(2)之用component做一个动态表单(一)
前端·javascript·vue.js
第七种黄昏1 小时前
Vue3 中的 ref、模板引用和 defineExpose 详解
前端·javascript·vue.js
我是哈哈hh2 小时前
【Node.js】ECMAScript标准 以及 npm安装
开发语言·前端·javascript·node.js
张元清2 小时前
电商 Feeds 流缓存策略:Temu vs 拼多多的技术选择
前端·javascript·面试
pepedd8642 小时前
还在开发vue2老项目吗?本文带你梳理vue版本区别
前端·vue.js·trae
pepedd8643 小时前
浅谈js拷贝问题-解决拷贝数据难题
前端·javascript·trae
@大迁世界3 小时前
useCallback 的陷阱:当 React Hooks 反而拖了后腿
前端·javascript·react.js·前端框架·ecmascript