365連休

にわかのandroidとかの開発メモ。

ピンチ操作による拡大縮小やドラッグ移動ができるImageViewの実装方法(Android Studio)

ScaleGestureDetectorを使ってスケール操作を検出できるらしいが、どっかのブログでAndroid 4.?以下は対応してないとかって書いてあった気がした。特定のイベントが発生しないらしい。※4.2か4.3だった思う。

本当に対応していないか真偽は不明。

あとドラッグ操作も苦手らしい。

 

 

そこで、自分で作ることにした。

まずFrameLayoutを継承したカスタムビューを考えた。

  • MyLayout extends FrameLyaout
    • ImageView

FrameLayoutのonTouchで、内部のImageViewの座標やスケールを変えるという作戦。

 

作っている途中で、ピンチ操作やドラッグ移動を検出するためのonTouchのオーバーライドがほぼ全てなことに気づき、onTouchListenerの実装クラスとすれば、他のViewにも適用出来て幸せになれることに気づいた。

  • OnTouchAndPinchListener implements View.OnTouchListener
    • void onTouchBefore ※イベントに関係なく常に発生
      • void onEventStart ※onEventEndまでに以下のイベントが1回または複数回連続発生
        • void onClick
        • void onDoubleClick
        • void onPinch
        • void onMove ※onDragでもよい
      • void onEventEnd
    • void onTouchEnd

 

 ハマりポイント

  • クリックが2回なのかダブルクリックなのかの閾値やTimerで判定するところ。
  • onMoveの感度が良すぎるため、はじめの数回はACTION_MOVEの無視が必要。
  • 2点同時タップをしたときに、ACTION_DOWNだったりACTION_POINTER_DOWNだったり定まらないこと。

あと、内部状態が複雑なのでステートマシンを導入した。

 

 

 

次はカスタムFrameLayoutだが、FrameLayoutにImageViewを含め操作するよりも、

ImageViewを継承し、Matrixで描画を変更した方が、スマートだと思い、ImageVIewを継承したカスタムImageViewを作ることにした。

 

ImageViewに前述のOnTouchListenerを設定し、拡大縮小やドラッグ移動ができるカスタムImageViewを作る。

MyImageView extends ImageView

コンストラクタ内でsetOnTouchListener( new OnTouchAndPinchListener )といった具合にして、onMoveとonPinchに反応して、MatrixにpostTranslateやpostScaleを行い、onTouchEndでImageView#setImageMatrixからのView#invalidateである。

 

このページの閲覧者はコード掲載を望んでいるだろうが、まだ調整中な事と、苦労した出来立てほやほやのコードを公開するのが惜しいため、またの機会とする。

 

ImageViewでのハマりポイントはMatrixである。

Matrixは直訳すると行列で、このクラスの機能はBitmapに適用したい演算を設定するものである。

ImageViewはsetScaleType(ScaleType.MATRIX)の場合、メンバ変数のDrawableに対してMatrixを適用した結果を描画する。Drawable自体が変化するものではないので勘違いしないように。

 

演算には基準座標の移動(Translate)、拡大(Scale)などがあり、それぞれset系、pre系、post系のメソッドがある。

Matrixは双方向操作ができるリスト構造に似ている。

  • set系は、リストのクリアと、絶対値による指定で演算を追加する。
  • pre系は、リストの先頭に相対値による指定で演算を追加する。
  • post系は、リストの最後尾に相対値による指定で演算を追加する。

相対値とあるが、移動は座標オフセットでわかりやすいが、

拡大は前回の倍率を何倍するかという意味の相対値であり、倍率オフセットではないので注意。え?そんなの勘違いしない?(;'∀')

 

なお、set系以外で全ての演算をクリアするには、Matrix#resetを呼べばよい。

今回のカスタムImageViewを作る際には、統一してpost系のみを使った。

 

Matrixの現在値の取得はgetValuesにfloat型配列を渡し、内部状態を直接転記してもらう。

ユーザ操作で無限に移動や拡大ができるとまずいので、上限・下限を設定するのだが、この際に最新のgetValuesを使わないとおかしなことになる。

つまり、演算の追加・変更がされるたびにgetValuesを行う必要がある。

 

そこで、この煩わしさの解決に、Matrixの値をfloat型配列ではなく構造体として保持するMatrixValueクラスと、post操作に連動して変わる内部値を保持するMatrixのラッパーを作った。

class MyMatrix {
    Matrix innerMatrix;
    MatrixValues value;
    MyMatrix(){
        innerMatrix = new Matrix();
    }
    void postHoge(){ //必要なラッパーを実装する
        innerMatrix.postHoge();
        value = new MatrixValues(innerMatrix);
    }
}
class MatrixValues {
    final float transX;
    final float transY;
    final float scaleX;
    final float scaleY;
    final float skewX;
    final float skewY;
    final float persp0;
    final float persp1;
    final float persp2;
    MatrixValues(Matrix matrix){
        float[] values = new float[9]; //キャッシュ
        matrix.getValues(values);
        transX = values[Matrix.MTRANS_X];
        transY = values[Matrix.MTRANS_Y];
        scaleX = values[Matrix.MSCALE_X];
        scaleY = values[Matrix.MSCALE_Y];
        skewX = values[Matrix.MSKEW_X];
        skewY = values[Matrix.MSKEW_Y];
        persp0 = values[Matrix.MPERSP_0];
        persp1 = values[Matrix.MPERSP_1];
        persp2 = values[Matrix.MPERSP_2];
    }
}

実際にはMatrixの値がDrawableに適用された結果、各種座標がどのように変化したかを知る必要があるため、計算結果構造体クラスを作り、MyMatrixのメンバ変数のMatrixValueと置き換えて実装した。

さらにpost系ラッパーもデフォルト以外に、ImageViewの中央に移動したり、絶対値でpostするものも実装した。

 

 

 

思いつくままだらだらと書いたが、上記を参考に自身で「拡大縮小やドラッグ移動ができるカスタムImageView」を作ってみて欲しい。

 

 

始めに参考にし、紆余曲折を経て、結果的に似た実装となったページ。

sukohi.blogspot.com

私の実装は340行ではなく、OnTouchListener400行、ImageVIew400行の計800行であった。才能の差?

あと、onDrawをオーバーライドしてないのになんか動くし、意外と実装方法が違うのかも。