uCrop源码思路分析--难点攻破

大家可以先参考流程梳理uCrop裁剪流程梳理
折腾这个开源库已经很久了,但是很多细节我也没有完全掌握,有的还需要画图分析,作者寥寥数语,我们要研究半天。
在折腾的时候会发现许多问题,有简单的,也有一时想不出来,得慢慢debug的。
如,在不放大的情况下进行平移。
切入点很重要,当然,我们要看ACTION_UP之后究竟发生了什么。

  if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
            setImageToWrapCropBounds();
  }

这个方法很核心,围绕着这个方法可以理解很多。
作者的解释是这样的:
如果图片没有填充满整个裁剪框的话,那么我们必须对图片进行平移,缩放操作来使其能够填满裁剪框。该方法计算出平移的距离,缩放的倍数,然后交给一个Runnable执行动画。
对于平移的距离,我只需要知道,裁剪框中心点的坐标和手指释放时图片的中心坐标就行了。
这里面有两个变量很重要

    /** * 当前图片的四个顶点的坐标 */
    protected final float[] mCurrentImageCorners = new float[RECT_CORNER_POINTS_COORDS];
    /** * 当前图片的中心点的坐标 */
    protected final float[] mCurrentImageCenter = new float[RECT_CENTER_POINT_COORDS];
   private void updateCurrentImagePoints() {
        mCurrentImageMatrix.mapPoints(mCurrentImageCorners, mInitialImageCorners);
        mCurrentImageMatrix.mapPoints(mCurrentImageCenter, mInitialImageCenter);
    }

上面一个方法很重要,每次我们执行postXX方法的时候,图片矩阵就会更新,有一点小细节,就是这个图片矩阵的变化时叠加的,比如,平移,这就类似于ScrollBy,每次增加一点点。Matrix的mapPoints方法是这样的,对源数组执行矩阵变换,将结果写入目标数组中。
所以,我们拿到了图片的实时顶点。
作者是这样做的,弄一个临时矩阵,copy一个临时数组,对这个数组进行矩阵平移变换,然后判断,平移后的矩阵是否被裁剪框包含(完全填充)。
另一个函数出现了

isImageWrapCropBounds(tempCurrentImageCorners)

判断一个矩形是否包含在另一个矩形里面。
难点:如果不是正的矩形呢?
就是说,裁剪框肯定是正矩形,而图片矩形,可能是旋转过的,如何判断一个旋转过的矩形是否完全填充一个正矩形。

   protected boolean isImageWrapCropBounds(float[] imageCorners) {
        mTempMatrix.reset();
        mTempMatrix.setRotate(-getCurrentAngle());
        float[] unrotatedImageCorners = Arrays.copyOf(imageCorners, imageCorners.length);
        mTempMatrix.mapPoints(unrotatedImageCorners);
        float[] unrotatedCropBoundsCorners = RectUtils.getCornersFromRect(mCropRect);
        mTempMatrix.mapPoints(unrotatedCropBoundsCorners);
        return RectUtils.trapToRect(unrotatedImageCorners).contains(RectUtils.trapToRect(unrotatedCropBoundsCorners));
    }

uCrop源码思路分析--难点攻破_第1张图片

uCrop源码思路分析--难点攻破_第2张图片
第一张是app显示出来的,裁剪框是不动的,图片是可以任意旋转的。由于我们无法在一个不正的矩形中判断自己是否包含另一个正的矩形。作者很机智做了处理,很好的解决的这个问题,看代码?
作者将图片和裁剪框都做了相同角度的反转,这样就变成了第二张图,蓝色的矩形是根据裁剪框的四个顶点得到的包含他的最小矩形,对应下面的函数

RectUtils.trapToRect(unrotatedCropBoundsCorners)

这样解释的话,上面的代码看起来就很清晰了(注意把握临时Matrix的作用).
回到setImageToWrapCropBounds()函数,刚才我们用临时矩阵对图片进行了平移,然后判断平移后的图片是否填充满了裁剪框。
如果答案为true
那说明根本不需要再进行缩放操作,直接平移就可以了。
false
计算出我们需要缩放的倍数。
注意临时矩阵的使用,我们并不会对具体的图形做出修改。

 RectF tempCropRect = new RectF(mCropRect);
                mTempMatrix.reset();
                mTempMatrix.setRotate(getCurrentAngle());
                mTempMatrix.mapRect(tempCropRect);
//旋转裁剪框
                final float[] currentImageSides = RectUtils.getRectSidesFromCorners(mCurrentImageCorners);//拿到当前图片的宽高

                deltaScale = Math.max(tempCropRect.width() / currentImageSides[0],
                        tempCropRect.height() / currentImageSides[1]);

作者是这样计算出缩放倍数的,将裁剪框进行一个同角度的同向旋转,此时tempCropRect 的四个顶点发生了变化,根据这四个顶点得出一个正矩形,至于作者为什么要这么做,确实让我想了一段时间,脑子一直没转过来哈,看来编程到了一定程度,确实是逻辑能力和算法能力了。
uCrop源码思路分析--难点攻破_第3张图片
蓝色的是裁剪框,黄色的是图片(注:初始时图片和裁剪框是吻合的),灰色的是图片需要放大的效果,那么我们怎么确定灰色的宽高呢?
这里面作者反复利用了一个思维,开始还真不好想,好多年没用这些知识了。
在之前判断矩形之间的包裹关系时,我们说到,判断不正的矩形是否包含正的矩形,在数学上实现起来很难,那作者是怎么做的呢?
他同时将裁剪框和图片进行反转,这样裁剪框就歪了,图片就正了,我们根据歪的裁剪框的顶点算出包含他的最小正矩形,如果这个正矩形完全在图片里面,就ok。
再来看看这个解决方法,同样,在数学上要求出灰色矩形的宽高是很有难度的(反正我不会),再利用上面的思想,利用歪的矩形算出包含他的最小正矩形。
uCrop源码思路分析--难点攻破_第4张图片
按照作者的思路,我们画出了这张图,看看他是怎样和上面一张联系起来的。
黄色的是裁剪框,青色的是图片,也是同角度旋转后的裁剪框矩形,外面灰色的是根据青色矩形得出的包含他的最小正矩形。
再回过头来看看我们的目的,我们需要知道图片放大后的那个矩形,那么这张图中的灰色矩形是我们需要的吗?
正是,因为在初始情况下,图片和裁剪框是吻合的,他们大小一致。我们最终只需要那个放大后的矩形的高宽而已,这个灰色的矩形和上图中的褐色矩形是一模一样的,所以,我们通过这种变换得到了我们所需要的矩形。
截止到这里,我们终于算出了两个最重要的参数–距离和倍数,剩下的就是根据这两个参数进行UI操作了。
作者在这里声明了一个Runnable来做这件事情。
但是作者的那一套计算时间流逝的方法本人实在看不懂,直接用ValueAnimator做会简单很多。做动画的具体细节就不详述了。

到这里,我们还有最后一个难点,裁剪,就是要理解裁剪的逻辑,裁剪究竟是做了什么。
当用户按下了裁剪按钮后,作者开启了一个后台线程来完成这一功能。
下面是三个很重要的方法:

    if (mMaxResultImageSizeX > 0 && mMaxResultImageSizeY > 0) { resize(); }

    if (mCurrentAngle != 0) { rotate(); }

    crop();

如果用户执行的裁剪的最小值和最大值,那么我们势必要做出一些调整。

  private void resize() {
  /**
  * 如果显示的图片放大了,而原来的bitmap并没有改变,我们实质是要从
  * 原生的bitmap,加上裁剪框的宽高创建出新的bitmap,那么,这里肯定要
  * 做一个除法运算
  /
        float cropWidth = mCropRect.width() / mCurrentScale;
        float cropHeight = mCropRect.height() / mCurrentScale;

        if (cropWidth > mMaxResultImageSizeX || cropHeight > mMaxResultImageSizeY) {

            float scaleX = mMaxResultImageSizeX / cropWidth;
            float scaleY = mMaxResultImageSizeY / cropHeight;
            float resizeScale = Math.min(scaleX, scaleY);

            Bitmap resizedBitmap = Bitmap.createScaledBitmap(mViewBitmap,
                    Math.round(mViewBitmap.getWidth() * resizeScale),
                    Math.round(mViewBitmap.getHeight() * resizeScale), false);
            if (mViewBitmap != resizedBitmap) {
                mViewBitmap.recycle();
            }
            mViewBitmap = resizedBitmap;

            mCurrentScale /= resizeScale;
        }
    }

我自己先梳理一个概念:Matrix并没有改变Bitmap,也就是我们最初设置给这个ImageView的图片,就算我们用Matrix对其进行了方大等等操作,这只是控件在绘制图片的时候将Matrix考虑了进来,而没有改变原来的Bitmap对象。
把这个概念弄清楚后,后面的rotate()和crop()操作也就很好理解了。

你可能感兴趣的:(源码,开源)