Fork me on GitHub

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

 

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

posted on 2017-11-11 12:36  SilentKnight  阅读(3720)  评论(0编辑  收藏  举报