乾貨--手把手擼vue移動UI框架:滑動輪播

前言

昨天寫了一篇側邊菜單組件的文章,閱讀人數挺多的,心裏很欣喜(偷着樂,第一篇文章有這麼多人看)!乘着這股勁,今天在繼續寫一篇咱們平時工做中更經常使用的滑動輪播組件的文章。javascript

效果展現

老規矩,我們先看作成後的效果,而後我們再一步步的開始製做:
css

組件組成分析

在實際的工做中,我們輪播中的內容形式可能有不少種:圖片、文本、視頻、其餘DOM結構等。因此我們的輪播組件必須能知足這幾種應用狀況。那麼咱們能夠把組件分兩部分:html

  1. 能夠高度定製的子組件,負責渲染輪播中的每個子項
  2. 負責輪播的父組件,用來處理通用的滑動事件、自動輪播、指示器等功能

咱們如今這定義子組件的名稱爲swiper-item;父組件名稱爲swiperjava

DOM組成

首先我們的子組件中負責渲染自定義的內容,則子組件中須要一個插槽slot。 web

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插槽用來自定義指示器的樣式,由於在實際使用過程當中指示器樣式極可能是須要定製的。flex

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分錢,因此也但願你們有錢的捧個錢場,沒錢的捧我的場,哈哈。(未完待續...)

相關文章
相關標籤/搜索