1. 程式人生 > >Android拍照及圖片裁剪、呼叫系統相簿(相容6.0以上許可權處理及7.0以上檔案管理)

Android拍照及圖片裁剪、呼叫系統相簿(相容6.0以上許可權處理及7.0以上檔案管理)

前言
最近工作修改較舊的專案時,涉及到了圖片相關功能 ,在使用安卓6.0手機及7.1手機拍照時,遇到了因許可權及檔案管理導致程式崩潰等問題。 剛好把功能修改完,把程式碼簡單地貼一下,方便以後使用。

本文demo包含以下要點:

  • Android6.0執行時許可權封裝(避免使用者選擇不再提示後無法獲取許可權的問題)

  • Android7.0 出現FileUriExposedException異常的問題

  • 對照片進行裁剪

  • PhotoUtils工具類對拍照和相簿獲取照片的封裝

  • CircleImageView圓形頭像

下面看下效果圖。

▲ Android 6.0 執行時許可權問題

關於Android 6.0 執行時許可權的使用和問題,本文不再贅述。需要的可以看看我的這篇文章 ->

Android6.0執行時許可權封裝(避免使用者選擇不再提示後無法獲取許可權的問題) 本文所使用的許可權請求程式碼均從該文摘取。沒看過的也沒關係,之後文章最後會給出demo專案,直接看裡面程式碼,註釋也寫的很清楚。

▲ Android 7.0 裝置會出現異常FileUriExposedException

主要原因是Google是反對放寬私有目錄的訪問許可權的,所以收起對私有檔案的訪問許可權是Android將來發展的趨勢。

Android7.0中嘗試傳遞 file:// URI 會觸發 FileUriExposedException,因為在Android7.0之後Google認為直接使用本地的根目錄即file:// URI是不安全的操作,直接訪問會丟擲FileUriExposedExCeption異常,這就意味著在Android7.0以前我們訪問相機拍照儲存時,如果使用URI的方式直接儲存剪裁圖片就會造成這個異常,那麼如何解決這個問題呢?

Google為我們提供了FileProvider類,進行一種特殊的內容提供,FileProvider時ContentProvide的子類,它使用了和內容提供器類似的機制來對資料進行保護,可以選擇性地將封裝過的Uri共享給外部,從而提高了應用的安全性。下面就讓我們看一下如何使用這個內容提供者進行資料訪問的:

使用FileProvider獲取Uri就會將以前的file:// URI準換成content:// URI,實現一種安全的應用間資料訪問。步奏如下

  • 在AndroidManifest.xml中註冊provider
        <provider
            android:name
="android.support.v4.content.FileProvider" android:authorities="com.donkor.demo.takephoto.fileprovider" android:exported="false" android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider>

注意 :

1 . android:exported:要求必須為false,為true則會報安全異常;

2 . android:grantUriPermissions:true,表示授予 URI 臨時訪問許可權;

3 . android:authorities這個屬性的值,建議寫包名+fileprovider,當然也可以起別的字串,但是在裝置中不能出現2個及以上的APP使用到同一個authorities屬性值,因為無法共存。

  • 指定共享的目錄

在app/src/main/res建立一個xml的目錄,如下:

向該目錄中新增一個file_paths.xml檔案(命名可以自由定義,但是需上下文一致),新增如下內容到file_paths.xml中。

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path
        name="camera_photos"
        path="" />
</paths>

注意 :

1 . 代表的根目錄:Context.getFilesDir()

2 . 代表的根目錄:Environment.getExternalStorageDirectory()

3 . 代表的根目錄:getCacheDir()

上述程式碼中path=“ ”,是有特殊意義的,它程式碼根目錄,也就是說你可以向其它的應用共享根目錄及其子目錄下任何一個檔案了,如果你將path設為path=“pictures”,那麼它代表著根目錄下的pictures目錄(eg:/storage/emulated/0/pictures),如果你向其它應用分享pictures目錄範圍之外的檔案是不行的。

  • 使用FileProvider加密Uri

程式碼中imageUri是用於儲存拍照後照片的Uri,呼叫相機拍照之前首先判斷一下系統版本,AndroidN也就是Android7.0以上的系統通過FileProvider獲取Uri方法的引數分別為,上下文物件、清單檔案配置的android:authorities和對應的拍照儲存的圖片。之後就是通過PhotoUtils呼叫系統相機進行拍照。

imageUri = Uri.fromFile(fileUri);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
   imageUri = FileProvider.getUriForFile(MainActivity.this, "com.donkor.demo.takephoto.fileprovider", fileUri);
PhotoUtils.takePicture(MainActivity.this, imageUri, CODE_CAMERA_REQUEST);

通過Intent呼叫系統相機拍照,如果本機版本大於等於anroid7.0需要臨時授權Uri的訪問許可權如下:
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
其他部分和以前正常呼叫系統相機一樣。

▲ PhotoUtils工具類

完整程式碼如下

public class PhotoUtils {

    /**
     * @param activity    當前activity
     * @param imageUri    拍照後照片儲存路徑
     * @param requestCode 呼叫系統相機請求碼
     */
    public static void takePicture(Activity activity, Uri imageUri, int requestCode) {
        //呼叫系統相機
        Intent intentCamera = new Intent();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            intentCamera.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //新增這一句表示對目標應用臨時授權該Uri所代表的檔案
        }
        intentCamera.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
        //將拍照結果儲存至photo_file的Uri中,不保留在相簿中
        intentCamera.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
        activity.startActivityForResult(intentCamera, requestCode);
    }

    /**
     * @param activity    當前activity
     * @param requestCode 開啟相簿的請求碼
     */
    public static void openPic(Activity activity, int requestCode) {
        Intent photoPickerIntent = new Intent(Intent.ACTION_GET_CONTENT);
        photoPickerIntent.setType("image/*");
        activity.startActivityForResult(photoPickerIntent, requestCode);
    }

    /**
     * @param activity    當前activity
     * @param orgUri      剪裁原圖的Uri
     * @param desUri      剪裁後的圖片的Uri
     * @param aspectX     X方向的比例
     * @param aspectY     Y方向的比例
     * @param width       剪裁圖片的寬度
     * @param height      剪裁圖片高度
     * @param requestCode 剪裁圖片的請求碼
     */
    public static void cropImageUri(Activity activity, Uri orgUri, Uri desUri, int aspectX, int aspectY, int width, int height, int requestCode) {
        Intent intent = new Intent("com.android.camera.action.CROP");
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        }
        intent.setDataAndType(orgUri, "image/*");
        intent.putExtra("crop", "true");
        intent.putExtra("aspectX", aspectX);
        intent.putExtra("aspectY", aspectY);
        intent.putExtra("outputX", width);
        intent.putExtra("outputY", height);
        intent.putExtra("scale", true);
        //將剪下的圖片儲存到目標Uri中
        intent.putExtra(MediaStore.EXTRA_OUTPUT, desUri);
        intent.putExtra("return-data", false);
        intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
        intent.putExtra("noFaceDetection", true);
        activity.startActivityForResult(intent, requestCode);
    }

    /**
     * 讀取uri所在的圖片
     *
     * @param uri      圖片對應的Uri
     * @param mContext 上下文物件
     * @return 獲取影象的Bitmap
     */
    public static Bitmap getBitmapFromUri(Uri uri, Context mContext) {
        try {
            Bitmap bitmap = MediaStore.Images.Media.getBitmap(mContext.getContentResolver(), uri);
            return bitmap;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * @param context 上下文物件
     * @param uri     當前相簿照片的Uri
     * @return 解析後的Uri對應的String
     */
    @SuppressLint("NewApi")
    public static String getPath(final Context context, final Uri uri) {

        final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
        String pathHead = "file:///";
        // DocumentProvider
        if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
            // ExternalStorageProvider
            if (isExternalStorageDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];
                if ("primary".equalsIgnoreCase(type)) {
                    return pathHead + Environment.getExternalStorageDirectory() + "/" + split[1];
                }
            }
            // DownloadsProvider
            else if (isDownloadsDocument(uri)) {

                final String id = DocumentsContract.getDocumentId(uri);

                final Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));

                return pathHead + getDataColumn(context, contentUri, null, null);
            }
            // MediaProvider
            else if (isMediaDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];

                Uri contentUri = null;
                if ("image".equals(type)) {
                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
                } else if ("video".equals(type)) {
                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
                } else if ("audio".equals(type)) {
                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
                }

                final String selection = "_id=?";
                final String[] selectionArgs = new String[]{split[1]};

                return pathHead + getDataColumn(context, contentUri, selection, selectionArgs);
            }
        }
        // MediaStore (and general)
        else if ("content".equalsIgnoreCase(uri.getScheme())) {
            return pathHead + getDataColumn(context, uri, null, null);
        }
        // File
        else if ("file".equalsIgnoreCase(uri.getScheme())) {
            return pathHead + uri.getPath();
        }
        return null;
    }

    /**
     * Get the value of the data column for this Uri. This is useful for
     * MediaStore Uris, and other file-based ContentProviders.
     *
     * @param context       The context.
     * @param uri           The Uri to query.
     * @param selection     (Optional) Filter used in the query.
     * @param selectionArgs (Optional) Selection arguments used in the query.
     * @return The value of the _data column, which is typically a file path.
     */
    private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {

        Cursor cursor = null;
        final String column = "_data";
        final String[] projection = {column};
        try {
            cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);
            if (cursor != null && cursor.moveToFirst()) {
                final int column_index = cursor.getColumnIndexOrThrow(column);
                return cursor.getString(column_index);
            }
        } finally {
            if (cursor != null)
                cursor.close();
        }
        return null;
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is ExternalStorageProvider.
     */
    private static boolean isExternalStorageDocument(Uri uri) {
        return "com.android.externalstorage.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is DownloadsProvider.
     */
    private static boolean isDownloadsDocument(Uri uri) {
        return "com.android.providers.downloads.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is MediaProvider.
     */
    private static boolean isMediaDocument(Uri uri) {
        return "com.android.providers.media.documents".equals(uri.getAuthority());
    }
}

注 : cropImageUri方法中,和以前正常剪裁程式碼基本相同,和上面意圖開啟相機一樣,如果本機版本大於等於
anroid7.0需要臨時授權Uri的訪問許可權如下:
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)

注 : Android4.4之後選取中的圖片不再返回真實的Uri了,而是封裝過的Uri,所以在4.4以上,就要對這個Uri進行解析,即上面的PhotoUtils.getPath()方法,
具體Uri的解析見PhotoUtils類中getPath()方法。android4.4以前直接data.getData就可以獲取到真實Uri不用解析。解析獲取真實的Uri後,判斷系統版本開始通過FileProvider獲取新的Uri之後就可以同樣的圖片剪裁了

▲ 拍照、呼叫相簿回撥

拍完照之後對照片進行裁剪

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        int output_X = 480, output_Y = 480;
        if (resultCode == RESULT_OK) {
            switch (requestCode) {
                case CODE_CAMERA_REQUEST://拍照完成回撥
                    cropImageUri = Uri.fromFile(fileCropUri);
                    PhotoUtils.cropImageUri(this, imageUri, cropImageUri, 1, 1, output_X, output_Y, CODE_RESULT_REQUEST);
                    break;
                case CODE_GALLERY_REQUEST://訪問相簿完成回撥
                    if (hasSdcard()) {
                        cropImageUri = Uri.fromFile(fileCropUri);
                        Uri newUri = Uri.parse(PhotoUtils.getPath(this, data.getData()));
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
                            newUri = FileProvider.getUriForFile(this, "com.donkor.demo.takephoto.fileprovider", new File(newUri.getPath()));
                        PhotoUtils.cropImageUri(this, newUri, cropImageUri, 1, 1, output_X, output_Y, CODE_RESULT_REQUEST);
                    } else {
                        Toast.makeText(MainActivity.this, "裝置沒有SD卡!", Toast.LENGTH_SHORT).show();
                    }
                    break;
                case CODE_RESULT_REQUEST:
                    Bitmap bitmap = PhotoUtils.getBitmapFromUri(cropImageUri, this);
                    if (bitmap != null) {
                        showImages(bitmap);
                    }
                    break;
            }
        }
    }

到此對關鍵程式碼簡單描述完成。

About me
Email :[email protected]
Android開發交流QQ群 : 537891203
Android開發交流群