I am building a QR code scanner using Jetpack Compose in Android. The scanning part and reading the bit values in the QR code work great and as expected. However, as a part of my prototype, I want to be able to draw a red box around the QR code.
Currently, I am dealing with two issues. The height of the red box appears to be correct, but the width is too small and I am not sure why. The second problem is that as I move my phone, the bounding box appears to go all over the place and not locked on the actual QR code. I'm not sure how to fix this.
Here is my CameraView composable function...
@Composable
fun CameraPreview() {
val context = LocalContext.current
val scanner = MultiFormatReader().apply {
val hints: MutableMap<DecodeHintType, Any> = EnumMap(DecodeHintType::class.java)
hints[DecodeHintType.POSSIBLE_FORMATS] = listOf(BarcodeFormat.QR_CODE)
setHints(hints)
}
val qrCodeBounds = remember { mutableStateOf<Rect?>(null) }
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val screenWidth = maxWidth
val scanBoxSize = screenWidth * 0.6f // adjust the size of the scanning area here
AndroidView(
factory = { ctx ->
val previewView = PreviewView(ctx)
val cameraProviderFuture = ProcessCameraProvider.getInstance(ctx)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
val imageAnalysis = ImageAnalysis.Builder()
.build()
.also {
it.setAnalyzer(ContextCompat.getMainExecutor(ctx)) { imageProxy ->
val result = scanQRCode(imageProxy, scanner)
imageProxy.close()
if (result != null) {
println("QR Code found: ${result.text}")
println("Image Proxy Width: ${imageProxy.width}, Height: ${imageProxy.height}")
qrCodeBounds.value = getBoundingBox(result.resultPoints, imageProxy.width, imageProxy.height, previewView.width, previewView.height)
} else {
qrCodeBounds.value = null
}
}
}
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
context as ComponentActivity,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
imageAnalysis
)
} catch (exc: Exception) {
// Handle exceptions
}
}, ContextCompat.getMainExecutor(ctx))
previewView
},
modifier = Modifier.fillMaxSize()
)
Box(modifier = Modifier
.matchParentSize()
.background(Color.Black.copy(alpha = 0.8f))
)
// Scanning area box with a clear cutout
Box(
modifier = Modifier
.size(scanBoxSize)
.align(Alignment.Center)
.drawBehind {
// Draw a rounded clear rectangle to create a cutout effect
drawRoundRect(
color = Color.Transparent,
topLeft = Offset(0f, 0f),
size = Size(size.width, size.height),
cornerRadius = CornerRadius(x = 12.dp.toPx(), y = 12.dp.toPx()),
blendMode = BlendMode.Clear
)
}
.border(2.dp, MaterialTheme.colorScheme.onPrimary, RoundedCornerShape(12.dp))
)
qrCodeBounds.value?.let { bounds ->
Canvas(modifier = Modifier.fillMaxSize()) {
drawRect(
color = Color.Red,
topLeft = Offset(bounds.left.toFloat(), bounds.top.toFloat()),
size = Size(bounds.width().toFloat(), bounds.height().toFloat()),
style = Stroke(width = 3.dp.toPx())
)
}
}
}
}
This is my getBoundingBox function
private fun getBoundingBox(resultPoints: Array<ResultPoint>?, imageWidth: Int, imageHeight: Int, previewWidth: Int, previewHeight: Int): Rect? {
if (resultPoints == null || resultPoints.size != 4) {
return null
}
// Calculate scale factors
val scaleX = previewWidth.toFloat() / imageWidth
val scaleY = previewHeight.toFloat() / imageHeight
// Apply scale factors to the coordinates
val left = (resultPoints[0].x * scaleX).toInt()
val top = (resultPoints[0].y * scaleY).toInt()
val right = (resultPoints[2].x * scaleX).toInt()
val bottom = (resultPoints[2].y * scaleY).toInt()
return Rect(left, top, right, bottom)
}
And this is my scanQRCode function
private fun scanQRCode(imageProxy: ImageProxy, scanner: MultiFormatReader): Result? {
val data = imageProxy.planes[0].buffer.let { buffer ->
val data = ByteArray(buffer.capacity())
buffer.get(data)
buffer.clear()
data
}
val source = PlanarYUVLuminanceSource(
data,
imageProxy.width,
imageProxy.height,
0,
0,
imageProxy.width,
imageProxy.height,
false
)
val binaryBitmap = BinaryBitmap(HybridBinarizer(source))
return try {
scanner.decodeWithState(binaryBitmap)
} catch (e: Exception) {
null // QR Code not found
}
}
