我们知道, Android操作系统一直在进化. 虽然说系统是越来越安全, 可靠, 但是对于开发者而言, 开发难度是越来越大的, 需要注意的兼容性问题, 也越来越多. 就比如在Android平台上拍照或者选择照片之后裁剪图片, 原本不需要考虑权限是否授予, 存储空间的访问安全性等问题. 比如, 在Android 6.0之后, 一些危险的权限诸如相机, 电话, 短信, 定位等, 都需要开发者主动向用户申请该权限才可以使用, 不像以前那样, 在AndroidManifest.xml里面配置一下即可. 再比如, 在Android 7.0之后, FileProvider的出现, 要求开发者需要手动授予访问本应用内部File, Uri等涉及到存储空间的Intent读取的权限, 这样外部的应用(比如相机, 文件选择器, 下载管理器等)才允许访问.
最近在公司的项目中, 又遇到了要求拍照或者选择图片, 裁减后上传到服务器的需求. 所以就怀着使后来人少踩坑的美好想象, 把这部分工程中大家可能都会遇到的共同问题, 给出一个比较合理通用的解决方案.
好, 下面我们就正式开始吧!
1, 在AndroidManifest.xml配置文件中, 添加对相机权限的使用:
1 <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文件, 这里对你的文件的位置, 进行了定义:
1 <?xml version="1.0" encoding="utf-8"?> 2 <paths> 3 <external-path 4 name="Download" 5 path="." /> 6 </paths>
4, 创建IntentUtil.java文件, 用于获取调用相机, 选择图片, 裁减图片的Intent:
1 public static Intent getIntentOfTakingPhoto(@NonNull Context context, @NonNull Uri photoUri) { 2 Intent takingPhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 3 takingPhotoIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri); 4 grantIntentAccessUriPermission(context, takingPhotoIntent, photoUri); 5 return takingPhotoIntent; 6 } 7 8 public static Intent getIntentOfPickingPicture() { 9 Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 10 intent.setType("image/*"); 11 return intent; 12 } 13 14 public static Intent getIntentOfCroppingImage(@NonNull Context context, @NonNull Uri imageUri) { 15 Intent croppingImageIntent = new Intent("com.android.camera.action.CROP"); 16 croppingImageIntent.setDataAndType(imageUri, "image/*"); 17 croppingImageIntent.putExtra("crop", "true"); 18 //crop into circle image 19 // croppingImageIntent.putExtra("circleCrop", "true"); 20 //The proportion of the crop box is 1:1 21 croppingImageIntent.putExtra("aspectX", 1); 22 croppingImageIntent.putExtra("aspectY", 1); 23 //Crop the output image size 24 croppingImageIntent.putExtra("outputX", 256);//输出的最终图片文件的尺寸, 单位是pixel 25 croppingImageIntent.putExtra("outputY", 256); 26 //scale selected content 27 croppingImageIntent.putExtra("scale", true); 28 //image type 29 croppingImageIntent.putExtra("outputFormat", "JPEG"); 30 croppingImageIntent.putExtra("noFaceDetection", true); 31 //false - don't return uri | true - return uri 32 croppingImageIntent.putExtra("return-data", true);// 33 croppingImageIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); 34 grantIntentAccessUriPermission(context, croppingImageIntent, imageUri); 35 return croppingImageIntent; 36 }
5, 在IntentUtil.java文件定义grantIntentAccessUriPermission(...)方法, 用于向访问相机, 裁减图片的Intent授予对本应用内容File和Uri读取的权限:
1 private static void grantIntentAccessUriPermission(@NonNull Context context, @NonNull Intent intent, @NonNull Uri uri) { 2 if (!Util.requireSDKInt(Build.VERSION_CODES.N)) {//in pre-N devices, manually grant uri permission. 3 List<ResolveInfo> resInfoList = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); 4 for (ResolveInfo resolveInfo : resInfoList) { 5 String packageName = resolveInfo.activityInfo.packageName; 6 context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); 7 } 8 } else { 9 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 10 intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 11 } 12 }
当然, 需要指出的是: 通过向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中需要手动的添加两个权限, 即
1 List<ResolveInfo> resInfoList = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); 2 for (ResolveInfo resolveInfo : resInfoList) { 3 String packageName = resolveInfo.activityInfo.packageName; 4 context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); 5 }
当然, 手动地添加读写权限同样适用于所有版本, 只不过通过向Intent添加flag的方法更加轻松, 更加简单而已. 如果你的应用最低版本高于Android 4.4, 则只使用添加flag的方法就行了.
6, 在需要使用相机, 选择图片, 裁减图片的Activity里面, 定义变量File(输出文件的具体位置)和Uri(包含文件的相关信息并供Intent使用):
1 private File avatarFile; 2 private Uri avatarUri;
7, 在使用相机的时候, 有如下逻辑: 开启相机前, 判断一下是否已经取得了相机的权限: a, 如果用户已经授予应用访问相机的权限, 则直接去开启相机. b, 如果用户没有授予相机的权限, 则主动向用户去请求. 在接收到授予权限的结果时, 如果用户授予了相机权限, 则直接打开相机. 如果用户拒绝了, 则给予相应的提示或者操作. c, 如果用户连续向该权限申请拒绝了两次, 即, 系统已经对相机权限的申请直接进行了拒绝, 不再向用户弹出授予权限的对话框, 则直接提示用户该权限已经被系统拒绝, 需要手动开启, 并直接跳转到相应的权限管理系统页面. 当然, 动态权限仅限于Android 6.0+使用.
8, 使用相机:
1 @Override 2 public void onCameraSelected() { 3 if (Util.checkPermissionGranted(this, Manifest.permission.CAMERA)) {//如果已经授予相机相关权限 4 openCamera(); 5 } else {//如果相机权限并未被授予, 主动向用户请求该权限 6 if (Util.requireSDKInt(Build.VERSION_CODES.M)) {//Android 6.0+时, 动态申请权限 7 requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_PERMISSION); 8 } else { 9 IntentUtil.openAppPermissionPage(this); 10 } 11 } 12 }
9, 对动态申请权限的结果进行处理, 具体代码如下:
1 @Override 2 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 3 super.onRequestPermissionsResult(requestCode, permissions, grantResults); 4 switch (requestCode) { 5 case REQUEST_PERMISSION: 6 // If request is cancelled, the result arrays are empty. 7 if (grantResults.length > 0 8 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 9 openCamera(); 10 } else { 11 // permission denied, boo! Disable the 12 // functionality that depends on this permission. 13 showPermissionDeniedDialog(); 14 } 15 break; 16 } 17 }
上述代码的逻辑是: 如果用户授予了权限, 则直接打开相机, 如果没有, 则显示一个权限被拒绝的对话框.
10, 选择图片: 这个Intent不需要授予读写权限, 注意一下:
1 @Override 2 public void onGallerySelected() { 3 Intent pickingPictureIntent = IntentUtil.getIntentOfPickingPicture(); 4 if (pickingPictureIntent.resolveActivity(getPackageManager()) != null) { 5 startActivityForResult(pickingPictureIntent, REQUEST_PICK_PICTURE); 6 } 7 }
11, 覆盖onActivityResult(...)方法, 对拍照和选择图片的结果进行处理, 然后进行裁减.
1 @Override 2 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 3 super.onActivityResult(requestCode, resultCode, data); 4 if (resultCode == Activity.RESULT_OK) { 5 if (requestCode == REQUEST_TAKE_PHOTO) { 6 avatarUri = UriUtil.getUriFromFileProvider(avatarFile); 7 cropImage(avatarUri); 8 } else if (requestCode == REQUEST_PICK_PICTURE) { 9 if (data != null && data.getData() != null) { 10 avatarFile = FileFactory.createTempImageFile(this); 11 /* 12 * Uri(data.getData()) from Intent(data) is not provided by our own FileProvider, 13 * so we can't grant it the permission of read and write programmatically 14 * through {@link IntentUtil#grantIntentAccessUriPermission(Context, Intent, Uri)}, 15 * So we have to copy to our own Uri with permission of write and read granted 16 */ 17 avatarUri = UriUtil.copy(this, data.getData(), avatarFile); 18 cropImage(avatarUri); 19 } 20 } else if (requestCode == REQUEST_CROP_IMAGE) { 21 mAvatar.setImageURI(avatarUri); 22 uploadAvatar(); 23 } 24 } else { 25 // nothing to do here 26 } 27 }
12, 对图片进行裁减.
1 private void cropImage(Uri uri) { 2 Intent croppingImageIntent = IntentUtil.getIntentOfCroppingImage(this, uri); 3 if (croppingImageIntent.resolveActivity(getPackageManager()) != null) { 4 startActivityForResult(croppingImageIntent, REQUEST_CROP_IMAGE); 5 } 6 }
对裁减的结果进行处理, 代码在(11)里面.
13, 最后再补充一下上述代码使用到的FileFactory.java和UriUtil.java两个文件里面的一些方法.
FileFactory.java:
1 public class FileFactory { 2 private FileFactory() { 3 } 4 5 private static String createImageFileName() { 6 String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); 7 String imageFileName = "JPEG_" + timeStamp + "_"; 8 return imageFileName; 9 } 10 11 public static File createTempImageFile(@NonNull Context context) { 12 try { 13 File storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES); 14 File image = File.createTempFile( 15 FileFactory.createImageFileName(), /* prefix */ 16 ".jpeg", /* suffix */ 17 storageDir /* directory */ 18 ); 19 return image; 20 } catch (IOException e) { 21 e.printStackTrace(); 22 } 23 return null; 24 } 25 26 public static File createImageFile(@NonNull Context context, @NonNull String fileName) { 27 try { 28 if (TextUtils.isEmpty(fileName)) { 29 fileName = "0000"; 30 } 31 File storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES); 32 File image = new File(storageDir, fileName + ".jpeg"); 33 if (!image.exists()) { 34 image.createNewFile(); 35 } else { 36 image.delete(); 37 image.createNewFile(); 38 } 39 return image; 40 } catch (IOException e) { 41 e.printStackTrace(); 42 } 43 return null; 44 } 45 46 public static byte[] readBytesFromFile(@NonNull File file) { 47 try (InputStream inputStream = new FileInputStream(file)) { 48 byte[] bytes = new byte[inputStream.available()]; 49 inputStream.read(bytes); 50 return bytes; 51 } catch (FileNotFoundException e) { 52 e.printStackTrace(); 53 } catch (IOException e) { 54 e.printStackTrace(); 55 } 56 return null; 57 } 58 }
UriUtil.java:
1 public class UriUtil { 2 private UriUtil() { 3 } 4 5 public static Uri getUriFromFileProvider(@NonNull File file) { 6 return FileProvider.getUriForFile(QxfApplication.getInstance(), 7 BuildConfig.APPLICATION_ID + ".file_provider", 8 file); 9 } 10 11 public static Uri copy(@NonNull Context context, @NonNull Uri fromUri, @NonNull File toFile) { 12 try (FileChannel source = ((FileInputStream) context.getContentResolver().openInputStream(fromUri)).getChannel(); 13 FileChannel destination = new FileOutputStream(toFile).getChannel()) { 14 if (source != null && destination != null) { 15 destination.transferFrom(source, 0, source.size()); 16 return UriUtil.getUriFromFileProvider(toFile); 17 } 18 } catch (FileNotFoundException e) { 19 e.printStackTrace(); 20 } catch (IOException e) { 21 e.printStackTrace(); 22 } 23 return null; 24 } 25 }
最后再添加两个方法, requireSDKInt(int)和checkPermissionGranted(context, permission), 分别用于判断是否要求最低Android版本是多少和检测某个权限是否已经被用户授予.
1 public static boolean requireSDKInt(int sdkInt) { 2 return Build.VERSION.SDK_INT >= sdkInt; 3 } 4 5 public static boolean checkPermissionGranted(Context context, String permission) { 6 return PermissionChecker.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED; 7 }
最后, 先拍照或者选择图片, 然后对结果进行图片的裁减, 兼容了所有Android版本的方式已经介绍完了, 无论是Android 6.0中动态权限的申请和Android 7.0中对存储空间的限制, 都已经进行了处理, 而且测试通过.
大家有什么问题, 可以在评论里面问我. 谢谢~