スマートフォンからデスクトップ画面まで、より多くのデバイスでタッチスクリーンが利用できるようになりました。アプリは、タップに直感的かつ美しい方法で反応する必要があります。
スマートフォンからデスクトップ画面まで、ますます多くのデバイスでタッチスクリーンが利用されています。ユーザーが 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;
}
ほとんどのモバイル ブラウザでは、要素がタップされると、カーソルを合わせた状態やフォーカスされた状態が要素に適用されます。hoverhover
設定するスタイルと、タップ後のスタイルについて慎重に検討してください。
デフォルトのブラウザ スタイルを抑制する
さまざまな状態に対応したスタイルを追加すると、ほとんどのブラウザはユーザーのタップに応じて独自のスタイルを実装していることがわかります。これは主に、モバイル デバイスがリリースされた当初、多くのサイトで :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 には、対処すべき副作用が 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 上でマウスをドラッグしたりするデフォルトの動作を抑制したい場合があります。
これは user-select
CSS プロパティで行うことができますが、ユーザーが要素内のテキストを選択したい場合、コンテンツに対してこれを行うとextremely不快な思いをさせる可能性があるので注意してください。使用する際は慎重に行ってください。
/* Example: Disable selecting text on a paragraph element: */
p.disable-text-selection {
user-select: none;
}
カスタム ジェスチャーを実装する
サイトのカスタム操作とジェスチャーについてアイデアがある場合は、次の 2 つのトピックを念頭に置いてください。
- すべてのブラウザをサポートする方法
- 高フレームレートを維持する方法
この記事では、すべてのブラウザに対応するうえでサポートする必要がある API と、これらのイベントを効率的に使用する方法について説明します。
ジェスチャーで何を行うかに応じて、ユーザーが一度に 1 つの要素を操作できるようにするか、複数の要素を同時に操作できるようにするかが考えられます。
この記事では、すべてのブラウザのサポートと、高いフレームレートを維持する方法を示す 2 つの例を見ていきます。
最初の例では、ユーザーが 1 つの要素を操作できます。この場合、操作が要素自体で最初に開始されている限り、すべてのタッチイベントをその要素に与えることができます。たとえば、スワイプ可能な要素から指を離しても、引き続きその要素を制御できます。
ユーザーにとっては柔軟性が高いものの、UI の操作方法には制限が適用されるため、これは便利です。
ただし、ユーザーがマルチタッチを使用して複数の要素を同時に操作することが想定される場合は、タップを特定の要素に制限する必要があります。
ユーザーにとっては柔軟性が高くなりますが、UI を操作するロジックが複雑になり、ユーザーエラーに対する耐性が低下します。
イベント リスナーを追加する
Chrome(バージョン 55 以降)、Internet Explorer および Edge では、PointerEvents
を使用してカスタム ジェスチャーを実装することをおすすめします。
他のブラウザでは、TouchEvents
と MouseEvents
を使用するのが適切です。
PointerEvents
の大きな特長は、マウスイベント、タッチイベント、ペンイベントなど、複数のタイプの入力を 1 つのコールバック セットに統合できることです。リッスンするイベントは 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 リスナーを追加します。MouseEvent の場合は、開始イベントのみを追加します。
- 開始ジェスチャー コールバック内で、マウスの移動イベントと終了イベントをドキュメントにバインドします。これにより、元の要素でイベントが発生したかどうかにかかわらず、すべてのマウスイベントを受信できます。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
には、タップデータを含む 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()
コールバックの実行時に、移動したい要素に対して変換を設定してから、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 タッチイベントをご覧ください。
タップ、マウス、ポインタのイベント
これらのイベントは、アプリに新しい操作を追加するための構成要素です。
タッチリスト
各タッチイベントには、次の 3 つのリスト属性が含まれます。
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);
}
}
};