我们知道, 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和动态权限的实现的更多相关文章

  1. mono for android 获取手机照片或拍照并裁剪保存

    axml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android ...

  2. Android 从本地图库或拍照后裁剪图片并设置头像

    在QQ和微信等应用都会有设置头像,一般都是从本地图库选取或相机拍照,然后再截图自己喜欢的部分,然后设置.最后一步把截取好的图片再保存到本地,来保存头像.为了大家使用方便,我把自己完整的代码贴出来,大家 ...

  3. android 开发 实现一个进入相机拍照后裁剪图片或者进入相册选中裁剪图片的功能

    实现思维路径: 以进入相机拍照的思维路线为例子: 1.进入app 2.判断之前是否保存头像,如果有就显示历史图像 (下面代码中在getOldAvatar();方法中执行这个逻辑) 3.点击更换图像的B ...

  4. Android调用相机实现拍照并裁剪图片,调用手机中的相冊图片并裁剪图片

    在 Android应用中,非常多时候我们须要实现上传图片,或者直接调用手机上的拍照功能拍照处理然后直接显示并上传功能,以下将讲述调用相机拍照处理图片然后显示和调用手机相冊中的图片处理然后显示的功能,要 ...

  5. [转]Android使用WebView从相册/拍照中添加图片

    原地址:http://blog.csdn.net/djcken/article/details/46379929 解决这个问题花了很长时间搜索了解,网上大部分使用openFileChooser但都没解 ...

  6. android: 从相册中选择照片

    虽然调用摄像头拍照既方便又快捷,但并不是每一次我们都需要去当场拍一张照片的. 因为每个人的手机相册里应该都会存有许许多多张照片,直接从相册里选取一张现有的照 片会比打开相机拍一张照片更加常用.一个优秀 ...

  7. android 开发 实现多个动态权限的方法(并且兼容6.0以下的版本权限授权)

    android开发权限授权因为版本的不同有不同的授权方式,6.0以下的版本使用的是在注册表中添加权限的静态授权(这种授权权限提示只会出现在app安装的时候),而6.0以上(包含6.0)就需要动态授权的 ...

  8. Android开发模板代码(一)——简单打开图库选择照片

    首先,先贴上样本代码 //检查权限 public void checkPermission() { if (ContextCompat.checkSelfPermission(this, Manife ...

  9. 关于前端本地压缩图片,兼容IOS/Android/PC且自动按需加载文件之lrz.bundle.js

    一.介绍说明主要特点: ①在前端压缩好要上传的图片可以更快的发送给后端,因此也特别适合在移动设备上使用. ②兼容IOS/Android,修复了IOS/Android某些版本已知的BUG. ③按需加载文 ...

随机推荐

  1. JavaWeb(一)Servlet中的ServletConfig与ServletContext

    前言 前面我介绍了一下什么是servlet,它的生命周期,执行过程和它的原理.这里我们做一个简单的回顾! 什么是Servlet? servlet 是运行在 Web 服务器中的小型 Java 程序(即: ...

  2. 2017-2018 ACM-ICPC, NEERC, Southern Subregional Contest, qualification stage (Online Mirror, ACM-ICPC Rules, Teams Preferred)

    题目链接:http://codeforces.com/problemset/problem/847/I I. Noise Level time limit per test 5 seconds mem ...

  3. 新版MySql 5.6.20,安装后无法登陆的解决办法

    1.按照提示安装好mysql 2.运行cmd 进入mysql的安装目录,我的安装目录C:\Program Files\MySQL\MySQL Server 5.6\bin 输入 cd C:\Progr ...

  4. 从头编写 asp.net core 2.0 web api 基础框架 (5) EF CRUD

    第1部分:http://www.cnblogs.com/cgzl/p/7637250.html 第2部分:http://www.cnblogs.com/cgzl/p/7640077.html 第3部分 ...

  5. checkValidity-表达验证方法。

    调用该方法,可以显示对表单元素进行有效验证,返回值是boolean. 代码如下: <!DOCTYPE html> <html> <head> <meta ch ...

  6. MV45AFZZ 销售订单的增强

    ***INCLUDE MV45AFZZ . *---------------------------------------------------------------------* * FORM ...

  7. Linux 进程状态 概念 Process State Definition

    From : http://www.linfo.org/process_state.html 进程状态是指在进程描述符中状态位的值. 进程,也可被称为任务,是指一个程序运行的实例. 一个进程描述符是一 ...

  8. stringstream缓存正确清除方法

    当需要清空stringsteam缓存的时候,到底是.str("")呢还是.clear(); 实际上,我认为,保守起见,两者都需要 clear() 是清除 state flag st ...

  9. C# MessageBox.Show每隔3秒自动关闭

    using System;using System.Collections.Generic;using System.ComponentModel;using System.Data;using Sy ...

  10. Vuex state 状态浅解

    对于Vuex中的state里面的理解总是有些欠缺,机制似乎理解了.但是还有很多的不足,在这就先浅谈下自己的理解. vuex 机制中,定义了全局Store,在各个vue组件面的this.$store指向 ...