如何兼容所有Android版本选择照片或拍照然后裁剪图片--基于FileProvider和动态权限的实现
我们知道, Android操作系统一直在进化. 虽然说系统是越来越安全, 可靠, 但是对于开发者而言, 开发难度是越来越大的, 需要注意的兼容性问题, 也越来越多. 就比如在Android平台上拍照或者选择照片之后裁剪图片, 原本不需要考虑权限是否授予, 存储空间的访问安全性等问题. 比如, 在Android 6.0之后, 一些危险的权限诸如相机, 电话, 短信, 定位等, 都需要开发者主动向用户申请该权限才可以使用, 不像以前那样, 在AndroidManifest.xml里面配置一下即可. 再比如, 在Android 7.0之后, FileProvider的出现, 要求开发者需要手动授予访问本应用内部File, Uri等涉及到存储空间的Intent读取的权限, 这样外部的应用(比如相机, 文件选择器, 下载管理器等)才允许访问.
最近在公司的项目中, 又遇到了要求拍照或者选择图片, 裁减后上传到服务器的需求. 所以就怀着使后来人少踩坑的美好想象, 把这部分工程中大家可能都会遇到的共同问题, 给出一个比较合理通用的解决方案.
好, 下面我们就正式开始吧!
1, 在AndroidManifest.xml配置文件中, 添加对相机权限的使用:
<uses-permission android:name="android.permission.CAMERA" />
2, 声明本应用对FileProvider使用, 在<application>里面添加元素<provider>:
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:theme="@style/QxfActionBarTheme"
tools:replace="android:icon">
//.... <provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.file_provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_path" />
</provider>
3, 在res目录下创建xml文件夹, 然后在其内容创建文件file_path.xml文件, 这里对你的文件的位置, 进行了定义:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="Download"
path="." />
</paths>
4, 创建IntentUtil.java文件, 用于获取调用相机, 选择图片, 裁减图片的Intent:
public static Intent getIntentOfTakingPhoto(@NonNull Context context, @NonNull Uri photoUri) {
Intent takingPhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
takingPhotoIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
grantIntentAccessUriPermission(context, takingPhotoIntent, photoUri);
return takingPhotoIntent;
} public static Intent getIntentOfPickingPicture() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
return intent;
} public static Intent getIntentOfCroppingImage(@NonNull Context context, @NonNull Uri imageUri) {
Intent croppingImageIntent = new Intent("com.android.camera.action.CROP");
croppingImageIntent.setDataAndType(imageUri, "image/*");
croppingImageIntent.putExtra("crop", "true");
//crop into circle image
// croppingImageIntent.putExtra("circleCrop", "true");
//The proportion of the crop box is 1:1
croppingImageIntent.putExtra("aspectX", 1);
croppingImageIntent.putExtra("aspectY", 1);
//Crop the output image size
croppingImageIntent.putExtra("outputX", 256);//输出的最终图片文件的尺寸, 单位是pixel
croppingImageIntent.putExtra("outputY", 256);
//scale selected content
croppingImageIntent.putExtra("scale", true);
//image type
croppingImageIntent.putExtra("outputFormat", "JPEG");
croppingImageIntent.putExtra("noFaceDetection", true);
//false - don't return uri | true - return uri
croppingImageIntent.putExtra("return-data", true);//
croppingImageIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
grantIntentAccessUriPermission(context, croppingImageIntent, imageUri);
return croppingImageIntent;
}
5, 在IntentUtil.java文件定义grantIntentAccessUriPermission(...)方法, 用于向访问相机, 裁减图片的Intent授予对本应用内容File和Uri读取的权限:
private static void grantIntentAccessUriPermission(@NonNull Context context, @NonNull Intent intent, @NonNull Uri uri) {
if (!Util.requireSDKInt(Build.VERSION_CODES.N)) {//in pre-N devices, manually grant uri permission.
List<ResolveInfo> resInfoList = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
} else {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
}
当然, 需要指出的是: 通过向Intent添加flag的方法, 即intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)和intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)适用于所有的Android版本, 除了Android 4.4. 在Android 4.4中需要手动的添加两个权限, 即
List<ResolveInfo> resInfoList = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
当然, 手动地添加读写权限同样适用于所有版本, 只不过通过向Intent添加flag的方法更加轻松, 更加简单而已. 如果你的应用最低版本高于Android 4.4, 则只使用添加flag的方法就行了.
6, 在需要使用相机, 选择图片, 裁减图片的Activity里面, 定义变量File(输出文件的具体位置)和Uri(包含文件的相关信息并供Intent使用):
private File avatarFile; private Uri avatarUri;
7, 在使用相机的时候, 有如下逻辑: 开启相机前, 判断一下是否已经取得了相机的权限: a, 如果用户已经授予应用访问相机的权限, 则直接去开启相机. b, 如果用户没有授予相机的权限, 则主动向用户去请求. 在接收到授予权限的结果时, 如果用户授予了相机权限, 则直接打开相机. 如果用户拒绝了, 则给予相应的提示或者操作. c, 如果用户连续向该权限申请拒绝了两次, 即, 系统已经对相机权限的申请直接进行了拒绝, 不再向用户弹出授予权限的对话框, 则直接提示用户该权限已经被系统拒绝, 需要手动开启, 并直接跳转到相应的权限管理系统页面. 当然, 动态权限仅限于Android 6.0+使用.
8, 使用相机:
@Override
public void onCameraSelected() {
if (Util.checkPermissionGranted(this, Manifest.permission.CAMERA)) {//如果已经授予相机相关权限
openCamera();
} else {//如果相机权限并未被授予, 主动向用户请求该权限
if (Util.requireSDKInt(Build.VERSION_CODES.M)) {//Android 6.0+时, 动态申请权限
requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_PERMISSION);
} else {
IntentUtil.openAppPermissionPage(this);
}
}
}
9, 对动态申请权限的结果进行处理, 具体代码如下:
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case REQUEST_PERMISSION:
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
openCamera();
} else {
// permission denied, boo! Disable the
// functionality that depends on this permission.
showPermissionDeniedDialog();
}
break;
}
}
上述代码的逻辑是: 如果用户授予了权限, 则直接打开相机, 如果没有, 则显示一个权限被拒绝的对话框.
10, 选择图片: 这个Intent不需要授予读写权限, 注意一下:
@Override
public void onGallerySelected() {
Intent pickingPictureIntent = IntentUtil.getIntentOfPickingPicture();
if (pickingPictureIntent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(pickingPictureIntent, REQUEST_PICK_PICTURE);
}
}
11, 覆盖onActivityResult(...)方法, 对拍照和选择图片的结果进行处理, 然后进行裁减.
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_TAKE_PHOTO) {
avatarUri = UriUtil.getUriFromFileProvider(avatarFile);
cropImage(avatarUri);
} else if (requestCode == REQUEST_PICK_PICTURE) {
if (data != null && data.getData() != null) {
avatarFile = FileFactory.createTempImageFile(this);
/*
* Uri(data.getData()) from Intent(data) is not provided by our own FileProvider,
* so we can't grant it the permission of read and write programmatically
* through {@link IntentUtil#grantIntentAccessUriPermission(Context, Intent, Uri)},
* So we have to copy to our own Uri with permission of write and read granted
*/
avatarUri = UriUtil.copy(this, data.getData(), avatarFile);
cropImage(avatarUri);
}
} else if (requestCode == REQUEST_CROP_IMAGE) {
mAvatar.setImageURI(avatarUri);
uploadAvatar();
}
} else {
// nothing to do here
}
}
12, 对图片进行裁减.
private void cropImage(Uri uri) {
Intent croppingImageIntent = IntentUtil.getIntentOfCroppingImage(this, uri);
if (croppingImageIntent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(croppingImageIntent, REQUEST_CROP_IMAGE);
}
}
对裁减的结果进行处理, 代码在(11)里面.
13, 最后再补充一下上述代码使用到的FileFactory.java和UriUtil.java两个文件里面的一些方法.
FileFactory.java:
public class FileFactory {
private FileFactory() {
} private static String createImageFileName() {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String imageFileName = "JPEG_" + timeStamp + "_";
return imageFileName;
} public static File createTempImageFile(@NonNull Context context) {
try {
File storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File image = File.createTempFile(
FileFactory.createImageFileName(), /* prefix */
".jpeg", /* suffix */
storageDir /* directory */
);
return image;
} catch (IOException e) {
e.printStackTrace();
}
return null;
} public static File createImageFile(@NonNull Context context, @NonNull String fileName) {
try {
if (TextUtils.isEmpty(fileName)) {
fileName = "0000";
}
File storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File image = new File(storageDir, fileName + ".jpeg");
if (!image.exists()) {
image.createNewFile();
} else {
image.delete();
image.createNewFile();
}
return image;
} catch (IOException e) {
e.printStackTrace();
}
return null;
} public static byte[] readBytesFromFile(@NonNull File file) {
try (InputStream inputStream = new FileInputStream(file)) {
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes);
return bytes;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
UriUtil.java:
public class UriUtil {
private UriUtil() {
} public static Uri getUriFromFileProvider(@NonNull File file) {
return FileProvider.getUriForFile(QxfApplication.getInstance(),
BuildConfig.APPLICATION_ID + ".file_provider",
file);
} public static Uri copy(@NonNull Context context, @NonNull Uri fromUri, @NonNull File toFile) {
try (FileChannel source = ((FileInputStream) context.getContentResolver().openInputStream(fromUri)).getChannel();
FileChannel destination = new FileOutputStream(toFile).getChannel()) {
if (source != null && destination != null) {
destination.transferFrom(source, 0, source.size());
return UriUtil.getUriFromFileProvider(toFile);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
最后再添加两个方法, requireSDKInt(int)和checkPermissionGranted(context, permission), 分别用于判断是否要求最低Android版本是多少和检测某个权限是否已经被用户授予.
public static boolean requireSDKInt(int sdkInt) {
return Build.VERSION.SDK_INT >= sdkInt;
} public static boolean checkPermissionGranted(Context context, String permission) {
return PermissionChecker.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED;
}
最后, 先拍照或者选择图片, 然后对结果进行图片的裁减, 兼容了所有Android版本的方式已经介绍完了, 无论是Android 6.0中动态权限的申请和Android 7.0中对存储空间的限制, 都已经进行了处理, 而且测试通过.
大家有什么问题, 可以在评论里面问我. 谢谢~
如何兼容所有Android版本选择照片或拍照然后裁剪图片--基于FileProvider和动态权限的实现的更多相关文章
- mono for android 获取手机照片或拍照并裁剪保存
axml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android ...
- Android 从本地图库或拍照后裁剪图片并设置头像
在QQ和微信等应用都会有设置头像,一般都是从本地图库选取或相机拍照,然后再截图自己喜欢的部分,然后设置.最后一步把截取好的图片再保存到本地,来保存头像.为了大家使用方便,我把自己完整的代码贴出来,大家 ...
- android 开发 实现一个进入相机拍照后裁剪图片或者进入相册选中裁剪图片的功能
实现思维路径: 以进入相机拍照的思维路线为例子: 1.进入app 2.判断之前是否保存头像,如果有就显示历史图像 (下面代码中在getOldAvatar();方法中执行这个逻辑) 3.点击更换图像的B ...
- Android调用相机实现拍照并裁剪图片,调用手机中的相冊图片并裁剪图片
在 Android应用中,非常多时候我们须要实现上传图片,或者直接调用手机上的拍照功能拍照处理然后直接显示并上传功能,以下将讲述调用相机拍照处理图片然后显示和调用手机相冊中的图片处理然后显示的功能,要 ...
- [转]Android使用WebView从相册/拍照中添加图片
原地址:http://blog.csdn.net/djcken/article/details/46379929 解决这个问题花了很长时间搜索了解,网上大部分使用openFileChooser但都没解 ...
- android: 从相册中选择照片
虽然调用摄像头拍照既方便又快捷,但并不是每一次我们都需要去当场拍一张照片的. 因为每个人的手机相册里应该都会存有许许多多张照片,直接从相册里选取一张现有的照 片会比打开相机拍一张照片更加常用.一个优秀 ...
- android 开发 实现多个动态权限的方法(并且兼容6.0以下的版本权限授权)
android开发权限授权因为版本的不同有不同的授权方式,6.0以下的版本使用的是在注册表中添加权限的静态授权(这种授权权限提示只会出现在app安装的时候),而6.0以上(包含6.0)就需要动态授权的 ...
- Android开发模板代码(一)——简单打开图库选择照片
首先,先贴上样本代码 //检查权限 public void checkPermission() { if (ContextCompat.checkSelfPermission(this, Manife ...
- 关于前端本地压缩图片,兼容IOS/Android/PC且自动按需加载文件之lrz.bundle.js
一.介绍说明主要特点: ①在前端压缩好要上传的图片可以更快的发送给后端,因此也特别适合在移动设备上使用. ②兼容IOS/Android,修复了IOS/Android某些版本已知的BUG. ③按需加载文 ...
随机推荐
- 17.tslib安装以及使用
1.先在网上下载 tslib-1.4.tar.gz压缩包 2.然后在ubuntu编译: tar xzf tslib-1.4.tar.gz cd tslib ./autogen.sh mkdir tmp ...
- Spring REST 与 Zuul 代理
http://www.baeldung.com/spring-rest-with-zuul-proxy 作者: Eugen Paraschiv 译者: http://oopsguy.com 1.概述 ...
- sql的存储过程使用详解--基本语法
存储过程简介 SQL语句需要先编译然后执行,而存储过程(Stored Procedure)是一组为了完成特定功能的SQL语句集,经编译后存储在数据库中,用户通过指定存储过程的名字并给定参数(如果该存储 ...
- SpringMVC框架(四)文件的上传下载,上下文路径
文件目录: SpringMVC配置文件: <?xml version="1.0" encoding="UTF-8"?> <beans xmln ...
- SSM框架整合项目 :租房管理系统
使用ssm框架整合,oracle数据库 框架: Spring SpringMVC MyBatis 导包: 1, spring 2, MyBatis 3, mybatis-spring 4, fastj ...
- cocos2dx - 控件扩展之pageview循环显示
接上一节内容:cocos2dx - shader实现任意动画的残影效果 本节主要讲一下扩展PageView控件功能 在实际游戏应用中,经常会碰到用原来的控件难以实现的功能.这时候就需要根据需求,通过选 ...
- C语言第一次实验报告————PTA实验1.2.3内容
一.PTA实验作业 题目1.温度转换 本题要求编写程序,计算华氏温度100°F对应的摄氏温度.计算公式:C=5×(F−32)/9,式中:C表示摄氏温度,F表示华氏温度,输出数据要求为整型. 1.实验代 ...
- DOS常用命令及进制转换
DOS是一种用户单任务磁盘操作系统.在DOS中,我们可以通过DOS命令来管理设备和文件,如打印文件.删除文件,复制文件,创建新的文件夹和文档并编写内容等功能同时也是JAVA编程基础的一个入门.进入DO ...
- jfinal使用配置文件注意事情
使用Popkit @Override public void configConstant(Constants me) { me.setViewType(ViewType.JSP); PropKit. ...
- 表单处理的方案与注意事项(servlet)
摘要 表单是后端程序员用的与接触最多的,我这里例举了常用处理办法,与注意事项 sevlet处理代码 package myform; import java.io.IOException; import ...