1. 程式人生 > >安卓手機拍照、裁剪、及相簿選擇圖片裁剪(適配7.0)

安卓手機拍照、裁剪、及相簿選擇圖片裁剪(適配7.0)

網上介紹安卓7.0呼叫系統拍照的部落格有很多,但感覺都不是很清晰,遂決定自己來寫。

Demo要實現的功能:

1.支援拍照並且可以對圖片進行裁剪

2.支援從相簿中選擇圖片並進行裁剪

3.無論是拍照的照片還是從相簿中選擇的照片(都是裁剪後的)統一儲存在同一個目錄下

問題點:

1.安卓7.0及以後,獲取圖片的uri方式發生了變化,7.0及以後的uri需要通過contentProvider提供,需要配置provider

2.動態申請危險許可權

先看效果圖

專案介紹

介面上有兩個按鈕,一個是拍照並裁剪,然後將裁剪後的圖片顯示在介面上。一個是從相簿中選擇圖片然後進行裁剪,將圖片顯示在介面上。

現在一條條說

1.配置許可權,在清單檔案中

 <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" />

動態許可權檢查,為減少重複的程式碼,我直接在onCreat中進行許可權檢查(其實就是為了偷懶),程式碼如下

 private static final String TAG = "MainActivity";
    private static final int REQUEST_TAKE_PHOTO = 0;// 拍照
    private static final int REQUEST_CROP = 1;// 裁剪
    private static final int SCAN_OPEN_PHONE = 2;// 相簿
    private static final int REQUEST_PERMISSION = 100;
    private ImageView img;
    private Uri imgUri; // 拍照時返回的uri
    private Uri mCutUri;// 圖片裁剪時返回的uri
    private boolean hasPermission = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.btn_takephoto).setOnClickListener(this);
        findViewById(R.id.btn_open_photo_album).setOnClickListener(this);
        img = findViewById(R.id.iv);

        checkPermissions();
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_takephoto:
                if (hasPermission) {
                    takePhone();
                }
                break;

            case R.id.btn_open_photo_album:
                if (hasPermission) {
                    openGallery();
                }

                break;
        }
    }

    private void openGallery() {
        Intent intent = new Intent(Intent.ACTION_PICK);
        intent.setType("image/*");
        startActivityForResult(intent, SCAN_OPEN_PHONE);
    }


    private void checkPermissions() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            // 檢查是否有儲存和拍照許可權
            if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
                    && checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
                    ) {
                hasPermission = true;
            } else {
                requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA}, REQUEST_PERMISSION);
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_PERMISSION) {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                hasPermission = true;
            } else {
                Toast.makeText(this, "許可權授予失敗!", Toast.LENGTH_SHORT).show();
                hasPermission = false;
            }
        }
    }

設定了個是否有許可權的標記位,主要是SD卡讀寫許可權和拍照許可權。有兩個點選事件,一個是拍照,一個是從相簿選擇圖片。

2.開啟系統拍照的intent及程式碼:

// 拍照
    private void takePhone() {
        // 要儲存的檔名
        String time = new SimpleDateFormat("yyyyMMddHHmmss", Locale.CHINA).format(new Date());
        String fileName = "photo_" + time;
        // 建立一個資料夾
        String path = Environment.getExternalStorageDirectory() + "/take_photo";
        File file = new File(path);
        if (!file.exists()) {
            file.mkdirs();
        }
        // 要儲存的圖片檔案
        File imgFile = new File(file, fileName + ".jpeg");
        // 將file轉換成uri
        // 注意7.0及以上與之前獲取的uri不一樣了,返回的是provider路徑
        imgUri = getUriForFile(this, imgFile);
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        // 新增Uri讀取許可權
        intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
        // 新增圖片儲存位置
        intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri);
        startActivityForResult(intent, REQUEST_TAKE_PHOTO);
    }

開啟相簿的程式碼:

    private void openGallery() {
        Intent intent = new Intent(Intent.ACTION_PICK);
        intent.setType("image/*");
        startActivityForResult(intent, SCAN_OPEN_PHONE);
    }

 這裡建立了一個資料夾take_photo,設定的檔名與當前時間相關, intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri);該程式碼指定了拍照後圖片的輸出位置,傳入的引數是個uri,這裡就需要處理uri的不同了。imgUri = getUriForFile(this, imgFile);該方法根據不同的系統版本返回不同的uri。

3.獲取不同uri的方法

 // 從file中獲取uri
    // 7.0及以上使用的uri是contentProvider content://com.rain.takephotodemo.FileProvider/images/photo_20180824173621.jpg
    // 6.0使用的uri為file:///storage/emulated/0/take_photo/photo_20180824171132.jpg
    private static Uri getUriForFile(Context context, File file) {
        if (context == null || file == null) {
            throw new NullPointerException();
        }
        Uri uri;
        if (Build.VERSION.SDK_INT >= 24) {
            uri = FileProvider.getUriForFile(context.getApplicationContext(), "com.rain.takephotodemo.FileProvider", file);
        } else {
            uri = Uri.fromFile(file);
        }
        return uri;
    }

註釋寫的非常清晰了。這裡需要配置contentProvider了。

4.在清單檔案中配置provider

   <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.rain.takephotodemo.FileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/public_file_path" />
        </provider>

android:authorities="com.rain.takephotodemo.FileProvider",該行程式碼指定了當前的provider,根據需要自己起名。

android:resource="@xml/public_file_path",指定了當前provider對應的檔案目錄。

在res資料夾下建立xml資料夾,在xml資料夾下建立xml檔案,檔名隨便起。檔案內容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <external-path
        name="images"
        path="take_photo/"/>
</resources>
  • <files-path>:內部儲存空間應用私有目錄下的 files/ 目錄,等同於 Context.getFilesDir() 所獲取的目錄路徑;
  • <cache-path>:內部儲存空間應用私有目錄下的 cache/ 目錄,等同於 Context.getCacheDir() 所獲取的目錄路徑;
  • <external-path>:外部儲存空間根目錄,等同於 Environment.getExternalStorageDirectory() 所獲取的目錄路徑;
  • <external-files-path>:外部儲存空間應用私有目錄下的 files/ 目錄,等同於 Context.getExternalFilesDir(null) 所獲取的目錄路徑;
  • <external-cache-path>:外部儲存空間應用私有目錄下的 cache/ 目錄,等同於 Context.getExternalCacheDir();

name 的值可以根據需要自己起,path對應當前目錄下資料夾的名字,要確保當前的資料夾存在,可以看到與程式碼中的建立的資料夾名字是對應的,即是我們要儲存的裁剪過後的圖片的目錄。

5.在onActivityResult中對拍照、裁剪、開啟相簿後結果進行處理,可以看到先前申請的常量值

    private static final int REQUEST_TAKE_PHOTO = 0;// 拍照
    private static final int REQUEST_CROP = 1;// 裁剪
    private static final int SCAN_OPEN_PHONE = 2;// 相簿
    private Uri imgUri; // 拍照時返回的uri
    private Uri mCutUri;// 圖片裁剪時返回的uri
  @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {
            switch (requestCode) {
                // 拍照並進行裁剪
                case REQUEST_TAKE_PHOTO:
                    Log.e(TAG, "onActivityResult: imgUri:REQUEST_TAKE_PHOTO:" + imgUri.toString());
                    cropPhoto(imgUri, true);
                    break;

                // 裁剪後設置圖片
                case REQUEST_CROP:
                    img.setImageURI(mCutUri);
                    Log.e(TAG, "onActivityResult: imgUri:REQUEST_CROP:" + mCutUri.toString());
                    break;
                // 開啟相簿獲取圖片並進行裁剪
                case SCAN_OPEN_PHONE:
                    Log.e(TAG, "onActivityResult: SCAN_OPEN_PHONE:" + data.getData().toString());
                    cropPhoto(data.getData(), false);
                    break;
            }
        }
    }

可以看到,對拍照、開啟相簿獲取到圖片的uri都進行了裁剪處理。

 // 圖片裁剪
    private void cropPhoto(Uri uri, boolean fromCapture) {
        Intent intent = new Intent("com.android.camera.action.CROP"); //開啟系統自帶的裁剪圖片的intent
        intent.setDataAndType(uri, "image/*");
        intent.putExtra("scale", true);

        // 設定裁剪區域的寬高比例
        intent.putExtra("aspectX", 1);
        intent.putExtra("aspectY", 1);

        // 設定裁剪區域的寬度和高度
        intent.putExtra("outputX", 200);
        intent.putExtra("outputY", 200);

        // 取消人臉識別
        intent.putExtra("noFaceDetection", true);
        // 圖片輸出格式
        intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());

        // 若為false則表示不返回資料
        intent.putExtra("return-data", false);

        // 指定裁剪完成以後的圖片所儲存的位置,pic info顯示有延時
        if (fromCapture) {
            // 如果是使用拍照,那麼原先的uri和最終目標的uri一致
            mCutUri = uri;
        } else { // 從相簿中選擇,那麼裁剪的圖片儲存在take_photo中
            String time = new SimpleDateFormat("yyyyMMddHHmmss", Locale.CHINA).format(new Date());
            String fileName = "photo_" + time;
            File mCutFile = new File(Environment.getExternalStorageDirectory() + "/take_photo", fileName + ".jpeg");
            if (!mCutFile.getParentFile().exists()) {
                mCutFile.getParentFile().mkdirs();
            }
            mCutUri = getUriForFile(this, mCutFile);
        }
        intent.putExtra(MediaStore.EXTRA_OUTPUT, mCutUri);
        Toast.makeText(this, "剪裁圖片", Toast.LENGTH_SHORT).show();
        // 以廣播方式刷新系統相簿,以便能夠在相簿中找到剛剛所拍攝和裁剪的照片
        Intent intentBc = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
        intentBc.setData(uri);
        this.sendBroadcast(intentBc);

        startActivityForResult(intent, REQUEST_CROP); //設定裁剪引數顯示圖片至ImageVie
    }

這裡需要注意的有兩點:

1.拍照的uri是之前自己定義的uri,從相簿選擇圖片並返回的uri為data.getData()方法中

2.拍照的uri指定的目錄既是take_photo,既是裁剪後圖片的目錄,mCutUri = uri;

對於相簿選擇圖片,需要自己指定裁剪後的uri,程式碼註釋很清楚,不再贅述。

最後,將mCutUri 設定給imageView

還有一點需要注意,看以下程式碼:

        // 以廣播方式刷新系統相簿,以便能夠在相簿中找到剛剛所拍攝和裁剪的照片
        Intent intentBc = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
        intentBc.setData(uri);
        this.sendBroadcast(intentBc);

當你最終生成圖片時,你開啟相簿可能不重新整理,這幾行程式碼告訴相簿進行重新整理。其實,如果沒有這幾行程式碼,你清空相簿後臺再開啟相簿也是可以看到新生成的圖片的。

6.執行時遇到的問題:

當我開啟相簿檢視剛生成的圖片時,檢視圖片的info資訊,會發現資訊顯示錯誤,還是原圖的資訊,過一會就顯示正常了,檢視圖片時可以通過studio >> Device File Exploer檢視。