安卓開發——拍照、裁剪並保存為頭像報錯:裁剪圖片無法保存的
在做學校大創項目的安卓開發時,需要從相冊獲取圖片或者拍照,然後裁剪保存為頭像。由於我是第一次弄安卓開發,也對Android現在越來越多的權限限制不了解,debug過程真的是異常心塞啊。
閑話不說(文末慢慢話癆),我開始是在網上找了一些代碼打算用到項目上試試,但是連個拍照或者從相冊選擇圖片都頻繁報錯(應該還是因為sd卡權限之類的吧),折騰了一晚上沒有解決,第二天還是老老實實的看《第一行代碼》,邊學邊寫。在這裏我簡單梳理一下流程(關於裁剪後圖片無法保存的問題的解釋請直接跳到水平線之後):
- 調用手機攝像頭拍照:
//創建file文件,用於存儲相機拍下的照片,這裏我命名為my_head_image.jpg,並將它放在//手機SD卡的應用關聯緩存中。 /* File outputImage = new File(getExternalCacheDir(), "my_head_image.jpg"); try { if (outputImage.exists()) { outputImage.delete(); } outputImage.createNewFile(); } catch (IOException e) { e.printStackTrace(); }//將File對象轉換為Uri對象,先進行系統版本的判定,Android7.0以後的版本和之前的版本不 //太一樣 if (Build.VERSION.SDK_INT >= 24) { imageUri = FileProvider.getUriForFile(ChangeMyDetails.this, "com.example.write.fileprovider", outputImage); } else { imageUri = Uri.fromFile(outputImage);*/
File outputImage = new File(Environment.getExternalStorageDirectory(), "my_head_image.jpg");
String path = outputImage.getAbsolutePath();
Log.i("ChangeMyDetails", "outputImage路徑為 "+path);
try {
if (outputImage.exists()) {
outputImage.delete();
}
outputImage.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
imageUri = Uri.fromFile(outputImage);
//啟動相機程序
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, TAKE_PHOTO);
橙色部分的代碼是《第一行代碼》上的,如果拍照後的圖片直接作為頭像不裁剪的話這段是沒問題的,但是你懂得,後來就崩了。在這裏首先創建fFile對象,用於存放拍下的照片,並將它保存在SD卡的應用關聯緩存目錄下,調用getCacheDir()得到這個目錄,具體路徑是/sdcard/Android/data/<你的package name>/cache。為什麽要放在這裏呢? 因為從Android6.0系統開始,讀寫SD卡被視為危險權限,如何放在其他目錄,都要在運行時進行權限處理,而使用應用關聯目錄則可以跳過這一步。註意:在這裏我就種下了裁剪後無法保存的隱患。(橙色下面的更正代碼是後話了,因為還要涉及權限問題,我後面會講,所以你看到這裏欣喜的粘貼到你的項目裏還是會報錯的)
接著進行系統版本判斷,FileProvider的getUriForFile()方法將File對象封裝為Uri對象。getUriForFile()方法接收3個參數,第一個是要求傳入的context對象,第二個可以是任意唯一的字符串(後面manifest.xml中註冊<procider>的android:authority要用到),第三個是要封裝的這個File對象。之所以添加這一步,因為Android7.0系統開始,直接使用本地Uri被認為是不安全的,會拋出FIleURIExposedException異常。FileProvider則是一種特殊的得內容提供器,可以選擇性的將封裝過的Uri共享給外部,更加安全。
關於內容提供器,還要在manifest,xml中進行註冊:
<provider android:name="android.support.v4.content.FileProvider" android:authorities="com.example.write.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths"/> </provider>
其中,android:authorities屬性值必須與FileProvider.getUriForFile()方法中的第二個參數一致,另外,用<meta-data>來指定Uri的共享路徑,並引用@xml/provider_paths資源,這個資源需要自己創建。
右擊res目錄→New→Directory,創建一個xml目錄,然後右擊xml目錄→New→File,創建一個provider_paths.xml的文件:
<?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <external-path name="my_images" path="."/> </paths>
其中,external-path 就是用來指定Uri共享的,name屬性值自定義就可以了,path屬性為空表示整個SD卡進行共享。
- 裁剪照片:
拍照時,使用startActivityForResult(intent, TAKE_PHOTO)來啟動活動,因此拍完照會有結果返回到onActivityResult()方法中,拍照成功後執行接下來的裁剪。 onActivityResult()方法中還有從相冊選擇圖片、裁剪成功後返回執行的操作,我就一起貼出來了。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { //用戶沒有進行有效的操作,返回 if (requestCode == RESULT_CANCELED) { Toast.makeText(getApplication(), "取消", Toast.LENGTH_LONG).show(); return; } switch (requestCode) { case FROM_GALLERY: if (resultCode == RESULT_OK) { if (Build.VERSION.SDK_INT >= 19) { //4.4以上系統使用 handleImageOnKitKat(data); } else { handleImageBeforeKitKat(data); } } break; case TAKE_PHOTO:// 裁剪照片 cropRawPhoto(imageUri); break; case RESULT_REQUEST_CODE: if (cropImgUri !=null) { try { Bitmap headImage = BitmapFactory.decodeStream(getContentResolver().openInputStream(cropImgUri)); headImageButton.setImageBitmap(headImage); } catch (Exception e) { e.printStackTrace(); } }else { Toast.makeText(this,"cropImgUri為空!",Toast.LENGTH_SHORT).show(); } break; } }
public void cropRawPhoto(Uri uri) {
//創建file文件,用於存儲剪裁後的照片
File cropImage = new File(Environment.getExternalStorageDirectory(), "crop_image.jpg");
String path = cropImage.getAbsolutePath();
try {
if (cropImage.exists()) {
cropImage.delete();
}
cropImage.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
cropImgUri = Uri.fromFile(cropImage);
Intent intent = new Intent("com.android.camera.action.CROP");
//設置源地址uri
intent.setDataAndType(imageUri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", 200);
intent.putExtra("outputY", 200);
intent.putExtra("scale", true);
//設置目的地址uri
intent.putExtra(MediaStore.EXTRA_OUTPUT, cropImgUri);
//設置圖片格式
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
intent.putExtra("return-data", false);
intent.putExtra("noFaceDetection", true); // no face detection
startActivityForResult(intent, RESULT_REQUEST_CODE);
}
startActivityForResult(intent, RESULT_REQUEST_CODE);執行後,跳轉到onActivityResult()中執行case RESULT_REQUEST_CODE:部分,代碼已經貼出來了。就是調用BitmapFactory.decodeStream()方法將cropImage解析為bitmap對象。
然後,開始運行。
那麽,問題來了(猜測是:由於裁剪後的圖片保存到Cache裏會耗費大量內存,Android是不允許你這樣做的):
這裏有一篇一篇博文進行了解釋:http://www.cnblogs.com/tianzhijiexian/p/4059006.html
最開始,我是把相機拍下的照片和裁剪後的照片都存在關聯應用緩存裏,就是前面橙色部分代碼的操作(貼上的代碼是我後來改正過的沒問題的代碼)。啟動相機程序拍照並存儲是正常的,圖片也保存了。但是,裁剪之後的圖片無法保存到Cache目錄裏,我沿著/sdcard/Android/data/<你的package name>/cache路徑打開看了看,是0kb。
本來最開始我就懷疑這個Cache存儲可能會有問題,但是又想,拍下照片都可以好好保存為什麽裁剪後的就不能保存呢?這不公平啊!於是乎,我著手改其他的我也懷疑的地方,在網上搜尋相關解答折騰很久還是解決不了。最後,我決定驗證最後一個猜想:裁剪後的圖片以某種詭異不明的方式,無法保存到Cache裏面。說幹就幹:
step1:把File路徑換成普通的,也就是把File outputImage = new File(getExternalCacheDir(), "my_head_image.jpg")換成File outputImage = new File(Environment.getExternalStorageDirectory(), "my_head_image.jpg");同理更改File cropImage = new File(Environment.getExternalStorageDirectory(), "crop_image.jpg");
step2:在manifest.xml中註冊權限:<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
step3:進行到這裏,我運行了一次,還是異常,應該還是權限問題沒有處理完,我在onCreate()方法裏添加了這樣一段:
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder(); StrictMode.setVmPolicy(builder.build()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { builder.detectFileUriExposure(); }
再運行,It works!
文末嘮叨
關於StrictMode我就不細講了(心累+懶)。
你懂得,在網上找解決bug的方法需要技巧、運氣、時間,兜了一大圈,對於我來說,我在網上找solution八成都會花掉大堆時間,很多問題那都是別人遇到的麻煩和解決方法,對自己不一定適用,但是自己還是得作死的去多嘗試,然後折騰一下午或者一晚上,心想著還不如在這個時間裏換種方式浪費生命,比如看劇、睡覺、和朋友閑聊、以及吃……
對於安卓開發來說,太久之前的solution可能並不適用於現在了,比如現在越來越嚴格的權限問題。
有時候,在網上瞎找,不如好好看書,搞清楚到底是怎麽一個流程,哪裏會出錯,反而會更快一些解決問題,也更有收獲。
總而言之,要高效率解決問題,還是得清楚整個代碼的流程。
好了,現在我要換種方式浪費生命了……
安卓開發——拍照、裁剪並保存為頭像報錯:裁剪圖片無法保存的