在 Android 开发中,摄像头几乎在每一个 App 都需要使用到。但是由于 Android 碎片化的问题,要在不同的机型上保持一致体验非常麻烦。 Google 为了解决这个问题,就创建了 CameraX
这个组件库。我们开发者只需要关心 CameraX
的 api 即可,CameraX
内部会对不同的机型进行适配处理。
要使用 CameraX
,需要引入如下的依赖:
def camerax_version = '1.1.0'
implementation "androidx.camera:camera-core:$camerax_version"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:$camerax_version"
申请权限
在使用 Camera
之前,我们需要先申请相应的权限。Camera
相关的权限如下所示:
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
预览
在 CameraX
中,我们需要使用 PreviewView
组件来显示预览图。我们首先需要将 PreviewView
添加到布局文件,代码如下:
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/camera_container"
android:background="@android:color/black"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.camera.view.PreviewView
android:id="@+id/view_finder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
布局文件添加好后,我们就可以开始实现预览效果了,代码示例如下:
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
// addListener 是为了监听 CameraX 初始化完成
cameraProviderFuture.addListener({
// 用来绑定生命周期组件,让 CameraX 具有生命周期感知能力
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// 预览组件
val preview = Preview.Builder()
.build()
.also {
// 绑定我们在布局中加入的 PreviewView
it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
}
// 设置选中的摄像头,这里选中的是后置摄像头
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// 解除之前的绑定
cameraProvider.unbindAll()
// CameraX 把不同的功能,如 预览、拍照等功能封装成不同的 UseCase
// 这样我们就可以根据业务的需要,来绑定不同的 UseCase 来实现具体的功能
cameraProvider.bindToLifecycle(
this, cameraSelector, preview)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
// ContextCompat.getMainExecutor 表示在主线程执行初始化
}, ContextCompat.getMainExecutor(this))
}
上面的代码就是我们使用 CameraX
的固定流程了。如果需要添加其他的功能,比如拍照、拍视频等,只需要添加对应的 UseCase 就可以了。
切换摄像头
在 CameraX
中如果需要切换摄像头,我们需要替换 cameraSelector
的值,然后重新绑定。代码示例如下,这里使用协程来实现。
private var cameraProvider: ProcessCameraProvider? = null
private var currentLensFacing: Int = CameraSelector.LENS_FACING_BACK
// 切换摄像头
fun switchCamera() {
val cameraProvider = cameraProvider ?: return
val lensFacing = when {
hasBackCamera() -> CameraSelector.LENS_FACING_BACK
hasFrontCamera() -> CameraSelector.LENS_FACING_FRONT
else -> throw IllegalStateException("Back and front camera are unavailable")
}
currentLensFacing = lensFacing
bindUseCases()
}
private suspend fun startCamera() {
// 使用 await 等待初始化,而不是 addListener
cameraProvider = cameraProviderFuture.get().await()
bindUseCases()
}
// 绑定 UseCase
private fun bindUseCases() {
val cameraProvider = cameraProvider ?: return
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
}
// 设置当前需要切换的摄像头
val cameraSelector = currentLensFacing
try {
// 解除之前的绑定
cameraProvider.unbindAll()
// CameraX 把不同的功能,如 预览、拍照等功能封装成不同的 UseCase
// 这样我们就可以根据业务的需要,来绑定不同的 UseCase 来实现具体的功能
cameraProvider.bindToLifecycle(
this, cameraSelector, preview)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}
为了避免摄像头不存在的异常情况,我们还需要提前判断摄像头是否存在,代码如下:
// 后置摄像头是否存在
private fun hasBackCamera(): Boolean {
return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false
}
// 前置摄像头是否存在
private fun hasFrontCamera(): Boolean {
return cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false
}
拍照
在 CameraX
中拍照片,我们需要添加 ImageCapture
这个 UseCase
并绑定,代码示例如下:
private var imageCapture: ImageCapture? = null
private fun bindUseCases() {
...
// 增加拍照的 UseCase
imageCapture = ImageCapture.Builder()
.build()
try {
...
// 增加 imageCapture 的绑定
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}
完成绑定后,我们就可以使用创建的 imageCapture
对象来实现拍照操作了,代码示例如下:
private val cameraExecutor = Executors.newSingleThreadExecutor()
private fun takePhoto() {
val imageCapture = imageCapture ?: return
// 创建用于保存图片的 MediaStore 内容值
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
.format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
}
}
// 设置所需的输出内容。这里将输出保存在 MediaStore 中
val outputOptions = ImageCapture.OutputFileOptions
.Builder(contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues)
.build()
// 执行拍照操作
imageCapture.takePicture(
outputOptions,
cameraExecutor, // 设置在子线程执行
object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
// 失败
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
override fun
onImageSaved(output: ImageCapture.OutputFileResults){
// 图片保存成功
}
}
)
}
让预览界面与相机捕获界面一致
由于摄像头支持的拍摄的比例(一般是 4:3)与屏幕的比例不一致,会导致拍照图与预览图结果不一样。如果你想要让预览图与拍照图一致,需要指定分辨率。代码示例如下:
private fun bindUseCases() {
...
// 相机设置为全屏分辨率
val metrics = windowManager.getCurrentWindowMetrics().bounds
val screenAspectRatio = aspectRatio(metrics.width(), metrics.height())
val rotation = fragmentCameraBinding.viewFinder.display.rotation
// 通过 setTargetAspectRatio 来设置相同的宽高比
// 如果设置的宽高比当前Camera不支持,CameraX 会找到最接近的宽高比来处理
preview = Preview.Builder()
.setTargetAspectRatio(screenAspectRatio)
.setTargetRotation(rotation)
.build()
imageCapture = ImageCapture.Builder()
.setTargetAspectRatio(screenAspectRatio)
.setTargetRotation(rotation)
.build()
}
录像
如果需要在 CameraX 中录像,我要添加绑定 VideoCapture
这个 UseCase
,代码示例如下:
private lateinit var videoCapture: VideoCapture<Recorder>
private fun bindUseCases() {
...
// 构建一个记录器,它可以:
// - 将视频/音频录制到 MediaStore 、File、ParcelFileDescriptor
// - 用于创建录音
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST))
.build()
videoCapture = VideoCapture.withOutput(recorder)
}
绑定完成后,就可以使用 VideoCapture
来录像了,代码示例如下:
private var recording: Recording? = null
private fun captureVideo() {
val videoCapture = this.videoCapture ?: return
val curRecording = recording
if (curRecording != null) {
// 停止录像
curRecording.stop()
recording = null
return
}
// 创建 MediaStore 视频内容对象,将系统时间戳作为视频的名字
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
.format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
}
}
// 设置所需的输出内容。将输出保存在 MediaStore 中
val mediaStoreOutputOptions = MediaStoreOutputOptions
.Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
.setContentValues(contentValues)
.build()
// 开始录像
recording = videoCapture.output
.prepareRecording(this, mediaStoreOutputOptions)
.start(ContextCompat.getMainExecutor(this)) { recordEvent ->
when(recordEvent) {
is VideoRecordEvent.Start -> {
// 录制开始
}
is VideoRecordEvent.Pause -> {
// 暂停
}
is VideoRecordEvent.Resume -> {
// 恢复
}
is VideoRecordEvent.Finalize -> {
// 录制结束
if (!recordEvent.hasError()) {
// 录制失败
} else {
// 录
}
}
}
}
}