
我们知道, 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中对存储空间的限制, 都已经进行了处理, 而且测试通过.
大家有什么问题, 可以在评论里面问我. 谢谢~