PictureSelector 至從2016年12月底提交第一個版本以來時至今日總共也已提交80多個版本,經過不斷的改良和適配現在也相對趨於穩定了並在Github上收穫超7900個star,再這也要感謝各位同學的鼓勵和提供的一些意見。
由於Google Android Q正式版預計在2019年8月份前後推出,所以針對AndroidQ的適配已經迫在眉睫了,其中Android Q一項比較重要的變更就是在外部存儲設備中爲每個應用提供了一個“隔離存儲沙盒”。任何其他應用都無法直接訪問您應用的沙盒文件,由於文件是您應用的私有文件,因此您不再需要任何權限即可在外部存儲設備中訪問和保存自己的文件。
由於PictureSelector是一款圖片選擇器所以自然只對存儲這一塊需要適配,如您需要了解更多Q相關的特性請參考 官網地址
先介紹一下沙盒機制下的三種存儲方式
1、context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);// 圖片存儲路徑
2、context.getExternalFilesDir(Environment.DIRECTORY_MOVIES);// 視頻存儲路徑
3、context.getExternalFilesDir(Environment.DIRECTORY_DIRECTORY_MUSIC);//音頻存儲路徑
作爲一款圖片選擇器,沙盒機制這個操作對PictureSelector肯定是影響比較大,爲了驗證有哪些問題方便適配我們把targetSdkVersion改成>=29然後在Q版本機型上運行後發現果然PictureSelector基本是處於不能使用的尷尬局面,然後也收到了很多小夥伴發來的郵件和信息報bug那麼廢話就不多說了,下面就講一下PictureSelector 2.0是如何適配Android Q;問題一、圖片加載失敗無法正常顯示圖片 因爲MediaStore.MediaColumns.DATA 字段在Q版本被標記已過時,原因上面有提到過任何其他應用都無法直接訪問您應用的沙盒文件;所以在Q上此字段返回的類似/storage/emulated/0/DCIM/Camera/IMG_20200105_19064545.jpg將無權限訪問,即使你用Glide這些優秀的第三方框架也顯示失敗,你在Logcat過濾一下Glide日誌就會發現會報如下異常:
```java.io.FileNotFoundException: open failed: EACCES (Permission
denied) ```意思是找不到文件&無權限訪問
這個解決方案也很簡單我們只需要通過``` MediaStore.Files.FileColumns._ID```拿到資源的Id進行拼接轉成Uri的方式content://media/external/file/20246就能正常顯示圖片,代碼如下:
long id = data.getLong(data.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID));
String path = isAndroidQ ? getRealPathAndroid_Q(id) : data.getString(data.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA));
/**
* 適配Android Q
*
* @param id
* @return
*/
private String getRealPathAndroid_Q(long id) {
return QUERY_URI.buildUpon().appendPath(ValueOf.toString(id)).build().toString();
}
問題二、Q版本中調用系統拍照或錄視頻如何保存至系統相冊?
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
File cameraFile = new File("存儲路徑");
Uri imageUri = PictureFileUtils.parUri(this, cameraFile);
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
在非Android Q版本下我們調用系統拍照按上面方式就可以了,但在Q版本上這個```File cameraFile = new File("存儲路徑");```就有講究了,如果你直接傳公共目錄地址肯定是不行的因爲你沒權限訪問,你只能傳應用內地址```ctx.getExternalFilesDir(Environment.DIRECTORY_PICTURES);```把圖片存儲在應用沙盒內,但我們是需要存儲到系統相冊中所以顯然這也不是我們所期望的,所以我們只能通過Uri來處理,這個Uri就不能通過parUri(this, cameraFile);來獲得了,可以通過以下方式創建拍照或錄視頻Uri;
/**
* 創建一條圖片地址uri,用於保存拍照後的照片
*
* @param context
* @return 圖片的uri
*/
@Nullable
public static Uri createImageUri(final Context context) {
final Uri[] imageFilePath = {null};
String status = Environment.getExternalStorageState();
String time = ValueOf.toString(System.currentTimeMillis());
// ContentValues是我們希望這條記錄被創建時包含的數據信息
ContentValues values = new ContentValues(3);
values.put(MediaStore.Images.Media.DISPLAY_NAME, DateUtils.getCreateFileName("IMG_"));
values.put(MediaStore.Images.Media.DATE_TAKEN, time);
values.put(MediaStore.Images.Media.MIME_TYPE, PictureMimeType.MIME_TYPE_IMAGE);
// 判斷是否有SD卡,優先使用SD卡存儲,當沒有SD卡時使用手機存儲
if (status.equals(Environment.MEDIA_MOUNTED)) {
values.put(MediaStore.Images.Media.RELATIVE_PATH, PictureMimeType.DCIM);
imageFilePath[0] = context.getContentResolver()
.insert(MediaStore.Images.Media.getContentUri("external"), values);
} else {
imageFilePath[0] = context.getContentResolver()
.insert(MediaStore.Images.Media.getContentUri("internal"), values);
}
return imageFilePath[0];
}
/**
* 創建一條視頻地址uri,用於保存錄制的視頻
*
* @param context
* @return 視頻的uri
*/
@Nullable
public static Uri createVideoUri(final Context context) {
final Uri[] imageFilePath = {null};
String status = Environment.getExternalStorageState();
String time = ValueOf.toString(System.currentTimeMillis());
// ContentValues是我們希望這條記錄被創建時包含的數據信息
ContentValues values = new ContentValues(3);
values.put(MediaStore.Video.Media.DISPLAY_NAME, DateUtils.getCreateFileName("VID_"));
values.put(MediaStore.Video.Media.DATE_TAKEN, time);
values.put(MediaStore.Video.Media.MIME_TYPE, PictureMimeType.MIME_TYPE_VIDEO);
// 判斷是否有SD卡,優先使用SD卡存儲,當沒有SD卡時使用手機存儲
if (status.equals(Environment.MEDIA_MOUNTED)) {
values.put(MediaStore.Video.Media.RELATIVE_PATH, PictureMimeType.DCIM);
imageFilePath[0] = context.getContentResolver()
.insert(MediaStore.Video.Media.getContentUri("external"), values);
} else {
imageFilePath[0] = context.getContentResolver()
.insert(MediaStore.Video.Media.getContentUri("internal"), values);
}
return imageFilePath[0];
}
將生成的Uri傳入 cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);這樣在Q手機上拍照或錄的視頻則可以保存到/storage/emulated/0/DCIM/Camera/目錄下了。
問題三、使用PictureSelector 2.0在Q機型上傳圖片失敗?
java.io.FileNotFoundException: open failed: EACCES (Permission denied) 無權限訪問或文件找不到,這同樣也是因爲我們在選擇公共目錄下的一些圖片資源上傳時你並沒有訪問權限所以也自然不能使用它進行上傳操作,其實我們在上面講到MediaStore.MediaColumns.DATA```已過時問題時你點開源碼就會發現其實已經告訴了我們替代方案了
/**
*
* @deprecated Apps may not have filesystem permissions to
* directly
* access this path. Instead of trying to open this path
* directly, apps should use
* {@link ContentResolver#openFileDescriptor(Uri, String)}
* to gain access.
*/
通過ContentResolver#openFileDescriptor(Uri, String);方法獲得訪問權限,PictureSelector的解決方法是
通過getContentResolver().openFileDescriptor(uri,"r");獲得文件的訪問權限然後將其拷貝到自己應用沙盒中然後再進行上傳操作,代碼如下:
ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(uri,"r");
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
FileInputStream inputStream = new FileInputStream(fileDescriptor);
boolean copyFileSuccess = copyFile(inputStream, outFile);
if (copyFileSuccess)
{
Log.i(TAG,"Copy File Success");
}
/**
* Copy File
*
* @param fileInputStream
* @param outFile
* @return
* @throws IOException
*/
public static boolean copyFile(FileInputStream fileInputStream, File outFile) throws
IOException {
if (fileInputStream == null) {
return false;
}
FileChannel inputChannel = null;
FileChannel outputChannel = null;
try {
inputChannel = fileInputStream.getChannel();
outputChannel = new FileOutputStream(outFile).getChannel();
inputChannel.transferTo(0, inputChannel.size(), outputChannel);
inputChannel.close();
return true;
} catch (Exception e) {
return false;
} finally {
if (inputChannel != null) inputChannel.close();
if (outputChannel != null) outputChannel.close();
}
}
以上就是PictureSelector2.0關於Android Q的適配一些方案,如果您有更好方案也歡迎提出參考;
另外:
有一些小夥伴在Q機型上使用PictureSelector2.0時總是問我爲什麼需要一個拷貝文件至應用沙盒內的操
作?說自己通過Uri轉Path也能拿到真實路徑進行上傳,在這裏我想說的是Uri轉Path其實早在AndroidQ未出
來之前就一直可以的,這也僅僅是路徑之間的相互轉換而已並沒有訪問的權限,如果有,那麼只能說明你應用
targetSdkVersion沒有改成>=29或沒有開啓沙盒機制而已。