本人能力不足,在看到源码最后一部分的时候大量抄袭可能是最详细的UCrop源码解析
1. uCrop简介
uCrop是目前较火的图片裁剪框架,开发者宣称他会比目前市面上所有的图片裁剪方案都要更流畅。外加他封装程度较高,可自定义,而且颜值很高(似乎这个才是重点),现在越来越多APP选择使用它。
github
2. 使用方法
得益于uCrop优秀的封装,uCrop的使用方法特简单。
2.1 导入依赖
先在项目的build.gradle
中添加
1
2
3
4
5
6
| allprojects {
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
2
| UCrop.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
| @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);
}
}
|
到这,基本用法就完了,你就可以尽情的使用uCrop。但是我前面说过,uCrop封装程度好,这点很多图片处理框架都可以做到,基本上都是把需要的数据传到自己的Activity之后由自己的Activity处理,所以很多框架看起来都有优秀的封装,那uCrop相比其他又有啥好呢,答案就是自定义灵活:
2.4 uCrop高阶用法
2.4.1 配置uCrop
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
| /**
* 启动裁剪
* @param activity 上下文
* @param sourceFilePath 需要裁剪图片的绝对路径
* @param requestCode 比如:UCrop.REQUEST_CROP
* @param aspectRatioX 裁剪图片宽高比
* @param aspectRatioY 裁剪图片宽高比
* @return
*/
public static String startUCrop(Activity activity, String sourceFilePath,
int requestCode, float aspectRatioX, float aspectRatioY) {
Uri sourceUri = Uri.fromFile(new File(sourceFilePath));
File outDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
if (!outDir.exists()) {
outDir.mkdirs();
}
File outFile = new File(outDir, System.currentTimeMillis() + ".jpg");
//裁剪后图片的绝对路径
String cameraScalePath = outFile.getAbsolutePath();
Uri destinationUri = Uri.fromFile(outFile);
//初始化,第一个参数:需要裁剪的图片;第二个参数:裁剪后图片
UCrop uCrop = UCrop.of(sourceUri, destinationUri);
//初始化UCrop配置
UCrop.Options options = new UCrop.Options();
//设置裁剪图片可操作的手势
options.setAllowedGestures(UCropActivity.SCALE, UCropActivity.ROTATE, UCropActivity.ALL);
//是否隐藏底部容器,默认显示
options.setHideBottomControls(true);
//设置toolbar颜色
options.setToolbarColor(ActivityCompat.getColor(activity, R.color.colorPrimary));
//设置状态栏颜色
options.setStatusBarColor(ActivityCompat.getColor(activity, R.color.colorPrimary));
//是否能调整裁剪框
options.setFreeStyleCropEnabled(true);
//UCrop配置
uCrop.withOptions(options);
//设置裁剪图片的宽高比,比如16:9
uCrop.withAspectRatio(aspectRatioX, aspectRatioY);
//uCrop.useSourceImageAspectRatio();
//跳转裁剪页面
uCrop.start(activity, requestCode);
return cameraScalePath;
}
|
2.4.2 其他配置
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
| //设置Toolbar标题
void setToolbarTitle(@Nullable String text)
//设置裁剪的图片格式
void setCompressionFormat(@NonNull Bitmap.CompressFormat format)
//设置裁剪的图片质量,取值0-100
void setCompressionQuality(@IntRange(from = 0) int compressQuality)
//设置最多缩放的比例尺
void setMaxScaleMultiplier(@FloatRange(from = 1.0, fromInclusive = false) float maxScaleMultiplier)
//动画时间
void setImageToCropBoundsAnimDuration(@IntRange(from = 100) int durationMillis)
//设置图片压缩最大值
void setMaxBitmapSize(@IntRange(from = 100) int maxBitmapSize)
//是否显示椭圆裁剪框阴影
void setOvalDimmedLayer(boolean isOval)
//设置椭圆裁剪框阴影颜色
void setDimmedLayerColor(@ColorInt int color)
//是否显示裁剪框
void setShowCropFrame(boolean show)
//设置裁剪框边的宽度
void setCropFrameStrokeWidth(@IntRange(from = 0) int width)
//是否显示裁剪框网格
void setShowCropGrid(boolean show)
//设置裁剪框网格颜色
void setCropGridColor(@ColorInt int color)
//设置裁剪框网格宽
void setCropGridStrokeWidth(@IntRange(from = 0) int width)
|
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| protected void drawDimmedLayer(@NonNull Canvas canvas) {
//先保存当前当前画布
canvas.save();
//判断是否显示圆框
if (mCircleDimmedLayer) {
//按Path路径裁剪
canvas.clipPath(mCircularPath, Region.Op.DIFFERENCE);
} else {
//裁剪矩形
canvas.clipRect(mCropViewRect, Region.Op.DIFFERENCE);
}
//着色
canvas.drawColor(mDimmedColor);
//恢复之前保存的Canvas的状态
canvas.restore();
if (mCircleDimmedLayer) { // 绘制1px笔划以修复反锯齿
canvas.drawCircle(mCropViewRect.centerX(), mCropViewRect.centerY(),
Math.min(mCropViewRect.width(), mCropViewRect.height()) / 2.f, mDimmedStrokePaint);
}
}
|
首先就是一个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
5
| val options = UCrop.Options()
options.setCircleDimmedLayer(true)
UCrop.of(uri, destinationUri)
.withOptions(options)
.start(this)
|
结果是:
然后就懂了,应该是能截一个圆形的图案吧,然后我点下了✔️,然后……
无话可说,作者牛逼!!!
回去回去,刚刚说到drawDimmedLayer()
,可以看到,如果mCircleDimmedLayer
为true
就调用clipPath()
跟着路径裁切一个矩形加原,不然的话就调用clipRect()
裁切一个矩形。然后加入颜色,然后完了
3.2.2 drawCropGrid()
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
| protected void drawCropGrid(@NonNull Canvas canvas) {
// 判断是否显示剪裁框
if (mShowCropGrid) {
// 判断矩形数据是否为空,mGridPoints 如果等于空的话进入填充数据
if (mGridPoints == null && !mCropViewRect.isEmpty()) {
// 该数组为 canvas.drawLines 的第一个参数,该参数要求其元素个数为 4 的倍数
mGridPoints = new float[(mCropGridRowCount) * 4 + (mCropGridColumnCount) * 4];
int index = 0;
// 组装数据,数据为每一组线段的坐标点
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此参数如果等于1的话 剪裁框为可移动状态,一般不用
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();
}
}
|
一开头又是一个和上面类似的变量mShowCropGrid
,这下我就不说我找的具体步骤,他的功能就是如果他是true
就会在裁剪框中显示9宫格线,为false
就没有。接着就是画线部分,我觉得这个我不用讲啥,也没啥讲的,唯一就是为什么mGridPoints这个数组的大小是4的倍数,大家可以看下这个博客Android Canvas DrawLines中第一个参数的解释
3.3 第三部分:GestureCropImageView(正在框架的内在,代码操作操作图片的地方)
这个是整个项目最核心的地方。前面的两部分都是UI的,而这个才是真正的对图片进行处理的部分,也是我最想知道了解的部分。
这部分作者也在他的博客里面说的最多最清楚。
作者把这部分的逻辑分为了三个部分
TransformImageView extends ImageView
他处理了
- 从源拿到图片
- 将图片进变换(平移、缩放、旋转),并应用到当前图片上
CropImageView extends TransformImageView
他处理了
- 绘制裁剪边框和网格
- 为裁剪区域设置一张图片(如果用户对图片操作导致裁剪区域出现了空白,那么图片应自动移动到边界填充空白区域)
- 继承父类方法,使用更精准的规则来操作矩阵(限制最大和最小缩放比)
- 添加方法和缩小的方法
- 裁剪图片
GestureCropImageView extends CropImageView
他处理了
- 监听用户手势,并调用对应的正确的方法
作者说这是最容易的部分。
在看这个类之前我们先来看看BitmapLoadTask
类,这个类是一切图像处理的基础,这个类负责了Uri
解码bitmap
,并处理分辨率:
首先根据拿到的Uri
解析位图:
1
2
3
4
5
6
7
8
9
10
11
12
13
| final 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
33
| final 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
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
| public void setImageToWrapCropBounds(boolean animate) {
//如果图片加载完毕并且图片不处于剪裁区域
if (mBitmapLaidOut && !isImageWrapCropBounds()) {
//获取中心点X,Y坐标
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);
//判断图片是否包含在剪裁区域
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]);
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线程来处理平移,并且通过时间差值的计算来移动动画,使动画看起来更真实:
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;
}
@Override
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
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
| /**
* 此可运行项用于设置图像缩放的动画。
* 给定值在动画期间内插。
* runnable可以终止vie {@link #cancelAllAnimations()}方法,
* 也可以在触发{@link ZoomImageToPosition#run()}方法内的某些条件时终止。
*/
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();
}
}
}
|
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
| /**
* 取消所有当前动画并设置图像以填充裁剪区域(不带动画)。
* 然后用适当的参数创建并执行{@link BitmapCropTask}。
*/
public void cropAndSaveImage(@NonNull Bitmap.CompressFormat compressFormat, int compressQuality,
@Nullable BitmapCropCallback cropCallback) {
//结束子线程
cancelAllAnimations();
//设置要剪裁的图片,不需要位移动画
setImageToWrapCropBounds(false);
//存储图片信息,四个参数分别为:mCropRect要剪裁的图片矩阵,当前图片要剪裁的矩阵,当前放大的值,当前旋转的角度
final ImageState imageState = new ImageState(
mCropRect, RectUtils.trapToRect(mCurrentImageCorners),
getCurrentScale(), getCurrentAngle());
//剪裁参数,mMaxResultImageSizeX,mMaxResultImageSizeY:剪裁图片的最大宽度、高度。
final CropParameters cropParameters = new CropParameters(
mMaxResultImageSizeX, mMaxResultImageSizeY,
compressFormat, compressQuality,
getImageInputPath(), getImageOutputPath(), getExifInfo());
//剪裁操作放到AsyncTask中执行
new BitmapCropTask(getViewBitmap(), imageState, cropParameters, cropCallback).execute();
}
|
这块核心方法还是在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,用户从屏幕上取下所有手指,必须纠正当前图像位置。
* 如果有两个以上的手指-更新焦点坐标。
* 如果已启用,则将事件传递给手势检测器。
*/
@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.onTouchEvent(event);
//两指缩放监听
if (mIsScaleEnabled) {
mScaleDetector.onTouchEvent(event);
}
//旋转监听
if (mIsRotateEnabled) {
mRotateDetector.onTouchEvent(event);
}
if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
//最后一指抬起时判断图片是否填充剪裁框
setImageToWrapCropBounds();
}
return true;
}
|