基于vue + less的移动端swipe轮播图组件

2,332 阅读3分钟

移动端的轮播图组件是一个使用场景十分广泛的组件,本次实现这个可复用的轮播图组件的目的是在于通过不断的练习来加固自身对于vue以及css的理解

  • 本次轮播图组件最要实现了如下几个功能
    1. 可实现自动播放,并且增加了播放时的动画
    2. 可自由控制播放动画的时长
    3. 可通过手势进行切换轮播图
    4. 父组件可监听到切换时的轮播图的索引值
    5. 可控制轮播图的初始位置索引值
    6. 可对指示器的颜色进行自定义

👇来看看实现的效果吧(轮播图切换的时候可以监听到索引值的改变)

👇我们来分析一下是如何实现这个轮播图的

    1. 我们首先想到的是几张图片按照水平排列在一起,占满父级元素的100%后,其他的都隐藏掉
    2. 通过计算获得轮播图的总图片数,来获取索引值,并且获得总的宽度
    3. 我们通过增加一个循环定时器来控制自动播放,原理就是使用transform:translateX()来进行水平偏移,
       然后通过计算属性来改变偏移的距离。

👇看看是如何实现的(swipe.vue)

    <template>
      <div class="swiper" ref="swiper" @click.prevent="handleClick">
        <div 
          class="swiper-list" 
          :style="{width: swiperListWidth + 'px', transform: 'translateX(' + translateX + 'px)', transitionDuration: transitionDuration + 's' }" 
          ref="swiperList"
        >
          <slot></slot>
        </div>
        <div class="dot">
          <span 
            v-for="(x, i) in sum" :key="'dot' + x" 
            :style="{background: i ===  index ? indicatorColor : 'rgba(255, 255, 255, .5)'}" 
            :class="[i === index ? 'on': '']"
          >
          </span>
        </div>
      </div>
    </template>
    
    <script>
    export default {
      name: 'v-swipe',
        data () {
        return {
          swiperWidth: '', // 轮播图盒子的宽度
          index: this.initialIndex, // 轮播图序号
          sum: 0, // 轮播图片数量
          transitionDuration: 0.5, // 切换动画时长
          timer: '', // 定时器
          startX: '', // touchstart的起始x坐标
          offset: '', // move偏移值
          Loop: 0
        }
      },
      props: {
        duration: {
          type: Number,
          default: 3000
        },
        initialIndex: {
          type: Number,
          default: 0
        },
        indicatorColor: {
          type: String,
          default: '#fff'
        }
      },
      computed: {
        // 轮播图列表的宽度
        swiperListWidth () {
          return this.swiperWidth * this.sum
        },
        // 轮播图列表偏移值
        translateX () {
          return this.index * this.swiperWidth * -1
        }
      },
      created () {
        this.$nextTick(() => {
          let swiper = this.$refs.swiper
          // 获得轮播图的图片数量
          let swiperItems = document.querySelectorAll('.swiper-item')
          this.sum = swiperItems.length
          // 为什么不取屏幕宽度,是因为通用性,由外部的盒子决定轮播图的宽
          this.swiperWidth = swiper.offsetWidth
          this.autoPlay()
          // addEventListener不可以用匿名函数,因为无法解除绑定
          swiper.addEventListener('touchstart', this.touchStart)
          swiper.addEventListener('touchmove', this.touchMove)
          swiper.addEventListener('touchend', this.touchEnd)
        })
      },
      methods: {
        autoPlay () {
          this.timer = setInterval(() => {
            let index = this.index + 1
            // 取余数运算,0%5=0,1%5=1,5%5=0,当然用if判断语句也是可以的
            this.index = index % this.sum
            this.$emit('change', this.index)
          }, this.duration)
        },
        touchStart (e) {
          // 只记录第一根手指触发的值
          clearInterval(this.timer)
          this.transitionDuration = 0
          this.startX = e.targetTouches[0].clientX
        },
        touchMove (e) {
          this.offset = this.startX - e.targetTouches[0].clientX
          this.$refs.swiperList.style.transform = `translateX(${this.translateX - this.offset}px)`
        },
        touchEnd () {
          this.transitionDuration = 0.5
          // 计算偏移值四舍五入,如果拖动距离大于等于0.5则换一张轮播图
          let num = Math.round(this.offset / this.swiperWidth)
          let sum = this.index + num
          // 先计算再赋值给this.index避免重复触发计算属性,为什么这里不取余数,是因为又负数出现
          if (sum > this.sum - 1) {
            sum = 0
          } else if (sum < 0) {
            sum = this.sum - 1
          }
          // 解决拖动距离小于一半,index值无变化,无法触发计算属性,主动还原偏移值
          if (sum === this.index) {
            this.$refs.swiperList.style.transform = `translateX(${this.translateX}px)`
          } else {
            this.index = sum
            this.$emit('change', this.index)
          }
          // 解决当第一次手势滑动后,再次点击offset不跟新问题
          this.offset = 0
          this.autoPlay()
        },
        handleClick (e) {
          e.preventDefault()
          this.$emit('click', this.index)
        }
      },
      // 实例销毁之前,移除绑定事件
      beforeDestroy () {
        let swiper = this.$refs.swiper
        swiper.removeEventListener('touchstart', this.touchStart)
        swiper.removeEventListener('touchmove', this.touchMove)
        swiper.removeEventListener('touchend', this.touchEnd)
      }
    }
    </script>
    
    <style lang="less" scoped>
        .swiper {
          position: relative;
          width: 100%;
          height: 12rem;
          overflow: hidden;
          .swiper-list {
            display: flex;
            width: 100%;
            height: 100%;
            transition-property: all;
            transition-timing-function: cubic-bezier(0.18, 0.89, 0.32, 1.28);
          }
          .dot {
            display: flex;
            position: absolute;
            width: 100%;
            margin-top: -15px;
            justify-content: center;
            span {
              @size: 8px;
              width: @size;
              height: @size;
              background-color: rgba(255, 255, 255, .5);
              border-radius: 50%;
              margin-left: 5px;
            }
            .on {
              width: 12px;
              border-radius: 30%;
              transition: width 0.3s linear;
            }
          }
        }
    </style>

👇看看是如何实现的(swipeItem.vue)---使用了slot来插入内容

    <template>
      <div class="swiper-item">
        <slot></slot>
      </div>
    </template>
    
    <script>
    export default {
      name: 'v-swipe-item'
    }
    </script>
    
    <style lang="less" scoped>
      .swiper-item {
          width: 100%;
        }
        img {
          display: block;
          width: 100%;
          height: 100%;
        }
    </style>