Hammer.js源碼簡析

開始

話說上週週末閒的蛋疼,忽然想了解一下前端手勢如何處理,好解開本身一個知識盲點,因而開始啃源碼。。。並紀錄一下。javascript

一個手勢

在咱們的前端頁面裏面複雜的手勢應該是很少見的,通常經常使用就是拖拉,雙擊,放大縮小這幾個,可是合理運用手勢很明顯也能爲咱們頁面的交互體驗有一點增色,那麼問題來了,如何識別一個手勢尼?css

Hammer.js

Hammer.js 應該算是前端使用的比較普遍的一個手勢框架了(我所瞭解的還有一個AlloyTouch,更小,固然它提供的抽象程度是不如Hammer.js的),今天就拿這個框架來開刀吧。前端

配置參數

咱們先來看Hammer.js的配置參數:java

{
      //手勢事件觸發時,是否同時觸發對應的一個自定義的dom事件,固然這個沒有直接綁定事件回調高效
      domEvents: false, 
      //這個會影響對應的css屬性touch-action的值,下面會接着說
      touchAction: TOUCH_ACTION_COMPUTE, 
      enable: true, //是否開啓手勢識別
      //能夠指定在其餘的元素上來檢測與touch相關的事件並做爲輸入源,若是沒設置就是當前檢測的元素了
      inputTarget: null, 
      inputClass: null, //輸入源類型,鼠標仍是觸摸或者是混合
      recognizers: [], //咱們配置的手勢識別器
      //預設的一些手勢識別器,格式:[RecognizerClass, options, [recognizeWith, ...], [requireFailure, ...]]
      preset: [ 
          [RotateRecognizer, { enable: false }],
          [PinchRecognizer, { enable: false }, ['rotate']],
          [SwipeRecognizer, { direction: DIRECTION_HORIZONTAL }],
          [PanRecognizer, { direction: DIRECTION_HORIZONTAL }, ['swipe']],
          [TapRecognizer],
          [TapRecognizer, { event: 'doubletap', taps: 2 }, ['tap']],
          [PressRecognizer]
      ],
      cssProps: { //額外的一些css屬性
        userSelect: 'none',
        touchSelect: 'none',
        touchCallout: 'none',
        contentZooming: 'none',
        userDrag: 'none',
        tapHighlightColor: 'rgba(0,0,0,0)'
     }
}

總的來講配置參數很少,也不算複雜,這個框架基本也算是開箱即用了,好,咱們接着再深刻一點。瀏覽器

初始化

接着來到源碼裏面manager.js,能夠看到如下一段的代碼:session

export default class Manager {
    constructor() {
        ...
        this.element = element;
        this.input = createInputInstance(this);// 1
        this.touchAction = new TouchAction(this,this.options.touchAction);// 2

        toggleCssProps(this, true);
        
        each(this.options.recognizers, (item) => { //3
           let recognizer = this.add(new (item[0])(item[1]));
               item[2] && recognizer.recognizeWith(item[2]);
               item[3] && recognizer.requireFailure(item[3]);
           }, this);
        }
    ...
}

1.新建一個輸入源
根據設備的不一樣手勢多是來自鼠標也有可能來自手機上的觸摸屏,並且mouse event的屬性和touch event的屬性是有一絲差別的(還有pointer event),因此爲了方便後續處理,Hammer.js也分別定義了不一樣類型輸入源:MouseInput,PointerEventInput,SingleTouchInput,TouchInput和TouchMouseInput;並針對不一樣的事件,對參數作了一個簡單處理(handler方法),最終獲得統一格式的數據輸出,就像這樣:框架

{
       pointers: touches[0],
       changedPointers: touches[1],
       pointerType: INPUT_TYPE_TOUCH,
       srcEvent: ev
    }

在獲取統一格式的輸入數據後,會交由InputHandler進一步處理,會判斷此次輸入是手勢的開始仍是結束,若是是開始就會新建一個手勢識別的session,而且計算一些與手勢相關的數據(角度,偏移距離,移動方向等),具體能夠在compute-input-data.js裏面看到。
通過以這一輪計算,咱們已經有足夠的數據來支持以後的手勢識別了。
另一提的是,這五種輸入源都繼承了Input,在Input裏面事件是這樣綁定的:dom

this.evEl && addEventListeners(this.element, this.evEl, this.domHandler);
    this.evTarget && addEventListeners(this.target, this.evTarget, this.domHandler);
    this.evWin && addEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler);

有三種綁定目標,當前的element,inputTarget,element所屬的window,在window上綁定事件處理器仍是很必要的(例如拖拉一個元素的時候);另外翻了一下代碼,inputTarget綁定都是touch相關的事件,不是很明白它的意圖和場景,爲何要分離一個目標單獨處理觸摸事件。性能

2.設置元素樣式裏touch-action的值
在手機瀏覽器裏面,通常也會自帶一些手勢處理,例如向右滑動或者向左滑動就是前進和後退,因此除了咱們本身定義手勢,還須要對瀏覽器的手勢作一些限制或者禁止。
這裏也舉個栗子吧,在Hammer.js裏面默認提供拖拉手勢的識別器(就是pan.js),當在檢測水平方向的拖拉的時候,這個識別器會把touch-action的值設爲pay-y(容許瀏覽器處理垂直方向的拖拉,能夠是一個垂直的滾動或者其餘),那若是我又接着定義一個垂直方向拖拉的識別器時,touch-action的值是多少尼?(答案就是none,瀏覽器不會幫咱們再處理了,垂直方向滾動也只能靠本身),那是怎樣計算出來的尼?ui

在建立TouchAction對象時,若是配置參數中touchAction的值爲TOUCH_ACTION_COMPUTE,便調用compute方法開始遍歷recognizers,收集它們所但願設置的touch-action的值:

compute() {
        let actions = [];
        each(this.manager.recognizers, (recognizer) => {
          if (boolOrFn(recognizer.options.enable, [recognizer])) {
            actions = actions.concat(recognizer.getTouchAction());
          }
        });
        return cleanTouchActions(actions.join(' '));
      }

最終在cleanTouchActions方法集中計算最終的值:

...
     let hasPanX = inStr(actions, TOUCH_ACTION_PAN_X);
     let hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y);
     if (hasPanX && hasPanY) {
       return TOUCH_ACTION_NONE;
     }
     ...

3.配置手勢識別器
主要是配置各個手勢識別器之間的關係,是否能夠協同仍是互斥,用官網一個例子:

var hammer = new Hammer(el, {});
    
    var singleTap = new Hammer.Tap({ event: 'singletap' });
    var doubleTap = new Hammer.Tap({event: 'doubletap', taps: 2 });
    var tripleTap = new Hammer.Tap({event: 'tripletap', taps: 3 });
    
    hammer.add([doubleTap, doubleTap, singleTap]);
    
    tripleTap.recognizeWith([doubleTap, singleTap]);
    doubleTap.recognizeWith(singleTap);
    
    doubleTap.requireFailure(tripleTap);
    singleTap.requireFailure([tripleTap, doubleTap]);

以上定義了三個手勢識別器:singleTap,doubleTap和tripleTap,很明顯這個三個識別器是互斥的,若是用戶點三下屏幕時都觸發就比較尷尬了;
這裏得注意添加的順序,由於Hammer.js是會按順序遍歷識別器調用他們的recognize方法,由於咱們已經設置了手勢的互斥,Hammer.js爲了知道手勢是單擊仍是雙擊,singleTap,doubleTap,tripleTap識別器都設置了300ms等待時間來判斷以後還會不會有點擊事件,根據識別順序,singleTap總能獲取tripleTap和doubleTap的識別結果來判斷是否要觸發事件,假如咱們不設置他們之間的互斥關係,Hammer.js默認一知足條件就會觸發,就會出現剛纔說的那種尷尬的場景。
那recognizeWith有啥做用尼,看如下代碼:

if (!curRecognizer || (curRecognizer && curRecognizer.state & STATE_RECOGNIZED)) {
          curRecognizer = session.curRecognizer = null;
        }
    
        let i = 0;
        while (i < recognizers.length) {
          recognizer = recognizers[i];
          if (session.stopped !== FORCED_STOP && (
                  !curRecognizer || recognizer === curRecognizer || 
                  recognizer.canRecognizeWith(curRecognizer))) {
            recognizer.recognize(inputData);
          } else {
            recognizer.reset();
          }
          if (!curRecognizer && recognizer.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED)) {
            curRecognizer = session.curRecognizer = recognizer;
          }
          i++;
        }

雖然singleTap,doubleTap和tripleTap從最終結果上應該是互斥的,可是一樣的數據輸入時可能會同時讓幾個手勢識別器識別,例如當用戶點擊一下屏幕,singleTap識別器的狀態多是STATE_RECOGNIZED或者STATE_BEGAN(等待doubleTap和tripleTap識別器的結果),session會把singTap識別器記錄爲當前的手勢識別器,可是doubleTap和tripleTap也是須要記錄一些狀態(例如當前點擊次數),由於頗有可能接下來又是一個單擊,變成雙擊手勢;當用戶接着再單擊一下,doubleTap識別器由於設置了recognizeWith(singleTap)和以協同singleTap識別數據輸入,而後doubleTap識別器開始進入STATE_RECOGNIZED或者STATE_BEGAN(等待tripleTap識別器的結果),此時session當前的手勢識別器就是doubleTap了,而singleTap識別器由於沒有設置recognizeWith(doubleTap),會被重置。

一點小的細節

咱們在旋轉一張圖片時,如何實現旋轉,怎麼知道旋轉的角度尼?
再回到computeInputData方法,有這樣一行代碼獲取偏轉角度:

...
    let center = input.center = getCenter(pointers);
    ...
    input.angle = getAngle(offsetCenter, center);
    ...

再跟蹤一下getCenter方法:

while (i < pointersLength) {
        x += pointers[i].clientX;
        y += pointers[i].clientY;
        i++;
      }
    
     return {
        x: round(x / pointersLength),
        y: round(y / pointersLength)
      };

很簡單的算出手勢的中心位置,當咱們雙指旋轉時,中心位置也會跟着移動,很容易計算出先後偏轉角度。

最後一點思考

Hammer.js都是在冒泡階段綁定事件處理器,爲何不在捕獲階段攔截事件尼,若是一個向右活動的手勢被識別,後續的事件(如touchMove)已經不必再傳給子節點,徹底能夠在攔截的元素上處理,這樣性能上也應該會有一點提高,挖個坑給本身之後實現一下。最後的最後。。。由於沒有使用經驗,單靠啃源碼,不免有所錯漏,望指正。

相關文章
相關標籤/搜索