uCrop框架用法和源码解析
本人能力不足,在看到源码最后一部分的时候大量抄袭可能是最详细的UCrop源码解析
1. uCrop简介
uCrop是目前较火的图片裁剪框架,开发者宣称他会比目前市面上所有的图片裁剪方案都要更流畅。外加他封装程度较高,可自定义,而且颜值很高(似乎这个才是重点),现在越来越多APP选择使用它。
github
2. 使用方法
得益于uCrop优秀的封装,uCrop的使用方法特简单。
2.1 导入依赖
先在项目的
build.gradle
中添加1
2
3
4
5
6allprojects {
repositories {
jcenter()
maven { url "https://jitpack.io" }
}
}并在
module
的build.gradle
中添加implementation 'com.github.yalantis:ucrop:2.2.3'
- 轻量级框架implementation 'com.github.yalantis:ucrop:2.2.3-native'
- 获得框架全部强大的功能以及图片的高质量(最终可能会导致apk的大小增加1.5MB以上)- 由于框架的本质是调用到另一个Activity去处理图片,所以需要在AndroidManifest.xml中将UCropActivity添加进去
1
2
3
4<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
到这你就能把cUrop全部导入到你的项目里面了,接下来咱们就拉将如何调用
2.2 开始基本的调用
调用起来很简单:1
2UCrop.of(sourceUri, destinationUri)
.start(context);
其中sourceUri
是输入图片的Uri
,destinationUri
是输出图片的Uri
。然后他就会由Intent
的调动跳到UCropActivity
,用户就在UCropActivity
里面进行图片裁剪操作,然后最后由UCropActivity
发起一个Intent
回到你的Activity
。
2.3 处理回来的数据
由于是从UCropAcitivity
传回数据,所以你需要在你的Activity
里面的onActivityResult
方法处理uCrop
返回的信息:1
2
3
4
5
6
7
8
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);
}
}
到这,基本用法就完了,你就可以尽情的使用uCrop。但是我前面说过,uCrop封装程度好,这点很多图片处理框架都可以做到,基本上都是把需要的数据传到自己的Activity之后由自己的Activity处理,所以很多框架看起来都有优秀的封装,那uCrop相比其他又有啥好呢,答案就是自定义灵活:
2.4 uCrop高阶用法
2.4.1 配置uCrop
1 | /** |
2.4.2 其他配置
1 | //设置Toolbar标题 |
3. 源码解析
在我开始说源码之前,我建议大家可以先看下我下面的连接,因为本框架的作者真的是个好人,他不仅为我们贡献了这么好的一个框架,还把自己写这个框架的思路都写了出来,大家可以看看
英文原版
国内网友翻译版
百度网页翻译机翻版
其实我个人感觉百度机翻没有谷歌翻译的好,大家有条件的可以使用谷歌翻译浏览器插件翻译整个网页(谷歌翻译好像国内可以直接访问)
代码结构大致分为三个部分:
3.1 第一部分:UCropActivity(整个框架的外在,用户操作图片的地方)
他的功能就是项目主要的界面,以及实现一些基本的初始化。你跳转到uCrop看到的那个操作图片的界面就是它。
这块看源码的时候代码居多,但是,说实话,就像刚刚说的一样,他除了初始化还是初始化。初始化完Toolbar
接着初始化ViewGroup
,初始化完ViewGroup
接着初始化Image
数据等等。所以这块我就没咋细看(其实是因为代码太长了,逃)
3.2 第二部分:OverlayView(绘制裁剪框)
这一块主要就是来画你所看到的图片中的裁剪的辅助线。
在构造方法里面就调用了一个方法,就是init()
,而init()
方法也就干了一件事——判断。当系统小于JELLY_BEA_MR2
也就是Android4.3
时,启动了硬件加速,至于为什么setLayerType(LAYER_TYPE_SOFTWARE, null);
这个看着就像启动硬件加速的方法,甚至参数里面还有软件这个单词的方法能启动硬件加速,请大家移步HenCoder Android 自定义 View 1-8 硬件加速(进去直接搜索这个方法即可,就能找到解释的地方),我再次不做解释。
这个类主要有两个方法
drawDimmedLayer()
绘制裁剪框之外的灰色部分drawCropGrid()
绘制裁剪框
那我们分别来看下这两个方法:
3.2.1 drawDimmedLayer()
1 | protected void drawDimmedLayer( { Canvas canvas) |
首先就是一个mCircleDimmedLayer
,这个我真的很迷,因为我不知道她是咋来的,于是我就看OverlayView
有没有对这个变量的赋值,于是整个类我就找到了一个setCircleDimmedLayer()
方法,于是我看这个方法是在哪被调用了的,然后我就找到他分别被UCropActivity和UCropFragment两个类调用到,而且一个是intent.getBooleanExtra()
方法一个是bundle.getBoolean()
方法,看到这个我相信大家都有点数了,这明显就是其他类传过来的啊,我发现他两的key的值都是UCrop.Options.EXTRA_CIRCLE_DIMMED_LAYER
,那我就懂了,找整个框架里面哪儿提到过这个值不就得了,于是我就发现除了上面两个方法以及他的初始化以外,我发现了第4个调用的地方,也是唯一一个调用的地方——Ucrop.setCircleDimmedLayer():1
2
3
4
5
6
7/**
* @param isCircle - set it to true if you want dimmed layer to have an circle inside
* iscircle-如果希望暗显层中有一个圆,请将其设置为true。
*/
public void setCircleDimmedLayer(boolean isCircle) {
mOptionBundle.putBoolean(EXTRA_CIRCLE_DIMMED_LAYER, isCircle);
}
注释上面是原话,下面是我百度机翻的翻译。看了就懂了吧,反正我没懂,我也完全没有见到哪调用过这个方法,我更不懂啥叫希望暗显层有个圆,啥玩意?充满线条的黑???
直到我将UCrop的调用方法修改了并运行之后我才懂了:1
2
3
4
5val options = UCrop.Options()
options.setCircleDimmedLayer(true)
UCrop.of(uri, destinationUri)
.withOptions(options)
.start(this)
结果是:
然后就懂了,应该是能截一个圆形的图案吧,然后我点下了✔️,然后……
无话可说,作者牛逼!!!
回去回去,刚刚说到drawDimmedLayer()
,可以看到,如果mCircleDimmedLayer
为true
就调用clipPath()
跟着路径裁切一个矩形加原,不然的话就调用clipRect()
裁切一个矩形。然后加入颜色,然后完了
3.2.2 drawCropGrid()
1 | protected void drawCropGrid( { Canvas canvas) |
一开头又是一个和上面类似的变量mShowCropGrid
,这下我就不说我找的具体步骤,他的功能就是如果他是true
就会在裁剪框中显示9宫格线,为false
就没有。接着就是画线部分,我觉得这个我不用讲啥,也没啥讲的,唯一就是为什么mGridPoints这个数组的大小是4的倍数,大家可以看下这个博客Android Canvas DrawLines中第一个参数的解释
3.3 第三部分:GestureCropImageView(正在框架的内在,代码操作操作图片的地方)
这个是整个项目最核心的地方。前面的两部分都是UI的,而这个才是真正的对图片进行处理的部分,也是我最想知道了解的部分。
这部分作者也在他的博客里面说的最多最清楚。
作者把这部分的逻辑分为了三个部分
TransformImageView extends ImageView
他处理了- 从源拿到图片
- 将图片进变换(平移、缩放、旋转),并应用到当前图片上
CropImageView extends TransformImageView
他处理了- 绘制裁剪边框和网格
- 为裁剪区域设置一张图片(如果用户对图片操作导致裁剪区域出现了空白,那么图片应自动移动到边界填充空白区域)
- 继承父类方法,使用更精准的规则来操作矩阵(限制最大和最小缩放比)
- 添加方法和缩小的方法
- 裁剪图片
GestureCropImageView extends CropImageView
他处理了- 监听用户手势,并调用对应的正确的方法
3.3.1 TransformImageView
作者说这是最容易的部分。
在看这个类之前我们先来看看BitmapLoadTask
类,这个类是一切图像处理的基础,这个类负责了Uri
解码bitmap
,并处理分辨率:
首先根据拿到的Uri
解析位图:1
2
3
4
5
6
7
8
9
10
11
12
13final ParcelFileDescriptor parcelFileDescriptor;
try {
parcelFileDescriptor = mContext.getContentResolver().openFileDescriptor(mInputUri, "r");
} catch (FileNotFoundException e) {
return new BitmapWorkerResult(e);
}
final FileDescriptor fileDescriptor;
if (parcelFileDescriptor != null) {
fileDescriptor = parcelFileDescriptor.getFileDescriptor();
} else {
return new BitmapWorkerResult(new NullPointerException("ParcelFileDescriptor was null for given Uri: [" + mInputUri + "]"));
}
现在,可以使用BitmapFactory
方法解码FileDescriptor
。
但在解码位图之前,有必要知道它的大小,因为如果分辨率太高,位图将被二次采样。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
options.inSampleSize = calculateInSampleSize(options, requiredWidth, requiredHeight);
options.inJustDecodeBounds = false;
Bitmap decodeSampledBitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
close(parcelFileDescriptor);
ExifInterface exif = getExif(uri);
if (exif != null) {
int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
return rotateBitmap(decodeSampledBitmap, exifToDegrees(exifOrientation));
} else {
return decodeSampledBitmap;
}
这样就拿到了bitmap
实例了,就可以去TansformImageView
去对图片进行调整了。
其实这个类我也不知道说啥😂,我觉得这个类也就是把Matrix
的postTranslate()
、postRotate()
和postScale()
方法给封装了下。
关于Matrix的知识大家可以参考这篇博客:安卓自定义View进阶-Matrix原理
3.3.2 CropImageView
这一层是最复杂的一层,作者的操作大致可以分为3步:图片裁剪框偏移计算、图片归为动画处理、裁剪图片
- 第一步:图片裁剪框偏移计算
当用户手指移开时,要确保图片处于裁剪区域中,如果不处于,需要通过平移把它移过来:
1 | public void setImageToWrapCropBounds(boolean animate) { |
- 第二步:处理平移
通过一个Runnable线程来处理平移,并且通过时间差值的计算来移动动画,使动画看起来更真实:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62/**
* 此可运行文件用于动画图像,使其完全填充裁剪边界。
* 给定值在动画期间内插。
* runnable可以终止于vie{@link #cancelAllAnimations()}方法,
* 也可以在触发{@link WrapCropBoundsRunnable#run()}方法内的某些条件时终止。
*/
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;
}
public void run() {
CropImageView cropImageView = mCropImageView.get();
if (cropImageView == null) {
return;
}
long now = System.currentTimeMillis();
float currentMs = Math.min(mDurationMs, now - mStartTime);
float newX = CubicEasing.easeOut(currentMs, 0, mCenterDiffX, mDurationMs);
float newY = CubicEasing.easeOut(currentMs, 0, mCenterDiffY, mDurationMs);
float newScale = CubicEasing.easeInOut(currentMs, 0, mDeltaScale, mDurationMs);
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());
}
if (!cropImageView.isImageWrapCropBounds()) {
cropImageView.post(this);
}
}
}
}
下面还有另一个线程,用于双击放大:
1 | /** |
- 第三步:裁剪图片
1 | /** |
这块核心方法还是在BitmapCropTask
中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62//调整剪裁大小,如果有设置最大剪裁大小也会在这里做调整到设置范围
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);
//四舍五入取整
cropOffsetX = Math.round((mCropRect.left - mCurrentImageRect.left) / mCurrentScale);
cropOffsetY = Math.round((mCropRect.top - mCurrentImageRect.top) / mCurrentScale);
mCroppedImageWidth = Math.round(mCropRect.width() / mCurrentScale);
mCroppedImageHeight = Math.round(mCropRect.height() / mCurrentScale);
//计算出图片是否需要被剪裁
boolean shouldCrop = shouldCrop(mCroppedImageWidth, mCroppedImageHeight);
Log.i(TAG, "Should crop: " + shouldCrop);
if (shouldCrop) {
//调用C++方法剪裁
boolean cropped = cropCImg(mImageInputPath, mImageOutputPath,
cropOffsetX, cropOffsetY, mCroppedImageWidth, mCroppedImageHeight,
mCurrentAngle, resizeScale, mCompressFormat.ordinal(), mCompressQuality,
mExifInfo.getExifDegrees(), mExifInfo.getExifTranslation());
//剪裁成功复制图片EXIF信息
if (cropped && mCompressFormat.equals(Bitmap.CompressFormat.JPEG)) {
ImageHeaderParser.copyExif(originalExif, mCroppedImageWidth, mCroppedImageHeight, mImageOutputPath);
}
return cropped;
} else {
//直接复制图片到目标文件夹
FileUtils.copyFile(mImageInputPath, mImageOutputPath);
return false;
}
}
3.3.3 GestureCropImageView
这个类主要就是对手势的监听,所以我们简单粗暴,直接找他的onTouchEvent方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36/**
* 如果是ACTION_DOWN event,用户触摸屏幕,必须取消所有当前动画。
* 如果是ACTION_UP event,用户从屏幕上取下所有手指,必须纠正当前图像位置。
* 如果有两个以上的手指-更新焦点坐标。
* 如果已启用,则将事件传递给手势检测器。
*/
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.onTouchEvent(event);
//两指缩放监听
if (mIsScaleEnabled) {
mScaleDetector.onTouchEvent(event);
}
//旋转监听
if (mIsRotateEnabled) {
mRotateDetector.onTouchEvent(event);
}
if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
//最后一指抬起时判断图片是否填充剪裁框
setImageToWrapCropBounds();
}
return true;
}