前言

最近Android對於檔案的許多方法進行了修改,網路上又沒有對Android4到Android11關於系統相機、系統相簿和系統裁剪的適配方案,我花了幾天事件總結了一下,先上原始碼

DEMO原始碼

先對Android的檔案系統進行一個初步的總結:

在AndroidQ(Android10)以前,Android的檔案系統並不是特別的嚴格,各個app可以獲取到各個位置的檔案的路徑,安全性非常差。

在AndroidQ以後,檔案系統進行了改革,使用了分割槽儲存模式(Scoped Storage),也叫沙盒模式,何謂沙盒?每個App在安裝之後會在檔案系統中建立一個名稱為該App包名命名的資料夾,這個資料夾就叫做沙盒。該模式下,應用只能訪問沙盒內部的檔案和公共目錄下的多媒體檔案和下載檔案。

拍照、選擇系統相簿、裁剪都需要用到Uri,Uri分為兩種,一種是file型別的,一種是content型別的,file型別的uri可直接得到該uri的真實路徑,content型別的uri是一個匿名uri,無法獲取具體的檔案路徑。

AndroidQ以上統一使用公共目錄進行拍照和裁剪圖片的儲存,而對於AndroidQ以下,還需進行AndroidN(Android7)的區分,在AndroidN到AndroidQ以下的拍照使用的uri變成了content,如果還是使用file型別的uri,則會報錯,所以需要使用FileProvider進行一個轉換,詳情看以下的適配過程:

Android版本 拍照傳入intent的uri型別 裁剪傳入intent的uri型別
Android7以下(不包括Android7) file file
Android7到Android10以下(不包括Android10) content file

對於拍照和裁剪得到的圖片,肯定也會收到影響,以下就進行適配的基本介紹。

適配介紹

在AndroidManifest.xml中新增以下配置:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.camerademo">
<!-- 相機許可權和檔案讀寫許可權 -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
...
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.camerademo.fileprovider2"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider><!-- app的fileProvider宣告,Android7.0-Android10配置 -->
</application>
</manifest>

在專案的res資料夾中建立一個xml目錄,並且在xml目錄下建立一個file_paths.xml檔案:

<?xml version="1.0" encoding="utf-8"?>
<!--自定義fileProvider路徑,Android7.0以上需配置-->
<paths>
<!--external-files-path代表的是context.getExternalFilesDir(null)路徑-->
<external-files-path
name="images"
path="."/>
</paths>

在Activity中定義一個全域性的Uri對圖片進行接收,以便後續操作:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private Uri uri;
......
}

1.拍照

檢查許可權:

if (CameraUtils.checkTakePhotoPermission(this)) {//檢查許可權
//有許可權,開啟相機
openCamera();
} else {
//無許可權,申請
CameraUtils.requestTakePhotoPermissions(this);
}

開啟相機,這裡的uri就是拍照後的圖片:

//開啟相機
private void openCamera() {
uri = CameraUtils.openCamera(this, "test", "albumDir");
}

具體邏輯:

/**
* 開啟相機
* AndroidQ以上:圖片儲存進公共目錄內(公共目錄/picture/子資料夾)
* AndroidQ以下:相片儲存進沙盒目錄內(沙盒目錄/picture/子資料夾)
* @param activity activity
* @param name 相片名
* @param child 存放的子資料夾
* @return 成功即為uri,失敗為null,等到相機拍照後,該uri即為照片
*/
public static Uri openCamera(Activity activity, String name, String child) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (intent.resolveActivity(activity.getPackageManager()) == null) {
//無相機
Log.e(TAG, "無相機");
return null;
}
if (name == null || name.equals("")) {
name = System.currentTimeMillis() + ".png";
} else {
name = name + ".png";
}
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
Log.e(TAG, "不存在儲存卡或沒有讀寫許可權");
return null;
}
Uri uri;
if (isAndroidQ) {
uri = createImageUriAboveAndroidQ(activity, name, child);
} else {
uri = createImageCameraUriBelowAndroidQ(activity, name, child);
}
if (uri == null) {
Log.e(TAG, "用於存放照片的uri建立失敗");
return null;
}
Log.e(TAG, "cameraUri:" + uri);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
activity.startActivityForResult(intent, CAMERA_TAKE_PHOTO);
return uri;
} /**
* AndroidQ以上建立用於儲存相片的uri,(公有目錄/pictures/child)
* @param activity activity
* @param name 檔名
* @param child 子資料夾
* @return uri
*/
private static Uri createImageUriAboveAndroidQ(Activity activity, String name, String child) {
ContentValues contentValues = new ContentValues();//內容
ContentResolver resolver = activity.getContentResolver();//內容解析器
contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, name);//檔名
contentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/*");//檔案型別
if (child != null && !child.equals("")) {
//存放子資料夾
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/" + child);
} else {
//存放picture目錄
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);
}
return resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
} /**
* AndroidQ以下建立用於儲存拍照的照片的uri,(沙盒目錄/pictures/child)
* 拍照傳入的intent中
* Android7以下:file型別的uri
* Android7以上:content型別的uri
* @param activity activity
* @param name 檔名
* @param child 子資料夾
* @return content uri
*/
private static Uri createImageCameraUriBelowAndroidQ(Activity activity, String name, String child) {
File pictureDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES);//標準圖片目錄
assert pictureDir != null;//獲取沙盒內標準目錄是不會為null的
if (getDir(pictureDir)) {
if (child != null && !child.equals("")) {//存放子資料夾
File childDir = new File(pictureDir + "/" + child);
if (getDir(childDir)) {
File picture = new File(childDir, name);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//適配Android7以上的path轉uri
return FileProvider.getUriForFile(activity, AUTHORITY, picture);
} else {
//Android7以下
return Uri.fromFile(picture);
}
} else {
return null;
}
} else {//存放當前目錄
File picture = new File(pictureDir, name);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//適配Android7以上的path轉uri,該方法得到的uri為content型別的
return FileProvider.getUriForFile(activity, AUTHORITY, picture);
} else {
//Android7以下,該方法得到的uri為file型別的
return Uri.fromFile(picture);
}
}
} else {
return null;
}
}

在onActivityResult中使用imageView的setImageURI()方法即可開啟該圖片,並且告知相簿圖片更新:

if (requestCode == CameraUtils.CAMERA_TAKE_PHOTO) {
//相機跳轉回調
ivPicture.setImageURI(uri);//展示圖片
//通知系統相簿更新資訊
CameraUtils.updateSystem(this, uri);
}

由於廣播更新的方法已經棄用:

context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));

使用以下方法更新相簿:

/**
* 更新系統相簿
* @param uri uri
*/
public static void updateSystem(Context context, Uri uri) {
if (uri == null) {
Log.e(TAG, "uri為空");
return;
}
MediaScannerConnection.scanFile(context, new String[]{uri.getPath()}, null, null);
}

2.相簿

檢查許可權,開啟相簿:

if (CameraUtils.checkSelectPhotoPermission(this)) {//檢查許可權
//有許可權,開啟相簿
openAlbum();
} else {
//無許可權,申請
CameraUtils.requestSelectPhotoPermissions(this);
} //開啟相簿
private void openAlbum() {
uri = null;
CameraUtils.openAlbum(this);
} //開啟相簿
public static void openAlbum(Activity activity) {
Intent intent = new Intent(Intent.ACTION_PICK, null);
intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
activity.startActivityForResult(intent, CAMERA_SELECT_PHOTO);
}

相簿回撥:

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
//activity跳轉回調
...
} else if (requestCode == CameraUtils.CAMERA_SELECT_PHOTO) {
//相簿跳轉回調
if (data != null){
ivPicture.setImageURI(data.getData());
uri = data.getData();
}
}
}

3.裁剪

檢查許可權,開啟裁剪:

//裁剪
if (CameraUtils.checkCropPermission(this)) {//檢查許可權
//有許可權,開啟裁剪
openCrop();
} else {
//無許可權,申請
CameraUtils.requestCropPermissions(this);
} private void openCrop() {
uri = CameraUtils.openCrop(this, uri, "testCrop", "cropDir");
}

具體邏輯:

/**
* 圖片裁剪,裁剪後存放在沙盒目錄下(沙盒目錄/picture/子資料夾)
* @param activity activity
* @param uri 圖片uri
* @param name 裁剪後的圖片名
* @param child 子資料夾
* @return 裁剪後的圖片uri
*/
public static Uri openCrop(Activity activity, Uri uri, String name, String child) {
if (uri == null) {
Log.e(TAG, "uri為空");
return null;
}
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
//未掛在儲存裝置或者沒有讀寫許可權
return null;
}
if (name != null && !name.equals("")) {
name = name + ".png";
} else {
name = System.currentTimeMillis() + ".png";
} Uri resultUri;
if (isAndroidQ) {
resultUri = createImageUriAboveAndroidQ(activity, name, child);
} else {
resultUri = createImageCropUriBelowAndroidQ(activity, name, child);
}
if (resultUri == null) {
Log.e(TAG, "用於存放照片的uri建立失敗");
return null;
}
Log.e(TAG, "cropUri:" + resultUri);
Intent intent = new Intent("com.android.camera.action.CROP");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
intent.setDataAndType(uri, "image/*");
// 設定裁剪
intent.putExtra("crop", "true");
// aspectX aspectY 是寬高的比例
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1); intent.putExtra(MediaStore.EXTRA_OUTPUT, resultUri);
// 圖片格式
intent.putExtra("outputFormat", "png");
intent.putExtra("noFaceDetection", true);// 取消人臉識別
intent.putExtra("return-data", true);// true:不返回uri,false:返回uri
activity.startActivityForResult(intent, CAMERA_CROP);
return resultUri;
} /**
* AndroidQ以下建立用於儲存裁剪的uri,(沙盒目錄/pictures/child)
* 裁剪傳入intent的uri跟拍照不同
* 在AndroidQ以下統一使用file型別的uri,所以統一用Uri.fromFile()方法返回
* @param activity activity
* @param name 檔名
* @param child 子資料夾
* @return file uri
*/
private static Uri createImageCropUriBelowAndroidQ(Activity activity, String name, String child) {
File pictureDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES);//標準圖片目錄
assert pictureDir != null;//獲取沙盒內標準目錄是不會為null的
if (getDir(pictureDir)) {
if (child != null && !child.equals("")) {//存放子資料夾
File childDir = new File(pictureDir + "/" + child);
if (getDir(childDir)) {
File picture = new File(childDir, name);
return Uri.fromFile(picture);
} else {
return null;
}
} else {//存放當前目錄
File picture = new File(pictureDir, name);
return Uri.fromFile(picture);
}
} else {
return null;
}
}

裁剪回撥:

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
//activity跳轉回調
...
} else if (requestCode == CameraUtils.CAMERA_CROP) {
//裁剪跳轉回調
if (uri == null) {
return;
}
ivPicture.setImageURI(uri);
//通知系統相簿更新資訊
CameraUtils.updateSystem(this, uri);
}
}

4.轉換File

相簿預設將圖片複製到沙盒內進行操作,拍照和裁剪在AndroidQ以下會直接拿到原始檔,AndroidQ以上預設複製到沙盒內操作

if (uri != null) {
File file = CameraUtils.uriToFile(this, uri);
if (file != null) {
tvFilePath.setText("路徑:" + file.getPath());
} else {
tvFilePath.setText("file:null");
}
} else {
tvFilePath.setText("null");
} /**
* 將uri轉換為file
* uri型別為file的直接轉換出路徑
* uri型別為content的將對應的檔案複製到沙盒內的cache目錄下進行操作
* @param context 上下文
* @param uri uri
* @return file
*/
public static File uriToFile(Context context, Uri uri) {
if (uri == null) {
Log.e(TAG, "uri為空");
return null;
}
File file = null;
if (uri.getScheme() != null) {
Log.e(TAG, "uri.getScheme():" + uri.getScheme());
if (uri.getScheme().equals(ContentResolver.SCHEME_FILE) && uri.getPath() != null) {
//此uri為檔案,並且path不為空(儲存在沙盒內的檔案可以隨意訪問,外部檔案path則為空)
file = new File(uri.getPath());
} else if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
//此uri為content型別,將該檔案複製到沙盒內
ContentResolver resolver = context.getContentResolver();
@SuppressLint("Recycle")
Cursor cursor = resolver.query(uri, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
String fileName = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
try {
InputStream inputStream = resolver.openInputStream(uri);
if (context.getExternalCacheDir() != null) {
//該檔案放入cache快取資料夾中
File cache = new File(context.getExternalCacheDir(), fileName);
FileOutputStream fileOutputStream = new FileOutputStream(cache);
if (inputStream != null) {
// FileUtils.copy(inputStream, fileOutputStream);
//上面的copy方法在低版本的手機中會報java.lang.NoSuchMethodError錯誤,使用原始的讀寫流操作進行復制
byte[] len = new byte[Math.min(inputStream.available(), 1024 * 1024)];
int read;
while ((read = inputStream.read(len)) != -1) {
fileOutputStream.write(len, 0, read);
}
file = cache;
fileOutputStream.close();
inputStream.close();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return file;
}

至此,適配已經完成,以下是測試結果:

機型 Android版本 拍照 相簿 裁剪 獲取file
紅米k30s至尊紀念版-Redmi K30S Uitra(真機) Android11 成功 成功 成功 拍照、相簿、裁剪均可
華為Mate10-HUAWEI ALP-AL00(mumu模擬器) Android6.0.1 成功 成功 成功 拍照、相簿、裁剪均可
小米9-MI 9(夜神模擬器) Android7.1.2 成功 成功 成功 拍照、相簿、裁剪均可
三星Note10-SM N976N(夜神模擬器) Android5.1.1 成功 成功 成功 拍照、相簿、裁剪均可
榮耀9-LLD-AL00(真機) Android9.1.0 成功 成功 成功 拍照、相簿、裁剪均可

在測試的最後發現一個問題,部分機型在拍照和裁剪之後,無法更新進系統相簿,有知道原因的請告知,謝謝!

如果文章內容有錯誤的,敬請批評指正!

歡迎新增本人QQ騷擾:1336140321