我們知道, 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中對存儲空間的限制, 都已經進行了處理, 而且測試通過.
大家有什么問題, 可以在評論里面問我. 謝謝~