Añadir la lectura de codigo de barras
This commit is contained in:
Generated
+1
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
|
||||
Generated
+1
-1
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
||||
@@ -5,7 +5,9 @@ Android application developed in Kotlin using Jetpack Compose that scans barcode
|
||||
## 🚀 Features
|
||||
|
||||
- **Instant Scan**: The camera opens automatically upon application startup.
|
||||
- **ML Kit Integration**: High-performance barcode detection using Google ML Kit.
|
||||
- **Visual Overlay**: Centered scanning guide with a red line to help align barcodes.
|
||||
- **ML Kit Integration**: High-performance barcode detection (HD 720p analysis).
|
||||
- **Confirmation Flow**: Scanned results are shown in a popup dialog before saving.
|
||||
- **MariaDB Connectivity**: Direct connection to a remote database via JDBC.
|
||||
- **Modern UI**: Built with Jetpack Compose and Material 3.
|
||||
- **Permission Handling**: Automatic runtime camera permission requests.
|
||||
@@ -16,6 +18,13 @@ Android application developed in Kotlin using Jetpack Compose that scans barcode
|
||||
- MariaDB Server accessible from the mobile device network.
|
||||
- Camera hardware.
|
||||
|
||||
## 📱 User Interface
|
||||
|
||||
The app features a professional scanning interface:
|
||||
1. **Scanner Guide**: A white square frame indicates the target area.
|
||||
2. **Detection Feedback**: Once a code is read, a popup dialog appears showing the value.
|
||||
3. **Database Actions**: Users can choose to "Guardar" (Save) to the database or "Cerrar" (Dismiss) to scan again.
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
### 1. Database Setup
|
||||
@@ -50,8 +59,8 @@ private const val PASSWORD = "YOUR_PASSWORD"
|
||||
|
||||
## 📦 Dependencies
|
||||
|
||||
- **CameraX**: For camera preview and frame capture.
|
||||
- **ML Kit Barcode Scanning**: For processing barcodes.
|
||||
- **CameraX**: For camera preview and high-resolution frame capture.
|
||||
- **ML Kit Barcode Scanning**: For processing all barcode formats.
|
||||
- **MariaDB Java Client**: JDBC driver for database communication.
|
||||
- **Coroutines**: For asynchronous database operations.
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.example.saipp
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
@@ -12,9 +13,12 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -71,6 +75,9 @@ fun MainScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
var scannedBarcode by remember { mutableStateOf<String?>(null) }
|
||||
var isSaving by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -78,27 +85,15 @@ fun MainScreen() {
|
||||
.padding(innerPadding)
|
||||
) {
|
||||
if (hasCameraPermission) {
|
||||
var isProcessing by remember { mutableStateOf(false) }
|
||||
|
||||
// Only scan if no barcode is currently being shown in the popup
|
||||
if (scannedBarcode == null) {
|
||||
BarcodeScannerView(onBarcodeDetected = { barcode ->
|
||||
if (!isProcessing) {
|
||||
isProcessing = true
|
||||
scope.launch {
|
||||
val result = repository.saveBarcode(barcode)
|
||||
result.onSuccess {
|
||||
Toast.makeText(context, "Guardado: $barcode", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
result.onFailure { e ->
|
||||
Toast.makeText(context, "Error: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
// Add a small delay to avoid multiple triggers for the same barcode
|
||||
kotlinx.coroutines.delay(2000)
|
||||
isProcessing = false
|
||||
}
|
||||
}
|
||||
Log.d("MainActivity", "Barcode detected: $barcode")
|
||||
scannedBarcode = barcode
|
||||
})
|
||||
}
|
||||
|
||||
if (isProcessing) {
|
||||
if (isSaving) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
@@ -109,6 +104,51 @@ fun MainScreen() {
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
|
||||
// Popup Dialog for scanned results
|
||||
scannedBarcode?.let { barcode ->
|
||||
ResultDialog(
|
||||
barcode = barcode,
|
||||
onDismiss = { scannedBarcode = null },
|
||||
onConfirm = {
|
||||
scannedBarcode = null
|
||||
isSaving = true
|
||||
scope.launch {
|
||||
val result = repository.saveBarcode(barcode)
|
||||
isSaving = false
|
||||
result.onSuccess {
|
||||
Toast.makeText(context, "Guardado con éxito", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
result.onFailure { e ->
|
||||
Toast.makeText(context, "Error al guardar: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ResultDialog(
|
||||
barcode: String,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(text = "Código Detectado") },
|
||||
text = { Text(text = "Se ha leído el código:\n\n$barcode\n\n¿Deseas guardarlo en la base de datos?") },
|
||||
confirmButton = {
|
||||
Button(onClick = onConfirm) {
|
||||
Text("Guardar")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Cerrar")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.example.saipp.ui.scanner
|
||||
|
||||
import android.util.Log
|
||||
import android.util.Size
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.camera.core.CameraSelector
|
||||
@@ -9,15 +10,32 @@ import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.Preview
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.RoundRect
|
||||
import androidx.compose.ui.graphics.ClipOp
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
import com.google.mlkit.vision.common.InputImage
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@@ -29,22 +47,35 @@ fun BarcodeScannerView(
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val cameraExecutor = remember { Executors.newSingleThreadExecutor() }
|
||||
val barcodeScanner = remember { BarcodeScanning.getClient() }
|
||||
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
PreviewView(ctx).apply {
|
||||
// Use updated state to ensure the analyzer always has the latest callback
|
||||
val currentOnBarcodeDetected by rememberUpdatedState(onBarcodeDetected)
|
||||
|
||||
val options = BarcodeScannerOptions.Builder()
|
||||
.setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
|
||||
.build()
|
||||
val barcodeScanner = remember { BarcodeScanning.getClient(options) }
|
||||
|
||||
val previewView = remember {
|
||||
PreviewView(context).apply {
|
||||
scaleType = PreviewView.ScaleType.FILL_CENTER
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
update = { previewView ->
|
||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
||||
}
|
||||
|
||||
// Cleanup resources when the composable is disposed
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
barcodeScanner.close()
|
||||
cameraExecutor.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(lifecycleOwner) {
|
||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
||||
cameraProviderFuture.addListener({
|
||||
val cameraProvider = cameraProviderFuture.get()
|
||||
|
||||
@@ -53,10 +84,18 @@ fun BarcodeScannerView(
|
||||
}
|
||||
|
||||
val imageAnalysis = ImageAnalysis.Builder()
|
||||
.setTargetResolution(Size(1280, 720)) // HD resolution for better detection
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build()
|
||||
.also { analysis ->
|
||||
var lastLogTime = 0L
|
||||
analysis.setAnalyzer(cameraExecutor) { imageProxy ->
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - lastLogTime > 2000) { // Log every 2 seconds
|
||||
Log.d("BarcodeScanner", "Analyzer: Processing frame. Rotation: ${imageProxy.imageInfo.rotationDegrees}")
|
||||
lastLogTime = currentTime
|
||||
}
|
||||
|
||||
val mediaImage = imageProxy.image
|
||||
if (mediaImage != null) {
|
||||
val image = InputImage.fromMediaImage(
|
||||
@@ -68,7 +107,8 @@ fun BarcodeScannerView(
|
||||
.addOnSuccessListener { barcodes ->
|
||||
for (barcode in barcodes) {
|
||||
barcode.rawValue?.let { value ->
|
||||
onBarcodeDetected(value)
|
||||
Log.d("BarcodeScanner", "Barcode detected: $value")
|
||||
currentOnBarcodeDetected(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,10 +134,63 @@ fun BarcodeScannerView(
|
||||
preview,
|
||||
imageAnalysis
|
||||
)
|
||||
Log.d("BarcodeScanner", "Camera and Analysis bound successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e("BarcodeScanner", "Use case binding failed", e)
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(context))
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
AndroidView(
|
||||
factory = { previewView },
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
ScannerOverlay()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScannerOverlay() {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
val canvasWidth = size.width
|
||||
val canvasHeight = size.height
|
||||
val boxWidth = canvasWidth * 0.7f
|
||||
val boxHeight = boxWidth
|
||||
val left = (canvasWidth - boxWidth) / 2
|
||||
val top = (canvasHeight - boxHeight) / 2
|
||||
|
||||
val rectPath = Path().apply {
|
||||
addRoundRect(
|
||||
RoundRect(
|
||||
rect = androidx.compose.ui.geometry.Rect(
|
||||
offset = Offset(left, top),
|
||||
size = androidx.compose.ui.geometry.Size(boxWidth, boxHeight)
|
||||
),
|
||||
cornerRadius = CornerRadius(16.dp.toPx())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
clipPath(rectPath, clipOp = ClipOp.Difference) {
|
||||
drawRect(color = Color.Black.copy(alpha = 0.5f))
|
||||
}
|
||||
|
||||
drawRoundRect(
|
||||
color = Color.White,
|
||||
topLeft = Offset(left, top),
|
||||
size = androidx.compose.ui.geometry.Size(boxWidth, boxHeight),
|
||||
cornerRadius = CornerRadius(16.dp.toPx()),
|
||||
style = Stroke(width = 2.dp.toPx())
|
||||
)
|
||||
|
||||
// Back to horizontal line as it's more effective for 1D barcodes
|
||||
drawLine(
|
||||
color = Color.Red,
|
||||
start = Offset(left + 20.dp.toPx(), canvasHeight / 2),
|
||||
end = Offset(left + boxWidth - 20.dp.toPx(), canvasHeight / 2),
|
||||
strokeWidth = 2.dp.toPx()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user