2011年10月31日月曜日

カスタムジェスチャーを作ろう!

iPhone アプリでジェスチャーを使うというと、特殊なアプリを想像しがちですが、メインで使うのではなく、ゲームの裏コマンド入力や、隠し機能、デバッグモードのオープンなどに積極的に使ってみるのはどうでしょうか。

オリジナルのジェスチャーを作るには、UIGestureRecognizer のサブクラスを作成して実装します。UIGestureRecognizer の詳細については、Apple の Event Handling Guide for iOS ドキュメント、またはこちらの日本語PDFをご覧ください。

UIGestureRecognizer は状態マシンとして機能する抽象クラスです。
ユーザーがこのクラスに関連付けられたビューをタッチすると、そのイベントがこのクラスにリダイレクトされます。そのタッチイベントを読み取って、ジェスチャーの進行中・失敗・成功の状態を遷移させるのがこのクラスの役目となります。

UIGestureRecognizer のサブクラスは、以下の5つのメソッドをオーバーライドします。

- (void)reset;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
reset 以外は、UIResponder のタッチメイベントそのままなので、馴染み深いでしょう。

ジェスチャーには大きく分けて、単発的なジェスチャーと、連続的なジェスチャーがあります。今回は、ジェスチャーの中でも比較的簡単な単発的なジェスチャーを扱います。
単発的なジェスチャーでは、タッチイベントの中でジェスチャーが確定したと判断できたら、state プロパティを UIGestureRecognizerStateRecognized に設定します。逆に失敗したと判断したら、UIGestureRecognizerStateFailed を設定します。
非常に簡単です。

公式ドキュメントには、チェックマーク(✔)ジェスチャーを実装する例がありますので、ここではもうちょっと複雑に以下のような三角旗ジェスチャーを作ってみました。

ヘッダファイルは以下のような変数を定義しておきます。
@interface FlagGesture : UIGestureRecognizer
{
 unsigned int actMode;  // ジェスチャー管理用ワーク
 CGPoint startPoint;      // ジェスチャー開始座標
 CGPoint topPoint;        // 三角旗の天井座標
}
touchesBegan:メソッドは以下のようになります。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
 [super touchesBegan:touches withEvent:event];
 if ([touches count]!=1) {
   // マルチタッチを感知したら失敗とする
  self.state = UIGestureRecognizerStateFailed;
  return;
 }
 // ジェスチャー開始座標格納
 startPoint = [[touches anyObject] locationInView:self.view];
}

touchesMoved:メソッドは以下のようになります。
actMode が 0 の時は、ジェスチャーが開始座標から旗の天井に向かうところを判定します。ここは垂直に上に伸びるジェスチャーなので、15°以上傾いたら失敗させるようにしています。
actMode が 1 の時は、ジェスチャーが旗の右端突端に向かうところを判定します。
actMode が 2 の時は、右端突端からジェスチャー開始地点まで戻るところを判定します。
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
 [super touchesMoved:touches withEvent:event];
 
 CGPoint nowPoint = [[touches anyObject] locationInView:self.view];
 CGPoint prevPoint = [[touches anyObject] previousLocationInView:self.view];
 
 switch (actMode) {
  case 0:  // 旗の天井に向かうアクション
   if (nowPoint.y<prevPoint.y) {
    float rot = atanf(fabsf(nowPoint.x-startPoint.x)/(startPoint.y-nowPoint.y));
    if (rot>=15) {
     // 15°以上傾いたら失敗
     self.state = UIGestureRecognizerStateFailed;
     return;
    }
   } else {
    if (nowPoint.x>prevPoint.x) {
     CGRect rect = self.view.bounds;
     // 垂直線が画面の高さの3分の1あるかを判定材料とする
     if ((startPoint.y-nowPoint.y)>=(rect.size.height*0.33f)) {
      // 旗の右端突端に向かうジェスチャーになったと仮定する
      actMode = 1;
      topPoint = nowPoint; // 旗の天井座標格納
      break;
     }
    }
    self.state = UIGestureRecognizerStateFailed;
   }
   break;
   
  case 1:  // 旗の右端突起に向かうアクション
   if ((nowPoint.x>=prevPoint.x)&&(nowPoint.y>=prevPoint.y)) {
    // OK
   } else if ((nowPoint.x<prevPoint.x)&&(nowPoint.y>=prevPoint.y)) {
    float dy = startPoint.y-topPoint.y;
    if (((topPoint.y+dy*0.35f)>nowPoint.y)||((startPoint.y-dy*0.35f)<nowPoint.y)) {
     // 突端のY座標が旗の垂直線の中央にないと失敗とする
     self.state = UIGestureRecognizerStateFailed;
     return;
    }
    actMode = 2;
   }
   break;
   
  case 2:  // 開始座標に戻るアクション
   if ((nowPoint.x<=prevPoint.x)&&(nowPoint.y>=prevPoint.y)) {
    // OK
   } else {
    self.state = UIGestureRecognizerStateFailed;
   }
   break;
 }
}

touchesEnded:メソッドは以下のようになります。
タッチが離れた時、actMode が 2 であり、且つ開始座標に近ければ、ジェスチャーが成功したとします。
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
 [super touchesEnded:touches withEvent:event];
 
 if ((self.state == UIGestureRecognizerStatePossible)&&(actMode==2)) {
  CGPoint nowPoint = [[touches anyObject] locationInView:self.view];
  CGPoint delta = CGPointMake(nowPoint.x-startPoint.x,nowPoint.y-startPoint.y);
  float k = delta.x*delta.x + delta.y*delta.y;
  if (k<(50*50)) {
   self.state = UIGestureRecognizerStateRecognized;
  }
 }
}

touchesCancelled:メソッドは以下のようになります。
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
 [super touchesCancelled:touches withEvent:event];
 self.state = UIGestureRecognizerStateFailed;
}

reset メソッドは以下のようになります。
ジェスチャーが失敗・成功後に自動的に呼ばれるメソッドです。管理用ワークを初期化しておきます。
- (void)reset
{
 [super reset];
 actMode = 0;
}

カスタムジェスチャークラスができたら、実際にビューに登録してみます。
以下の例では、ビューコントローラーの viewDidLoad メソッドから登録しています。 viewDidUnload メソッドでは、登録したジェスチャーを取り除いています。
ジェスチャーの登録・削除は、各アプリで必要な箇所に組み込む必要があるでしょう。
- (void)viewDidLoad
{
 [super viewDidLoad];
 FlagGesture *gesture = [[[FlagGesture alloc] initWithTarget:self action:@selector(handleGestureFlag:)] autorelease];
 [self.view addGestureRecognizer:gesture];
}

- (void)viewDidUnload
{
 [super viewDidUnload];
 NSEnumerator *e = [self.view.gestureRecognizers objectEnumerator];

 while (UIGestureRecognizer *gesture = (UIGestureRecognizer*)[e nextObject]) {
  [self.view removeGestureRecognizer:gesture];
 }
}

- (void)handleGestureFlag:(UIGestureRecognizer*)sender
{
 NSLog(@"FlagGesture OK! : state = %d",sender.state);
}

今回は、ちょっと長くなってしまいました。ぜひ、ご自分用の楽しいジェスチャーを作ってみて下さい。

0 件のコメント:

コメントを投稿

Related Posts Plugin for WordPress, Blogger...