為網站增添觸控功能

觸控螢幕可用於越來越多裝置,從手機到電腦螢幕皆可使用。您的應用程式應該要以直覺、美觀的方式回應使用者輕觸回應。

Matt Gaunt

觸控螢幕可用於越來越多裝置,從手機到電腦螢幕皆可使用。當使用者選擇與 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;
}

實作自訂手勢

如果您有為網站自訂互動動作和手勢的想法,請注意以下兩個主題:

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

在本文中,我們會實際探討這些主題,說明利用 API 支援所有瀏覽器所需的 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 都會追蹤啟動後的手勢,且無論觸控在 DOM 元素上發生的位置為何,PointerEvents 都會追蹤事件。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 以移動 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-actionmanipulation,即可避免預設的雙擊行為。

這可讓你自行實作輕觸兩下手勢。

以下列出常用的 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 有助於鼓勵使用者與元素互動。
:focus
聚焦狀態的按鈕
使用者在網頁上按下 Tab 鍵時觸發。焦點狀態可讓使用者瞭解目前正在與哪個元素互動,也能讓使用者輕鬆使用鍵盤瀏覽 UI。
:active
按下狀態的按鈕
在選取元素時進入,例如使用者點選或輕觸元素時。

如需觸控事件的完整參考資料,請參閱 W3C 觸控事件

觸控、滑鼠和指標事件

這些事件是將新手勢新增至應用程式的構件:

觸控、滑鼠、指標事件
touchstartmousedownpointerdown 當手指首次觸碰元素,或使用者按下滑鼠時,系統會呼叫此方法。
touchmovemousemovepointermove 當使用者手指在螢幕上移動,或是隨著滑鼠拖曳時,系統會呼叫此方法。
touchendmouseuppointerup 當使用者將手指從螢幕上移開或放開滑鼠時,系統會呼叫此方法。
touchcancel pointercancel 當瀏覽器取消觸控手勢時,系統會呼叫此方法。舉例來說,使用者輕觸網頁應用程式,然後變更分頁。

觸控清單

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

觸控事件屬性
touches 畫面上目前的所有觸控動作清單 (無論如何輕觸元素)。
targetTouches 在做為目前事件目標的元素上開始的觸控清單。舉例來說,如果您繫結至 <button>,您只會收到目前在該按鈕上的觸控動作。如果已繫結至文件,就能掌握文件當前的所有細節。
changedTouches 觸控動作變更清單,導致事件觸發:
  • 針對 touchstart 事件:列出目前事件剛剛啟用的接觸點。
  • 針對 touchmove 事件:自上次事件以來移動的觸控點清單。
  • 針對 touchend touchcancel 事件:列出剛從表面移除的觸控點。

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