Android7.0及以上拍照,从相册选取和裁剪功能的实现

Android7.0调用相机时出现的一个错误:
android.os.FileUriExposedException: file:///storage/emulated/0/test.jpg exposed beyond app through ClipData.Item.getUri()
解决办法:在Application的onCreat()方法中添加以下代码:

  // android 7.0系统解决拍照的问题
  StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
  StrictMode.setVmPolicy(builder.build());
  builder.detectFileUriExposure(); 

以前调用相机的时候,代码大概是这样的

Intent openCameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
Uri imageUri = Uri.fromFile(mediaFile);
openCameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(openCameraIntent, TAKE_PICTURE);

但是7.0之后直接被废弃,不改就会崩溃,所以这代码肯定要改进。
首先是权限的申请,这里就不说明了,主要是这三个权限:

Manifest.permission.WRITE_EXTERNAL_STORAGE
Manifest.permission.READ_EXTERNAL_STORAGE
Manifest.permission.CAMERA

7.0之后的不同之处在于用content://uri来代替file://uri,所以需要用ContentProvider去访问文件,FileProvider是很好的选择。

 
            
        

有必要说明一下属性:
authorities:值一般是"项目的包名 + .provider",也可以不是,使用FileProvider.getUriForFile方法时参数和清单文件注册时的保持一致才能正常使用。
exported:是否对外开放
grantUriPermissions:是否授予临时权限,设置为true。
标签里面是用来指定共享的路径。
就是我们的共享路径配置的xml文件,可以自己命名。该文件放在res/xml文件夹下,若没有xml文件夹,自己创建一个。文件取名为filefile_pathspaths.xml。



    
    
    
    
    
    
    
    
    

然后就是URI的获取

/**
     * 获取URI,android7.0以后的uri发生改变
     *
     * @param context
     * @return
     */
    public static Uri getImageUri(Context context, String imgName) {
        String path = context.getFilesDir() + File.separator + "images" + File.separator;
        File file = new File(path, imgName);
        if (!file.getParentFile().exists())
            file.getParentFile().mkdirs();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            return FileProvider.getUriForFile(context, context.getPackageName(), file);
        } else {
            return Uri.fromFile(file);
        }
    }

需要注意的是,这边参数需要和清单文件里面声明的保持一致。

 /**
     * 拍照
     *
     * @param activity
     * @param cameraUri
     * @param requestCode
     */
    public static void takePhoto(Activity activity, Uri cameraUri, int requestCode) {
        Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
        intent.putExtra(MediaStore.EXTRA_OUTPUT, cameraUri);
        activity.startActivityForResult(intent, requestCode);
    }

cameraUri就是调用getImageUri获取到的uri,因为这个uri在onActivityResult中有其他用途,所以需要在相关的activity声明变量持有。
通过上诉代码就能正常调用拍照了,然后就是onActivityResult的一些业务逻辑了。

protected void setActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        if (requestCode == RESULT_CANCELED) {
            return;
        }
        outputUri = getOutputHeaderUri(IMAGE_HEADER_NAME);
        switch (requestCode) {
            case CODE_GALLERY_REQUEST:
                if (data != null) {
                   cropRawPhoto(mActivity, data.getData(), outputUri, CODE_RESULT_REQUEST);
                }
                break;
            case CODE_CAMERA_REQUEST:
                if (resultCode == RESULT_OK) {
                   cropRawPhoto(mActivity, cameraUri, outputUri, CODE_RESULT_REQUEST);
                }
            case CODE_RESULT_REQUEST:
                if (resultCode == RESULT_OK) {
                    try {
                        Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(outputUri));
                        mIvHeader.setImageBitmap(bitmap);
                        //上传给服务器
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                break;
            default:
                break;
        }
    }

之所以没有在 (requestCode == RESULT_CANCELED) 判断中加入data的判空,是因为有些时候返回值就是null的,比如上面的那个拍照方法调用的返回结果就是个null。
cropRawPhoto裁剪功能,拍照啊,选取图片得到的图片可能并不符合需求,需要经过裁剪,比如头像的设置。

 /**
     * 裁剪图片
     *
     * @param activity
     * @param inputUri
     * @param outputUri
     * @param requestCode
     */
    public static void cropRawPhoto(Activity activity, Uri inputUri, Uri outputUri, int requestCode) {
        Intent intent = new Intent("com.android.camera.action.CROP");
        intent.setDataAndType(inputUri, "image/*");
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        intent.putExtra("crop", "true");
        intent.putExtra("aspectX", 1);
        intent.putExtra("aspectY", 1);
        intent.putExtra("outputX", 300);
        intent.putExtra("outputY", 300);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
        intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
        activity.startActivityForResult(intent, requestCode);
    }

需要强调的是 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);是因为app提示无法保存裁剪后的图片,查询之后增加的。

 /**
     * 获取一个保存的uri
     *
     * @param name
     * @return
     */
    public static Uri getOutputHeaderUri(String name) {
        return Uri.parse("file://" + "/" + Environment.getExternalStorageDirectory().getPath() + "/" + name);
    }

这个方法我本来是直接使用getImageUri获取的,结果反而报错了,查询后发现这个确实要使用原来的。
还有个方法

 /**
     * 选取相册中的图片
     *
     * @param activity
     * @param requestCode
     */
    public static void choseHeadImageFromGallery(Activity activity, int requestCode) {
        Intent intentFromGallery = new Intent();
        intentFromGallery.setType("image/*");
        intentFromGallery.setAction(Intent.ACTION_PICK);
        activity.startActivityForResult(intentFromGallery, requestCode);
    }

基本上代码都齐了,另外就是一些变量和常量

//请求码
    private static final int CODE_GALLERY_REQUEST = 0xa0;
    private static final int CODE_CAMERA_REQUEST = 0xa1;
    private static final int CODE_RESULT_REQUEST = 0xa2;
    //拍照保存的URI和确定上传到后台的图片URI
    private Uri cameraUri;
    private Uri outputUri;
    private static final String IMAGE_FILE_NAME = "temp_header.jpg";
    private static final String IMAGE_HEADER_NAME = "header.jpg";

至于点击事件和权限的获取个人实现方式不同就不越俎代庖了,记得是在获取成功之后才调用这些方法的。
有什么错误的地方请大佬多多指教!

你可能感兴趣的:(Android7.0及以上拍照,从相册选取和裁剪功能的实现)