干货--手把手撸vue移动UI框架:滑动轮播

Posted homehtml

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了干货--手把手撸vue移动UI框架:滑动轮播相关的知识,希望对你有一定的参考价值。

前言

昨天写了一篇侧边菜单组件的文章,阅读人数挺多的,内心很欣喜(偷着乐,第一篇文章有这么多人看)!乘着这股劲,今天在继续写一篇我们平时工作中更常用的滑动轮播组件的文章。

效果展示

老规矩,咱们先看做成后的效果,然后咱们再一步步的开始制作:
技术图片

组件组成分析

在实际的工作中,咱们轮播中的内容形式可能有很多种:图片、文本、视频、其他DOM结构等。所以咱们的轮播组件必须能满足这几种应用情况。那么我们可以把组件分两部分:

  1. 可以高度定制的子组件,负责渲染轮播中的每一个子项
  2. 负责轮播的父组件,用来处理通用的滑动事件、自动轮播、指示器等功能

我们现在这定义子组件的名称为swiper-item;父组件名称为swiper

DOM组成

首先咱们的子组件中负责渲染自定义的内容,则子组件中需要一个插槽slot。

swiper-item:

<template>
  <div class="r-swiper-item">
    <slot></slot>
  </div>
</template>

其次父组件中负责通用的功能,以及轮播的整体架构,其DOM结构如下。

swiper:

<template>
  <div class="r-swiper">
    <slot></slot>
    <slot name="indicator">
      <div class="indicator"></div>
    </slot>
  </div>
</template>

默认插槽在使用的时候渲染咱们轮播的子项,通常为swiper-item;indicator插槽用来自定义指示器的样式,因为在实际使用过程中指示器样式很可能是需要定制的。

css样式

移动端的视图大小有限,子项的大小一般是父组件的全部可视视图。
swiper-item:

<style lang="scss">
.r-swiper-item{
  position: absolute;
  left:0;
  top:0;
  width: 100%;
  height: 100%;
}
</style>

下面的vw是一种移动端的适配方案(https://www.w3cplus.com/css/t...)。其他的适配方案还有淘宝的flexible,这个css大家根据自己的适配方案更改下,这里不做过多描述,大家感兴趣的自行百度。
swiper:

<style lang="scss">
.r-swiper{
  position: relative;
  overflow: hidden;
  .indicator{
    position: absolute;
    right: 3vw;
    bottom: 3vw;
    width: 10vw;
    height: 10vw;
    line-height: 10vw;
    border-radius: 5vw;
    text-align: center;
    background-color: rgba(0,0,0,.5);
    color: #fff;
    font-size: 14px;
  }
}
</style>

javascript

老规矩,写JS代码前咱们先理清交互逻辑:

  1. 页面渲染开始,首先把所有子组件挂载到DOM上,当所有子节点挂载好了后初始化父组件
  2. 同理,当轮播组件销毁的时候先销毁子组件,再销毁父组件
  3. 初始化的时候需要:

    1. 获取父组件容器DOM节点、以及父组件节点的宽度
    2. 获取到所有子组件节点
    3. 给所有子节点依次设置好初始坐标
    4. 给父节点绑定touch事件
  4. 初始化完成后,当手指触摸到屏幕瞬间,记录当前手指起始的坐标
  5. 当手指移动的过程中,阻止页面中的默认事件,根据当前坐标以及起始坐标计算手指X、Y轴移动的距离
  6. 如果X轴移动比Y轴多则判断手指在横向移动,否则为竖向移动
  7. 如果横向移动则移动子项中的位置,修改所有子项的坐标
  8. 手指离开屏幕的时候判断手指移动总距离,如果大于一个临界值则轮播切换到下一屏或者上一屏(根据滑动方向判定),否则重置会原始状态

swiper-item:

export default {
  mounted () {
    this.$nextTick(() => {
      this.$parent.init()
    })
  },
  beforeDestroy () {
    this.$nextTick(() => {
      this.$parent.destroy()
    })
  }
}

swiper:

<template>
  <div ref="swiper" class="r-swiper" :style="{height: _height}"
  @touchstart="moveStart"
  @touchmove="moving"
  @touchend="moveEnd">
    <slot></slot>
    <slot name="indicator">
      <div class="indicator"></div>
    </slot>
  </div>
</template>

<script>
let each = function (ary, callback) {
  for (let i = 0, l = ary.length; i < l; i++) {
    if (callback(ary[i], i) === false) break
  }
}

export default{
  props: {
    // 设置父容器的高度,使用过程中自定义
    height: {
      type: [Number, String],
      default: ‘auto‘
    }
  },
  data () {
    return {
      _width: 0,
      duration: 300,
      container: null,
      items: [],
      active: 0,
      start: {
        x: 0,
        y: 0
      },
      move: {
        x: 0,
        y: 0
      }
    }
  },
  computed: {
   // 根据传入参数类型设置正确的高度样式
    _height () {
      if (typeof this.height === ‘number‘) {
        return this.height + ‘px‘
      } else {
        return this.height
      }
    }
  },
  methods: {
    init () {
      // 获得父容器节点
      this.container = this.$refs.swiper
      // 获得所有的子节点
      this.items = this.container.querySelectorAll(‘.r-swiper-item‘)
      this.updateItemWidth()
      this.setTransform()
      this.setTransition(‘none‘)
    },
    // 获取父容器宽度,并且更新所有的子节点宽度,因为我们默认所有子节点的宽高等于父节点的宽高
    updateItemWidth () {
      this._width = this.container.offsetWidth || document.documentElement.offsetWidth
    },
    // 根据当前活动子项的下标计算各个子项的X轴位置
    // 计算公式(子项的下标 - 当前活动下标) * 子项宽度 + 偏移(手指移动距离);
    setTransform (offset) {
      offset = offset || 0
      each(this.items, (item, i) => {
        let distance = (i - this.active) * this._width + offset
        let transform = `translate3d(${distance}px, 0, 0)`
        item.style.webkitTransform = transform
        item.style.transform = transform
      })
    },
    // 给每一个子项添加transition过度动画
    setTransition (duration) {
      duration = duration || this.duration
      duration = typeof duration === ‘number‘ ? (duration + ‘ms‘) : duration
      each(this.items, (item) => {
        item.style.webkitTransition = duration
        item.style.transition = duration
      })
    },
    moveStart (e) {},
    moving (e) {},
    moveEnd (e) {},
    destroy () {
      this.removeEvent()
    }
  }
}
</script>

初始化完成后,咱们接下来编写咱们的moveStart、moving、moveEnd三个touch事件,在methods中完善这三个函数,并添加一个临界值sensitivity以及一个阻力系数,阻力系数有啥用,注意看下面代码的注释:

data () {
  return {
    sensitivity: 60,
    resistance: 0.3
  }
},
methods: {
  moveStart (e) {
    this.start.x = e.changedTouches[0].pageX
    this.start.y = e.changedTouches[0].pageY
    this.setTransition(‘none‘)
  },
  moving (e) {
    e.preventDefault()
    e.stopPropagation()
    let distanceX = e.changedTouches[0].pageX - this.start.x
    let distanceY = e.changedTouches[0].pageY - this.start.y
    if (Math.abs(distanceX) > Math.abs(distanceY)) {
      this.isMoving = true
      this.move.x = this.start.x + distanceX
      this.move.y = this.start.y + distanceY
      // 当活动子项为第一项且手指向右滑动或者活动项为最后一项切向左滑动的时候,添加阻力,形成一个拉弹簧的效果
      if ((this.active === 0 && distanceX > 0) || (this.active === (this.items.length - 1) && distanceX < 0)) {
        distanceX = distanceX * this.resistance
      }
      this.setTransform(distanceX)
    }
  },
  moveEnd (e) {
    if (this.isMoving) {
      e.preventDefault()
      e.stopPropagation()
      let distance = this.move.x - this.start.x
      if (Math.abs(distance) > this.sensitivity) {
        if (distance < 0) {
          this.next()
        } else {
          this.prev()
        }
      } else {
        this.back()
      }
      this.reset()
      this.isMoving = false
    }
  },
  // 切换下一屏
  next () {},
  // 切换下一屏
  prev () {},
  // 如果滑动达不到阈值,所有元素重置回之前状态
  back () {},
  // 重置动画中用到的一些变量
  reset () {},
  destroy () {
    this.setTransition(‘none‘)
  }
}

接下来咱们完善下next、prev、back、reset函数:

next () {
  let index = this.active + 1
  // 运用动画切换到指定下标的子项
  this.go(index)
},
prev () {
  let index = this.active - 1
  // 运用动画切换到指定下标的子项
  this.go(index)
},
reset () {
  this.start.x = 0
  this.start.y = 0
  this.move.x = 0
  this.move.y = 0
},
back () {
  this.setTransition()
  this.setTransform()
},
go (index) {}

go函数用来做轮播切换的效果。我们在写代码的过程中,可以先定义一个函数来做某个事情,然后再后面用代码来实现逻辑,这样的咱们写代码过程中的思路就会很清晰。接下来实现go函数:

// 运用动画切换到指定下标的子项
go (index) {
  this.active = index
  if (this.active < 0) {
    this.active = 0
  } else if (this.active > this.items.length - 1) {
    this.active = this.items.length - 1
  }
  this.$emit(‘change‘, this.active)
  this.setTransition()
  this.setTransform()
}

到此为止,咱们就已经完成了一个初步的滑动切换轮播图的功能了。但是很多时候,我们的轮播是需要自动播放的,那么如何在现在的基础上增加自动轮播呢?请大家自己思考下,哈哈。下面我们把当前代码整合下:

<template>
  <div ref="swiper" class="r-swiper" :style="{height: _height}"
  @touchstart="moveStart"
  @touchmove="moving"
  @touchend="moveEnd">
    <slot></slot>
    <slot name="indicator">
      <div class="indicator"></div>
    </slot>
  </div>
</template>

<script>
let each = function (ary, callback) {
  for (let i = 0, l = ary.length; i < l; i++) {
    if (callback(ary[i], i) === false) break
  }
}

export default{
  props: {
    height: {
      type: [Number, String],
      default: ‘auto‘
    }
  },
  data () {
    return {
      isMoving: false,
      _width: 0,
      duration: 300,
      container: null,
      items: [],
      active: 0,
      sensitivity: 60, // 触发切换的阈值
      resistance: 0.3, // 阻力系数
      start: {
        x: 0,
        y: 0
      },
      move: {
        x: 0,
        y: 0
      }
    }
  },
  computed: {
    _height () {
      if (typeof this.height === ‘number‘) {
        return this.height + ‘px‘
      } else {
        return this.height
      }
    }
  },
  methods: {
    init () {
      this.container = this.$refs.swiper
      this.items = this.container.querySelectorAll(‘.r-swiper-item‘)
      this.updateItemWidth()
      this.setTransform()
      this.setTransition(‘none‘)
    },
    updateItemWidth () {
      this._width = this.container.offsetWidth || document.documentElement.offsetWidth
    },
    setTransform (offset) {
      offset = offset || 0
      each(this.items, (item, i) => {
        let distance = (i - this.active) * this._width + offset
        let transform = `translate3d(${distance}px, 0, 0)`
        item.style.webkitTransform = transform
        item.style.transform = transform
      })
    },
    setTransition (duration) {
      duration = duration || this.duration
      duration = typeof duration === ‘number‘ ? (duration + ‘ms‘) : duration
      each(this.items, (item) => {
        item.style.webkitTransition = duration
        item.style.transition = duration
      })
    },
    moveStart (e) {
      this.start.x = e.changedTouches[0].pageX
      this.start.y = e.changedTouches[0].pageY
      this.setTransition(‘none‘)
    },
    moving (e) {
      e.preventDefault()
      e.stopPropagation()
      let distanceX = e.changedTouches[0].pageX - this.start.x
      let distanceY = e.changedTouches[0].pageY - this.start.y
      if (Math.abs(distanceX) > Math.abs(distanceY)) {
        this.isMoving = true
        this.move.x = this.start.x + distanceX
        this.move.y = this.start.y + distanceY
        if ((this.active === 0 && distanceX > 0) || (this.active === (this.items.length - 1) && distanceX < 0)) {
          distanceX = distanceX * this.resistance
        }
        this.setTransform(distanceX)
      }
    },
    moveEnd (e) {
      if (this.isMoving) {
        e.preventDefault()
        e.stopPropagation()
        let distance = this.move.x - this.start.x
        if (Math.abs(distance) > this.sensitivity) {
          if (distance < 0) {
            this.next()
          } else {
            this.prev()
          }
        } else {
          this.back()
        }
        this.reset()
        this.isMoving = false
      }
    },
    next () {
      let index = this.active + 1
      this.go(index)
    },
    prev () {
      let index = this.active - 1
      this.go(index)
    },
    reset () {
      this.start.x = 0
      this.start.y = 0
      this.move.x = 0
      this.move.y = 0
    },
    back () {
      this.setTransition()
      this.setTransform()
    },
    destroy () {
      this.setTransition(‘none‘)
      this.clearTimer()
    },
    go (index) {
      this.active = index
      if (this.active < 0) {
        this.active = this.isMoving ? 0 : this.items.length - 1
      } else if (this.active > this.items.length - 1) {
        this.active = this.isMoving ? this.items.length - 1 : 0
      }
      this.$emit(‘change‘, this.active)
      this.setTransition()
      this.setTransform()
    }
  }
}
</script>

<style lang="scss">
@import "../style/color.scss";
@import "../style/fontSize.scss";
@import "../style/mixin.scss";

.r-swiper{
  position: relative;
  overflow: hidden;
  .indicator{
    position: absolute;
    right: 3vw;
    bottom: 3vw;
    width: 10vw;
    height: 10vw;
    line-height: 10vw;
    border-radius: 5vw;
    text-align: center;
    background-color: rgba(0,0,0,.5);
    color: #fff;
    font-size: 14px;
  }
}
</style>

写在最后

今天写这篇文章的时候发现有两个兄弟给我微信转了钱,很谢谢这两个兄弟,感谢你们的支持。其实说实话,我花心思写这个主要目的不是为了钱,而是兴趣,否则我用这个时间用来做点私活什么的收入比这个多多了。只是看到大家的支持,内心很有成就感,尽管很多时候只有1分钱,所以也希望大家有钱的捧个钱场,没钱的捧个人场,哈哈。(未完待续...)

技术图片

技术图片

以上是关于干货--手把手撸vue移动UI框架:滑动轮播的主要内容,如果未能解决你的问题,请参考以下文章

干货--手把手撸vue移动UI框架: 滑动加载

移动端滑动轮播图

滑动轮播的组件

移动端轮播图左右滑动页面会抖是啥原因

CSS 轮播图的实现(纯CSS,连续滑动无倒滑效果)

淘宝滑动轮播案例