我们知道, Android操作系统一直在进化. 虽然说系统是越来越安全, 可靠, 但是对于开发者而言, 开发难度是越来越大的, 需要注意的兼容性问题, 也越来越多. 就比如在Android平台上拍照或者选择照片之后裁剪图片, 原本不需要考虑权限是否授予, 存储空间的访问安全性等问题. 比如, 在Android 6.0之后, 一些危险的权限诸如相机, 电话, 短信, 定位等, 都需要开发者主动向用户申请该权限才可以使用, 不像以前那样, 在AndroidManifest.xml里面配置一下即可. 再比如, 在Android 7.0之后, FileProvider的出现, 要求开发者需要手动授予访问本应用内部File, Uri等涉及到存储空间的Intent读取的权限, 这样外部的应用(比如相机, 文件选择器, 下载管理器等)才允许访问.

最近在公司的项目中, 又遇到了要求拍照或者选择图片, 裁减后上传到服务器的需求. 所以就怀着使后来人少踩坑的美好想象, 把这部分工程中大家可能都会遇到的共同问题, 给出一个比较合理通用的解决方案.

好, 下面我们就正式开始吧!

 

1, 在AndroidManifest.xml配置文件中, 添加对相机权限的使用:

1

 

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

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中对存储空间的限制, 都已经进行了处理, 而且测试通过.

 

大家有什么问题, 可以在评论里面问我. 谢谢~