ジェスチャーシリーズの最後となるのは二本指を使った回転の実装を行う。
前回までの記事
[Android] 様々なジェスチャーを処理する(1) 拡大縮小
[Android] 様々なジェスチャーを処理する(2) 移動
二本指での回転は、一見するととても難易度が高いように思える。
しかし、ある数学の考え方を使えば驚くほど簡単に実現できる。
今回の実装を行うにあたり、ここを参考にさせていただいた。
Android Two finger rotation - Stack Overflow
前回までの記事
[Android] 様々なジェスチャーを処理する(1) 拡大縮小
[Android] 様々なジェスチャーを処理する(2) 移動
二本指での回転は、一見するととても難易度が高いように思える。
しかし、ある数学の考え方を使えば驚くほど簡単に実現できる。
今回の実装を行うにあたり、ここを参考にさせていただいた。
Android Two finger rotation - Stack Overflow
回転を検知する考え方
まずは、どうやって回転を検知するのか考えてみよう。
二本指を回転させた場合このような動きになる。
A1とB1は回転を開始するときの指の位置、A2とB2は回転のために動かした指の位置となる。
この時の角度αを求めることが出来れば後はMatrixで処理するだけで出来そうだ。
ではこの角度αはどうやって求めれば良いのか。
A1B1とA2B2を三角形として考えてみよう。
このように考え、β1とβ2の値がわかればその差であるαを求めることが出来る。
ではどうすればβ1とβ2を求めることが出来るのか。
その答えはアークタンジェントだ。
JavaではMathクラスにatan2()というメソッドがあり、これを使えば魔法のようにβ1とβ2を求めることが出来る。
詳細な理屈はここでは説明しないが、以下の様な三角形があったとき
Math.atan2(y, x)
とすればθが返される。
それでは早速実装を行ってみよう。
回転ジェスチャーの実装
実装の流れは前回行った移動と同じだ。
まずは雛形を作成する。
回転のジェスチャーを受け取るためのリスナーはこうなる。
次はジェスチャー本体の雛形だ。
まずはACTION_POINTER_DOWNを見てみよう。
ここは当然二本目以降の指がタッチした場合に呼ばれる。
最初の指と二本目の指との場合で、ポインタIDと座標を保持しておき、その上でもう一つのポインタIDがある(つまりもう一つの指がタッチしている)場合に回転開始とする。
次はACTION_POINTER_UPだ。
タッチしている二本の指のうち、どちらかが離されたら回転は終了ということにする。
対となるポインタIDを-1かどうかチェックしているのは、mListener.onRotationEnd()が二回呼ばれないようにするためだ。
次はACTION_MOVEを実装してみよう。
以下のコードをACTION_MOVEに追加する。
順に見ていこう。
1.それぞれの指に対応する座標を取得する。
最初に示した図のA2とB2だ。
2.タッチしている二本の指の座標から角度を計算する。
angle1は2番めに示した図のβ2、angle2はβ1だ。
calcAngle()は後ほど実装する。
3.二つの角度の差を求めることで今回私達が欲しい角度が手に入る。
ただし、角度は-180~180に収まるように補正しておく。
4.今回使用した座標は次にACTION_MOVEが呼び出された際に必要なので保持しておく。
5.mFocusXとmFocusYはタッチしている指の中心の座標だ。
図形を回転させるときの当たり判定等で使うことが想定される。
calcCenter()は後ほど実装する。
6.最後にリスナーを呼び出して完了だ。
次は未実装だった2つのメソッドを実装しよう。
まずはcalcAngle()だ。
先ほど説明したように、Math.atan2()を使用しているだけだ。
ただし、このメソッドで返される値はラジアンなので、角度に変換するようにしてある。
次はcalcCenter()だ。
さてこれでロジックは出揃った。
後は呼び出し側で使用するための値を返すメソッドを作成しよう。
動作を確認するために、次はMainActivityに必要な処理を追加しよう。
とはいえ、ここからの処理は移動の場合とほぼ同じなので、難しいことは無い。
画像を回転する処理の実装
まずMySurfaceViewに以下のインスタンス変数を追加する。
次はMySurfaceViewのコンストラクタのTranslationGestureDetectorの初期化の下に以下のコードを追加しよう。
次は上記の引数で渡してある回転のリスナーを作成する。
以下のコードをMySurfaceViewに追加しよう。
これをMatrixで処理してやれば回転が行われるはずだ。
present()メソッドのMatrixの処理の箇所に回転処理を追加しよう。
これが正しくないとおかしな挙動になるので注意して欲しい。
最後にonTouch()でジェスチャーの処理を行うようにしてやれば完成だ。
拡大縮小、移動、回転が自由自在に行えるようになった!
これで基本的なジェスチャーはほぼカバーできたはずだ。
これらとAndroid標準のGestureDetectorを組み合わせれば、おおよそのことには対応できるだろう。
今回完成したサンプルはGithubに上げてある。
chicketen / AndroidSample
まずは、どうやって回転を検知するのか考えてみよう。
二本指を回転させた場合このような動きになる。
A1とB1は回転を開始するときの指の位置、A2とB2は回転のために動かした指の位置となる。
この時の角度αを求めることが出来れば後はMatrixで処理するだけで出来そうだ。
ではこの角度αはどうやって求めれば良いのか。
A1B1とA2B2を三角形として考えてみよう。
このように考え、β1とβ2の値がわかればその差であるαを求めることが出来る。
ではどうすればβ1とβ2を求めることが出来るのか。
その答えはアークタンジェントだ。
JavaではMathクラスにatan2()というメソッドがあり、これを使えば魔法のようにβ1とβ2を求めることが出来る。
詳細な理屈はここでは説明しないが、以下の様な三角形があったとき
Math.atan2(y, x)
とすればθが返される。
それでは早速実装を行ってみよう。
回転ジェスチャーの実装
実装の流れは前回行った移動と同じだ。
まずは雛形を作成する。
回転のジェスチャーを受け取るためのリスナーはこうなる。
public class RotationGestureListener { public void onRotation(RotationGestureDetector detector) { } public void onRotationBegin(RotationGestureDetector detector) { } public void onRotationEnd(RotationGestureDetector detector) { } }移動の場合と形式は同じなので説明は不要だろう。
次はジェスチャー本体の雛形だ。
public class RotationGestureDetector { private RotationGestureListener mListener; private float mX1, mY1, mX2, mY2; private int mPointerID1, mPointerID2; private float mFocusX, mFocusY; private float mAngle; public RotationGestureDetector(RotationGestureListener listener) { mListener = listener; } public boolean onTouchEvent(MotionEvent event) { int eventAction = event.getActionMasked(); int pointerIndex = event.getActionIndex(); int pointerId = event.getPointerId(pointerIndex); float x = event.getX(pointerIndex); float y = event.getY(pointerIndex); switch (eventAction) { case MotionEvent.ACTION_DOWN: // 最初の指の設定 mPointerID1 = pointerId; mPointerID2 = -1; mX1 = x; mY1 = y; break; case MotionEvent.ACTION_POINTER_DOWN: // 3本目の指以降は無視する if (mPointerID1 == -1) { mPointerID1 = pointerId; mX1 = x; mY1 = y; if (mPointerID2 != -1) { int ptrIndex = event.findPointerIndex(mPointerID2); mX2 = event.getX(ptrIndex); mY2 = event.getY(ptrIndex); mListener.onRotationBegin(this); } } else if (mPointerID2 == -1) { mPointerID2 = pointerId; mX2 = x; mY2 = y; if (mPointerID1 != -1) { int ptrIndex = event.findPointerIndex(mPointerID1); mX1 = event.getX(ptrIndex); mY1 = event.getY(ptrIndex); mListener.onRotationBegin(this); } } break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: mPointerID1 = -1; mPointerID2 = -1; break; case MotionEvent.ACTION_POINTER_UP: if (mPointerID1 == pointerId) { mPointerID1 = -1; if (mPointerID2 != -1) { mListener.onRotationEnd(this); } } else if (mPointerID2 == pointerId) { mPointerID2 = -1; if (mPointerID1 != -1) { mListener.onRotationEnd(this); } } break; } return true; } }こちらもおおまかなところは移動の場合とほぼ同じだ。
まずはACTION_POINTER_DOWNを見てみよう。
ここは当然二本目以降の指がタッチした場合に呼ばれる。
最初の指と二本目の指との場合で、ポインタIDと座標を保持しておき、その上でもう一つのポインタIDがある(つまりもう一つの指がタッチしている)場合に回転開始とする。
次はACTION_POINTER_UPだ。
タッチしている二本の指のうち、どちらかが離されたら回転は終了ということにする。
対となるポインタIDを-1かどうかチェックしているのは、mListener.onRotationEnd()が二回呼ばれないようにするためだ。
次はACTION_MOVEを実装してみよう。
以下のコードをACTION_MOVEに追加する。
// 1 float x1 = event.getX(event.findPointerIndex(mPointerID1)); float y1 = event.getY(event.findPointerIndex(mPointerID1)); float x2 = event.getX(event.findPointerIndex(mPointerID2)); float y2 = event.getY(event.findPointerIndex(mPointerID2)); // 2 float angle1 = calcAngle(mX1, mY1, mX2, mY2); float angle2 = calcAngle(x1, y1, x2, y2); // 3 float deltaAngle = angle2 - angle1; if (deltaAngle < -180.0f) { deltaAngle += 360.0f; } else if (deltaAngle > 180.0f) { deltaAngle -= 360.0f; } mAngle = deltaAngle; mX2 = x2; mY2 = y2; mX1 = x1; mY1 = y1; // 4 mFocusX = calcCenter(x1, x2); mFocusY = calcCenter(y1, y2); // 5 mListener.onRotation(this);ここでは回転処理の全てを行っている。
順に見ていこう。
1.それぞれの指に対応する座標を取得する。
最初に示した図のA2とB2だ。
2.タッチしている二本の指の座標から角度を計算する。
angle1は2番めに示した図のβ2、angle2はβ1だ。
calcAngle()は後ほど実装する。
3.二つの角度の差を求めることで今回私達が欲しい角度が手に入る。
ただし、角度は-180~180に収まるように補正しておく。
4.今回使用した座標は次にACTION_MOVEが呼び出された際に必要なので保持しておく。
5.mFocusXとmFocusYはタッチしている指の中心の座標だ。
図形を回転させるときの当たり判定等で使うことが想定される。
calcCenter()は後ほど実装する。
6.最後にリスナーを呼び出して完了だ。
次は未実装だった2つのメソッドを実装しよう。
まずはcalcAngle()だ。
private float calcAngle(float x1, float y1, float x2, float y2) { return (float)Math.toDegrees(Math.atan2((y2 - y1), (x2 - x1))); }これが角度を導き出す魔法だ。
先ほど説明したように、Math.atan2()を使用しているだけだ。
ただし、このメソッドで返される値はラジアンなので、角度に変換するようにしてある。
次はcalcCenter()だ。
private float calcCenter(float p1, float p2) { return (p1 + p2) / 2; }これは単純に2つの値の中間値を計算しているだけだ。
さてこれでロジックは出揃った。
後は呼び出し側で使用するための値を返すメソッドを作成しよう。
public float getX1() { return mX1; } public float getY1() { return mY1; } public float getX2() { return mX2; } public float getY2() { return mY2; } public float getFocusX() { return mFocusX; } public float getFocusY() { return mFocusY; } public float getDeltaAngle() { return mAngle; }これでRotationGestureDetectorは完成だ。
動作を確認するために、次はMainActivityに必要な処理を追加しよう。
とはいえ、ここからの処理は移動の場合とほぼ同じなので、難しいことは無い。
画像を回転する処理の実装
まずMySurfaceViewに以下のインスタンス変数を追加する。
private float mAngle; private RotationGestureDetector mRotationGestureDetector;mAngleは回転角度を保持するためのもので、mRotationGestureDetectorは先ほど作成したジェスチャークラスのインスタンスだ。
次はMySurfaceViewのコンストラクタのTranslationGestureDetectorの初期化の下に以下のコードを追加しよう。
mRotationGestureDetector = new RotationGestureDetector(mRotationListener);これで準備は完了だ。
次は上記の引数で渡してある回転のリスナーを作成する。
以下のコードをMySurfaceViewに追加しよう。
private RotationGestureListener mRotationListener = new RotationGestureListener() { @Override public void onRotation(RotationGestureDetector detector) { mAngle += detector.getDeltaAngle(); } @Override public void onRotationBegin(RotationGestureDetector detector) { } @Override public void onRotationEnd(RotationGestureDetector detector) { } };onRotation()が呼ばれる度に発生した角度の差分をmAngleに足していく。
これをMatrixで処理してやれば回転が行われるはずだ。
present()メソッドのMatrixの処理の箇所に回転処理を追加しよう。
mMatrix.reset(); mMatrix.postScale(mScale, mScale); mMatrix.postTranslate(-mBitmap.getWidth() / 2 * mScale, -mBitmap.getHeight() / 2 * mScale); mMatrix.postRotate(mAngle); mMatrix.postTranslate(mTranslateX, mTranslateY);移動の時にも言及したが、このメソッドを呼び出す順番はとても重要だ。
これが正しくないとおかしな挙動になるので注意して欲しい。
最後にonTouch()でジェスチャーの処理を行うようにしてやれば完成だ。
public boolean onTouch(View v, MotionEvent event) { mRotationGestureDetector.onTouchEvent(event); mTranslationGestureDetector.onTouch(v, event); mScaleGestureDetector.onTouchEvent(event); present(); return true; }さあ、実行して操作してみよう。
拡大縮小、移動、回転が自由自在に行えるようになった!
これで基本的なジェスチャーはほぼカバーできたはずだ。
これらとAndroid標準のGestureDetectorを組み合わせれば、おおよそのことには対応できるだろう。
今回完成したサンプルはGithubに上げてある。
chicketen / AndroidSample
コメント