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

от автора

В этой статье рассмотрим, как сканировать баркоды в 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. При использовании внешнего сканера мы работаем с обычным полем ввода. Пример кода выложен здесь.


ссылка на оригинал статьи https://habr.com/ru/articles/901630/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *