從手機到電腦螢幕,市面上有更多裝置支援觸控螢幕。您的應用程式應該以直覺、美觀的方式回應觸控動作。
有越來越多的裝置支援觸控螢幕,包括手機和電腦螢幕。使用者選擇與 UI 互動時,應用程式應以直覺化的方式回應觸控動作。
回應元素狀態
是否曾在網頁上輕觸或點選某個元素,卻不確定網站是否實際偵測到該元素?
只要在使用者輕觸或與使用者介面的特定部分互動時修改元素顏色,就能確保網站運作符合需求。這不僅能解決使用上的不便,還能提供快速流暢的體驗。
DOM 元素可沿用下列任一狀態:預設、聚焦、懸停和使用中。如要變更這些狀態的 UI,必須將樣式套用至下列虛擬類別 :hover
、:focus
和 :active
,如下所示:
.btn {
background-color: #4285f4;
}
.btn:hover {
background-color: #296cdb;
}
.btn:focus {
background-color: #0f52c1;
/* The outline parameter suppresses the border
color / outline when focused */
outline: 0;
}
.btn:active {
background-color: #0039a8;
}
在多數行動瀏覽器中,「懸停」hover和/或「聚焦」hover狀態可套用至使用者輕觸的元素。
請仔細考慮您設定的樣式,以及使用者輕觸後會呈現的效果。
不使用預設瀏覽器樣式
為不同狀態新增樣式後,就會發現大多數瀏覽器會根據使用者的觸控實作自己的樣式。這主要是因為行動裝置剛推出時,許多網站沒有 :active
狀態的樣式。因此,許多瀏覽器加入了其他醒目顯示顏色或樣式,以向使用者提供意見回饋。
多數瀏覽器會在聚焦元素時,使用 outline
CSS 屬性在元素周圍顯示環。你可以透過下列方式隱藏留言:
.btn:focus {
outline: 0;
/* Add replacement focus styling here (i.e. border) */
}
Safari 和 Chrome 會加入可使用 -webkit-tap-highlight-color
CSS 屬性防止的輕觸醒目顯示顏色:
/* Webkit / Chrome Specific CSS to remove tap
highlight color */
.btn {
-webkit-tap-highlight-color: transparent;
}
Windows Phone 上的 Internet Explorer 的操作方式類似,但會透過中繼標記隱藏:
<meta name="msapplication-tap-highlight" content="no">
Firefox 提供兩種副作用。
-moz-focus-inner
虛擬類別會在可觸控元素上新增外框,您可以設定 border: 0
予以移除。
如果您在 Firefox 中使用 <button>
元素,系統會套用漸層,而您可以設定 background-image: none
來移除漸層。
/* Firefox Specific CSS to remove button
differences and focus ring */
.btn {
background-image: none;
}
.btn::-moz-focus-inner {
border: 0;
}
停用使用者選取功能
建立 UI 時,在某些情況下,您可能想讓使用者與元素互動,但您希望略過預設行為,也就是長按文字或在 UI 上拖曳選取文字。
方法是使用 user-select
CSS 屬性,但請注意,在使用者想選取元素中的文字時,這種做法可能會extremely影響。因此請務必謹慎使用。
/* Example: Disable selecting text on a paragraph element: */
p.disable-text-selection {
user-select: none;
}
實作自訂手勢
如果您有網站的自訂互動和手勢概念,請記住以下兩個主題:
- 如何支援所有瀏覽器。
- 如何維持高影格速率。
本文將逐一介紹我們為支援所有瀏覽器所需的 API 功能,並涵蓋這些主題,並探討我們如何有效運用這些事件。
視您希望手勢執行的動作而定,您可能會希望使用者一次與一個元素互動,「或是」您會希望使用者可以同時與多個元素互動。
本文將探討兩個範例,示範可支援所有瀏覽器,以及如何保持高畫面更新率。
第一個範例可讓使用者與一個元素互動。在此情況下,建議您將所有觸控事件都提供給該元素,但前提是手勢一開始是在元素上啟動。舉例來說,將手指從滑動式元素上移出仍可控制元素。
這種做法為使用者提供相當大的彈性,但會強制執行限制,讓使用者瞭解如何與 UI 互動。
但是,如果您預期使用者會同時與多個元素互動 (使用多點觸控),則應將觸控限制在特定元素上。
這對使用者來說更具彈性,但複雜,使操控 UI 變得更加複雜,並較不容易受到使用者錯誤影響。
新增事件接聽程式
在 Chrome (55 以上版本) 中,建議透過 Internet Explorer 和 Edge 使用 PointerEvents
實作自訂手勢。
使用其他瀏覽器 TouchEvents
和 MouseEvents
是正確的方法。
PointerEvents
的一大優點是,它會將多種輸入類型 (包括滑鼠、觸控和畫筆事件) 合併成一組回呼。要監聽的事件為 pointerdown
、pointermove
、pointerup
和 pointercancel
。
其他瀏覽器的對等點為 touchstart
、touchmove
、touchend
和 touchcancel
適用於觸控事件。如果您要對滑鼠輸入實作相同的手勢,就必須實作 mousedown
、mousemove
和 mouseup
。
如果您對該使用的事件有任何疑問,請參閱「輕觸、滑鼠和指標事件」表格。
如要使用這些事件,必須對 DOM 元素呼叫 addEventListener()
方法,並提供事件名稱、回呼函式和布林值。布林值決定您是否要在其他元素有機會擷取及解讀事件之前或之後擷取事件。(true
代表您希望事件優先於其他元素)。
以下示範如何監聽互動開始。
// Check if pointer events are supported.
if (window.PointerEvent) {
// Add Pointer Event Listener
swipeFrontElement.addEventListener('pointerdown', this.handleGestureStart, true);
swipeFrontElement.addEventListener('pointermove', this.handleGestureMove, true);
swipeFrontElement.addEventListener('pointerup', this.handleGestureEnd, true);
swipeFrontElement.addEventListener('pointercancel', this.handleGestureEnd, true);
} else {
// Add Touch Listener
swipeFrontElement.addEventListener('touchstart', this.handleGestureStart, true);
swipeFrontElement.addEventListener('touchmove', this.handleGestureMove, true);
swipeFrontElement.addEventListener('touchend', this.handleGestureEnd, true);
swipeFrontElement.addEventListener('touchcancel', this.handleGestureEnd, true);
// Add Mouse Listener
swipeFrontElement.addEventListener('mousedown', this.handleGestureStart, true);
}
處理單一元素互動
在上方的簡短程式碼片段中,我們只為滑鼠事件新增起始事件監聽器。這是因為只有在滑鼠遊標懸停到要新增事件監聽器的元素時,才會觸發滑鼠事件。
啟動後,TouchEvents
將追蹤啟動後的手勢,無論觸控動作發生的位置為何,PointerEvents
都會追蹤事件,不論觸控動作是在 DOM 元素上呼叫 setPointerCapture
後發生的位置為何,系統都會追蹤事件。
針對滑鼠移動和結束事件,我們會在手勢啟動方法「中」加入事件監聽器,並將事件監聽器新增至文件,讓事件監聽器可以一直追蹤遊標,直到手勢完成為止。
實作此功能的步驟如下:
- 新增所有 TouchEvent 和 PointerEvent 事件監聽器。針對 MouseEvents 「只」新增啟動事件。
- 在啟動手勢回呼中,將滑鼠移動和結束事件繫結至文件。這樣一來,無論原始元素上是否有事件發生,系統都會接收所有滑鼠事件。針對 PointerEvents,我們需要在原始元素上呼叫
setPointerCapture()
,才能接收所有後續事件。然後處理手勢的開頭。 - 處理移動事件。
- 在結束事件中,從文件中移除滑鼠移動和結束事件監聽器,然後結束手勢。
以下是 handleGestureStart()
方法的程式碼片段,可將移動和結束事件新增至文件:
// Handle the start of gestures
this.handleGestureStart = function(evt) {
evt.preventDefault();
if(evt.touches && evt.touches.length > 1) {
return;
}
// Add the move and end listeners
if (window.PointerEvent) {
evt.target.setPointerCapture(evt.pointerId);
} else {
// Add Mouse Listeners
document.addEventListener('mousemove', this.handleGestureMove, true);
document.addEventListener('mouseup', this.handleGestureEnd, true);
}
initialTouchPos = getGesturePointFromEvent(evt);
swipeFrontElement.style.transition = 'initial';
}.bind(this);
我們新增的結束回呼為 handleGestureEnd()
,會從文件中移除移動和結束事件監聽器,並在手勢完成時釋出指標擷取內容,如下所示:
// Handle end gestures
this.handleGestureEnd = function(evt) {
evt.preventDefault();
if (evt.touches && evt.touches.length > 0) {
return;
}
rafPending = false;
// Remove Event Listeners
if (window.PointerEvent) {
evt.target.releasePointerCapture(evt.pointerId);
} else {
// Remove Mouse Listeners
document.removeEventListener('mousemove', this.handleGestureMove, true);
document.removeEventListener('mouseup', this.handleGestureEnd, true);
}
updateSwipeRestPosition();
initialTouchPos = null;
}.bind(this);
按照這個新增移動事件的模式進行操作,如果使用者開始與元素互動,並將手勢移出元素,系統就會繼續在文件何處移動滑鼠動作,因為事件會從文件接收到事件。
這張圖表展示了在手勢開始後,在文件中新增移動和結束事件時,觸控事件的作用。
有效回應觸控
現在,我們已處理開始和結束事件,現在可以實際回應觸控事件了。
您可以針對任何開始和移動事件,輕鬆從事件中擷取 x
和 y
。
以下範例會檢查 targetTouches
是否存在,檢查事件是否來自 TouchEvent
。如果包含,系統就會從第一次輕觸時擷取 clientX
和 clientY
。如果事件為 PointerEvent
或 MouseEvent
,則會直接從事件本身擷取 clientX
和 clientY
。
function getGesturePointFromEvent(evt) {
var point = {};
if (evt.targetTouches) {
// Prefer Touch Events
point.x = evt.targetTouches[0].clientX;
point.y = evt.targetTouches[0].clientY;
} else {
// Either Mouse event or Pointer Event
point.x = evt.clientX;
point.y = evt.clientY;
}
return point;
}
TouchEvent
有三份清單,內含觸控資料:
touches
:螢幕上目前的所有觸控清單,無論其所在 DOM 元素為何。targetTouches
:目前與事件繫結的 DOM 元素上的觸控清單。changedTouches
:變更導致事件觸發的觸控清單。
在大多數情況下,targetTouches
能滿足你的所有需求。(如要進一步瞭解這些清單,請參閱「觸控清單」一文)。
使用 requestAnimationFrame
由於事件回呼會在主要執行緒上觸發,因此我們希望能在事件的回呼中盡可能執行少量程式碼,藉此維持高影格速率並防止卡頓。
使用 requestAnimationFrame()
時,我們有機會在瀏覽器想要繪製影格之前更新 UI,並協助我們將一些工作交由事件回呼處理。
如果您不熟悉 requestAnimationFrame()
,可以按這裡瞭解詳情。
一般實作方式是儲存起始事件和移動事件的 x
和 y
座標,並在移動事件回呼中要求動畫影格。
在我們的示範中,我們會將初始觸控位置儲存在 handleGestureStart()
中 (尋找 initialTouchPos
):
// Handle the start of gestures
this.handleGestureStart = function(evt) {
evt.preventDefault();
if (evt.touches && evt.touches.length > 1) {
return;
}
// Add the move and end listeners
if (window.PointerEvent) {
evt.target.setPointerCapture(evt.pointerId);
} else {
// Add Mouse Listeners
document.addEventListener('mousemove', this.handleGestureMove, true);
document.addEventListener('mouseup', this.handleGestureEnd, true);
}
initialTouchPos = getGesturePointFromEvent(evt);
swipeFrontElement.style.transition = 'initial';
}.bind(this);
handleGestureMove()
方法會視需要儲存事件的位置,然後再要求動畫影格,並傳入 onAnimFrame()
函式做為回呼:
this.handleGestureMove = function (evt) {
evt.preventDefault();
if (!initialTouchPos) {
return;
}
lastTouchPos = getGesturePointFromEvent(evt);
if (rafPending) {
return;
}
rafPending = true;
window.requestAnimFrame(onAnimFrame);
}.bind(this);
onAnimFrame
值是一種函式,在呼叫時會變更 UI 來將其移動。只要將這個函式傳遞至 requestAnimationFrame()
,即可指示瀏覽器在即將更新頁面前呼叫這個函式 (例如對頁面繪製任何變更)。
在 handleGestureMove()
回呼中,我們一開始會檢查 rafPending
是否為 false,這代表自上次移動事件以來,requestAnimationFrame()
是否已呼叫 onAnimFrame()
。這表示我們只有一個 requestAnimationFrame()
,正在等待執行。
執行 onAnimFrame()
回呼時,我們會針對想要移動的任何元素設定轉換,然後再將 rafPending
更新為 false
,以便下一個觸控事件要求新的動畫影格。
function onAnimFrame() {
if (!rafPending) {
return;
}
var differenceInX = initialTouchPos.x - lastTouchPos.x;
var newXTransform = (currentXPosition - differenceInX)+'px';
var transformStyle = 'translateX('+newXTransform+')';
swipeFrontElement.style.webkitTransform = transformStyle;
swipeFrontElement.style.MozTransform = transformStyle;
swipeFrontElement.style.msTransform = transformStyle;
swipeFrontElement.style.transform = transformStyle;
rafPending = false;
}
使用觸控動作控製手勢
CSS 屬性 touch-action
可讓您控制元素的預設觸控行為。在範例中,我們使用 touch-action: none
避免瀏覽器在使用者輕觸時執行任何操作,藉此攔截所有觸控事件。
/* Pass all touches to javascript: */
button.custom-touch-logic {
touch-action: none;
}
使用 touch-action: none
並不容易,因為它會阻止所有預設瀏覽器行為。在許多情況下,選擇以下其中一個選項是更好的解決方案。
touch-action
可讓您停用瀏覽器實作的手勢。例如,IE10+ 支援輕觸兩下縮放手勢。設定 manipulation
的 touch-action
可防止預設的輕觸兩下行為。
方便你自行實作輕觸兩下手勢。
以下是常用的 touch-action
值清單:
支援舊版 IE
如果想支援 IE10,就必須處理供應商前置字元的 PointerEvents
版本。
如要檢查 PointerEvents
是否支援,您通常會尋找 window.PointerEvent
,但在 IE10 中請找出 window.navigator.msPointerEnabled
。
包含供應商前置字元的事件名稱為:'MSPointerDown'
、'MSPointerUp'
和 'MSPointerMove'
。
以下範例說明如何檢查支援情況,並切換事件名稱。
var pointerDownName = 'pointerdown';
var pointerUpName = 'pointerup';
var pointerMoveName = 'pointermove';
if (window.navigator.msPointerEnabled) {
pointerDownName = 'MSPointerDown';
pointerUpName = 'MSPointerUp';
pointerMoveName = 'MSPointerMove';
}
// Simple way to check if some form of pointerevents is enabled or not
window.PointerEventsSupport = false;
if (window.PointerEvent || window.navigator.msPointerEnabled) {
window.PointerEventsSupport = true;
}
詳情請參閱這篇 Microsoft 更新內容。
參考資料
觸控狀態的虛擬類別
如需最終觸控事件的參考資料,請參閱:W3C 觸控事件。
觸控、滑鼠和指標事件
下列事件是在應用程式中新增手勢的基石:
觸控清單
每個觸控事件都包含三個清單屬性:
在 iOS 上啟用主動狀態支援
不幸的是,iOS 版 Safari 預設不會套用有效狀態,因此您必須將 touchstart
事件監聽器新增至文件內文或每個元素。
這個方法必須在使用者代理程式測試中完成,因此只能在 iOS 裝置上執行。
在內文中加入觸控開始的做法,可套用至 DOM 中的所有元素,但這可能會在捲動頁面時造成效能問題。
window.onload = function() {
if (/iP(hone|ad)/.test(window.navigator.userAgent)) {
document.body.addEventListener('touchstart', function() {}, false);
}
};
替代方法是將觸控啟動事件監聽器新增至網頁上的所有可互動元素,從而減輕部分效能問題。
window.onload = function() {
if (/iP(hone|ad)/.test(window.navigator.userAgent)) {
var elements = document.querySelectorAll('button');
var emptyFunction = function() {};
for (var i = 0; i < elements.length; i++) {
elements[i].addEventListener('touchstart', emptyFunction, false);
}
}
};