Android Jetpack Compose中ZXing二维码扫描框绘制问题求助
Jetpack Compose二维码扫描器边框问题修复指南
问题描述
我用Android Jetpack Compose开发二维码扫描器,扫描读取功能正常,但给识别到的二维码绘制红色边框时遇到两个问题:
- 红色边框高度正常,但宽度过窄
- 移动手机时边框无法锁定二维码,出现偏移

原代码实现
CameraPreview可组合函数
@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 // 调整扫描区域大小 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) { // 异常处理 } }, ContextCompat.getMainExecutor(ctx)) previewView }, modifier = Modifier.fillMaxSize() ) Box(modifier = Modifier .matchParentSize() .background(Color.Black.copy(alpha = 0.8f)) ) // 带镂空效果的扫描区域框 Box( modifier = Modifier .size(scanBoxSize) .align(Alignment.Center) .drawBehind { 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 } // 计算缩放比例 val scaleX = previewWidth.toFloat() / imageWidth val scaleY = previewHeight.toFloat() / imageHeight // 应用缩放比例到坐标 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 // 未识别到二维码 } }
问题原因分析
- 边框宽度过窄:固定取ResultPoints的第0和第2个点计算边界,无法准确获取二维码真实边界(ResultPoints顺序不固定);未考虑相机旋转角度,导致宽高缩放比例计算错误。
- 边框偏移:相机输出图像方向与屏幕显示方向不一致,坐标转换时未做旋转适配;PreviewView缩放类型未明确,导致预览与分析图像适配逻辑不统一。
修复方案
1. 修改scanQRCode函数,处理图像旋转
根据相机旋转角度调整图像方向,确保识别坐标与预览界面匹配:
private fun scanQRCode(imageProxy: ImageProxy, scanner: MultiFormatReader): Result? { val rotationDegrees = imageProxy.imageInfo.rotationDegrees val data = imageProxy.planes[0].buffer.let { buffer -> val data = ByteArray(buffer.capacity()) buffer.get(data) buffer.clear() data } // 根据旋转角度调整亮度源方向 val source = if (rotationDegrees == 90 || rotationDegrees == 270) { PlanarYUVLuminanceSource( data, imageProxy.width, imageProxy.height, 0, 0, imageProxy.width, imageProxy.height, false ).rotateCounterClockwise() } else { 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 } }
2. 重写getBoundingBox函数,准确计算边界
遍历所有ResultPoint获取真实坐标范围,结合旋转角度和预览尺寸计算正确的缩放与偏移:
private fun getBoundingBox( resultPoints: Array<ResultPoint>?, imageWidth: Int, imageHeight: Int, previewWidth: Int, previewHeight: Int, rotationDegrees: Int ): Rect? { if (resultPoints.isNullOrEmpty()) return null // 遍历所有点,找到最小/最大坐标 var minX = Float.MAX_VALUE var maxX = Float.MIN_VALUE var minY = Float.MAX_VALUE var maxY = Float.MIN_VALUE resultPoints.forEach { point -> minX = min(minX, point.x) maxX = max(maxX, point.x) minY = min(minY, point.y) maxY = max(maxY, point.y) } // 根据旋转角度调整图像宽高 val (adjustedImageWidth, adjustedImageHeight) = if (rotationDegrees == 90 || rotationDegrees == 270) { imageHeight to imageWidth } else { imageWidth to imageHeight } // 计算适配比例(以FILL_CENTER缩放类型为准) val scaleX = previewWidth.toFloat() / adjustedImageWidth val scaleY = previewHeight.toFloat() / adjustedImageHeight val scale = min(scaleX, scaleY) // 计算居中显示的偏移量 val offsetX = (previewWidth - adjustedImageWidth * scale) / 2 val offsetY = (previewHeight - adjustedImageHeight * scale) / 2 // 转换坐标并添加偏移 val left = (minX * scale + offsetX).toInt() val top = (minY * scale + offsetY).toInt() val right = (maxX * scale + offsetX).toInt() val bottom = (maxY * scale + offsetY).toInt() return Rect(left, top, right, bottom) }
3. 修改CameraPreview函数,完善预览配置
明确设置PreviewView缩放类型,并传递旋转角度给getBoundingBox:
@Composable fun CameraPreview() { val context = LocalContext.current val scanner = remember { 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 AndroidView( factory = { ctx -> val previewView = PreviewView(ctx).apply { // 明确设置缩放类型,确保预览与分析图像适配一致 scaleType = PreviewView.ScaleType.FILL_CENTER } 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 rotationDegrees = imageProxy.imageInfo.rotationDegrees val result = scanQRCode(imageProxy, scanner) imageProxy.close() if (result != null) { println("QR Code found: ${result.text}") qrCodeBounds.value = getBoundingBox( result.resultPoints, imageProxy.width, imageProxy.height, previewView.width, previewView.height, rotationDegrees ) } else { qrCodeBounds.value = null } } } try { cameraProvider.unbindAll() cameraProvider.bindToLifecycle( context as ComponentActivity, CameraSelector.DEFAULT_BACK_CAMERA, preview, imageAnalysis ) } catch (exc: Exception) { exc.printStackTrace() } }, ContextCompat.getMainExecutor(ctx)) previewView }, modifier = Modifier.fillMaxSize() ) // 其余UI代码保持不变 Box(modifier = Modifier .matchParentSize() .background(Color.Black.copy(alpha = 0.8f)) ) Box( modifier = Modifier .size(scanBoxSize) .align(Alignment.Center) .drawBehind { 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()) ) } } } }
额外说明
- 确保使用最新版本的ZXing库,避免旋转方法缺失问题
- 若
rotateCounterClockwise()报错,可手动实现亮度源旋转逻辑 - 测试时覆盖不同屏幕尺寸和方向的设备,确保边框适配一致
内容的提问来源于stack exchange,提问作者Hasnain Ali
相关产品推荐
相关产品推荐

