Android 7.0下拍照和裁剪图片
最近,公司的APP集中爆发了头像上传中拍照或照片裁剪的bug,让我才意识到及时学习Android新特性是多么重要,一个过去式的APP是无法一直满足日益变化的新需求,毕竟即使当时编码的开发者再牛逼,他也不可能预料到未来的技术变更,所以Android APP 兼容适配不可避免。
通过阅读本文,就拍照和裁剪图片等问题,你可以以最小时间代价完成兼容适配7.0设备。
代码片段(适配前)
在页面中有头像编辑部分,点击则会弹出自定义的SelectPhotoDialog来进行图片选择,如下图所示:
(1)快速选择
通过监听回调获取到图片在设备中的路径,如:
/storage/emulated/0/DCIM/Camera/IMG_20150817_214317.jpg
,
然后通过以下代码进行处理:
Uri uri = Uri.fromFile(new File(path));
startActivityForResult(CropUtils.invokeSystemCrop(uri), CROP_RESULT);
CropUtils内部封装了调用系统裁剪的相关方法,其中主要方法为invokeSystemCrop()
,顾名思义是指调用系统裁剪功能的意思。
public class CropUtils {
private static String mFile;
public static String getPath() {
//resize image to thumb
if (mFile == null) {
mFile = Environment.getExternalStorageDirectory() + "/" + SettingManager.SD_CARD_DIR_THUMB + "/" + "temp.jpg";
}
return mFile;
}
/**
* 调用系统照片的裁剪功能
*/
public static Intent invokeSystemCrop(Uri uri) {
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, "image/*");
// crop为true是设置在开启的intent中设置显示的view可以剪裁
intent.putExtra("crop", "true");
intent.putExtra("scale", true);
// aspectX aspectY 是宽高的比例
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
// outputX,outputY 是剪裁图片的宽高
intent.putExtra("outputX", 300);
intent.putExtra("outputY", 300);
intent.putExtra("return-data", false);
intent.putExtra("noFaceDetection", true);
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(getPath())));
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
return intent;
}
}
最终的头像上传在:
Activity.onActivityResult(int requestCode, int resultCode, Intent data)
中被集中处理了,因为不管上哪种方式,最终的目的就是上传选择裁剪后的照片。
onActivityResult()部分代码如下:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
//都会最终执行这里的方法
if(requestCode == CROP_RESULT && resultCode ==RESULT_OK){
if (data != null){
uploadAvatar(CropUtils.getPath());
}
}
//...
if (requestCode == MyInfoAdapter.REQUEST_CODE_UPDATE_LOCAL_DATA && resultCode == RESULT_OK) {
initData();
setupRecyclerView();
}
super.onActivityResult(requestCode, resultCode, data);
}
上述代码,在Android 7.0之前完全没有问题,但在7.0上设备会出现异常FileUriExposedException。
异常原因探究
在Android7.0中为了提高私有文件的安全性,面向 Android N 或更高版本的应用私有目录将被限制访问。此外,Android 7.0 对系统进行了很多的优化:例如文件访问权限,省电,网络,后台等等,其中最突出的就是应用外的Uri访问。
禁止应用间共享文件
在Android7.0系统上,Android 框架强制执行了 StrictMode API 政策禁止向你的应用外公开 file:// URI。 如果一项包含文件 file:// URI类型 的 Intent 离开你的应用,应用失败,并出现 FileUriExposedException 异常,如调用系统相机拍照,或裁切照片。
应对策略:若要在应用间共享文件,可以发送 content:// URI类型的Uri,并授予 URI 临时访问权限。 进行此授权的最简单方式是使用 FileProvider类。
下面来介绍下如何使用FileProvider解决Uri访问抛出安全异常的问题:
使用FileProvider
使用FileProvider的大致步骤如下:
(1)在AndroidManifest.xml中注册provider
<!--解决 Android N 上报错:android.os.FileUriExposedException-->
<provider
android:authorities="com.zbiti.yuntu.qyj.fileprovider"
android:name="android.support.v4.content.FileProvider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
注意:
- android:exported:要求必须为false,为true则会报安全异常;
- android:grantUriPermissions:true,表示授予 URI 临时访问权限;
- android:authorities这个属性的值,建议写包名+fileprovider,当然也可以起别的字符串,但是在设备中不能出现2个及以上的APP使用到同一个authorities属性值,因为无法共存。
(2)指定共享的目录
在app/src/创建一个xml的目录,如下:
向该目录中添加一个provider_paths.xml文件(命名可以自由定义,但是需上下文一致),添加如下内容到provider_paths.xml中。
<?xml version="1.0" encoding="utf-8"?>
<paths>
<root-path
name="external_files"
path=""/>
</paths>
- 代表的根目录:Context.getFilesDir()
- 代表的根目录:Environment.getExternalStorageDirectory()
- 代表的根目录:getCacheDir()
上述代码中path=“ ”,是有特殊意义的,它代码根目录,也就是说你可以向其它的应用共享根目录及其子目录下任何一个文件了,如果你将path设为path=“pictures”,那么它代表着根目录下的pictures目录(eg:/storage/emulated/0/pictures),如果你向其它应用分享pictures目录范围之外的文件是不行的。
(3)使用FileProvider加密Uri
在代码中,通过增加判断系统版本号来执行生成不同的Uri:
//解决Android 7.0之后的Uri安全问题
private Uri getUriForFile(Context context, File file) {
if (context == null || file == null) {
throw new NullPointerException();
}
Uri uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
uri = FileProvider
.getUriForFile(context.getApplicationContext(), "com.zbiti.yuntu.qyj.fileprovider", file);
} else {
uri = Uri.fromFile(file);
}
return uri;
}
代码片段(适配后)
通过上面的分析,我们已经知道如何就拍照和调用系统裁剪图片等适配7.0设备,以下是相关代码对比:
调用系统拍照功能
适配前拍照的点击事件:
mFromCamera.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent i = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
fileUri = getOutputMediaFileUri();
imagePath = fileUri.getPath();
i.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
((Activity) context).startActivityForResult(i, FROM_CAMERA);
dlg.dismiss();
}
});
private Uri getOutputMediaFileUri() {
return Uri.fromFile(getOutputMediaFile());
}
适配后拍照的点击事件:
mFromCamera.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent i = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //添加这一句表示对目标应用临时授权该Uri所代表的文件
fileUri = getOutputMediaFileUri();
imagePath = fileUri.getPath().substring(15);
i.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
((Activity) context).startActivityForResult(i, FROM_CAMERA);
dlg.dismiss();
}
});
private Uri getOutputMediaFileUri() {
// return Uri.fromFile(getOutputMediaFile());
return getUriForFile(this.context,getOutputMediaFile());
}
上述代码中主要有两处改变:将之前Uri的scheme类型为file的Uri改成了有FileProvider创建一个content类型的Uri;添加了intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);来对目标应用临时授权该Uri所代表的拍照后的图片。
调用系统裁剪功能
前面介绍过,调用系统图片裁剪的方法在CropUtils,我们来看看这个类又修改了那些地方:
public class CropUtils {
private static String mFile;
public static String getPath() {
//resize image to thumb
if (mFile == null) {
mFile = Environment.getExternalStorageDirectory() + "/" + SettingManager.SD_CARD_DIR_THUMB + "/" + "temp.jpg";
}
return mFile;
}
/**
* 调用系统照片的裁剪功能,修改编辑头像的选择模式(适配Android7.0)
*/
public static Intent invokeSystemCrop(Uri uri) {
Intent intent = new Intent("com.android.camera.action.CROP");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//添加这一句表示对目标应用临时授权该Uri所代表的文件
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
intent.setDataAndType(uri, "image/*");
// crop为true是设置在开启的intent中设置显示的view可以剪裁
intent.putExtra("crop", "true");
intent.putExtra("scale", true);
// aspectX aspectY 是宽高的比例
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
// outputX,outputY 是剪裁图片的宽高
intent.putExtra("outputX", 300);
intent.putExtra("outputY", 300);
intent.putExtra("return-data", false);
intent.putExtra("noFaceDetection", true);
File out = new File(getPath());
if (!out.getParentFile().exists()) {
out.getParentFile().mkdirs();
}
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(out));
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
return intent;
}
}
上述代码中主要有两处改变:添加了intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);来对目标应用临时授权该Uri所代表的图片;另外,优化了裁剪后图片的输出目录的获取,这里如果不去判断指定目录是否存在,就会出现回调方法onActivityResult()值resultCode总是为0的问题。
【参考博客】
1.Android7.0适配教程,心得
2.适配android7.0:获取文件的Uri
3.Android6.0机型上调用系统相机拍照返回的resultCode值始终等于0的问题
4.Android 7.0 适配相机及裁剪图片