滑動效果的原理及實踐一個滑動小插件

本文轉載自blogcss

轉載請註明出處html

目錄

  • 前言
  • 基本原理
  • html結構
  • 實踐
  • 小結

前言

移動端,滑動是很常見的需求。不少同窗都用過swiper.js,本文從原理出發,實踐出一個類swiper的滑動小插件ice-skatinggit

小插件的例子:github

在寫代碼的過程當中產生的一些思考:web

  • 滑動的原理是什麼
  • 怎麼判斷動畫完成
  • 事件綁定到哪一個元素,能否使用事件委託優化
  • pc端和移動端滑動有何不一樣
  • 正在進行的動畫觸摸時怎麼取得當前樣式
  • 如何實現輪播

基本原理

滑動就是用transform: translate(x,y)或者transform: translate3d(x,y,z)去控制元素的移動,在鬆手的時候斷定元素最後的位置,元素的樣式應用transform: translate3d(endx , endy, 0)transition-duration: time來達到一個動畫恢復的效果。標準瀏覽器提供transitionend事件監聽動畫結束,在結束時將動畫時間歸零。算法

Note: 這裏不討論非標準瀏覽器的實現,對於不支持transformtransition的瀏覽器,可使用position: absolute配合lefttop進行移動,而後用基於時間的動畫的算法來模擬動畫效果。瀏覽器

html結構

舉例一個基本的結構: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-skatingexample文件中裏查看完整的css。css代碼並非惟一的,簡單說只要實現下圖的結構就能夠。框架

從圖中能夠直觀的看出,移動的是綠色的元素。className爲ice-slide的元素的寬乘於當前索引(offsetWidth * index),就是每次穩定時的偏移量。例如最開始transform: translate3d(offsetWidth * 0, 0, 0),切換到slide2後,transform: translate3d(offsetWidth * 1, 0, 0),大體就是這樣的過程。ide

實踐

源碼位於ice-skatingdist/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';

})));

狀態容器

用兩個對象來存儲信息

  • 一個頁面能夠實例化不少滑動對象,mainStore存儲的是每一個對象的信息,好比寬高,配置參數之類的。
  • state存儲的是觸摸之類的臨時信息,每次觸摸後都會清空。
var mainStore = Object.create(null);
var state = Object.create(null);

Object.create(null)建立的對象不會帶有Object.prototype上的方法,由於咱們不須要它們,例如toStringvalueOfhasOwnProperty之類的。

構造函數

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;
    }
};

touchStarttransitionDurationEndFn函數每一個實例的容器都會綁定,可是全部實例共用touchMovetouchEnd函數,它們只綁定在document,而且只會綁定一次。使用事件委託有兩個好處:

  1. 減小了元素綁定的事件數,提升了性能。
  2. 若是將touchMovetouchEnd也綁定在容器元素上,當鼠標移出容器元素時,咱們會「失去控制」。在document上意味着能夠「掌控全局」。

過程分析

不會把封裝的函數的代碼都一一列出來,但會說明它的做用。

觸碰瞬間

touchStart函數:

會在觸碰的第一時間調用,基本都在初始化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函數:

移動時會持續調用,若是隻是點擊操做,不會觸發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);
        }
    };

translate函數:

若是支持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)');
        }
    };

觸摸結束

touchEnd函數:

在觸摸結束時調用。

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);
        }   
    }
};

transitionDurationEndFn函數:

動畫執行完成後調用

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)中setTimeoutsetInterval會放入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);     
};

小結

本文記錄了我思考的過程,代碼應該還有不少地方值得完善。

相關文章
相關標籤/搜索