27、LiveData与CameraX:相机预览帧数据、使用LiveData传递图片数据、实战:拍照应用
说实话,CameraX 这个库刚出来的时候,我内心是拒绝的。毕竟之前用 Camera2 写预览、调参数,那叫一个酸爽——回调套回调,状态机写到手抽筋。后来项目里要做一个扫码功能,我硬着头皮试了试 CameraX,嗯,真香。
这一章咱们就聊聊,怎么把 LiveData 和 CameraX 结合起来,做一个完整的拍照应用。说白了,就是用 LiveData 这把「数据快递」的利器,把相机预览帧和拍下来的图片,安全高效地送到 UI 层。
27.1 CameraX 的核心概念
先简单过一下 CameraX 的几个关键角色,不然你后面看代码可能会懵。
| 组件 | 作用 |
|---|---|
| ProcessCameraProvider | 生命周期绑定器,把相机和 LifecycleOwner 绑在一起 |
| Preview | 预览用例,把画面显示到 PreviewView 上 |
| ImageCapture | 拍照用例,拍一张照片 |
| ImageAnalysis | 分析用例,每一帧回调给你处理 |
我个人习惯把这三个用例看成「三兄弟」:Preview 负责显示,ImageCapture 负责拍照,ImageAnalysis 负责分析。它们可以同时运行,互不干扰。
27.2 用 LiveData 传递预览帧数据
先来看一个常见场景:实时分析相机画面,比如扫码、人脸检测。这时候 ImageAnalysis 就派上用场了。
我记得第一次做实时帧分析时,直接在分析器里更新 UI,结果卡成 PPT。后来才意识到,分析线程和主线程是两码事,必须用 LiveData 做桥梁。
核心思路:在 ImageAnalysis.Analyzer 的 analyze() 方法中,把帧数据 post 到 LiveData 上,UI 层观察这个 LiveData 即可。
class FrameAnalyzer : ImageAnalysis.Analyzer {
private val _frameData = MutableLiveData<Bitmap>()
val frameData: LiveData<Bitmap> = _frameData
override fun analyze(image: ImageProxy) {
// 把 ImageProxy 转成 Bitmap
val bitmap = imageProxyToBitmap(image)
// 发到主线程
_frameData.postValue(bitmap)
// 别忘了关闭 image,否则会卡死
image.close()
}
private fun imageProxyToBitmap(image: ImageProxy): Bitmap {
// 这里省略 YUV 转 Bitmap 的细节
// 实际项目中可以用 RenderScript 或 YuvImage
return Bitmap.createBitmap(/* ... */)
}
}
小提示:postValue() 和 setValue() 的区别你肯定知道——postValue 可以在子线程调用,setValue 只能在主线程。分析器运行在后台线程,所以必须用 postValue。
然后在 ViewModel 里暴露这个 LiveData:
class CameraViewModel : ViewModel() {
private val analyzer = FrameAnalyzer()
// 对外暴露不可变的 LiveData
val frameData: LiveData<Bitmap> = analyzer.frameData
fun getAnalyzer(): ImageAnalysis.Analyzer = analyzer
}
UI 层观察:
viewModel.frameData.observe(viewLifecycleOwner) { bitmap ->
// 更新到 ImageView 或者做其他处理
binding.ivPreview.setImageBitmap(bitmap)
}
你看,这样就把相机帧数据和 UI 层解耦了。分析器只管生产数据,UI 层只管消费数据,中间通过 LiveData 自动切换线程。
27.3 用 LiveData 传递拍照图片
拍照的逻辑其实更简单。ImageCapture 拍完照会回调一个 OnImageCapturedCallback,我们在回调里把图片数据塞进 LiveData。
我曾经踩过一个坑:拍照回调里直接更新 UI,结果 Activity 重建后图片丢了。用 LiveData 就能完美解决——数据跟着 ViewModel 走,配置变更也不怕。
class CameraViewModel : ViewModel() {
private val _capturedImage = MutableLiveData<Bitmap>()
val capturedImage: LiveData<Bitmap> = _capturedImage
private val imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.build()
fun takePhoto() {
imageCapture.takePicture(
ContextCompat.getMainExecutor(appContext),
object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
val bitmap = imageProxyToBitmap(image)
_capturedImage.postValue(bitmap)
image.close()
}
override fun onError(exception: ImageCaptureException) {
// 错误处理
}
}
)
}
}
注意:takePicture 的回调可以指定执行线程。我建议用主线程执行器,这样 setValue 和 postValue 都可以用。但如果你在回调里做耗时操作,记得切到后台线程。
27.4 实战:完整拍照应用
好,理论说完了,咱们动手写一个完整的拍照应用。麻雀虽小五脏俱全,包含预览、拍照、显示结果三个功能。
27.4.1 项目结构
先看看整体架构,我画了一张图帮你理解:
27.4.2 核心代码实现
首先是布局文件,很简单:一个 PreviewView 用于预览,一个 Button 用于拍照,一个 ImageView 用于显示拍好的照片。
<!-- activity_main.xml -->
<androidx.constraintlayout.widget.ConstraintLayout
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="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/btnCapture" />
<Button
android:id="@+id/btnCapture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="拍照"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<ImageView
android:id="@+id/ivResult"
android:layout_width="100dp"
android:layout_height="100dp"
android:scaleType="centerCrop"
android:layout_margin="16dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
然后是 ViewModel,我们把前面两个功能合并到一起:
class CameraViewModel : ViewModel() {
// 预览帧数据
private val _frameData = MutableLiveData<Bitmap>()
val frameData: LiveData<Bitmap> = _frameData
// 拍照结果
private val _capturedImage = MutableLiveData<Bitmap>()
val capturedImage: LiveData<Bitmap> = _capturedImage
// 相机相关
private var imageCapture: ImageCapture? = null
private var cameraProvider: ProcessCameraProvider? = null
fun startCamera(context: Context, lifecycleOwner: LifecycleOwner, previewView: PreviewView) {
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
cameraProvider = cameraProviderFuture.get()
// 预览
val preview = Preview.Builder().build()
preview.setSurfaceProvider(previewView.surfaceProvider)
// 拍照
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.build()
// 分析
val analysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
analysis.setAnalyzer(Executors.newSingleThreadExecutor(), createFrameAnalyzer())
// 绑定生命周期
cameraProvider?.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
imageCapture,
analysis
)
}, ContextCompat.getMainExecutor(context))
}
private fun createFrameAnalyzer(): ImageAnalysis.Analyzer {
return ImageAnalysis.Analyzer { image ->
val bitmap = imageProxyToBitmap(image)
_frameData.postValue(bitmap)
image.close()
}
}
fun takePhoto() {
imageCapture?.takePicture(
ContextCompat.getMainExecutor(appContext),
object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
val bitmap = imageProxyToBitmap(image)
_capturedImage.postValue(bitmap)
image.close()
}
override fun onError(exception: ImageCaptureException) {
// 处理错误
}
}
)
}
override fun onCleared() {
super.onCleared()
cameraProvider?.unbindAll()
}
}
最后是 Activity 里的调用:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: CameraViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 启动相机
viewModel.startCamera(this, this, binding.previewView)
// 观察拍照结果
viewModel.capturedImage.observe(this) { bitmap ->
binding.ivResult.setImageBitmap(bitmap)
}
// 拍照按钮
binding.btnCapture.setOnClickListener {
viewModel.takePhoto()
}
}
}
个人经验:startCamera 方法里我用了 STRATEGY_KEEP_ONLY_LATEST 策略。什么意思呢?就是如果分析器处理不过来,就丢掉旧的帧,只保留最新的。这样能保证 UI 上看到的是最新的画面,不会越积越多。
27.5 避坑指南
嗯,这里要重点说说我踩过的几个坑,你遇到了可以少走弯路。
- ImageProxy 必须 close:我曾经忘记在 analyze 方法里调用 image.close(),结果相机预览卡死,报错说「ImageProxy 未关闭」。记住,每次分析完或者拍完照,一定要 close。
- 生命周期绑定:CameraX 虽然会自动处理生命周期,但如果你在 onPause 之后还去调用 takePhoto,可能会 crash。建议在 ViewModel 里加一个状态判断。
- Bitmap 转换性能:YUV 转 Bitmap 是个耗时操作,我建议用 RenderScript 或者直接使用 CameraX 的 ImageProxy 的 planes 数据,不要每次都 new 一个 Bitmap。
- LiveData 的粘性事件:如果你用 LiveData 传递拍照结果,旋转屏幕后可能会再次触发观察者。这时候可以用 SingleLiveEvent 或者 Kotlin 的 Channel 来解决。
特别注意:CameraX 的 ImageAnalysis 分析器运行在后台线程,千万不要在里面更新 UI。所有 UI 更新必须通过 LiveData 切回主线程。这是新手最容易犯的错误。
好了,这一章的内容就到这里。LiveData 和 CameraX 的结合,说白了就是「生产者-消费者」模式的一种体现。你只要记住:CameraX 负责生产数据,LiveData 负责运输数据,UI 层负责消费数据。各司其职,代码自然清晰。
如果你在实际项目中遇到什么问题,欢迎交流。毕竟,相机开发这条路,坑多路滑,咱们一起趟过去。
公众号:蓝海资料掘金营,微信deep3321