1. 程式人生 > 其它 >android 8.0 調系統拍照_Android 系統拍照及開啟系統相簿 完美適配 Android 10、9、8、7、6、5、4...

android 8.0 調系統拍照_Android 系統拍照及開啟系統相簿 完美適配 Android 10、9、8、7、6、5、4...

技術標籤:android 8.0 調系統拍照android 從手機獲取相簿android 圖片拼接android 開啟系統相簿android 相簿許可權android 獲取版本號

有一個小夥伴說,現在 Android 11都馬上出來了,快幫我適配一下 Android 10 的一些功能,然後忙了兩天之後,匆匆忙忙的就趕來了。

適配背景

640?wx_fmt=png

1、Android 4.4及以上裝置

Android 4.4(簡稱 4.4)及以上裝置 的圖片檔案路徑與4.4以下裝置的路徑是完全不一樣的,需要開發者自行拼接。

2、Android 6.0 及以上裝置

Android 6.0(簡稱 6.0 )及以上裝置 在呼叫一些功能的時候,開發者系統申請許可權,部分許可權屬於危險許可權,涉及到使用者隱私相關問題,現在應用市場都強制要求進行相關適配了,如果開發者還未適配,那麼只能證明您的軟體,使用者量好像很低了,建議開發者進行跳槽吧。

3、Android 8.0 及以上裝置

Android 8.0(簡稱8.0)及以上裝置 在呼叫系統相機進行拍照的時候 需要在 `AndroidManifest.xml` 進行配置 ` provider` 將照片儲存的位置 進行共享,否則使用者拍攝之後的照片,就只會靜靜的躺在相簿裡,對了,配置,可能還會造成崩潰。

4、Android 10及以上裝置

在做檔案的操作時都會申請儲存空間的讀寫許可權。但是這些許可權完全被濫用,造成的問題就是手機的儲存空間中充斥著大量不明作用的檔案,並且應用解除安裝後它也沒有刪除掉。為了解決這個問題,Android 10 中引入了Scoped Storage 的概念,通過新增外部儲存訪問限制來實現更好的檔案管理。

這裡有一個問題是,什麼是外部儲存,什麼是內部儲存 ?

1)、內部儲存:

/data 目錄。一般我們使用 getFilesDir() 或 getCacheDir() 方法獲取本應用的內部儲存路徑,讀寫該路徑下的檔案不需要申請儲存空間讀寫許可權,且解除安裝應用時會自動刪除。

2)、外部儲存:

/storage 或 /mnt 目錄。一般我們使用 getExternalStorageDirectory() 方法獲取的路徑來存取檔案。

故在 Android 10 上即便擁有了儲存空間的讀寫許可權,也無法保證可以正常的進行檔案的讀寫操作。

二、規避 Android 10 適配

640?wx_fmt=png

在AndroidManifest.xml中新增 android:requestLegacyExternalStorage="true" 來請求使用舊的儲存模式進行臨時規避 Android 10 適配。但是存在缺陷,當 Android 11 正式上線之後,接踵而至的就是各種 bug 日誌 反饋了,所以,還是踏踏實實做人,老老實實做事吧。

三、 適配

640?wx_fmt=png

(一)、系統相簿回撥 圖片 地址適配

1、適配 Android 系統版本號 >=4.4 &&<=10.0 版本

/**
* 適配Android 4.4及以上,根據uri獲取圖片的絕對路徑
*
* @paramcontext 上下文物件
* @paramuri 圖片的Uri
* @return如果Uri對應的圖片存在, 那麼返回該圖片的絕對路徑, 否則返回null
*/
@SuppressLint("NewApi")
privatestaticString getRealPathFromUriAboveApiAndroidK(Context context, Uri uri){
String filePath = null;
if(DocumentsContract.isDocumentUri(context, uri)) {
// 如果是document型別的 uri, 則通過document id來進行處理
String documentId = DocumentsContract.getDocumentId(uri);
if(isMediaDocument(uri)) {
// 使用':'分割
String id = documentId.split(":")[1];
String selection = MediaStore.Images.Media._ID + "=?";
String[] selectionArgs = {id};
filePath = getDataColumn(context, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection, selectionArgs);
} elseif(isDownloadsDocument(uri)) {
Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(documentId));
filePath = getDataColumn(context, contentUri, null, null);
}
} elseif("content".equalsIgnoreCase(uri.getScheme())) {
// 如果是 content 型別的 Uri
filePath = getDataColumn(context, uri, null, null);
} elseif("file".equals(uri.getScheme())) {
// 如果是 file 型別的 Uri,直接獲取圖片對應的路徑
filePath = uri.getPath();
}
returnfilePath;
}

/**
* @paramuri the Uri to check
* @returnWhether the Uri authority is MediaProvider
*/
privatestaticbooleanisMediaDocument(Uri uri){
return"com.android.providers.media.documents".equals(uri.getAuthority());
}

/**
* @paramuri the Uri to check
* @returnWhether the Uri authority is DownloadsProvider
*/
privatestaticbooleanisDownloadsDocument(Uri uri){
return"com.android.providers.downloads.documents".equals(uri.getAuthority());
}

這裡相比於 4.4 系統以下裝置的程式碼,已經明顯的不一樣了相對複雜了許多,需要自己主動去構建 路徑, uri等,對使用者友好了,但是對於開發者,也就那樣吧。

2、適配 Android 系統版本號 >= 10.0版本

使用系統相簿進行圖片選擇時 。無法直接使用File,而應使用Uri。否則報錯如下:

W/Glide: Loadfailedfor/storage/emulated/0/Pictures/albumCameraImg/1591.jpg withsize[549x549]

java.io.FileNotFoundException: openfailed: EACCES (Permission denied)

Uri 的獲取方式使用到了`MediaStore`

獲取圖片程式碼為:

/**
* 適配 Android 10以上相簿選取照片操作
*
* @paramcontext 上下文
* @paramuri 圖片uri
* @return圖片地址
*/
privatestaticString getRealPathFromUriAboveApiAndroidQ(Context context, Uri uri){
Cursor cursor = null;
String path = getRealPathFromUriAboveApiAndroidK(context, uri);
try{
cursor = context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
newString[]{MediaStore.Images.Media._ID}, MediaStore.Images.Media.DATA + "=? ",
newString[]{path}, null);
if(cursor != null&& cursor.moveToFirst()) {
intid = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
Uri baseUri = Uri.parse("content://media/external/images/media");
returnString.valueOf(Uri.withAppendedPath(baseUri, ""+ id));
} else{
// 如果圖片不在手機的共享圖片資料庫,就先把它插入。
if(newFile(path).exists()) {
ContentValues values = newContentValues();
values.put(MediaStore.Images.Media.DATA, path);
returnString.valueOf(context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values));
} else{
returnnull;
}
}
} catch(Exception e) {
if(cursor != null)
cursor.close();
}
returnnull;
}

/**
* 適配Android 4.4以下(不包括api19),根據uri獲取圖片的絕對路徑
*
* @paramcontext 上下文物件
* @paramuri 圖片的Uri
* @return如果Uri對應的圖片存在, 那麼返回該圖片的絕對路徑, 否則返回null
*/
privatestaticString getRealPathFromUriBelowApiAndroidK(Context context, Uri uri){
returngetDataColumn(context, uri, null, null);
}

/**
* 適配Android 4.4及以上,根據uri獲取圖片的絕對路徑
*
* @paramcontext 上下文物件
* @paramuri 圖片的Uri
* @return如果Uri對應的圖片存在, 那麼返回該圖片的絕對路徑, 否則返回null
*/
@SuppressLint("NewApi")
privatestaticString getRealPathFromUriAboveApiAndroidK(Context context, Uri uri){
String filePath = null;
if(DocumentsContract.isDocumentUri(context, uri)) {
// 如果是document型別的 uri, 則通過document id來進行處理
String documentId = DocumentsContract.getDocumentId(uri);
if(isMediaDocument(uri)) {
// 使用':'分割
String id = documentId.split(":")[1];
String selection = MediaStore.Images.Media._ID + "=?";
String[] selectionArgs = {id};
filePath = getDataColumn(context, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection, selectionArgs);
} elseif(isDownloadsDocument(uri)) {
Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(documentId));
filePath = getDataColumn(context, contentUri, null, null);
}
} elseif("content".equalsIgnoreCase(uri.getScheme())) {
// 如果是 content 型別的 Uri
filePath = getDataColumn(context, uri, null, null);
} elseif("file".equals(uri.getScheme())) {
// 如果是 file 型別的 Uri,直接獲取圖片對應的路徑
filePath = uri.getPath();
}
returnfilePath;
}

/**
* @paramuri the Uri to check
* @returnWhether the Uri authority is MediaProvider
*/
privatestaticbooleanisMediaDocument(Uri uri){
return"com.android.providers.media.documents".equals(uri.getAuthority());
}

/**
* @paramuri the Uri to check
* @returnWhether the Uri authority is DownloadsProvider
*/
privatestaticbooleanisDownloadsDocument(Uri uri){
return"com.android.providers.downloads.documents".equals(uri.getAuthority());
}

大概也就是這樣了,足可以讓你玩轉 Android 當前各個版本從系統相簿內選擇照片的需要。

(二)、系統相機

1、Android 6.0 及以上裝置

6.0 及以上裝置需要開發者 不僅僅在 ` AndroidManifest.xml `被註明必要許可權,部分危險許可權還需要開發者自己在程式碼邏輯中進行主動申請,這裡很簡單了,我只 推薦我用的一個框架,類似的許可權申請框架還有很多,需要開發者自己主動發現了。[AndPermission](https://github.com/yanzhenjie/AndPermission)

AndPermission 鏈式呼叫,作者還進行了不少主動封裝,也適配了 Androidx ,不過它也也存在不足,並且目前不知道作者是否還在自己進行維護,畢竟 issues 已經提了很多條了也沒人維護。

640?wx_fmt=png

2、Android 8.0 及以上裝置

(1)、8.0 及以上裝置需要開發者在 ` AndroidManifest.xml `內註明 provider 了 如下

android:name="androidx.core.content.FileProvider"
android:authorities="你的包名"
android:exported="false"
android:grantUriPermissions="true">
<meta-dataandroid:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/file_paths"/>
provider>

authorities :值一般是"專案的包名 + .provider"。當我們使用 FileProvider 的 getUriForFile 方法時引數需和 清單檔案註冊時的保持一致。

exported :是否對外開放,除非是對第三方提供服務,否則一般為false。

grantUriPermissions :是否授予臨時許可權,設定為true。

resource:標籤裡面是用來指定共享的路徑。就是我們的共享路徑配置的 xml 檔案,可以自己命名。該檔案放在 res/xml 資料夾下,若沒有 xml 資料夾,建立一個。

(2)、xml 檔案內的內容:

xml version="1.0"encoding="utf-8"?>
<paths>
<external-pathname="external_files"path="Pictures/"/>
paths>

可被替換成、、、等。下面給出五個的區別:

:共享外部儲存卡,對應/storage/emulated/0目錄,即Environment..getExternalStorageDirectory()

:共享外部儲存的檔案目錄,對應/storage/emulated/0/Android/data/包名/files,即Context.getExternalFilesDir()

:共享外部儲存的快取目錄,對應/storage/emulated/0/Android/data/包名/cache,即Context.getExternalCacheDir()

:共享內部檔案儲存目錄,對應 /data/data/包名/files目錄,即Context.getFilesDir()

:共享內部快取目錄,對應 /data/data/包名/cache目錄,即Context.getCacheDir()

name:隨便定義

path:需要臨時授權訪問的路徑。可以為空,表示指定目錄下的所有檔案、資料夾都可以被共享

舉例:以上方程式碼為例,最後的物理路徑為 /storage/emulated/0/Pictures。

如果將換成,則路徑為:/data/data/包名/files/Pictures

如果將換成,則路徑為:/data/data/包名/cache/Pictures

(3)、邏輯配置

//安全的獲取臨時 Uri
Uri imageUri = FileProvider.getUriForFile(context, "你的包名", fileName);
//新增這一句表示對目標應用臨時授權該Uri所代表的檔案
intentCamera.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

3、Android 10.0 及以上裝置

Uri imageUri = createImageUri();
intentCamera.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

@RequiresApi(api = Build.VERSION_CODES.Q)
publicUri createImageUri(){
ContentValues values = newContentValues();
// 需要指定檔案資訊時,非必須
values.put(MediaStore.Images.Media.DESCRIPTION, "This is an image");
values.put(MediaStore.Images.Media.DISPLAY_NAME, System.currentTimeMillis() + ".jpg");
values.put(MediaStore.Images.Media.TITLE, System.currentTimeMillis() + ".jpg");
values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/albumCameraImg");
returngetContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
}
640?wx_fmt=png

總結

Android 版本號每年都有改變,只需要不斷適配 Google 對使用者的各種考慮即可。奉上 demo 地址:https://github.com/xiangshiweiyu/AlbumAndCameraSystem

沒有放上效果,但是絕對沒問題,如果有問題請微信公眾號聯絡我。