uCrop源码分析

作者: SkyKai | 来源:发表于2016-03-20 14:37 被阅读3881次

    我每周会写一篇源代码分析的文章,以后也可能会有其他主题.
    如果你喜欢我写的文章的话,欢迎关注我的新浪微博@达达达达sky
    地址: http://weibo.com/u/2030683111
    每周我会第一时间在微博分享我写的文章,也会积极转发更多有用的知识给大家.谢谢关注_,说不定什么时候会有福利哈.


    项目地址:uCrop,本文分析版本: 83b77c0

    1.简介

    uCrop-classes-relation.png

    uCrop的整体设计非常的清晰,这里的类图我省去了UCrop类和UCropActivity类,我只画了最核心功能的类图,从类图上来看UCropView包含了OverlayView是用来绘制裁剪页面上的裁剪格子的,整体的裁剪功能都是通过GestureCropImageView继承CropImageView继承TransformImageView然后最终继承自ImageView的这三个类来完成了,GestureCropImageView负责监听各种手势然后调用父类的方法来完成图片的旋转,方法和位移操作,CropImageView是用来完成图片裁剪工作,以及确保图片是处在正确的状态,以及负责完成一些动画.TransformImageView则是负责旋转,放大,缩小以及位移操作的。我们先对uCrop有一个整体的了解,下面我们就来具体看看uCrop是如何实现的:

    4.源码分析

    1.UCrop和UCropActivity的实现

    UCropActivity就是我们用来裁剪照片的Activity了,对于Activity大家应该都很熟悉了,我们就不多说了.而UCrop类在前面的使用方法中我们也介绍过了,主要是用来提供一个入口以及一系列的调用方法和提供自定义参数的设定,在此类开源项目中很常见,下面我们就主要介绍uCrop库核心的裁剪功能是如何实现的。

    2.调用流程分析

    看到这里,我假设大家已经有使用过uCrop或者已经至少把项目clone下来run了一遍体验了一下了,在uCrop里我们可以通过手势来缩放,旋转图片。那么我们就从一次双指缩放的手势来对整个调用流程进行分析:

    (1)GestureCropImageView的实现

    在看这类有UI控件的项目的时候,我们一般直接找到对应控件然后看具体是如何实现的就行了,这里我们看了UCropActivity的布局以及代码就能知道我们想知道的就在GestureCropImageView中,所以我们直接看看GestureCropImageView是如何实现的:

    
    public class GestureCropImageView extends CropImageView {
    
        private ScaleGestureDetector mScaleDetector;
        private RotationGestureDetector mRotateDetector;
        private GestureDetector mGestureDetector;
        private float mMidPntX, mMidPntY;
    
        ...
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
                cancelAllAnimations();
            }
            //如果有多个手指触摸,则计算出两个手指之前的中心点坐标
            if (event.getPointerCount() > 1) {
                mMidPntX = (event.getX(0) + event.getX(1)) / 2;
                mMidPntY = (event.getY(0) + event.getY(1)) / 2;
            }
            //依次将事件传递给mGestureDetector,mIsRotateEnabled,mRotateDetector处理
            mGestureDetector.onTouchEvent(event);
    
            if (mIsScaleEnabled) {
                mScaleDetector.onTouchEvent(event);
            }
    
            if (mIsRotateEnabled) {
                mRotateDetector.onTouchEvent(event);
            }
            //如果手指离开,则检测图片是否完全充满裁剪框,如果没有则缩放至完全充满裁剪框
            if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
                setImageToWrapCropBounds();
            }
            return true;
        }
    
        @Override
        protected void init() {
            super.init();
            setupGestureListeners();
        }
    
        private void setupGestureListeners() {
            mGestureDetector = new GestureDetector(getContext(), new GestureListener(), null, true);
            mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());
            mRotateDetector = new RotationGestureDetector(new RotateListener());
        }
        ...
    }
    

    上面就是GestureCropImageView的实现,这里省略了构造方法以及其余一些代码,总体代码不多,我们可以很清楚的看到首先初始化了ScaleGestureDetector,RotationGestureDetectorGestureDetector三个手势监听的相关类,然后在onTouchEvent()方法中依次交给这三个GestureDetector来处理触摸事件。如果有指定的触摸事件发生则会回调对应的接口,然后就执行相应的操作了。

    在使用uCrop中我们发现可以将图片拖动出我们的裁剪框之外,但是松手之后图片都会自动回弹回去并自动适应裁剪框,当缩放或者旋转操作时都有可能触发,那么这个到底是如何实现的呢?实际上就是上面onTouchEvent()方法中最后调用的那个方法setImageToWrapCropBounds();中实现的,我们下面会详细分析。那么好我们已经知道了GestureCropImageView类是如何实现以及它的职责了,那么现在我们假设我们做了一个双指缩放的手势,这将会回调ScaleListeneronScale()方法,代码如下:

    
        private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
            @Override
            public boolean onScale(ScaleGestureDetector detector) {
                postScale(detector.getScaleFactor(), mMidPntX, mMidPntY);
                return true;
            }
        }
    

    可以看到调用了postScale()方法,其中detector.getScaleFactor()是表示当前两个手指之间的距离与上一次移动的手指距离之比,mMidPntXmMidPntY是量手指之间的中心点坐标,我们跟进postScale()方法,发现它是在CropImageView里也就是GestureCropImageView的父类中实现的,接下来我们转到CropImageView中。

    (2)CropImageView中postScale()的实现

        public void postScale(float deltaScale, float px, float py) {
            if (deltaScale > 1 && getCurrentScale() * deltaScale <= getMaxScale()) {
                super.postScale(deltaScale, px, py);
            } else if (deltaScale < 1 && getCurrentScale() * deltaScale >= getMinScale()) {
                super.postScale(deltaScale, px, py);
            }
        }
    

    很简单,就是判断了还是否可以缩放,然后继续调用了父类的postScale()方法,我们知道CropImageView父类是TransformImageView那我们继续来看:

    (3)TransformImageView中postScale()的实现

    
        public void postScale(float deltaScale, float px, float py) {
            if (deltaScale != 0) {
                //变化当前的matrix对象
                mCurrentImageMatrix.postScale(deltaScale, deltaScale, px, py);
                //设置ImageMatrix
                setImageMatrix(mCurrentImageMatrix);
                //回调mTransformImageListener接口
                if (mTransformImageListener != null) {
                    mTransformImageListener.onScale(getMatrixScale(mCurrentImageMatrix));
                }
            }
        }
    

    可以看到TransformImageView中根据设置ImageView中的matrix对象来使图片进行缩放变化的,Matrix在我们进行图像变换处理时经常用到,具体的介绍和详解请参照这篇文章。如果原理看不懂可以直接看下面代码中是如何使用的即可。

    然后我们看到setImageMatrix(mCurrentImageMatrix);又调用了updateCurrentImagePoints();方法:

    
        private void updateCurrentImagePoints() {
            mCurrentImageMatrix.mapPoints(mCurrentImageCorners, mInitialImageCorners);
            mCurrentImageMatrix.mapPoints(mCurrentImageCenter, mInitialImageCenter);
        }
    

    这里的mCurrentImageMatrix.mapPoints(float[] dst, float[] src);方法的意思就是将src数组通过这个matrix变换赋值给dst数组,在这里的意思就是将最初我们保存的四个顶点的数组mInitialImageCorners通过这个matrix变换后赋值给mCurrentImageCorners。同样mInitialImageCenter中保存的中点坐标也进行对应的操作,之所以保存这些是因为我们接下来的运算要使用.

    其实到这里我们一次缩放的手势就分析完了,这时候如果我们将手指抬起就会调用GestureCropImageView中的setImageToWrapCropBounds();方法,前面我们已经介绍了这个方法的作用,下面我们就具体来看看它是怎么实现的:

    3.setImageToWrapCropBounds()方法的实现

    setImageToWrapCropBounds();方法是在CropImageView里实现的:

    
        public void setImageToWrapCropBounds() {
            setImageToWrapCropBounds(true);
        }
    
        public void setImageToWrapCropBounds(boolean animate) {
            if (!isImageWrapCropBounds()) {
                ...
            }
        }
    

    直接调用了setImageToWrapCropBounds(boolean animate);所以这里传入的bool值就是是否做动画,这里是true,这里先判断了isImageWrapCropBounds(),如果返回false才执行具体的代码.这里我们先省略,那么isImageWrapCropBounds()方法从名字上看是检测图片当前是不是包裹了裁剪的区域。如果返回是false那么里面的逻辑肯定是对图片进行位移或者缩放变换然后充满裁剪区域。我们先来看看isImageWrapCropBounds()如何实现的:

    
        protected boolean isImageWrapCropBounds() {
            //将当前保存的图片的四个顶点数组传入
            return isImageWrapCropBounds(mCurrentImageCorners);
        }
    
        protected boolean isImageWrapCropBounds(float[] imageCorners) {
            mTempMatrix.reset();
            //利用一个matrix先逆旋转当前旋转的角度.
            mTempMatrix.setRotate(-getCurrentAngle());
            //得到不旋转的图片的顶点数组
            float[] unrotatedImageCorners = Arrays.copyOf(imageCorners, imageCorners.length);
            mTempMatrix.mapPoints(unrotatedImageCorners);
            //先从mCropRect得到四个顶点数组,然后做matrix变换,这里就有逆向的旋转变换了
            float[] unrotatedCropBoundsCorners = RectUtils.getCornersFromRect(mCropRect);
            mTempMatrix.mapPoints(unrotatedCropBoundsCorners);
            //最后比较当前图片所形成的Rect是否包含旋转过后的mCropRect所形成的Rect
            //RectUtils.trapToRect(float[] array)方法是用来获得包含当前所有点的最小矩形
            return RectUtils.trapToRect(unrotatedImageCorners).contains(RectUtils.trapToRect(unrotatedCropBoundsCorners));
        }
    

    因为这里也算是一个比较trick的做法,先将原图转换成未旋转的状态,然后再旋转我们裁剪的区域,然后获得到这个区域形成的最小矩形,看看是否包含在原图的区域中来判断裁剪区域是否完全在图片中,这里也有一篇uCrop的开发者写的文章我们是如何开发uCrop的。里面同样解释了为何这样做。

    如果这里返回了false就会执行if里的内容,那么我们来看看到底是如何实现的:

    
        public void setImageToWrapCropBounds(boolean animate) {
            if (!isImageWrapCropBounds()) {
                //得到当前图片的中心点坐标
                float currentX = mCurrentImageCenter[0];
                float currentY = mCurrentImageCenter[1];
                //得到当前图片的缩放比例
                float currentScale = getCurrentScale();
                //得到裁剪区域中心和当前图片中心的偏移量
                float deltaX = mCropRect.centerX() - currentX;
                float deltaY = mCropRect.centerY() - currentY;
                float deltaScale = 0;
                //给mTempMatrix 设置偏移量
                mTempMatrix.reset();
                mTempMatrix.setTranslate(deltaX, deltaY);
                //对图片做matrix变换.
                final float[] tempCurrentImageCorners = Arrays.copyOf(mCurrentImageCorners, mCurrentImageCorners.length);
                mTempMatrix.mapPoints(tempCurrentImageCorners);
                //做完变换后再检测是否平移变换之后,图片就充满了裁剪区域
                boolean willImageWrapCropBoundsAfterTranslate = isImageWrapCropBounds(tempCurrentImageCorners);
    
                if (willImageWrapCropBoundsAfterTranslate) {
                    //如果平移转换就可以充满裁剪区域
                    //那么就找出最合适的偏移量
                    final float[] imageIndents = calculateImageIndents();
                    deltaX = -(imageIndents[0] + imageIndents[2]);
                    deltaY = -(imageIndents[1] + imageIndents[3]);
                } else {
                    //如果不能充满裁剪区域
                    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]);
                    // Ugly but there are always couple pixels that want to hide because of all these calculations
                    deltaScale *= 1.01;
                    deltaScale = deltaScale * currentScale - currentScale;
                }
    
                //如果需要动画
                if (animate) {
                    post(mWrapCropBoundsRunnable = new WrapCropBoundsRunnable(
                            CropImageView.this, mImageToWrapCropBoundsAnimDuration, currentX, currentY, deltaX, deltaY,
                            currentScale, deltaScale, willImageWrapCropBoundsAfterTranslate));
                } else {
                    postTranslate(deltaX, deltaY);
                    if (!willImageWrapCropBoundsAfterTranslate) {
                        zoomInImage(currentScale + deltaScale, mCropRect.centerX(), mCropRect.centerY());
                    }
                }
            }
        }
    
    

    上面就是如何计算位移以及缩放的代码了,注意在最后的时候如果需要动画的话,则通过一个Runnable对象mWrapCropBoundsRunnable来进行动画,具体的逻辑大家可以自行去看看也是比较清晰的。

    至此我们就大致分析了uCrop总体上是如何实现的,关于怎么加载的图片以及如何裁剪的图片我们这篇文章就不分析了,有兴趣的同学可以自行研究。

    5.个人评价

    不愧是目前最优秀的图片剪裁库,无论从整个库产品方面的设计还是从代码的结构上来看,uCrop都是值得我们学习与使用的,以前也阅读过其他裁剪项目的源代码,整体对比上来看uCrop提供的方法以及定制性和易用性都是很棒的,值得推荐!

    相关文章

      网友评论

      • da1b76f3ca12:楼主你好!请问<activity
        android:name="com.yalantis.ucrop.UCropActivity"
        android:screenOrientation="portrait"
        android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>这里的UCropActivity在哪?还是我的理解有问题
      • df4a04c38b09:我也遇到了一个问题就是在裁剪图片那个页面,右上角一直转圈加载中,就是加载不出图片,但是有的时候就可以,不可以的情况居多,有大神知道怎么回事嘛?
      • Danny_Boy:uCrop作者Oleksii Shliama是我的同事。
      • 8ac8d5e34a1d:昨天刚才github看到就down下来跑了下,确实挺不错的,今天又看到楼主的源码分析,收益良多。感谢楼主的分析,赞
      • timmy_tan:现在我使用ucrop遇到一个需求问题,希望用户裁剪时预览是什么样,裁剪之后就是什么样的。但是ucrop在isImageWrapCropBounds(float[] imageCorners)这个方法中做了判断,但用户所选择的区域有一个点不再图片的范围内部,就会自动移动。这样剪切出来的图片就和预览时候的图片有微小的偏差。不知道楼主是否有好的解决办法。
        timmy_tan:我把这个 setImageToWrapCropBounds(boolean animate)做了如下修改setImageToWrapCropBounds(boolean animate, boolean save),保存的时候传true,其他情况传false,然后在else里面加入if (save) {
        Logger.d(TAG, "被裁剪的图片偏出裁剪框,这是保存过程");
        final float[] imageIndents = calculateImageIndents();
        deltaX = -(imageIndents[0] + imageIndents[2]);
        deltaY = -(imageIndents[1] + imageIndents[3]);
        },可以基本上解决我说的问题了。
        timmy_tan:@达达达达sky 不是这个意思,就是把图片移动到剪切框的边缘的时候,这个方法isImageWrapCropBounds(float[] imageCorners)有肯判断出,当前图片或许有些像素点不再剪切框内,就会做移动处理,走下面的else,然后裁剪出的图片很预览的图片就存在了偏移
        SkyKai:@timmy_one 你的意思是允许用户裁剪出有黑色区域的图片是吗?那么把这些判断去掉可能就可以了,如果位置不对那还要修改一些保存图片的代码
      • ColSmart:请问你那个画类关系图的工具是什么?
      • b71372c3de8a:请问,都是原创的么
        SkyKai: @雨墨北是的,全是原创
      • 8af2032729ed:请问大神,能开发安卓微信类软件么
        SkyKai:@chens5205 额 目前没做过类似的软件。。。一上来就要做微信啊 =。= 有点难。。
      • e13c67a94a46:stormzhang微信公众号过来的,学习!!!
        SkyKai:@EastYoung 共同学习
      • 下一页会是幸福吗:粉丝团助威
        SkyKai:@下一页会是幸福吗 谢谢支持~
      • Zack_zhou:一大波粉丝在路上,老夫先评为敬
        SkyKai:@Zack_zhou 谢谢支持 :blush:

      本文标题:uCrop源码分析

      本文链接:https://www.haomeiwen.com/subject/plhhlttx.html