觸控螢幕可用於越來越多裝置,從手機到電腦螢幕皆可使用。您的應用程式應該要以直覺、美觀的方式回應使用者輕觸回應。
觸控螢幕可用於越來越多裝置,從手機到電腦螢幕皆可使用。當使用者選擇與 UI 互動時,應用程式應以直覺的方式回應觸控動作。
回應元素狀態
您是否曾經在網頁上觸碰或點選元素,但懷疑網站是否實際偵測到該元素?
只要在使用者輕觸或與 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;
}
在大多數行動瀏覽器中,元素在遭到點選後,系統會套用懸停和/或聚焦狀態。
請仔細考量您設定的樣式,以及使用者在觸控完成後看到的樣式。
隱藏預設瀏覽器樣式
為不同狀態新增樣式後,您會發現大多數瀏覽器會根據使用者的觸控動作實作各自的樣式。這主要是因為行動裝置剛推出時,許多網站都沒有 :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 屬性執行這項操作,但請注意,如果使用者想要選取元素中的文字,對內容執行這項操作可能會讓他們非常生氣。因此請謹慎使用。
/* Example: Disable selecting text on a paragraph element: */
p.disable-text-selection {
user-select: none;
}
實作自訂手勢
如果您有為網站自訂互動動作和手勢的想法,請注意以下兩個主題:
- 如何支援所有瀏覽器。
- 如何維持高影格速率。
在本文中,我們會實際探討這些主題,說明利用 API 支援所有瀏覽器所需的 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
都會追蹤啟動後的手勢,且無論觸控在 DOM 元素上發生的位置為何,PointerEvents
都會追蹤事件。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 以移動 UI。將這個函式傳遞至 requestAnimationFrame()
後,我們會指示瀏覽器在即將更新頁面 (即對頁面進行任何變更) 前呼叫該函式。
在 handleGestureMove()
回呼中,我們一開始會檢查 rafPending
是否為否。這會指出自上次移動事件以來,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 以上版本支援雙擊縮放手勢。設定 touch-action
為 manipulation
,即可避免預設的雙擊行為。
這可讓你自行實作輕觸兩下手勢。
以下列出常用的 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 預設不會套用「active」狀態,因此您必須在「document body」或各個元素中新增 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);
}
}
};