我每周會寫一篇源代碼分析的文章,以后也可能會有其他主題.
如果你喜歡我寫的文章的話,歡迎關(guān)注我的新浪微博@達達達達sky
地址: http://weibo.com/u/2030683111
每周我會第一時間在微博分享我寫的文章,也會積極轉(zhuǎn)發(fā)更多有用的知識給大家.謝謝關(guān)注_,說不定什么時候會有福利哈.
1.簡介
在項目開發(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
的整體設(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
,RotationGestureDetector
和GestureDetector
三個手勢監(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)ScaleListener
的onScale()
方法,代碼如下:
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)前兩個手指之間的距離與上一次移動的手指距離之比,mMidPntX
和mMidPntY
是量手指之間的中心點坐標,我們跟進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
提供的方法以及定制性和易用性都是很棒的,值得推薦!