22、CameraX:CameraX架构、Preview、ImageCapture、ImageAnalysis、实战:相机应用开发

说到Android上的相机开发,我猜不少朋友都跟我一样,踩过Camera1和Camera2的坑。Camera1功能太弱,Camera2又复杂得要命——光是一个CameraCaptureSession的回调嵌套就能让你怀疑人生。直到Google推出了CameraX,我才觉得,嗯,这才是现代Android该有的样子。

CameraX是Jetpack家族的一员。它基于Camera2封装,但用起来简单太多了。说白了,它就是帮你把那些繁琐的状态管理、生命周期绑定、设备兼容性问题都处理掉了。你只需要关心业务逻辑就行。

CameraX的核心架构

CameraX的架构其实不复杂。它围绕几个核心概念展开:CameraProviderProcessCameraProviderUseCase,以及CameraSelector

我画了一张图,帮你快速理解它们之间的关系:

CameraX 核心架构图 ProcessCameraProvider LifecycleOwner CameraSelector Preview ImageCapture ImageAnalysis 一个 Camera 实例可以同时绑定多个 UseCase 生命周期由 LifecycleOwner 自动管理

你看,整个流程很清晰:ProcessCameraProvider是核心入口,它负责把LifecycleOwnerCameraSelector和各个UseCase绑定到一起。绑定之后,相机的生命周期就跟着Activity或Fragment走了,不用你手动去开开关关。

关键点:CameraX 的 UseCase 是独立的模块。你可以只预览不拍照,也可以只分析不预览。它们互不干扰,组合起来非常灵活。

Preview:预览画面

Preview 是最常用的 UseCase。它的作用就是把相机画面显示到屏幕上。在 CameraX 中,Preview 需要配合 PreviewView 使用。

我个人习惯这样写:

val preview = Preview.Builder()
    .setTargetAspectRatio(AspectRatio.RATIO_16_9)
    .build()

preview.setSurfaceProvider(previewView.surfaceProvider)

cameraProvider.bindToLifecycle(
    lifecycleOwner,
    cameraSelector,
    preview
)

这里有个细节要注意:setTargetAspectRatio 设置的是目标宽高比,不是强制裁剪。CameraX 会尽量适配,但最终显示效果取决于设备屏幕和摄像头传感器的实际比例。

小技巧:如果你希望预览画面填满屏幕,可以给 PreviewView 设置 scaleType = fitCentercenterCrop。我一般用 centerCrop,这样画面不会留黑边。

ImageCapture:拍照

拍照功能由 ImageCapture 提供。它支持 JPEG 格式,也支持原始的 YUV_420_888 格式。不过大多数场景下,我们直接用 JPEG 就够了。

我曾经在一个项目中遇到一个问题:拍照后图片方向不对。后来发现是没设置 setTargetRotation。CameraX 虽然会自动处理大部分旋转,但如果你在竖屏下拍照,最好显式指定一下:

val imageCapture = ImageCapture.Builder()
    .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
    .setTargetRotation(Surface.ROTATION_0)
    .build()

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

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

注意:CAPTURE_MODE_MINIMIZE_LATENCY 会优先保证快门速度,适合抓拍。如果你更在意画质,可以用 CAPTURE_MODE_MAXIMIZE_QUALITY。但要注意,高画质模式在低端设备上可能会有延迟。

ImageAnalysis:图像分析

ImageAnalysis 是 CameraX 里我最喜欢的一个 UseCase。它让你可以拿到每一帧的原始数据,然后做自定义处理——比如二维码扫描、人脸检测、物体识别等等。

它的用法也很直接:

val imageAnalysis = ImageAnalysis.Builder()
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
    .build()

imageAnalysis.setAnalyzer(executor) { imageProxy ->
    // imageProxy 就是当前帧的数据
    val buffer = imageProxy.planes[0].buffer
    // 在这里做你的分析逻辑
    imageProxy.close() // 记得关闭!
}

嗯,这里有个坑我必须提一下:一定要调用 imageProxy.close()。如果不关,CameraX 会认为你还在处理这一帧,就不会给你下一帧。结果就是画面卡住,分析也停了。我曾经因为这个 bug 排查了半天,最后发现就是少了一行 close。

关于背压策略:

  • STRATEGY_KEEP_ONLY_LATEST:只保留最新的一帧,旧的丢弃。适合实时性要求高的场景。
  • STRATEGY_BLOCK_PRODUCER:如果分析器忙,就阻塞相机输出。适合需要处理每一帧的场景。

我建议大部分场景用 KEEP_ONLY_LATEST,性能更友好。

实战:一个完整的相机应用

好了,理论说完了,我们来写一个完整的例子。这个应用包含预览、拍照、图像分析三个功能。代码不长,但涵盖了 CameraX 的核心用法。

首先,在 build.gradle 里添加依赖:

dependencies {
    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 和两个按钮:

<androidx.camera.view.PreviewView
    android:id="@+id/previewView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

<Button
    android:id="@+id/btnCapture"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="拍照" />

<Button
    android:id="@+id/btnAnalyze"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="开始分析" />

最后,在 Activity 里初始化 CameraX:

class CameraActivity : AppCompatActivity() {
    private lateinit var previewView: PreviewView
    private var imageCapture: ImageCapture? = null
    private var imageAnalysis: ImageAnalysis? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_camera)

        previewView = findViewById(R.id.previewView)

        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()

            // 1. 创建 Preview
            val preview = Preview.Builder()
                .setTargetAspectRatio(AspectRatio.RATIO_16_9)
                .build()
            preview.setSurfaceProvider(previewView.surfaceProvider)

            // 2. 创建 ImageCapture
            imageCapture = ImageCapture.Builder()
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                .build()

            // 3. 创建 ImageAnalysis
            imageAnalysis = ImageAnalysis.Builder()
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build()
            imageAnalysis?.setAnalyzer(Executors.newSingleThreadExecutor()) { imageProxy ->
                // 这里可以放你的分析逻辑
                imageProxy.close()
            }

            // 4. 选择后置摄像头
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            // 5. 绑定到生命周期
            cameraProvider.bindToLifecycle(
                this,
                cameraSelector,
                preview,
                imageCapture,
                imageAnalysis
            )

        }, ContextCompat.getMainExecutor(this))

        // 拍照按钮
        findViewById<Button>(R.id.btnCapture).setOnClickListener {
            val file = File(externalMediaDirs.first(), "photo_${System.currentTimeMillis()}.jpg")
            val outputOptions = ImageCapture.OutputFileOptions.Builder(file).build()
            imageCapture?.takePicture(
                outputOptions,
                ContextCompat.getMainExecutor(this),
                object : ImageCapture.OnImageSavedCallback {
                    override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                        Toast.makeText(this@CameraActivity, "照片已保存", Toast.LENGTH_SHORT).show()
                    }

                    override fun onError(exception: ImageCaptureException) {
                        Toast.makeText(this@CameraActivity, "拍照失败", Toast.LENGTH_SHORT).show()
                    }
                }
            )
        }
    }
}

这段代码跑起来,你就能看到预览画面,点击拍照按钮会保存照片,同时后台还在进行图像分析。三个 UseCase 同时工作,互不干扰。

避坑指南:我曾经在绑定 UseCase 时忘记检查 cameraProvider 是否已经解绑,导致重复绑定报错。建议在 onDestroy 里调用 cameraProvider.unbindAll(),确保资源释放干净。

总结

CameraX 的出现,让 Android 相机开发的门槛降低了不少。你不需要再跟 Camera2 的那些回调、状态机、Session 打交道。只需要关注三个 UseCase:Preview 负责显示,ImageCapture 负责拍照,ImageAnalysis 负责分析。组合起来,就能快速搭建一个功能完整的相机应用。

当然,CameraX 也不是万能的。如果你需要非常底层的控制,比如手动调节曝光时间、ISO、对焦模式,那还是得用 Camera2。但对于绝大多数应用场景,CameraX 已经足够好了。

最后,记住我前面说的:别忘了 close ImageProxy。这个坑我替你踩过了,你就别再踩一次了。


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