uCrop图片裁剪
uCrop使用
github地址
https://github.com/Yalantis/uCrop
然后clone或下载到本地,运行之。
效果预览
app/build.gradle
compile 'com.yalantis:ucrop:1.5.0'
AndroidManifest.xml
1 <activity 2 android:name="com.yalantis.ucrop.UCropActivity" 3 android:screenOrientation="portrait" 4 android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
这里theme可以改成自己的
配置uCrop
1 /** 2 * 启动裁剪 3 * @param activity 上下文 4 * @param sourceFilePath 需要裁剪图片的绝对路径 5 * @param requestCode 比如:UCrop.REQUEST_CROP 6 * @param aspectRatioX 裁剪图片宽高比 7 * @param aspectRatioY 裁剪图片宽高比 8 * @return 9 */ 10 public static String startUCrop(Activity activity, String sourceFilePath, 11 int requestCode, float aspectRatioX, float aspectRatioY) { 12 Uri sourceUri = Uri.fromFile(new File(sourceFilePath)); 13 File outDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); 14 if (!outDir.exists()) { 15 outDir.mkdirs(); 16 } 17 File outFile = new File(outDir, System.currentTimeMillis() + ".jpg"); 18 //裁剪后图片的绝对路径 19 String cameraScalePath = outFile.getAbsolutePath(); 20 Uri destinationUri = Uri.fromFile(outFile); 21 //初始化,第一个参数:需要裁剪的图片;第二个参数:裁剪后图片 22 UCrop uCrop = UCrop.of(sourceUri, destinationUri); 23 //初始化UCrop配置 24 UCrop.Options options = new UCrop.Options(); 25 //设置裁剪图片可操作的手势 26 options.setAllowedGestures(UCropActivity.SCALE, UCropActivity.ROTATE, UCropActivity.ALL); 27 //是否隐藏底部容器,默认显示 28 options.setHideBottomControls(true); 29 //设置toolbar颜色 30 options.setToolbarColor(ActivityCompat.getColor(activity, R.color.colorPrimary)); 31 //设置状态栏颜色 32 options.setStatusBarColor(ActivityCompat.getColor(activity, R.color.colorPrimary)); 33 //是否能调整裁剪框 34 options.setFreeStyleCropEnabled(true); 35 //UCrop配置 36 uCrop.withOptions(options); 37 //设置裁剪图片的宽高比,比如16:9 38 uCrop.withAspectRatio(aspectRatioX, aspectRatioY); 39 //uCrop.useSourceImageAspectRatio(); 40 //跳转裁剪页面 41 uCrop.start(activity, requestCode); 42 return cameraScalePath; 43 }
其他配置
1 //设置Toolbar标题 2 void setToolbarTitle(@Nullable String text) 3 //设置裁剪的图片格式 4 void setCompressionFormat(@NonNull Bitmap.CompressFormat format) 5 //设置裁剪的图片质量,取值0-100 6 void setCompressionQuality(@IntRange(from = 0) int compressQuality) 7 //设置最多缩放的比例尺 8 void setMaxScaleMultiplier(@FloatRange(from = 1.0, fromInclusive = false) float maxScaleMultiplier) 9 //动画时间 10 void setImageToCropBoundsAnimDuration(@IntRange(from = 100) int durationMillis) 11 //设置图片压缩最大值 12 void setMaxBitmapSize(@IntRange(from = 100) int maxBitmapSize) 13 //是否显示椭圆裁剪框阴影 14 void setOvalDimmedLayer(boolean isOval) 15 //设置椭圆裁剪框阴影颜色 16 void setDimmedLayerColor(@ColorInt int color) 17 //是否显示裁剪框 18 void setShowCropFrame(boolean show) 19 //设置裁剪框边的宽度 20 void setCropFrameStrokeWidth(@IntRange(from = 0) int width) 21 //是否显示裁剪框网格 22 void setShowCropGrid(boolean show) 23 //设置裁剪框网格颜色 24 void setCropGridColor(@ColorInt int color) 25 //设置裁剪框网格宽 26 void setCropGridStrokeWidth(@IntRange(from = 0) int width)
onActivityResult
经过裁剪,返回结果,这里我一般只需要裁剪后的图片绝对路径(调用上面startUCrop,即返回图片路径),然后调接口上传服务器。
1 @Override 2 public void onActivityResult(int requestCode, int resultCode, Intent data) { 3 if (resultCode == RESULT_OK && requestCode == UCrop.REQUEST_CROP) { 4 final Uri resultUri = UCrop.getOutput(data); 5 } else if (resultCode == UCrop.RESULT_ERROR) { 6 final Throwable cropError = UCrop.getError(data); 7 } 8 }
uCrop源码浅析
uCrop源码能学习的东西有很多,比如左右滑的标尺,不过我们这里源码浅析只关注裁剪部分。
类关系
首先有个大概了解:
GestureCropImageView:负责监听各种手势
CropImageView:主要完成图片裁剪工作,和判断裁剪图片是否充满裁剪框
TransformImageView:负责图片旋转、缩放、位移操作
入口
由上面的效果图可知,点击右上角,调用裁剪操作,代码如下:
1 @Override 2 public boolean onOptionsItemSelected(MenuItem item) { 3 if (item.getItemId() == R.id.menu_crop) { 4 cropAndSaveImage(); 5 } 6 return super.onOptionsItemSelected(item); 7 } 8 //裁剪和保存图片 9 protected void cropAndSaveImage() { 10 …… 11 mGestureCropImageView.cropAndSaveImage(mCompressFormat, mCompressQuality, new BitmapCropCallback() { 12 @Override 13 public void onBitmapCropped(@NonNull Uri resultUri) { 14 setResultUri(resultUri, mGestureCropImageView.getTargetAspectRatio()); 15 finish(); 16 } 17 @Override 18 public void onCropFailure(@NonNull Throwable t) { 19 setResultError(t); 20 finish(); 21 } 22 }); 23 }
这里调用了GestureCropImageView&cropAndSaveImage方法,如下:
1 /** 2 * @param compressFormat 图片压缩格式 3 * @param compressQuality 图片压缩质量 4 * @param cropCallback 图片压缩回调 5 */ 6 public void cropAndSaveImage(@NonNull Bitmap.CompressFormat compressFormat, int compressQuality,@Nullable BitmapCropCallback cropCallback) { 7 //取消所有动画 8 cancelAllAnimations(); 9 //判断裁剪图片是否充满裁剪框 10 setImageToWrapCropBounds(false); 11 //进行裁剪 12 new BitmapCropTask(getViewBitmap(), mCropRect, RectUtils.trapToRect(mCurrentImageCorners), 13 getCurrentScale(), getCurrentAngle(), 14 mMaxResultImageSizeX, mMaxResultImageSizeY, 15 compressFormat, compressQuality, 16 getImageInputPath(), getImageOutputPath(), 17 cropCallback).execute(); 18 }
裁剪之前
setImageToWrapCropBounds
裁剪之前,先判断裁剪图片是否充满裁剪框,如果没有,进行移动和缩放让其充满。
1 public void setImageToWrapCropBounds(boolean animate) { 2 //mBitmapLaidOut图片加载OK,isImageWrapCropBounds()检查图片是否充满裁剪框 3 if (mBitmapLaidOut && !isImageWrapCropBounds()) { 4 //当前图片中心X点 5 float currentX = mCurrentImageCenter[0]; 6 //当前图片中心Y点 7 float currentY = mCurrentImageCenter[1]; 8 //当前图片缩放值 9 float currentScale = getCurrentScale(); 10 //差量 11 float deltaX = mCropRect.centerX() - currentX; 12 float deltaY = mCropRect.centerY() - currentY; 13 float deltaScale = 0; 14 //临时矩阵重置 15 mTempMatrix.reset(); 16 //临时矩阵移动 17 mTempMatrix.setTranslate(deltaX, deltaY); 18 //复制到新的数组 19 final float[] tempCurrentImageCorners = Arrays.copyOf(mCurrentImageCorners, mCurrentImageCorners.length); 20 //将此矩阵应用于二维点的数组,并编写转换后的指向数组的点 21 mTempMatrix.mapPoints(tempCurrentImageCorners); 22 //再检查图片是否充满裁剪框 23 boolean willImageWrapCropBoundsAfterTranslate = isImageWrapCropBounds(tempCurrentImageCorners); 24 if (willImageWrapCropBoundsAfterTranslate) { 25 //图片缩进的数组 26 final float[] imageIndents = calculateImageIndents(); 27 deltaX = -(imageIndents[0] + imageIndents[2]); 28 deltaY = -(imageIndents[1] + imageIndents[3]); 29 } else { 30 RectF tempCropRect = new RectF(mCropRect); 31 mTempMatrix.reset(); 32 mTempMatrix.setRotate(getCurrentAngle()); 33 mTempMatrix.mapRect(tempCropRect); 34 //获取裁剪图片的边 35 final float[] currentImageSides = RectUtils.getRectSidesFromCorners(mCurrentImageCorners); 36 deltaScale = Math.max(tempCropRect.width() / currentImageSides[0], 37 tempCropRect.height() / currentImageSides[1]); 38 deltaScale = deltaScale * currentScale - currentScale; 39 } 40 if (animate) { 41 //移动或缩放图片(有动画效果) 42 post(mWrapCropBoundsRunnable = new WrapCropBoundsRunnable( 43 CropImageView.this, mImageToWrapCropBoundsAnimDuration, currentX, currentY, deltaX, deltaY, 44 currentScale, deltaScale, willImageWrapCropBoundsAfterTranslate)); 45 } else { 46 //移动图片 47 postTranslate(deltaX, deltaY); 48 if (!willImageWrapCropBoundsAfterTranslate) { 49 //缩放图片 50 zoomInImage(currentScale + deltaScale, mCropRect.centerX(), mCropRect.centerY()); 51 } 52 } 53 } 54 }
进行裁剪
裁剪放到了异步,即BitmapCropTask继承AsyncTask,先设置原始图片resizeScale值,然后通过ExifInterface保存新的图片,即裁剪后的图片
1 public class BitmapCropTask extends AsyncTask<Void, Void, Throwable> { 2 3 …… 4 /** 5 * @param viewBitmap 裁剪图片bitmap 6 * @param cropRect 裁剪矩形 7 * @param currentImageRect 当前图片矩形 8 * @param currentScale 当前图片缩放值 9 * @param currentAngle 当前图片角度 10 * @param maxResultImageSizeX 图片裁剪后最大宽值 11 * @param maxResultImageSizeY 图片裁剪后最大高值 12 * @param compressFormat 图片裁剪的格式 13 * @param compressQuality 图片裁剪的质量 14 * @param imageInputPath 裁剪图片路径 15 * @param imageOutputPath 图片裁剪后路径 16 * @param cropCallback 裁剪回调 17 */ 18 public BitmapCropTask(@Nullable Bitmap viewBitmap, 19 @NonNull RectF cropRect, @NonNull RectF currentImageRect, 20 float currentScale, float currentAngle, 21 int maxResultImageSizeX, int maxResultImageSizeY, 22 @NonNull Bitmap.CompressFormat compressFormat, int compressQuality, 23 @NonNull String imageInputPath, @NonNull String imageOutputPath, 24 @Nullable BitmapCropCallback cropCallback) { 25 …… 26 } 27 @Override 28 @Nullable 29 protected Throwable doInBackground(Void... params) { 30 if (mViewBitmap == null || mViewBitmap.isRecycled()) { 31 return new NullPointerException("ViewBitmap is null or already recycled"); 32 } 33 if (mCurrentImageRect.isEmpty()) { 34 return new NullPointerException("CurrentImageRect is empty"); 35 } 36 //设置resizeScale值 37 float resizeScale = resize(); 38 try { 39 //裁剪 40 crop(resizeScale); 41 //回收 42 mViewBitmap.recycle(); 43 mViewBitmap = null; 44 } catch (Throwable throwable) { 45 return throwable; 46 } 47 return null; 48 } 49 private float resize() { 50 //初始Options 51 final BitmapFactory.Options options = new BitmapFactory.Options(); 52 //查询该位图,而无需分配存储器,可获取outHeight(图片原始高度)和 outWidth(图片的原始宽度) 53 options.inJustDecodeBounds = true; 54 //裁剪图片解码 55 BitmapFactory.decodeFile(mImageInputPath, options); 56 //原始图片和裁剪后图片比值 57 float scaleX = options.outWidth / mViewBitmap.getWidth(); 58 float scaleY = options.outHeight / mViewBitmap.getHeight(); 59 float resizeScale = Math.min(scaleX, scaleY); 60 mCurrentScale /= resizeScale; 61 //初始化值为1 62 resizeScale = 1; 63 if (mMaxResultImageSizeX > 0 && mMaxResultImageSizeY > 0) { 64 float cropWidth = mCropRect.width() / mCurrentScale; 65 float cropHeight = mCropRect.height() / mCurrentScale; 66 if (cropWidth > mMaxResultImageSizeX || cropHeight > mMaxResultImageSizeY) { 67 scaleX = mMaxResultImageSizeX / cropWidth; 68 scaleY = mMaxResultImageSizeY / cropHeight; 69 //设置resizeScale,如果是2就是高度和宽度都是原始的一半 70 resizeScale = Math.min(scaleX, scaleY); 71 mCurrentScale /= resizeScale; 72 } 73 } 74 return resizeScale; 75 } 76 private boolean crop(float resizeScale) throws IOException { 77 //ExifInterface这个接口提供了图片文件的旋转,gps,时间等信息,从原始图片读出Exif标签 78 ExifInterface originalExif = new ExifInterface(mImageInputPath); 79 int top = Math.round((mCropRect.top - mCurrentImageRect.top) / mCurrentScale); 80 int left = Math.round((mCropRect.left - mCurrentImageRect.left) / mCurrentScale); 81 int width = Math.round(mCropRect.width() / mCurrentScale); 82 int height = Math.round(mCropRect.height() / mCurrentScale); 83 //复制图片 84 boolean cropped = cropCImg(mImageInputPath, mImageOutputPath, 85 left, top, width, height, mCurrentAngle, resizeScale, 86 mCompressFormat.ordinal(), mCompressQuality); 87 if (cropped) { 88 //拿到裁剪后图片 89 copyExif(originalExif, width, height); 90 } 91 return cropped; 92 } 93 @SuppressWarnings("JniMissingFunction") 94 native public boolean cropCImg(String inputPath, String outputPath, 95 int left, int top, int width, int height, float angle, float resizeScale, 96 int format, int quality) throws IOException, OutOfMemoryError; 97 /** 98 * @param originalExif 原始图片Exif 99 * @param width 裁剪后图片宽 100 * @param height 裁剪后图片高 101 * @throws IOException 是否异常 102 */ 103 public void copyExif(ExifInterface originalExif, int width, int height) throws IOException { 104 //Exif标签数组 105 String[] attributes = new String[]{ 106 ExifInterface.TAG_APERTURE, 107 …… 108 }; 109 //指定裁剪后图片路径,初始化新的ExifInterface 110 ExifInterface newExif = new ExifInterface(mImageOutputPath); 111 String value; 112 for (String attribute : attributes) { 113 value = originalExif.getAttribute(attribute); 114 if (!TextUtils.isEmpty(value)) { 115 //设置Exif标签 116 newExif.setAttribute(attribute, value); 117 } 118 } 119 newExif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, String.valueOf(width)); 120 newExif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, String.valueOf(height)); 121 newExif.setAttribute(ExifInterface.TAG_ORIENTATION, "0"); 122 //保存 123 newExif.saveAttributes(); 124 } 125 @Override 126 protected void onPostExecute(@Nullable Throwable t) { 127 if (mCropCallback != null) { 128 if (t == null) { 129 //接口回调,over 130 mCropCallback.onBitmapCropped(Uri.fromFile(new File(mImageOutputPath))); 131 } else { 132 mCropCallback.onCropFailure(t); 133 } 134 } 135 } 136 }
总结
uCrop功能强大,对于我来说,有很多东西值得学习,难点如Rect包含问题(其实这块还不是很理解),新知识如ExifInterface操作图片,BitmapFactory显示图片的知识点温故等,还有自定义左右滑的标尺,都是不错的学习源码。抛砖引玉至此,over。