Android拍照適配方案,實用干貨

說起調用系統相機來拍照的功能,大家肯定不陌生,現在所有應用都具備這個功能。例如最基本的,用戶拍照上傳頭像。Android開發的孩紙都知道,碎片化給拍照這個功能的實現帶來挺多頭疼的問題。所以,我決定寫寫一些網上不多見但又經常聽到童鞋們吐槽的問題。
Android 程序上實現拍照功能的方式分為兩種:第一種是利用相機的 API 來自定義相機,第二種是利用 Intent 調用系統指定的相機拍照。下面講的內容都是針對第二種實現方式的適配。
通常情況下,我們調用拍照的業務場景是如下面這樣的:
A 界面,點擊按鈕調用相機拍照;
A 界面得到拍完照片,跳轉到 B 界面進行預覽;
B 界面有個按鈕,點擊后觸發某個業務流程來處理這張照片;
實現的大體流程代碼如下:

//1、調用相機
FilemPhotoFile=newFile(folder,filename);
IntentcaptureIntent=newIntent(MediaStore.ACTION_IMAGE_CAPTURE);
UrifileUri=Uri.fromFile(mPhotoFile);
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT,fileUri);
mActivity.startActivityForResult(captureIntent,CAPTURE_PHOTO_REQUEST_CODE);

//2、拿到照片
@Override
protected void onActivityResult(intrequestCode,intresultCode,Intentdata {
   if(requestCode==CapturePhotoHelper.CAPTURE_PHOTO_REQUEST_CODE
         &&resultCode==RESULT_OK){
  FilephotoFile=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() {
    PackageManagerpackageManager=mActivity.getPackageManager();
    Intentintent=newIntent(MediaStore.ACTION_IMAGE_CAPTURE);
    Listlist=packageManager.queryIntentActivities(intent,PackageManager.MATCH_DEFAULT_ONLY);
    returnlist.size()>0;
}

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

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

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


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

/**
* 獲取圖片的旋轉角度
*
* @param path 圖片絕對路徑
* @return 圖片的旋轉角度
*/
public static int getBitmapDegree(Stringpath){
  intdegree=0;
try{
    // 從指定路徑下讀取圖片,并獲取其EXIF信息
    ExifInterfaceexifInterface=newExifInterface(path);
    // 獲取圖片的旋轉信息
    intorientation=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(IOExceptione) {
        e.printStackTrace();
   }
 return degree;
}

/**
* 將圖片按照指定的角度進行旋轉
*
* @param bitmap 需要旋轉的圖片
* @param degree 指定的旋轉角度
* @return 旋轉后的圖片
*/public static BitmaprotateBitmapByDegree(Bitmapbitmap,intdegree) {
    // 根據旋轉角度,生成旋轉矩陣
    Matrixmatrix=newMatrix();matrix.postRotate(degree);
    // 將原始圖片按照旋轉矩陣進行旋轉,并得到新的圖片     
    BitmapnewBitmap=Bitmap.createBitmap(bitmap,0,0,bitmap.getWidth(),bitmap.getHeight(),matrix,true);
    if(bitmap!=null&&!bitmap.isRecycled()){
       bitmap.recycle();
      }
    return newBitmap;
  }

ExifInterface 能拿到的信息遠遠不止旋轉角度,其他的參數感興趣的童鞋可以看看 API 文檔。

拍完照怎么閃退了?

曾在小米和魅族的某些機型上遇到過這樣的問題,調用系統相機拍照,拍完點擊確定回到自己的app里面卻莫名奇妙的閃退了。這種閃退有兩個特點:
沒有什么錯誤日志(有些機子啥日志都沒有,有些機子會出來個空異常錯誤日志);
同個機子上非必現(有時候怎么拍都不閃退,有時候一拍就閃退);
對待非必現問題往往比較頭疼,當初遇到這樣的問題也是非常不解。上網搜羅了一圈也沒方案,后來留意到一個比較有意思信息:有些系統廠商的 ROM 會給自帶相機應用做優化,當某個 app 通過 intent 進入相機拍照界面時,系統會把這個 app 當前最上層的 Activity 銷毀回收。(注意:我遇到的情況是有時候很快就回收掉,有時候怎么等也不回收,沒有什么必現規律)為了驗證一下,便在啟動相機的 Activity 中對 onDestory 方法進行加 log 。果不其然,終于發現進入拍照界面的時候 onDestory 方法被執行了。所以,前面提到的閃退基本可以推測是 Activity 被回收導致某些非UI控件的成員變量為空導致的。(有些機子會報出空異常錯誤日志,但是有些機子閃退了什么都不報,是不是覺得很奇葩!)
既然涉及到 Activity 被回收的問題,自然要想起 onSaveInstanceState 和 onRestoreInstanceState 這對方法。去到 onSaveInstanceState 把數據保存,并在 onRestoreInstanceState 方法中進行恢復即可。大體代碼思路如下:

@Override
protected void onSaveInstanceState(BundleoutState){
    super.onSaveInstanceState(outState);
    mRestorePhotoFile=mCapturePhotoHelper.getPhoto();
    if(mRestorePhotoFile!=null){
    outState.putSerializable(EXTRA_RESTORE_PHOTO,mRestorePhotoFile);
   }
}

@Override 
protected void onRestoreInstanceState(BundlesavedInstanceState){
    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 BitmapdecodeBitmapFromFile(StringimagePath,intrequestWidth,intrequestHeight){
  if(!TextUtils.isEmpty(imagePath)){
    if(requestWidth<=0||requestHeight<=0){
      Bitmapbitmap=BitmapFactory.decodeFile(imagePath);
      return bitmap;
    }
    BitmapFactory.Optionsoptions=newBitmapFactory.Options();
    options.inJustDecodeBounds=true;//不加載圖片到內存,僅獲得圖片寬高  
    BitmapFactory.decodeFile(imagePath,options);
    options.inSampleSize=calculateInSampleSize(options,requestWidth,requestHeight);//計算獲取新的采樣率
    options.inJustDecodeBounds=false;
    return BitmapFactory.decodeFile(imagePath,options);
 }else{
    return null;
  }
}

public static int calculateInSampleSize(BitmapFactory.Optionsoptions,intreqWidth,intreqHeight){
  final int height=options.outHeight;
  final int width=options.outWidth;
  intinSampleSize=1;
  Log.i(TAG,"height: "+height);
  Log.i(TAG,"width: "+width);
  if(height>reqHeight||width>reqWidth){
      finalinthalfHeight=height/2;
      finalinthalfWidth=width/2;
      while((halfHeight/inSampleSize)>reqHeight&&(halfWidth/inSampleSize)>reqWidth){
          inSampleSize*=2;
      }
      longtotalPixels=width*height/inSampleSize;
      finallongtotalReqPixelsCap=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 BitmapdecodeBitmapFromFile(StringimagePath,intrequestWidth,intrequestHeight){
  if(!TextUtils.isEmpty(imagePath)){
      Log.i(TAG,"requestWidth: "+requestWidth);
      Log.i(TAG,"requestHeight: "+requestHeight);
      if(requestWidth<=0||requestHeight<=0){
          Bitmapbitmap=BitmapFactory.decodeFile(imagePath);
          return bitmap;
       }
      BitmapFactory.Optionsoptions=newBitmapFactory.Options();
      options.inJustDecodeBounds=true;//不加載圖片到內存,僅獲得圖片寬高    
      BitmapFactory.decodeFile(imagePath,options);
      Log.i(TAG,"original height: "+options.outHeight);
      Log.i(TAG,"original width: "+options.outWidth);
      options.inSampleSize=calculateInSampleSize(options,requestWidth,requestHeight);//計算獲取新的采樣率
      Log.i(TAG,"inSampleSize: "+options.inSampleSize);
      options.inJustDecodeBounds=false;
      return BitmapFactory.decodeFile(imagePath,options);
  }else{
      return null;
  }
}

運行打印出來的日志如下:

圖片原來的寬高居然都是 -1 ,真是奇葩了!難怪,inSampleSize 經過處理之后結果還是 1 。狠狠的吐槽了之后,總是要回來解決問題的。那么,圖片的寬高信息都丟失了,我去哪里找啊? 像下面這樣?

public static Bitmap decodeBitmapFromFile(StringimagePath,intrequestWidth,intrequestHeight){
    ...
    BitmapFactory.Optionsoptions=newBitmapFactory.Options();
    options.inJustDecodeBounds=true;//不加載圖片到內存,僅獲得圖片寬高
    Bitmapbitmap=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 BitmapdecodeBitmapFromFile(StringimagePath,intrequestWidth,intrequestHeight){
    if(!TextUtils.isEmpty(imagePath)){
        Log.i(TAG,"requestWidth: "+requestWidth);
        Log.i(TAG,"requestHeight: "+requestHeight);
        if(requestWidth<=0||requestHeight<=0){
            Bitmapbitmap=BitmapFactory.decodeFile(imagePath);
            return bitmap;
        }
        BitmapFactory.Optionsoptions=newBitmapFactory.Options();
        options.inJustDecodeBounds=true;//不加載圖片到內存,僅獲得圖片寬高          
        BitmapFactory.decodeFile(imagePath,options);
        Log.i(TAG,"original height: "+options.outHeight);
        Log.i(TAG,"original width: "+options.outWidth);
        if(options.outHeight==-1||options.outWidth==-1){
            try{
                ExifInterfaceexifInterface=newExifInterface(imagePath);
                intheight=exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH,
                          ExifInterface.ORIENTATION_NORMAL);//獲取圖片的高度
                intwidth=exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH,
                          ExifInterface.ORIENTATION_NORMAL);//獲取圖片的寬度
                Log.i(TAG,"exif height: "+height);Log.i(TAG,"exif width: "+width);
                options.outWidth=width;
                options.outHeight=height;
          }catch(IOExceptione){
                e.printStackTrace();
        }
    }
        options.inSampleSize=calculateInSampleSize(options,requestWidth,requestHeight);//計算獲取新的采樣率
        Log.i(TAG,"inSampleSize: "+options.inSampleSize);
        options.inJustDecodeBounds=false;
        return BitmapFactory.decodeFile(imagePath,options);
    }else{
        return null;
    }
}

再看一下,打印出來的log

這樣就可以解決問題啦。

總結

以上總結了這么些身邊童鞋經常問起,但網上又不多見的適配問題,希望可以幫到一些開發童鞋少走彎路。文中多次提到小米的機子,并不代表只有MIUI上有這樣的問題存在,僅僅只是因為我身邊帶的幾部機子大都是小米的。對待適配問題,在搜索引擎都無法提供多少有效的信息時,我們只能靠斷點、打log、觀察控制臺的日志、以及API文檔來尋找一些蛛絲馬跡作為突破口,相信辦法總比困難多。
以上的示例代碼已經整理到:https://github.com/D-clock/AndroidStudyCode ,主要的代碼在下面紅圈部分

感興趣的童鞋可以自行查看!如有錯誤,歡迎大家指正!
文章轉自 :http://diycode.cc/topics/101

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。