Camera2 教程 · 第二章 · 開啟相機

Android Camera
上一章《Camera2 概覽》裡我們介紹了一些 Camera2 的基礎知識,但是並沒有涉及太多的 API,從本章開始我們會開發一個具有完整相機功能的應用程式,並且將相機知識分成多個篇章進行介紹,而本章所要介紹的就是相機的開啟流程。
閱讀本章之後,你將學會以下幾個知識點:
- 如何註冊相機相關的許可權
- 如何配置相機特性要求
- 如何開啟相機
- 如何關閉相機
你可以在 https://github.com/darylgo/Camera2Sample 下載相關的原始碼,並且切換到 Tutorial2 標籤下。
1 建立相機專案
正如前所說的,我們會開發一個具有完整相機功能的應用程式,所以第一步要做的就是建立一個相機專案,這裡我用 AS 建立了一個叫 Camera2Sample 的專案,並且有一個 Activity 叫 MainActivity。我們使用的開發語言是 Kotlin,所以如果你對 Kotlin 還不熟悉的話,建議你先去學習下 Kotlin 的基礎知識。
為了降低原始碼的閱讀難度,我不打算引入任何的第三方庫,不去關注效能問題,也不進行任何模式上的設計,大部分的程式碼我都會寫在這個 MainActivity 裡面,所有的功能的實現都儘可能簡化,讓閱讀者可以只關注重點。
2 註冊相關許可權
在使用相機 API 之前,必須在 AndroidManifest.xml 註冊相機許可權 android.permission.CAMERA,宣告我們開發的應用程式需要相機許可權,另外如果你有儲存照片的操作,那麼讀寫 SD 卡的許可權也是必須的:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.darylgo.camera.sample"> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> </manifest>
需要注意的是 6.0 以上的系統需要我們在程式執行的時候進行動態許可權申請,所以我們需要在程式啟動的時候去檢查許可權,有任何一個必要的許可權被使用者拒絕時,我們就彈窗提示使用者程式因為許可權被拒絕而無法正常工作:
class MainActivity : AppCompatActivity() { companion object { private const val REQUEST_PERMISSION_CODE: Int = 1 private val REQUIRED_PERMISSIONS: Array<String> = arrayOf( android.Manifest.permission.CAMERA, android.Manifest.permission.WRITE_EXTERNAL_STORAGE ) } /** * 判斷我們需要的許可權是否被授予,只要有一個沒有授權,我們都會返回 false,並且進行許可權申請操作。 * * @return true 許可權都被授權 */ private fun checkRequiredPermissions(): Boolean { val deniedPermissions = mutableListOf<String>() for (permission in REQUIRED_PERMISSIONS) { if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_DENIED) { deniedPermissions.add(permission) } } if (deniedPermissions.isEmpty().not()) { requestPermissions(deniedPermissions.toTypedArray(), REQUEST_PERMISSION_CODE) } return deniedPermissions.isEmpty() } }
3 配置相機特性要求
你一定不希望使用者在一臺沒有任何相機的手機上安裝你的相機應用程式吧,因為那樣做是沒有意義的。所以接下來要做的就是在 AndroidManifest.xml 中配置一些程式執行時必要的相機特性,如果這些特性不支援,那麼使用者在安裝 apk 的時候就會因為條件不符合而無法安裝。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.darylgo.camera.sample"> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-feature android:name="android.hardware.camera" android:required="true" /> </manifest>
我們通過 <uses-feature> 標籤聲明瞭我們的應用程式必須在具有相機的手機上才能執行。另外你還可以配置更多的特性要求,例如必須支援自動對焦的相機才能執行你的應用程式,更多的特性可以在 官方文件 上查詢。
4 獲取 CameraManager 例項
CameraManager 是一個負責查詢和建立相機連線的系統服務,可以說 CameraManager 是 Camera2 使用流程的起點,所以首先我們要通過 getSystemService() 獲取 CameraManager 例項:
private val cameraManager: CameraManager by lazy { getSystemService(CameraManager::class.java) }
5 獲取相機 ID 列表
接下來我們要獲取所有可用的相機 ID 列表,這個 ID 列表的長度也代表有多少個相機可以使用。使用的 API 是 CameraManager.getCameraIdList(),它會返回一個包含所有可用相機 ID 的字串陣列:
val cameraIdList = cameraManager.cameraIdList
注意:Kotlin 會將很多 Java API 的 getter 直接轉換成 Kotlin 的 property 語法,所以你會看到 getCameraIdList() 被轉換成了 cameraIdList,後續會有很多類似的轉換,這裡提前說明下,避免誤解。
6 根據相機 ID 獲取 CameraCharacteristics
CameraCharacteristics 是相機資訊的提供者,通過它我們可以獲取所有相機資訊,這裡我們需要根據攝像頭的方向篩選出前置和後置攝像頭,所以首先我們要獲取所有相機的 CameraCharacteristics 例項,涉及的 API 是 CameraManager.getCameraCharacteristics(),它會根據你指定的相機 ID 返回對應的相機資訊:
// 遍歷所有可用的攝像頭 ID,只取出其中的前置和後置攝像頭資訊。 val cameraIdList = cameraManager.cameraIdList cameraIdList.forEach { cameraId -> val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId) if (cameraCharacteristics[CameraCharacteristics.LENS_FACING] == CameraCharacteristics.LENS_FACING_FRONT) { frontCameraId = cameraId frontCameraCharacteristics = cameraCharacteristics } else if (cameraCharacteristics[CameraCharacteristics.LENS_FACING] == CameraCharacteristics.LENS_FACING_BACK) { backCameraId = cameraId backCameraCharacteristics = cameraCharacteristics } }
7 開啟相機
接下來我們要做的就是呼叫 CameraManager.openCamera() 方法開啟相機了,該方法要求我們傳遞兩個引數,一個是相機 ID,一個是監聽相機狀態的 CameraStateCallback。當相機被成功開啟的時候會通過 CameraStateCallback.onOpened() 方法回撥一個 CameraDevice 例項給你,否則的話會通過 CameraStateCallback.onError() 方法回撥一個 CameraDevice 例項和一個錯誤碼給你。onOpened() 和 onError() 其實都意味著相機已經被開啟了,唯一的區別是 onError() 表示開啟過程中出了問題,你必須把傳遞給你的 CameraDevice 關閉,而不是繼續使用它,具體的 API 介紹可以自行檢視文件。另外,你必須確保在開啟相機之前已經被授予了相機許可權,否則會拋許可權異常。一個比較穩妥的做法就是每次開啟相機之前檢查相機許可權。下面是主要程式碼片段:
private data class OpenCameraMessage(val cameraId: String, val cameraStateCallback: CameraStateCallback) @SuppressLint("MissingPermission") override fun handleMessage(msg: Message): Boolean { when (msg.what) { MSG_OPEN_CAMERA -> { val openCameraMessage = msg.obj as OpenCameraMessage val cameraId = openCameraMessage.cameraId val cameraStateCallback = openCameraMessage.cameraStateCallback cameraManager.openCamera(cameraId, cameraStateCallback, cameraHandler) Log.d(TAG, "Handle message: MSG_OPEN_CAMERA") } } return false } private fun openCamera() { // 有限選擇後置攝像頭,其次才是前置攝像頭。 val cameraId = backCameraId ?: frontCameraId if (cameraId != null) { val openCameraMessage = OpenCameraMessage(cameraId, CameraStateCallback()) cameraHandler?.obtainMessage(MSG_OPEN_CAMERA, openCameraMessage)?.sendToTarget() } else { throw RuntimeException("Camera id must not be null.") } }
private inner class CameraStateCallback : CameraDevice.StateCallback() { @WorkerThread override fun onOpened(camera: CameraDevice) { cameraDevice = camera runOnUiThread { Toast.makeText(this@MainActivity, "相機已開啟", Toast.LENGTH_SHORT).show() } } @WorkerThread override fun onError(camera: CameraDevice, error: Int) { camera.close() cameraDevice = null } }
8 關閉相機
和其他硬體資源的使用一樣,當我們不再需要使用相機時記得呼叫 CameraDevice.close() 方法及時關閉相機回收資源。關閉相機的操作至關重要,因為如果你一直佔用相機資源,其他基於相機開發的功能都會無法正常使用,嚴重情況下直接導致其他相機相關的 APP 無法正常使用,當相機被完全關閉的時候會通過 CameraStateCallback.onCllosed() 方法通知你相機已經被關閉。那麼在什麼時候關閉相機最合適呢?我個人的建議是在 onPause() 的時候就一定要關閉相機,因為在這個時候相機頁面已經不是使用者關注的焦點,大部分情況下已經可以關閉相機了。
@SuppressLint("MissingPermission") override fun handleMessage(msg: Message): Boolean { when (msg.what) { MSG_CLOSE_CAMERA -> { cameraDevice?.close() Log.d(TAG, "Handle message: MSG_CLOSE_CAMERA") } } return false } override fun onPause() { super.onPause() closeCamera() } private fun closeCamera() { cameraHandler?.sendEmptyMessage(MSG_CLOSE_CAMERA) }
private inner class CameraStateCallback : CameraDevice.StateCallback() { @WorkerThread override fun onClosed(camera: CameraDevice) { cameraDevice = null runOnUiThread { Toast.makeText(this@MainActivity, "相機已關閉", Toast.LENGTH_SHORT).show() } } }
至此,我們關於開啟相機的教程就結束了,下一章我們會介紹如何開啟預覽。