前回の記事でマルチタッチの処理を一通り理解することが出来た。
そこで今回から数回に渡り、それを応用して様々なジェスチャーを処理する方法を理解していく。

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

第1回目の今回は拡大縮小を行うサンプルを作成する。

ベースとなるActivityを作成する
まずは表示を行ったりタッチを取得するActivityを作成しよう。
public class MainActivity extends Activity {
    private MySurfaceView mSurfaceView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mSurfaceView = new MySurfaceView(getApplicationContext());
        setContentView(mSurfaceView);
    }

    class MySurfaceView extends SurfaceView
    implements SurfaceHolder.Callback
    {
        public MySurfaceView(Context context) {
            super(context);
            
            getHolder().addCallback(this);
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        }

        @Override
        public void surfaceCreated(SurfaceHolder holder) {
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
        }
        
    }
}
これについては特に解説は必要無いだろう。
空のSurfaceViewを設定してあるだけだ。
実行すると黒い画面が表示される。

画像を表示させる
拡大縮小をわかりやすく実感するには画像を使うのが良さそうだ。
そこで、このような画像を用意した。
test

まずはこの画像を表示させてみよう。

MySurfaceViewに画像を保持するインスタンス変数を追加する。
private Bitmap mBitmap;
次にコンストラクタのsuper()の後に画像を読み込む処理を追加する。
AssetManager manager = getAssets();
InputStream is = null;
try {
    is = manager.open("test.jpg");
    mBitmap = BitmapFactory.decodeStream(is);
} catch (Exception e) {
} finally {
    try {
        is.close();
    } catch (IOException e) {}
}
これで画像の準備は完了だ。
次はこの画像を表示させてみよう。
まずは以下の様なインスタンス変数をMySurfaceViewに追加する。
private SurfaceHolder mHolder;
SurfaceHolderは描画に必要で、そのインスタンスはsurfaceCreated()やsurfaceChanged()で渡される。
surfaceChanged()に以下の一文を追加しよう。
mHolder = holder;
次は描画を行う関数を作成する。
以下のメソッドをMySurfaceViewに追加しよう。
public void present()
{
    Canvas canvas = mHolder.lockCanvas();
    
    canvas.drawColor(Color.BLACK);
    canvas.drawBitmap(mBitmap, 0, 0, null);
    
    mHolder.unlockCanvasAndPost(canvas);
}
これも特に解説は必要無いだろう。
黒背景に画像を表示させるための処理を記述している。

それではsurfaceChanged()でこのメソッドを呼び出してみよう。
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    mHolder = holder;
    
    present();
}
これで画像を表示する単純なサンプルは完成だ。
実行するとこのように表示される。
gesture_ss1

画像を中央に表示させる
次は画像を中央に表示させてみよう。
今回行うのは拡大縮小だけだが、次回以降に行う移動と回転のことも考慮して、Matrixクラスを使うのが良さそうだ。
MySurfaceViewに次のインスタンス変数を追加しよう。
private Matrix mMatrix;
private float mTranslateX, mTranslateY;
mTranslateXとmTranslateYは画像の表示位置を保持するためのものだ。
これらに適切な値を設定してやるにはsurfaceChanged()で渡される幅と高さを使えば良い。
surfaceChanged()のpresent()呼び出しの上に以下のコードを追加する。
mTranslateX = width / 2;
mTranslateY = height / 2;
これで中央に表示する準備は出来た。
次はMatrixの設定だ。
MySurfaceViewのコンストラクタのSurfaceHohlderのコールバックの設定の上に以下のコードを追加する。
mMatrix = new Matrix();
次にpresent()の中を以下のようにMatrix設定のコードを追加し、画像の描画もMatrixを使ったものに変える。
Canvas canvas = mHolder.lockCanvas();

mMatrix.reset();
mMatrix.postTranslate(mTranslateX, mTranslateY);

canvas.drawColor(Color.BLACK);
canvas.drawBitmap(mBitmap, mMatrix, null);

mHolder.unlockCanvasAndPost(canvas);
実行するとこのように表示される。
gesture_ss2
中央から右下にズレてしまっている。
これは描画で指定した座標は画像の左上が開始位置になるためだ。

先ほど追加したMatrixのコードのリセットと移動の間に以下のように画像位置を画像サイズの半分ズラすコードを追加しよう。
mMatrix.reset();
mMatrix.postTranslate(-mBitmap.getWidth() / 2, -mBitmap.getHeight() / 2);
mMatrix.postTranslate(mTranslateX, mTranslateY);
実行するとこのように表示される。
gesture_ss3
期待通りの結果となった。
これで中央寄せは完成だ。

次はいよいよ拡大縮小に取り組もう。

画像を拡大縮小する
ここからが大変な気がするが、実は拡大縮小のためのクラスがAndroidには用意されている。
ScaleGestureDetectorというクラスだ。
これを使うととても簡単に拡大縮小を実装することが出来る。

MySurfaceViewに以下のようにインスタンス変数を追加しよう。
private float mScale;
private ScaleGestureDetector mScaleGestureDetector;
mScaleは拡大縮小率を保持しておくためのものだ。

次にMySurfaceViewのコンストラクタのmMatrixを初期化している下に以下のコードを追加する。
mScale = 1.0f;
mScaleGestureDetector = new ScaleGestureDetector(context, mOnScaleListener);
これでScaleGestureDetectorの準備は完了だ。
引数で渡しているmOnScaleListenerは拡大縮小の結果を受け取るためのリスナークラスのインスタンスで、この後に実装を行う。
mScaleにはデフォルトの拡大縮小率である1.0を入れておく。
これを忘れると大きさがゼロということになってしまって、画像は表示されない。

MySurfaceViewに以下のようにリスナーインスタンスを追加しよう。
private SimpleOnScaleGestureListener mOnScaleListener
= new ScaleGestureDetector.SimpleOnScaleGestureListener() {
    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        return super.onScaleBegin(detector);
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
        super.onScaleEnd(detector);
    }
    
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        mScale *= detector.getScaleFactor();
        return true;
    };
};
onScaleBegin()は拡大縮小が開始される(2本指でタッチする)と呼ばれる。
onScaleEnd()拡大縮小が終了する(2本指のどちらかを離す)と呼ばれる。
onScale()は拡大縮小が続いている間、何度も呼び出される。

なお、これら3つのメソッドはabstractではないので、省略は自由だ。
ここではonScaleBegin()とonScaleEnd()は使わないが、あえて記述しておいた。
onScale()の中に注目してみよう。
detector.getScaleFactor()というメソッドは前の状態からの拡大縮小率を返す。
よって、この値をmScaleに保持させておく。

次はpresent()メソッドの中に拡大縮小の処理を追加しよう。
mMatrix.reset();
mMatrix.postScale(mScale, mScale);
mMatrix.postTranslate(-mBitmap.getWidth() / 2 * mScale, -mBitmap.getHeight() / 2 * mScale);
mMatrix.postTranslate(mTranslateX, mTranslateY);
postScale()を追加する位置はとても重要だ。
これを間違うとおかしな位置を中心にして拡大縮小が実行されることになる。
また、画像半分ズラすための処理(3行)にもmScaleを適用する。
これを行わないと中央を中心に拡大縮小されない。

さあ、完成まであと一歩だ。
MySurfaceViewでタッチを取得できるようにOnTouchListenerを追加する。
class MySurfaceView extends SurfaceView
    implements SurfaceHolder.Callback, View.OnTouchListener {
次にMySurfaceViewのコンストラクタの一番下にタッチのリスナー設定を追加する。
setOnTouchListener(this);
そしていよいよ仕上げの処理だ。
onTouch()をMySurfaceViewに追加する。
public boolean onTouch(View v, MotionEvent event) {
    mScaleGestureDetector.onTouchEvent(event);
    
    present();
    return true;
}
mScaleGestureDetector.onTouchEvent()の呼び出しはここでしっかり行っておく必要がある。
また、拡大縮小が行われたら再描画も必要なのでpresent()の呼び出しも行っている。

これでコードは完成だ。
早速実行してみよう。
gesture_ss4
期待通り、ピンチイン・ピンチアウトで中央を中心に拡大縮小することが出来た!
これで今回やりたかったことは一通り終了だ。

次回は画像の移動を行っていく。

関連記事
[Android] 様々なジェスチャーを処理する(2) 移動
[Android] 様々なジェスチャーを処理する(3) 回転