JavaScript

JavaScriptでTouchEvents(Level-2)を擬似発火する

More than 3 years have passed since last update.

TouchEvents Level-2では、JavaScriptからのTouchEvent擬似発火が簡単になった。

さっそく試してみよう。

注意: まだ仕様はEditor's Draftなので変わる可能性あり。


対応ブラウザ

MDNによると、

Browser
version

Chrome
48.0

Firefox
?

IE
12.0

Opera
15.0

Safari
?

?ってなにこれ。

試してみたら、Firefoxでは使えなかった。ジャンク屋の動作未確認のようなもんだな!

まあ、今回はChromeで試すだけなので問題ない。


TouchEvent Level-2以前の世界

v2以前のタッチの擬似発火は本当につらいものだった。

初期化用のinitTouchEvent関数には15個くらい引数が必要となる。(座標指定も必要な割には無視される。)

その引数の順番はブラウザごとに異なるうえに、リファレンスなんて気の利いたものはありゃしないのだ。

記事ではChromeの仕様をChromiumソースコードから読み取っている。

ソースコードはドキュメント!みんなも困ったらブラウザのソース読もう!

ChromeもMSDNやMDN的なものあったらいいのに!

なお、initTouchEventがあまりにもカオスなので、W3Cも共通化を諦めたようだ。

ですよねー。


TouchEventコンストラクタ

さてv2では、TouchEventにコンストラクタがついてる。やったぜ。

https://w3c.github.io/touch-events/#touchevent-interface


WebIDL

dictionary TouchEventInit : EventModifierInit {

sequence<Touch> touches = [];
sequence<Touch> targetTouches = [];
sequence<Touch> changedTouches = [];
};

[Constructor(DOMString type, optional TouchEventInit eventInitDict)]
interface TouchEvent : UIEvent {
readonly attribute TouchList touches;
readonly attribute TouchList targetTouches;
readonly attribute TouchList changedTouches;
readonly attribute boolean altKey;
readonly attribute boolean metaKey;
readonly attribute boolean ctrlKey;
readonly attribute boolean shiftKey;
};


DOMStringはStringと同じ。(ECMAScriptでは)

TouchEventInitは定義通りだが結構階層が深い。

EventInitでbubblesとcancelableがデフォルトfalseになっているのにびっくり。

擬似発火するときはそりゃもちろんバブリングしてほしいから、trueを指定してやる必要がある。

肝心のタッチ指定は配列で渡すだけというお手軽さ。TouchListとやらを生成する必要もない。

// タッチイベント生成 (touch1-3の生成方法は後述)

const event = new TouchEvent('touchstart', {
touches: [touch1, touch2, touch3],
targetTouches: [touch2, touch3],
changedTouches: [touch3],
bubbles: true,
cancelable: true
});
element.dispatchEvent(event);

各タッチの意味は以下のとおり。


  • touches: 画面全ての指リスト

  • targetTouches: 同じ要素上の指リスト

  • changedTouches: 今回発火したイベント(指が触れたなど)に関連する指リスト


Touchコンストラクタ

https://w3c.github.io/touch-events/#touch-interface


WebIDL

dictionary TouchInit {

required long identifier;
required EventTarget target;
double clientX = 0;
double clientY = 0;
double screenX = 0;
double screenY = 0;
double pageX = 0;
double pageY = 0;
float radiusX = 0;
float radiusY = 0;
float rotationAngle = 0;
float force = 0;
};

[Constructor(TouchInit touchInitDict)]
interface Touch {
readonly attribute long identifier;
readonly attribute EventTarget target;
readonly attribute double screenX;
readonly attribute double screenY;
readonly attribute double clientX;
readonly attribute double clientY;
readonly attribute double pageX;
readonly attribute double pageY;
readonly attribute float radiusX;
readonly attribute float radiusY;
readonly attribute float rotationAngle;
readonly attribute float force;
};


こいつもコンストラクタがついてる。

引数はなんとオブジェクト1つだけ。必須プロパティも2つだけ。

シンプルでいいね。まあ普通こうするよなあ。

タッチの場合、指を識別するためのidentifierプロパティが生えている。

他にもいろいろプロパティが生えてて面白い。


  • radiusX, radiusY: 指の大きさ

  • rotationAngle: 指の回転角

  • force: 押した強さ


擬似発火

これでTouchを生成してTouchEvent擬似発火ができる。

ちょっと長いけどこんな感じ。エラー処理はなし。


es6

class TouchEmulator {

constructor(){
this.touches = []; // 全タッチを保持
}

touchstart(id, point){
const target = document.elementFromPoint(point.x, point.y);
const touch = this.createTouch(id, target, point);

// touchesに追加
this.touches.push(touch);

this.triggerTouchEvent('touchstart', touch);
}

touchmove(id, point){
const index = this.touches.findIndex(t => t.identifier === id);
const target = this.touches[index].target;

const touch = this.createTouch(id, target, point);

// touchesを更新
this.touches[index] = touch;

this.triggerTouchEvent('touchmove', touch);
}

touchend(id, point){
const target = this.touches.find(t => t.identifier === id).target;

const touch = this.createTouch(id, target, point);

// touchesから除去
this.touches = this.touches.filter(t => t.identifier !== id);

this.triggerTouchEvent('touchend', touch);
}

// Touchをxとyから生成する
createTouch(identifier, target, point){
return new Touch({
identifier,
target,
clientX: point.x,
clientY: point.y,
pageX: point.x + window.pageXOffset,
pageY: point.y + window.pageYOffset,
radiusX: 10,
radiusY: 10,
force: 1
});
}

// TouchEventを作って発火する
triggerTouchEvent(name, touch) {
// targetが同じTouchを取り出す
const targetTouches = this.touches.filter(t => t.target === touch.target);
const event = new TouchEvent(name, {
touches: this.touches,
targetTouches,
changedTouches: [touch],
bubbles: true, // これがないとバブリングしない
cancelable: true,
view: window
});
touch.target.dispatchEvent(event);
}
}


追記: ソース一部修正。jsdo.itでサンプル書いてます。

document.elementFromPoint(x,y)で座標から要素を取得している。

こうやって使う。

var emulator = new TouchEmulator();

elmulator.touchstart(1, {x: 100, y: 200});
elmulator.touchmove(1, {x: 120, y: 200});
elmulator.touchstart(2, {x: 200, y: 250});
elmulator.touchend(1, {x: 150, y: 220});
elmulator.touchend(2, {x: 200, y: 250});

こいつの一般的な用途は知らない。業務で必要になったのだよ。


その他メモ


  • TouchEventはtouchmove,touchendイベントもtouchstartと同じ要素で発火する。



  • アプリ側でスクリプトによる擬似発火イベントかどうかを見分けたいなら、Event.isTrustedが使える。



  • タッチイベント系のライブラリがうまく騙せなくて困っている。