タッチベースのユーザインターフェイスを高度にサポートするため、タッチスクリーンやトラックパッドでの指 (あるいはスタイラス) の動きを解釈する機能を、touch イベントが提供します。
タッチイベントのインターフェイスは、2 本の指によるジェスチャーなどアプリケーション固有のマルチタッチ操作をサポートするために使用できる、比較的低レベルの API です。マルチタッチ操作は、1 本の指 (またはスタイラス) を始めに接触面へタッチしたときから始まります。その後に他の指をタッチして、任意でタッチ面上で動かします。指を接触面から離すと、操作が終了します。操作している間、アプリケーションは開始・移動・終了の各段階中にタッチイベントを受け取ります。
タッチイベントはマウスイベントに似ていますが、タッチ面上の異なる場所で同時に発生するタッチをサポートすることが異なります。TouchEvent
インターフェイスは、現在アクティブなすべてのタッチ個所を包含します。Touch
インターフェイスはひとつのタッチ個所を表し、ブラウザのビューポートを基準にしたタッチ個所の位置などの情報を含みます。
定義
- Surface
- タッチに反応する面。スクリーンやトラックパッドでしょう。
- Touch point
- Surface に接触した点。これは指 (あるいはひじ、耳、鼻などでもよいのですが、たいてい指でしょう) またはスタイラスでしょう。
インターフェイス
TouchEvent
- サーフェスでタッチ状態が変化したときに発生するイベントを表します。
Touch
- ユーザとタッチサーフェスが接した点 1 個を表します。
TouchList
- タッチのグループを表します。例えば、ユーザが複数の指を同時にサーフェス上に置いた場合に使用します。
例
ここでは一度に複数のタッチポイントを取得しており、ユーザが一度に複数の指で <canvas>
に描くことができるようになっています。このサンプルはタッチイベントをサポートするブラウザのみで動作します。
canvas を生成する
<canvas id="canvas" width="600" height="600" style="border:solid black 1px;"> Your browser does not support canvas element. </canvas> <br> <button onclick="startup()">Initialize</button> <br> Log: <pre id="log" style="border: 1px solid #ccc;"></pre>
イベントハンドラを設定する
ページを読み込むとき、以下の startup()
関数を <body>
要素の onload
属性で呼び出します (ただしこのサンプルでは、MDN の Live Example システムの制限によりボタンを押して呼び出します)。
function startup() { var el = document.getElementsByTagName("canvas")[0]; el.addEventListener("touchstart", handleStart, false); el.addEventListener("touchend", handleEnd, false); el.addEventListener("touchcancel", handleCancel, false); el.addEventListener("touchmove", handleMove, false); log("initialized."); }
これは単に <canvas>
要素へすべてのイベントリスナを設定している関数であり、タッチイベントの発生に応じてハンドリングできるようになります。
新たなタッチをトラッキングする
進行中のタッチをトラッキングし続けます。
var ongoingTouches = new Array();
サーフェス上で新たなタッチが発生したことを示す touchstart
イベントが発生すると、handleStart()
関数を呼び出します。
function handleStart(evt) { evt.preventDefault(); log("touchstart."); var el = document.getElementsByTagName("canvas")[0]; var ctx = el.getContext("2d"); var touches = evt.changedTouches; for (var i = 0; i < touches.length; i++) { log("touchstart:" + i + "..."); ongoingTouches.push(copyTouch(touches[i])); var color = colorForTouch(touches[i]); ctx.beginPath(); ctx.arc(touches[i].pageX, touches[i].pageY, 4, 0, 2 * Math.PI, false); // a circle at the start ctx.fillStyle = color; ctx.fill(); log("touchstart:" + i + "."); } }
ここでは、ブラウザがタッチイベントの処理を続けないようにするため event.preventDefault()
を呼び出します (また、マウスイベントの伝達も抑止します)。そしてコンテキストを取得して、イベントの TouchEvent.changedTouches
プロパティから変化したタッチポイントのリストを取り込みます。
その後に、リスト内のすべての Touch
オブジェクトをイテレートしてアクティブなタッチポイントの配列に送り込み、描画を開始する位置に小さな丸印を描画します。この例では 4 ピクセル幅の線を使用しますので、半径 4 ピクセルの円がきれいに見えます。
タッチの移動に合わせて描画する
1 本以上の指が移動するたびに touchmove
イベントが発生しますので、その結果 handleMove()
関数が呼び出されます。これはキャッシュしたタッチ情報を更新して、タッチごとに以前の位置から現在の位置まで線を描画する役割を担っています。
function handleMove(evt) { evt.preventDefault(); var el = document.getElementsByTagName("canvas")[0]; var ctx = el.getContext("2d"); var touches = evt.changedTouches; for (var i = 0; i < touches.length; i++) { var color = colorForTouch(touches[i]); var idx = ongoingTouchIndexById(touches[i].identifier); if (idx >= 0) { log("continuing touch "+idx); ctx.beginPath(); log("ctx.moveTo(" + ongoingTouches[idx].pageX + ", " + ongoingTouches[idx].pageY + ");"); ctx.moveTo(ongoingTouches[idx].pageX, ongoingTouches[idx].pageY); log("ctx.lineTo(" + touches[i].pageX + ", " + touches[i].pageY + ");"); ctx.lineTo(touches[i].pageX, touches[i].pageY); ctx.lineWidth = 4; ctx.strokeStyle = color; ctx.stroke(); ongoingTouches.splice(idx, 1, copyTouch(touches[i])); // swap in the new touch record log("."); } else { log("can't figure out which touch to continue"); } } }
これは同様に変更されたタッチに対してイテレートしていますが、各タッチで新たに描画する線分の開始点を検出するために、各タッチの以前の情報についてキャッシュしたタッチ情報の配列を参照しています。これは、各タッチの Touch.identifier
プロパティを確認して行います。このプロパティは各タッチで一意の識別子であり、指とタッチ面との接触が続いている間、値が固定されます。
これにより各タッチの前の位置の座標を取得して、2 つの点を結ぶ線分を描画するために適切なコンテキストメソッドを使用できます。
線分を描画した後、前のタッチ個所の情報を ongoingTouches
配列内にある現在の情報に置き換えるため、Array.splice()
を呼び出します。
タッチの終了を制御する
ユーザがタッチ面から指を離すと、touchend
イベントが発生します。私たちはこれらの両方を、以下の handleEnd()
関数を呼び出すというひとつの方法で扱います。この関数の役割は、終了したタッチについて最後の線分を描画することと、継続中のタッチのリストからタッチ個所を削除することです。
function handleEnd(evt) { evt.preventDefault(); log("touchend"); var el = document.getElementsByTagName("canvas")[0]; var ctx = el.getContext("2d"); var touches = evt.changedTouches; for (var i = 0; i < touches.length; i++) { var color = colorForTouch(touches[i]); var idx = ongoingTouchIndexById(touches[i].identifier); if (idx >= 0) { ctx.lineWidth = 4; ctx.fillStyle = color; ctx.beginPath(); ctx.moveTo(ongoingTouches[idx].pageX, ongoingTouches[idx].pageY); ctx.lineTo(touches[i].pageX, touches[i].pageY); ctx.fillRect(touches[i].pageX - 4, touches[i].pageY - 4, 8, 8); // and a square at the end ongoingTouches.splice(idx, 1); // remove it; we're done } else { log("can't figure out which touch to end"); } } }
これは前の関数にとても似ていますが、終端を表す小さな四角形を描画することと、Array.splice()
を呼び出して、更新後の情報を追加せずに継続中のタッチリストから古い項目を削除することが異なります。この結果、タッチ個所の追跡を停止します。
取り消されたタッチを制御する
ユーザの指がブラウザの UI に入り込んだり、その他にタッチをキャンセルしなければならないときには touchcancel
イベントが発生して、以下の handleCancel()
関数が実行されます。
function handleCancel(evt) { evt.preventDefault(); log("touchcancel."); var touches = evt.changedTouches; for (var i = 0; i < touches.length; i++) { ongoingTouches.splice(i, 1); // remove it; we're done } }
即座にタッチを取り消すという考え方から最終の線分を描画せずに、継続中のタッチリストから単純にタッチを削除します。
便利な関数
この例ではコードの残りの部分をより明確にすることを助ける、簡単に見ておくべきである 2 つの便利な関数を使用しています。
それぞれのタッチの色を選択する
それぞれのタッチの外見を区別して描画するために、タッチの一意な識別子を元に色を選択する colorForTouch()
関数を使用します。この識別子は不明瞭な数値ですが、少なくとも現在アクティブなタッチを区別することはできます。
function colorForTouch(touch) { var r = touch.identifier % 16; var g = Math.floor(touch.identifier / 3) % 16; var b = Math.floor(touch.identifier / 7) % 16; r = r.toString(16); // make it a hex digit g = g.toString(16); // make it a hex digit b = b.toString(16); // make it a hex digit var color = "#" + r + g + b; log("color for touch with identifier " + touch.identifier + " = " + color); return color; }
この関数の戻り値は、描画色を設定するために <canvas>
関数を呼び出すときに使用できる文字列です。例えば Touch.identifier
の値が 10 であれば、戻り値は文字列 "#aaa" になります。
touch オブジェクトをコピーする
一部のブラウザ (例えばモバイル版 Safari) はイベント間で touch オブジェクトを再使用するため、オブジェクト全体を参照するよりも、関心がある部分をコピーするほうが最善です。
function copyTouch(touch) { return { identifier: touch.identifier, pageX: touch.pageX, pageY: touch.pageY }; }
継続中のタッチを発見する
以下の ongoingTouchIndexById()
関数は、指定した識別にマッチするタッチを見つけるために配列 ongoingTouches
を探索して、そのタッチの配列内における添字を返します。
function ongoingTouchIndexById(idToFind) { for (var i = 0; i < ongoingTouches.length; i++) { var id = ongoingTouches[i].identifier; if (id == idToFind) { return i; } } return -1; // not found }
何を行っているか表示する
function log(msg) { var p = document.getElementById('log'); p.innerHTML = msg + "\n" + p.innerHTML; }
ブラウザがサポートしていれば、実際に試すことができます。
追加の Tips
この章では Web アプリケーションでタッチイベントを扱う方法について、追加の Tips を紹介します。
クリックを制御する
touchstart
あるいは一連の中で最初の touchmove
で preventDefault()
を呼び出すと対応するマウスイベントの発生を抑制できるため、touchstart
よりも touchmove
で preventDefault()
を呼び出すことが一般的です。この方法では従来どおりマウスイベントが発生して、リンクなどが引き続き動作します。代わりに一部のフレームワークでは同様の目的で、タッチイベントをマウスイベントとして再発生させています。(この例は過度に単純化しており、奇妙な動作になるかもしれません。ガイドとして掲載しているに過ぎません。)
function onTouch(evt) { evt.preventDefault(); if (evt.touches.length > 1 || (evt.type == "touchend" && evt.touches.length > 0)) return; var newEvt = document.createEvent("MouseEvents"); var type = null; var touch = null; switch (evt.type) { case "touchstart": type = "mousedown"; touch = evt.changedTouches[0]; break; case "touchmove": type = "mousemove"; touch = evt.changedTouches[0]; break; case "touchend": type = "mouseup"; touch = evt.changedTouches[0]; break; } newEvt.initMouseEvent(type, true, true, evt.originalTarget.ownerDocument.defaultView, 0, touch.screenX, touch.screenY, touch.clientX, touch.clientY, evt.ctrlKey, evt.altKey, evt.shiftKey, evt.metaKey, 0, null); evt.originalTarget.dispatchEvent(newEvt); }
2 番目のタッチのみで preventDefault() を呼び出す
ページ上で pinchZoom
と言った操作を防ぐテクニックのひとつとして、一連のタッチの 2 番目で preventDefault()
を呼び出す方法があります。この動作はタッチイベントの仕様書で明示されておらず、ブラザによって結果が異なります (iOS ではズームを防ぎますが、パンは可能です。Android はズームが可能ですが、パンはできません。Opera および Firefox は現状、パンもズームも防ぎます)。現在、このケースで特定の動作に依存することは推奨されず、メタビューポートのズームを防ぐと考えてください。
仕様
仕様書 | 策定状況 | コメント |
---|---|---|
Touch Events – Level 2 Touch の定義 |
勧告改訂案 | radiusX 、radiusY 、rotationAngle 、force プロパティを追加。 |
Touch Events Touch の定義 |
勧告 | 最初期の定義 |
ブラウザ実装状況
機能 | Chrome | Firefox (Gecko) | Internet Explorer | Opera | Safari |
---|---|---|---|---|---|
基本サポート | 22.0 | 18.0 (18.0)[1] | 未サポート | 未サポート | 未サポート |
機能 | Android | Android Webview | Chrome for Android | Firefox Mobile (Gecko) | Firefox OS | IE Mobile | Opera Mobile | Safari Mobile |
---|---|---|---|---|---|---|---|---|
基本サポート | (有) | (有) | (有) | 6.0 (6.0) | (有) | 11 | (有) | (有) |
[1] 3 つの値をとる設定項目 dom.w3c_touch_events.enabled
を使用できます。標準のタッチイベントに対して無効 (0)、有効 (1)、自動検出 (2) を指定でき、既定値は自動検出 (2) です。
Gecko 12 (Firefox 12.0 / Thunderbird 12.0 / SeaMonkey 2.9) より前のバージョンでは、マルチタッチをサポートしません。同時に 1 つのタッチのみ通知します。
Gecko 24.0 (Firefox 24.0 / Thunderbird 24.0 / SeaMonkey 2.21) では、Gecko 18.0 (Firefox 18.0 / Thunderbird 18.0 / SeaMonkey 2.15) でサポートしたタッチイベントをデスクトップ版の Firefox で無効にしています (バグ 888304)。Google や Twitter など、一部の著名サイトが正常に動作しないためです。バグが修正され次第、再び API を有効化する予定です。それでも有効化したい場合は、about:config
を開いて dom.w3c_touch_events.enabled
を 2
に設定してください。Android 版 Firefox や Firefox OS といったモバイル版は、このバグの影響を受けません。また、Windows 8 用の Metro 版 Firefox ではこの API を有効化しています。
Gecko 6.0 (Firefox 6.0 / Thunderbird 6.0 / SeaMonkey 2.3) より前のバージョンでは、プロプライエタリなタッチイベント API を提供していました。この API は非推奨です。標準の API に切り替えてください。