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)
以下解释是为了方便记忆,不保证科学性。
左乘以: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。得出结论:左乘以 == 右乘,右乘以 == 左乘。
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原理。这里引用几个重要的变换。
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Δy1⎦⎤⋅⎣⎡x0y01⎦⎤
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⎦⎤=⎣⎡k1000K20001⎦⎤⋅⎣⎡x0y01⎦⎤
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θ0−sinθcosθ0001⎦⎤⋅⎣⎡x0y01⎦⎤
旋转是一个矩阵元素组合实现。
Matrix 里有很多 pre 、 post 方法。pre 表示前乘(原始Matrix放前面),A pre B 输出 AB,按照我们刚才对矩阵左乘右乘的解释,前乘相当于矩阵的左乘以、右乘。post 表示后乘(原始Matrix放后面),A post B 输出 BA,后乘相当于矩阵的右乘以、左乘。
Matrix 里常用的变换有 平移(Translate)、旋转(Rotate)、缩放(Scale)。
具体各个方法使用参考:安卓自定义View进阶-Matrix详解,各方法在 Matrix 类里的注解都非常详细, 很多方法基本一看注解就能懂。
这里放几个典型的。
这个方法是 src 到 dst 的变换,可以理解为把 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 上。
mapRect有两个方法,具体含义写在注释中。
/** * 对 rect 进行 matrix 变换,并将结果存在 rect 中 */ public boolean mapRect(RectF rect) {} /** * 对 src 进行 matrix 变换,并将结果存在 dst中 */ public boolean mapRect(RectF dst, RectF src) {}这个比较复杂,建议详细看下安卓自定义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相当于reset1相当于translate2可以进行 缩放、旋转、平移 变换3可以进行 缩放、旋转、平移、错切 变换4可以进行 缩放、旋转、平移、错切以及任何形变PinchImageView 使用 GestureDetector 来处理长按、点击、双击、惯性滑动事件,在 onTouchEvent 里处理双指缩放和单指移动等事件。
里面有两个矩阵,一个是外部变换矩阵(mOuterMatrix),主要记录手势操作的结果,一个是内部变换矩阵(getInnerMatrix(Matrix)),就是根据 fitCenter 等缩放模式进行缩放平移后的初始矩阵。这里区分两个矩阵可能是借鉴了PhotoView的经验,手势操作和原始缩放互不影响,手势操作后最终的缩放只需要两个矩阵相乘就好了。
下面的代码分析不一定会完全贴源码,有的是经过稍微改动的。
长按和点击就是调用回调,我们主要来看双击和惯性滑动。
PinchImageView 只做了一级的放大缩小 ,就是说只能在最大和初始缩放值之间切换。
基本原理:捕获双击事件,拿到双击点的x、y坐标,对图片进行缩放变换,将双击点位置移动到视图中间。
代码较长,我们一点点拆分。
这里要先介绍下 PinchImageView 的对象池(ObjectsPool)。
ObjectsPool 维护一个对象队列,在容量范围内可以循环复用对象。大致使用流程如下图所示:
在队列里获取 innerMatrix 对象(take()),队列为空则新建一个对象返回,否则出队一个对象重置后返回。在队列里获取 targetMatrix 对象。使用完 targetMatrix 归还(given(obj))。使用完 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.rectFTake 跟 matrixTake 方法是一样的,只是取出的是 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 也要改下。
PinchImageView 的惯性滑动是自己处理衰减的…… 每次衰减的程度还一样,不支持插值器,比起PhotoView 使用 OverScroller 来处理滑动,就显得有点简陋了。
GestureDetector 的 onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) 包含x、y轴的加速度,加速度单位是像素/秒,每秒60帧,转换成像素/帧即 velocityX/60 、 velocityY/60。PinchImageView 使用 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 给图片设置新矩阵。
双指缩放、单指移动是在 onTouch 里做的。
原理:记录双指在屏幕上距离,外部矩阵的缩放值与此距离相除的商为单位距离的缩放值,以这个缩放值去乘以双指滑动后的距离得到一个新的缩放值,用这个缩放值给外部矩阵做缩放变换得到最终的外部矩阵。
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()记录上面提到的 mScaleBase 和 mScaleCenter 。在 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; } }注释是我改过的。
单指移动主要是调用 scrollBy,之前已经分析过了。