camerax中提供了camera-compose库,用于在compose中使用camerax来实现扫描二维码。该库提供了一个CameraXViewfinder组件,这是一个可组合的适配器,它通过完成提供的SurfaceRequest来显示来自CameraX的帧。它是一个Viewfinder的封装器,它会在内部将CameraX SurfaceRequest转换为ViewfinderSurfaceRequest。此外,所有通常通过ViewfinderSurfaceRequest处理的交互都将从SurfaceRequest派生而来。
配置相关依赖
在gradle中配置camerax相关的依赖
dependencies {
implementation("androidx.camera:camera-core:1.5.1")
implementation("androidx.camera:camera-camera2:1.5.1")
implementation("androidx.camera:camera-compose:1.5.1")
implementation("androidx.camera:camera-lifecycle:1.5.1")
implementation("com.google.zxing:core:3.5.3")
implementation("com.google.mlkit:barcode-scanning:17.3.0")
}
core库提供了camerax的核心,camera2是camerax的具体实现,compose库提供了CameraXViewfinder组件,lifecycle用于管理生命周期,zxing和mlkit用于二维码的扫描。
Zxing与MLKit对比
- 核心功能对比
| 特性 | Zxing | MLKit |
|---|---|---|
| 支持格式 | 支持多种条码(QR Code、UPC、EAN等) | 支持多种条码(QR Code、Data Matrix等) |
| 离线支持 | 完全离线 | 部分功能需依赖 Google Play 服务 |
| 集成复杂度 | 中等(需手动配置解码逻辑) | 简单(API 封装完善) |
| 扫描速度 | 较快 | 极快(基于设备硬件加速) |
| 准确性 | 较高(依赖光照条件) | 极高(支持动态调整和机器学习优化) |
| 多码识别 | 不支持 | 支持(可同时识别多个二维码) |
- 性能对比
Zxing采用纯Java/Kotlin实现,不依赖外部服务,适合对隐私要求高的场景(完全离线)。但在复杂背景或低光照下识别率有所下降,需要手动优化解码参数。MLKit基于Google的机器学习模型,自动优化识别效果,支持多码识别,实时性非常高。
如果项目需要完全离线,对扫描速度要求不高,场景简单,可以选择Zxing。如果需要高精度、多码识别或动态环境支持,则选择MLKit。
使用Zxing/MLKit实现扫码功能
通过ImageAnalysis获取到图像Bitmap后,再通过Zxing/MLKit进行识别分析,也可以通过其它工具进行识别。首先定义一个接口用于分析Bitmap,再分别用Zxing和MLKit去实现具体的分析逻辑。
interface QrcodeAdapter {
fun onScan(bitmap: Bitmap, result: Consumer<String>)
}
class ZxingQrcodeAdapter(private val reader: MultiFormatReader = MultiFormatReader()) : QrcodeAdapter {
override fun onScan(bitmap: Bitmap, result: Consumer<String>) {
IntArray(bitmap.width * bitmap.height).let { pixels ->
bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)
runCatching {
reader.decode(BinaryBitmap(HybridBinarizer(RGBLuminanceSource(bitmap.width, bitmap.height, pixels)))).also {
if (it.text.isNotEmpty()) {
result.accept("zxing scan result is ${it.text}")
}
}
}
}
}
}
class MlQrcodeAdapter(private val client: BarcodeScanner = BarcodeScanning.getClient()) : QrcodeAdapter {
override fun onScan(bitmap: Bitmap, result: Consumer<String>) {
client.process(InputImage.fromBitmap(bitmap, 0)).addOnSuccessListener { list ->
if (list.isNotEmpty()) {
list[0].rawValue?.takeUnless { it.isEmpty() }?.also {
result.accept("mlkit scan result is $it")
}
}
}
}
}
在setAnalyzer方法中拿到Bitmap后,调用QrcodeAdapter的onScan方法进行分析识别,并将结果通过Consumer回调给调用方。
权限申请
拍摄照片需要相机权限,如果是录视频,还需要录音权限。先在AndroidManifest中申明,然后在使用的地方动态申请。
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
动态申请权限
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {}
launcher.launch(arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO))
CameraXViewfinder绑定相机并预览画面
CameraXViewfinder的使用非常简单,只需要传入一个SurfaceRequest参数就可以了。
@Composable
fun cameraLayout() {
CameraXViewfinder(
surfaceRequest = surfaceRequest,
modifier = Modifier.fillMaxSize()
)
}
surfaceRequest的获取需要绑定相机,并在预览中获取。首先使用ProcessCameraProvider将摄像头的生命周期绑定到应用程序进程内的任何LifecycleOwner上,一个进程中只能存在一个进程摄像头提供。程序重量级资源(例如已打开并正在运行的摄像头设备)的作用域将限定在bindToLifecycle提供的生命周期内。其他轻量级资源(例如静态摄像头特性)可以在首次使用getInstance检索此提供程序时被检索并缓存,并在进程的整个生命周期内保持有效。示例如下
fun cameraLayout(onResult: Consumer<String>) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val viewModel: CameraViewModel = viewModel<CameraViewModel>()
val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
viewModel.bindToCamera(lifecycleOwner,onResult)
}
surfaceRequest?.also {
CameraXViewfinder(
surfaceRequest = it,
modifier = Modifier.fillMaxSize()
)
}
}
UseCaseGroup用于管理一组用例集合,当用例组绑定到生命周期时,它会将所有用例绑定到同一个生命周期。用例组内的用例通常共享一些公共属性,例如由视口定义的视野范围。camerax使用不同的用例来管理相机,例如提供相机预览能力的Preview用例,以及提供录图像分析能力的ImageAnalysis用例。最后processCameraProvider调用bindToLifecycle绑定生命周期,这里使用CameraSelector.DEFAULT_BACK_CAMERA后置摄像头进行预览。
使用ImageAnalysis获取图像数据并进行分析
ImageAnalysis图像分析通过ImageReader从摄像头获取图像。每张图像都会被传递给ImageAnalysis.Analyzer函数,该函数可由应用程序代码实现,并通过ImageProxy访问图像数据以进行应用程序分析。 应用程序负责调用close函数来关闭图像。如果未能关闭图像,则后续图像的加载将会停滞或丢弃,具体取决于反压策略。
class CameraViewModel(application: Application) : AndroidViewModel(application) {
private val _surfaceRequest = MutableStateFlow<SurfaceRequest?>(null)
val surfaceRequest = _surfaceRequest.asStateFlow()
suspend fun bindToCamera(lifecycleOwner: LifecycleOwner, onResult: Consumer<String>) {
val zxingAdapter = ZxingQrcodeAdapter()
val mlAdapter = MlQrcodeAdapter()
val provider = ProcessCameraProvider.awaitInstance(application)
val group = UseCaseGroup.Builder()
.addUseCase(Preview.Builder().build().apply {
setSurfaceProvider { _surfaceRequest.value = it }
})
.addUseCase(
ImageAnalysis.Builder().setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888).build().apply {
setAnalyzer(CameraXExecutors.ioExecutor()) { proxy ->
proxy.use {
val bitmap = it.toBitmap()
zxingAdapter.onScan(bitmap, onResult)
mlAdapter.onScan(bitmap, onResult)
}
}
}).build()
provider.bindToLifecycle(lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, group).also {
cameraControl = it.cameraControl
try {
awaitCancellation()
} finally {
provider.unbindAll()
cameraControl = null
}
}
}
}