前回の記事

[Android] 様々なジェスチャーを処理する(1) 拡大縮小

今回はスワイプによる移動処理を実装する。
単にこれを実装するだけならonTouch()の中でやってしまえばいいが、クラス化することで色んな場面で使いまわせるようになるだろう。

AndroidにはGestureDetectorというとても便利なクラスがあり、これを使えば前回のように簡単に移動処理を行うことが出来る。
なのに、自前で実装する意味は何か。
スワイプの開始と終了のタイミングが欲しいからだ。

例えば、図形をタッチして移動させたい場合はタッチ開始位置で当たり判定を行わなければならないので、開始をリスナーで通知するようにしてあると非常に便利だろう。
また、仕様をScaleGestureDetectorに合わせたいのもある。

それでは実装を開始しよう。
 
移動用のクラスの雛形を作成する。

まずはTranslationGestureListenerというリスナーを作成する。
public class TranslationGestureListener {
    public void onTranslation(TranslationGestureDetector detector)
    {
    }
    
    public void onTranslationBegin(TranslationGestureDetector detector)
    {
    }
    
    public void onTranslationEnd(TranslationGestureDetector detector)
    {
    }
}
これで完成だ。
インターフェースではなくクラスにしたのは、開始と終了が必要無い場合に使う側が余計なメソッドをオーバーライドしなくても良いようにするためだ。

次にTranslationGestureDetectorというクラスを作成する。
雛形はこのようになる。
public class TranslationGestureDetector {
    private TranslationGestureListener mListener;
    private float mX, mY; // タッチイベント時の座標
    private int mPointerID1, mPointerID2; // ポインタID記憶用
    
    public TranslationGestureDetector(TranslationGestureListener listener)
    {
        mListener = listener;
    }

    /**
     * タッチ処理
     */
    public boolean onTouch(View v, 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;
            break;
            
        case MotionEvent.ACTION_POINTER_DOWN:
            // 3本目の指以降は無視する
            if (mPointerID2 == -1)
            {
                mPointerID2 = pointerId;
            }
            break;
            
        case MotionEvent.ACTION_POINTER_UP:
            if (mPointerID1 == pointerId)
            {
                mPointerID1 = -1;
            }
            else if (mPointerID2 == pointerId)
            {
                mPointerID2 = -1;
            }
            break;

        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            mPointerID1 = -1;
            mPointerID2 = -1;
            break;
            
        case MotionEvent.ACTION_MOVE:
            break;
        }
        
        return true;
    }
}
この段階の処理の詳細は以前の記事で紹介した。

[Android] マルチタッチ処理を理解する

この段階のものを前回作成したMainActivityにも取り入れておこう。
まずMySurfaceViewに以下のインスタンス変数を追加する。
private float mTranslateX, mTranslateY;
private TranslationGestureDetector mTranslationGestureDetector;
 
次に結果を受け取るリスナーの実装を追加する。
private TranslationGestureListener mTranslationListener
= new TranslationGestureListener() {
    @Override
    public void onTranslationEnd(TranslationGestureDetector detector) {
    }
    
    @Override
    public void onTranslationBegin(TranslationGestureDetector detector) {
    }
    
    @Override
    public void onTranslation(TranslationGestureDetector detector) {
    }
};
3つのメソッドは好きなものをオーバーライドすればよい。
(まずあり得ないが)一つもオーバーライドしなくても問題ない。

次にMySurfaceViewのコンストラクタのScaleGestureDetectorのインスタンスを作成している下に以下のコードを追加する。
mTranslationGestureDetector = new TranslationGestureDetector(mTranslationListener);

最後にMySurfaceViewのonTouch()の先頭にジェスチャーの処理を行うための呼び出しを追加する。
これもScaleGestureDetectorの場合と同じだ。
mTranslationGestureDetector.onTouch(v, event);

これで準備は完了だ。

仕様を決めておく
実装を開始する前にタッチに関する仕様を決めておこう。
  • 移動の対象となるのは最初にタッチした指のみ
  • 複数の指でタッチしている場合は移動の操作を行わない
  • 最初にタッチした指を離すと、他の指がまだタッチしていても移動は終了とする
あくまでも最初にタッチした指を中心として処理を行うということにする。

移動処理を実装する
それでは実装を開始しよう。
まずは移動開始の処理だ。
TranslationGestureDetectorのonTouch()のACTION_DOWNの最後に以下のコードを追加しよう。
mX = x;
mY = y;
mListener.onTranslationBegin(this);
タッチした座標を保持しておき、リスナーを呼び出している。
これで開始処理は完了だ。

ただし、これだけではリスナー側で開始位置を特定できないので、以下のメソッドを追加しよう。
public float getX()
{
    return mX;
}

public float getY()
{
    return mY;
}
これでリスナー側でタッチされた座標を取得することが出来るようになった。
ここでは省略するが、MySurfaceViewのTranslationGestureListenerの実装内のonTranslationBegin()にログを出力するコードを追加して実際に動作させてみて欲しい。

次に終了処理を実装する。
ACTION_UPとACTION_CANCELの箇所に以下のコードを追加する。
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
    if (mPointerID1 != -1)
    {
        mX = x;
        mY = y;
        mListener.onTranslationEnd(this);
    }
    mPointerID1 = -1;
    mPointerID2 = -1;
    break;
一つ目の指が離れた時にonTranslationEnd()を呼び出すようにしている。
わざわざmPointerID1の値をチェックしているのは、二本指でタッチしていて、先に一本目の指を離して次に二本目の指を離した場合、このイベントが発生するのは二本目の指に関してだからだ。
もちろん座標も二本目の指を離した座標になる。

この場合、一本目の指を離した段階で移動終了とする仕様なので、ACTION_POINTER_UPにも同様の処理を入れておこう。
case MotionEvent.ACTION_POINTER_UP:
    if (mPointerID1 == pointerId)
    {
        mPointerID1 = -1;
        mX = x;
        mY = y;
        mListener.onTranslationEnd(this);
    }
これで終了処理は完成だ。
次は移動中の処理を実装する。
ACTION_MOVEに以下のコードを追加しよう。
if (mPointerID1 >= 0 && mPointerID2 == -1)
{
    int ptrIndex = event.findPointerIndex(mPointerID1);
    mX = event.getX(ptrIndex);
    mY = event.getY(ptrIndex);
    
    mListener.onTranslation(this);
}
最初の指だけが動いている状態でのみ処理するように判定を行っている。
また、ポインタインデックスは明確にmPointerID1から取得するようにしよう。
最初の指だけが動いているということは、onTouch()の頭で宣言したpointerIndexは最初の指に対応しているはずだが、念には念を入れてというやつだ。

次に今のコードの下に以下のコードを追加しよう。
if (mPointerID1 >= 0)
{
    int ptrIndex = event.findPointerIndex(mPointerID1);
    mX = event.getX(ptrIndex);
    mY = event.getY(ptrIndex);
}
最初にタッチした指の座標を更新している。
これでTranslationGestureDetectorは完成だ。
次はMySurfaceViewに画像を移動させる処理を入れてみよう。

画像を移動する処理の実装
移動の仕様としては、前回onTranslation()が呼ばれた時からの差分だけ移動させるようにしたい。
従って、前の座標を保持するために以下のインスタンス変数をMySurfaceViewに追加しよう。
private float mPrevX, mPrevY;

次にonTranslationBegin()でこのインスタンス変数を初期化する。
@Override
public void onTranslationBegin(TranslationGestureDetector detector) {
    mPrevX = detector.getX();
    mPrevY = detector.getY();
}
これで準備は完了だ。
次はonTranslation()に以下のコードを追加しよう。
float deltaX = detector.getX() - mPrevX;
float deltaY = detector.getY() - mPrevY;
mTranslateX += deltaX;
mTranslateY += deltaY;
mPrevX = detector.getX();
mPrevY = detector.getY();
deltaXとdeltaYは前の座標との差分だ。
これらをmTranslateXとmTranslateYに足してやる。
最後にmPrevXとmPrevYを更新すれば処理は完了だ。

mTranslateXとmTranslateYのMatrix処理は前回の記事内で実装してあるので、今回特に追加することは無い。

それでは実行してみよう。
期待通りの結果を得ることが出来た!
gesture_ss5
次回はこのシリーズ最大のヤマ場となる二本指での回転を攻略する。

関連記事
[Android] 様々なジェスチャーを処理する(1) 拡大縮小
[Android] 様々なジェスチャーを処理する(3) 回転