とうとうこの時が来てしまいました(´・ω・`)イメージの移動制限の時間です(^^;)
兎にも角にもコードを載せます。最後のMatrix#postTranslateの辺りだけは以前載せたので見覚え有りますよね。
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 |
private final boolean processDrag(MotionEvent event) { float dx = event.getX() - mPrimaryX; float dy = event.getY() - mPrimaryY; // calculate the corner coordinates of image applied matrix // [(left,top),(right,top),(right,bottom),(left.bottom)] mTrans[0] = mTrans[6] = mImageRect.left; mTrans[1] = mTrans[3] = mImageRect.top; mTrans[5] = mTrans[7] = mImageRect.bottom; mTrans[2] = mTrans[4] = mImageRect.right; mImageMatrix.mapPoints(mTrans); for (int i = 0; i < 8; i += 2) { mTrans[i] += dx; mTrans[i+1] += dy; } // check whether the image can move // if we can ignore rotating, the limit check is more easy... boolean canMove // check whether at lease one corner of image bounds is in the limitRect = mLimitRect.contains(mTrans[0], mTrans[1]) || mLimitRect.contains(mTrans[2], mTrans[3]) || mLimitRect.contains(mTrans[4], mTrans[5]) || mLimitRect.contains(mTrans[6], mTrans[7]) // check whether at least one corner of limitRect is in the image bounds || ptInPoly(mLimitRect.left, mLimitRect.top, mTrans) || ptInPoly(mLimitRect.right, mLimitRect.top, mTrans) || ptInPoly(mLimitRect.right, mLimitRect.bottom, mTrans) || ptInPoly(mLimitRect.left, mLimitRect.bottom, mTrans); if (!canMove) { // when no corner is in, we need additional check whether at least // one side of image bounds intersect with the limit rectangle if (mLimitSegments[0] == null) { mLimitSegments[0] = new LineSegment(mLimitRect.left, mLimitRect.top, mLimitRect.right, mLimitRect.top); mLimitSegments[1] = new LineSegment(mLimitRect.right, mLimitRect.top, mLimitRect.right, mLimitRect.bottom); mLimitSegments[2] = new LineSegment(mLimitRect.right, mLimitRect.bottom, mLimitRect.left, mLimitRect.bottom); mLimitSegments[3] = new LineSegment(mLimitRect.left, mLimitRect.bottom, mLimitRect.left, mLimitRect.top); } final LineSegment side = new LineSegment(mTrans[0], mTrans[1], mTrans[2], mTrans[3]); canMove = checkIntersect(side, mLimitSegments); if (!canMove) { side.set(mTrans[2], mTrans[3], mTrans[4], mTrans[5]); canMove = checkIntersect(side, mLimitSegments); if (!canMove) { side.set(mTrans[4], mTrans[5], mTrans[6], mTrans[7]); canMove = checkIntersect(side, mLimitSegments); if (!canMove) { side.set(mTrans[6], mTrans[7], mTrans[0], mTrans[1]); canMove = checkIntersect(side, mLimitSegments); } } } } if (canMove) { // TODO we need adjust dx/dy not to penetrate into the limit rectangle // otherwise the image can not move when one side is on the border of limit rectangle. // only calculate without rotation now because its calculation is to heavy when rotation applied. if (!mIsRotating) { final float left = Math.min(Math.min(mTrans[0], mTrans[2]), Math.min(mTrans[4], mTrans[6])); final float right = Math.max(Math.max(mTrans[0], mTrans[2]), Math.max(mTrans[4], mTrans[6])); final float top = Math.min(Math.min(mTrans[1], mTrans[3]), Math.min(mTrans[5], mTrans[7])); final float bottom = Math.max(Math.max(mTrans[1], mTrans[3]), Math.max(mTrans[5], mTrans[7])); if (right < mLimitRect.left) { dx = mLimitRect.left - right; } else if (left + EPS > mLimitRect.right) { dx = mLimitRect.right - left - EPS; } if (bottom < mLimitRect.top) { dy = mLimitRect.top - bottom; } else if (top + EPS > mLimitRect.bottom) { dy = mLimitRect.bottom - top - EPS; } } if ((dx != 0) || (dy != 0)) { // apply move if (mImageMatrix.postTranslate(dx, dy)) { // when image is really moved? mImageMatrixChanged = true; // apply to super class super.setImageMatrix(mImageMatrix); } } } mPrimaryX = event.getX(); mPrimaryY = event.getY(); return canMove; } |
なんということでしょう、回転させさせなければ数行で終わってしまうはずの処理が、80行を超える巨大メソッドに成長してしまいました。実際にはこのメソッドから別のメソッドも呼び出しているのでゆうに100行超えです。
まず前提として、ImageViewの表示領域の上下左右を一定幅inset(各辺を内側に向けて一定幅ずつ小さく)したのがmLimitRect(移動可能範囲矩形)で、イメージの実際の大きさがmImageRectです(#initで設定しています)。また。mImageRectの四隅の点をMatrix#mapPointsを用いて拡大縮小・移動・回転させた座標がmTrans配列(x,yの座標ペア✕4頂点, イメージ矩形)になります。
この時、ImageView様内にイメージが見えている条件としては、
- mTransで定義される矩形の四隅の内少なくとも1つがmLimitRect内に存在する。
- mLimitRectで定義される矩形の四隅の内少なくとも1つがmTransで定義される四角内に存在する。
- mTransで定義される矩形の四辺の内少なくとも1辺がmLimitRectで定義される矩形の四辺の1つと交差している。
この3項目、合計24条件の内1つでも満たせばImageView内にイメージが見えていることになります。
なので、タッチ位置から移動距離を計算して、移動後の各座標を求め上記項目の内1つでも満たせばイメージを移動可能ということになります。ソースで言うと、18〜23行が項目1のチェック、24〜28行が項目2のチェック、32〜48行が項目3のチェックになります。53〜73行が移動可能範囲外にめり込まないように移動量を補正する部分、そして74〜84行のたった10行(実質4行)が実際の移動処理になります。
この処理の一番のポイントは11行目のMatrix#mapPointsですね。ImageView内で実際にイメージを表示する際と同等の座標変換を行ってImageViewのローカル座標として結果を得ることが出来ます。
実はMatrixにはRectFで定義される矩形に対して同様の座標変換を行うMatrix#mapRectというメソッドも有るのですが、得られる結果が四隅の座標では無く、元の頂点を座標変換した矩形に外接する各辺がx軸またはy軸と並行な矩形になってしまい、回転させた場合にかなりの確率で移動チェックをすり抜けてしまうので、今回は使えませんでした(でも回転させたイメージをはみ出すこと無く別のビットマップとして取得する時には便利ですよ)。
2つ目のポイントは、凸n角形(n>=3)内に点が含まれるかどうかを判定している#ptInPolyです。
コードはこんな感じ。
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 |
private static final boolean ptInPoly(float x, float y, float[] poly) { final int n = poly.length & 0x7fffffff; // minimum 3 points(3 pair of x/y coordinates) need to calculate >> length >= 6 if (n < 6) return false; boolean result = true; final Vector v1 = new Vector(); final Vector v2 = new Vector(); for (int i = 0; i < n; i += 2) { v1.set(x, y).dec(poly[i], poly[i + 1]); if (i + 2 < n) v2.set(poly[i + 2], poly[i + 3]); else v2.set(poly[0], poly[1]); v2.dec(poly[i], poly[i + 1]); if (crossProduct(v1, v2) > 0) { result = false; break; } } return result; } /** * 2Dベクトルクラス(使う部分だけ) */ private static final class Vector { public float x, y; public Vector() { } public Vector(float x, float y) { set(x, y); } public Vector set(float x, float y) { this.x = x; this.y = y; return this; } public Vector sub(Vector other) { return new Vector(x - other.x, y - other.y); } public Vector dec(float x, float y) { this.x -= x; this.y -= y; return this; } } |
またまたベクトル登場です。ある頂点Aから時計回りの隣Bに向かう線分をベクトルとみなして、指定した点Pがそのベクトルの左右どちらに有るかを外積を使って判定しています。今回は引数を時計回りの凸n角形(n>=3)に限定しているので、指定した点が、全ての各頂点から時計回りに向かって右側または線分上(=外積が負または0)であれば、凸n角形内に指定した点が存在することになります。図を入れないとわかんないかなぁ?
本当はここまでの処理で、移動範囲のチェックが終わるつもりでした。でも見通しが甘かったですね。まだまだすり抜けてしまうのです。そこで登場したのが座標変換後のイメージ矩形と移動範囲矩形の辺の交差判定です。
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 |
private static final boolean checkIntersect(LineSegment seg, LineSegment[] segs) { boolean result = false; final int n = segs != null ? segs.length : 0; final Vector a = seg.p2.sub(seg.p1); Vector b, c, d; for (int i= 0; i < n; i++) { c = segs[i].p1.sub(seg.p1); d = segs[i].p2.sub(seg.p1); result = crossProduct(a, c) * crossProduct(a, d) < EPS; if (result) { b = segs[i].p2.sub(segs[i].p1); c = seg.p1.sub(segs[i].p1); d = seg.p2.sub(segs[i].p1); result = crossProduct(b, c) * crossProduct(b, d) < EPS; if (result) { break; } } } return result; } /** * 線分クラス */ private static final class LineSegment { public final Vector p1; public final Vector p2; public LineSegment (float x0, float y0, float x1, float y1) { p1 = new Vector(x0, y0); p2 = new Vector(x1, y1); } public LineSegment set(float x0, float y0, float x1, float y1) { p1.set(x0, y0); p2.set(x1, y1); return this; } } |
またまた…またベクトル・・・線分の交差判定です。詳しくはWebで(笑)そのうち説明を載せるかもしれません。
とりあえず今回はここまで。お疲れ様でした。
あっ、ちなみに今回はひたすら計算に頼って判定していますが、他にもいくつか方法は有るので興味が有る方は調べてみてくださいね。
コメント
[…] こちらの記事が移動制限を実装するのに非常に役立ちそうな予感です。実装できたらまた記事書きたいと思います(そればっか)。 […]