Android 調起系統相機拍照
概述
最近在看 nanChen 寫的圖片選擇器 ImagePicker,感覺寫得很不錯,也打算把從中學到的東西寫下來。很多時候,遇到一個好的框架能夠降低開發成本這是好事。但是也要去了解其內部具體實現邏輯,說不定哪天你需要完成一個類似的小功能,你知道原理就能快速寫出來,而不是引入整個框架。
本文講其中的第一個功能:如何調起手機的相機拍照?
系統現有相機應用
對於如何呼叫系統現有應用,這裡簡單再說一下。在開發的應用中呼叫系統現有應用,需要使用 Intent 指定開啟的應用的 Action 和 Category,然後通過 startActivity(Intent) 或者 startActivityForResult(Intent, int) 開啟指定的 Activity,如果使用 startActivityForResult() 方法開啟並需要返回值,再重寫 onActivityResult(int, int, Intent) 即可。
先來看看系統現有相機應用的 AndroidManifest.xml 清單檔案定義的 Activity:
<activity android:name="com.android.camera.Camera" android:clearTaskOnLaunch="true" android:configChanges="orientation|keyboardHidden" android:screenOrientation="landscape" android:taskAffinity="android.task.camera" android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <categroy android:name="android.intent.category.DEFAULT" /> <categroy android:name="android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter> <action android:name="android.media.action.IMAGE_CAPTURE" /> <categroy android:name="android.intent.category.DEFAULT" /> </intent-filter> <intent-filter> <action android:name="android.media.action.STILL_IMAGE_CAMERA" /> <categroy android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> <activity android:name="com.android.camera.VideoCamera" android:clearTaskOnLaunch="true" android:configChanges="origientation|keyboardHidden" android:label="@string/video_camera_label" android:screenOrientation="landscape" android:taskAffinity="android.task.camcorder" android:theme="@android:style/theme.Black.NoTitleBar.Fullscreen" > <intent-filter> <action android:name="android.media.action.VIDEO_CAMERA" /> <categroy android:name="android.intent.category.DEFAULT" /> </intent-filter> <intent-filter> <action android:name="android.media.action.VIDEO_CAPTURE" /> <categroy android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
它定義了兩個 Activity,com.android.camera.Camera 表示照相機,com.android.camera.VideoCamera 表示攝像機。從字面意思可以看出,為了捕獲系統相機返回的資料,一般需要使用一下兩個 Action 即可開啟照相機與攝像機:
-
android.media.action.IMAGE_CAPTURE:Intent 的 Action 型別,從現有的相機應用中請求一張圖片。
-
android.media.action.VIDEO_CAPTURE:Intent 的 Action 型別,從現有的相機應用中請求一段視訊。
上面兩個引數,均在 MediaStore 類中以靜態常量的形式定義好了,分別是:MediaStore.ACTION_IMAGE_CAPTURE (相機) 和 MediaStore.ACTION_VIDEO_CAPTURE (攝像機)。
獲取系統現有相機拍攝的圖片
在新開啟的 Activity 中,如果需要獲取它的返回值,則需要使用 startActivityForResult(Intent,int) 方法開啟 Activity,並重寫 onActivityResult(int, int, Intent) 獲取系統相機的返回資料,那麼我們只需要在 onActivityResult() 中獲取到返回值即可。
系統相機拍攝的照片,如果不指定路徑,會儲存在系統預設資料夾下,可以使用 Intent.getExtra() 方法得到,得到的是一個 Uri 地址,表示了一個內容提供者的地址。如果通過MediaStore.EXTRA_OUTPUT 指定了儲存路徑,那麼通過 Intent.getExtra() 得到的將是一個空地址,但是既然是我們指定的地址,那麼也不愁找不到它了。
但是如果是7.0以上,需要使用 FileProvider 來得到這個 uri 地址。
實現方案
說清楚流程之後,下面就是具體程式碼實現:
首先是在 AndroidManiFest.xml 下宣告拍照許可權:
<uses-permission android:name="android.permission.CAMERA" />
宣告許可權後,在開始拍照前,還是需要判斷使用者是否給了我們拍照的許可權:
if (( mActivity).checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(mActivity, new String[]{Manifest.permission.CAMERA}, GalleryActivity.REQUEST_PERMISSION_CAMERA); } else { imagePicker.takePicture(mActivity, GalleryActivity.REQUEST_CODE_TAKE); }
如果使用者沒有給許可權,那麼需要申請許可權,許可權申請以後,會有一個回撥通知開發者是否允許了,具體見下發的程式碼:
@Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_PERMISSION_CAMERA) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { imagePicker.takePicture(this, REQUEST_CODE_TAKE); } else { showToast("許可權被禁止,無法開啟相機"); } } }
許可權允許之後,通過 imagePicker.takePicture 去拍照。下面看下拍照的具體程式碼邏輯:
/** * 拍照的方法 */ public void takePicture(Activity activity, int requestCode) { Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); takePictureIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); if (takePictureIntent.resolveActivity(activity.getPackageManager()) != null) { if (Utils.existSDCard()) takeImageFile = new File(Environment.getExternalStorageDirectory(), "/DCIM/camera/"); else takeImageFile = Environment.getDataDirectory(); takeImageFile = createFile(takeImageFile, "IMG_", ".jpg"); if (takeImageFile != null) { // 預設情況下,即不需要指定intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); // 照相機有自己預設的儲存路徑,拍攝的照片將返回一個縮圖。如果想訪問原始圖片, // 可以通過dat extra能夠得到原始圖片位置。即,如果指定了目標uri,data就沒有資料, // 如果沒有指定uri,則data就返回有資料! Uri uri; if (VERSION.SDK_INT <= VERSION_CODES.M) { uri = Uri.fromFile(takeImageFile); } else { /** * 7.0 呼叫系統相機拍照不再允許使用Uri方式,應該替換為FileProvider * 並且這樣可以解決MIUI系統上拍照返回size為0的情況 */ uri = FileProvider.getUriForFile(activity, "com.example.myapplication.provider", takeImageFile); //加入uri許可權 要不三星手機不能拍照 List<ResolveInfo> resInfoList = activity.getPackageManager().queryIntentActivities(takePictureIntent, PackageManager.MATCH_DEFAULT_ONLY); for (ResolveInfo resolveInfo : resInfoList) { String packageName = resolveInfo.activityInfo.packageName; activity.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); } } // Log.e("nanchen", ProviderUtil.getFileProviderName(activity)); takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri); } } activity.startActivityForResult(takePictureIntent, requestCode); }
關於 Intent.resolveActivity 作用,簡單來說就是當你在呼叫第三方軟體或者系統 Activity,類似開啟相機,傳送圖片等隱式 Intent,是並不一定能夠在所有的 Android 裝置上都正常執行。通過該方法判斷這個 intent 對應的 activity 是否存在,確保不會出現崩潰。
takeImageFile 是圖片儲存地址,在7.0以前,需要用 Uri.fromFile 進行處理。7.0之後的採用 FileProvider。下面介紹下 FileProvider 的使用方法:
註冊
使用 FileProvider 需要在 AndroidManiFest.xml 裡宣告:
<provider android:name="android.support.v4.content.FileProvider" android:authorities="com.example.myapplication.provider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_path" /> </provider>
這裡是直接使用的 v4 包中的 FileProvider,我們也可以直接繼承 FileProvider 類,適當重寫過載函式,但不建議如此做。下面來介紹上面的幾個設定:
-
name: provider 的類名,若使用預設的 v4 的 FileProvider 可使用 "android.support.v4.content.FileProvider",也可以設定為自定義的繼承 FileProvider 的 provider 類;
-
authorities: 一個簽名認證,可以自定義,但在獲取 uri 的時候需要保持一致;
-
grantUriPermissions: 使用 FileProvider 的使用需要我們給流出的URI 賦予臨時訪問許可權(READ 和 WRITE),該設定是允許我們行使該項權力;
-
meta-data: meta-data 配置的是我們可以訪問的檔案的路徑配置資訊,需要使用 xml 檔案進行配置,FileProvider 會通過解析 xml 檔案獲取配置項,其中 name 名字不可改變為: android.support.FILE_PROVIDER_PATHS,resource 為配置路徑資訊的配置專案。
路徑配置
可訪問的路徑配置可以在 res 中建立一個 xml 檔案下面建立一個配置檔案,格式如下:
<?xml version="1.0" encoding="utf-8"?> <paths> <!--path:需要臨時授權訪問的路徑(.代表所有路徑)--> <!--name:就是你給這個訪問路徑起個名字--> <external-path name="cam" path="." /> </paths>
下面解釋下:
-
<files-path/> 代表的根目錄: Context.getFilesDir()
- <external-path/> 代表的根目錄: Environment.getExternalStorageDirectory()
- <cache-path/> 代表的根目錄: getCacheDir()
最後,將 uri 放到 MediaStore.EXTRA_OUTPUT 中。
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
拍照完成後,會回撥 onActivityResult,在這裡我們可以根據先前傳的值將圖片展示到 ImageView 中:
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { Log.i(TAG, "系統相機拍照完成,resultCode=" + resultCode + " " + requestCode); if (requestCode == REQUEST_CODE_TAKE) { Uri uri = Uri.fromFile(takeImageFile); iv_CameraImg.setImageURI(uri); } }
到此,呼叫系統相機拍照的過程到此就結束了。
&n