uCrop源碼分析

我每周會寫一篇源代碼分析的文章,以后也可能會有其他主題.
如果你喜歡我寫的文章的話,歡迎關(guān)注我的新浪微博@達達達達sky
地址: http://weibo.com/u/2030683111
每周我會第一時間在微博分享我寫的文章,也會積極轉(zhuǎn)發(fā)更多有用的知識給大家.謝謝關(guān)注_,說不定什么時候會有福利哈.


項目地址:uCrop蟹肘,本文分析版本: 83b77c0

1.簡介

uCrop.png
uCrop.png

在項目開發(fā)中,我們難免會有一些功能,比如上傳用戶頭像,上傳圖片等等會使用到圖片裁剪的功能,為了節(jié)省開發(fā)時間,我們一般不會去自己開發(fā)一個圖片剪裁庫,這時候我們會去github上尋找各種開源的圖片裁剪庫,找來找去會發(fā)現(xiàn)目前最好用的就要數(shù)Yalantis公司出的uCrop了.這也是最近剛剛開源的一個庫,Yalantis專門寫了一篇文章來說明問什么會有uCrop這個項目,以及對比了目前主流的幾個圖片剪裁的項目,地址在這.英文不錯的同學(xué)可以去看看,話說英文對工程師來說還是很重要的,讀英文資料以及逛英文社區(qū)可以很好的拓寬我們的技術(shù)視野,也能第一時間接觸最前沿的技術(shù)崩泡。好了廢話不多說,今天我們就來看看uCrop這個目前最好的圖片剪裁庫是如何使用以及實現(xiàn)的:

2.使用方法

作為最好用的圖片剪裁庫,uCrop的使用方法當(dāng)然是相當(dāng)簡單的:

1.AndroidManifest中注冊UCropActivity

    <activity
        android:name="com.yalantis.ucrop.UCropActivity"
        android:screenOrientation="portrait"
        android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>

2.配置uCrop參數(shù)

你可以通過建造者模式來創(chuàng)建一個uCrop對象,并且可以通過UCrop.Options來設(shè)置一些個性化的參數(shù):

    UCrop.of(sourceUri, destinationUri)
        .withAspectRatio(16, 9)
        .withMaxResultSize(maxWidth, maxHeight)
        .start(context);

其中sourceUri代表選擇圖片的Uri地址,destinationUri代表圖片裁剪完畢后保存的Uri地址,withAspectRatio(16, 9)表示設(shè)定你需要裁剪的圖片的寬高比,這里表示希望16:9,當(dāng)然你也可以選擇useSourceImageAspectRatio()來確保裁剪后的圖片的寬高比和原圖相同,如果選擇了上面兩項,則進入裁剪界面后是無法再改變裁剪的寬高比的。如果你需要動態(tài)的改變圖片裁剪的比例,那么什么都不設(shè)置就好了。withMaxResultSize(maxWidth, maxHeight)設(shè)置裁剪后的圖片的最大寬度和高度,start(context)方法即可開啟裁剪.

uCrop很友善的提供給了我們更多可以自定義方法,我們可以通過UCrop.Options來設(shè)定:


    UCrop.Options options = new UCrop.Options();
    //設(shè)置裁剪圖片的保存格式
    options.setCompressionFormat(Bitmap.CompressFormat.PNG);
    //設(shè)置裁剪圖片的圖片質(zhì)量
    options.setCompressionQuality(90);
    //設(shè)置你想要指定的可操作的手勢
    options.setAllowedGestures(UCropActivity.SCALE, UCropActivity.ROTATE, UCropActivity.ALL);
    //設(shè)置uCropActivity里的UI樣式
    options.setMaxScaleMultiplier(5);
    options.setImageToCropBoundsAnimDuration(666);
    options.setDimmedLayerColor(Color.CYAN);
    options.setOvalDimmedLayer(true);
    options.setShowCropFrame(false);
    options.setCropGridStrokeWidth(20);
    options.setCropGridColor(Color.GREEN);
    options.setCropGridColumnCount(2);
    options.setCropGridRowCount(1);
    //最后別忘記調(diào)用
    uCrop.withOptions(options);

3.在onActivityResult中處理裁剪后的結(jié)果

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode == RESULT_OK && requestCode == UCrop.REQUEST_CROP) {
            final Uri resultUri = UCrop.getOutput(data);
        } else if (resultCode == UCrop.RESULT_ERROR) {
            final Throwable cropError = UCrop.getError(data);
        }
    }

3.類關(guān)系圖

uCrop-classes-relation.png

uCrop的整體設(shè)計非常的清晰,這里的類圖我省去了UCrop類和UCropActivity類,我只畫了最核心功能的類圖,從類圖上來看UCropView包含了OverlayView是用來繪制裁剪頁面上的裁剪格子的,整體的裁剪功能都是通過GestureCropImageView繼承CropImageView繼承TransformImageView然后最終繼承自ImageView的這三個類來完成了,GestureCropImageView負責(zé)監(jiān)聽各種手勢然后調(diào)用父類的方法來完成圖片的旋轉(zhuǎn),方法和位移操作,CropImageView是用來完成圖片裁剪工作,以及確保圖片是處在正確的狀態(tài),以及負責(zé)完成一些動畫.TransformImageView則是負責(zé)旋轉(zhuǎn),放大,縮小以及位移操作的凝颇。我們先對uCrop有一個整體的了解,下面我們就來具體看看uCrop是如何實現(xiàn)的:

4.源碼分析

1.UCrop和UCropActivity的實現(xiàn)

UCropActivity就是我們用來裁剪照片的Activity了,對于Activity大家應(yīng)該都很熟悉了,我們就不多說了.而UCrop類在前面的使用方法中我們也介紹過了,主要是用來提供一個入口以及一系列的調(diào)用方法和提供自定義參數(shù)的設(shè)定,在此類開源項目中很常見,下面我們就主要介紹uCrop庫核心的裁剪功能是如何實現(xiàn)的。

2.調(diào)用流程分析

看到這里,我假設(shè)大家已經(jīng)有使用過uCrop或者已經(jīng)至少把項目clone下來run了一遍體驗了一下了,在uCrop里我們可以通過手勢來縮放,旋轉(zhuǎn)圖片由驹。那么我們就從一次雙指縮放的手勢來對整個調(diào)用流程進行分析:

(1)GestureCropImageView的實現(xiàn)

在看這類有UI控件的項目的時候,我們一般直接找到對應(yīng)控件然后看具體是如何實現(xiàn)的就行了,這里我們看了UCropActivity的布局以及代碼就能知道我們想知道的就在GestureCropImageView中,所以我們直接看看GestureCropImageView是如何實現(xiàn)的:


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的實現(xiàn),這里省略了構(gòu)造方法以及其余一些代碼,總體代碼不多,我們可以很清楚的看到首先初始化了ScaleGestureDetector,RotationGestureDetectorGestureDetector三個手勢監(jiān)聽的相關(guān)類,然后在onTouchEvent()方法中依次交給這三個GestureDetector來處理觸摸事件邓厕。如果有指定的觸摸事件發(fā)生則會回調(diào)對應(yīng)的接口,然后就執(zhí)行相應(yīng)的操作了草讶。

在使用uCrop中我們發(fā)現(xiàn)可以將圖片拖動出我們的裁剪框之外,但是松手之后圖片都會自動回彈回去并自動適應(yīng)裁剪框,當(dāng)縮放或者旋轉(zhuǎn)操作時都有可能觸發(fā),那么這個到底是如何實現(xiàn)的呢?實際上就是上面onTouchEvent()方法中最后調(diào)用的那個方法setImageToWrapCropBounds();中實現(xiàn)的,我們下面會詳細分析洽糟。那么好我們已經(jīng)知道了GestureCropImageView類是如何實現(xiàn)以及它的職責(zé)了,那么現(xiàn)在我們假設(shè)我們做了一個雙指縮放的手勢,這將會回調(diào)ScaleListeneronScale()方法,代碼如下:


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

可以看到調(diào)用了postScale()方法,其中detector.getScaleFactor()是表示當(dāng)前兩個手指之間的距離與上一次移動的手指距離之比,mMidPntXmMidPntY是量手指之間的中心點坐標,我們跟進postScale()方法,發(fā)現(xiàn)它是在CropImageView里也就是GestureCropImageView的父類中實現(xiàn)的,接下來我們轉(zhuǎn)到CropImageView中。

(2)CropImageView中postScale()的實現(xiàn)

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

很簡單,就是判斷了還是否可以縮放,然后繼續(xù)調(diào)用了父類的postScale()方法,我們知道CropImageView父類是TransformImageView那我們繼續(xù)來看:

(3)TransformImageView中postScale()的實現(xiàn)


    public void postScale(float deltaScale, float px, float py) {
        if (deltaScale != 0) {
            //變化當(dāng)前的matrix對象
            mCurrentImageMatrix.postScale(deltaScale, deltaScale, px, py);
            //設(shè)置ImageMatrix
            setImageMatrix(mCurrentImageMatrix);
            //回調(diào)mTransformImageListener接口
            if (mTransformImageListener != null) {
                mTransformImageListener.onScale(getMatrixScale(mCurrentImageMatrix));
            }
        }
    }

可以看到TransformImageView中根據(jù)設(shè)置ImageView中的matrix對象來使圖片進行縮放變化的,Matrix在我們進行圖像變換處理時經(jīng)常用到,具體的介紹和詳解請參照這篇文章到涂。如果原理看不懂可以直接看下面代碼中是如何使用的即可脊框。

然后我們看到setImageMatrix(mCurrentImageMatrix);又調(diào)用了updateCurrentImagePoints();方法:


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

這里的mCurrentImageMatrix.mapPoints(float[] dst, float[] src);方法的意思就是將src數(shù)組通過這個matrix變換賦值給dst數(shù)組,在這里的意思就是將最初我們保存的四個頂點的數(shù)組mInitialImageCorners通過這個matrix變換后賦值給mCurrentImageCorners颁督。同樣mInitialImageCenter中保存的中點坐標也進行對應(yīng)的操作,之所以保存這些是因為我們接下來的運算要使用.

其實到這里我們一次縮放的手勢就分析完了,這時候如果我們將手指抬起就會調(diào)用GestureCropImageView中的setImageToWrapCropBounds();方法,前面我們已經(jīng)介紹了這個方法的作用,下面我們就具體來看看它是怎么實現(xiàn)的:

3.setImageToWrapCropBounds()方法的實現(xiàn)

setImageToWrapCropBounds();方法是在CropImageView里實現(xiàn)的:


    public void setImageToWrapCropBounds() {
        setImageToWrapCropBounds(true);
    }

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

直接調(diào)用了setImageToWrapCropBounds(boolean animate);所以這里傳入的bool值就是是否做動畫,這里是true,這里先判斷了isImageWrapCropBounds(),如果返回false才執(zhí)行具體的代碼.這里我們先省略,那么isImageWrapCropBounds()方法從名字上看是檢測圖片當(dāng)前是不是包裹了裁剪的區(qū)域践啄。如果返回是false那么里面的邏輯肯定是對圖片進行位移或者縮放變換然后充滿裁剪區(qū)域。我們先來看看isImageWrapCropBounds()如何實現(xiàn)的:


    protected boolean isImageWrapCropBounds() {
        //將當(dāng)前保存的圖片的四個頂點數(shù)組傳入
        return isImageWrapCropBounds(mCurrentImageCorners);
    }

    protected boolean isImageWrapCropBounds(float[] imageCorners) {
        mTempMatrix.reset();
        //利用一個matrix先逆旋轉(zhuǎn)當(dāng)前旋轉(zhuǎn)的角度.
        mTempMatrix.setRotate(-getCurrentAngle());
        //得到不旋轉(zhuǎn)的圖片的頂點數(shù)組
        float[] unrotatedImageCorners = Arrays.copyOf(imageCorners, imageCorners.length);
        mTempMatrix.mapPoints(unrotatedImageCorners);
        //先從mCropRect得到四個頂點數(shù)組,然后做matrix變換,這里就有逆向的旋轉(zhuǎn)變換了
        float[] unrotatedCropBoundsCorners = RectUtils.getCornersFromRect(mCropRect);
        mTempMatrix.mapPoints(unrotatedCropBoundsCorners);
        //最后比較當(dāng)前圖片所形成的Rect是否包含旋轉(zhuǎn)過后的mCropRect所形成的Rect
        //RectUtils.trapToRect(float[] array)方法是用來獲得包含當(dāng)前所有點的最小矩形
        return RectUtils.trapToRect(unrotatedImageCorners).contains(RectUtils.trapToRect(unrotatedCropBoundsCorners));
    }

因為這里也算是一個比較trick的做法,先將原圖轉(zhuǎn)換成未旋轉(zhuǎn)的狀態(tài),然后再旋轉(zhuǎn)我們裁剪的區(qū)域,然后獲得到這個區(qū)域形成的最小矩形,看看是否包含在原圖的區(qū)域中來判斷裁剪區(qū)域是否完全在圖片中,這里也有一篇uCrop的開發(fā)者寫的文章我們是如何開發(fā)uCrop的沉御。里面同樣解釋了為何這樣做屿讽。

如果這里返回了false就會執(zhí)行if里的內(nèi)容,那么我們來看看到底是如何實現(xiàn)的:


    public void setImageToWrapCropBounds(boolean animate) {
        if (!isImageWrapCropBounds()) {
            //得到當(dāng)前圖片的中心點坐標
            float currentX = mCurrentImageCenter[0];
            float currentY = mCurrentImageCenter[1];
            //得到當(dāng)前圖片的縮放比例
            float currentScale = getCurrentScale();
            //得到裁剪區(qū)域中心和當(dāng)前圖片中心的偏移量
            float deltaX = mCropRect.centerX() - currentX;
            float deltaY = mCropRect.centerY() - currentY;
            float deltaScale = 0;
            //給mTempMatrix 設(shè)置偏移量
            mTempMatrix.reset();
            mTempMatrix.setTranslate(deltaX, deltaY);
            //對圖片做matrix變換.
            final float[] tempCurrentImageCorners = Arrays.copyOf(mCurrentImageCorners, mCurrentImageCorners.length);
            mTempMatrix.mapPoints(tempCurrentImageCorners);
            //做完變換后再檢測是否平移變換之后,圖片就充滿了裁剪區(qū)域
            boolean willImageWrapCropBoundsAfterTranslate = isImageWrapCropBounds(tempCurrentImageCorners);

            if (willImageWrapCropBoundsAfterTranslate) {
                //如果平移轉(zhuǎn)換就可以充滿裁剪區(qū)域
                //那么就找出最合適的偏移量
                final float[] imageIndents = calculateImageIndents();
                deltaX = -(imageIndents[0] + imageIndents[2]);
                deltaY = -(imageIndents[1] + imageIndents[3]);
            } else {
                //如果不能充滿裁剪區(qū)域
                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總體上是如何實現(xiàn)的,關(guān)于怎么加載的圖片以及如何裁剪的圖片我們這篇文章就不分析了,有興趣的同學(xué)可以自行研究吠裆。

5.個人評價

不愧是目前最優(yōu)秀的圖片剪裁庫,無論從整個庫產(chǎn)品方面的設(shè)計還是從代碼的結(jié)構(gòu)上來看,uCrop都是值得我們學(xué)習(xí)與使用的,以前也閱讀過其他裁剪項目的源代碼,整體對比上來看uCrop提供的方法以及定制性和易用性都是很棒的,值得推薦!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末伐谈,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子试疙,更是在濱河造成了極大的恐慌诵棵,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,919評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件祝旷,死亡現(xiàn)場離奇詭異履澳,居然都是意外死亡,警方通過查閱死者的電腦和手機怀跛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評論 3 392
  • 文/潘曉璐 我一進店門距贷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人吻谋,你說我怎么就攤上這事忠蝗。” “怎么了漓拾?”我有些...
    開封第一講書人閱讀 163,316評論 0 353
  • 文/不壞的土叔 我叫張陵阁最,是天一觀的道長。 經(jīng)常有香客問我骇两,道長速种,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,294評論 1 292
  • 正文 為了忘掉前任脯颜,我火速辦了婚禮哟旗,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己闸餐,他們只是感情好饱亮,可當(dāng)我...
    茶點故事閱讀 67,318評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著舍沙,像睡著了一般近上。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拂铡,一...
    開封第一講書人閱讀 51,245評論 1 299
  • 那天壹无,我揣著相機與錄音,去河邊找鬼感帅。 笑死斗锭,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的失球。 我是一名探鬼主播岖是,決...
    沈念sama閱讀 40,120評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼实苞!你這毒婦竟也來了豺撑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,964評論 0 275
  • 序言:老撾萬榮一對情侶失蹤黔牵,失蹤者是張志新(化名)和其女友劉穎聪轿,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體猾浦,經(jīng)...
    沈念sama閱讀 45,376評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡陆错,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,592評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了跃巡。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片危号。...
    茶點故事閱讀 39,764評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖素邪,靈堂內(nèi)的尸體忽然破棺而出外莲,到底是詐尸還是另有隱情,我是刑警寧澤兔朦,帶...
    沈念sama閱讀 35,460評論 5 344
  • 正文 年R本政府宣布偷线,位于F島的核電站,受9級特大地震影響沽甥,放射性物質(zhì)發(fā)生泄漏声邦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,070評論 3 327
  • 文/蒙蒙 一摆舟、第九天 我趴在偏房一處隱蔽的房頂上張望亥曹。 院中可真熱鬧邓了,春花似錦、人聲如沸媳瞪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蛇受。三九已至句葵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間兢仰,已是汗流浹背乍丈。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留把将,地道東北人轻专。 一個月前我還...
    沈念sama閱讀 47,819評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像秸弛,于是被迫代替她去往敵國和親铭若。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,665評論 2 354

推薦閱讀更多精彩內(nèi)容