適配Android4.4~Android11,調用系統相機,系統相冊,系統圖片裁剪,轉換文件(對圖片進行上傳等操作)


前言

  最近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

 


免責聲明!

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



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