我正在 Android 中使用 Jetpack Compose 构建 QR 码扫描仪。扫描部分和读取 QR 码中的位值工作良好且符合预期。然而,作为我原型的一部分,我希望能够在二维码周围画一个红色框。
目前,我正在处理两个问题。红色框的高度似乎是正确的,但宽度太小,我不知道为什么。第二个问题是,当我移动手机时,边界框似乎到处都是,并且没有锁定实际的二维码。我不知道如何解决这个问题。
这是我的
CameraView
可组合函数...
@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())
)
}
}
}
}
这是我的
getBoundingBox
功能
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)
}
这是我的
scanQRCode
功能
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
}
}
我很高兴有人用这种方式制造扫描仪。很久以前我也遇到过同样的问题。我将在这里留下我的解决方案,以及二维码的理想位置。我希望它有帮助。
请将代码粘贴到单独的屏幕上并查看结果。
最主要的是CameraX不会在整个画布上显示整个相机预览。默认预览视图设置是:
scaleType = PreviewView.ScaleType.FILL_CENTER
这有助于填充整个屏幕并剪掉剩余部分,从而使坐标的计算变得复杂。对我来说:
scaleType = PreviewView.ScaleType.FIT_START
扫描库:Google ML Kit
对不起翻译者
使用扫描仪预览相机的完整代码:
@Composable
fun CameraView(
modifier: Modifier = Modifier,
) {
val density = LocalDensity.current
val lifecycleOwner = LocalLifecycleOwner.current
var barcodes by remember { mutableStateOf<Pair<Size, List<Barcode>>?>(null) }
Box(Modifier.height(with(density) { 1920f.toDp() })) {
AndroidView(
modifier = modifier
.align(Alignment.TopStart)
.drawWithContent {
drawContent()
barcodes?.second?.forEachIndexed { barcodeIndex, barcodes ->
barcodes.cornerPoints?.let {
val path = Path()
it.forEachIndexed { index, point ->
if (index == 0)
path.moveTo(
point.x.toFloat() * (size.width / 720f),
point.y.toFloat() * (size.height / 1280f)
)
else
path.lineTo(
point.x.toFloat() * (size.width / 720f),
point.y.toFloat() * (size.height / 1280f)
)
}
path.close()
drawPath(
path,
if (barcodeIndex == 0) Color.Red else Color.Blue,
style = Stroke(20f)
)
}
}
}
.fillMaxSize(),
factory = { context ->
val cameraExecutor = Executors.newSingleThreadExecutor()
val barcodeScanner = BarcodeScanning.getClient(
BarcodeScannerOptions.Builder()
.build()
).also {
lifecycleOwner.lifecycle.addObserver(it)
}
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
val previewView = PreviewView(context)
.apply {
scaleType = PreviewView.ScaleType.FIT_START
}
val imageAnalyzer = ImageAnalysis.Builder()
.setTargetResolution(Size(720, 1280))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also {
it.setAnalyzer(cameraExecutor) { imageProxy ->
imageProxy.image?.let { image ->
barcodeScanner.process(
InputImage.fromMediaImage(
image,
imageProxy.imageInfo.rotationDegrees
)
).addOnSuccessListener { results ->
barcodes = Size(image.width, image.height) to results
result = results.firstOrNull()
}.addOnCompleteListener {
imageProxy.close()
}
}
}
}
cameraProviderFuture.addListener(
{
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.setTargetResolution(Size(1080, 1920))
.build()
.also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
lifecycleOwner, cameraSelector, preview, imageAnalyzer
)
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(context)
)
previewView
}
)
Column(
Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.secondary.copy(0.2f))
.align(Alignment.TopCenter),
horizontalAlignment = Alignment.CenterHorizontally
) {
barcodes?.second?.forEach {
Text(it.displayValue ?: "")
}
}
}
}