22、CameraX:CameraX架构、Preview、ImageCapture、ImageAnalysis、实战:相机应用开发
说到Android上的相机开发,我猜不少朋友都跟我一样,踩过Camera1和Camera2的坑。Camera1功能太弱,Camera2又复杂得要命——光是一个CameraCaptureSession的回调嵌套就能让你怀疑人生。直到Google推出了CameraX,我才觉得,嗯,这才是现代Android该有的样子。
CameraX是Jetpack家族的一员。它基于Camera2封装,但用起来简单太多了。说白了,它就是帮你把那些繁琐的状态管理、生命周期绑定、设备兼容性问题都处理掉了。你只需要关心业务逻辑就行。
CameraX的核心架构
CameraX的架构其实不复杂。它围绕几个核心概念展开:CameraProvider、ProcessCameraProvider、UseCase,以及CameraSelector。
我画了一张图,帮你快速理解它们之间的关系:
你看,整个流程很清晰:ProcessCameraProvider是核心入口,它负责把LifecycleOwner、CameraSelector和各个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 = fitCenter 或 centerCrop。我一般用 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。这个坑我替你踩过了,你就别再踩一次了。