サイトにタッチを追加する

スマートフォンからデスクトップ画面に至るまで、タッチスクリーンはますます多くのデバイスで利用できます。アプリはタップ操作に直感的かつ美しく反応する必要があります。

スマートフォンからパソコンの画面に至るまで、ますます多くの端末でタッチスクリーンが利用可能になっています。ユーザーが 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;
}

試してみる

ボタンの状態によって色が異なることを示す画像

ほとんどのモバイル ブラウザでは、要素がタップされた後の状態として「hover」と「focus」の両方、またはこのどちらかを要素に適用します。

適用するスタイルとユーザーがタップした後の外観については、慎重に検討してください。

デフォルトのブラウザ スタイルを無効にする

さまざまな状態用のスタイルを追加すると、ほとんどのブラウザではユーザーのタップ操作に応じて独自のスタイルが実装されていることがわかります。これは主に、モバイル端末が初めてリリースされた当時、:active 状態用のスタイルが用意されていないサイトが多かったことが原因です。結果的に、多くのブラウザでユーザー操作に応答するためにハイライト色やスタイルが追加されました。

ほとんどのブラウザでは、outline CSS プロパティを使用して、フォーカスされた要素の輪郭線を表示しています。この動作は、以下のようにすると無効にできます。

.btn:focus {
    outline: 0;

    /* Add replacement focus styling here (i.e. border) */
}

Safari と Chrome ではタップした要素がハイライト表示されますが、この動作は次のように CSS プロパティ -webkit-tap-highlight-color で無効にできます。

/* 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 では、次の 2 つの副作用に対処する必要があります。

タップ可能な要素に輪郭線を追加する疑似クラス -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 上でのマウスによるドラッグ操作など、一部のデフォルト動作を無効にしたい場合があります。

これは、CSS プロパティ user-select を使用すると実現できます。ただし、コンテンツに対してこのような処理をすると、要素内のテキストを選択したいと思っているユーザーは、非常にストレスを感じることがあるため注意が必要です。そのため、慎重に使用し、使用頻度を抑えてください。

/* Example: Disable selecting text on a paragraph element: */
p.disable-text-selection {
  user-select: none;
}

カスタム ジェスチャーを実装する

サイトでカスタムの操作およびジェスチャーを使用することを考えている場合は、以下の 2 つのトピックに留意してください。

  1. すべてのブラウザに対応する方法。
  2. 高いフレームレートを維持する方法。

この記事では、すべてのブラウザをサポートするために必要な API と、そのイベントを効率的に使用する方法について説明します。

ジェスチャーで行う動作に応じて、ユーザーが一度に操作できる要素を 1 つにするか、複数の要素を同時に操作できるようにするかが決まります。

この記事では、すべてのブラウザをサポートする方法と、フレームレートを高く保つ方法を示す 2 つの例を紹介します。

ドキュメント上の GIF 画像をタップする例

最初の例では、ユーザーは 1 つの要素を操作できます。このケースでは、この要素上でジェスチャーが開始される場合に限り、この要素にすべてのタッチイベントを通知します。たとえば、このスワイプ可能な要素は指を放したあとでも制御が可能です。

結果的に柔軟性と利便性は大いに高まりますが、ユーザーが UI を操作する方法は限られます。

要素上の GIF 画像をタップする例

一方、マルチタップによってユーザーに複数の要素を一度に操作して欲しい場合は、特定の要素に対するタップを制限する必要があります。

ユーザーにとってはさらに柔軟性が高くなりますが、UI を処理するロジックは複雑化し、ユーザーエラーに対処するのが難しくなります。

イベント リスナーを追加する

Chrome(バージョン 55 以降)、Internet Explorer、Edge では、カスタム ジェスチャーの実装に PointerEvents を使用することをおすすめします。

他のブラウザでは、TouchEventsMouseEvents をご利用ください。

PointerEvents の優れた機能は、マウス、タップ、ペンなどの複数の入力タイプを 1 つのコールバック セットに統合できることです。リッスンするイベントは 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 を取得します。イベントが PointerEvent または MouseEvent であれば、イベント自体から直接 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 には、タップデータを含む 3 つのリストがあります。

  • 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 かどうかを確認します。false は、最後の移動イベント以降に requestAnimationFrame()onAnimFrame() を呼び出したかどうかを示します。つまり、実行待ちの requestAnimationFrame() は常に 1 つしか存在しないということになります。

onAnimFrame() コールバックが実行されたら、移動したい要素に対して遷移の設定を行ったあと、rafPendingfalse に更新して、次のタップイベントで新しいアニメーション フレームをリクエストできるようにします。

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 を使用すると、ブラウザで実装された操作を無効にできます。たとえば、Internet Explorer バージョン 10 以降では、ダブルタップによるズーム操作がサポートされています。touch-actionmanipulation に設定すると、デフォルトのダブルタップ動作を防ぐことができます。

これにより、自身でダブルタップ操作を実装することができます。

以下は、よく使用される touch-action 値のリストです。

タップ操作のパラメータ
touch-action: none タップ操作はブラウザによって処理されません。
touch-action: pinch-zoom 「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
フォーカスが当たった状態のボタン
ページ上の要素までユーザーがタブで移動したときの状態です。フォーカス状態によって、ユーザーは現在操作している要素を把握できます。また、キーボードを使用して UI を簡単に操作することもできます。
:active
押されている状態のボタン
ユーザーが要素をクリックまたはタップしたときなど、要素が選択されたときに入力されます。

最終的なタッチイベント リファレンスについては、W3C タッチイベントをご覧ください。

タッチ、マウス、ポインタのイベント

これらのイベントは、新しいジェスチャーをアプリケーションに追加するために必要な要素です。

タップ、マウス、ポインタ イベント
touchstartmousedownpointerdown 指で最初に要素に触れたとき、またはユーザーがマウスをクリックしたときに呼び出されます。
touchmovemousemovepointermove ユーザーが画面上で指を動かしたり、マウスでドラッグしたりすると呼び出されます。
touchendmouseuppointerup ユーザーが画面から指を離したとき、またはマウスを離したときに呼び出されます。
touchcancel pointercancel タップ操作がブラウザによってキャンセルされたときに発生します。たとえば、ユーザーがウェブアプリをタップしたあとに、タブを移動した場合などです。

タップリスト

各タップイベントには、次の 3 つのリスト属性が含まれます。

タッチイベントの属性
touches タップされている要素は問わず、現在画面上にあるすべてのタップのリスト。
targetTouches 現在のイベントの対象である要素で開始されたタッチのリスト。たとえば、<button> にバインドすると、現在のそのボタン上でのタップのみが取得されます。ドキュメントにバインドすると、現在のドキュメント上のすべてのタップが取得されます。
changedTouches イベントの発生原因となった変化が生じたタップのリスト:
  • touchstart イベントの場合 - 現在のイベントでアクティブになったばかりのタップポイントのリスト。
  • touchmove イベントの場合 - 最後のイベント以降に移動したタッチポイントのリスト。
  • touchend イベントと touchcancel イベントの場合(サーフェスから削除されたばかりのタッチポイントのリスト)。

iOS で active 状態をサポートする

iOS 版の Safari では、残念ながらデフォルトで「active」状態を適用できません。適用可能にするには、「documentbody」または要素ごとに touchstart イベントリスナを追加する必要があります。

これは iOS 端末に特化した処理なので、ユーザー エージェントのテスト後に行ってください。

タッチ開始のリスナを body に追加すると、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);
    }
  }
};