前段时间做用户头像设置与上次,由于设计需要,将用户头像裁剪成圆形并设置上传.感觉裁剪成圆形图片的功能,以后很可能会用到,加之网上这一类的demo总结注释的不好,于是自己就做了demo并写好注释上传.
废话少说,先上图:
关键代码:
一:调用系统摄像头拍照后获取图片然后裁剪流程
1:调用系统摄像头拍照
/** * 打开系统摄像头拍照获取图片 */ private void openCamera() { String state = Environment.getExternalStorageState(); if (state.equals(Environment.MEDIA_MOUNTED)) { Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);//设置Action为拍照 if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { imageUri = Uri.fromFile(headIconFile); } else { //FileProvider为7.0新增应用间共享文件,在7.0上暴露文件路径会报FileUriExposedException //为了适配7.0,所以需要使用FileProvider,具体使用百度一下即可 imageUri = FileProvider.getUriForFile(this, "com.channelst.headimgclip.fileprovider", headIconFile);//通过FileProvider创建一个content类型的Uri } intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //添加这一句表示对目标应用临时授权该Uri所代表的文件 intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); startActivityForResult(intent, TAKE_PHOTO_REQUEST_CODE); Log.e(TAG, "openCamera()---intent" + intent); } }
2.调用系统摄像头返回,进入case TAKE_PHOTO_REQUEST_CODE
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { Log.e(TAG, "onActivityResult()---requestCode" + requestCode + ", resultCode : " + resultCode); switch (requestCode) { case CLIP_PHOTO_BY_SYSTEM_REQUEST_CODE: Log.d(TAG,"调用系统剪辑照片后返回........."); if (resultCode == RESULT_OK) { Bitmap bm = BitmapFactory.decodeFile(headClipFile.getAbsolutePath()); headImg.setImageBitmap(bm); Log.e(TAG, "onActivityResult()---bm : " + bm); } else { Log.e(TAG, "onActivityResult()---resultCode : " + resultCode); } break; case TAKE_PHOTO_REQUEST_CODE: Log.i(TAG,"拍照后返回........."); if (resultCode == RESULT_OK) { //拍照后返回,调用系统裁剪,系统裁剪无法裁剪成圆形 //clipPhotoBySystem(imageUri); //调用自定义裁剪 clipPhotoBySelf(headIconFile.getAbsolutePath()); } break; case CHOOSE_PHOTO_REQUEST_CODE: Log.i(TAG, "从相册选取照片后返回...."); if (resultCode == RESULT_OK) { if (data != null) { String filePath = ""; Uri originalUri = data.getData(); // 获得图片的uri Log.i(TAG, "originalUri : " + originalUri); if (originalUri != null) { filePath = GetImagePath.getPath(this,originalUri); } Log.i(TAG, "filePath : " + filePath); if (filePath != null && filePath.length() > 0) { //clipPhotoBySystem(originalUri); //调用自定义裁剪 clipPhotoBySelf(filePath); } } } break; case CLIP_PHOTO_BY_SELF_REQUEST_CODE: Log.i(TAG, "从自定义切图返回.........."); if (resultCode == RESULT_OK) { Bitmap bm = BitmapFactory.decodeFile(headClipFile.getAbsolutePath()); headImg.setImageBitmap(bm); Log.i(TAG, "onActivityResult()---bm : " + bm); } else { Log.i(TAG, "onActivityResult()---resultCode : " + resultCode); } break; } }3.调用自定义裁剪方法,进入ClipPitctureActivity,如图一界面
/** * 调用自定义切图方法 * * @param filePath */ protected void clipPhotoBySelf(String filePath) { Log.i(TAG, "通过自定义方式去剪辑这个照片"); //进入裁剪页面,此处用的是自定义的裁剪页面而不是调用系统裁剪 Intent intent = new Intent(this, ClipPictureActivity.class); intent.putExtra(ClipPictureActivity.IMAGE_PATH_ORIGINAL, filePath); intent.putExtra(ClipPictureActivity.IMAGE_PATH_AFTER_CROP, headClipFile.getAbsolutePath()); startActivityForResult(intent, CLIP_PHOTO_BY_SELF_REQUEST_CODE); }
4.在ClipPitctureActivity里,在主要布局加载完成后,加入一个自定义的ClipView,该View主要是在图片上方覆盖一层幕布,然后从中间抠出一个圆形
我们先来看clipView的初始化过程:
/** * 初始化截图区域,并将源图按裁剪框比例缩放 * */ private void initClipView() { Intent intent = getIntent(); final String originalImgPath = intent.getStringExtra(IMAGE_PATH_ORIGINAL); croppedImagePath = intent.getStringExtra(IMAGE_PATH_AFTER_CROP); // 首先判断源文件是否存在--防止垃圾数据的影响: // 一张图片在SD卡上已经被删除,但是媒体库中还有该数据。 File file = new File(originalImgPath); if (!file.exists()) { Toast.makeText(this, "源文件在SD卡上不存在", Toast.LENGTH_SHORT).show(); finish(); return; } ...... //初始化截图区域自定义view clipview = new ClipView(ClipPictureActivity.this); clipview.addOnDrawCompleteListener(new ClipView.OnDrawListenerComplete() { public void onDrawComplete() { clipview.removeOnDrawCompleteListener(); int radius = (int) clipview.getRadius(); int midX = (int) clipview.getCircleCenterPX(); int midY = (int) clipview.getCircleCenterPY(); int imageWidth = bitmap.getWidth(); int imageHeight = bitmap.getHeight(); // 按裁剪框求缩放比例 float scale = (radius * 3.0f) / imageWidth; // 起始中心点 float imageMidX = imageWidth * scale / 2; float imageMidY = imageHeight * scale / 2; srcPic.setScaleType(ImageView.ScaleType.MATRIX); // 缩放 matrix.postScale(scale, scale); // 平移 matrix.postTranslate(midX - imageMidX, midY - imageMidY); srcPic.setImageMatrix(matrix); srcPic.setImageBitmap(bitmap); } }); matrix.reset(); srcLayout.addView(clipview, new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); }
再来看clipView的onDraw(),如下,onDraw里是先画一个矩形幕布,然后抠出一个圆来,最后画一个白色的边框
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int width = this.getWidth(); int height = this.getHeight(); //画矩形region1 canvas.clipRect(0, 0, width, height); //竖屏的时候width<height,取width的1/3作为半径 // 横屏的时候width>height,取height的1/3作为半径 int shortWidth = width<height?width:height; //画圆形region2 Path path = new Path(); circleCenterPX = (float) width/2.0f; circleCenterPY = (float) height/2.0f; radius = shortWidth/3.0f; path.addCircle(circleCenterPX, circleCenterPY, radius, Path.Direction.CCW); Log.i("ClipView", "onDraw()--circleCenterPX : " + circleCenterPX + ", circleCenterPY : " + circleCenterPY + ", radius : " + radius); Log.i("ClipView", "onDraw()--width : " + width + ", height : " + height); //path.addCircle(150,150,100, Path.Direction.CCW); //XOP表示补集就是全集的减去交集剩余部分,这剩余部分不用遮罩 //也就相当于从遮罩里抠出一个圆形来 canvas.clipPath(path, Region.Op.XOR); //canvas.clipRect(0,0,400,400); paint.setAlpha(((int)(255*0.4f))); canvas.drawRect(0, 0, width, height,paint); canvas.save(); canvas.restore(); // 画圆形边框 borderPaint.setStyle(Paint.Style.STROKE); borderPaint.setAntiAlias(true); borderPaint.setColor(Color.WHITE); borderPaint.setStrokeWidth(clipBorderWidth); canvas.drawCircle(circleCenterPX, circleCenterPY, radius,borderPaint); clipWidth = clipHeight = (int) (radius*2); if (listenerComplete != null) { listenerComplete.onDrawComplete(); } }
5.接下来我们看保存图片的过程.
1)获取图片中的包含圆的矩形,2)把获取的矩形选取中间的圆形,也就是getCircleBitmap()方法
/** * @return 裁剪后的图片 * @description 获取裁剪框内截图 */ private Bitmap getBitmap() { try { // srcPic.getDrawingCache()获取View截图在某些情况下报错了。 // 现在用一种新的获取view中图像的方法取代getDrawingCache()方法. // 另:在使用createBitmap()增加try..catch..以防止不断生成bitmap可能导致的oom int startX = (int) (clipview.getCircleCenterPX() - clipview.getRadius()); int startY = (int) (clipview.getCircleCenterPY() - clipview.getRadius()); Log.i(TAG, "getBitmap():startX=" + startX + ",startY=" + startY + ",clipview.getClipWidth()=" + clipview.getClipWidth() + ",clipview.getWidth()=" + clipview.getWidth() + ",clipview.getCircleCenterPX()=" + clipview.getCircleCenterPX() + ",clipview.getRadius()=" + clipview.getRadius() + ",clipview.getCircleCenterPY()=" + clipview.getCircleCenterPY()); Bitmap finalBitmap = Bitmap.createBitmap( loadBitmapFromView(srcPic), startX, startY, clipview.getClipWidth(), clipview.getClipHeight()); // 释放资源 srcPic.destroyDrawingCache(); Log.i(TAG, "getBitmap() finalBitmap=" + finalBitmap); return getCircleBitmap(finalBitmap); } catch (OutOfMemoryError err) { Toast.makeText(this, "保存头像失败", Toast.LENGTH_SHORT).show(); Log.e(TAG, err.getMessage()); return null; } catch (Exception e) { Toast.makeText(this, "保存头像失败!", Toast.LENGTH_SHORT).show(); Log.e(TAG, e.getMessage()); return null; } } /** * @description 获取圆形裁剪框内截图 * @param bitmap src图片 * @return */ public static Bitmap getCircleBitmap(Bitmap bitmap) { Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(output); final int color = 0xff424242; final Paint paint = new Paint(); final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); paint.setAntiAlias(true); paint.setFilterBitmap(true); paint.setDither(true); canvas.drawARGB(0, 0, 0, 0); paint.setColor(color); //在画布上绘制一个圆 -1是为了去掉白色的边框 canvas.drawCircle(bitmap.getWidth() / 2, bitmap.getHeight() / 2, bitmap.getWidth() / 2 - 1, paint); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); canvas.drawBitmap(bitmap, rect, rect, paint); Log.i(TAG, "getCircleBitmap() output=" + output); return output; }6.将圆形bitmap通过io流写入到图片文件中,如下
/** * @return void * @Title: saveMyBitmap * @Description: 保存bitmap对象到裁剪后的文件中 */ public static void saveMyBitmap(File file, Bitmap mBitmap) { try { if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } if (!file.exists()) { file.createNewFile(); } } catch (IOException e) { e.printStackTrace(); } FileOutputStream fOut = null; try { fOut = new FileOutputStream(file); } catch (FileNotFoundException e) { e.printStackTrace(); } mBitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut); try { fOut.flush(); } catch (IOException e) { e.printStackTrace(); } try { fOut.close(); } catch (IOException e) { e.printStackTrace(); } }
7.通过setResutl,返回ok
/** * 保存图片 * 首先获取裁剪框里的图片 * 然后保存到裁剪文件croppedImagePath中 */ private void saveBitmap() { if (bitmap != null) { // 取出裁剪图片 Bitmap clipBitmap = getBitmap(); Log.i(TAG, "saveBitmap() clipBitmap=" + clipBitmap); File file = new File(croppedImagePath); saveMyBitmap(file, clipBitmap); } Intent intent = new Intent(); ClipPictureActivity.this.setResult(RESULT_OK, intent); finish(); }
8.这时候Mainactivity就收到CLIP_PHOTO_BY_SELF_REQUEST_CODE这个返回,在OnActivityResult即会进入该case
然后headImg这个imageView就将其显示出来.如图二
二:从图库中选择图片然后裁剪流程
从图库中选择图片然后裁剪流程和拍照获取图片裁剪流程差不多,差异只在于图片的获取方式
这里只介绍一下调用系统图库的代码:
点击"从相册选择"这个button即触发如下方法,启动系统相册
/** * 从系统图库中选择图片 */ private void choosePhoto() { String state = Environment.getExternalStorageState(); if (state.equals(Environment.MEDIA_MOUNTED)) { Intent openAlbumIntent = new Intent(Intent.ACTION_GET_CONTENT); openAlbumIntent.setType(IMAGE_TYPE); startActivityForResult(openAlbumIntent, CHOOSE_PHOTO_REQUEST_CODE); } }选择图片后返回界面即进入CHOOSE_PHOTO_REQUEST_CODE这个case,然后调用自定义裁剪,之后的过程与上面一致了.
最后,把源码上传一下,希望对别人对自己也有帮助.
@author 北京青牛软件南方基地_码农crst_luo 2017-11-06