1. 程式人生 > >android 拍照遇到圖片旋轉,照片、相機未找到的問題解決

android 拍照遇到圖片旋轉,照片、相機未找到的問題解決

寫在前面:android手機廠商眾多,在開發的時候,相機拍攝程式碼的問題也層出不窮,雖然很多的utils或者jar包能幫我們解決這些問題,但我們沒必要因為一個小的問題依賴別人龐大的專案包。做一個理性的碼農。

拍照功能實現

Android 程式上實現拍照功能的方式分為兩種:第一種是利用相機的 API 來自定義相機,第二種是利用 Intent 呼叫系統指定的相機拍照。下面講的內容都是針對第二種實現方式的適配。

通常情況下,我們呼叫拍照的業務場景是如下面這樣的:

  1. A 介面,點選按鈕呼叫相機拍照;
  2. A 介面得到拍完照片,跳轉到 B 介面進行預覽;
  3. B 介面有個按鈕,點選後觸發某個業務流程來處理這張照片;

實現的大體流程程式碼如下:

    //1、呼叫相機
    File mPhotoFile = new File(folder,filename);
    Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    Uri fileUri = Uri.fromFile(mPhotoFile);
    captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
    mActivity.startActivityForResult(captureIntent, CAPTURE_PHOTO_REQUEST_CODE);
 
    //2、拿到照片
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == CapturePhotoHelper.CAPTURE_PHOTO_REQUEST_CODE & resultCode == RESULT_OK) {
            File photoFile = mCapturePhotoHelper.getPhoto();//獲取拍完的照片
            if (photoFile != null) {
                PhotoPreviewActivity.preview(this, photoFile);//跳轉到預覽介面
            }
            finish();
        } else {
            super.onActivityResult(requestCode, resultCode, data);
        }
    }
 
    //3、各種各樣處理這張圖片的業務程式碼

到這裡基本科普完了如何呼叫系統相機拍照,相信這些網上一搜一大把的程式碼,很多童鞋都能看懂。

有沒有相機可用?

前面講到我們是呼叫系統指定的相機app來拍照,那麼系統是否存在可以被我們呼叫的app呢?這個我們不敢確定,畢竟 Android 奇葩問題多,還真有遇到過這種極端的情況導致閃退的。雖然很極端,但作為客戶端人員還是要進行處理,方式有二:

  1. 呼叫相機時,簡單粗暴的 try-catch
  2. 呼叫相機前,檢測系統有沒有相機 app 可用

try-catch 這種粗暴的方式大家肯定很熟悉了,那麼要如何檢測系統有沒有相機 app 可用呢?系統在 PackageManager 裡為我們提供這樣一個 API

通過這樣一個 API ,可以知道系統是否存在 action 為 MediaStore.ACTION_IMAGE_CAPTURE 的 intent 可以喚起的拍照介面,具體實現程式碼如下:

  /**
     * 判斷系統中是否存在可以啟動的相機應用
     *
     * @return 存在返回true,不存在返回false
     */
    public boolean hasCamera() {
        PackageManager packageManager = mActivity.getPackageManager();
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        List list = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
        return list.size() > 0;
    }

拍出來的照片“歪了”!!!

經常會遇到一種情況,拍照時看到照片是正的,但是當我們的 app 獲取到這張照片時,卻發現旋轉了 90 度(也有可能是180、270,不過90度比較多見,貌似都是由於手機感測器導致的)。很多童鞋對此感到很困擾,因為不是所有手機都會出現這種情況,就算會是出現這種情況的手機上,也並非每次必現。要怎麼解決這個問題呢?從解決的思路上看,只要獲取到照片旋轉的角度,利用 Matrix 來進行角度糾正即可。那麼問題來了,要怎麼知道照片旋轉的角度呢?細心的童鞋可能會發現,拍完一張照片去到相簿點選屬性檢視,能看到下面這樣一堆關於照片的屬性資料


這裡面就有一個旋轉角度,倘若拍照後儲存的成像照片檔案發生了角度旋轉,這個圖片的屬性引數就能告訴我們到底旋轉了多少度。只要獲取到這個角度值,我們就能進行糾正的工作了。 Android 系統提供了 ExifInterface 類來滿足獲取圖片各個屬性的操作


通過 ExifInterface 類拿到 TAG_ORIENTATION 屬性對應的值,即為我們想要得到旋轉角度。再根據利用 Matrix 進行旋轉糾正即可。實現程式碼大致如下:

   /**
     * 獲取圖片的旋轉角度
     *
     * @param path 圖片絕對路徑
     * @return 圖片的旋轉角度
     */
    public static int getBitmapDegree(String path) {
        int degree = 0;
        try {
            // 從指定路徑下讀取圖片,並獲取其EXIF資訊
            ExifInterface exifInterface = new ExifInterface(path);
            // 獲取圖片的旋轉資訊
            int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
            switch (orientation) {
                case ExifInterface.ORIENTATION_ROTATE_90:
                    degree = 90;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_180:
                    degree = 180;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_270:
                    degree = 270;
                    break;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return degree;
    }
 
    /**
     * 將圖片按照指定的角度進行旋轉
     *
     * @param bitmap 需要旋轉的圖片
     * @param degree 指定的旋轉角度
     * @return 旋轉後的圖片
     */
    public static Bitmap rotateBitmapByDegree(Bitmap bitmap, int degree) {
        // 根據旋轉角度,生成旋轉矩陣
        Matrix matrix = new Matrix();
        matrix.postRotate(degree);
        // 將原始圖片按照旋轉矩陣進行旋轉,並得到新的圖片
        Bitmap newBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
        if (bitmap != null & !bitmap.isRecycled()) {
            bitmap.recycle();
        }
        return newBitmap;
    }

ExifInterface 能拿到的資訊遠遠不止旋轉角度,其他的引數感興趣的童鞋可以看看 API 文件。

拍完照怎麼閃退了?

曾在小米和魅族的某些機型上遇到過這樣的問題,呼叫系統相機拍照,拍完點選確定回到自己的app裡面卻莫名奇妙的閃退了。這種閃退有兩個特點:

  1. 沒有什麼錯誤日誌(有些機子啥日誌都沒有,有些機子會出來個空異常錯誤日誌);
  2. 同個機子上非必現(有時候怎麼拍都不閃退,有時候一拍就閃退);

對待非必現問題往往比較頭疼,當初遇到這樣的問題也是非常不解。上網蒐羅了一圈也沒方案,後來留意到一個比較有意思資訊:有些系統廠商的 ROM 會給自帶相機應用做優化,當某個 app 通過 intent 進入相機拍照介面時,系統會把這個 app 當前最上層的 Activity 銷燬回收。(注意:我遇到的情況是有時候很快就回收掉,有時候怎麼等也不回收,沒有什麼必現規律)為了驗證一下,便在啟動相機的 Activity 中對 onDestory 方法進行加 log 。果不其然,終於發現進入拍照介面的時候 onDestory 方法被執行了。所以,前面提到的閃退基本可以推測是 Activity 被回收導致某些非UI控制元件的成員變數為空導致的。(有些機子會報出空異常錯誤日誌,但是有些機子閃退了什麼都不報,是不是覺得很奇葩!)

既然涉及到 Activity 被回收的問題,自然要想起 onSaveInstanceState 和 onRestoreInstanceState 這對方法。去到 onSaveInstanceState 把資料儲存,並在 onRestoreInstanceState 方法中進行恢復即可。大體程式碼思路如下:

   @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        mRestorePhotoFile = mCapturePhotoHelper.getPhoto();
        if (mRestorePhotoFile != null) {
            outState.putSerializable(EXTRA_RESTORE_PHOTO, mRestorePhotoFile);
        }
 
    }
 
    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        mRestorePhotoFile = (File) savedInstanceState.getSerializable(EXTRA_RESTORE_PHOTO);
        mCapturePhotoHelper.setPhoto(mRestorePhotoFile);
    }

對於 onSaveInstanceState 和 onRestoreInstanceState 方法的作用還不熟悉的童鞋,網上資料很多,可以自行搜尋。

到這裡,可能有童鞋要問,這種閃退並不能保證復現,我要怎麼知道問題所在和是否修復了呢?我們可以去到開發者選項裡開啟不保留活動這一項進行除錯驗證


它作用是保留當前和使用者接觸的 Activity ,並將目前無法和使用者互動 Activity 進行銷燬回收。開啟這個除錯選項就可以滿足驗證的需求,當你的 app 的某個 Activity 跳轉到拍照的 Activity 後,這個 Activity 立馬就會被系統銷燬回收,這樣就可以很好的完全復現閃退的場景,幫助開發者確認問題有沒有修復了。

涉及到 Activity 被銷燬,還想提一下程式碼實現上的問題。假設當前有兩個 Activity ,MainActivity 中有個 Button ,點選可以呼叫系統相機拍照並顯示到 PreviewActivity 進行預覽。有下面兩種實現方案:

  • 方案一:MainActivity 中點選 Button 後,啟動系統相機拍照,並在 MainActivity 的 onActivityResult 方法中獲取拍下來的照片,並啟動跳轉到 PreviewActivity 介面進行效果預覽;
  • 方案二:MainActivity 中點選 Button 後,啟動 PreviewActivity 介面,在 PreviewActivity 的 onCreate(或者onStart、onResume)方法中啟動系統相機拍照,然後在 PreviewActivity 的 onActivityResult 方法中獲取拍下來的照片進行預覽;

上面兩種方案得到的實現效果是一模一樣的,但是第二種方案卻存在很大的問題。因為啟動相機的程式碼放在 onCreate(或者onStart、onResume)中,當進入拍照介面後,PreviewActivity 隨即被銷燬,拍完照確認後回到 PreviewActivity 時,被銷燬的 PreviewActivity 需要重建,又要走一遍 onCreate、onStart、onResume,又呼叫了啟動相機拍照的程式碼,周而復始的進入了死迴圈狀態。為了避免讓你的使用者抓狂,果斷明智的選擇方案一。

以上這種情況提到呼叫系統拍照時,Activity就回收的情況,在小米4S小米4 LTE機子上(MIUI的版本是7.3,Android系統版本是6.0)出現的概率很高。 所以,建議看到此文的童鞋也可以去驗證適配一下。

圖片無法顯示

圖片無法顯示這個問題也是略坑,如何坑法?往下看,同樣是在小米4S小米4 LTE機子上(MIUI的版本是7.3,Android系統版本是6.0)出現概率很高的場景(當然,不保證其他機子沒出現過)。按照我們前面提到的業務場景,呼叫相機拍照完成後,我們的 app 會有一個預覽圖片的介面。但是在用了小米的機子進行拍照後,自己 app 的預覽介面卻怎麼也無法顯示出照片來,同樣是相當鬱悶,鬱悶完後還是要一步一步去排查解決問題的!為此,需要一步一步猜測驗證問題所在。

  • 猜測一:沒有拿到照片路徑,所以無法顯示?

直接斷點打 log 跟蹤,猜測一很快被推翻,路徑是有的。

  • 猜測二:Bitmap太大了,無法顯示?

直接在 AS 的 log 控制檯仔細的觀察了一下系統 log ,發現了一些蛛絲馬跡



OpenGLRenderer: Bitmap too large to be uploaded into a texture

每次拍完照片,都會出現上面這樣的 log ,果然,因為圖片太大而導致在 ImageView 上無法顯示。到這裡有童鞋要吐槽了,沒對圖片的取樣率inSampleSize 做處理?天地良心啊,絕對做處理了,直接看程式碼:

    /**
     * 壓縮Bitmap的大小
     *
     * @param imagePath     圖片檔案路徑
     * @param requestWidth  壓縮到想要的寬度
     * @param requestHeight 壓縮到想要的高度
     * @return
     */
    public static Bitmap decodeBitmapFromFile(String imagePath, int requestWidth, int requestHeight) {
        if (!TextUtils.isEmpty(imagePath)) {
            if (requestWidth  reqHeight || width > reqWidth) {

            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            while ((halfHeight / inSampleSize) > reqHeight & (halfWidth / inSampleSize) > reqWidth) {
                inSampleSize *= 2;
            }

            long totalPixels = width * height / inSampleSize;

            final long totalReqPixelsCap = reqWidth * reqHeight * 2;

            while (totalPixels > totalReqPixelsCap) {
                inSampleSize *= 2;
                totalPixels /= 2;
            }
        }
        return inSampleSize;
    }

瞄了程式碼後,是不是覺得沒有問題了?沒錯,inSampleSize 確確實實經過處理,那為什麼圖片還是太大而顯示不出來呢? requestWidth、int requestHeight 設定得太大導致 inSampleSize 太小了?不可能啊,我都試著把長寬都設定成 100 了還是沒法顯示!乾脆,直接列印 inSampleSize 值,一列印,inSampleSize 值居然為 1 。 我去,徹底打臉了,明明說好的處理過了,居然還是1 !!!!為了一探究竟,乾脆加 log 。

    public static Bitmap decodeBitmapFromFile(String imagePath, int requestWidth, int requestHeight) {
        if (!TextUtils.isEmpty(imagePath)) {
            Log.i(TAG, "requestWidth: " + requestWidth);
            Log.i(TAG, "requestHeight: " + requestHeight);
            if (requestWidth
執行打印出來的日誌圖片原來的寬高居然都是 -1 ,真是奇葩了!難怪,inSampleSize 經過處理之後結果還是 1 。狠狠的吐槽了之後,總是要回來解決問題的。那麼,圖片的寬高資訊都丟失了,我去哪裡找啊? 像下面這樣?
    public static Bitmap decodeBitmapFromFile(String imagePath, int requestWidth, int requestHeight) {
            ...
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;//不載入圖片到記憶體,僅獲得圖片寬高
            Bitmap bitmap = BitmapFactory.decodeFile(imagePath, options);
            bitmap.getWidth();
            bitmap.getHeight();
            ...
        } else {
            return null;
        }
    }
no,此方案行不通,inJustDecodeBounds = true 時,BitmapFactory 獲得 Bitmap 物件是 null;那要怎樣才能獲圖片的寬高呢?前面提到的 ExifInterface 再次幫了我們大忙,通過它的下面兩個屬性即可拿到圖片真正的寬高


順手吐槽一下,為什麼高不是 TAG_IMAGE_HEIGHT 而是 TAG_IMAGE_LENGTH。改良過後的程式碼實現如下:

  public static Bitmap decodeBitmapFromFile(String imagePath, int requestWidth, int requestHeight) {
        if (!TextUtils.isEmpty(imagePath)) {
            Log.i(TAG, "requestWidth: " + requestWidth);
            Log.i(TAG, "requestHeight: " + requestHeight);
            if (requestWidth

原文連結:點選開啟連結 示例程式碼已經整理到點選開啟連結