はじめに
スマホ向けのUIを作っていると、「右にスワイプしたらメニューを閉じる」「2本指でピンチしたら画像を拡大する」といった操作を実装したくなる場面が必ず出てきます。しかしtouchstart・touchmove・touchendを自前で組み合わせて、指の移動量や速度からジェスチャーを判定するコードを書いたことがある人なら分かるとおり、これは想像以上に地味で厄介な作業です。
Hammer.jsは、このタッチジェスチャーの検知をライブラリ側に任せてしまうためのJavaScriptライブラリです。要素にHammerインスタンスを紐づけるだけで、tap・pan・swipe・pinch・rotate・pressといった代表的なジェスチャーをイベントとして受け取れるようになります。タッチだけでなくマウス操作にも対応しているため、PCのブラウザで開発しながら動作確認ができるのも実務上ありがたいポイントです。
とはいえ、説明を読むよりまず動かしてみたほうが早いと思います。ドラッグやスワイプがどうイベントとして拾われるか、先に挙動を見たい方はこちらからどうぞ。
Hammer.jsとは
Hammer.jsは、DOM要素へのタッチ・マウス操作を解析し、tap(タップ)、pan(ドラッグ)、swipe(払う動作)、pinch(つまむ動作)、rotate(回転)、press(長押し)といったジェスチャーをイベントとして発火させるライブラリです。2011年から公開されている歴史のあるライブラリで、多くのUIライブラリやフレームワークのタッチ操作実装の裏側で使われてきました。
主な特徴
- 依存ライブラリなし - Hammer.js単体で動作し、他のライブラリを必要としません
- 6種類のジェスチャーを標準搭載 - tap・doubletap・press・pan・swipe・pinch・rotateを最初から認識できます
- タッチとマウスの両対応 - モバイル実機だけでなく、PCのマウス操作でも同じイベントが発火します
- カスタムジェスチャーを組み立てられる -
Hammer.Managerを使えば、認識器(Recognizer)を自分で組み合わせて独自のジェスチャーを定義できます
インストール
npmでインストールする場合は以下のコマンドを実行します。
npm install hammerjs
Yarnの場合は次のとおりです。
yarn add hammerjs
CDN経由で読み込みたい場合は、cdnjsなどから直接<script>タグで読み込むことも可能です。
Hammer.jsのサンプルを動かす
下のサンプルは、Hammer.jsのnew Hammer(element)で要素をラップし、panイベントとswipeイベントを購読しているだけのシンプルな例です。中央のボックスをドラッグすると位置がリアルタイムに動き、指(またはマウス)を離す直前の速度に応じてswipeleft・swiperightイベントが発火します。
要点となる部分だけを抜き出すと、次のようになります。
// 要素を渡してHammerインスタンスを作る
const hammer = new Hammer(box);
// panイベントで移動量(deltaX, deltaY)を取得
hammer.on('pan', (e) => {
box.style.transform = `translate(${e.deltaX}px, ${e.deltaY}px)`;
});
// swipeイベントで払う方向を判定
hammer.on('swipeleft swiperight', (e) => {
console.log(e.type); // 'swipeleft' または 'swiperight'
});
実際に動かせるものが下です。ボックスをドラッグして動きを確認し、素早く左右に払うとログにswipeleft・swiperightが出力されます。
e.deltaX・e.deltaYはジェスチャー開始位置からの移動量、e.velocityXはドラッグの速度を表します。hammer.on('pan', ...)をhammer.on('panleft panright', ...)に変えると、左右方向の移動だけを個別に検知することもできます。
基本的な使い方
Hammer.jsの最小構成は、対象要素をnew Hammer()に渡し、on()でイベント名を購読するだけです。
import Hammer from 'hammerjs';
const element = document.querySelector('.target');
const mc = new Hammer(element);
mc.on('tap', (e) => {
console.log('tapped', e.center);
});
mc.on('press', (e) => {
console.log('long pressed');
});
デフォルトで有効になっているのはtap・doubletap・press・pan(全方向)・swipe(全方向)です。pinchとrotateは複数指の操作と競合しやすいため、初期状態では無効化されています。使う場合は明示的に有効化します。
mc.get('pinch').set({ enable: true });
mc.get('rotate').set({ enable: true });
実践的なユースケース
パンで方向を制限してドラッグさせる
スライダーのつまみやドラッグ可能なカードなど、動かせる方向を左右だけ・上下だけに限定したい場面があります。Hammer.jsのPan認識器はdirectionオプションで許可する方向を制御できます。
Hammer.DIRECTION_HORIZONTALをHammer.DIRECTION_VERTICALに変えると、今度は上下方向にしかドラッグできなくなります。directionには他にDIRECTION_ALL・DIRECTION_LEFTなど、片方向のみ許可する定数も用意されています。
ピンチとローテートで拡大・回転させる
画像ビューアや地図UIでよく見る「2本指でつまんで拡大し、ひねって回転させる」操作も、Hammer.jsならpinchとrotateを有効化するだけで実装できます。
const mc = new Hammer(element);
mc.get('pinch').set({ enable: true });
mc.get('rotate').set({ enable: true });
let scale = 1;
let rotation = 0;
mc.on('pinch rotate', (e) => {
element.style.transform = `scale(${scale * e.scale}) rotate(${rotation + e.rotation}deg)`;
});
mc.on('pinchend rotateend', (e) => {
scale *= e.scale;
rotation += e.rotation;
});
e.scaleは操作前を1とした拡大率、e.rotationは操作開始からの回転角度(度数)です。pinchend・rotateendのタイミングで現在値に確定させることで、次のジェスチャーが前回の続きから動くようになります。
複数の認識器を組み合わせて競合を制御する
tapとdoubletapのように、放っておくと同時に発火してしまうジェスチャー同士は、recognizeWithやrequireFailureで優先関係を指定できます。Hammer.Managerを直接使うと、標準のnew Hammer()が内部で行っている構成を自分で組み立てられます。
const manager = new Hammer.Manager(element);
const doubleTap = new Hammer.Tap({ event: 'doubletap', taps: 2 });
const singleTap = new Hammer.Tap({ event: 'tap' });
// doubletapの判定が確定するまでtapの発火を待たせる
singleTap.requireFailure(doubleTap);
doubleTap.recognizeWith(singleTap);
manager.add([doubleTap, singleTap]);
manager.on('tap doubletap', (e) => console.log(e.type));
このようにrequireFailureを使うことで、「ダブルタップの可能性がなくなるまでシングルタップを確定させない」という優先順位を明示的に制御できます。カスタムジェスチャーを増やす場合も同じ仕組みで衝突を解消します。
まとめ
Hammer.jsを使うと、touchstart・touchmove・touchendを自前で組み合わせて指の動きを解析するコードを書かずに、tap・pan・swipe・pinch・rotate・pressといったジェスチャーをイベントとして扱えるようになります。デフォルトの認識器で足りない場合も、directionオプションでの方向制限やHammer.Managerによる認識器の直接構成まで対応できるため、シンプルなスワイプ検知から複雑なマルチタッチ操作まで同じAPIでカバーできます。
タッチUIを実装する機会があれば、まずはnew Hammer(element)から試してみてください。
