Android画像処理:PinchImageViewソースコード分析

PinchImageView使用して、GestureDetector長押し、クリック、ダブルクリック、慣性スライド事象に対処するためにonTouchEvent、このようなイベントで1本指のピンチズームや移動を扱います。2つの行列があります。変換行列は外部(mOuterMatrix)であり、主にジェスチャを記録する操作の結果であり、内部変換行列(getInnerMatrix(Matrix))であり、fitCenter最初の行列画像変換後のズームモードに基づいています。ここでの2つのマトリックスの違いは、PhotoViewの経験に基づいている可能性があります。ジェスチャ操作と元のズームは相互に影響しません。ジェスチャ操作後の最後のズームは、2つのマトリックスを掛けるだけで済みます。次のコード分析では、ソースコードが完全に投稿されていない可能性があり、一部はわずかに変更されています。

1ダブルクリック、慣性スライド

長押しとクリックはコールバックを呼び出しています。主にダブルクリックと慣性スライドに注目します。

1.1ダブルクリック

PinchImageViewズームのレベルは1つだけです。つまり、ズームの最大値と初期値のみを切り替えることができます。基本原則:ダブルクリックイベントをキャプチャし、ダブルクリックポイントのx座標とy座標を取得し、画像を拡大して変換し、ダブルクリックポイントをビューの中央に移動します。コードは長く、少し分割します。この最初のプレゼンテーションPinchImageViewオブジェクトプール(ObjectsPool)の下。ObjectsPoolオブジェクトキューを維持し、容量範囲内でオブジェクトをリサイクルします。一般的な使用プロセスを次の図に示します。

  1. キューinnerMatrixオブジェクトを取得し(take())、キューは空で、新しいオブジェクトを作成します。それ以外の場合は、リセットされたオブジェクトが返されます。
  2. キューtargetMatrixオブジェクトを取得します。
  3. 完全なtargetMatrixリターンを使用します(与えられた(obj))。
  4. 完全なinnerMatrix返還を使用してください

返品の順番は関係ありません。

/**
 * 对象池
 *
 * 防止频繁new对象产生内存抖动.
 * 由于对象池最大长度限制,如果吞吐量超过对象池容量,仍然会发生抖动.
 * 此时需要增大对象池容量,但是会占用更多内存.
 *
 * @param <T> 对象池容纳的对象类型
 */
private static abstract class ObjectsPool<T> {
    /**
     * 对象池的最大容量
     */
    private int mSize;
    /**
     * 对象池队列
     */
    private Queue<T> mQueue;

    public ObjectsPool(int size) {
        mSize = size;
        mQueue = new LinkedList<T>();
    }

    public T take() {
        //如果池内为空就创建一个
        if (mQueue.size() == 0) {
            return newInstance();
        } else {
            //对象池里有就从顶端拿出来一个返回
            return resetInstance(mQueue.poll());
        }
    }
    public void given(T obj) {
        //如果对象池还有空位子就归还对象
        if (obj != null && mQueue.size() < mSize) {
            mQueue.offer(obj);
        }
    }

    abstract protected T newInstance();

    abstract protected T resetInstance(T obj);
}

ダブルクリックイベントの処理を引き続き確認します。

private void doubleTap(float x, float y) {
    //获取第一层变换矩阵
    Matrix innerMatrix = MathUtils.matrixTake();
    getInnerMatrix(innerMatrix);

    ……
    MathUtils.matrixGiven(innerMatrix);
}

1つ目は、内部変換行列を取得することです。オブジェクト内のターゲットプール(MatrixPool)の取得MathUtils.matrixTake()からMatrixMatrix

public static Matrix matrixTake() {
    return mMatrixPool.take();
}
/**
 * 获取某个矩阵的copy
 */
public static Matrix matrixTake(Matrix matrix) {
    Matrix result = mMatrixPool.take();
    if (matrix != null) {
        result.set(matrix);
    }
    return result;
}

次に、内部変換行列、およびの存在を取得しinnerMatrixます。

public Matrix getInnerMatrix(Matrix matrix) {
    ……

    //原图大小
    RectF tempSrc = MathUtils.rectFTake(0, 0, getDrawable().getIntrinsicWidth(), getDrawable().getIntrinsicHeight());
    //控件大小
    RectF tempDst = MathUtils.rectFTake(0, 0, getWidth(), getHeight());
    //计算fit center矩阵
    matrix.setRectToRect(tempSrc, tempDst, Matrix.ScaleToFit.CENTER);

    ……

    return matrix;
}

MathUtils.rectFTakematrixTake同じ方法で、それのうちrectF重要なのは、matrix.setRectToRect上記のアプローチが導入されたことです。見下ろし続けます:

//当前总的缩放比例
float innerScale = MathUtils.getMatrixScale(innerMatrix)[0];
float outerScale = MathUtils.getMatrixScale(mOuterMatrix)[0];
float currentScale = innerScale * outerScale;

ここでは、内部マトリックススケーリングと外部スケーリングを乗算して最終的なスケーリングを取得します。内部要因と外部要因に影響を与えない設計は非常に優れています。次に、計算とスケーリングを開始します。

float nextScale = currentScale < MAX_SCALE ? MAX_SCALE : innerScale;
//如果接下来放大大于最大值或者小于fit center值,则取边界
if (nextScale > maxScale) {
    nextScale = maxScale;
}
if (nextScale < innerScale) {
    nextScale = innerScale;
}
//开始计算缩放动画的结果矩阵
Matrix animEnd = MathUtils.matrixTake(mOuterMatrix);
//计算还需缩放的倍数
animEnd.postScale(nextScale / currentScale, nextScale / currentScale, x, y);
//将放大点移动到控件中心
animEnd.postTranslate(displayWidth / 2f - x, displayHeight / 2f - y);
……
//启动矩阵动画
mScaleAnimator = new ScaleAnimator(mOuterMatrix, animEnd);
mScaleAnimator.start();

このコードは表示されています。縮小されたアイデアを整理しましょう。画像をダブルクリックし、アニメーションの形式で実行してください。もちろん、ムービーの先頭は現在の変化する位置であり、ターゲットに変換されます。スケール値のnextScale倍数はnextScale/ currentScale、外部マトリックスのmOuterMatrix原則に記録されたコンプライアンスジェスチャ操作matrix、コピーからの最初のアニメーションmOuterMatrixです。このコードは実際には問題があります。変換された値innerScalefitCenterスケーリングするための画像です。大きな画像、変換されたinnerScale値0.2f、maxScale2を想定しています。outerScaleその結果、ジェスチャ操作は行われていません。その結果、今回はカウントを確認します。

ダブルクリックすると、突然写真が10倍に拡大されます......今では多くの人が幅と高さの地図が電話の画面よりも大きいことを知りたがっています......ScaleAnimatorたった1つのことで、常に更新されていますmOuterMatrix値、次にinvalidateonDraw[ビューの更新]で。

@Override
public void onAnimationUpdate(ValueAnimator animation) {
    //获取动画进度
    float value = (Float) animation.getAnimatedValue();
    //根据动画进度计算矩阵中间插值
    for (int i = 0; i < 9; i++) {
        mResult[i] = mStart[i] + (mEnd[i] - mStart[i]) * value;
    }
    //设置矩阵并重绘
    mOuterMatrix.setValues(mResult);
    ……
    invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
    ……
    //在绘制前设置变换矩阵
    setImageMatrix(getCurrentImageMatrix(matrix));
    ……
    super.onDraw(canvas);
    ……
}

ズームとパンを行った後、画像のフレームが画像コントロールに入る可能性があるため、この時点で位置を修正する必要があります。最終的なズーム画像の境界とコントロールの境界を使用して、比較および修正します。

Matrix testMatrix = MathUtils.matrixTake(innerMatrix);
testMatrix.postConcat(animEnd);
RectF testBound = MathUtils.rectFTake(0, 0, getDrawable().getIntrinsicWidth(), getDrawable().getIntrinsicHeight());
testMatrix.mapRect(testBound);

ご存知のように、animEnd記録されるのは、外側の行列に作用する現在のダブルクリック変換操作の結果です。これに内側の行列(innerMatrix)を掛け、元の画像testBoundtestMatrixの最終的な変換行列(を取得します。

//修正位置
float postX = 0;
float postY = 0;
if (testBound.right - testBound.left < displayWidth) {
    postX = displayWidth / 2f - (testBound.right + testBound.left) / 2f;
} else if (testBound.left > 0) {
    postX = -testBound.left;
} else if (testBound.right < displayWidth) {
    postX = displayWidth - testBound.right;
}
……
//应用修正位置
animEnd.postTranslate(postX, postY);

ここでは、補正位置が正しい2つのエラーソースに、言っていない、理解しやすいです:postX = displayWidth / 2f - (testBound.right + testBound.left) / 2f;どこtestBound.right + testBound.leftでなければなりませんtestBound.right - testBound.left私は突き出ていませんでしたpostY次に変更する必要があります。

1.2慣性スライディング(フリング)

PinchImageView慣性スライディングは独自の減衰を処理します......減衰の程度はすべて同じであり、補間器をサポートしていません。スライドを処理PhotoViewするOverScrollerために使用するよりも、少しぼろぼろになります。含むX、ピクセルのY軸加速度、加速度/秒、毎秒60のフレーム、すなわちピクセル/フレームに変換されますアニメーションを実行するために使用し、最初の更新からスライドをアニメーション化し、次に使用する更新まで減衰値(0.9)を掛けます。GestureDetectoronFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)velocityX/60velocityY/60PinchImageViewFlingAnimatorvelocityX/60FLING_DAMPING_FACTOR

//移动图像并给出结果
boolean result = scrollBy(mVector[0], mVector[1], null);
mVector[0] *= FLING_DAMPING_FACTOR;
mVector[1] *= FLING_DAMPING_FACTOR;
//速度太小或者不能移动了就结束
if (!result || MathUtils.getDistance(0, 0, mVector[0], mVector[1]) < 1f) {
    animation.cancel();
}

scrollBy(float xDiff, float yDiff, MotionEvent motionEvent) この方法は、主に画像境界と制御境界の処理を考慮してスクロールを処理します。原理は上ズーム時の補正位置と同じであり、画像境界の取得もズーム時と同じです。

//获取内部变换矩阵
matrix = getInnerMatrix(matrix);
//乘上外部变换矩阵
matrix.postConcat(mOuterMatrix);
rectF.set(0, 0, getDrawable().getIntrinsicWidth(), getDrawable().getIntrinsicHeight());
matrix.mapRect(rectF);

最後に、mOuterMatrixパン変換(postTranslate)、新しい画像マトリックスを設定するinvalidateためのトリガーonDraw

3.2 2本指ズーム、1本指移動

ピンチズーム、1本の指の動きが行わonTouchれています。

3.2.12本指ズーム

原理:画面上の2本の指の間の距離を記録します。単位距離のスケーリング値は、外部マトリックスのスケーリング値をこの距離で割った商です。このスケーリング値を使用して、2本の指がスライドした後の距離を乗算します。新しいスケーリング値を取得します。このスケーリング値は、スケーリング変換を外部マトリックスに適用して、最終的な外部マトリックスを取得します。

明らかにmScaleBase、単位距離あたりのズーム値は勾配であり、2本指ズームの速度を決定します。次に、2本指ズームの速度を決定する要因は次のとおりです。現在の外部マトリックスのズームサイズ、および2本の指の間の初期距離。外部マトリックスズームが大きいほど、2本の指の間の初期距離が小さくなり、2本の指のスライドズームが速くなります。もう1つの注意点は、画像のズームインの中心点ですPinchImageView。2本指のズーム変換は単位行列で実行されます。したがって、外部行列変換の前に中心点を記録する必要があるときに2本の指を押すと、mScaleCenterメンバー変数に点を記録するソースがあります(PS:このコメントで使用されるすべての変数にソースシールドを配置することを肉眼で推奨します、あなたはハローになります)。関連するコードをざっと見てみましょう。

private PointF mScaleCenter = new PointF();
private float mScaleBase = 0;
……
public boolean onTouchEvent(MotionEvent event) {
    ……
    int action = event.getAction() & MotionEvent.ACTION_MASK;
    if (action == MotionEvent.ACTION_POINTER_DOWN) {
        //切换到缩放模式
        mPinchMode = PINCH_MODE_SCALE;
        //保存缩放的两个手指
        saveScaleContext(event.getX(0), event.getY(0), event.getX(1), event.getY(1));
    }else if (action == MotionEvent.ACTION_MOVE) {
        ……
        //两个缩放点间的距离
        float distance = MathUtils.getDistance(event.getX(0), event.getY(0), event.getX(1), event.getY(1));
        //保存缩放点中点
        float[] lineCenter = MathUtils.getCenterPoint(event.getX(0), event.getY(0), event.getX(1), event.getY(1));
        mLastMovePoint.set(lineCenter[0], lineCenter[1]);
        //处理缩放
        scale(mScaleCenter, mScaleBase, distance, mLastMovePoint);
        ……
    }
}

現在時刻を押して多重記録手段に、デュアルズームモードであり、saveScaleContext()上記記録mScaleBase及びmScaleCenterMotionEvent.ACTION_MOVEインサイドハンドルスケーリングロジック。saveScaleContext処理を見てください

private void saveScaleContext(float x1, float y1, float x2, float y2) {
    mScaleBase = MathUtils.getMatrixScale(mOuterMatrix)[0] / MathUtils.getDistance(x1, y1, x2, y2);
    float[] center = MathUtils.inverseMatrixPoint(MathUtils.getCenterPoint(x1, y1, x2, y2), mOuterMatrix);
    mScaleCenter.set(center[0], center[1]);
}

mScaleBase私はすでにそれについて上で話しました、ここに主な言及がありinverseMatrixPointます、メソッド定義を見てみましょう:

public static float[] inverseMatrixPoint(float[] point, Matrix matrix) {
    if (point != null && matrix != null) {
        float[] dst = new float[2];
        //计算matrix的逆矩阵
        Matrix inverse = matrixTake();
        matrix.invert(inverse);
        //用逆矩阵变换point到dst,dst就是结果
        inverse.mapPoints(dst, point);
        //清除临时变量
        matrixGiven(inverse);
        return dst;
    } else {
        return new float[2];
    }
}

srcMatrix.invert(targetMatrix)srcMatrix保つために逆行列targetMatrixmartrix.mapPoints(targetPoint, srcPoint);srcPointのアプリケーションと行列変換を置くtargetPointに。明らかに、このメソッドの機能は、行列変換の前にポイントを取得することです。mScaleCenter保存されるのは、外部行列変換前のポイントの位置です。次に、スケーリングプロセスを見てみましょう。

private void scale(PointF scaleCenter, float scaleBase, float distance, PointF lineCenter) {
    ……
    //计算图片从fit center状态到目标状态的缩放比例
    float scale = scaleBase * distance;
    Matrix matrix = MathUtils.matrixTake();
    //按照图片缩放中心缩放,并且让缩放中心在缩放点中点上
    matrix.postScale(scale, scale, scaleCenter.x, scaleCenter.y);
    //让图片的缩放中点跟随手指缩放中点
    matrix.postTranslate(lineCenter.x - scaleCenter.x, lineCenter.y - scaleCenter.y);
    mOuterMatrix.set(matrix);
    ……
}

理解するのは簡単です、私はすでにそれについて上で話しました。Tucaoはここを見て、mOuterMatrix障害が発生した場合、カット、回転、遠近法の変換、それはそれを廃棄しませんか?1本の指を複数の指で持ち上げる場合もあります。コメントが改訂され、わかりやすくなっています。

if (action == MotionEvent.ACTION_POINTER_UP) {
    if (mPinchMode == PINCH_MODE_SCALE) {
        //event.getPointerCount()表示抬起手指时点的数量,包含抬起的那个点
        if (event.getPointerCount() > 2) {
            //event.getAction() >> 8得到的是当前抬起的点的索引。第一个点抬起了,那么让第二个点和第三个点作为缩放控制点
            if (event.getAction() >> 8 == 0) {
                saveScaleContext(event.getX(1), event.getY(1), event.getX(2), event.getY(2));
                //第二个点抬起了,那么让第一个点和第三个点作为缩放控制点
            } else if (event.getAction() >> 8 == 1) {
                saveScaleContext(event.getX(0), event.getY(0), event.getX(2), event.getY(2));
            }
        }
        //如果抬起的点等于2,那么此时只剩下一个点,也不允许进入单指模式,因为此时可能图片没有在正确的位置上
    }
}

最後に、手放すときに下限を修正する必要があります。scaleEnd方法。実際、ほとんどのコードは分析したばかりであり、ここで変数について説明していますscalePost

private void scaleEnd() {
    ……
    getCurrentImageMatrix(currentMatrix);
    float currentScale = MathUtils.getMatrixScale(currentMatrix)[0];
    float outerScale = MathUtils.getMatrixScale(mOuterMatrix)[0];
    //比例修正
    float scalePost = 1f;
    //如果整体缩放比例大于最大比例,进行缩放修正
    if (currentScale > maxScale) {
        scalePost = maxScale / currentScale;
    }
    //如果缩放修正后整体导致外部矩阵缩放小于1(外部矩阵的初始值就是1,如果操作导致比初始值还小,就还原回去),重新修正缩放
    if (outerScale * scalePost < 1f) {
        scalePost = 1f / outerScale;
    }
}

コメントは私が変更しました。

3.2.11本の指の動き

一本指の動きは主に呼び出しscrollByであり、これは以前に分析されています。

分析は基本的にここにあります。

おすすめ

転載: blog.csdn.net/zhireshini233/article/details/115264895