zoukankan      html  css  js  c++  java
  • 从相机(相册)获取图片并剪裁的最佳实践

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

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

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

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

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

    3、相机拍摄时,我们使用Intent调用系统相机,并将设置输出设置到SDCardxxphoto_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调用系统工具进行剪裁,并设置输出设置到SDCardxxcrop_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设备中的最新系统中测试通过,在小米、三星等一些设备中表现也很完美。如果在你的设备上存在缺陷,一定要跟帖给我反馈,谢谢!

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

  • 相关阅读:
    LeetCode Path Sum II
    LeetCode Longest Palindromic Substring
    LeetCode Populating Next Right Pointers in Each Node II
    LeetCode Best Time to Buy and Sell Stock III
    LeetCode Binary Tree Maximum Path Sum
    LeetCode Find Peak Element
    LeetCode Maximum Product Subarray
    LeetCode Intersection of Two Linked Lists
    一天一个设计模式(1)——工厂模式
    PHP迭代器 Iterator
  • 原文地址:https://www.cnblogs.com/chuanstone/p/4705550.html
Copyright © 2011-2022 走看看