上一章《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 是相机信息的提供者,通过它我们可以获取所有相机信息,这里我们需要根据摄像头的方向筛选出前置和后置摄像头,并且要求相机的 Hardware Level 必须是 FULL 及以上,所以首先我们要获取所有相机的 CameraCharacteristics 实例,涉及的 API 是 CameraManager.getCameraCharacteristics(),它会根据你指定的相机 ID 返回对应的相机信息:
/**
* 判断相机的 Hardware Level 是否大于等于指定的 Level。
*/
fun CameraCharacteristics.isHardwareLevelSupported(requiredLevel: Int): Boolean {
val sortedLevels = intArrayOf(
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
)
val deviceLevel = this[CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL]
if (requiredLevel == deviceLevel) {
return true
}
for (sortedLevel in sortedLevels) {
if (requiredLevel == sortedLevel) {
return true
} else if (deviceLevel == sortedLevel) {
return false
}
}
return false
}
// 遍历所有可用的摄像头 ID,只取出其中的前置和后置摄像头信息。
val cameraIdList = cameraManager.cameraIdList
cameraIdList.forEach { cameraId ->
val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId)
if (cameraCharacteristics.isHardwareLevelSupported(REQUIRED_SUPPORTED_HARDWARE_LEVEL)) {
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() }
}
}
至此,关于开关相机的教程就结束了,下一章我们会介绍如何开启预览。