Android image processing: PinchImageView source code analysis

PinchImageViewUse GestureDetectorto deal with a long press, click, double click, inertial sliding events in onTouchEventhandling single-finger pinch zoom and move in events such. There are two matrices, a transformation matrix is external (mOuterMatrix), mainly the result of the operation of recording a gesture, an internal transformation matrix (getInnerMatrix (Matrix)), it is based on fitCenterthe zoom mode after initial matrix image translation. The difference between the two matrices here may be based on the experience of PhotoView . The gesture operation and the original zoom do not affect each other. The final zoom after the gesture operation only needs to be multiplied by the two matrices. The following code analysis may not completely post the source code, and some have been slightly modified.

1 Double click, inertial sliding

Long press and click are calling callbacks. We mainly look at double click and inertial sliding.

1.1 Double click

PinchImageViewOnly one level of zooming is done, which means that you can only switch between the maximum and initial zoom values. Basic principle: Capture the double-click event, get the x and y coordinates of the double-click point, zoom in and transform the picture, and move the double-click point to the middle of the view. The code is longer, we split it a little bit. Under this first presentation PinchImageViewobject pool (ObjectsPool). ObjectsPoolMaintain an object queue, and recycle objects within the capacity range. The general usage process is shown in the figure below:

  1. Get in the queue innerMatrixobjects (take ()), the queue is empty, create a new object is returned, otherwise the team after a reset object is returned.
  2. Get in the queue targetMatrixobject.
  3. Use complete targetMatrixreturn (given (obj)).
  4. Use complete innerMatrixrestitution.

The order of return does not matter.

/**
 * 对象池
 *
 * 防止频繁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);
}

Continue to see the handling of the double-click event.

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

    ……
    MathUtils.matrixGiven(innerMatrix);
}

The first is to obtain the internal transformation matrix. MathUtils.matrixTake()From Matrixacquiring a target pool (MatrixPool) in the Matrixobject.

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;
}

Then to acquire internal transformation matrix, and the presence innerMatrixof.

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.rectFTakeWith matrixTakethe same method, but out of that rectF. The key is to matrix.setRectToRectapproach above has been introduced. Continue to look down:

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

Here we multiply the internal matrix scaling and external scaling to get the final scaling. The design that does not affect the internal and external factors is really good. Next, start to calculate and scale.

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();

This code is show, let's sort out the scaled down ideas: Double-click the picture, be sure to do it in the form of animation, then the beginning of the movie, of course, is the current changing positions, converted to the target scale value nextScalemultiples are nextScale/ currentScale, compliance gesture operation recorded in the external matrix mOuterMatrixprinciple, the initial animation matrixfrom the copy mOuterMatrix. This code is actually problematic. innerScaleIs a picture for fitCenterscaling the transformed values, assuming a large picture, transformed innerScalevalue 0.2f, maxScale2, there has been no gesture operation, outerScaleas a result, this time look at the count:

That you double-click, all of a sudden see the pictures enlarged 10 times ...... Now many want to know the width and height map is larger than phone screen ...... ScaleAnimatorin only one thing, constantly updated mOuterMatrixvalues, and then invalidate, in onDrawRefresh the view.

@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);
    ……
}

After zooming and panning, the frame of the picture may enter the picture control, and the position needs to be corrected at this time. Use the final zoomed picture boundary and the control boundary to compare and correct.

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

As you have just known, what is animEndrecorded is the result of the current double-click transformation operation acting on the external matrix. innerMatrixMultiply it with the internal matrix ( ) to get the final testBoundtransformation matrix ( testMatrix) of the original image ( ).

//修正位置
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);

Here correction position is easy to understand, not to say, to correct two errors source: postX = displayWidth / 2f - (testBound.right + testBound.left) / 2f;where testBound.right + testBound.leftshould be testBound.right - testBound.left. I did not stick out postYhave to be changed next.

1.2 Inertial sliding (Fling)

PinchImageViewThe inertial sliding handle their own decay ...... every degree of attenuation is still the same, does not support the interpolator, than PhotoViewuse OverScrollerto handle slide, it becomes a bit shabby. GestureDetectorThe onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)containing x, y-axis acceleration, acceleration in pixels / second, 60 frames per second, is converted into pixels / frame i.e. velocityX/60, velocityY/60. PinchImageViewUse FlingAnimatorto do animation, animated slide from the initial update velocityX/60, and then multiplied by the attenuation value ( FLING_DAMPING_FACTOR0.9), until the next update to use.

//移动图像并给出结果
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) The method handles scrolling, mainly considering the processing of the picture boundary and the control boundary. The principle is the same as the correction position when zooming above, and the acquisition of the picture boundary is also the same as when zooming.

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

Finally, the mOuterMatrixpan transform ( postTranslate), invalidatethe trigger onDrawto set a new picture matrix.

3.2 Two-finger zoom, one-finger move

Pinch zoom, single finger movement is onTouchin the doing.

3.2.1 Two-finger zoom

Principle: Record the distance between two fingers on the screen. The scaling value of the unit distance is the quotient of the scaling value of the external matrix divided by this distance. Use this scaling value to multiply the distance after the two fingers slide to get a new scaling value. This scaling value applies scaling transformation to the external matrix to obtain the final external matrix.

Obviously, mScaleBasethe zoom value per unit distance is the slope, which determines the speed of the two-finger zoom. Then the factors that determine the speed of the two-finger zoom are: the zoom size of the current external matrix, and the initial distance between the two fingers. The larger the external matrix zoom, the smaller the initial distance between the two fingers, and the faster the two-finger sliding zoom. Another to note is the center point of the picture zoom in PinchImageView, the two-finger zoom conversion is carried out in the unit matrix. So when two fingers pressed when the need to record the center point before the external matrix transformation, with a source in the mScaleCentermember variables to record the point (PS: recommend to the naked eye Source shield in place in all the variables used in this comment, you will halo) . Take a quick look at the relevant code:

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);
        ……
    }
}

In a multi-recording means to press the current time is a dual-zoom mode, saveScaleContext()recording the above-mentioned mScaleBaseand mScaleCenter. In the MotionEvent.ACTION_MOVEinside handle scaling logic. Look saveScaleContextprocessing.

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]);
}

mScaleBaseI have already talked about it above, here is the main mention inverseMatrixPoint, let's look at the method definition:

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)The srcMatrixinverse matrix to keep targetMatrixin martrix.mapPoints(targetPoint, srcPoint);on srcPointthe application and place the matrix transformation targetPointin. Obviously, the function of this method is to get the points before the matrix transformation. mScaleCenterWhat is stored is the position of the point before the external matrix transformation. Next, let's look at the scaling process.

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);
    ……
}

It's easy to understand, I've already talked about it above. Tucao look here, if mOuterMatrixthe fault occurs cut, rotation, perspective transformation, that does not scrap it? There is also a case where one finger is lifted by multiple fingers. The comments have been revised and are easy to understand.

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,那么此时只剩下一个点,也不允许进入单指模式,因为此时可能图片没有在正确的位置上
    }
}

Finally, the lower boundary needs to be corrected when letting go. Into the scaleEndmethod. In fact, most of the code just have analyzed, and talk about a variable here 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;
    }
}

The comment was changed by me.

3.2.1 One-finger movement

Single-finger movement is mainly a call scrollBy, which has been analyzed before.

The analysis is basically over here.

Guess you like

Origin blog.csdn.net/zhireshini233/article/details/115264895