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

本文介绍如何解决Android7.0系统中调用相机时出现的FileUriExposedException错误,包括使用FileProvider替代FileUri,更新权限申请,并提供具体的代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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是很好的选择。

 <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="你的包名"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

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

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <files-path
        name="my_images"
        path="images" />
    <!--<external-path>    可被替换成<external-files-path>、<external-cache-path>、<file-path>、<cache-path>等。下面给出五个的区别:-->
    <!--<external-path>:共享外部存储卡,对应/storage/emulated/0目录,即Environment..getExternalStorageDirectory()-->
    <!--<external-files-path>:共享外部存储的文件目录,对应/storage/emulated/0/Android/data/包名/files,即Context.getExternalFilesDir()-->
    <!--<external-cache-path>:共享外部存储的缓存目录,对应/storage/emulated/0/Android/data/包名/cache,即Context.getExternalCacheDir()-->
    <!--<file-path>:共享内部文件存储目录,对应 /data/data/包名/files目录,即Context.getFilesDir()-->
    <!--<cache-path>:共享内部缓存目录,对应 /data/data/包名/cache目录,即Context.getCacheDir()-->
    <!--name:随便定义-->
    <!--path :需要临时授权访问的路径。可以为空,表示指定目录下的所有文件、文件夹都可以被共享-->
</paths>

然后就是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";

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值