【相機】(1)——Intent調相機的2種方式以及那些你知道的和不知道的坑
要不要都行的開篇
隨著現代資訊量的瘋狂增長、資訊的快速交流,單純的文字資訊已經難以滿足日常、工作的溝通,一張圖片往往能達到一圖勝過千萬言的效果,前段時間不是盛行“有圖有真相的”說法;還有一些場景則需要通過上傳照片來驗證身份的合法性,比如手機銀行要求上傳身份證正反照,手持身份證照 ……廢話多了點,總之,作為移動端開發者的你,肯定會遇到這樣那樣拍照上傳的需求,接下來我們來試試 Intent 呼叫 Android 系統相機的2種方式,說說其中的貓膩。
Intent 呼叫系統相機的2種方式
Intent 作為 Android 四大元件之間的紐帶,可以在其間進行相互呼叫喚醒、傳輸資料。因此我們可以通過 Intent 呼叫系統的相機,系統已經為我們預留好了相機的 intent-action,最簡單的直接通過 Intent 隱式呼叫即可。
不指定儲存路徑的呼叫方式
最簡單的做法就是隻設定action,別的任何資訊都不指定:
// 不指定拍照儲存路徑的方式調起相機
Intent intent = new Intent();
intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(intent, REQUEST_CODE);
拍照完成後的返回處理:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (REQUEST_CAMERA == requestCode
&& RESULT_OK == resultCode) {
if (data != null) {
if (data.getExtras() != null) {
Log.e("WangJ", "result-Extras: " + data.getExtras());
imgThumbnail.setImageBitmap((Bitmap) data.getParcelableExtra("data" ));
} else {
Log.e("WangJ", "無intent extra返回");
}
Uri uri;
if ((uri = data.getData()) != null) {
Log.e("WangJ", "result-Uri: " + uri.toString());
tvFileUri.setText(uri.toString());
imgOriginal.setImageBitmap(getBitmapByUri(uri));
Log.e("WangJ", "按返回Uri重新整理相簿");
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(uri);
sendBroadcast(intent);
} else {
Log.e("WangJ", "無Uri返回");
}
} else {
Log.e("WangJ", "result為空!");
imgOriginal.setImageBitmap(getBitmapByUri(imageUri));
Log.e("WangJ", "按自定義Uri重新整理相簿");
/* 以下程式碼的作用是通知系統相簿重新整理目標位置,這樣即可在相簿中顯示該圖片,
* 如果不想在相簿中顯示,可以不傳送次廣播即可 */
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(imageUri);
sendBroadcast(intent);
}
}
}
private Bitmap getBitmapByUri(Uri picuUi) {
Bitmap bitmap = null;
try {
InputStream in = getContentResolver().openInputStream(picuUi);
bitmap = BitmapFactory.decodeStream(in);
Log.e("WangJ", "從流中獲取的原始大小: " + bitmap.getWidth() * bitmap.getHeight());
bitmap = ThumbnailUtils.extractThumbnail(bitmap, 800, 1280);
Log.e("WangJ", "壓縮後大小: " + bitmap.getWidth() * bitmap.getHeight());
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return bitmap;
}
我們開看看效果(別問我這裡的結果處理怎麼想到是怎麼寫的,結合前任的經驗一次次試過來的,如有不足,敬請指導)。眾所周知,Android 系統被各大廠商進行了深淺不一的定製,可能表現出不一樣的結果,這裡就看到很多前人的說法,現以手中現有的測試機型加以驗證:
小米4C的結果
環境 | 版本 |
---|---|
AndroidVersion | 4.4.4 KTU84P |
MIUIVersion | 6.2.1.0(KXDCNBK)穩定版 |
輸出日誌
有沒有發現日誌中顯示Bundle中有個大傢伙,size約90k,這就是我們上邊取出來的縮圖bitmap。
由此可見:在此裝置上,這種呼叫方式只返回一個縮圖,沒有別的資訊返回。並且我在手機中也未找到剛才拍攝的照片。
SAMSUNG Galaxy S5的結果
環境 | 版本 |
---|---|
AndroidVersion | 6.0.1 |
型號 | SM-G9008W |
輸出日誌(不要糾結上邊顯示編號86,怎麼日誌是90,因為錄屏的時候日式給跑過了,只好重新跑一個截個日誌):
由此可見:SAMSUNG Galaxy S5按此方式呼叫既返回了縮圖,也返回了所拍照片的儲存Uri,然後根據這個Uri能查到原圖,並且在相簿中也能找到這個圖。
HUAWEI Mate7的結果
環境 | 版本 |
---|---|
AndroidVersion | 6.0 |
型號 | HUAWEI MT7-TL10 |
EMUI | EMUI 4.0 |
執行結果和 MI 4C 結果一模一樣,不再浪費篇章。
結論
採用這種單純action呼叫相機而不指定其他資訊的方法在一些主流機器上並不可靠,即使能夠返回縮圖,但拿不到原圖,甚至在手機中也沒有記錄。
但是,Intent呼叫相機時還能指定目標影象的儲存路徑,看接下來的方法。
指定路徑的呼叫方式
直接上程式碼:
String filePath = Environment.getExternalStorageDirectory() + File.separator
+ Environment.DIRECTORY_PICTURES + File.separator;
String fileName = "IMG_"
+ DateFormat.format("yyyyMMdd_hhmmss", Calendar.getInstance(Locale.CHINA))
+ ".jpg";
imageUri = Uri.fromFile(new File(filePath + fileName));
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, REQUEST_CAMERA);
其餘程式碼不變。再看結果:
小米4C的結果
輸出日誌
由此可見:使用 Intent 呼叫相機時通過為Intent設定intent.putExtra(MediaStore.EXTRA_OUTPUT, ”路徑”);可以指定將拍照結果存至指定位置,拍照完成後 Intent 中不再返回任何資訊,但是 resultCode 明確顯示操作成功。此時我們可以根據之前指定的儲存位置重新整理相簿即可在相簿中顯示拍照結果,通過該Uri獲取原圖也沒有問題。
有圖有真相
SAMSUNG Galaxy S5的結果
日誌列印
由此可見:指定影象儲存位置時,SAMSUNG Galaxy S5和小米4C的表現一致。
HUAWEI Mate7的結果
和以上一致,不再贅述,截圖為證:
結論
綜合以上的驗證並且查驗大量前人的經驗,在使用 Intent 呼叫系統相機進行拍照時,推薦為目標影象指定儲存位置,這樣在主流機型上表現一致,並且這個 Uri 自己指定的,增加了更大的自定義性,變過過程也更加靈活。
另外,在拍照完成後,影象已存至指定位置,想不想在系統相簿中看到這張圖片,還不是你說了算?
一些坑和一些問題
許可權問題
這個坑沒啥歧義,但是要是不注意可能你連Demo都跑不起來:
- 呼叫相機需要相機許可權
- 自定義圖片儲存位置方式下用 Uri 取圖片需要讀取儲存的許可權
- 當你新建工程targetSdkVersion >= 23時,如果沒有支援動態許可權,預設許可權都是關閉的,為了跑Demo你需要手動去 ‘應用管理’>’許可權’ 中允許許可權。
Uri的表現形式
如果你在跑Demo時列印那些用到的 Uri,你肯定會發現類似這樣的問題:
- 我們自定義的目標Uri形如: file:///storage/emulated/0/Pictures/IMG_20170912_045146.jpg
- 系統返回的儲存Uri形如: content://media/external/images/media/98
其實他們只是表現形式不一樣而已,就像王老五的小名叫小五,王老五、小五都是指的同一個人。file:/// 開頭的Uri使用的是絕對路徑,而 content:// 開頭的是系統從內容提供器 ContentProvider 中拿到的標記。這2種形式不同的Uri都能被系統 順藤摸瓜 找出源資料。
相機拍照後點擊確認無法返回
可能出現拍照完成後點選“確認”或 對號 沒有響應,這是因為你指定的儲存 Uri 的問題。
這個是自己在嘗試將照片存入不同資料夾下發現的一個問題,如果你指定的儲存路徑Uri指向的資料夾不存在,那麼系統不會自動建立,除非你在程式碼中加以處理(比如判斷資料夾是否已經存在,否則新建)。這裡我建議存入系統常用資料夾下,比如Pictures、Camera、DCIM等目錄每個手機都是有的,99.99%不會出錯。
SAMSUNG手機相機的問題
(宣告:這個問題我沒有遇到,但是作為可能的問題借鑑前人的經驗先記下來)
Samsung的系統相機,版式是橫板的,如果你的activity恰巧是豎版的,那麼獲取這個回撥uri的時候,很可能為空!原因在於,如果你沒有設定版式改變的時候,activity不要呼叫onCreate方法!這就是要命的地方!
1.解決方法其實很簡單:
在Manfest.xml中,給activity新增一個屬性:
android:configChanges="orientation|keyboardHidden"
2.在activity中新增:
@Override
public void onConfigurationChanged(Configuration config) {
super.onConfigurationChanged(config);
}
相機重力感應自動旋轉
如上圖,某些相機本身帶有重力感應,拍照時會隨著手機姿態進行調整,導致照片會有對應的旋轉,此時拿到的照片可能和期望值之間有一個旋轉角度(90度、180度、270度)。此時可以
ExifInterface exifInterface = new ExifInterface(圖片路徑);
// 獲取圖片的旋轉資訊
int orientation = exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
...(省略)
// orientation的值如下類似如下
public static final int ORIENTATION_ROTATE_180 = 3;
public static final int ORIENTATION_ROTATE_270 = 8;
public static final int ORIENTATION_ROTATE_90 = 6;
... 還有更多
獲取到圖片的旋轉資訊後,根據需要再利用旋轉矩陣將圖還原即可。
Activity的載入模式問題
Activity 的載入模式會影響 Activity 之間 Intent 傳遞資料的通路。舉個例子:
以下為驗證結果:
Activity A載入模式 | Activity B載入模式 | 現象 |
---|---|---|
singleInstance | 任意 | 失敗 |
任意 | singleInstance | 失敗 |
任意 | singleTask | 失敗 |
總之: 被請求的 Activity 不能使用 singleInstance、singleTask;發起請求的Activity不能使用 singleInstance,要慎用singleTask。
(提示:如果你執行Demo中的Intent傳遞資料部分,不僅注意觀察日誌輸出內容,也要關注日誌輸出的時機,因為 Activity 的 launchMode 不同,觸發 onActivityResult() 方法的時機也不同!)