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 onEventStart ※onEventEndまでに以下のイベントが1回または複数回連続発生
- void onTouchEnd
- void onTouchBefore ※イベントに関係なく常に発生
ハマりポイント
- クリックが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」を作ってみて欲しい。
始めに参考にし、紆余曲折を経て、結果的に似た実装となったページ。
私の実装は340行ではなく、OnTouchListener400行、ImageVIew400行の計800行であった。才能の差?
あと、onDrawをオーバーライドしてないのになんか動くし、意外と実装方法が違うのかも。