UCrop是目前比較火的開源android圖片剪裁框架,效果如下:
地址:
Git源碼地址
本文重點(diǎn)解釋核心功能代碼变勇,梳理項(xiàng)目流程坦报,建議閱讀時(shí)結(jié)合源碼一起看~司抱。
業(yè)務(wù)流程:
選擇圖片(從系統(tǒng)圖片庫選擇圖片)→ 放置圖片(把圖片放置到操作臺)→ 操作圖片(包括旋轉(zhuǎn),縮放,位移等操作來得到需要的圖片)→ 剪裁圖片(根據(jù)原始比例剪裁框剪裁目標(biāo)圖片或根據(jù)給定的比例剪裁)→ 獲得目標(biāo)圖片(返回bitmap并保存到本地)
代碼結(jié)構(gòu)解析:
項(xiàng)目使用Bulider設(shè)計(jì)模式碴犬,結(jié)構(gòu)功能分工明確,下面就來看看作者是怎么實(shí)現(xiàn)的梆暮,注意看核心代碼的注釋
代碼結(jié)構(gòu)大致分為三個(gè)部分:
-
第一部分: UCropAcitivity(圖片的操作窗口)
它主要作為項(xiàng)目的入口和一些初始化工作以及加載自定義View服协;
使用時(shí)直接傳入一個(gè)目標(biāo)圖片的URI和目標(biāo)存儲文件的URI即可開始剪裁。
這里作者封裝了一個(gè)UCropActivity的helper類UCrop啦粹,把startActivityForResult和Bundle封裝在了里面偿荷,Options內(nèi)部類用作用戶初始參數(shù)配置包括(setToolbarCropDrawable設(shè)置toolbar圖片,setToolbarTitle標(biāo)題等等)唠椭。UCropActivity中包含自定義UCropView跳纳,UCropView中包含 GestureCropImageView和OverlayView兩個(gè)自定義View。
OverlayView負(fù)責(zé)繪制剪裁框
GestureCropImageView負(fù)責(zé)操作選擇圖片 -
第二部分:OverlayView-負(fù)責(zé)繪制剪裁框
View在初始化的時(shí)候做了判斷贪嫂,當(dāng)系統(tǒng)大于3.0小于4.3時(shí)啟動硬件加速寺庄,為什么選擇這個(gè)區(qū)間呢,因?yàn)樵赼ndroid系統(tǒng)3.0之前android還不支持硬件加速力崇,而在4.3之后Android 4.4斗塘,也就是KitKat版本,這個(gè)版本對內(nèi)存方法做了很大的優(yōu)化:一方面是通過優(yōu)化內(nèi)存使用餐曹,另一方面是可選地支持使用ART運(yùn)行時(shí)替換Dalvik虛擬機(jī)逛拱,來提高應(yīng)用程序的運(yùn)行效率,所以選擇4.4之后不開啟硬件加速台猴。
剪裁框的繪制主要分為兩個(gè)部分:
1朽合、drawlines()繪制剪裁框內(nèi)分割線段,
2、drawRect()繪制矩形饱狂,從而形成了一個(gè)帶橫縱線平均分割的矩形剪裁框核心方法:
drawCropGrid()該方法主要功能為繪制剪裁框,根據(jù)設(shè)定的剪裁框分割線數(shù)量使用canvas.drawlines()方法繪制分割線和限制矩形曹步,具體代碼如下:
繪制剪裁框:
protected void drawCropGrid(@NonNull Canvas canvas) {
//判斷是否顯示剪裁框
if (mShowCropGrid) {
//判斷矩形數(shù)據(jù)是否為空,mGridPoints如果等于空的話進(jìn)入填充數(shù)據(jù)
if (mGridPoints == null && !mCropViewRect.isEmpty()) {
//該數(shù)組為canvas.drawLines的第一個(gè)參數(shù)休讳,該參數(shù)要求其元素個(gè)數(shù)為4的倍數(shù)
mGridPoints = new float[(mCropGridRowCount) * 4 + (mCropGridColumnCount) * 4];
int index = 0;
//組裝數(shù)據(jù)举瑰,數(shù)據(jù)為每一組線段的坐標(biāo)點(diǎn)
for (int i = 0; i < mCropGridRowCount; i++) {
mGridPoints[index++] = mCropViewRect.left;
mGridPoints[index++] = (mCropViewRect.height() * (((float) i + 1.0f) / (float) (mCropGridRowCount + 1))) + mCropViewRect.top;
mGridPoints[index++] = mCropViewRect.right;
mGridPoints[index++] = (mCropViewRect.height() * (((float) i + 1.0f) / (float) (mCropGridRowCount + 1))) + mCropViewRect.top;
}for (int i = 0; i < mCropGridColumnCount; i++) { mGridPoints[index++] = (mCropViewRect.width() * (((float) i + 1.0f) / (float) (mCropGridColumnCount + 1))) + mCropViewRect.left; mGridPoints[index++] = mCropViewRect.top; mGridPoints[index++] = (mCropViewRect.width() * (((float) i + 1.0f) / (float) (mCropGridColumnCount + 1))) + mCropViewRect.left; mGridPoints[index++] = mCropViewRect.bottom; } } //繪制線段 if (mGridPoints != null) { canvas.drawLines(mGridPoints, mCropGridPaint); } } //繪制矩形包裹線段 if (mShowCropFrame) { canvas.drawRect(mCropViewRect, mCropFramePaint); } //繪制邊角包裹,mFreestyleCropMode此參數(shù)如果等于1的話 剪裁框?yàn)榭梢苿訝顟B(tài)科盛,一般不用 if (mFreestyleCropMode != FREESTYLE_CROP_MODE_DISABLE) { canvas.save(); mTempRect.set(mCropViewRect); mTempRect.inset(mCropRectCornerTouchAreaLineLength, -mCropRectCornerTouchAreaLineLength); canvas.clipRect(mTempRect, Region.Op.DIFFERENCE); mTempRect.set(mCropViewRect); mTempRect.inset(-mCropRectCornerTouchAreaLineLength, mCropRectCornerTouchAreaLineLength); canvas.clipRect(mTempRect, Region.Op.DIFFERENCE); canvas.drawRect(mCropViewRect, mCropFrameCornersPaint); canvas.restore(); }
}
這部分主要負(fù)責(zé)第一層的剪裁框繪制,保存剪裁框矩陣用于后期剪裁用
第三部分:GestureCropImageView-負(fù)責(zé)操作選擇圖片
這一部分應(yīng)該是項(xiàng)目最核心的部分,實(shí)現(xiàn)邏輯作者在他的說明文章中也說的比較清楚罢坝。
這一部分的邏輯解耦做的非常好棵癣,把View的功能邏輯劃分為3層过椎,每一層負(fù)責(zé)各自的功能:
-
第一層:
TransformImageView extends ImageView
他的工作:
1.從圖片源拿到圖片
2.將矩陣進(jìn)行轉(zhuǎn)換(平移橡庞、縮放、旋轉(zhuǎn))留晚,并應(yīng)用到當(dāng)前圖片上
這一層并不知道裁剪或者手勢等行為酵紫,但提供了手勢行為操作供子類調(diào)用。
這里作者選擇使用ImageView的Matrix做旋轉(zhuǎn)縮放而沒有使用重寫onDraw方法,因?yàn)閛nDraw方法重繪有可能會有閃屏的情況而且在性能比較差的機(jī)器上可能體驗(yàn)會很差奖地,使用Matrix則會好許多橄唬。
Matrix在android中其實(shí)一個(gè)3*3的矩陣如下:
MSCALE_X MSKEW_X MTRANS_X
MSKEY_Y MSCALE_Y MTRANS_Y
MPERSP_0 MPERSP_1 MPERSP_2
Matrix對圖像的處理分為四類基本變換:
Translate 平移變換
Rotate 旋轉(zhuǎn)變換
Scale 縮放變換
Skew 錯(cuò)切變換
在項(xiàng)目中只用到了前三種,它們對應(yīng)的api如下:
//平移變換参歹,在X軸上平移x多個(gè)距離仰楚,在Y軸上平移y多個(gè)距離
postTranslate(float x,floaty);//旋轉(zhuǎn)變換,degrees旋轉(zhuǎn)度數(shù)泽示,px缸血,py旋轉(zhuǎn)原點(diǎn)
postRotate(float degrees,float px,float py);//縮放變換 x變換距離蜜氨,y變換距離械筛,px,py縮放原點(diǎn)
postScale(float sx,float sx,float px,float py);作者在這一層寫了postTranslate飒炎,postScale埋哟,postRotate三個(gè)方法暴露給子類調(diào)用,封裝Matrix屬性后在調(diào)用setImageMatrix()實(shí)現(xiàn)變換效果郎汪,mCurrentImageMatrix.mapPoints這個(gè)方法為更新當(dāng)前圖像的角點(diǎn)和存儲的中心點(diǎn)赤赊,這些變量在CropImageView中會被使用到.
-
第二層:
CropImageView extends TransformImageView
他要做的事:
1.畫出裁剪的邊界和網(wǎng)格
2.為裁剪區(qū)域設(shè)置一張圖片(如果用戶對圖片操作導(dǎo)致裁剪區(qū)域內(nèi)出現(xiàn)了空白,那么圖片應(yīng)該要自動移動到邊界填充空白區(qū)域)
3.繼承父親的方法煞赢,使用更精細(xì)的規(guī)則來操作矩陣(限制最小和最大的縮放比例)
4.添加方法和縮小的方法(動畫變換)
5.裁剪圖片
這一層幾乎囊括了所有的要對圖片進(jìn)行變換和裁剪的所有操作,但也僅僅是指明了做這些事情的方法抛计,我們還需要支持手勢。在這一層中作者重寫了onImageLaidOut方法照筑,該方法為上層View TransformImageView的圖片加載完畢回調(diào)方法吹截,在此方法中作者設(shè)置了初始要剪裁的矩形,并且把圖片移動到屏幕居中的位置操作(因?yàn)镮mageView設(shè)置的ScaleType為Matrix所以圖像一開始是默認(rèn)在屏幕上方的)
這一層中的操作大概可以分為三步操作:圖片偏移剪裁框偏移計(jì)算凝危、圖片歸位動畫處理波俄,剪裁圖片
第一步(也是最復(fù)雜的一種):
當(dāng)手指離開屏幕時(shí)要保證圖片處于剪裁區(qū)域中如果不在剪裁區(qū)域中通過位移變換來移動到剪裁區(qū)域,看代碼:public void setImageToWrapCropBounds(boolean animate) { //如果圖片加載完畢并且圖片不處于剪裁區(qū)域 if (mBitmapLaidOut && !isImageWrapCropBounds()) { //獲取中心點(diǎn)X,Y坐標(biāo) 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.reset(); mTempMatrix.setTranslate(deltaX, deltaY); final float[] tempCurrentImageCorners = Arrays.copyOf(mCurrentImageCorners, mCurrentImageCorners.length); mTempMatrix.mapPoints(tempCurrentImageCorners); //判斷圖片是否包含在剪裁區(qū)域 boolean willImageWrapCropBoundsAfterTranslate = isImageWrapCropBounds(tempCurrentImageCorners); //如果包含在剪裁區(qū)域 if (willImageWrapCropBoundsAfterTranslate) { //獲取偏移的距離 final float[] imageIndents = calculateImageIndents(); //偏移的距離蛾默,橫坐標(biāo)加橫坐標(biāo) 縱坐標(biāo)加縱坐標(biāo) deltaX = -(imageIndents[0] + imageIndents[2]); deltaY = -(imageIndents[1] + imageIndents[3]); } else { //如果不包含在剪裁區(qū)域懦铺,創(chuàng)建臨時(shí)矩形 RectF tempCropRect = new RectF(mCropRect); mTempMatrix.reset(); //設(shè)置偏移角度 mTempMatrix.setRotate(getCurrentAngle()); mTempMatrix.mapRect(tempCropRect); //獲得矩形的邊長坐標(biāo) final float[] currentImageSides = RectUtils.getRectSidesFromCorners(mCurrentImageCorners); //獲取放大比例 deltaScale = Math.max(tempCropRect.width() / currentImageSides[0], tempCropRect.height() / currentImageSides[1]); deltaScale = deltaScale * currentScale - currentScale; } //如果需要?jiǎng)赢? if (animate) { post(mWrapCropBoundsRunnable = new WrapCropBoundsRunnable( CropImageView.this, mImageToWrapCropBoundsAnimDuration, currentX, currentY, deltaX, deltaY, currentScale, deltaScale, willImageWrapCropBoundsAfterTranslate)); } else { //不需要?jiǎng)赢嫞苯右苿拥侥繕?biāo)位置 postTranslate(deltaX, deltaY); if (!willImageWrapCropBoundsAfterTranslate) { zoomInImage(currentScale + deltaScale, mCropRect.centerX(), mCropRect.centerY()); } } }
}
第二步:作者在這里使用了一個(gè)Runable線程來操作支鸡,使用時(shí)間差值的計(jì)算來移動動畫冬念,使動畫看起來更真實(shí)
此方法主要處理偏移回歸的動畫 寫在一個(gè)Runable子線程中
/**
* This Runnable is used to animate an image so it fills the crop bounds entirely.
* Given values are interpolated during the animation time.
* Runnable can be terminated either vie {@link #cancelAllAnimations()} method
* or when certain conditions inside {@link WrapCropBoundsRunnable#run()} method are triggered.
* 在這里,我計(jì)算出當(dāng)前流逝的時(shí)間牧挣,使用CubicEasing這個(gè)類急前,我對平移量和縮放量進(jìn)行插值操作。
* 使用插值器替換過的值確實(shí)可以改善你的動畫浸踩,使人們的眼睛看起來更自然叔汁。
* 最終,這些值被應(yīng)用到圖片矩陣,當(dāng)時(shí)間溢出或者圖片完全填充了裁剪區(qū)域的時(shí)候据块,Runnable任務(wù)就會停止码邻。
*/
private static class WrapCropBoundsRunnable implements Runnable {
private final WeakReference<CropImageView> mCropImageView;
private final long mDurationMs, mStartTime;
private final float mOldX, mOldY;
private final float mCenterDiffX, mCenterDiffY;
private final float mOldScale;
private final float mDeltaScale;
private final boolean mWillBeImageInBoundsAfterTranslate;
public WrapCropBoundsRunnable(CropImageView cropImageView,
long durationMs,
float oldX, float oldY,
float centerDiffX, float centerDiffY,
float oldScale, float deltaScale,
boolean willBeImageInBoundsAfterTranslate) {
mCropImageView = new WeakReference<>(cropImageView);
mDurationMs = durationMs;
mStartTime = System.currentTimeMillis();
mOldX = oldX;
mOldY = oldY;
mCenterDiffX = centerDiffX;
mCenterDiffY = centerDiffY;
mOldScale = oldScale;
mDeltaScale = deltaScale;
mWillBeImageInBoundsAfterTranslate = willBeImageInBoundsAfterTranslate;
}
@Override
public void run() {
CropImageView cropImageView = mCropImageView.get();
if (cropImageView == null) {
return;
}
long now = System.currentTimeMillis();
//花費(fèi)的時(shí)間,最多500ms另假,
float currentMs = Math.min(mDurationMs, now - mStartTime);
//計(jì)算出當(dāng)前流逝的時(shí)間像屋,我對平移量和縮放量進(jìn)行插值操作。
float newX = CubicEasing.easeOut(currentMs, 0, mCenterDiffX, mDurationMs);
float newY = CubicEasing.easeOut(currentMs, 0, mCenterDiffY, mDurationMs);
float newScale = CubicEasing.easeInOut(currentMs, 0, mDeltaScale, mDurationMs);
//如果時(shí)間溢出 停止任務(wù)
if (currentMs < mDurationMs) {
cropImageView.postTranslate(newX - (cropImageView.mCurrentImageCenter[0] - mOldX), newY - (cropImageView.mCurrentImageCenter[1] - mOldY));
if (!mWillBeImageInBoundsAfterTranslate) {
cropImageView.zoomInImage(mOldScale + newScale, cropImageView.mCropRect.centerX(), cropImageView.mCropRect.centerY());
}
//如果圖片還沒填充滿剪裁區(qū)域边篮,繼續(xù)移動
if (!cropImageView.isImageWrapCropBounds()) {
cropImageView.post(this);
}
}
}
}
另一個(gè)Runable方法類己莺,用于雙擊放大時(shí)使用
同樣使用了時(shí)間差值計(jì)算偏移大小動畫
MaxScale為圖片最大的放大值,大小為最小尺寸的10倍
minScale為圖片縮小的最小值戈轿,大小為初始矩形的寬和高分別除以剪裁框的寬高取最小值凌受。
/**
* This Runnable is used to animate an image zoom.
* Given values are interpolated during the animation time.
* Runnable can be terminated either vie {@link #cancelAllAnimations()} method
* or when certain conditions inside {@link ZoomImageToPosition#run()} method are triggered.
*/
private static class ZoomImageToPosition implements Runnable {
private final WeakReference<CropImageView> mCropImageView;
private final long mDurationMs, mStartTime;
private final float mOldScale;
private final float mDeltaScale;
private final float mDestX;
private final float mDestY;
public ZoomImageToPosition(CropImageView cropImageView,
long durationMs,
float oldScale, float deltaScale,
float destX, float destY) {
mCropImageView = new WeakReference<>(cropImageView);
mStartTime = System.currentTimeMillis();
mDurationMs = durationMs;
mOldScale = oldScale;
mDeltaScale = deltaScale;
mDestX = destX;
mDestY = destY;
}
@Override
public void run() {
CropImageView cropImageView = mCropImageView.get();
if (cropImageView == null) {
return;
}
long now = System.currentTimeMillis();
float currentMs = Math.min(mDurationMs, now - mStartTime);
float newScale = CubicEasing.easeInOut(currentMs, 0, mDeltaScale, mDurationMs);
if (currentMs < mDurationMs) {
cropImageView.zoomInImage(mOldScale + newScale, mDestX, mDestY);
cropImageView.post(this);
} else {
cropImageView.setImageToWrapCropBounds();
}
}
}
- 第三步:最后一步,剪裁圖片
/**
* Cancels all current animations and sets image to fill crop area (without animation).
* Then creates and executes {@link BitmapCropTask} with proper parameters.
*/
public void cropAndSaveImage(@NonNull Bitmap.CompressFormat compressFormat, int compressQuality,
@Nullable BitmapCropCallback cropCallback) {
//結(jié)束子線程
cancelAllAnimations();
//設(shè)置要剪裁的圖片思杯,不需要位移動畫
setImageToWrapCropBounds(false);
//存儲圖片信息胜蛉,四個(gè)參數(shù)分別為:mCropRect要剪裁的圖片矩陣,當(dāng)前圖片要剪裁的矩陣色乾,當(dāng)前放大的值誊册,當(dāng)前旋轉(zhuǎn)的角度
final ImageState imageState = new ImageState(
mCropRect, RectUtils.trapToRect(mCurrentImageCorners),
getCurrentScale(), getCurrentAngle());
//剪裁參數(shù),mMaxResultImageSizeX暖璧,mMaxResultImageSizeY:剪裁圖片的最大寬度案怯、高度。
final CropParameters cropParameters = new CropParameters(
mMaxResultImageSizeX, mMaxResultImageSizeY,
compressFormat, compressQuality,
getImageInputPath(), getImageOutputPath(), getExifInfo());
//剪裁操作放到AsyncTask中執(zhí)行
new BitmapCropTask(getViewBitmap(), imageState, cropParameters, cropCallback).execute();
}
剪裁部分的核心代碼: float resizeScale = resize(); crop(resizeScale);
//調(diào)整剪裁大小澎办,如果有設(shè)置最大剪裁大小也會在這里做調(diào)整到設(shè)置范圍
private float resize() {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(mImageInputPath, options);
boolean swapSides = mExifInfo.getExifDegrees() == 90 || mExifInfo.getExifDegrees() == 270;
float scaleX = (swapSides ? options.outHeight : options.outWidth) / (float) mViewBitmap.getWidth();
float scaleY = (swapSides ? options.outWidth : options.outHeight) / (float) mViewBitmap.getHeight();
float resizeScale = Math.min(scaleX, scaleY);
mCurrentScale /= resizeScale;
resizeScale = 1;
if (mMaxResultImageSizeX > 0 && mMaxResultImageSizeY > 0) {
float cropWidth = mCropRect.width() / mCurrentScale;
float cropHeight = mCropRect.height() / mCurrentScale;
if (cropWidth > mMaxResultImageSizeX || cropHeight > mMaxResultImageSizeY) {
scaleX = mMaxResultImageSizeX / cropWidth;
scaleY = mMaxResultImageSizeY / cropHeight;
resizeScale = Math.min(scaleX, scaleY);
mCurrentScale /= resizeScale;
}
}
return resizeScale;
}
/**
* 剪裁圖片
*/
private boolean crop(float resizeScale) throws IOException {
ExifInterface originalExif = new ExifInterface(mImageInputPath);
//四舍五入取整
int top = Math.round((mCropRect.top - mCurrentImageRect.top) / mCurrentScale);
int left = Math.round((mCropRect.left - mCurrentImageRect.left) / mCurrentScale);
mCroppedImageWidth = Math.round(mCropRect.width() / mCurrentScale);
mCroppedImageHeight = Math.round(mCropRect.height() / mCurrentScale);
//計(jì)算出圖片是否需要被剪裁
boolean shouldCrop = shouldCrop(mCroppedImageWidth, mCroppedImageHeight);
Log.i(TAG, "Should crop: " + shouldCrop);
if (shouldCrop) {
//調(diào)用C++方法剪裁
boolean cropped = cropCImg(mImageInputPath, mImageOutputPath,
left, top, mCroppedImageWidth, mCroppedImageHeight, mCurrentAngle, resizeScale,
mCompressFormat.ordinal(), mCompressQuality,
mExifInfo.getExifDegrees(), mExifInfo.getExifTranslation());
//剪裁成功復(fù)制圖片EXIF信息
if (cropped && mCompressFormat.equals(Bitmap.CompressFormat.JPEG)) {
ImageHeaderParser.copyExif(originalExif, mCroppedImageWidth, mCroppedImageHeight, mImageOutputPath);
}
return cropped;
} else {
//直接復(fù)制圖片到目標(biāo)文件夾
FileUtils.copyFile(mImageInputPath, mImageOutputPath);
return false;
}
}
第三層:
GestureImageView extends CropImageView
他的功能:
監(jiān)聽用戶的手勢嘲碱,調(diào)用合適的方法
由于系統(tǒng)對手勢操作已經(jīng)有了監(jiān)聽方法,所以作者在這里使用了系統(tǒng)的監(jiān)聽方法:
ScaleGestureDetector:用來檢測兩個(gè)手指在屏幕上做縮放的手勢浮驳。
GestureListener:這個(gè)類我們可以識別很多的手勢悍汛,作者在這里重寫了雙擊onDoubleTap,拖動onScroll至会,兩種手勢處理离咐。
RotationGestureDetector:兩只以上的手指觸摸屏幕才會產(chǎn)生旋轉(zhuǎn)事件用這個(gè)接口回調(diào)。
/**
* If it's ACTION_DOWN event - user touches the screen and all current animation must be canceled.
* If it's ACTION_UP event - user removed all fingers from the screen and current image position must be corrected.
* If there are more than 2 fingers - update focal point coordinates.
* Pass the event to the gesture detectors if those are enabled.
*/
@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;
}
//雙擊監(jiān)聽和拖動監(jiān)聽
mGestureDetector.onTouchEvent(event);
//兩指縮放監(jiān)聽
if (mIsScaleEnabled) {
mScaleDetector.onTouchEvent(event);
}
//旋轉(zhuǎn)監(jiān)聽
if (mIsRotateEnabled) {
mRotateDetector.onTouchEvent(event);
}
if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
//最后一指抬起時(shí)判斷圖片是否填充剪裁框
setImageToWrapCropBounds();
}
return true;
}
大致的核心邏輯基本就這些
項(xiàng)目中的異步操作使用AsyncTask奉件,一共兩個(gè)主要的AsyncTask:BitmapLoadTask用于初次進(jìn)入load圖片宵蛀,BitmapCropTask圖片剪裁異步操作。
項(xiàng)目涉及到的技術(shù)點(diǎn):
自定義View县貌,手勢操作監(jiān)聽术陶,Matrix實(shí)現(xiàn)圖片變換縮放,Canvas繪制View煤痕,exif存儲圖片信息梧宫,文件存儲操作接谨,以及大量的計(jì)算。
*有疑問的可以在評論區(qū)留言一起討論~~