25、CameraX KTX:CameraX的KTX扩展,拍照与视频录制

CameraX 是 Jetpack 家族里我最喜欢的成员之一。说实话,以前做相机开发真的挺痛苦的——要处理各种厂商的兼容性问题,还要写一堆模板代码。CameraX 的出现,算是把 Android 相机开发的门槛降了一大截。而加上 KTX 扩展之后,代码更是清爽得像刚洗过的镜头。

这一章,我们就来聊聊 CameraX KTX 到底能帮我们省多少事。我会结合拍照和视频录制这两个最常用的场景,带你看看 KTX 是怎么把回调地狱变成协程天堂的。

25.1 CameraX KTX 到底扩展了什么?

CameraX KTX 的核心思路,就是把原本基于回调的 API 包装成 Kotlin 协程友好的形式。说白了,就是让你能用 suspendFlow 来写相机逻辑,而不是嵌套一堆 OnCaptureCallback

我个人习惯把 CameraX KTX 的扩展分成三类:

扩展类型 作用 典型场景
生命周期绑定 将 CameraX 与 Lifecycle 自动关联 Activity/Fragment 中快速启动相机
协程扩展 将回调转为 suspend 函数 拍照、视频录制、对焦等操作
Flow 扩展 将连续事件转为 Flow 流 相机状态监听、帧分析

你想想看,如果没有 KTX,你要写一个拍照功能,得先设置 ImageCapture.OnCaptureCallback,然后在回调里处理成功或失败。代码一多,回调嵌套就深了。有了 KTX,直接 imageCapture.takePicture(context, executor) 配合 await() 就搞定了。

25.2 快速上手:用 KTX 实现拍照

我们先从最简单的拍照功能开始。假设你已经配置好了 ProcessCameraProviderPreview,现在要加一个拍照按钮。

传统写法是这样的:

val imageCapture = ImageCapture.Builder().build()

// 传统回调方式
imageCapture.takePicture(
    context,
    executor,
    object : ImageCapture.OnImageSavedCallback {
        override fun onImageSaved(output: ImageCapture.OutputFileResults) {
            // 保存成功
        }

        override fun onError(exception: ImageCaptureException) {
            // 处理错误
        }
    }
)

嗯,这段代码其实不算太差,但如果你有多个连续操作——比如先拍照、再分析、再保存——回调就会开始嵌套了。

用 KTX 扩展之后,代码变成这样:

// 需要引入依赖:androidx.camera:camera-camera2
// 以及 androidx.camera:camera-lifecycle

lifecycleScope.launch {
    try {
        val outputFile = File(cacheDir, "photo_${System.currentTimeMillis()}.jpg")
        val outputOptions = ImageCapture.OutputFileOptions.Builder(outputFile).build()

        // 注意:takePicture 的 KTX 扩展需要传入 OutputFileOptions
        imageCapture.takePicture(outputOptions, executor).await()

        // 到这里,照片已经保存成功了
        Log.d("CameraX", "照片已保存到:${outputFile.absolutePath}")
    } catch (e: ImageCaptureException) {
        Log.e("CameraX", "拍照失败", e)
    }
}

关键就在于 .await() 这个扩展函数。它把原本的回调包装成了挂起函数,你可以在协程里直接等待结果。我在项目中遇到过好几次因为回调线程切换导致的 UI 更新问题,用了 await() 之后,一切都变得可控了。

小提示: takePicture 的 KTX 扩展要求传入 Executor。你可以用 ContextCompat.getMainExecutor(context) 来获取主线程的 Executor,这样回调会在主线程执行,方便更新 UI。

25.3 视频录制:用 KTX 简化生命周期管理

视频录制比拍照复杂一些,因为涉及到开始、暂停、停止等状态管理。CameraX 提供了 VideoCapture 用例,配合 KTX 扩展,我们可以用协程来管理录制流程。

先看一个完整的录制示例:

// 配置 VideoCapture
val videoCapture = VideoCapture.Builder()
    .setBitRate(10_000_000) // 10 Mbps
    .build()

// 绑定到生命周期
cameraProvider.bindToLifecycle(
    lifecycleOwner,
    cameraSelector,
    preview,
    videoCapture
)

// 开始录制
lifecycleScope.launch {
    try {
        val outputFile = File(cacheDir, "video_${System.currentTimeMillis()}.mp4")
        val outputOptions = VideoCapture.OutputFileOptions.Builder(outputFile).build()

        // 注意:VideoCapture 的 KTX 扩展是 startRecording
        videoCapture.startRecording(outputOptions, executor).await()

        // 录制中... 这里可以更新 UI 显示录制时长

        // 停止录制
        videoCapture.stopRecording()
        Log.d("CameraX", "视频已保存到:${outputFile.absolutePath}")
    } catch (e: Exception) {
        Log.e("CameraX", "录制失败", e)
    }
}

这里有个细节我要提醒你:startRecordingawait() 并不是等待录制结束,而是等待录制开始。也就是说,它会在录制真正启动后才返回。这样做的好处是,你可以确保录制已经开始了,再去做其他操作,比如显示录制指示器。

注意: 如果你在 await() 之后直接写停止逻辑,那录制会立即停止。正确的做法是:在 await() 之后启动一个协程来等待用户操作(比如点击停止按钮),或者设置一个定时器。

我曾经犯过这个错误——在 await() 后面直接调了 stopRecording(),结果录出来的视频只有几百毫秒。排查了半天才发现是逻辑顺序搞反了。

25.4 用 Flow 监听相机状态

CameraX KTX 还提供了 CameraState 的 Flow 扩展,让你可以响应式地监听相机状态变化。比如,当相机被其他应用占用时,你可以及时通知用户。

// 获取 Camera 对象
val camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview)

// 监听相机状态
lifecycleScope.launch {
    camera.cameraStateFlow.collect { state ->
        when (state.type) {
            CameraState.Type.PENDING_OPEN -> {
                // 相机正在打开
                Log.d("CameraX", "相机正在打开...")
            }
            CameraState.Type.OPEN -> {
                // 相机已打开,可以开始预览
                Log.d("CameraX", "相机已就绪")
            }
            CameraState.Type.CLOSING -> {
                // 相机正在关闭
                Log.d("CameraX", "相机正在关闭...")
            }
            CameraState.Type.CLOSED -> {
                // 相机已关闭
                val error = state.error
                if (error != null) {
                    Log.e("CameraX", "相机异常关闭:${error.cause}")
                }
            }
        }
    }
}

这个 Flow 扩展特别适合用来做 UI 状态管理。比如,当相机状态变为 CLOSED 且带有错误时,你可以弹出一个对话框提示用户。我在一个视频通话项目里就用这个特性来检测相机被系统回收的情况,用户体验好了很多。

25.5 核心知识体系

为了让你更直观地理解 CameraX KTX 的整体结构,我画了一张图:

CameraX KTX 生命周期绑定 bindToLifecycle 自动管理生命周期 协程扩展 takePicture().await() startRecording().await() Flow 扩展 cameraStateFlow 响应式状态监听 应用场景:拍照、视频录制、相机状态管理

从这张图你可以看到,CameraX KTX 的三个扩展方向是相互独立的,你可以按需使用。我个人建议,至少把生命周期绑定和协程扩展用起来,这两个能帮你省掉大量模板代码。

25.6 避坑指南:我踩过的几个坑

最后,分享几个我在项目中遇到的实际问题,希望能帮你少走弯路。

坑一:忘记添加依赖

CameraX KTX 的扩展函数分散在不同的依赖里。拍照和视频录制的 await()camera-camera2 中,而生命周期绑定在 camera-lifecycle 中。如果你发现 IDE 提示找不到 .await(),先检查一下依赖是否齐全。

// 必要的依赖
implementation("androidx.camera:camera-core:1.3.0")
implementation("androidx.camera:camera-camera2:1.3.0")
implementation("androidx.camera:camera-lifecycle:1.3.0")
implementation("androidx.camera:camera-view:1.3.0") // 可选,用于 PreviewView

坑二:Executor 线程问题

我曾经在 takePicture 中传了一个后台线程的 Executor,结果 await() 返回后,我直接更新了 UI,导致崩溃。记住,await() 返回的线程取决于你传入的 Executor。如果你要在协程里更新 UI,要么用 withContext(Dispatchers.Main),要么传入主线程的 Executor。

坑三:视频录制文件被占用

如果你在录制结束后立即尝试读取视频文件,可能会遇到文件被占用的错误。这是因为 CameraX 内部还在做一些收尾工作。我建议在 stopRecording() 之后加一个短暂的延迟,或者监听录制完成的事件。

总结一下: CameraX KTX 的核心价值在于用 Kotlin 协程和 Flow 简化了相机 API 的使用。拍照用 takePicture().await(),视频录制用 startRecording().await(),状态监听用 cameraStateFlow。记住这三个,你就能写出既简洁又健壮的相机代码。

好了,这一章的内容就到这里。CameraX 本身还在不断演进,KTX 扩展也会越来越完善。如果你在实际项目中遇到了什么奇怪的问题,欢迎随时交流。

公众号:蓝海资料掘金营,微信deep3321