Android图片处理一:Matrix与手势

1 矩阵基础

A = [ k 1 k 2 k 3 k 4 k 5 k 6 k 7 k 8 k 9 ] B = [ x 0 x 1 y 0 y 1 1 1 ] C = A B = [ k 1 x 0 + k 2 y 0 + k 3 k 1 x 1 + k 2 y 1 + k 3 k 4 x 0 + k 5 y 0 + k 6 k 4 x 1 + k 5 y 1 + k 6 k 7 x 0 + k 8 y 0 + k 9 k 7 x 1 + k 8 y 1 + k 9 ] \begin{aligned} A&=\begin{bmatrix} k_1 & k_2 & k_3\\ k_4 & k_5 & k_6\\ k_7 & k_8 & k_9 \end{bmatrix}\\ B&=\begin{bmatrix} x_0 & x_1\\ y_0 & y_1\\ 1 & 1 \end{bmatrix}\\ C=AB&=\begin{bmatrix} k_1x_0 + k_2y_0 + k_3 & k_1x_1 + k_2y_1 + k_3\\ k_4x_0 + k_5y_0 + k6 & k_4x_1 + k_5y_1 + k6\\ k_7x_0 + k_8y_0 + k9 & k_7x_1 + k_8y_1 + k9 \end{bmatrix} \end{aligned} ABC=AB=k1k4k7k2k5k8k3k6k9=x0y01x1y11=k1x0+k2y0+k3k4x0+k5y0+k6k7x0+k8y0+k9k1x1+k2y1+k3k4x1+k5y1+k6k7x1+k8y1+k9

用 A(3行3列) 的行去乘以 B(3行2列) 的所有列,得出一个3行2列的矩阵。

1、当矩阵A的列数(column)等于矩阵B的行数(row)时,A与B可以相乘。
2、矩阵C的行数等于矩阵A的行数,C的列数等于B的列数。
3、乘积C的第m行第n列的元素等于矩阵A的第m行的元素与矩阵B的第n列对应元素乘积之和。

1、2点可总结为:A(m×p)×B(p×n)=C(m×n)

1.1 左乘和右乘

以下解释是为了方便记忆,不保证科学性。

左乘以:C = AB 称为A左乘以B,“以” 表示用,A左乘以B表示用B去乘A,A在左边,跟普通乘法顺序一致,从左到右。
左乘:与左乘以相反,C = AB 称为B左乘A(B左边乘A)
右乘以:C = BA 称为A右乘以B,用B去乘A,A在右边
右乘:与右乘以相反,C = BA 称为B右乘A(B右边乘A)

所以,C = AB可以称为:A左乘以B,B左乘A,B右乘以A,A右乘B。得出结论:左乘以 == 右乘,右乘以 == 左乘。

2 Matrix基础

Matrix 是 Android 图形库里的一个坐标转换类,它里面保存了一个 3 × 3 的矩阵,矩阵里各元素的对应关系如下所示:

[ 缩 放 _ X 错 切 _ X 平 移 _ X 错 切 _ Y 缩 放 _ Y 平 移 _ Y 透 视 _ 0 透 视 _ 1 透 视 _ 2 ] \begin{bmatrix} 缩放\_X & 错切\_X & 平移\_X\\ 错切\_Y & 缩放\_Y & 平移\_Y\\ 透视\_0 & 透视\_1 & 透视\_2 \end{bmatrix} _X_Y_0_X_Y_1_X_Y_2

具体为何这样对应请参考安卓自定义View进阶-Matrix原理。这里引用几个重要的变换。

2.1 平移

x = x 0 + Δ x y = y 0 + Δ y \begin{aligned} x = x_0 + \Delta x \\ y = y_0 + \Delta y \end{aligned} x=x0+Δxy=y0+Δy

用矩阵表示:

[ x y 1 ] = [ 1 0 Δ x 0 1 Δ y 0 0 1 ] ⋅ [ x 0 y 0 1 ] \begin{aligned} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & \Delta x\\ 0 & 1 & \Delta y\\ 0 & 0 & 1 \end{bmatrix} \cdot \begin{bmatrix} x_0 \\ y_0 \\ 1 \end{bmatrix} \end{aligned} xy1=100010ΔxΔy1x0y01

2.2 缩放

x = k 1 x 0 y = k 2 y 0 \begin{aligned} x = k_1x_0 \\ y = k_2y_0 \end{aligned} x=k1x0y=k2y0

用矩阵表示:

[ x y 1 ] = [ k 1 0 0 0 K 2 0 0 0 1 ] ⋅ [ x 0 y 0 1 ] \begin{aligned} \begin{bmatrix} x \\ y \\1 \end{bmatrix} = \begin{bmatrix} k_1 & 0 & 0 \\ 0 & K_2 & 0 \\ 0 & 0 & 1 \end{bmatrix} \cdot \begin{bmatrix} x_0 \\ y_0 \\ 1 \end{bmatrix} \end{aligned} xy1=k1000K20001x0y01

2.3 旋转

x 0 = r cos ⁡ α y 0 = r sin ⁡ α x = r cos ⁡ ( α + θ ) = r cos ⁡ α cos ⁡ θ − r sin ⁡ α sin ⁡ θ = x 0 cos ⁡ θ − y 0 sin ⁡ θ y = r sin ⁡ ( α + θ ) = r sin ⁡ α cos ⁡ θ + r cos ⁡ α sin ⁡ θ = y 0 cos ⁡ θ + x 0 sin ⁡ θ \begin{aligned} x_0 &= r\cos\alpha \\ y_0 &= r\sin\alpha \\ x = r\cos(\alpha + \theta) &= r\cos\alpha\cos\theta - r\sin\alpha\sin\theta = x_0\cos\theta - y_0\sin\theta \\ y = r\sin(\alpha + \theta) &= r\sin\alpha\cos\theta + r\cos\alpha\sin\theta = y_0\cos\theta + x_0\sin\theta \end{aligned} x0y0x=rcos(α+θ)y=rsin(α+θ)=rcosα=rsinα=rcosαcosθrsinαsinθ=x0cosθy0sinθ=rsinαcosθ+rcosαsinθ=y0cosθ+x0sinθ

用矩阵表示:

[ x y 1 ] = [ cos ⁡ θ − sin ⁡ θ 0 sin ⁡ θ cos ⁡ θ 0 0 0 1 ] ⋅ [ x 0 y 0 1 ] \begin{aligned} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} \cos\theta & -\sin\theta & 0\\ \sin\theta & \cos\theta & 0\\ 0 & 0 & 1 \end{bmatrix} \cdot \begin{bmatrix} x_0 \\ y_0 \\ 1 \end{bmatrix} \end{aligned} xy1=cosθsinθ0sinθcosθ0001x0y01

旋转是一个矩阵元素组合实现。

Matrix 里有很多 prepost 方法。pre 表示前乘(原始Matrix放前面),A pre B 输出 AB,按照我们刚才对矩阵左乘右乘的解释,前乘相当于矩阵的左乘以、右乘。post 表示后乘(原始Matrix放后面),A post B 输出 BA,后乘相当于矩阵的右乘以、左乘。

Matrix 里常用的变换有 平移(Translate)、旋转(Rotate)、缩放(Scale)。

2.4 Matrix常用方法

方法类别相关API摘要
基本方法equals hashCode toString toShortString比较、 获取哈希值、 转换为字符串
数值操作set reset setValues getValues设置、 重置、 设置数值、 获取数值
数值计算mapPoints mapRadius mapRect mapVectors计算变换后的数值
设置(set)setConcat setRotate setScale setSkew setTranslate设置变换
前乘(pre)preConcat preRotate preScale preSkew preTranslate前乘变换
后乘(post)postConcat postRotate postScale postSkew postTranslate后乘变换
特殊方法setPolyToPoly setRectToRect rectStaysRect setSinCos一些特殊操作
矩阵相关invert isAffine isIdentity求逆矩阵、 是否为仿射矩阵、 是否为单位矩阵 …

具体各个方法使用参考:安卓自定义View进阶-Matrix详解,各方法在 Matrix 类里的注解都非常详细, 很多方法基本一看注解就能懂。

这里放几个典型的。

2.4.1 setRectToRect
public boolean setRectToRect(RectF src, RectF dst, ScaleToFit stf) {}

这个方法是 srcdst 的变换,可以理解为把 src 放进 dst 里去的的变换,第三个参数 stf 的缩放填充模式(充满FILL、保持比例左上START、居中CENTER、右下END)。

RectF src= new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight() );
RectF dst = new RectF(0, 0, mViewWidth, mViewHeight );
mRectMatrix.setRectToRect(src, dst, Matrix.ScaleToFit.CENTER);
canvas.drawBitmap(mBitmap, mRectMatrix, new Paint());

代码很容易理解,把 bitmap 通过 matrix 变换绘制到 view 上。

2.4.2 mapRect

mapRect有两个方法,具体含义写在注释中。

/**
* 对 rect 进行 matrix 变换,并将结果存在 rect 中
*/
public boolean mapRect(RectF rect) {}
/**
* 对 src 进行 matrix 变换,并将结果存在 dst中
*/
public boolean mapRect(RectF dst, RectF src) {}
2.4.3 setPolyToPoly

这个比较复杂,建议详细看下安卓自定义View进阶-Matrix详解里的 setPolyToPoly 详细讲解。这里摘录下重点:

先看代码和效果:

float[] src = {0, 0,                                    // 左上
        mBitmap.getWidth(), 0,                          // 右上
        mBitmap.getWidth(), mBitmap.getHeight(),        // 右下
        0, mBitmap.getHeight()};                        // 左下

float[] dst = {0, 0,                                    // 左上
        mBitmap.getWidth(), 400,                        // 右上
        mBitmap.getWidth(), mBitmap.getHeight() - 200,  // 右下
        0, mBitmap.getHeight()};                        // 左下

// 核心要点
mPolyMatrix.setPolyToPoly(src, 0, dst, 0, src.length >> 1); // src.length >> 1 为位移运算 相当于处以2

在这里插入图片描述

效果图里我们看到了图片右边产生了上下都往里缩的形变,再看代码,src 数组存放了原图的四个边界点,在 dst 数组里存放了变换后的四个边界点,src.length >> 1 即为边界点个数,很容易看出 setPolyToPoly 是能精确到数组里各个点的变换。再看方法定义就好理解很多了:

boolean setPolyToPoly (
        float[] src,    // 原始数组 src [x,y],存储内容为一组点
        int srcIndex,   // 原始数组开始位置
        float[] dst,    // 目标数组 dst [x,y],存储内容为一组点
        int dstIndex,   // 目标数组开始位置
        int pointCount) // 测控点的数量 取值范围是: 0到4

测控点数量对功能的影响:

pointCount摘要
0相当于reset
1相当于translate
2可以进行 缩放、旋转、平移 变换
3可以进行 缩放、旋转、平移、错切 变换
4可以进行 缩放、旋转、平移、错切以及任何形变

3 PinchImageView分析

PinchImageView 使用 GestureDetector 来处理长按、点击、双击、惯性滑动事件,在 onTouchEvent 里处理双指缩放和单指移动等事件。

里面有两个矩阵,一个是外部变换矩阵(mOuterMatrix),主要记录手势操作的结果,一个是内部变换矩阵(getInnerMatrix(Matrix)),就是根据 fitCenter 等缩放模式进行缩放平移后的初始矩阵。这里区分两个矩阵可能是借鉴了PhotoView的经验,手势操作和原始缩放互不影响,手势操作后最终的缩放只需要两个矩阵相乘就好了。

下面的代码分析不一定会完全贴源码,有的是经过稍微改动的。

3.1 双击、惯性滑动

长按和点击就是调用回调,我们主要来看双击和惯性滑动。

3.1.1 双击

PinchImageView 只做了一级的放大缩小 ,就是说只能在最大和初始缩放值之间切换。

基本原理:捕获双击事件,拿到双击点的x、y坐标,对图片进行缩放变换,将双击点位置移动到视图中间。

代码较长,我们一点点拆分。

这里要先介绍下 PinchImageView 的对象池(ObjectsPool)。

ObjectsPool 维护一个对象队列,在容量范围内可以循环复用对象。大致使用流程如下图所示:

  1. 在队列里获取 innerMatrix 对象(take()),队列为空则新建一个对象返回,否则出队一个对象重置后返回。
  2. 在队列里获取 targetMatrix 对象。
  3. 使用完 targetMatrix 归还(given(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);
}

首先是获取内部变换矩阵。MathUtils.matrixTake() 是从 Matrix 对象池(MatrixPool)里获取一个 Matrix 对象。

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

这段代码其实是有问题的。innerScale 是对原图进行 fitCenter 变换后的缩放值,假设原图很大,变换后 innerScale 值为0.2f, maxScale 为2,没有进行过手势操作,outerScale 为1,这时候来看下算的结果:
c u r r e n t S c a l e = i n n e r S c a l e × o u t e r S c a l e = 0.2 × 1 = 0.2 n e x t S c a l e = 0.2 < 2   ?   2 : 0.2 = 2 n e x t S c a l e c u r r e n t S c a l e = 2 0.2 = 10 currentScale = innerScale \times outerScale = 0.2 \times 1 = 0.2 \\ nextScale = 0.2 < 2\ ?\ 2 : 0.2 = 2 \\ \frac{nextScale}{currentScale} = \frac{2}{0.2} = 10 currentScale=innerScale×outerScale=0.2×1=0.2nextScale=0.2<2 ? 2:0.2=2currentScalenextScale=0.22=10
就是说你双击一下,一下子看到的图片放大了10倍…… 要知道现在很多图宽高都是比手机屏幕大的……

ScaleAnimator 里只做了一件事,不断更新 mOuterMatrix 的值,然后 invalidate ,在 onDraw 里刷新视图。

@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)相乘就得到了最终对原图(testBound)的变换矩阵(testMatrix)。

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

这里修正位置很容易看懂,就不说了,纠正源码的两个错误:

postX = displayWidth / 2f - (testBound.right + testBound.left) / 2f; 里的 testBound.right + testBound.left 应为 testBound.right - testBound.left。没贴出来的 postY 也要改下。

3.1.2 惯性滑动(Fling)

PinchImageView 的惯性滑动是自己处理衰减的…… 每次衰减的程度还一样,不支持插值器,比起PhotoView 使用 OverScroller 来处理滑动,就显得有点简陋了。

GestureDetectoronFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) 包含x、y轴的加速度,加速度单位是像素/秒,每秒60帧,转换成像素/帧即 velocityX/60velocityY/60PinchImageView 使用 FlingAnimator来做动画,动画更新初始滑动距离 velocityX/60,然后乘以衰减值(FLING_DAMPING_FACTOR,0.9),待下次更新使用。

//移动图像并给出结果
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 双指缩放、单指移动

双指缩放、单指移动是在 onTouch 里做的。

3.2.1 双指缩放

原理:记录双指在屏幕上距离,外部矩阵的缩放值与此距离相除的商为单位距离的缩放值,以这个缩放值去乘以双指滑动后的距离得到一个新的缩放值,用这个缩放值给外部矩阵做缩放变换得到最终的外部矩阵。

m S c a l e B a s e = m O u t e r M a t r i x . s c a l e i n i t i a l D i s t a n c e n e x t S c a l e = m S c a l e B a s e × n e w D i s t a n c e y ( n e x t S c a l e ) = k ( m S c a l e B a s e ) x 0 ( n e w D i s t a n c e ) mScaleBase = \frac{mOuterMatrix.scale}{initialDistance}\\ nextScale= mScaleBase \times newDistance\\ y(nextScale) = k(mScaleBase)x_0(newDistance) mScaleBase=initialDistancemOuterMatrix.scalenextScale=mScaleBase×newDistancey(nextScale)=k(mScaleBase)x0(newDistance)

很明显,mScaleBase 这个单位距离的缩放值是斜率,决定了双指缩放的速度。那么决定双指缩放速度的因素有:当前外部矩阵的缩放大小、双指间初始距离。外部矩阵缩放越大,双指间初始距离越小,双指滑动缩放越快。

还有一个要注意的是图片的缩放中心点,在 PinchImageView 中,双指缩放变换是在单位矩阵中进行的。所以当双指按下的时候需要记录外部矩阵变换之前的中心点,源码里用 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()记录上面提到的 mScaleBasemScaleCenter 。在 MotionEvent.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 矩阵的逆矩阵存到 targetMatrix 中,martrix.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);
    ……
}

很容易看懂,上面都讲过了。这里吐槽一下,如果 mOuterMatrix 发生过错切、旋转、透视变换,那不就废了吗?

还有一个多个手指抬起一个手指的情况。注释已经修改过了,很容易看懂。

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.1 单指移动

单指移动主要是调用 scrollBy,之前已经分析过了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值