前段时间做用户头像设置与上次,由于设计需要,将用户头像裁剪成圆形并设置上传.感觉裁剪成圆形图片的功能,以后很可能会用到,加之网上这一类的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,然后调用自定义裁剪,之后的过程与上面一致了.
最后,把源码上传一下,希望对别人对自己也有帮助.
demo链接
@author 北京青牛软件南方基地_码农crst_luo 2017-11-06