Compose使用Camerax结合Zxing与MLKit实现二维码扫描

80 阅读4分钟

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对比

  • 核心功能对比
特性ZxingMLKit
支持格式支持多种条码(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
      }
    }
  }
}

img003_1.png