Material Design是Google最新發布的跨平台統一視覺設計語言。直接翻譯是物質設計,但是我更傾向於使用"擬物設計"更為准確。
據谷歌介紹,Material Design基於“真實的觸感,靈感源自對紙和墨水的研究,” 能夠讓用戶 “理解那些用於替代真實世界的可視線索,”“而又不違背力學原理。”另外,光線、表面和移動的基本原理是表現對象如何移動、交互和相互關聯地存在於空間中的關鍵。逼真的光影效果可以顯示區塊間的接縫、划分空間、以及標識移動的部件。
Material Design在動畫、風格和布局方面提出了一系列的原則,並且為大量的視覺組件,包括按鈕、卡片、網格和對話框等,以及相關的動作和手勢提供了建議,另外還包含了一些與可訪問性有關的指引。
原理
擬物就隱喻
擬物是對空間和動作的一致整體的模擬。擬物系統的設計基於觸摸感,得力於紙墨原理,借力高科技, 最終為我們打開了一道想象之門。
系統正交分解
- 組件 - UI Component (Directives, Services, ARIA)
- 布局 - Layout CSS (FlexBox, Attribute, Child Aligment)
- 風格 - Theme (Color Palettes)
擬物的世界
3D世界
擬物的世界是3D世界,每個物體都有X, Y, Z三個方向的坐標。其中,Z是垂直於屏幕的軸,每一層在Z方向上都有標准的1dp厚度。
光和影
擬物的世界還引入了虛擬光源,而實際上我們是看不到這個光源的,我們看到是這個光源在物體上留下的影子。
擬物世界僅僅引入了兩種光源,所有的物體的影子都是由這兩種光源照射的結果。
主光
主光源在物體上留下的是單方向的影子。
散光
散光源在物體上留下的是多方向均勻而一致的模糊影子。
兩種光源同時照射
物質的特性
物質一些內在的特性和行為,理解這些特性可以幫助我們更好理解擬物設計。
物理特性
- 我們的物質可以有不同的長度和寬度(X軸和Y軸的度量),但是厚度是統一的(1dp),而且厚度永遠不為0。
- 物質的陰影總是來源於它的相對(其它元素)高度 (Z軸的度量)
- 顯示在物質上的內容不受物質本身的限制,可以是任何形狀,任何顏色。
- 內容的行為與物質的行為是解耦的,但是物質的邊界仍然能夠限制內容的顯示。
- 物質是實體,任何操作時間不能透過最上層物質應用到被擋住的下一層去。
- 多個物質元素不能在空間中占據同一個點,他們是互斥的。
- 一個物質元素不能穿過其他物質元素。
布局
Angular Material的 響應式CSS布局是基於flexbox實現的。整個布局體系是用元素的屬性來標示,而不用CSS類。這也是正交設計的一個體現:屬性來定義布局,類定義風格。
例子:使用layout
屬性來定義內部元素的布局,橫向排列(layout="row"
)或者豎向排列 (layout="column"
)
<div layout="row">
<div>I'm left.</div>
<div>I'm right.</div>
</div>
<div layout="column">
<div>I'm above.</div>
<div>I'm below.</div>
</div>
手勢
iPhone的出現讓手勢操作大為流行,也使得手勢編程成為開發人員的挑戰。 擬物設計也把手勢編程納入在內,大概也想制定一個在交互模型標准。現階段因為MD還在預發布階段,因此還只實現了單點手勢(一個指頭),可是已經有足夠的東西值得學習,無論對我們應用還是自己設計手勢編程都是大有裨益。
MD有兩個手划控件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 | |
[無] |
[AngularJS的實現] (https://material.angularjs.org )