52、CameraX相机开发:预览、拍照、分析、自定义相机界面

CameraX 是 Google 推出的 Jetpack 相机库。说白了,它把 Camera2 那套复杂的状态机封装成了几个好用的用例(UseCase)。我最早接触 Android 相机开发时,用的还是 Camera1,那叫一个痛苦——各种回调、各种生命周期要自己管。后来换到 Camera2,虽然功能强了,但代码量直接翻倍。直到 CameraX 出来,我才觉得「嗯,这才是现代 Android 该有的样子」。

核心要点:CameraX 提供了三个开箱即用的用例——Preview(预览)、ImageCapture(拍照)、ImageAnalysis(分析)。你可以组合使用,也可以单独用。生命周期自动绑定,配置也简单。

为什么选 CameraX?

我个人习惯用 CameraX 做相机功能,原因有三:

  • 生命周期感知:自动绑定到 LifecycleOwner,不用手动释放资源
  • 设备兼容性:Google 做了大量兼容测试,覆盖了市面上 90% 以上的设备
  • 代码简洁:实现预览+拍照,核心代码不到 50 行

你想想看,如果用 Camera2 实现同样的功能,光处理各种状态回调就得写几百行。我在项目中遇到过一台老款华为手机,用 Camera2 拍照总是闪退,换成 CameraX 后直接跑通了——这就是兼容性的价值。

核心架构

CameraX 的架构其实很清晰。我画了一张图,你看一眼就明白了:

CameraX 核心架构 ProcessCameraProvider 绑定 LifecycleOwner Preview 预览 ImageCapture 拍照 ImageAnalysis 分析 CameraX 底层(Camera2 适配 + 设备兼容层)

看到没?最上层是 ProcessCameraProvider,它负责把 CameraX 和你的 Activity/Fragment 生命周期绑定。下面三个用例可以任意组合。底层是 Camera2 的封装,但你不必关心那些细节。

实战:实现预览 + 拍照

直接上代码。这是我项目里常用的写法:

// 1. 在布局中添加 PreviewView
// activity_camera.xml
<androidx.camera.view.PreviewView
    android:id="@+id/previewView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

// 2. 初始化 CameraX
class CameraActivity : AppCompatActivity() {
    private lateinit var previewView: PreviewView
    private var imageCapture: ImageCapture? = null
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_camera)
        previewView = findViewById(R.id.previewView)
        
        startCamera()
    }
    
    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        
        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()
            
            // 创建预览用例
            val preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(previewView.surfaceProvider)
                }
            
            // 创建拍照用例
            imageCapture = ImageCapture.Builder()
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                .build()
            
            // 选择后置摄像头
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
            
            try {
                // 解绑之前的用例
                cameraProvider.unbindAll()
                // 绑定生命周期
                cameraProvider.bindToLifecycle(
                    this, cameraSelector, preview, imageCapture
                )
            } catch (e: Exception) {
                Log.e("CameraX", "绑定失败", e)
            }
        }, ContextCompat.getMainExecutor(this))
    }
    
    // 拍照方法
    fun takePhoto() {
        val photoFile = File(
            externalMediaDirs.first(),
            "${System.currentTimeMillis()}.jpg"
        )
        
        val outputOptions = ImageCapture.OutputFileOptions
            .Builder(photoFile)
            .build()
        
        imageCapture?.takePicture(
            outputOptions,
            ContextCompat.getMainExecutor(this),
            object : ImageCapture.OnImageSavedCallback {
                override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                    Toast.makeText(this@CameraActivity, 
                        "照片已保存: ${photoFile.absolutePath}", 
                        Toast.LENGTH_SHORT).show()
                }
                
                override fun onError(exception: ImageCaptureException) {
                    Log.e("CameraX", "拍照失败", exception)
                }
            }
        )
    }
}

我的习惯:拍照模式我一般用 CAPTURE_MODE_MINIMIZE_LATENCY,这样按快门几乎没有延迟。如果你需要最高画质,可以用 CAPTURE_MODE_MAXIMIZE_QUALITY,但快门会慢一点。

图像分析:实时处理每一帧

图像分析用例很有意思。它可以把相机预览的每一帧数据回调给你,让你做实时处理。比如扫码、人脸检测、滤镜等。

// 创建分析用例
val imageAnalysis = ImageAnalysis.Builder()
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
    .build()

imageAnalysis.setAnalyzer(Executors.newSingleThreadExecutor()) { imageProxy ->
    val buffer = imageProxy.planes[0].buffer
    val bytes = ByteArray(buffer.remaining())
    buffer.get(bytes)
    
    // 在这里处理图像数据
    // 比如做颜色分析、边缘检测等
    
    // 重要:处理完后必须关闭
    imageProxy.close()
}

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

这里有个坑,我曾经踩过:imageProxy 用完后一定要调用 close(),否则 CameraX 会认为你还在处理,不会给你下一帧数据。说白了,这就是一个背压机制——你处理得慢,帧率就降下来。

注意:图像分析的回调是在子线程执行的。不要在回调里直接更新 UI,记得切回主线程。另外,STRATEGY_KEEP_ONLY_LATEST 会丢弃积压的帧,只保留最新的,适合实时性要求高的场景。

自定义相机界面

默认的 PreviewView 就是一个普通的 View,你可以随意叠加 UI。我常用的布局结构是这样的:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <!-- 相机预览 -->
    <androidx.camera.view.PreviewView
        android:id="@+id/previewView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    
    <!-- 自定义遮罩层 -->
    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/camera_overlay" />
    
    <!-- 底部控制栏 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:orientation="horizontal"
        android:padding="24dp">
        
        <Button
            android:id="@+id/btnCapture"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:background="@drawable/btn_shutter"
            android:layout_gravity="center" />
        
        <Button
            android:id="@+id/btnSwitchCamera"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:text="切换"
            android:layout_gravity="center" />
    </LinearLayout>
</FrameLayout>

我个人习惯把拍照按钮做成圆形,中间带一个白色圆点,模仿原生相机的快门样式。切换摄像头按钮放在右下角,这样单手操作时拇指刚好够到。

切换摄像头

这个功能实现起来很简单:

private var cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

fun switchCamera() {
    cameraSelector = if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) {
        CameraSelector.DEFAULT_FRONT_CAMERA
    } else {
        CameraSelector.DEFAULT_BACK_CAMERA
    }
    
    // 重新绑定
    cameraProvider.unbindAll()
    cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
}

嗯,这里要注意:切换摄像头时,之前绑定的用例会自动解绑。你只需要重新 bindToLifecycle 就行。我在项目中遇到过一个问题——切换太快会导致闪一下黑屏。解决办法是在切换时加一个过渡动画,或者等新摄像头准备好再显示预览。

常见问题与避坑

问题 原因 解决方案
预览黑屏 PreviewView 未正确绑定 SurfaceProvider 检查 preview.setSurfaceProvider(previewView.surfaceProvider) 是否调用
拍照后图片旋转 未处理设备方向 使用 ImageCapture.Builder().setTargetRotation() 设置目标旋转角度
分析帧率低 分析器处理太慢 改用 STRATEGY_KEEP_ONLY_LATEST,或降低分辨率
应用崩溃 CameraX 版本不兼容 确保使用最新稳定版,检查 minSdk 是否 ≥ 21

我曾经踩过的坑:有一次在 Fragment 中使用 CameraX,切换 Fragment 时没有解绑,导致相机资源泄漏。后来我改成在 onDestroyView() 中调用 cameraProvider.unbindAll(),问题解决。记住:CameraX 虽然自动绑定生命周期,但如果你手动管理 Fragment,最好显式解绑。

性能优化建议

  • 降低预览分辨率:如果不需要高清预览,可以用 Preview.Builder().setTargetResolution() 设置 720p 甚至 480p,省电又流畅
  • 按需启动分析:图像分析很耗电,只在需要时绑定,用完就解绑
  • 使用合适的执行器:分析器用独立线程池,不要用主线程
  • 缓存拍照文件:用 File.createTempFile() 代替每次都新建文件,减少 IO 开销

CameraX 其实还有很多高级玩法,比如同时绑定多个分析器、使用扩展模式(HDR、夜景)等。但核心思路就这些——三个用例,组合使用,生命周期交给库管理。你只要把精力放在 UI 和业务逻辑上就行。

一句话总结:CameraX 让 Android 相机开发从「折腾底层 API」变成了「搭积木」。预览、拍照、分析,三个积木一拼,一个完整的相机功能就出来了。


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