本文轉載自blogcss
轉載請註明出處html
移動端,滑動是很常見的需求。不少同窗都用過swiper.js,本文從原理出發,實踐出一個類swiper的滑動小插件ice-skating。git
小插件的例子:github
在寫代碼的過程當中產生的一些思考:web
滑動就是用transform: translate(x,y)
或者transform: translate3d(x,y,z)
去控制元素的移動,在鬆手的時候斷定元素最後的位置,元素的樣式應用transform: translate3d(endx , endy, 0)
和transition-duration: time
來達到一個動畫恢復的效果。標準瀏覽器提供transitionend
事件監聽動畫結束,在結束時將動畫時間歸零。算法
Note: 這裏不討論非標準瀏覽器的實現,對於不支持transform
和transition
的瀏覽器,可使用position: absolute
配合left
和top
進行移動,而後用基於時間的動畫的算法來模擬動畫效果。瀏覽器
舉例一個基本的結構:app
//example <div class="ice-container"> <div class="ice-wrapper" id="myIceId"> <div class="ice-slide">Slide 1</div> <div class="ice-slide">Slide 2</div> <div class="ice-slide">Slide 3</div> </div> </div>
transform: translate3d(x,y,z)
就是應用在className爲ice-slide
的元素上。這裏不展現css代碼,能夠在ice-skating的example
文件中裏查看完整的css。css代碼並非惟一的,簡單說只要實現下圖的結構就能夠。框架
從圖中能夠直觀的看出,移動的是綠色的元素。className爲ice-slide
的元素的寬乘於當前索引(offsetWidth * index),就是每次穩定時的偏移量。例如最開始transform: translate3d(offsetWidth * 0, 0, 0)
,切換到slide2後,transform: translate3d(offsetWidth * 1, 0, 0)
,大體就是這樣的過程。ide
源碼位於ice-skating的dist/iceSkating.js
。我給插件起名叫ice-skating
,但願它像在冰面同樣順暢^_^
之前咱們會將代碼包裹在一個簡單的匿名函數裏,如今須要加一些額外的代碼來兼容各類模塊標準。
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (factory((global))); }(this, (function (exports) { 'use strict'; })));
用兩個對象來存儲信息
var mainStore = Object.create(null); var state = Object.create(null);
Object.create(null)
建立的對象不會帶有Object.prototype
上的方法,由於咱們不須要它們,例如toString
、valueOf
、hasOwnProperty
之類的。
function iceSkating(option){ if (!(this instanceof iceSkating)) return new iceSkating(option); } iceSkating.prototype = { }
if (!(this instanceof iceSkating)) return new iceSkating(option);
不少庫和框架都有這句,簡單說就是不用new生成也能夠生成實例。
對於觸摸事件,在移動端,咱們會用touchEvent
,在pc端,咱們則用mouseEvent
。因此咱們須要檢測支持什麼事件。
iceSkating.prototype = { support: { touch: (function(){ return !!(('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch); })() }
支持touch則認爲是移動端,不然爲pc端
var events = ic.support.touch ? ['touchstart', 'touchmove', 'touchend']:['mousedown','mousemove','mouseup'];
pc端和移動端這3個函數是通用的。
var touchStart = function(e){}; var touchMove = function(e){}; var touchEnd = function(e){};
var ic = this; var initEvent = function(){ var events = ic.support.touch ? ['touchstart', 'touchmove', 'touchend']: ['mousedown','mousemove','mouseup']; var transitionEndEvents = ['webkitTransitionEnd', 'transitionend', 'oTransitionEnd', 'MSTransitionEnd', 'msTransitionEnd']; for (var i = 0; i < transitionEndEvents.length; i++) { ic.addEvent(container, transitionEndEvents[i], transitionDurationEndFn, false); } ic.addEvent(container, events[0], touchStart, false); //默認阻止容器元素的click事件 if(ic.store.preventClicks) ic.addEvent(container, 'click', ic.preventClicks, false); if(!isInit){ ic.addEvent(document, events[1], touchMove, false); ic.addEvent(document, events[2], touchEnd, false); isInit = true; } };
touchStart
和transitionDurationEndFn
函數每一個實例的容器都會綁定,可是全部實例共用touchMove
和touchEnd
函數,它們只綁定在document
,而且只會綁定一次。使用事件委託有兩個好處:
touchMove
和touchEnd
也綁定在容器元素上,當鼠標移出容器元素時,咱們會「失去控制」。在document
上意味着能夠「掌控全局」。不會把封裝的函數的代碼都一一列出來,但會說明它的做用。
會在觸碰的第一時間調用,基本都在初始化state的信息
var touchStart = function(e){ //mouse事件會提供which值, e.which爲3時表示按下鼠標右鍵,鼠標右鍵會觸發mouseup,但右鍵不容許移動滑塊 if (!ic.support.touch && 'which' in e && e.which === 3) return; //獲取起始座標。TouchEvent使用e.targetTouches[0].pageX,MouseEvent使用e.pageX。 state.startX = e.type === 'touchstart' ? e.targetTouches[0].pageX : e.pageX; state.startY = e.type === 'touchstart' ? e.targetTouches[0].pageY : e.pageY; //時間戳 state.startTime = e.timeStamp; //綁定事件的元素 state.currentTarget = e.currentTarget; state.id = e.currentTarget.id; //觸發事件的元素 state.target = e.target; //獲取當前滑塊的參數信息 state.currStore = mainStore[e.currentTarget.id]; //state的touchStart 、touchMove、touchEnd表明是否進入該函數 state.touchEnd = state.touchMove = false; state.touchStart = true; //表示滑塊移動的距離 state.diffX = state.diffY = 0; //動畫運行時的座標與動畫運行前的座標差值 state.animatingX = state.animatingY = 0; };
在移動滑塊時,可能滑塊正在動畫中,這是須要考慮一種特殊狀況。滑塊的移動應該依據如今的位置計算。
如何知道動畫運行中的信息呢,可使用window.getComputedStyle(element, [pseudoElt])
,它返回的樣式是一個實時的CSSStyleDeclaration
對象。用它取transform
的值會返回一個 2D 變換矩陣,像這樣matrix(1, 0, 0, 1, -414.001, 0)
,最後兩位就是x,y值。
簡單封裝一下,就能夠取得當前動畫translate的x,y值了。
var getTranslate = function(el){ var curStyle = window.getComputedStyle(el); var curTransform = curStyle.transform || curStyle.webkitTransform; var x,y; x = y = 0; curTransform = curTransform.split(', '); if (curTransform.length === 6) { x = parseInt(curTransform[4], 10); y = parseInt(curTransform[5], 10); } return {'x': x,'y': y}; };
移動時會持續調用,若是隻是點擊操做,不會觸發touchMove。
var touchMove = function(e){ // 1. 若是當前觸發touchMove的元素和觸發touchStart的元素不一致,不容許滑動。 // 2. 執行touchMove時,需保證touchStart已執行,且touchEnd未執行。 if(e.target !== state.target || state.touchEnd || !state.touchStart) return; state.touchMove = true; //取得當前座標 var currentX = e.type === 'touchmove' ? e.targetTouches[0].pageX : e.pageX; var currentY = e.type === 'touchmove' ? e.targetTouches[0].pageY : e.pageY; var currStore = state.currStore; //觸摸時若是動畫正在運行 if(currStore.animating){ // 取得當前元素translate的信息 var animationTranslate = getTranslate(state.currentTarget); //計算動畫的偏移量,currStore.translateX和currStore.translateY表示的是滑塊最近一次穩定時的translate值 state.animatingX = animationTranslate.x - currStore.translateX; state.animatingY = animationTranslate.y - currStore.translateY; currStore.animating = false; //移除動畫時間 removeTransitionDuration(currStore.container); } //若是輪播進行中,將定時器清除 if(currStore.autoPlayID !== null){ clearTimeout(currStore.autoPlayID); currStore.autoPlayID = null; } //判斷移動方向是水平仍是垂直 if(currStore.direction === 'x'){ //currStore.touchRatio是移動係數 state.diffX = Math.round((currentX - state.startX) * currStore.touchRatio); //移動元素 translate(currStore.container, state.animatingX + state.diffX + state.currStore.translateX, 0, 0); }else{ state.diffY = Math.round((currentY - state.startY) * state.currStore.touchRatio); translate(currStore.container, 0, state.animatingY + state.diffY + state.currStore.translateY, 0); } };
若是支持translate3d,會優先使用它,translate3d會提供硬件加速。有興趣能夠看看這篇blog兩張圖解釋CSS動畫的性能
var translate = function(ele, x, y, z){ if (ic.support.transforms3d){ transform(ele, 'translate3d(' + x + 'px, ' + y + 'px, ' + z + 'px)'); } else { transform(ele, 'translate(' + x + 'px, ' + y + 'px)'); } };
在觸摸結束時調用。
var touchEnd = function(e){ state.touchEnd = true; if(!state.touchStart) return; var fastClick ; var currStore = state.currStore; //若是整個觸摸過程時間小於fastClickTime,會認爲這次操做是點擊。但默認是屏蔽了容器的click事件的,因此提供一個clickCallback參數,會在點擊操做時調用。 if(fastClick = (e.timeStamp - state.startTime) < currStore.fastClickTime && !state.touchMove && typeof currStore.clickCallback === 'function'){ currStore.clickCallback(); } if(!state.touchMove) return; //若是移動距離沒達到切換頁的臨界值,則讓它恢復到最近的一次穩定狀態 if(fastClick || (Math.abs(state.diffX) < currStore.limitDisX && Math.abs(state.diffY) < currStore.limitDisY)){ //在transitionend事件綁定的函數中斷定是否重啓輪播,可是若是transform先後兩次的值同樣時,不會觸發transitionend事件,因此在這裏斷定是否重啓輪播 if(state.diffX === 0 && state.diffY === 0 && currStore.autoPlay) autoPlay(currStore); //恢復到最近的一次穩定狀態 recover(currStore, currStore.translateX, currStore.translateY, 0); }else{ //位移知足切換 if(state.diffX > 0 || state.diffY > 0) { //切換到上一個滑塊 moveTo(currStore, currStore.index - 1); }else{ //切換到下一個滑塊 moveTo(currStore, currStore.index + 1); } } };
動畫執行完成後調用
var transitionDurationEndFn = function(){ //將動畫狀態設置爲false ic.store.animating = false; //執行自定義的iceEndCallBack函數 if(typeof ic.store.iceEndCallBack === 'function') ic.store.iceEndCallBack(); //將動畫時間歸零 transitionDuration(container, 0); //清空state if(ic.store.id === state.id) state = Object.create(null); };
至此,一個完整的滑動過程結束。
第一時間想到的是使用setInterval
或者遞歸setTimeout
實現輪播,但這樣作並不優雅。
事件循環(EventLoop)中setTimeout
或setInterval
會放入macrotask
隊列中,裏面的函數會放入microtask
,當這個macrotask
執行結束後全部可用的 microtask
將會在同一個事件循環中執行。
咱們極端的假設setInterval
設定爲200ms,動畫時間設爲1000ms。每隔200ms, macrotask
隊列中就會插入setInterval
,但咱們的動畫此時沒有完成,因此用setInterval
或者遞歸setTimeout
的輪播在這種狀況下是有問題的。
最佳思路是在每次動畫結束後再將輪播開啓。
動畫結束執行的函數: var transitionDurationEndFn = function(){ ... //檢測是否開啓輪播 if(ic.store.autoPlay) autoPlay(ic.store); };
輪播函數也至關簡單
var autoPlay = function(store){ store.autoPlayID = setTimeout(function(){ //當前滑塊的索引 var index = store.index; ++index; //到最後一個了,重置爲0 if(index === store.childLength){ index = 0; } //移動 moveTo(store, index); },store.autoplayDelay); };
本文記錄了我思考的過程,代碼應該還有不少地方值得完善。