Android实现读取相机(相册)图片并进行剪裁

时间:2022-05-23 06:46:16

我们先说一下思路,在android系统中就自带了图片剪切的应用,所以,我们只需要将我们获取到的相片传给图片剪切应用,再将剪切好的相片返回到我们自己的界面显示就ok了

在开发一些app的过程中,我们可能涉及到头像的处理,比如从手机或者相册获取头像,剪裁成自己需要的头像,设置或上传头像等。网上一些相关的资料也是多不胜数,但在实际应用中往往会存在各种问题,没有一个完美的解决方案。由于近期项目的需求,就研究了一下,目前看来还没有什么问题。

这里我们只讨论获取、剪裁与设置,上传流程根据自己的业务需求添加。先上一张流程图:

Android实现读取相机(相册)图片并进行剪裁

这图是用google drive的绘图工具绘制的,不得不赞叹google可以把在线编辑工具做得如此强大。好吧,我就是google的脑残粉!回到主题,这是我设计的思路,接下来进行详细分析:

1、获得图片的途径无非就两种,第一是相机拍摄,第二是从本地相册获取。

2、我在sd卡上创建了一个文件夹,里面有两个uri,一个是用于保存拍照时获得的原始图片,一个是保存剪裁后的图片。之前我考虑过用同一个uri来保存图片,但是在实践中遇到一个问题,当拍照后不进行剪裁,那么下次从sd卡拿到就是拍照保存的大图,不仅丢失了之前剪裁的图片,还会因为加载大图导致内存崩溃。基于此考虑,我选择了两个uri来分别保存图片。

3、相机拍摄时,我们使用intent调用系统相机,并将设置输出设置到sdcard\xx\photo_file.jpg,以下是代码片段:

?
1
2
3
4
5
//调用系统相机
intent intentcamera = new intent(mediastore.action_image_capture);
//将拍照结果保存至photo_file的uri中,不保留在相册中
intentcamera.putextra(mediastore.extra_output, imagephotouri);
startactivityforresult(intentcamera, photo_request_carema);

在回调时,我们需要对photo_file.jpg调用系统工具进行剪裁,并设置输出设置到sdcard\xx\crop_file.jpg,以下是代码片段:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
case photo_request_carema:
  if (resultcode == result_ok) {
    //从相机拍摄保存的uri中取出图片,调用系统剪裁工具
    if (imagephotouri != null) {
      croputils.cropimageuri(this, imagephotouri, imageuri, ibusericon.getwidth(), ibusericon.getheight(), photo_request_cut);
    } else {
      toastutils.show(this, "没有得到拍照图片");
    }
  } else if (resultcode == result_canceled) {
    toastutils.show(this, "取消拍照");
  } else {
    toastutils.show(this, "拍照失败");
  }
  break;
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//调用系统的剪裁处理图片并保存至imageuri中
public static void cropimageuri(activity activity, uri orguri, uri desuri, int width, int height, int requestcode) {
  intent intent = new intent("com.android.camera.action.crop");
  intent.setdataandtype(orguri, "image/*");
  intent.putextra("crop", "true");
  intent.putextra("aspectx", 1);
  intent.putextra("aspecty", 1);
  intent.putextra("outputx", width);
  intent.putextra("outputy", height);
  intent.putextra("scale", true);
  //将剪切的图片保存到目标uri中
  intent.putextra(mediastore.extra_output, desuri);
  intent.putextra("return-data", false);
  intent.putextra("outputformat", bitmap.compressformat.jpeg.tostring());
  intent.putextra("nofacedetection", true);
  activity.startactivityforresult(intent, requestcode);
}

最后,我们需要在回调中取出crop_file.jpg,因为剪裁时,对图片已经进行了压缩,所以也不用担心内存的问题,在这里我提供两个方法,第一个是直接获取原始图片的bitmap,第二个是获取原始图片并做成圆形,相信大多数的人对后者比较感兴趣,哈哈!以下是代码片段:

?
1
2
3
4
5
6
7
8
9
case photo_request_cut:
  if (resultcode == result_ok) {
    bitmap bitmap = decodeuriiasbimap(this,imagecropuri)
  } else if (resultcode == result_canceled) {
    toastutils.show(this, "取消剪切图片");
  } else {
    toastutils.show(this, "剪切失败");
  }
  break;
?
1
2
3
4
5
6
7
8
9
10
11
//从uri中获取bitmap格式的图片
private static bitmap decodeuriasbitmap(context context, uri uri) {
  bitmap bitmap;
  try {
    bitmap = bitmapfactory.decodestream(context.getcontentresolver().openinputstream(uri));
  } catch (filenotfoundexception e) {
    e.printstacktrace();
    return null;
  }
  return bitmap;
}
?
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
//获取圆形图片
public static bitmap getroundedcornerbitmap(bitmap bitmap) {
  if (bitmap == null) {
  return null;
  }
  bitmap output = bitmap.createbitmap(bitmap.getwidth(), bitmap.getheight(), bitmap.config.argb_8888);
  canvas canvas = new canvas(output);
  final paint paint = new paint();
  /* 去锯齿 */
  paint.setantialias(true);
  paint.setfilterbitmap(true);
  paint.setdither(true);
  // 保证是方形,并且从中心画
  int width = bitmap.getwidth();
  int height = bitmap.getheight();
  int w;
  int deltax = 0;
  int deltay = 0;
  if (width <= height) {
    w = width;
    deltay = height - w;
  } else {
    w = height;
    deltax = width - w;
  }
  final rect rect = new rect(deltax, deltay, w, w);
  final rectf rectf = new rectf(rect);
 
  paint.setantialias(true);
  canvas.drawargb(0, 0, 0, 0);
  // 圆形,所有只用一个
  int radius = (int) (math.sqrt(w * w * 2.0d) / 2);
  canvas.drawroundrect(rectf, radius, radius, paint);
  paint.setxfermode(new porterduffxfermode(porterduff.mode.src_in));
  canvas.drawbitmap(bitmap, rect, rect, paint);
  return output;
}

4、相册获取时,这也是最难的地方。android 4.4以下的版本,从相册获取的图片uri能够完美调用系统剪裁工具,或者直接从选取相册是带入剪裁图片的intent,而且效果非常完美。但是在android 4.4及其以上的版本,获取到的uri根本无法调用系统剪裁工具,会直接导致程序崩溃。我也是研究了很久,才发现两者的uri有很大的区别,google官方文档中让开发者使用intent.action_get_content代替以前的action,并且就算你仍然使用以前的action,都会返回一种新型的uri,我个人猜测是因为google把所有的内容获取分享做成一个统一的uri,如有不对,请指正!想通这一点后,问题就变得简单了,我把这种新型的uri重新封装一次,得到以为"file:\\..."标准的绝对路劲,传入系统剪裁工具中,果然成功了,只是这个封装过程及其艰难,查阅了很多资料,终于还是拿到了。下面说下具体步骤:

第一、调用系统相册,以下是代码片段:

?
1
2
3
4
//调用系统相册
  intent photopickerintent = new intent(intent.action_get_content);
  photopickerintent.settype("image/*");
  startactivityforresult(photopickerintent, photo_request_gallery);

第二、在回调中,重新封装uri,并调用系统剪裁工具将输出设置到crop_file.jpg,调用系统剪裁工具代码在拍照获取的步骤中已经贴出,这里就不重复制造车轮了,重点贴重新封装uri的代码,以下是代码片段:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
case photo_request_gallery:
  if (resultcode == result_ok) {
    //从相册选取成功后,需要从uri中拿出图片的绝对路径,再调用剪切
    uri newuri = uri.parse("file:///" + croputils.getpath(this, data.getdata()));
    if (newuri != null) {
      croputils.cropimageuri(this, newuri, imageuri, ibusericon.getwidth(),
      ibusericon.getheight(), photo_request_cut);
    } else {
      toastutils.show(this, "没有得到相册图片");
    }
  } else if (resultcode == result_canceled) {
    toastutils.show(this, "从相册选取取消");
  } else {
    toastutils.show(this, "从相册选取失败");
  }
  break;
?
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
@suppresslint("newapi")
public static string getpath(final context context, final uri uri) {
 
final boolean iskitkat = build.version.sdk_int >= build.version_codes.kitkat;
 
// documentprovider
if (iskitkat && documentscontract.isdocumenturi(context, uri)) {
  // externalstorageprovider
  if (isexternalstoragedocument(uri)) {
    final string docid = documentscontract.getdocumentid(uri);
    final string[] split = docid.split(":");
    final string type = split[0];
 
    if ("primary".equalsignorecase(type)) {
      return environment.getexternalstoragedirectory() + "/"+ split[1];
    }
 
  }
  // downloadsprovider
  else if (isdownloadsdocument(uri)) {
 
    final string id = documentscontract.getdocumentid(uri);
    final uri contenturi = contenturis.withappendedid(uri.parse("content://downloads/public_downloads"),long.valueof(id));
 
    return getdatacolumn(context, contenturi, null, null);
  }
  // mediaprovider
  else if (ismediadocument(uri)) {
    final string docid = documentscontract.getdocumentid(uri);
    final string[] split = docid.split(":");
    final string type = split[0];
 
    uri contenturi = null;
    if ("image".equals(type)) {
      contenturi = mediastore.images.media.external_content_uri;
    } else if ("video".equals(type)) {
      contenturi = mediastore.video.media.external_content_uri;
    } else if ("audio".equals(type)) {
      contenturi = mediastore.audio.media.external_content_uri;
    }
 
    final string selection = "_id=?";
    final string[] selectionargs = new string[]{split[1]};
 
    return getdatacolumn(context, contenturi, selection,selectionargs);
    }
  }
  // mediastore (and general)
  else if ("content".equalsignorecase(uri.getscheme())) {
    return getdatacolumn(context, uri, null, null);
  }
  // file
  else if ("file".equalsignorecase(uri.getscheme())) {
    return uri.getpath();
  }
 
  return null;
}
 
/**
* get the value of the data column for this uri. this is useful for
* mediastore uris, and other file-based contentproviders.
*
* @param context    the context.
* @param uri      the uri to query.
* @param selection   (optional) filter used in the query.
* @param selectionargs (optional) selection arguments used in the query.
* @return the value of the _data column, which is typically a file path.
*/
private static string getdatacolumn(context context, uri uri,string selection, string[] selectionargs) {
 
  cursor cursor = null;
  final string column = "_data";
  final string[] projection = {column};
 
  try {
    cursor = context.getcontentresolver().query(uri, projection,selection, selectionargs, null);
    if (cursor != null && cursor.movetofirst()) {
      final int column_index = cursor.getcolumnindexorthrow(column);
      return cursor.getstring(column_index);
    }
  } finally {
    if (cursor != null)
      cursor.close();
  }
  return null;
}
 
/**
* @param uri the uri to check.
* @return whether the uri authority is externalstorageprovider.
*/
private static boolean isexternalstoragedocument(uri uri) {
  return "com.android.externalstorage.documents".equals(uri.getauthority());
}
 
/**
* @param uri the uri to check.
* @return whether the uri authority is downloadsprovider.
*/
private static boolean isdownloadsdocument(uri uri) {
  return "com.android.providers.downloads.documents".equals(uri.getauthority());
}
 
/**
* @param uri the uri to check.
* @return whether the uri authority is mediaprovider.
*/
private static boolean ismediadocument(uri uri) {
  return "com.android.providers.media.documents".equals(uri.getauthority());
}

后续的系统剪裁工具调用跟拍照获取步骤一致,请参见上的代码。

5、所有步骤完成,在nexus 5设备中的最新系统中测试通过,在小米、三星等一些设备中表现也很完美。如果在你的设备上存在缺陷,一定要跟帖给我反馈,谢谢!

文章结尾附上一个网友的完整示例,给了我很多的参考

?
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package com.only.android.app;
 
import java.io.file;
 
import android.app.activity;
import android.app.alertdialog;
import android.content.dialoginterface;
import android.content.intent;
import android.graphics.bitmap;
import android.graphics.bitmapfactory;
import android.net.uri;
import android.os.bundle;
import android.os.systemclock;
import android.provider.mediastore;
import android.view.view;
import android.widget.button;
import android.widget.imageview;
 
import com.only.android.r;
 
public class copyofimagescaleactivity extends activity implements view.onclicklistener {
  /** called when the activity is first created. */
  private button selectimagebtn;
  private imageview imageview;
   
  private file sdcardtempfile;
  private alertdialog dialog;
  private int crop = 180;
 
  @override
  public void oncreate(bundle savedinstancestate) {
    super.oncreate(savedinstancestate);
    setcontentview(r.layout.imagescale);
 
    selectimagebtn = (button) findviewbyid(r.id.selectimagebtn);
    imageview = (imageview) findviewbyid(r.id.imageview);
 
    selectimagebtn.setonclicklistener(this);
    sdcardtempfile = new file("/mnt/sdcard/", "tmp_pic_" + systemclock.currentthreadtimemillis() + ".jpg");
 
  }
 
  @override
  public void onclick(view v) {
    if (v == selectimagebtn) {
      if (dialog == null) {
        dialog = new alertdialog.builder(this).setitems(new string[] { "相机", "相册" }, new dialoginterface.onclicklistener() {
          @override
          public void onclick(dialoginterface dialog, int which) {
            if (which == 0) {
              intent intent = new intent("android.media.action.image_capture");
              intent.putextra("output", uri.fromfile(sdcardtempfile));
              intent.putextra("crop", "true");
              intent.putextra("aspectx", 1);// 裁剪框比例
              intent.putextra("aspecty", 1);
              intent.putextra("outputx", crop);// 输出图片大小
              intent.putextra("outputy", crop);
              startactivityforresult(intent, 101);
            } else {
              intent intent = new intent("android.intent.action.pick");
              intent.setdataandtype(mediastore.images.media.internal_content_uri, "image/*");
              intent.putextra("output", uri.fromfile(sdcardtempfile));
              intent.putextra("crop", "true");
              intent.putextra("aspectx", 1);// 裁剪框比例
              intent.putextra("aspecty", 1);
              intent.putextra("outputx", crop);// 输出图片大小
              intent.putextra("outputy", crop);
              startactivityforresult(intent, 100);
            }
          }
        }).create();
      }
      if (!dialog.isshowing()) {
        dialog.show();
      }
    }
  }
 
  @override
  protected void onactivityresult(int requestcode, int resultcode, intent intent) {
    if (resultcode == result_ok) {
      bitmap bmp = bitmapfactory.decodefile(sdcardtempfile.getabsolutepath());
      imageview.setimagebitmap(bmp);
    }
  }
}

最后再啰嗦一句,功能虽然已经实现了,但是实际代码还是可以进一步优化的,感兴趣的童鞋们可以改进下。