LightTable
2本指でタッチした時のイベント処理を参考にする為に上記ソースを読んでみたところイベントの処理で面白いコードがあったので紹介する。
LightTable
LightTable を立ち上げると白いウィンドウが起動する。ここへ画像をD&Dするとその画像をジェスチャー操作で動かすことができるようになる。
画像は2本指ドラッグで移動、ピンチで拡大縮小操作ができる。3本指でスワイプすると左側の設定エリアの表示を切り替えられる。
ジェスチャーを扱う
ピンチやスワイプ、回転といったジェスチャーはあらかじめ NSResponder にメソッドが用意されているので簡単に利用できる。これは前回までで紹介した。一方、NSResponderに用意されていない、2本指のドラッグ操作などは自前で複数のタッチイベントを処理する必要がある。こういったマルチタッチ操作を意味のあるジェスチャーとして扱うには個々のタッチイベントの状態を管理して、一連の操作がジェスチャーだとみなす為の条件をチェックを行う必要があるので複雑になりがちである。例えば2本指操作の場合、最初に1本目のタッチ、次に2本目のタッチ、続いて両方の指が一定以上移動したかどうかチェック、そこで初めて2本指ドラッグのジェスチャーとみなされる。このあたりの処理を LightTable は実にうまく対処している。
以下に LightTable のクラス図をまとめてみた。
タッチ系のイベントは NSResponder で受ける。通常はそのサブクラスである NSView なり NSWindow でこれを処理することになる。サンプルプログラム LightTable の場合もメインのビューでイベントを受け取っているのだが、実際の処理は NSResponder のサブクラスである InputTracker へ移譲して行わせている。
ユーザからのタッチイベントは最初に LTView に届けられる(図内①)。LTView はこれを InputTracker のサブクラス(ClickTracker, DragTracker, DualTouchTracker)のインスタンスへ転送する(図内②)。InputTracker は NSMutableArray* _inputTrackers に格納されているのでイベント受信時には配列内の InputTracker の当該メソッドをまとめて呼び出している。
@implementation LTView - (void)mouseDown:(NSEvent *)event { [_inputTrackers makeObjectsPerformSelector:_cmd withObject:event]; } - (void)mouseDragged:(NSEvent *)event { [_inputTrackers makeObjectsPerformSelector:_cmd withObject:event]; } - (void)mouseUp:(NSEvent *)event { [_inputTrackers makeObjectsPerformSelector:_cmd withObject:event]; } - (void)touchesBeganWithEvent:(NSEvent *)event { [_inputTrackers makeObjectsPerformSelector:_cmd withObject:event]; } - (void)touchesMovedWithEvent:(NSEvent *)event { [_inputTrackers makeObjectsPerformSelector:_cmd withObject:event]; } - (void)touchesEndedWithEvent:(NSEvent *)event { [_inputTrackers makeObjectsPerformSelector:_cmd withObject:event]; } - (void)touchesCancelledWithEvent:(NSEvent *)event { [_inputTrackers makeObjectsPerformSelector:_cmd withObject:event]; }上記コードは InputTrackerのインスタンスに _cmd 、すなわち現在処理中のメソッド名と同じ名前のセレクタをメッセージとして送る。例えば mouseDown: 内の [_inputTrackers makeObjectsPerformSelector:_cmd withObject:event] は配列内すべてのインスタンスに対して [(InputTrackerインスタンス) mouseDown:event] を実行するのと等価。
InputTracker の各サブクラス(のインスタンス)はそれぞれタッチイベントの状態を管理していて、一連のタッチイベントによってジェスチャーが構成されたタイミングで LTView の所定のメソッドをコールバックする(図内③)。例えば2本指のジェスチャーを管理している DualTouchTracker では touchesBeganWithEvent:、 touchesMovedWithEvent: の順番でイベントを受け取り、2本指でなおかつ移動量が一定のしきい値(threshold)を越えたタイミングで LTView の dualTouchesBegan: をコールバックしている。コールバックメソッドはプロパティの (SEL)beginTrackingAction で管理している。こうすることで低レベルなタッチイベントの管理は DualTouchTrackerに隠蔽され、LTView 側は目的の2本指によるジェスチャー(この場合はドラッグ)のみ必要な時にコールバックを受け取って処理を行うことができる。
このコードが優れている点はなんといっても一連のタッチイベントで構成されるジェスチャーという複雑な処理を InputTracker というクラスで隠蔽できるところ。利用側はジェスチャーが構成されたタイミングでコールバックを受けるだけでいい。さらに InputTracker は派生クラスを作ることで様々なジェスチャーに対応することができる。
また InputTracker の管理の仕方もよく出来ている。使う側は、あらかじめ使いたいジェスチャーに対応する InputTrackerの派生クラスのインスタンスを NSMutableArray へ詰めておき、NSResponder のタッチイベントを受け取ったらそれを配列内の複数の InputTracker へ転送するだけで良い。今後対応したいジェスチャーが増えた時は、必要なインスタンスを配列へ詰めておくだけで良い。一種のプラグイン的な構成で拡張性に優れている。
- - - -
これはサンプルだけでは勿体無いな。標準の NSresponder の拡張機能としてこのプラグイン構造をサポートしてくれると良かった。
...と書いていたら iOS の UIGestureRecognizer を思い出した。
UIGestureRecognizer Class Reference
UIGestureRecognizer は 3.2 から導入されていて複数タッチを意味のあるジェスチャーイベントとして処理する仕組みがある。これはまさに LightTable の考え方に近い仕組みでもう少し洗練させたものと考えていい。UIGestureRecognizer ではサブクラスが何種類か用意されていて標準的なジェスチャーの処理に利用できる。
UITapGestureRecognizer UIPinchGestureRecognizer UIRotationGestureRecognizer UISwipeGestureRecognizer UIPanGestureRecognizer UILongPressGestureRecognizer使い方は LightTable と同様に UIGestureRecognizer のインスタンスを作成し、コールバックメソッド(セレクタ)を渡しておく。そしてそのインスタンスを UIView へ渡すだけ。LightTable では内部に NSMutableArray を持って自前で InputTracker の管理を行っていたが UIView の場合はお任せで管理コードを書く必要がない。これはいい。Lion(10.7) には 是非 UIGestureRecognizer相当の仕組み(NSGestureRecognizer?)を導入して欲しいな。