Androidで二本指の回転ジェスチャーを検出する


考え方

下図のように、二つのベクトルの間の角度を計算することを考えます。

これは図から明らかなように二つのベクトルそれぞれのx軸からの角度の差です。
それではあるベクトルについてx軸からの角度を計算するにはどうすれば良いのでしょうか?
これは三角法の定義から簡単にわかります。

tan-1 という見慣れない式がでてきますが、これは tan の逆の計算をすることのできる関数です。arctan や atan と書かれることもあります。
この計算の詳細を知る必要はありません。なぜならMath.atan()というメソッドを呼び出すだけで計算することができるからです。
ただし、Math.atan() の引数が分数になっている場合により使いやすいMath.atan2()というメソッドがあるので、今回はこちらを使用します。
atan2の詳細はWikipediaで。

これで、あるベクトルについてのx軸からの角度は計算できました。
同じ計算をもう一つのベクトルでも行い、出てきた値の差を取ればそれがベクトル同士のなす角度になります。

ジェスチャーの実装をするときは、2本の指の片方からもう一方へ伸びるベクトルを考えます。これは2本の指それぞれのx,y座標の差を取ることで計算できます。
ベクトルが計算できたら、このベクトルがx軸となす角度を計算します(atan2を使う)。
そして、直前の角度の計算結果と現在の計算結果の差を取ります。
この差がジェスチャーによる回転角です。

実装

GestureDetector(Compat) や ScaleGestureDetector と同じように使える RotateGestureDetector を作ってみました。
実装にあたっては最初のイベントの時だけ直前の角度が存在しないので、その分の場合分けが必要になります。あとは上で説明したような考え方をそのままあてはめます。

上に出てきていない focus という変数がありますが、これは2本の指の中心点を表します。画面を描画する場合、この中心点を回転の中心とすると上手くいくことが多いと思います。

import android.content.Context;
import android.view.MotionEvent;

public class RotateGestureDetector {
  public static interface OnRotateListener {
    boolean onRotate(float degrees, float focusX, float focusY);
  }

  public static class SimpleOnRotateGestureDetector implements OnRotateListener {
    @Override
    public boolean onRotate(float degrees, float focusX, float focusY) {
      return false;
    }
  }

  private static float RADIAN_TO_DEGREES = (float) (180.0 / Math.PI);
  private OnRotateListener listener;
  private float prevX = 0.0f;
  private float prevY = 0.0f;
  private float prevTan;

  public RotateGestureDetector(OnRotateListener listener) {
    this.listener = listener;
  }

  public boolean onTouchEvent(MotionEvent event) {
    if (event.getPointerCount() == 2 && event.getActionMasked() == MotionEvent.ACTION_MOVE) {
      boolean result = true;
      float x = event.getX(1) - event.getX(0);
      float y = event.getY(1) - event.getY(0);
      float focusX = (event.getX(1) + event.getX(0)) * 0.5f;
      float focusY = (event.getY(1) + event.getY(0)) * 0.5f;
      float tan = (float) Math.atan2(y, x);

      if (prevX != 0.0f && prevY != 0.0f) {
        result = listener.onRotate((tan - prevTan) * RADIAN_TO_DEGREES, focusX, focusY);
      }

      prevX = x;
      prevY = y;
      prevTan = tan;
      return result;
    } else {
      prevX = prevY = prevTan = 0.0f;
      return true;
    }
  }
}

使い方

public class UserActivity extends Activity {
  private RotateGestureDetector rotateGestureDetector;

  protected void onCreate(Bundle savedInstanceState) {
    rotateGestureDetector = new RotateGestureDetector(new RotateGestureDetector.SimpleOnRotateGestureDetector() {
      @Override
      public boolean onRotate(float degrees, float focusX, float focusY) {
        // something with rotate
        return true;
      }
    });
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    rotateGestureDetector.onTouchEvent(event);
    return true;
  }
}