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 的架构其实很清晰。我画了一张图,你看一眼就明白了:
看到没?最上层是 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