Jetpack Compose + CameraX+ MlKit 实现 二维码扫描(一)

498 阅读1分钟

(一)通过CameraX+Jetpack Compose 实现相机预览

前言

目前很多Android项目已经开始基于Jetpack Compose 进行开发了,项目中有个功能,需要扫描二维码,可以直接使用zxing,基于过往开发经验,zxing 的识别效率并没有mlkit 高,因此下面我们一步步完成MlKit版本的 二维码扫描功能。

技术选型

基于以上分析,技术选型如下:

功能技术
页面编写Jetpack Compose
二维码识别Mlkit
相机画面预览CameraX

最终效果示例:

out4.gif

build.gradle依赖

// CameraX
val cameraxVersion = "1.2.2"
api("androidx.camera:camera-lifecycle:$cameraxVersion")
api("androidx.camera:camera-core:$cameraxVersion")
api("androidx.camera:camera-camera2:$cameraxVersion")
api("androidx.camera:camera-view:$cameraxVersion")

api("androidx.camera:camera-extensions:$cameraxVersion")

// Jetpack Compose 相关依赖
api("androidx.activity:activity-compose:1.10.0")
api(platform("androidx.compose:compose-bom:2025.02.00"))
api("androidx.compose.ui:ui")
api("androidx.compose.ui:ui-graphics")
api("androidx.compose.ui:ui-tooling-preview")
api("androidx.compose.material:material")
api("androidx.compose.material3:material3")
api("androidx.compose.runtime:runtime")
api("androidx.compose.ui:ui-tooling")

因为Jetpack Compose 中没有直接可用的相机组件,因此,需要通过AndroidView 包装Androidx 中的androidx.camera.view.PreviewView 组件,实现CameraView 组件

CameraView.kt

package com.alan.scanbarcode.scan

import android.util.Log
import android.util.Size
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.zIndex
import androidx.core.content.ContextCompat

/**
 * 画面预览,拍照
 */
@Composable
fun CameraView() {
    // 上下文
    val context = LocalContext.current
    // 声明周期
    val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
    // 协程域
    val cameraProviderFuture = remember {
        ProcessCameraProvider.getInstance(context)
    }
    // 线程池
    val executor = remember { ContextCompat.getMainExecutor(context) }
    // View 系统CameraX 预览组件
    val previewView = remember { PreviewView(context) }
    // 预览
    val preview = remember {
        Preview
            .Builder()
            .build().also {
                it.setSurfaceProvider(previewView.surfaceProvider)
            }
    }
    // 摄像头选择
    val cameraSelector = remember {
        CameraSelector.Builder()
            .requireLensFacing(CameraSelector.LENS_FACING_BACK)
            .build()
    }
    val cameraProvider = remember { cameraProviderFuture.get() }
    // 使用executor 去执行上面的内容

    LaunchedEffect(Unit) {
        cameraProviderFuture.addListener({
            cameraProvider.unbindAll()
            cameraProvider.bindToLifecycle(
                lifecycleOwner,
                cameraSelector,
                preview,
            )

        }, executor)
    }

    Box(modifier = Modifier.zIndex(1f)) {
        AndroidView(
            factory = { previewView },
            modifier = Modifier
                .fillMaxSize()
        )
    }
}

实现扫描页面

ScanPage.kt

var offset by remember { mutableStateOf(0f) }
// 无限重复动画
val infiniteTransition = rememberInfiniteTransition()
offset = infiniteTransition.animateFloat(
    initialValue = 0f,
    targetValue = 1f,
    animationSpec = infiniteRepeatable(
        animation = tween(durationMillis = 1500, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    )
).value



    Scaffold(modifier = Modifier.fillMaxSize()) {
        Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
            CameraView()
            Box(
                modifier = Modifier
                    .size(200.dp)
                    .zIndex(1000f)
                    .border(1.dp, color = Color.Blue),
                contentAlignment = Alignment.Center,
            ) {
// 扫描线
                Divider(
                    color = Color.Green.copy(alpha = 0.7f),
                    thickness = 2.dp,
                    modifier = Modifier
                        .fillMaxWidth()
                        .offset(y = 200.dp * offset - 100.dp)
                        .shadow(4.dp, shape = RectangleShape)
                )
            }

        }
    }