В этой статье рассмотрим, как сканировать баркоды в 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/
Добавить комментарий