如何兼容所有Android版本選擇照片或拍照然后裁剪圖片--基於FileProvider和動態權限的實現


我們知道, 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中對存儲空間的限制, 都已經進行了處理, 而且測試通過.

 

大家有什么問題, 可以在評論里面問我. 謝謝~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM