昨天寫了一篇側邊菜單組件的文章,閱讀人數挺多的,心裏很欣喜(偷着樂,第一篇文章有這麼多人看)!乘着這股勁,今天在繼續寫一篇咱們平時工做中更經常使用的滑動輪播組件的文章。javascript
老規矩,我們先看作成後的效果,而後我們再一步步的開始製做:
css
在實際的工做中,我們輪播中的內容形式可能有不少種:圖片、文本、視頻、其餘DOM結構等。因此我們的輪播組件必須能知足這幾種應用狀況。那麼咱們能夠把組件分兩部分:html
咱們如今這定義子組件的名稱爲swiper-item;父組件名稱爲swiperjava
首先我們的子組件中負責渲染自定義的內容,則子組件中須要一個插槽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
移動端的視圖大小有限,子項的大小通常是父組件的所有可視視圖。
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>
老規矩,寫JS代碼前我們先理清交互邏輯:
初始化的時候須要:
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分錢,因此也但願你們有錢的捧個錢場,沒錢的捧我的場,哈哈。(未完待續...)