安卓手機拍照、裁剪、及相簿選擇圖片裁剪(適配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檢視。