轮播支持巨长的图片

269 阅读4分钟

起因

www.gov.cn/fuwu/2020-0…

以前开发H5的问题,我们的H5的轮播实现以前是由两种方案的:

目前使用最多的是swiper,无论是页面内的轮播,还是点击图片预览的轮播,但还是有点问题,无法自适应长图。同样,photoswipe也有相同的问题。

  • 无法自适应长图
  • 代码繁琐

问题分析

通常使一张长图能够上下滑动,是不需要进行任何操作的,但是你是一个轮播图,它们都是会有一个overflow:hidden,这就造成了长图无法滚动; "图片自定义高度" height="200" width="350" 那么就有了一个初步的想法,判断图片的高度,如果图片的高度小于屏幕的高度,则垂直剧中。反之,顶部对齐;

实现

首先一个图片的数组,其中包括长图及横向展示图片,进行最基础的渲染: "图片自定义高度" height="" width="" 这里我们就要介绍一个属性screen(有关用户屏幕的信息),这个属性是可以直接使用的;

  • screen.width
  • screen.height
  • screen.availWidth(访问者屏幕的宽度,以像素计,减去诸如窗口工具条之类的界面特征)
  • screen.availHeight
// startPosition 当前显示图片的索引
// 表示当前正在展示的图片的索引
this.activeIndex = this.startPosition

计算出图片需要向左移动的距离

this.translateX = this.activeIndex * -screen.width

对应的dom结构

<div 
    class="image-preview__swiper-wrapper"
    :style=
        "{
          transform: 'translate3d(' + translateX +'px, 0px, 0px)',
          'transition-duration': duration + 's'
        }"
>...</div>

接下来就是遍历urls数组中的每一个元素 将true添加到center数组中,表示在图片加载时默认设置为垂直居中true添加到loading数组中,表示在图片加载时默认设置为正在加载

// 监听图片加载,加载完成后取消loading状态
for (let i = 0; i < this.urls.length; i++) {
  const img = new Image()

  // 初始默认配置
  this.center.push(true)
  this.loading.push(true)

  img.onload = (e) => {
   ...
  }
  img.src = this.urls[i]
}

在onload中等到图片加载完成,根据比例公式图片的高度/图片的宽度 = 屏幕的高度/屏幕的宽度,将图片与屏幕的宽高进行等比转化,图片转换后的高度小于屏幕高度则垂直居中,反之顶部对齐。

const height = e.target.height * screen.width / e.target.width
this.center.splice(i, 1, height < screen.height)
this.loading.splice(i, 1, false)

"图片自定义高度" height="250" width="450" 对应的dom结构

<div
  v-for="(src, index) in urls"
  :key="index"
  class="image-preview__swiper-slider"
  :class="{ 'is-active': activeIndex === index, 'image-preview__center': center[index] }"
  @touchstart="handleTouchStart"
  @touchmove="handleTouchMove"
  @touchend="handleTouchEnd">
  <div class="image-preview__loading" v-if="loading[index]">
    <img class="loading__image" src="./loading.png" alt="">
  </div>
  <img v-else style="width: 100%" class="image-preview__image" :src="src" />
</div>

css

.image-preview__swiper-slider {
  background-color: #000;
  overflow-y: auto;
  flex-shrink: 0;
  width: 100%;
  overflow: auto;
  -webkit-overflow-scrolling: touch
}

.image-preview__center {
  display: flex;
  align-items: center;
  justify-content: center;
}

photoswipe

当我准备接着写的时候,看photoswipe的官方文档,发现photoswipe支持传递四个参数,其中第三个参数是支持传递dom的。

var imgArr = [];
if (this.imgList instanceof Array) {
    for (var i in this.imgList) {
        let str = `<div style="height: 100%; width: 100%;overflow: scroll;"><img style="height: auto; width: 100%;"  src="${this.imgList[i]}"/></div>`;
        imgArr.push({html: str});
    }
} else {
    let str = `<div style="height: 100%; width: 100%;overflow: scroll;"><img style="height: auto; width: 100%;" src="${this.imgList}"/></div>`;
    imgArr.push({html: str});
}
let options = {
    optionName: 'option value',
    history: false,
    focus: false,
    showAnimationDuration: 0,
    hideAnimationDuration: 0,
    fullscreenEl: false,
    zoomEl: false,
    shareEl: false,
    tapToToggleControls: false,
    tapToClose: true
};
self.gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, imgArr, options);
self.gallery.init();
}

完善轮播

切换

主要使用的方法

  • @touchstart="handleTouchStart"
  • @touchmove="handleTouchMove"
  • @touchend="handleTouchEnd"

首先touchstart,通过返回的e.touch长度,判断你的是单指还是多指操作。 如果你是单指操作,记录下触摸点在屏幕中的x坐标和y坐标。 如果是多指操作,记录下触碰点位数组

if (e.touches.length === 1) {
  this.beginTouch.time = new Date().getTime();
  this.beginTouch.screenX = e.touches[0].screenX;
  this.beginTouch.screenY = e.touches[0].screenY;
} else {
  this.beginMultiTouch[0] = e.touches[0];
  this.beginMultiTouch[1] = e.touches[1];
}

接下来在移动过程中,通过touchmove,获取手指头滑动的位置,并记录一下偏移的方向。

this.offsetX = e.touches[0].screenX - this.beginTouch.screenX;
this.offsetY = e.touches[0].screenY - this.beginTouch.screenY;

如果Y轴偏移大于X轴偏移时,移动方向为Y轴,且在手指离开之前不再更新;

if (!this.moveDirection) {
  this.moveDirection = Math.abs(this.offsetX) > Math.abs(this.offsetY) ? "x" : "y";
}

之后渲染样式

this.translateX = this.activeIndex * -screen.width + this.offsetX

当图片为第一张或者第二张的时候,禁止向左或者向右滑动。刚一开始是直接判断下标activeIndex,然后return。

if (this.activeIndex === 0 && this.offsetX > 0) return

if (this.activeIndex === this.urls.length - 1 && this.offsetX < 0) return

优化

if (this.activeIndex === 0 && this.offsetX > 0) {
  this.translateX = this.activeIndex * -screen.width + this.offsetX * 0.4
  return
}

if (this.activeIndex === this.urls.length - 1 && this.offsetX < 0) {
  this.translateX = this.activeIndex * -screen.width + this.offsetX * 0.4
  return
}

滑动结束,手指移开,触发touchend,如果滑动的距离超过屏幕宽度的一半。 向左滑动:this.activeIndex -= 1 向右滑动:this.activeIndex += 1

计算整体偏移:

this.translateX = this.activeIndex * -screen.width

放大

在刚一开始我们就通过touchstart记录了是多指点击还是单指,默认多指的时候是放大、缩小的。 touchstart获取触屏的位置

 handleTouchStart (e) {
    this.beginMultiTouch[0] = e.touches[0]
    this.beginMultiTouch[1] = e.touches[1]
}

touchmove获取滑动的位置

this.moveMultiTouch[0] = e.touches[0]
this.moveMultiTouch[1] = e.touches[1]

js 计算两点间距离公式AB²=(x1-x2)²+(y1-y2)²

"图片自定义高度" height="" width=""

twoPointDistance(p1,p2){
    let dep = Math.sqrt(Math.pow((p1.x - p2.x), 2) + Math.pow((p1.z - p2.z), 2));
    return dep;
}
let scale = this.getDistance(this.moveMultiTouch[0], this.moveMultiTouch[1]) / this.getDistance(this.beginMultiTouch[0], this.beginMultiTouch[1])
scale = this.scaleDatum * scale

if (scale < 1) {
    this.scale = scale < 0.8 ? 0.8 : scale
} else {
    this.scale = scale > 4 ? 4 : scale
}

图片放大操作

const dom = document.querySelectorAll('.image-preview__image')[this.activeIndex]
dom.style.width = `${this.scale * 100}%`

当手指离开,触发touchend,如果是多指,且scale小于1,就把比例设置成原始值,所有值清空。

if (this.scale < 1) {
    this.scale = 1
    this.scaleDatum = 1
    const dom = document.querySelectorAll('.image-preview__image')[this.activeIndex]
    dom.style.width = `${this.scale * 100}%`
}
this.offsetX = 0
this.offsetY = 0
this.moveDirection = ''

优化

  • 图片放大的时候,不支持滑动到下一张
if (this.scale > 1 || this.moveDirection === 'y') return
  • 记录手指点击到离开的间隔,如果挪移距离很小,就代表关闭图片
handleTouchStart (e) {
    this.beginTouch.time= new Date().getTime()
}
handleTouchEnd (e) {
  this.touchgap = new Date().getTime() - this.beginTouch.time
}
if (Math.abs(this.offsetX) < 2 && Math.abs(this.offsetY) < 2 && this.touchgap < 300) {
    this.visible = false
}