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