読者です 読者をやめる 読者になる 読者になる

Paw.jsというのを書いたのと今から始めるマルチタッチイベント処理

どうも、連載予定は絶対に次回を書かないでおなじみの僕です。

Paw.js is 何

マルチタッチに対応してない、いわゆるfastclickを実現するものやtapイベントを発行するライブラリ、やたら機能が多くかつ特にtouchmoveでのイベント過多で処理落ちしかねないライブラリが多かったので、現実的に使うであろう範囲でめちゃ軽なやつが欲しかったので作りました。

Paw.js

あなたがもしモバイルWeb開発者で60fpsを出さなければ死ぬのであれば使えばいいと思いますし、そんな必要が無いもしくはフリックやジェスチャも取りたいと言うのであればHammer.jsとか使えばいいんじゃないでしょうか。


  • シンプル, 小さい, 速い
  • touchevent, pointereventのマルチタッチ対応
  • tap, doubletap, pressの3つのカスタムイベントを発行
  • fastclick機能

です。Webサイトやアプリケーションで現実的かつ恒常的に使用されうる機能に絞りました。もし需要があるならフリック系も作ろうとは思いますが現状Hammer.jsで十分でしょう。ってかみんなWeb上でジェスチャとか使うんですかね。
また、スワイプイベントでよく見られる"指が動き終わってからイベントが発火"するというのがありますが、指に付いてこないで動かし終わってから描画が追いつくという気持ち悪い状態になりますのでそういうのはやらないためにも実装してません。逆に言うとそういうのは普通に自分でやった方が軽く気持ちよく動かせます。

カスタムイベントを使用(非搭載ブラウザは普通のイベント)を使ってますので、標準APIはもちろんjQueryでも$('div').on('tap', fn)のようにイベントを拾うことが出来ます。もちろんBackboneとかその辺でも使えるようになってます。

ちなみに Paw は犬猫などの足という意味で肉球を思い出させ、ライブラリ制作者も使用者にも癒やしを与える効果があります。

相変わらずUI周りのテストは書きづらくまだ手を付けてないのですが、一通りの端末ではテストしましたのでご興味があればお使いください。

※通常状態だとdocumentにtouchmoveべた貼りするのでchromeにperformance warningだされますが、一部のAndroidさんがいきなりtouchcancel呼んだりしていきなり別の要素でtouchmoveされたりする挙動で何のタッチか分からなくなる問題のため現状は仕方ありません。

いい加減覚えるマルチタッチイベント処理

タッチイベント周りはマルチタッチ ウェブ開発 - HTML5ROCKSを呼んでいただければ大体ご理解いただけると思いますが、今回は要約して説明していきます。

TouchEvent と PointerEvent

え?IEの話はどうでもいい?まぁまぁ落ち着いてください。昨今はSurface等のデバイスもありますし覚えておいて損はありません。

TouchEventはtouches, targetTouches, changedTouchesというTouchListオブジェクトが付与されたイベントデータと考えればよいと思います。このTouchListにはそれぞれ以下のようなTouchオブジェクトが入っています。

  • touches: 現在画面上にある全ての指のデータ
  • targetTouches: 現在のDOM要素上にある指のリスト
  • changedTouches: 現在のイベントに関与している指のリスト

JavaScriptからハンドリングする場合、touchesは2本以上の指でのジェスチャ、targetTouches&changedTouchesはタップやスワイプなどの1つの指を扱う場合が多いです。

変わってPointerEventはTouchListオブジェクトのようなものは備えておらず、1つのイベントで1つのポインターを扱うというようになります。(TouchEventでいうとchangedTouchesに入ったTouchオブジェクトを常に1つ受け取るような感覚です)

識別子

タッチイベントを扱う上(特にマルチタッチ)で重要なのが識別子です。TouchEventであればTouchオブジェクト、PointerEventであればイベントのパラメータに必ず識別子が付いているので、その識別子を元にイベントを扱っていくと良いでしょう。

TouchEventの識別子

TouchEventの識別子は、touches, targetTouches, changedTouchesのTouchListオブジェクト内に入ったTouchオブジェクトのプロパティidentifierに付与されています。

// ex. 識別子を取る
document.addEventListener('touchstart', function(ev) {
    // 最初のTouchオブジェクトを取る
    var touch = ev.touches[0];     

    // 識別子
    console.log(touch.identifier);
});

識別子で判断することで、以下のように別の場所が他の指でタッチされた場合の処理を書くことが出来ます。

var lastTouchId = null;

document.addEventListener('touchstart', function(ev) {
    var touch = ev.changedTouches[0];

    lastTouchId = touch.identifier;
});

doucment.addEventListener('touchmove', function() {
    var touch = ev.changedTouches[0];    

    // 一番最後にタッチされ始めた指以外は処理しない
    if (lastTouchId !== touch.identifier) {
        return;
    }
});

Note.

例ではtouch, changedTouchesの最初のTouchオブジェクトのみ扱ってますが、複数のTouchオブジェクトが存在する可能性があるためfor等で列挙して処理するほうが良いでしょう。

PointerEventの識別子

PointerEventは通常のUIEventにポインター情報のパラメータが追加で付与されたようなオブジェクトとなっています。

document.addEventListener('pointerdown', function(ev) {
    // 識別子 
    ev.pointerId
});

上でも書きましたがTouchEventでいうchangedTouchesのTouchオブジェクトがイベントパラメータとして入ってくるような状態になるので、pointerIdプロパティを元にタッチごとの判定を行います。

touchmove, pointermove, mousemove (+ scroll)で無理をしない

若干マルチタッチとは離れますが、上記のイベントの中で特にCSSの変更やレイアウトプロパティへのアクセスを行うべきではありません。

これはブラウザによるスタイルの再計算や再描画が高頻度で走ることによるブラウザの全体的なパフォーマンスの落ち込みを防止するためですが、昨今流行りのパララックスエフェクト等ではガッツリと使われている現状が残念でなりません。(まぁPCブラウザならそんなに気にすることもありませんがね…)

こういった場合は、以下のようにイベントと描画のロジックを切り離し、描画の頻度を下げることで回避できたりします。

//// touchmoveごとにスタイルを変更したい(適当バージョン)

var fps = 30; // 30fpsに制限
var frameTime = 1000 / fps;
var isAnimated = false;
var lastTouchInfo = null;
var element = document.getElementById('some-element');

document.addEventListener('touchstart', function(ev) {
    // 最初のデータを入れてアニメーションを開始
    lastTouchInfo = ev.changedTouches[0];
    isAnimated = true;
    animation();
});

document.addEventListener('touchmove', function(ev) {
    // touchmoveイベントの中ではデータの更新だけする
    lastTouchInfo = ev.changedTouches[0];
});

document.addEventListener('touchend', function() {
    // アニメーションの終了
    isAnimated = false;
    lastTouchInfo = null;
});

document.addEventListener('touchcancel', function() {
    // アニメーションの終了
    isAnimated = false;
    lastTouchInfo = null;
});

function animation() {
    // isAnimatedフラグが立ってなかったら終了
    if (!isAnimated) {
        return; 
    }

    // 最後のタッチイベントからデータを取得
    var x = lastTouchInfo.pageX;
    var y = lastTouchInfo.pageY;

    // 何かのスタイルを変更したりする
    element.style.top = y + 'px';
    element.style.left = x + 'px';

    // 次のanimationを登録
    setTimeout(animation, frameTime);
}

実はマルチタッチのデータを上手く扱う方法はこのくらいです。
が、実際にそのデータを扱い始めるとまた別の難しい問題(指が動いた距離、角度など)が出てきますので、その辺りは頑張ってみてください。