转自:http://www.jb51.net/article/70671.htm
在 Android应用中,很多时候我们需要实现上传图片,或者直接调用手机上的拍照功能拍照处理然后直接显示并上传功能,下面将讲述调用相机拍照处理图片然后显示和调用手机相册中的图片处理然后显示的功能
我们先说一下思路,在android系统中就自带了图片剪切的应用,所以,我们只需要将我们获取到的相片传给图片剪切应用,再将剪切好的相片返回到我们自己的界面显示就ok了
在开发一些APP的过程中,我们可能涉及到头像的处理,比如从手机或者相册获取头像,剪裁成自己需要的头像,设置或上传头像等。网上一些相关的资料也是多不胜数,但在实际应用中往往会存在各种问题,没有一个完美的解决方案。由于近期项目的需求,就研究了一下,目前看来还没有什么问题。
这里我们只讨论获取、剪裁与设置,上传流程根据自己的业务需求添加。先上一张流程图:
这图是用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中拿出图片的绝对路径,再调用剪切
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);
}
}
}
|
最后再啰嗦一句,功能虽然已经实现了,但是实际代码还是可以进一步优化的,感兴趣的童鞋们可以改进下。