从相机(相册)获取图片并剪裁的最佳实践

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

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

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

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

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

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

//调用系统相机
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,以下是代码片段:

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;
//调用系统的剪裁处理图片并保存至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,第二个是获取原始图片并做成圆形,相信大多数的人对后者比较感兴趣,哈哈!以下是代码片段:

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;
//从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;
}
//获取圆形图片
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:\\..."标准的绝对路劲,传入系统剪裁工具中,果然成功了,只是这个封装过程及其艰难,查阅了很多资料,终于还是拿到了。下面说下具体步骤:

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

//调用系统相册
  Intent photoPickerIntent = new Intent(Intent.ACTION_GET_CONTENT);
  photoPickerIntent.setType("image/*");
  startActivityForResult(photoPickerIntent, PHOTO_REQUEST_GALLERY);

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

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;
@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设备中的最新系统中测试通过,在小米、三星等一些设备中表现也很完美。如果在你的设备上存在缺陷,一定要跟帖给我反馈,谢谢!

记录于此,希望能帮助到一些正遇到这种问题的朋友!

时间: 2024-10-10 02:56:38

从相机(相册)获取图片并剪裁的最佳实践的相关文章

从相机相册获取图片裁剪后用于评论晒图或更换背景图

这是我人生中写的第一篇博客,是否要纪念一下这一刻(2016.09.01 16:52).其实关于写博客,老早就有这种写法,首先觉得他能够帮我总结我学到的和用过的技术,其次还能帮助那些和我有一样需求的人,我也是很开心啊,但是至于为什么现在才写第一篇,首先没有想好写什么,然后前段时间也确实比较忙.是不是那些来观技术的人已经想骂人了啊...啊哦!原谅我第一次写博客的激动心情吧! 废话不多说,开始我们的问题吧,首先因为我做了两次关于调用相机和相册获取图片的功能,觉得很有必要总结一下,下面我将从这两个功能出

iOS 从相机或相册获取图片并裁剪

/load user image - (void)UesrImageClicked { UIActionSheet *sheet; // 判断是否支持相机 if([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { sheet = [[UIActionSheet alloc] initWithTitle:@"选择图像" delegate:self cancel

android 调用系统相机获取图片、调用系统相册获取图片,并对图片进行截取

打开系统相册获取图片并截取,代码相对简单 1 Intent intent = new Intent(Intent.ACTION_GET_CONTENT,null); 2 intent.setType("image/*"); 3 intent.putExtra("crop", "true"); 4 5 //WIDTH 和 HEIGHT指的是截取框的宽高比例,如设WIDTH = 1,HEIGHT = 1,截取框就为正方形 6 intent.putEx

从相册获取图片及调用相机拍照获取图片,最后上传图片到服务器

调用相机拍照获取图片: 跳转到到拍照界面: Intent takeIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); //下面这句指定调用相机拍照后的照片存储的路径 mSzImageFileName = Long.toString(System.currentTimeMillis()) + ".png"; takeIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new F

Android相机、相册获取图片显示(压缩)并保存到SD卡

做过类似需求的同学都知道,在Activity中通过如下代码可以启动相机,然后在重写的onActivityResult方法中可以获取到返回的照片数据: Intent openCameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); startActivityForResult(openCameraIntent, TAKE_PICTURE); 在onActivityResult方法里通过Intent的getData方法获取的数据转换成bi

android 从相册获取图片

从相册获取图片 在android应用中是运用得很多的一项技术了 其实很简单 使用intent就可以了 刚敲了一个dome  睡觉之前还是分享一下把 1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 2 xmlns:tools="http://schemas.android.com/tools" 3 android:layout_width="match

Android从相册获取图片

应用中经常会有去相册获取图片的需求,每次总是会忘,现在这做个记录,以便以后查阅. 从应用去相册获取图片主要有以下几个步骤: 1.设置intent调用系统相册 2.进入相册选取图片,如果相册没有满意的图片,可以使用相机照相获取,最后返回数据到应用 3.应用获取图片后,调用裁剪程序对图片进行裁剪 4.返回裁剪的图片并显示出来 一.设置intent调用系统相册,并进入相册选取图片 /** * 调用相册 */ private void goAlbums() { Intent intent = new I

ios中摄像头/相册获取图片,压缩图片,上传服务器方法总结

相册 iphone的相册包含摄像头胶卷+用户计算机同步的部分照片.用户可以通过UIImagePickerController类提供的交互对话框来从相册中选择图像.但是,注意:相册中的图片机器路径无法直接从应用程序访问,只能通过终端用户去选择和使用相册图片 应用程序包 应用程序包可能会将图像与可执行程序.Info.plist文件和其他资源一同存储.我们可以通过本地文件路径来读取这些基于包的图像并在应用程序中显示它们. 沙盒 借助沙盒,我们可以把图片存储到Documents.Library.tmp文

iOS 使用AFN 进行单图和多图上传 摄像头/相册获取图片,压缩图片

图片上传时必要将图片进行压缩,不然会上传失败 首先是同系统相册选择图片和视频.iOS系统自带有UIImagePickerController,可以选择或拍摄图片视频,但是最大的问题是只支持单选,由于项目要求需要支持多选,只能自己自定义.获取系统图库的框架有两个,一个是ALAssetsLibrary,兼容iOS低版本,但是在iOS9中是不建议使用的:另一个是PHAsset,但最低要求iOS8以上.兼容到iOS7,可以选择了ALAssetsLibrary 现在我们先说选择一张图的情况 一.单图多图上