1. 程式人生 > >安卓開發——拍照、裁剪並保存為頭像報錯:裁剪圖片無法保存的

安卓開發——拍照、裁剪並保存為頭像報錯:裁剪圖片無法保存的

結果 pen strong ica toa car 生命 detection xposed

 

在做學校大創項目的安卓開發時,需要從相冊獲取圖片或者拍照,然後裁剪保存為頭像。由於我是第一次弄安卓開發,也對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可能並不適用於現在了,比如現在越來越嚴格的權限問題。

  有時候,在網上瞎找,不如好好看書,搞清楚到底是怎麽一個流程,哪裏會出錯,反而會更快一些解決問題,也更有收獲。

  總而言之,要高效率解決問題,還是得清楚整個代碼的流程。

  好了,現在我要換種方式浪費生命了……

 

技術分享

安卓開發——拍照、裁剪並保存為頭像報錯:裁剪圖片無法保存的