Как стать автором
Обновить
382.8
Сбер
Технологии, меняющие мир

Сканирование баркодов c помощью камеры и внешних устройств в Compose

Уровень сложностиСредний
Время на прочтение6 мин
Количество просмотров974

В этой статье рассмотрим, как сканировать баркоды в Android- приложениях, как в Compose работать с камерой (предпросмотр и логика сканирования), а также как поддерживать внешние сканеры, в ситуациях, когда сканирование происходит без камеры и мы не управляем источником результата

Сканирование barcode

Для сканирования баркодов рассмотрим библиотеку от Google barcode-scanning. Сначала нужно подключить её к проекту:

dependencies {
  // ...
  // Use this dependency to bundle the model with your app
  implementation 'com.google.mlkit:barcode-scanning:17.3.0'
}

Далее нужно указать, какие типы кодов кодов будем считывать:

val options = BarcodeScannerOptions.Builder()
        .setBarcodeFormats(
                Barcode.FORMAT_QR_CODE,
                Barcode.FORMAT_AZTEC)
        .build()

Типов может быть много:

  • Код 128 ( FORMAT_CODE_128 )

  • Код 39 ( FORMAT_CODE_39 )

  • Код 93 ( FORMAT_CODE_93 )

  • Кодабар ( FORMAT_CODABAR )

  • EAN-13 ( FORMAT_EAN_13 )

  • EAN-8 ( FORMAT_EAN_8 )

  • ITF ( FORMAT_ITF )

  • СКП-А ( FORMAT_UPC_A )

  • UPC-E ( FORMAT_UPC_E )

  • QR-код ( FORMAT_QR_CODE )

  • PDF417 ( FORMAT_PDF417 )

  • Ацтекский ( FORMAT_AZTEC )

  • Матрица данных ( FORMAT_DATA_MATRIX )

Теперь нужно сделать связку между камерой и библиотекой сканирования баркодов. Схематично выглядит так:

Проще говоря, мы получаем данные с камеры, отправляем на вход библиотеки анализа баркодов и подписываемся на получение результата, после чего обрабатываем.

Для примера рассмотрим структуру экрана:

На экране отображается: зона предпросмотра сканера и переключатель режима. При нажатии на переключатель на экран выводится предпросмотр камеры или интерфейс для работы с внешним сканером.

Сканирование с помощью камеры

Рассмотрим, как это описывается с помощью кода. Вначале подключаем библиотеку камеры:

implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.compose)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.camera2)

Далее описываем работу с камерой. Нам нужен предпросмотр в Compose. Функция будет выглядеть так:

@Composable
fun CameraPreview(
    modifier: Modifier = Modifier,
    scaleType: PreviewView.ScaleType = PreviewView.ScaleType.FILL_CENTER,
    cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA,
    barCodeListener: (barCode: String) -> Unit
) {
    val coroutineScope = rememberCoroutineScope()
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val cameraProviderFuture =
        remember(context) { ProcessCameraProvider.getInstance(context) }
    val executor = remember(context) { ContextCompat.getMainExecutor(context) }
    val previewView = PreviewView(context).apply {
        this.scaleType = scaleType
        layoutParams = ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.WRAP_CONTENT
        )
    }


    AndroidView(
        modifier = modifier,
        factory = { _ ->
            cameraProviderFuture.addListener({

                // ImageAnalysis
                imageAnalyzer = ImageAnalysis.Builder()
                    .setBackpressureStrategy(ImageAnalysis.STRATEGY_BLOCK_PRODUCER)

                    .build()
                    .also {
                        it.setAnalyzer(cameraExecutor,
                            BarcodeAnalyzer() { barcode ->
                                available.acquire()
                                barCodeListener(barcode)
                                available.release()
                            })
                    }
            }, executor)

            // Preview
            val previewUseCase = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(previewView.surfaceProvider)
                }

            coroutineScope.launch {
                val cameraProvider = context.getCameraProvider()
                try {
                    // Must unbind the use-cases before rebinding them.
                    cameraProvider.unbindAll()

                    cameraProvider.bindToLifecycle(
                        lifecycleOwner, cameraSelector, previewUseCase, imageAnalyzer
                    )

                } catch (ex: Exception) {
                    Log.e("CameraPreview", "Use case binding failed", ex)
                }
            }
            previewView
        }
    )
}

Для предпросмотра камеры используем обвёртку с AndroidView. На текущий момент в альфа-версии библиотеки camerax появилась возможность реализовать предпросмотр полностью на Compose, но здесь мы этого не рассматриваем.  В приведённом выше примере кода есть класс BarcodeAnalyzer, в котором реализована логика анализа баркодов:

class BarcodeAnalyzer(
    val listener: (barcode: String) -> Unit
) :  ImageAnalysis.Analyzer {
    private var isBusy = AtomicBoolean(false)

    @SuppressLint("UnsafeExperimentalUsageError", "UnsafeOptInUsageError")
    override fun analyze(image: ImageProxy) {
        if (isBusy.compareAndSet(false, true)) {
            image.image?.let { imageItem ->
                val visionImage =
                    InputImage.fromMediaImage(imageItem, image.imageInfo.rotationDegrees)
                val options = BarcodeScannerOptions.Builder().enableAllPotentialBarcodes().build()
                BarcodeScanning.getClient(options).process(visionImage)
                    .addOnCompleteListener { task ->
                        if (task.isSuccessful) {
                            task.result?.let { barcodes ->
                                for (barcode in barcodes) {
                                    listener(barcode.rawValue ?: "")
                                }
                            }
                        } else {
                            Log.e("scanner", "failed to scan image: ${task.exception?.message}")
                        }
                        image.close()
                        isBusy.set(false)
                    }
            }
        } else {
            image.close()
        }
    }
}

Этот класс реализует интерфейс ImageAnalysis.Analyzer в методе analyze. Он отвечает за логику анализирования результата и вызов callback-функции в случае успеха.

Мы рассмотрели работу с камерой при сканировании баркодов. Теперь добавим поддержку внешних сканеров и опишем сам экран: 

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            var resultScan by remember { mutableStateOf("") }
            var usingExternalScan by remember { mutableStateOf(true) }
            var scanning by remember { mutableStateOf(true) }
            BarCodeScanTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Column(
                        modifier = Modifier.padding(innerPadding),
                        verticalArrangement = Arrangement.spacedBy(32.dp)
                    ) {
                        if (!usingExternalScan) {
                            CameraPreviewScreen(
                                resultScan = { barCode ->
                                    if (scanning) {
                                        resultScan = barCode
                                        viewModel.action(ScanActions.Camera(number = barCode))
                                    }
                                }
                            )
                        } else {
                            ExternalScanner(
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .height(350.dp),
                                result = { barCode ->
                                    if (scanning) {
                                        resultScan = barCode
                                        viewModel.action(ScanActions.External(number = barCode))
                                    }
                                }
                            )

                        }
                        Spacer(Modifier.height(100.dp))

                        Text(
                            modifier = Modifier.fillMaxWidth(),
                            text = resultScan,
                            textAlign = TextAlign.Center
                        )

                        ExternalScannerToggle(text = "Using External Scanner",
                            modifier = Modifier.padding(start = 16.dp, end = 16.dp),
                            callBack = { isUsing ->
                                usingExternalScan = isUsing
                            })
                    }
                }
            }

            val uiState by viewModel.stateFlow.collectAsState()
            when (uiState) {
                ScanState.HandlingResult -> {
                    scanning = false
                }

                ScanState.Scanning -> {
                    scanning = true
                    resultScan = ""
                }
            }
        }
    }
}

В нашем простом случае есть переключатель toggle, который меняет режим работы между камерой и внешним сканером. Для работы с камерой вначале проверяются разрешения, после их получения показываем предпросмотр камеры. State с viewModel переключает режимы:

  • сканирование и отправка результата для обработки;

  • игнорирование получения результата сканирования, пока не завершится обработка предыдущего, чтобы не спамить обработчик. 

Сканирование с помощью внешнего устройства

При получении данных от внешнего сканера мы просто работаем с полем ввода, чаще всего без отображения в интерфейса клавиатуры, и показываем только вводимое значения. Например:

@Composable
fun ExternalScanner(
    modifier: Modifier = Modifier,
    result: (String) -> Unit = {}
) {
    val textValue = remember {
        mutableStateOf("")
    }

    val focusRequester = remember { FocusRequester() }

    Box(
        modifier = modifier
            .background(color = Color.Blue)
    ) {
        OutlinedTextField(
            value = textValue.value,
            onValueChange = {
                textValue.value = it.trim()
                result.invoke(it)
            },
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp)
                .align(Alignment.Center)
                .focusRequester(focusRequester),
            colors = TextFieldDefaults.colors(
                focusedIndicatorColor = Color.Transparent,
                unfocusedIndicatorColor = Color.Transparent
            )
        )
    }

    LaunchedEffect(Unit) {
        focusRequester.requestFocus()
    }
}

При работе с внешним сканером вам, скорей всего, понадобится удерживать фокус на поле ввода и скрыть клавиатуру при вводе значения в поле.  Рассмотрим вид viewModel нашего экрана:

class ScanViewModel : ViewModel() {

    private val _stateFlow: MutableStateFlow<ScanState> = MutableStateFlow(ScanState.Scanning)

    val stateFlow: StateFlow<ScanState> = _stateFlow

    private val text = MutableStateFlow("")

    fun action(actions: ScanActions) {
        when (actions) {
            is ScanActions.Camera -> {
                _stateFlow.value = ScanState.HandlingResult
                viewModelScope.launch {
                    handleResult()
                }
            }

            is ScanActions.External -> {
                viewModelScope.launch {
                    text.debounce(2000)
                        .distinctUntilChanged()
                        .collect {
                            _stateFlow.value = ScanState.HandlingResult
                            handleResult()
                        }
                }
            }
        }
    }

    private suspend fun handleResult() {
        // handler result simulation
        delay(1000)
        _stateFlow.value = ScanState.Scanning
    }
}

При обработке результата с камеры мы игнорируем получение других результатов сканирования, пока обрабатываем текущее. После завершения получаем на обработку новые значения.

При работе с внешним сканером с помощью debounce ждём завершения ввода и запускаем обработку результата.

Резюме

Мы рассмотрели логику сканирования баркодов c помощью библиотеки barcode-scanning в двух режимах: при использовании встроенной камеры или внешнего устройства. Предпросмотр камеры мы реализовали с обвёрткой view в Compose, но с выходом новой версии camerax можно реализовать полностью на Compose. При использовании внешнего сканера мы работаем с обычным полем ввода. Пример кода выложен здесь.

Теги:
Хабы:
+12
Комментарии0

Информация

Сайт
www.sber.ru
Дата регистрации
Дата основания
Численность
свыше 10 000 человек
Местоположение
Россия