swipe的使用是要引入swipe組件和swipe-item組件就可以使用了。
swipe組件是輪播圖父級盒子,swipe-item組件是父級盒子里面的一個個輪播圖。
使用的時候類似這樣:
<swipe :auto="4000"> <swipeItem v-for="(item, index) in bannerList" :key="index"> <img src="xxxxxxxxxxxxxxxxxxxxxxxxxxxxx" alt=""> </swipeItem> </swipe>
下面是swipe.vue:
<template>
<div class="mint-swipe">
<div class="mint-swipe-items-wrap" ref="wrap">
<slot></slot>
</div>
<div class="mint-swipe-indicators" v-show="showIndicators">
<div class="mint-swipe-indicator" v-for="(page, $index) in pages" :key="$index" :class="{ 'is-active': $index === index }"></div>
</div>
</div>
</template>
<style lang="scss" scoped>
.mint-swipe {
overflow: hidden;
position: relative;
height: 100%;
.mint-swipe-items-wrap {
position: relative;
overflow: hidden;
height: 100%;
& > div {
position: absolute;
transform: translateX(-100%);
size: 100% 100%;
display: none;
&.is-active {
display: block;
transform: none;
}
}
}
.mint-swipe-indicators {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
}
.mint-swipe-indicator {
width: 8px;
height: 8px;
size: 8px 8px;
display: inline-block;
border-radius: 100%;
background: #000;
opacity: 0.2;
margin: 0 3px;
&.is-active {
background: #ccc;
}
}
}
</style>
<script>
import { once } from '@/ui/utils/dom.js';
import { addClass, removeClass } from '@/ui/utils/dom.js';
export default {
name: 'mt-swipe',
created() {
this.dragState = {}; //拖動時存儲拖動狀態
},
data() {
return {
ready: false,
dragging: false,
userScrolling: false,
animating: false,
index: 0, //當前顯示的輪播的索引index
pages: [], //swipeItem子組件的DOM數組
timer: null,
reInitTimer: null,
noDrag: false,
isDone: false
};
},
props: {
speed: { //動畫的速度
type: Number,
default: 300
},
defaultIndex: { //默認第一次顯示的圖片的索引index
type: Number,
default: 0
},
auto: { //自動播放間隔時間
type: Number,
default: 3000
},
continuous: { //是否循環播放
type: Boolean,
default: true
},
showIndicators: { //是否顯示indicators
type: Boolean,
default: true
},
noDragWhenSingle: { //只有一個圖片的時候不允許拖拽
type: Boolean,
default: true
},
prevent: { //是否在 touchstart 事件觸發時阻止事件的默認行為。設為 true 可提高運行在低版本安卓瀏覽器時的性能
type: Boolean,
default: false
},
stopPropagation: { //是否在 touchstart 事件觸發時阻止冒泡。
type: Boolean,
default: false
}
},
watch: { //偵聽器,如果index發生變化,就觸發change自定義事件
index(newIndex) {
this.$emit('change', newIndex);
}
},
methods: {
swipeItemCreated() { //swipeItem調用,新的swipeItem創建后更新pages數組
if (!this.ready) return;
clearTimeout(this.reInitTimer);
this.reInitTimer = setTimeout(() => {
this.reInitPages();
}, 100);
},
swipeItemDestroyed() { //swipeItem調用,新的swipeItem銷毀后更新pages數組
if (!this.ready) return;
clearTimeout(this.reInitTimer);
this.reInitTimer = setTimeout(() => {
this.reInitPages();
}, 100);
},
rafTranslate(element, initOffset, offset, callback, nextElement) {
let ALPHA = 0.88;
this.animating = true; //正在動畫標識變為true
var _offset = initOffset; //用戶滑動偏移量
var raf = 0;
function animationLoop() {
if (Math.abs(_offset - offset) < 0.5) { //如果用戶已經基本上滑動完了,那么就不用再滑動了
this.animating = false;
_offset = offset; //用戶滑動的偏移直接等於一頁的寬度
element.style.webkitTransform = '';
if (nextElement) {
nextElement.style.webkitTransform = '';
}
//上面取消element和nextElement上的平移動畫
cancelAnimationFrame(raf); //取消一個先前通過調用window.requestAnimationFrame()方法添加到計划中的動畫幀請求.
if (callback) {
callback();
}
return;
}
_offset = ALPHA * _offset + (1.0 - ALPHA) * offset;
element.style.webkitTransform = `translate3d(${_offset}px, 0, 0)`; //element移動出邊界看不到
if (nextElement) {
nextElement.style.webkitTransform = `translate3d(${_offset - offset}px, 0, 0)`; //nextElement剛好移動到中間成為當前輪播圖
}
raf = requestAnimationFrame(animationLoop.bind(this));//requestAnimationFrame方法告訴瀏覽器您希望執行動畫並請求瀏覽器在下一次重繪之前調用指定的函數來更新動畫
}
animationLoop.call(this);
},
translate(element, offset, speed, callback) {
if (speed) { //如果傳遞了speed則動畫是有時間過渡的
this.animating = true; //正在進行動畫標識變為true
element.style.webkitTransition = '-webkit-transform ' + speed + 'ms ease-in-out';
setTimeout(() => {
element.style.webkitTransform = `translate3d(${offset}px, 0, 0)`;
}, 50);
var called = false;
var transitionEndCallback = () => {
if (called) return;
called = true;
this.animating = false; //正在進行動畫標識變為false
element.style.webkitTransition = '';
element.style.webkitTransform = '';
//清空動畫樣式
if (callback) {//如果有回調就執行回調
callback.apply(this, arguments);
}
};
once(element, 'webkitTransitionEnd', transitionEndCallback); //給對象綁定一個webkitTransitionEnd事件,在過渡動畫結束后調用
setTimeout(transitionEndCallback, speed + 100); // webkitTransitionEnd maybe not fire on lower version android.
} else { //沒有傳遞speed的動畫是立即執行的
element.style.webkitTransition = '';
element.style.webkitTransform = `translate3d(${offset}px, 0, 0)`;
}
},
reInitPages() { //獲取子組件DOM元素存入this.pages
var children = this.$children; //vm.$children當前實例的子組件,也就是swipeItem組件
this.noDrag = children.length === 1 && this.noDragWhenSingle;
//如果只有一張圖且設置了noDragWhenSingle為true,就設置this.noDrag為true,只有一張圖的時候不允許拖動
var pages = [];
var intDefaultIndex = Math.floor(this.defaultIndex);
var defaultIndex = (intDefaultIndex >= 0 && intDefaultIndex < children.length) ? intDefaultIndex : 0;
this.index = defaultIndex;
//根據用戶傳入的defaultIndex計算出默認一開始顯示的圖片的索引,然后把它賦值給this.index
children.forEach(function(child, index) {
pages.push(child.$el);
//循環swipeItem子組件數組,將子組件的根DOM元素存入pages數組
removeClass(child.$el, 'is-active');
//移除子組件上的active樣式
if (index === defaultIndex) {
addClass(child.$el, 'is-active');
}
//給當前顯示的子組件添加active樣式
});
this.pages = pages;
//this.pages是swipeItem子組件的根DOM數組
},
doAnimate(towards, options) { //切換輪播動畫
if (this.$children.length === 0) return;//如果沒有輪播圖,直接retrun
if (!options && this.$children.length < 2) return; //如果只有一張圖且沒有options輪播狀態參數,就return
var prevPage, nextPage, currentPage, pageWidth, offsetLeft, speedX;
//prevPage前一頁輪播的DOM,nextPage下一頁輪播的DOM,currentPage當前輪播圖的DOM,pageWidth輪播圖的寬度,offsetLeft觸摸滑動的距離,speedX觸摸滑動的速度
var speed = this.speed || 300; //動畫速度
var index = this.index; //當前索引
var pages = this.pages; //swipeItem的DOM數組
var pageCount = pages.length; //輪播圖數量
if (!options) {
pageWidth = this.$el.clientWidth; //輪播圖的寬度
currentPage = pages[index]; //當前輪播圖的DOM
prevPage = pages[index - 1]; //前一頁輪播的DOM
nextPage = pages[index + 1]; //下一頁輪播的DOM
if (this.continuous && pages.length > 1) { //如果用戶設置了循環播放
if (!prevPage) {
prevPage = pages[pages.length - 1]; //循環播放的時候,如果沒有前一頁那么前一頁就是數組里最后一頁
}
if (!nextPage) {
nextPage = pages[0]; //循環播放的時候,如果沒有下一頁那么下一頁就是數組里的第一頁
}
}
if (prevPage) { //前一頁歸位且display:block
prevPage.style.display = 'block';
this.translate(prevPage, -pageWidth);
}
if (nextPage) { //下一頁歸位且display:block
nextPage.style.display = 'block';
this.translate(nextPage, pageWidth);
}
} else {
prevPage = options.prevPage;
currentPage = options.currentPage;
nextPage = options.nextPage;
pageWidth = options.pageWidth;
offsetLeft = options.offsetLeft;
speedX = options.speedX;
}
var newIndex;
var oldPage = this.$children[index].$el; //舊的current輪播DOM
if (towards === 'prev') { //翻到前一頁
if (index > 0) {
newIndex = index - 1; //計算出新的current輪播的索引
}
if (this.continuous && index === 0) {
newIndex = pageCount - 1; //如果循環,而且是第一頁,那么新的一頁就是最后一頁
}
} else if (towards === 'next') { //翻到下一頁
if (index < pageCount - 1) {
newIndex = index + 1; //計算出新的current輪播的索引
}
if (this.continuous && index === pageCount - 1) {
newIndex = 0; //如果循環,而且到最后一頁了,那么新的一頁就是第一頁
}
}
var callback = () => {
if (newIndex !== undefined) {
var newPage = this.$children[newIndex].$el;
removeClass(oldPage, 'is-active');
addClass(newPage, 'is-active');
this.index = newIndex;
//將active類名換到當前頁身上,這樣oldPage就會display:none
}
if (this.isDone) {
this.end(); //觸發end事件
}
if (prevPage) {
prevPage.style.display = '';
}
if (nextPage) {
nextPage.style.display = '';
}
//前一頁和后一頁的display樣式都回歸默認狀態
};
setTimeout(() => {
if (towards === 'next') {//方向是下一頁
this.isDone = true;
this.before(currentPage); //觸發before事件
if (speedX) { //如果是用戶滑動就執行rafTranslate
this.rafTranslate(currentPage, offsetLeft, -pageWidth, callback, nextPage);
} else { //自動輪播調用translate
this.translate(currentPage, -pageWidth, speed, callback); //當前頁移動到左邊
if (nextPage) {
this.translate(nextPage, 0, speed); //下一頁移動到中間變成當前頁
}
}
} else if (towards === 'prev') { //方向是前一頁
this.isDone = true;
this.before(currentPage); //觸發before事件
if (speedX) { //如果是用戶滑動就執行rafTranslate
this.rafTranslate(currentPage, offsetLeft, pageWidth, callback, prevPage);
} else { //自動輪播調用translate
this.translate(currentPage, pageWidth, speed, callback); //當前頁移動到右邊
if (prevPage) {
this.translate(prevPage, 0, speed); //前一頁移動到中間變成當前頁
}
}
} else { //towards為null的情況,只有一張圖片,或者用戶設置不能循環時滑到了開頭或者結尾
this.isDone = false;
//只有一張圖的時候什么都不變化,不移動,多張圖片且不循環的時候讓當前圖片拖動后再次回到正中的原位置
this.translate(currentPage, 0, speed, callback);
//下面是如果有前一張或者后一張圖片的話就讓他們彈回去,不讓他們到正中的位置
if (typeof offsetLeft !== 'undefined') {
if (prevPage && offsetLeft > 0) {
this.translate(prevPage, pageWidth * -1, speed);
}
if (nextPage && offsetLeft < 0) {
this.translate(nextPage, pageWidth, speed);
}
} else {
if (prevPage) {
this.translate(prevPage, pageWidth * -1, speed);
}
if (nextPage) {
this.translate(nextPage, pageWidth, speed);
}
}
}
}, 10);
},
next() { //每過this.auto時間就執行next()方法切換輪播
this.doAnimate('next');
},
prev() {
this.doAnimate('prev');
},
before() {
this.$emit('before', this.index); //觸發before事件
},
end() {
this.$emit('end', this.index); //觸發end事件
},
doOnTouchStart(event) {
if (this.noDrag) return; //如果只有一張圖且用戶設置不允許拖動,就return
var element = this.$el; //element是swipe的DOM元素
var dragState = this.dragState; //拖拽狀態
var touch = event.touches[0];
//TouchEvent.touches獲取到touch事件的觸點,一根手指就只有一個,兩根手指就兩個,此處獲取第一個觸點
dragState.startTime = new Date(); //拖拽起始時間
dragState.startLeft = touch.pageX; //記下拖拽起始的時候x軸坐標
dragState.startTop = touch.pageY; //記下拖拽起始的時候y軸坐標
dragState.startTopAbsolute = touch.clientY; //觸點相對於可見視區上沿的y坐標
dragState.pageWidth = element.offsetWidth; //swipe盒子寬度
dragState.pageHeight = element.offsetHeight; //swipe盒子高度
var prevPage = this.$children[this.index - 1]; //前一頁對應的swipeItem組件
var dragPage = this.$children[this.index]; //當前頁對應的swipeItem組件
var nextPage = this.$children[this.index + 1]; //下一頁對應的swipeItem組件
if (this.continuous && this.pages.length > 1) {
//如果設置了循環播放,沒有前一頁前一頁就是最后一頁,沒有后一頁后一頁就是第一頁
if (!prevPage) {
prevPage = this.$children[this.$children.length - 1];
}
if (!nextPage) {
nextPage = this.$children[0];
}
}
dragState.prevPage = prevPage ? prevPage.$el : null;
dragState.dragPage = dragPage ? dragPage.$el : null;
dragState.nextPage = nextPage ? nextPage.$el : null;
//講前一頁,當前頁,后一頁對應的DOM元素存入this.dragState
if (dragState.prevPage) {
dragState.prevPage.style.display = 'block';
}
if (dragState.nextPage) {
dragState.nextPage.style.display = 'block';
}
//前一頁和后一頁的顯示狀態都從none變為block
},
doOnTouchMove(event) {
if (this.noDrag) return; //如果只有一張圖且用戶設置不允許拖動,就return
var dragState = this.dragState; //拖拽狀態
var touch = event.touches[0]; //觸點
dragState.speedX = touch.pageX - dragState.currentLeft; //一瞬間x軸移動偏移量用來計算速度
dragState.currentLeft = touch.pageX; //拖拽移動后當前的x軸坐標
dragState.currentTop = touch.pageY; //拖拽移動后當前y軸坐標
dragState.currentTopAbsolute = touch.clientY; //拖拽移動后觸點相對於可見視區上沿的y坐標
var offsetLeft = dragState.currentLeft - dragState.startLeft; //當前x軸移動的總偏移
var offsetTop = dragState.currentTopAbsolute - dragState.startTopAbsolute; //當前y軸移動總偏移
var distanceX = Math.abs(offsetLeft); //x軸移動距離絕對值
var distanceY = Math.abs(offsetTop); //y軸移動距離絕對值
if (distanceX < 5 || (distanceX >= 5 && distanceY >= 1.73 * distanceX)) {
//判斷用戶滑動是否是為了切換輪播,如果不是就說明用戶滑動是為了操作其他元素,就return
this.userScrolling = true;
return;
} else {
//如果的確是為了操作輪播,那么就阻止touch默認行為
this.userScrolling = false;
event.preventDefault();
}
offsetLeft = Math.min(Math.max(-dragState.pageWidth + 1, offsetLeft), dragState.pageWidth - 1);
//根據x軸的偏移判斷出輪播的方向
var towards = offsetLeft < 0 ? 'next' : 'prev';
if (dragState.prevPage && towards === 'prev') {
this.translate(dragState.prevPage, offsetLeft - dragState.pageWidth);
}
this.translate(dragState.dragPage, offsetLeft);
if (dragState.nextPage && towards === 'next') {
this.translate(dragState.nextPage, offsetLeft + dragState.pageWidth);
}
//根據滑動的方向來移動當前頁dragPage,下一頁nextPage或者上一頁prevPage
//如果偏移小於0,說明從右往左滑動,方向next,nextPage的滑動距離剛好是偏移加上swipe寬度,因為這時候偏移是負數
//如果偏移大於0,說明從左往右滑動,方向prev,prevPage的滑動距離剛好是偏移減去swipe寬度,因為這時候偏移是正數
},
doOnTouchEnd() {
if (this.noDrag) return; //如果只有一張圖且用戶設置不允許拖動,就return
var dragState = this.dragState; //拖拽狀態
var dragDuration = new Date() - dragState.startTime; //拖拽花費的時間
var towards = null; //拖拽方向置空
var offsetLeft = dragState.currentLeft - dragState.startLeft; //拖動的x軸偏移量
var offsetTop = dragState.currentTop - dragState.startTop; //拖動y軸偏移量
var pageWidth = dragState.pageWidth; //swipe寬度
var index = this.index; //當前輪播索引
var pageCount = this.pages.length; //輪播圖數量
if (dragDuration < 300) { //如果拖動花費時間小於300毫秒
let fireTap = Math.abs(offsetLeft) < 5 && Math.abs(offsetTop) < 5; //如果x軸和y軸的移動距離都小於5
if (isNaN(offsetLeft) || isNaN(offsetTop)) {
fireTap = true;
}
if (fireTap) {
this.$children[this.index].$emit('tap'); //當移動距離很小的時候,說明是點擊事件,就觸發swipeItem組件的tap事件
}
}
if (dragDuration < 300 && dragState.currentLeft === undefined) return;
if (dragDuration < 300 || Math.abs(offsetLeft) > pageWidth / 2) {
towards = offsetLeft < 0 ? 'next' : 'prev'; //判斷方向
}
if (!this.continuous) { //如果用戶設置了不能循環,且到了開頭或者結尾,那就方向為null
if ((index === 0 && towards === 'prev') || (index === pageCount - 1 && towards === 'next')) {
towards = null;
}
}
if (this.$children.length < 2) { //如果只有一張圖,方向為null
towards = null;
}
this.doAnimate(towards, {
offsetLeft: offsetLeft,
pageWidth: dragState.pageWidth,
prevPage: dragState.prevPage,
currentPage: dragState.dragPage,
nextPage: dragState.nextPage,
speedX: dragState.speedX
});
this.dragState = {};
},
initTimer() { //初始化自動切換輪播的定時器
if (this.auto > 0 && !this.timer) { //auto是自動切換輪播的間隔時間
this.timer = setInterval(() => {
if (!this.continuous && (this.index >= this.pages.length - 1)) {
//如果設置continuous為false,則不循環切換,到最后一頁就會停止
return this.clearTimer();
}
if (!this.dragging && !this.animating) {
this.next();
}
}, this.auto);
}
},
clearTimer() { //清除自動切換定時器
clearInterval(this.timer);
this.timer = null;
}
},
destroyed() { //實例被銷毀時調用
if (this.timer) { //清除自動切換定時器
this.clearTimer();
}
if (this.reInitTimer) {
clearTimeout(this.reInitTimer);
this.reInitTimer = null;
}
},
mounted() {
this.ready = true;
this.initTimer(); //初始化自動切換定時器
this.reInitPages(); //獲取子組件DOM元素存入this.pages
var element = this.$el;
//以下為swipeDOM元素添加touch事件
element.addEventListener('touchstart', (event) => {
if (this.prevent) event.preventDefault(); //如果用戶設置了就阻止默認行為
if (this.stopPropagation) event.stopPropagation(); //如果用戶設置了就阻止冒泡
if (this.animating) return; //如果正在進行切換動畫就return
this.dragging = true; //this.dragging正在拖拽標識變為true
this.userScrolling = false;
this.doOnTouchStart(event);
});
element.addEventListener('touchmove', (event) => {
if (!this.dragging) return;
if (this.timer) this.clearTimer(); //用戶開始拖拽后清除掉自動切換的定時器
this.doOnTouchMove(event);
});
element.addEventListener('touchend', (event) => {
if (this.userScrolling) { //如果用戶的touch不是為了操作了輪播圖,就回歸初始狀態
this.dragging = false;
this.dragState = {};
return;
}
if (!this.dragging) return;
this.initTimer(); //拖拽結束后重新開啟自動切換定時器
this.doOnTouchEnd(event);
this.dragging = false; //this.dragging正在拖拽標示變為false
});
}
}
</script>
下面是swipe-item:
<template>
<div class="mint-swipe-item">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'mt-swipe-item',
mounted() {
this.$parent && this.$parent.swipeItemCreated(this); //調用父組件的方法更新DOM數組
},
destroyed() {
this.$parent && this.$parent.swipeItemDestroyed(this); //調用父組件的方法更新DOM數組
}
};
</script>
輪播圖里的輪播切換分為兩種情況:一種是自動切換,一種是用戶touch事件的時候手動切換。用戶手動切換的時候,自動切換的定時器會被清除掉。
輪播切換是依靠css屬性transform變換里的translateX來改變輪播圖的x軸坐標。只有當用戶touchstart的時候,或者自動切換的定時器觸發動畫開始的時候,前一張輪播圖prevPage和后一張輪播圖nextPage才會display:block,在不改變輪播圖的時候,前一張和后一張圖片都是display:none的。
