[Android]ドラッグ、ピンチイン/アウトできるMatrixImageView ver0.1

Androidでちょっとやりたいことがあって、拡大した画像を自由にスクロール表示させる必要があった。
で、そういうViewがあるのだと思って探していたら、意外にも見つからない。

よくピンチイン/アウトしたりドラッグしたりする画像ビューアあるけど、あれみんな自力で実装してるということ?
そんなバカな、と思ったけど探しても見つからないし、とりあえず必要最低限のものはすぐできそうだったので作ってみた。

こちらのプログラムをかなり参考に、ベースにさせていただきました。
利用イメージはこんな感じ。

使い方は以下のようになります。
最後に記載しているMatrixImageView.javaのソースコードを貼っつけて、xmlファイル内でImageViewと同じように使用します。(もちろんソースコード内で直接MatrixImageViewを扱ってもOK)

1
2
3
4
5
6
7
8
9
    <jp.obanet.android.MatrixImageView android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/img_view"
            android:src="@drawable/sample"
 
            interval="20"
            inertial="true"
            speedDecRatio="0.85"
            angleSpeedDecRatio="0.85" />

上4行はImageViewに指定するものと全く同じ。画像ファイルの指定や表示方法など。
下4行がMatrixImageView独自のパラメータです。省略したらデフォルト値が使用されます。

パラメータ名 意味 タイプ デフォルト値
inertial (指を離した後)慣性動作させるかどうか boolean true
interval 慣性のアニメーションの更新間隔(ミリ秒)。小さい値ほど綺麗に動いて見えます。もちろんその分処理コストはかかります。 int 20
speedDecRatio ドラッグ移動の慣性の減衰率。つまり摩擦。1に近いほどツルツルすべる。氷に近いイメージ。 float 0.85
angleSpeedDecRatio 回転の慣性の減衰率。同じく1に近いほどツルツル回る。 float 0.85

今後の改良点としては以下を一応考えてますが、僕の目的は現状でも十分達せられるため、実装するかどうかはわかりません。

  • 画像が外に飛び出していかないようにする機能
  • 内部の処理効率化、リファクタリング
  • その他Androidアプリ一般に必要なことの考慮(今回が初めてのAndroid開発なので色々足りないことがあると思ってる)

ちなみにMatrixImageViewにはインナークラスLineがあり、これは以前ActionScript用に作成したもの([ActionScript]2直線の交点を求める)を移植、取り入れました。
直線同士の交点を出すためのものだったのですが、今回の必要に応じて2直線の角度を算出する機能をつけました。ここで配布するのに便利なようにインナークラスとして使っています。

ソースコードは続きに記載します。
利用、改変に関しては、僕が勝手に作った「猿ロッキングライセンス」を適用します。
一番最後の猿ロッキングライセンスに関する注意事項をよく読んでください。

MatrixImageView.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
package jp.obanet.android;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.FloatMath;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.ImageView;
public class MatrixImageView extends ImageView implements OnTouchListener{
//繰り返し処理関連
private int interval = 20;
private Handler handler = new Handler();
private Runnable runnable;
// 移動、回転、ズーム用の変換行列
private Matrix matrix      = new Matrix();
//ズーム関連
private float oldDist      = 0f;
private PointF mid         = new PointF();
private float curRatio     = 1f;
//慣性移動に関するもの
private boolean inertial = true;
private float speedDecRatio = 0.8f;
private float angleSpeedDecRatio = 0.8f;
private PointF previous =new PointF();
private PointF speed = new PointF();
private float angleSpeed = 0;
//回転に関するもの
private Line previousLine;
//モード判別(NONE: 未操作状態, ONE_POINT: ドラッグ中, TWO_POINT: 拡大縮小、回転中)
enum Mode{
NONE,
ONE_POINT,
TWO_POINT
}
private Mode mode = Mode.NONE;
public MatrixImageView(Context context) {
this(context, null);
}
public MatrixImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MatrixImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView(context, attrs, defStyle);
}
private void initView(Context context, AttributeSet attrs, int defStyle){
//ScaleTypeは必ずMATRIXを設定
setScaleType(ScaleType.MATRIX);
if( attrs != null){
interval = attrs.getAttributeIntValue(null, "interval", 20);
speedDecRatio = attrs.getAttributeFloatValue(null, "speedDecRatio", 0.8f);
angleSpeedDecRatio = attrs.getAttributeFloatValue(null, "angleSpeedDecRatio", 0.8f);
inertial = attrs.getAttributeBooleanValue(null, "inertial", true);
}
setOnTouchListener(this);
if(inertial){
runnable = new Runnable() {
@Override
public void run() {
redraw();
handler.postDelayed(this, interval);
}
};
handler.postDelayed(runnable, interval);
}
}
/**
* 慣性効果の処理
*/
private void redraw(){
if(mode == Mode.NONE){
//移動、回転の慣性反映
previous.set(previous.x + speed.x, previous.y + speed.y);
matrix.postTranslate(speed.x, speed.y);
matrix.postRotate(angleSpeed, previous.x, previous.y);
//以下、次回の慣性のための移動スピード、回転スピード軽減処理
//移動による慣性
speed.set(speed.x * speedDecRatio, speed.y * speedDecRatio);
//慣性の移動量が十分に小さくなったときは停止
if(-1 &lt; speed.x &amp;&amp; speed.x &lt; 1){
speed.x = 0;
}
if(-1 &lt; speed.y &amp;&amp; speed.y &lt; 1){
speed.y = 0;
}
//回転による慣性
angleSpeed = angleSpeed * angleSpeedDecRatio;
//慣性による回転量が十分に小さくなったときは停止
if(-1 &lt; angleSpeed &amp;&amp; angleSpeed &lt; 1){
angleSpeed = 0;
}
setImageMatrix(matrix);
}
}
@Override
public boolean onTouch(View v, MotionEvent event) {
ImageView view = (ImageView)v;
switch(event.getAction() &amp; MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
//現在の慣性をリセット
speed.set(0, 0);
angleSpeed = 0;
//移動(ズームは無し)開始
previous.set(event.getX(), event.getY());
mode = Mode.ONE_POINT;
break;
case MotionEvent.ACTION_POINTER_DOWN:
//移動・回転・ズーム開始
previous.set(event.getX(), event.getY());
oldDist = spacing(event);
// Android のポジション誤検知を無視
if (oldDist &gt; 10f) {
midPoint(previous, event);
mode = Mode.TWO_POINT;
previousLine = new Line(
new PointF(event.getX(0), event.getY(0)),
new PointF(event.getX(1), event.getY(1))
);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
mode = Mode.NONE;
break;
case MotionEvent.ACTION_MOVE:
PointF current = null;
if (mode == Mode.ONE_POINT) {
current = new PointF(event.getX(), event.getY());
}else if (mode == Mode.TWO_POINT) {
current = new PointF();
midPoint(current, event);
}else{
return false;
}
//移動処理
float distanceX = current.x - previous.x;
float distanceY = current.y - previous.y;
matrix.postTranslate(distanceX, distanceY);
if (mode == Mode.TWO_POINT) {
//ズーム処理
float newDist = spacing(event);
midPoint(mid, event);
float scale = newDist / oldDist;
float tempRatio = curRatio * scale;
oldDist = newDist;
//倍率が上限値下限値の範囲外なら補正する
curRatio = Math.min(Math.max(0.1f, curRatio), 20f);
if (0.1f &lt; tempRatio &amp;&amp; tempRatio &lt; 20f) {
curRatio = tempRatio;
matrix.postScale(scale, scale, mid.x, mid.y);
}
//回転処理
Line line = new Line(
new PointF(event.getX(0), event.getY(0)),
new PointF(event.getX(1), event.getY(1))
);
float angle = (float) (previousLine.getAngle(line) * 180 / Math.PI);
matrix.postRotate(angle, current.x, current.y);
//次回の準備
angleSpeed = angle;
previousLine = line;
}
//次回の準備
speed = new PointF(current.x - previous.x, current.y - previous.y);
previous.set(current.x, current.y);
}
// 変換の実行
view.setImageMatrix(matrix);
return true; // イベントがハンドリングされたことを示す
}
/**
* 2点間の距離を計算
*/
private float spacing(MotionEvent event) {
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return FloatMath.sqrt(x * x + y * y);
}
/**
* 2点間の中間点を計算
*/
private void midPoint(PointF point, MotionEvent event) {
float x = event.getX(0) + event.getX(1);
float y = event.getY(0) + event.getY(1);
point.set(x / 2, y / 2);
}
static class Line{
enum LineType{
/** 第1引数の点と第2引数の点を通る直線(終端はない) */
STRAIGHT,
/** 第1引数の点から第2引数のほうに伸びる半直線 */
HALF,
/** 第1引数の点と第2引数の点の間の線分 */
SEGMENT
}
public PointF p1;
public PointF p2;
public LineType type;
/**
* @param p1
* @param p2
*/
public Line(PointF p1, PointF p2){
this(p1, p2, null);
}
/**
* @param p1
* @param p2
* @param type
*/
public Line(PointF p1, PointF p2, LineType type){
this.p1 = p1;
this.p2 = p2;
this.type = (type == null) ? LineType.STRAIGHT : type;
}
/**
* 2つのLineインスタンスの交点を表わすPointインスタンスを取得する
* 交点がない場合はnullを返す
* @param line
* @return
*
*/
public PointF getIntersectionPoint(Line line){
PointF vector1 = this.getVector();
PointF vector2 = line.getVector();
if(cross(vector1, vector2) == 0.0){
//2直線が並行の場合はnullを返す
return null;
}
// 交点を this.p1 + s * vector1 としたとき
float s = cross(vector2, subtract(line.p1, this.p1)) / cross(vector2, vector1);
// 交点を line.p1 + t * vector2 としたとき
float t = cross(vector1, subtract(this.p1, line.p1)) / cross(vector1, vector2);
if(this.validateIntersect(s) &amp;&amp; line.validateIntersect(t)){
vector1.x *= s;
vector1.y *= s;
this.p1.set(p1.x + vector1.x, p1.y + vector1.y);
return p1;
}else{
return null;
}
}
/**
* 2つのLineインスタンスが作る角度のラジアン値を返す
* @param line
* @return
*/
public float getAngle(Line line){
PointF vector1 = this.getVector();
PointF vector2 = line.getVector();
return (float)Math.atan2(vector1.x * vector2.y - vector1.y * vector2.x, vector1.x * vector2.x + vector1.y * vector2.y);
}
public PointF getVector(){
return new PointF(p2.x - p1.x, p2.y - p1.y);
}
public PointF subtract(PointF p1, PointF p2){
return new PointF(p1.x - p2.x, p1.y - p2.y);
}
/**
* 交点までのベクトルを p1 + n * (p2 - p1) であらわしたとき、
* nが適切な値の範囲内かどうかを判定する。
*
* 直線の場合:nはどの値でもよい
* 半直線の場合:nは0以上である必要がある
* 線分の場合:nは0以上1以下である必要がある
* @param n
* @return
*
*/
private boolean validateIntersect(float n){
if(LineType.HALF.equals(this.type)){
return (0 &lt;= n);
}else if(LineType.SEGMENT.equals(this.type)){
return ((0 &lt;= n) &amp;&amp; (n &lt;= 1));
}else{
return true;
}
}
/**
* 2つの2次元ベクトルの外積を返す
* @param vector1 2次ベクトルを表わすPointインスタンス
* @param vector2 2次ベクトルを表わすPointインスタンス
* @return
*
*/
private float cross(PointF vector1, PointF vector2){
return (vector1.x * vector2.y - vector1.y * vector2.x);
}
public String toString(){
String str = "";
if(LineType.STRAIGHT.equals(type)){
str += "---&gt; ";
}
str += "(" + p1.x + ", " + p1.y + ") ---&gt; (" + p2.x + ", " + p2.y + ")";
if(LineType.STRAIGHT.equals(type) || LineType.HALF.equals(type)){
str += " ---&gt;";
}
return str;
}
}
}

猿ロッキングライセンスとは

以下の動画を再生、閲覧し猿ロッキングキャンペーンへの貢献をしてくれたらプログラムの利用、改変すべて自由となるライセンスです。
(2011年6月4日(土)以降は再生せずとも自由に利用できます)

100万円ほしいっ!!
(猿ロッキングについてはこちら

Leave a Comment