スマートフォンからデスクトップ画面に至るまで、タッチスクリーンはますます多くのデバイスで利用できます。アプリはタップ操作に直感的かつ美しく反応する必要があります。
スマートフォンからパソコンの画面に至るまで、ますます多くの端末でタッチスクリーンが利用可能になっています。ユーザーが 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 つのトピックに留意してください。
- すべてのブラウザに対応する方法。
- 高いフレームレートを維持する方法。
この記事では、すべてのブラウザをサポートするために必要な 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 リスナを追加します。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
には、タップデータを含む 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
を使用すると、ブラウザで実装された操作を無効にできます。たとえば、Internet Explorer バージョン 10 以降では、ダブルタップによるズーム操作がサポートされています。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 で 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);
}
}
};