拍照功能實現git
Android 程序上實現拍照功能的方式分爲兩種:第一種是利用相機的 API 來自定義相機,第二種是利用 Intent 調用系統指定的相機拍照。下面講的內容都是針對第二種實現方式的適配。github
一般狀況下,咱們調用拍照的業務場景是以下面這樣的:app
A 界面,點擊按鈕調用相機拍照; A 界面獲得拍完照片,跳轉到 B 界面進行預覽; B 界面有個按鈕,點擊後觸發某個業務流程來處理這張照片; 實現的大致流程代碼以下:ide
//一、調用相機 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); //二、拿到照片 @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); } } //三、各類各樣處理這張圖片的業務代碼
到這裏基本科普完了如何調用系統相機拍照,相信這些網上一搜一大把的代碼,不少童鞋都能看懂。post
拍出來的照片「歪了」!!!優化
常常會遇到一種狀況,拍照時看到照片是正的,可是當咱們的 app 獲取到這張照片時,卻發現旋轉了 90 度(也有多是180、270,不過90度比較多見,貌似都是因爲手機傳感器致使的)。不少童鞋對此感到很困擾,由於不是全部手機都會出現這種狀況,就算會是出現這種狀況的手機上,也並不是每次必現。要怎麼解決這個問題呢?從解決的思路上看,只要獲取到照片旋轉的角度,利用 Matrix 來進行角度糾正便可。那麼問題來了,要怎麼知道照片旋轉的角度呢?細心的童鞋可能會發現,拍完一張照片去到相冊點擊屬性查看,能看到下面這樣一堆關於照片的屬性數據this
沒錯,這裏面就有一個旋轉角度,假若拍照後保存的成像照片文件發生了角度旋轉,這個圖片的屬性參數就能告訴咱們到底旋轉了多少度。只要獲取到這個角度值,咱們就能進行糾正的工做了。 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裏面卻莫名奇妙的閃退了。這種閃退有兩個特色:
沒有什麼錯誤日誌(有些機子啥日誌都沒有,有些機子會出來個空異常錯誤日誌); 同個機子上非必現(有時候怎麼拍都不閃退,有時候一拍就閃退); 對待非必現問題每每比較頭疼,當初遇到這樣的問題也是很是不解。上網蒐羅了一圈也沒方案,後來留意到一個比較有意思信息:有些系統廠商的 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 <= 0 || requestHeight <= 0) { Bitmap bitmap = BitmapFactory.decodeFile(imagePath); return bitmap; } BitmapFactory.Options options = new BitmapFactory.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.Options options, int reqWidth, int reqHeight) { final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; Log.i(TAG, "height: " + height); Log.i(TAG, "width: " + width); if (height > 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 <= 0 || requestHeight <= 0) { Bitmap bitmap = BitmapFactory.decodeFile(imagePath); return bitmap; } BitmapFactory.Options options = new BitmapFactory.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(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 再次幫了咱們大忙,經過它的下面兩個屬性便可拿到圖片真正的寬高
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 <= 0 || requestHeight <= 0) { Bitmap bitmap = BitmapFactory.decodeFile(imagePath); return bitmap; } BitmapFactory.Options options = new BitmapFactory.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 { ExifInterface exifInterface = new ExifInterface(imagePath); int height = exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, ExifInterface.ORIENTATION_NORMAL);//獲取圖片的高度 int width = 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 (IOException e) { 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