From 027d2391b7c1e3bf2d5fdbcefd55fd7f900ad8fe Mon Sep 17 00:00:00 2001 From: Hadrian Burkhardt Date: Thu, 26 Feb 2026 03:19:53 +0100 Subject: [PATCH] speedup + minor fixes --- ROADMAP.md | 2 +- .../data/scanner/MlKitBarcodeAnalyzer.kt | 22 ++++++++++ .../scanner/ui/components/CameraPreview.kt | 5 ++- .../clean/scanner/ui/screens/ScannerScreen.kt | 42 ++++++++++++------- build.gradle.kts | 2 +- 5 files changed, 54 insertions(+), 19 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index a8e5c88..e64ed3c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -3,7 +3,7 @@ ## Quick Wins (1-3 days) - [x] Duplicate UX polish -- Add subtle in-app banner/snackbar (in addition to toast) with optional "View in history". +- Add subtle in-app banner/snack bar (in addition to toast) with optional "View in history". - [x] Batch mode polish - Add per-item copy/share in batch panel. diff --git a/app/src/main/java/com/clean/scanner/data/scanner/MlKitBarcodeAnalyzer.kt b/app/src/main/java/com/clean/scanner/data/scanner/MlKitBarcodeAnalyzer.kt index 508a877..96cc60c 100644 --- a/app/src/main/java/com/clean/scanner/data/scanner/MlKitBarcodeAnalyzer.kt +++ b/app/src/main/java/com/clean/scanner/data/scanner/MlKitBarcodeAnalyzer.kt @@ -1,6 +1,7 @@ package com.clean.scanner.data.scanner import android.graphics.Rect +import android.os.SystemClock import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageProxy import com.clean.scanner.domain.ScanResult @@ -47,6 +48,8 @@ class MlKitBarcodeAnalyzer( const val MATCH_DISTANCE_THRESHOLD = 0.18f const val BOX_SMOOTHING_ALPHA = 0.35f const val MAX_MISSED_FRAMES = 2 + const val MIN_ANALYSIS_INTERVAL_MS = 45L + const val MIN_STATE_PUBLISH_INTERVAL_MS = 66L } private val scanner = BarcodeScanning.getClient( @@ -58,6 +61,10 @@ class MlKitBarcodeAnalyzer( @Volatile private var processing = false @Volatile + private var lastAnalysisStartMs = 0L + @Volatile + private var lastStatePublishMs = 0L + @Volatile private var lastHasPotential = false @Volatile private var lastHasReadable = false @@ -86,6 +93,11 @@ class MlKitBarcodeAnalyzer( imageProxy.close() return } + val nowMs = SystemClock.elapsedRealtime() + if (nowMs - lastAnalysisStartMs < MIN_ANALYSIS_INTERVAL_MS) { + imageProxy.close() + return + } if (processing) { imageProxy.close() return @@ -100,6 +112,7 @@ class MlKitBarcodeAnalyzer( val sourceHeight = if (rotationDegrees == 90 || rotationDegrees == 270) imageProxy.width else imageProxy.height val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) processing = true + lastAnalysisStartMs = nowMs scanner.process(image) .addOnSuccessListener { barcodes -> @@ -210,6 +223,7 @@ class MlKitBarcodeAnalyzer( sourceWidth: Int, sourceHeight: Int ) { + val nowMs = SystemClock.elapsedRealtime() if ( lastHasPotential == hasPotential && lastHasReadable == hasReadable && @@ -217,11 +231,19 @@ class MlKitBarcodeAnalyzer( lastSourceWidth == sourceWidth && lastSourceHeight == sourceHeight ) return + val isStateKindUnchanged = lastHasPotential == hasPotential && + lastHasReadable == hasReadable && + lastSourceWidth == sourceWidth && + lastSourceHeight == sourceHeight + if (isStateKindUnchanged && nowMs - lastStatePublishMs < MIN_STATE_PUBLISH_INTERVAL_MS) { + return + } lastHasPotential = hasPotential lastHasReadable = hasReadable lastBoxes = boxes lastSourceWidth = sourceWidth lastSourceHeight = sourceHeight + lastStatePublishMs = nowMs onDetectionStateChanged(hasPotential, hasReadable, boxes, sourceWidth, sourceHeight) } diff --git a/app/src/main/java/com/clean/scanner/ui/components/CameraPreview.kt b/app/src/main/java/com/clean/scanner/ui/components/CameraPreview.kt index e1c2edc..852fee9 100644 --- a/app/src/main/java/com/clean/scanner/ui/components/CameraPreview.kt +++ b/app/src/main/java/com/clean/scanner/ui/components/CameraPreview.kt @@ -1,6 +1,7 @@ package com.clean.scanner.ui.components import android.annotation.SuppressLint +import android.util.Size import android.view.MotionEvent import android.view.ScaleGestureDetector import androidx.camera.core.CameraSelector @@ -12,6 +13,7 @@ import androidx.camera.view.PreviewView import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState @@ -49,7 +51,7 @@ fun CameraPreview( val mainExecutor = remember(context) { ContextCompat.getMainExecutor(context) } val previewView = remember { PreviewView(context) } val cameraRef = remember { mutableStateOf(null) } - val zoomRatio = remember { mutableStateOf(1f) } + val zoomRatio = remember { mutableFloatStateOf(1f) } val latestAnalysisEnabled = rememberUpdatedState(analysisEnabled) val latestOnScan = rememberUpdatedState(onScan) val latestOnDetectionStateChanged = rememberUpdatedState(onDetectionStateChanged) @@ -107,6 +109,7 @@ fun CameraPreview( val imageAnalysis = ImageAnalysis.Builder() .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .setTargetResolution(Size(1280, 720)) .build().apply { setAnalyzer(cameraExecutor, analyzer) } diff --git a/app/src/main/java/com/clean/scanner/ui/screens/ScannerScreen.kt b/app/src/main/java/com/clean/scanner/ui/screens/ScannerScreen.kt index 7a96712..6abd17b 100644 --- a/app/src/main/java/com/clean/scanner/ui/screens/ScannerScreen.kt +++ b/app/src/main/java/com/clean/scanner/ui/screens/ScannerScreen.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.gestures.detectTransformGestures import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -55,6 +54,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -62,6 +63,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.asImageBitmap @@ -85,6 +87,7 @@ import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import com.clean.scanner.R import com.clean.scanner.data.scanner.DetectionBox +import com.clean.scanner.data.scanner.DetectionPoint import com.clean.scanner.domain.ScanResult import com.clean.scanner.ui.BatchScanRecord import com.clean.scanner.ui.components.CameraPreview @@ -93,6 +96,7 @@ import com.clean.scanner.util.Intents import com.clean.scanner.util.ScanContentParsers import com.clean.scanner.util.UrlRiskScorer import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.barcode.BarcodeScanner import com.google.mlkit.vision.barcode.BarcodeScannerOptions import com.google.mlkit.vision.barcode.common.Barcode import com.google.mlkit.vision.common.InputImage @@ -150,8 +154,8 @@ fun ScannerScreen( var hasPotentialInView by remember { mutableStateOf(false) } var hasReadableInView by remember { mutableStateOf(false) } var detectionBoxes by remember { mutableStateOf>(emptyList()) } - var detectionSourceWidth by remember { mutableStateOf(0) } - var detectionSourceHeight by remember { mutableStateOf(0) } + var detectionSourceWidth by remember { mutableIntStateOf(0) } + var detectionSourceHeight by remember { mutableIntStateOf(0) } val activity = context as? Activity val imageScanner = remember { BarcodeScanning.getClient( @@ -193,7 +197,7 @@ fun ScannerScreen( val raw = barcode.rawValue?.takeIf { it.isNotBlank() } ?: return@mapNotNull null val normalizedBox = barcode.boundingBox?.let { bounds -> val corners = barcode.cornerPoints?.map { p -> - com.clean.scanner.data.scanner.DetectionPoint( + DetectionPoint( x = (p.x / image.width.toFloat()).coerceIn(0f, 1f), y = (p.y / image.height.toFloat()).coerceIn(0f, 1f) ) @@ -254,10 +258,16 @@ fun ScannerScreen( } } - BoxWithConstraints(modifier = Modifier.fillMaxSize()) { + var containerSize by remember { mutableStateOf(IntSize.Zero) } + + Box( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { containerSize = it } + ) { val density = LocalDensity.current - val viewW = with(density) { maxWidth.toPx() } - val viewH = with(density) { maxHeight.toPx() } + val viewW = containerSize.width.toFloat() + val viewH = containerSize.height.toFloat() val galleryOpen = imageScanPreviewUri != null val aimW = viewW * 0.62f val aimH = with(density) { 200.dp.toPx() } @@ -324,7 +334,7 @@ fun ScannerScreen( val right = offsetX + (box.right * sourceW * scale) val bottom = offsetY + (box.bottom * sourceH * scale) val mappedCorners = box.corners.map { p -> - androidx.compose.ui.geometry.Offset( + Offset( x = offsetX + (p.x * sourceW * scale), y = offsetY + (p.y * sourceH * scale) ) @@ -343,8 +353,8 @@ fun ScannerScreen( } else if (right > left && bottom > top) { drawRoundRect( color = boxColor.copy(alpha = 0.95f), - topLeft = androidx.compose.ui.geometry.Offset(left, top), - size = androidx.compose.ui.geometry.Size(right - left, bottom - top), + topLeft = Offset(left, top), + size = Size(right - left, bottom - top), cornerRadius = CornerRadius(14f, 14f), style = Stroke(width = 4f) ) @@ -750,7 +760,7 @@ private fun loadBitmapFromUri(context: android.content.Context, uri: Uri): Bitma } private suspend fun detectBarcodes( - scanner: com.google.mlkit.vision.barcode.BarcodeScanner, + scanner: BarcodeScanner, image: InputImage ): List = suspendCancellableCoroutine { cont -> scanner.process(image) @@ -772,10 +782,10 @@ private fun GalleryScanPreviewDialog( val context = LocalContext.current val bitmap = remember(imageUri) { imageUri?.let { loadBitmapFromUri(context, it) } } var liveCandidates by remember(imageUri, candidates) { mutableStateOf(candidates) } - var zoom by remember(imageUri) { mutableStateOf(1f) } + var zoom by remember(imageUri) { mutableFloatStateOf(1f) } var pan by remember(imageUri) { mutableStateOf(Offset.Zero) } var viewportSize by remember { mutableStateOf(IntSize.Zero) } - var scanTick by remember { mutableStateOf(0) } + var scanTick by remember { mutableIntStateOf(0) } val markerPaint = remember { Paint().apply { color = android.graphics.Color.WHITE @@ -850,7 +860,7 @@ private fun GalleryScanPreviewDialog( val rightN = ((bounds.right + cropLeft) / imgW).coerceIn(0f, 1f) val bottomN = ((bounds.bottom + cropTop) / imgH).coerceIn(0f, 1f) val corners = barcode.cornerPoints?.map { p -> - com.clean.scanner.data.scanner.DetectionPoint( + DetectionPoint( x = ((p.x + cropLeft) / imgW).coerceIn(0f, 1f), y = ((p.y + cropTop) / imgH).coerceIn(0f, 1f) ) @@ -952,8 +962,8 @@ private fun GalleryScanPreviewDialog( if (right > left && bottom > top) { drawRoundRect( color = color, - topLeft = androidx.compose.ui.geometry.Offset(left, top), - size = androidx.compose.ui.geometry.Size(right - left, bottom - top), + topLeft = Offset(left, top), + size = Size(right - left, bottom - top), cornerRadius = CornerRadius(10f, 10f), style = Stroke(width = 4f) ) diff --git a/build.gradle.kts b/build.gradle.kts index 577fa7a..a85fa5c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("com.android.application") version "9.0.0" apply false + id("com.android.application") version "9.0.1" apply false id("com.google.devtools.ksp") version "2.3.5" apply false id("org.jetbrains.kotlin.plugin.compose") version "2.3.10" apply false }