轉(zhuǎn)載自http://blog.coderclock.com/2016/05/22/android/你需要知道的Android拍照適配問題
說起調(diào)用系統(tǒng)相機來拍照的功能,大家肯定不陌生,現(xiàn)在所有應(yīng)用都具備這個功能。例如最基本的,用戶拍照上傳頭像。Android開發(fā)的孩紙都知道,碎片化給拍照這個功能的實現(xiàn)帶來挺多頭疼的問題。所以,我決定寫寫一些網(wǎng)上不多見但又經(jīng)常聽到童鞋們吐槽的問題。
拍照功能實現(xiàn)
Android 程序上實現(xiàn)拍照功能的方式分為兩種:第一種是利用相機的 API 來自定義相機,第二種是利用 Intent
調(diào)用系統(tǒng)指定的相機拍照。下面講的內(nèi)容都是針對第二種實現(xiàn)方式的適配。
通常情況下,我們調(diào)用拍照的業(yè)務(wù)場景是如下面這樣的:
1.A 界面,點擊按鈕調(diào)用相機拍照;
2.A 界面得到拍完照片,跳轉(zhuǎn)到 B 界面進(jìn)行預(yù)覽;
3.B 界面有個按鈕,點擊后觸發(fā)某個業(yè)務(wù)流程來處理這張照片;
實現(xiàn)的大體流程代碼如下:
//1、調(diào)用相機
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);//跳轉(zhuǎn)到預(yù)覽界面
}
finish();
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
//3、各種各樣處理這張圖片的業(yè)務(wù)代碼
到這里基本科普完了如何調(diào)用系統(tǒng)相機拍照,相信這些網(wǎng)上一搜一大把的代碼,很多童鞋都能看懂。
有沒有相機可用?
前面講到我們是調(diào)用系統(tǒng)指定的相機app來拍照,那么系統(tǒng)是否存在可以被我們調(diào)用的app呢?這個我們不敢確定,畢竟 Android 奇葩問題多,還真有遇到過這種極端的情況導(dǎo)致閃退的。雖然很極端,但作為客戶端人員還是要進(jìn)行處理,方式有二:
1.調(diào)用相機時,簡單粗暴的 try-catch
2.調(diào)用相機前,檢測系統(tǒng)有沒有相機 app 可用
try-catch 這種粗暴的方式大家肯定很熟悉了,那么要如何檢測系統(tǒng)有沒有相機 app 可用呢?系統(tǒng)在 PackageManager
里為我們提供這樣一個 API
通過這樣一個 API ,可以知道系統(tǒng)是否存在 action
為 MediaStore.ACTION_IMAGE_CAPTURE
的 intent
可以喚起的拍照界面,具體實現(xiàn)代碼如下:
/**
* 判斷系統(tǒng)中是否存在可以啟動的相機應(yīng)用
*
* @return 存在返回true,不存在返回false
*/
public boolean hasCamera() {
PackageManager packageManager = mActivity.getPackageManager();
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
List<ResolveInfo> list = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
return list.size() > 0;
}
拍出來的照片“歪了”!!!
經(jīng)常會遇到一種情況,拍照時看到照片是正的,但是當(dāng)我們的 app 獲取到這張照片時,卻發(fā)現(xiàn)旋轉(zhuǎn)了 90 度(也有可能是180、270,不過90度比較多見,貌似都是由于手機傳感器導(dǎo)致的)。很多童鞋對此感到很困擾,因為不是所有手機都會出現(xiàn)這種情況,就算會是出現(xiàn)這種情況的手機上,也并非每次必現(xiàn)。要怎么解決這個問題呢?從解決的思路上看,只要獲取到照片旋轉(zhuǎn)的角度,利用 Matrix 來進(jìn)行角度糾正即可。那么問題來了,要怎么知道照片旋轉(zhuǎn)的角度呢?細(xì)心的童鞋可能會發(fā)現(xiàn),拍完一張照片去到相冊點擊屬性查看,能看到下面這樣一堆關(guān)于照片的屬性數(shù)據(jù)
沒錯,這里面就有一個旋轉(zhuǎn)角度,倘若拍照后保存的成像照片文件發(fā)生了角度旋轉(zhuǎn),這個圖片的屬性參數(shù)就能告訴我們到底旋轉(zhuǎn)了多少度。只要獲取到這個角度值,我們就能進(jìn)行糾正的工作了。 Android 系統(tǒng)提供了
ExifInterface
類來滿足獲取圖片各個屬性的操作通過
ExifInterface
類拿到 TAG_ORIENTATION
屬性對應(yīng)的值,即為我們想要得到旋轉(zhuǎn)角度。再根據(jù)利用 Matrix
進(jìn)行旋轉(zhuǎn)糾正即可。實現(xiàn)代碼大致如下:/**
* 獲取圖片的旋轉(zhuǎn)角度
*
* @param path 圖片絕對路徑
* @return 圖片的旋轉(zhuǎn)角度
/
public static int getBitmapDegree(String path) {
int degree = 0;
try {
// 從指定路徑下讀取圖片,并獲取其EXIF信息
ExifInterface exifInterface = new ExifInterface(path);
// 獲取圖片的旋轉(zhuǎn)信息
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;
}
/*
* 將圖片按照指定的角度進(jìn)行旋轉(zhuǎn)
*
* @param bitmap 需要旋轉(zhuǎn)的圖片
* @param degree 指定的旋轉(zhuǎn)角度
* @return 旋轉(zhuǎn)后的圖片
*/
public static Bitmap rotateBitmapByDegree(Bitmap bitmap, int degree) {
// 根據(jù)旋轉(zhuǎn)角度,生成旋轉(zhuǎn)矩陣
Matrix matrix = new Matrix();
matrix.postRotate(degree);
// 將原始圖片按照旋轉(zhuǎn)矩陣進(jìn)行旋轉(zhuǎn),并得到新的圖片
Bitmap newBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
return newBitmap;
}
ExifInterface
能拿到的信息遠(yuǎn)遠(yuǎn)不止旋轉(zhuǎn)角度,其他的參數(shù)感興趣的童鞋可以看看 API 文檔。
拍完照怎么閃退了?
曾在小米和魅族的某些機型上遇到過這樣的問題,調(diào)用系統(tǒng)相機拍照,拍完點擊確定回到自己的app里面卻莫名奇妙的閃退了。這種閃退有兩個特點:
1.沒有什么錯誤日志(有些機子啥日志都沒有,有些機子會出來個空異常錯誤日志);
2.同個機子上非必現(xiàn)(有時候怎么拍都不閃退,有時候一拍就閃退);
對待非必現(xiàn)問題往往比較頭疼,當(dāng)初遇到這樣的問題也是非常不解。上網(wǎng)搜羅了一圈也沒方案,后來留意到一個比較有意思信息:有些系統(tǒng)廠商的 ROM 會給自帶相機應(yīng)用做優(yōu)化,當(dāng)某個 app 通過 intent
進(jìn)入相機拍照界面時,系統(tǒng)會把這個 app 當(dāng)前最上層的 Activity
銷毀回收。(注意:我遇到的情況是有時候很快就回收掉,有時候怎么等也不回收,沒有什么必現(xiàn)規(guī)律)為了驗證一下,便在啟動相機的 Activity
中對 onDestory
方法進(jìn)行加 log 。果不其然,終于發(fā)現(xiàn)進(jìn)入拍照界面的時候 onDestory
方法被執(zhí)行了。所以,前面提到的閃退基本可以推測是 Activity
被回收導(dǎo)致某些非UI控件的成員變量為空導(dǎo)致的。(有些機子會報出空異常錯誤日志,但是有些機子閃退了什么都不報,是不是覺得很奇葩!)
既然涉及到 Activity
被回收的問題,自然要想起 onSaveInstanceState
和 onRestoreInstanceState
這對方法。去到 onSaveInstanceState
把數(shù)據(jù)保存,并在 onRestoreInstanceState
方法中進(jìn)行恢復(fù)即可。大體代碼思路如下:
@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
方法的作用還不熟悉的童鞋,網(wǎng)上資料很多,可以自行搜索。
到這里,可能有童鞋要問,這種閃退并不能保證復(fù)現(xiàn),我要怎么知道問題所在和是否修復(fù)了呢?我們可以去到開發(fā)者選項里開啟不保留活動這一項進(jìn)行調(diào)試驗證
它作用是保留當(dāng)前和用戶接觸的 Activity
,并將目前無法和用戶交互 Activity
進(jìn)行銷毀回收。打開這個調(diào)試選項就可以滿足驗證的需求,當(dāng)你的 app 的某個 Activity
跳轉(zhuǎn)到拍照的 Activity
后,這個 Activity
立馬就會被系統(tǒng)銷毀回收,這樣就可以很好的完全復(fù)現(xiàn)閃退的場景,幫助開發(fā)者確認(rèn)問題有沒有修復(fù)了。
涉及到 Activity
被銷毀,還想提一下代碼實現(xiàn)上的問題。假設(shè)當(dāng)前有兩個 Activity
,MainActivity
中有個 Button
,點擊可以調(diào)用系統(tǒng)相機拍照并顯示到 PreviewActivity
進(jìn)行預(yù)覽。有下面兩種實現(xiàn)方案:
● 方案一:MainActivity
中點擊 Button
后,啟動系統(tǒng)相機拍照,并在 MainActivity
的 onActivityResult
方法中獲取拍下來的照片,并啟動跳轉(zhuǎn)到 PreviewActivity
界面進(jìn)行效果預(yù)覽;
● 方案二:MainActivity
中點擊 Button
后,啟動 PreviewActivity
界面,在 PreviewActivity
的 onCreate
(或者onStart
、onResume
)方法中啟動系統(tǒng)相機拍照,然后在 PreviewActivity
的 onActivityResult
方法中獲取拍下來的照片進(jìn)行預(yù)覽;
上面兩種方案得到的實現(xiàn)效果是一模一樣的,但是第二種方案卻存在很大的問題。因為啟動相機的代碼放在 onCreate
(或者onStart
、onResume
)中,當(dāng)進(jìn)入拍照界面后,PreviewActivity
隨即被銷毀,拍完照確認(rèn)后回到 PreviewActivity
時,被銷毀的 PreviewActivity
需要重建,又要走一遍 onCreate
、onStart
、onResume
,又調(diào)用了啟動相機拍照的代碼,周而復(fù)始的進(jìn)入了死循環(huán)狀態(tài)。為了避免讓你的用戶抓狂,果斷明智的選擇方案一。
以上這種情況提到調(diào)用系統(tǒng)拍照時,Activity
就回收的情況,在小米4S和小米4 LTE機子上(MIUI的版本是7.3,Android系統(tǒng)版本是6.0)出現(xiàn)的概率很高。 所以,建議看到此文的童鞋也可以去驗證適配一下。
圖片無法顯示
圖片無法顯示這個問題也是略坑,如何坑法?往下看,同樣是在小米4S和小米4 LTE機子上(MIUI的版本是7.3,Android系統(tǒng)版本是6.0)出現(xiàn)概率很高的場景(當(dāng)然,不保證其他機子沒出現(xiàn)過)。按照我們前面提到的業(yè)務(wù)場景,調(diào)用相機拍照完成后,我們的 app 會有一個預(yù)覽圖片的界面。但是在用了小米的機子進(jìn)行拍照后,自己 app 的預(yù)覽界面卻怎么也無法顯示出照片來,同樣是相當(dāng)郁悶,郁悶完后還是要一步一步去排查解決問題的!為此,需要一步一步猜測驗證問題所在。
● 猜測一:沒有拿到照片路徑,所以無法顯示?
直接斷點打 log 跟蹤,猜測一很快被推翻,路徑是有的。
● 猜測二:Bitmap
太大了,無法顯示?
直接在 AS 的 log 控制臺仔細(xì)的觀察了一下系統(tǒng) log ,發(fā)現(xiàn)了一些蛛絲馬跡
OpenGLRenderer: Bitmap too large to be uploaded into a texture
每次拍完照片,都會出現(xiàn)上面這樣的 log ,果然,因為圖片太大而導(dǎo)致在 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;//不加載圖片到內(nèi)存,僅獲得圖片寬高
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
確確實實經(jīng)過處理,那為什么圖片還是太大而顯示不出來呢? requestWidth
、int requestHeight
設(shè)置得太大導(dǎo)致 inSampleSize
太小了?不可能啊,我都試著把長寬都設(shè)置成 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;//不加載圖片到內(nèi)存,僅獲得圖片寬高
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
經(jīng)過處理之后結(jié)果還是 1 。狠狠的吐槽了之后,總是要回來解決問題的。那么,圖片的寬高信息都丟失了,我去哪里找啊? 像下面這樣?
public static Bitmap decodeBitmapFromFile(String imagePath, int requestWidth, int requestHeight) {
...
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;//不加載圖片到內(nèi)存,僅獲得圖片寬高
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
。改良過后的代碼實現(xiàn)如下:
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;//不加載圖片到內(nèi)存,僅獲得圖片寬高
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
這樣就可以解決問題啦。
總結(jié)
以上總結(jié)了這么些身邊童鞋經(jīng)常問起,但網(wǎng)上又不多見的適配問題,希望可以幫到一些開發(fā)童鞋少走彎路。文中多次提到小米的機子,并不代表只有MIUI上有這樣的問題存在,僅僅只是因為我身邊帶的幾部機子大都是小米的。對待適配問題,在搜索引擎都無法提供多少有效的信息時,我們只能靠斷點、打log、觀察控制臺的日志、以及API文檔來尋找一些蛛絲馬跡作為突破口,相信辦法總比困難多。
以上的示例代碼已經(jīng)整理到:https://github.com/D-clock/AndroidStudyCode ,主要的代碼在下面紅圈部分
感興趣的童鞋可以自行查看!如有錯誤,歡迎大家指正!