iPhone的出現讓手勢操作大為流行,也使得手勢編程成為開發人員的挑戰。 擬物設計也把手勢編程納入在內,大概也想制定一個在交互模型標准。現階段因為MD還在預發布階段,因此還只實現了單點手勢(一個指頭),可是已經有足夠的東西值得學習,無論對我們應用還是自己設計手勢編程都是大有裨益。
Angular Material有兩個手划控件
mdSwipeLeft
和mdSwipeRight
,然而真正的代碼支持卻不在這兩個控件的定義中,而是在核心代碼中,文件位置src\core\services\gesture\gesture.js
這里也就是我們深入研究手勢實現的地方。
基本屏幕事件
做過界面的人都熟悉mousedown, mouseup, mousemove
等事件,很多后台函數多與這些事件綁定,從而能夠與用戶交互。但是這些事件都有些單薄而僵硬,手勢事件卻更友好和人性化,這也是其大受歡迎的根本原因。
手勢事件不是空中樓閣,它們本身是需要這些基本事件的支持,這些基本屏幕事件也就成為了手勢模型的一個組成部分,成為最底的一層。
這些事件首先被划分為三類,說是三類,理解成三個事件更為恰當,它們與手指與屏幕的交互一一對應:開始事件就是手指按下屏幕;移動事件就是手指在屏幕移動;結束事件就是手指離開屏幕。非常簡單而直觀。
從下面MD對這三類事件的定義,我們也可以看到每類事件中的變體大都與設備的不同有關而不是真正的不同事件,如鼠標的按下,和手指的按下。這也是我上面說的把它們理解為三個事件更為恰當。
- START_EVENTS =>'mousedown touchstart pointerdown';
- MOVE_EVENTS => 'mousemove touchmove pointermove';
- END_EVENTS => 'mouseup mouseleave touchend touchcancel pointerup pointercancel';
手勢歸納
基本事件都是瞬間事件,不存在延時和邏輯判斷,按下就是按下,松開就是松開;這也是稱之為基本事件的原因。
而手勢卻恰恰相反,
- 手勢是綜合事件,如滑動手勢,直觀的感覺就是手指按下快速向左(右)滑動,並同時松開手指,這整個過程完成才是一個滑動手勢。
- 手勢還有邏輯判斷,還是滑動手勢,不僅僅要在以上的全過程之后才激發,手指的還要超過一定的速度才能算是滑動手勢。
因此,可以把手勢看作在基本事件之上的一個封裝,在MD的實現也是用GestureHandler的函數還偵聽基本事件然后作出綜合處理。
偵聽
這里是MD綁定基本事件的代碼:
angular.element(document)
.on(START_EVENTS, gestureStart)
.on(MOVE_EVENTS, gestureMove)
.on(END_EVENTS, gestureEnd)
MD移動事件的偵聽處理函數:
function gestureMove(ev) {
if (!pointer || !typesMatch(ev, pointer)) return;
updatePointerState(ev, pointer);
runHandlers('move', ev);
}
其它兩個(開始和結束事件)都與此類似,只不過有更多的處理過程。這個因為簡單,可以用來好好分析關鍵過程。我們可以看到,這個偵聽函數的關鍵一步就是調用處理器(runHandler
)。這個函數內部並不復雜,只是簡單的遍歷預存處理器,然后調用該處理器定義的對應的基本事件處理器。這個處理器就是手勢處理器,它會分析歸納基本事件當條件滿足時觸發手勢事件。
手勢處理器$$MdGestureHandler
MD用工廠(factory
)的方式定義了手勢處理器的模板(或者可以理解為基類幫助理解),這個factory名稱就是$$MdGestureHandler,為了便於理解,我們把它分解成三部分來看。
基本屏幕事件處理
第一部分:4個方法,分別與三類基本屏幕事件對應(cancel是輔助方法),也是用來分別處理三類屏幕事件的,上面的runHandler
就是調用的源頭。
start: function(ev, pointer) {
if (this.state.isRunning) return;
var parentTarget = this.getNearestParent(ev.target);
var parentTargetOptions = parentTarget && parentTarget.$mdGesture[this.name] || {};
this.state = {
isRunning: true,
options: angular.extend({}, this.options, parentTargetOptions),
registeredParent: parentTarget
};
this.onStart(ev, pointer);
},
move: function(ev, pointer) {
if (!this.state.isRunning) return;
this.onMove(ev, pointer);
},
end: function(ev, pointer) {
if (!this.state.isRunning) return;
this.onEnd(ev, pointer);
this.state.isRunning = false;
},
cancel: function(ev, pointer) {
this.onCancel(ev, pointer);
this.state = {};
},
優化的屏幕事件
第二部分:4個內部事件,也是基本與以上4個方法對應,並在4個方法中適當的時機觸發,可以看作是對原始基本事件的梳理之后的重新拋出。 你如果創建自己的手勢處理器,要做的也就是重載這4個事件。從以下代碼我們也可以看到,MD為每一個事件給出了空實現(`angular.noop'),目的就是為了讓自定義處理器自己重載實現。
onStart: angular.noop,
onMove: angular.noop,
onEnd: angular.noop,
onCancel: angular.noop,
手勢的觸發
第三部分:也是最后最關鍵的一個方法,手勢事件的觸發dispatchEvent
。自定義的手勢處理器最終都是要調用這個方法來觸發手勢事件。大部分觸發時機都在onEnd
中,當是不是必須的,要根據你具體的手勢的含義來定。
dispatchEvent的實現:
dispatchEvent: dispatchEvent,
...
/*
* NOTE: dispatchEvent is very performance sensitive.
*/
function dispatchEvent(srcEvent, eventType, eventPointer, /*original DOMEvent */ev) {
eventPointer = eventPointer || pointer;
var eventObj;
if (eventType === 'click') {
eventObj = document.createEvent('MouseEvents');
eventObj.initMouseEvent(
'click', true, true, window, ev.detail,
ev.screenX, ev.screenY, ev.clientX, ev.clientY,
ev.ctrlKey, ev.altKey, ev.shiftKey, ev.metaKey,
ev.button, ev.relatedTarget || null
);
} else {
eventObj = document.createEvent('CustomEvent');
eventObj.initCustomEvent(eventType, true, true, {});
}
eventObj.$material = true;
eventObj.pointer = eventPointer;
eventObj.srcEvent = srcEvent;
eventPointer.target.dispatchEvent(eventObj);
}
手勢實例解析
手勢內部實現過程雖然較為復雜,以上的流程解析也是為了更好的理解從而有個直觀的感覺。到了每一個手勢的實現時,真正用到的卻不算多,主要就是那4個優化的事件onStart, onMove, onEnd, onCancel
和一個觸發的方法'dispatchEvent`。我們來看看一些手勢實例,親身感受一下,良好建模以后的手勢實現。
滑動手勢 - Swipe
屏幕事件 | 觸發條件 | 觸發事件 |
---|---|---|
[無] | ||
按下 | ||
移動 | ||
移動 | ||
移動 | ||
移動 | ||
松開 | 超過最低速度和位移 | $md.swiperight |
[無] |
拖動手勢 - Drag
屏幕事件 | 觸發條件 | 觸發事件 |
---|---|---|
[無] | ||
按下 | ||
移動 | ||
移動 | 當前觸點與起點位移超過閥值 | $md.dragstart |
移動 | $md.drag | |
移動 | $md.drag | |
松開 | $md.dragend | |
[無] |