為網站增添觸控功能

從手機到電腦螢幕,市面上有更多裝置支援觸控螢幕。您的應用程式應該以直覺、美觀的方式回應觸控動作。

Matt Gaunt

有越來越多的裝置支援觸控螢幕,包括手機和電腦螢幕。使用者選擇與 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;
}

實作自訂手勢

如果您有網站的自訂互動和手勢概念,請記住以下兩個主題:

  1. 如何支援所有瀏覽器。
  2. 如何維持高影格速率。

本文將逐一介紹我們為支援所有瀏覽器所需的 API 功能,並涵蓋這些主題,並探討我們如何有效運用這些事件。

視您希望手勢執行的動作而定,您可能會希望使用者一次與一個元素互動,「或是」您會希望使用者可以同時與多個元素互動。

本文將探討兩個範例,示範可支援所有瀏覽器,以及如何保持高畫面更新率。

文件上輕觸的 GIF 範例

第一個範例可讓使用者與一個元素互動。在此情況下,建議您將所有觸控事件都提供給該元素,但前提是手勢一開始是在元素上啟動。舉例來說,將手指從滑動式元素上移出仍可控制元素。

這種做法為使用者提供相當大的彈性,但會強制執行限制,讓使用者瞭解如何與 UI 互動。

輕觸元素上的 GIF 範例

但是,如果您預期使用者會同時與多個元素互動 (使用多點觸控),則應將觸控限制在特定元素上。

這對使用者來說更具彈性,但複雜,使操控 UI 變得更加複雜,並較不容易受到使用者錯誤影響。

新增事件接聽程式

在 Chrome (55 以上版本) 中,建議透過 Internet Explorer 和 Edge 使用 PointerEvents 實作自訂手勢。

使用其他瀏覽器 TouchEventsMouseEvents 是正確的方法。

PointerEvents 的一大優點是,它會將多種輸入類型 (包括滑鼠、觸控和畫筆事件) 合併成一組回呼。要監聽的事件為 pointerdownpointermovepointeruppointercancel

其他瀏覽器的對等點為 touchstarttouchmovetouchendtouchcancel 適用於觸控事件。如果您要對滑鼠輸入實作相同的手勢,就必須實作 mousedownmousemovemouseup

如果您對該使用的事件有任何疑問,請參閱「輕觸、滑鼠和指標事件」表格。

如要使用這些事件,必須對 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 後發生的位置為何,系統都會追蹤事件。

針對滑鼠移動和結束事件,我們會在手勢啟動方法「中」加入事件監聽器,並將事件監聽器新增至文件,讓事件監聽器可以一直追蹤遊標,直到手勢完成為止。

實作此功能的步驟如下:

  1. 新增所有 TouchEvent 和 PointerEvent 事件監聽器。針對 MouseEvents 「只」新增啟動事件。
  2. 在啟動手勢回呼中,將滑鼠移動和結束事件繫結至文件。這樣一來,無論原始元素上是否有事件發生,系統都會接收所有滑鼠事件。針對 PointerEvents,我們需要在原始元素上呼叫 setPointerCapture(),才能接收所有後續事件。然後處理手勢的開頭。
  3. 處理移動事件。
  4. 在結束事件中,從文件中移除滑鼠移動和結束事件監聽器,然後結束手勢。

以下是 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);

試用

按照這個新增移動事件的模式進行操作,如果使用者開始與元素互動,並將手勢移出元素,系統就會繼續在文件何處移動滑鼠動作,因為事件會從文件接收到事件。

這張圖表展示了在手勢開始後,在文件中新增移動和結束事件時,觸控事件的作用。

插圖:將繫結觸控事件以記錄在「Touchstart」中

有效回應觸控

現在,我們已處理開始和結束事件,現在可以實際回應觸控事件了。

您可以針對任何開始和移動事件,輕鬆從事件中擷取 xy

以下範例會檢查 targetTouches 是否存在,檢查事件是否來自 TouchEvent。如果包含,系統就會從第一次輕觸時擷取 clientXclientY。如果事件為 PointerEventMouseEvent,則會直接從事件本身擷取 clientXclientY

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(),可以按這裡瞭解詳情

一般實作方式是儲存起始事件和移動事件的 xy 座標,並在移動事件回呼中要求動畫影格。

在我們的示範中,我們會將初始觸控位置儲存在 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+ 支援輕觸兩下縮放手勢。設定 manipulationtouch-action 可防止預設的輕觸兩下行為。

方便你自行實作輕觸兩下手勢。

以下是常用的 touch-action 值清單:

觸控動作參數
touch-action: none 瀏覽器不會處理任何觸控互動。
touch-action: pinch-zoom 停用所有瀏覽器互動 (例如「Touch-action: none」),以及「Pinch-zoom」,且瀏覽器仍由瀏覽器處理。
touch-action: pan-y pinch-zoom 在 JavaScript 中處理水平捲動,不需要停用垂直捲動或雙指撥動縮放功能 (例如圖片輪轉介面)。
touch-action: manipulation 停用輕觸兩下手勢,避免瀏覽器發生任何點擊延遲。讓使用者捲動並向上撥動瀏覽器畫面。

支援舊版 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 更新內容

參考資料

觸控狀態的虛擬類別

類別 範例 說明
:hover
已按下狀態的按鈕
將遊標放在元素上方時輸入。懸停在使用者介面上的變更有助於鼓勵使用者與元素互動。
:對焦
顯示焦點狀態的按鈕
在使用者分頁頁面上元素時輸入。焦點狀態可讓使用者瞭解自己目前正在與哪些元素互動,也能讓使用者使用鍵盤輕鬆瀏覽 UI。
:啟用
已按下狀態的按鈕
會在使用者點選或輕觸元素時輸入。

如需最終觸控事件的參考資料,請參閱:W3C 觸控事件

觸控、滑鼠和指標事件

下列事件是在應用程式中新增手勢的基石:

觸控、滑鼠、指標事件
touchstartmousedownpointerdown 手指首次輕觸元素或點擊滑鼠時,系統就會呼叫此方法。
touchmovemousemovepointermove 當使用者在螢幕上移動手指或用滑鼠拖曳時,系統就會呼叫此方法。
touchendmouseuppointerup 系統會在使用者將手指離開螢幕或放開滑鼠時呼叫此方法。
touchcancel pointercancel 當瀏覽器取消觸控手勢時,系統就會呼叫此方法。例如,使用者輕觸某個網頁應用程式,然後變更分頁。

觸控清單

每個觸控事件都包含三個清單屬性:

觸控事件屬性
touches 目前輕觸螢幕的所有觸控動作清單 (無論輕觸的元素為何)。
targetTouches 在目前事件的目標元素上啟動的觸控清單。例如,如果繫結至 <button>,則只會獲得該按鈕目前上的觸控動作。如果繫結至文件,您會取得文件中目前的所有更新內容。
changedTouches 導致事件觸發的觸控清單:
  • 針對 touchstart 事件,列出剛透過目前事件啟用的接觸點。
  • 針對 touchmove 事件,列出自上次事件以來已移動的接觸點。
  • 如果是 touchend touchcancel 事件,則會列出剛從途徑中移除的接觸點。

在 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);
    }
  }
};