From f63b421915da189c48a81650225a5416ddcb7542 Mon Sep 17 00:00:00 2001 From: AllenTom Date: Tue, 11 Nov 2025 10:13:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=89=AB=E7=A0=81=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 `ScanQrScreen.kt` 文件,用于实现二维码扫描界面。 - 使用 CameraX 和 ML Kit Barcode Scanning 实现二维码识别。 - 请求相机权限,并在权限被拒绝时显示提示信息。 - 扫描成功后,通过 `savedStateHandle` 将结果返回给上一个界面并关闭当前屏幕。 --- .../aiosman/ravenow/ui/scan/ScanQrScreen.kt | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 app/src/main/java/com/aiosman/ravenow/ui/scan/ScanQrScreen.kt diff --git a/app/src/main/java/com/aiosman/ravenow/ui/scan/ScanQrScreen.kt b/app/src/main/java/com/aiosman/ravenow/ui/scan/ScanQrScreen.kt new file mode 100644 index 0000000..ed32c88 --- /dev/null +++ b/app/src/main/java/com/aiosman/ravenow/ui/scan/ScanQrScreen.kt @@ -0,0 +1,171 @@ +package com.aiosman.ravenow.ui.scan + +import android.Manifest +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat +import com.aiosman.ravenow.LocalNavController +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.barcode.BarcodeScannerOptions +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.common.InputImage +import java.util.concurrent.Executors + +@Composable +fun ScanQrScreen() { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + val navController = LocalNavController.current + + var cameraGranted by remember { mutableStateOf(null) } + var hasResult by remember { mutableStateOf(false) } + + val requestPermissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = { granted -> + cameraGranted = granted + } + ) + + LaunchedEffect(Unit) { + requestPermissionLauncher.launch(Manifest.permission.CAMERA) + } + + val scanner = remember { + val options = BarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_QR_CODE) + .build() + BarcodeScanning.getClient(options) + } + + when (cameraGranted) { + null -> { + // 等待权限结果 + Box(modifier = Modifier.fillMaxSize().background(Color.Black)) + } + false -> { + // 权限被拒绝 + Column( + modifier = Modifier.fillMaxSize().background(Color.Black), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(text = "需要相机权限以扫码", color = Color.White, modifier = Modifier.padding(16.dp)) + Button(onClick = { navController.popBackStack() }) { + Text(text = "返回") + } + } + } + true -> { + Box(modifier = Modifier.fillMaxSize().background(Color.Black)) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { ctx -> + PreviewView(ctx).apply { + this.scaleType = PreviewView.ScaleType.FILL_CENTER + } + }, + update = { previewView -> + val cameraProviderFuture = ProcessCameraProvider.getInstance(context) + cameraProviderFuture.addListener({ + val cameraProvider = cameraProviderFuture.get() + + val preview = Preview.Builder().build().also { p -> + p.setSurfaceProvider(previewView.surfaceProvider) + } + + val analysis = ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + .also { imageAnalysis -> + val executor = Executors.newSingleThreadExecutor() + imageAnalysis.setAnalyzer(executor) { imageProxy -> + val mediaImage = imageProxy.image + if (mediaImage == null) { + imageProxy.close() + return@setAnalyzer + } + val image = InputImage.fromMediaImage( + mediaImage, + imageProxy.imageInfo.rotationDegrees + ) + scanner.process(image) + .addOnSuccessListener { barcodes -> + if (!hasResult && !barcodes.isNullOrEmpty()) { + val first = barcodes.firstOrNull() + val rawValue = first?.rawValue ?: first?.displayValue + if (!rawValue.isNullOrBlank()) { + hasResult = true + navController.previousBackStackEntry + ?.savedStateHandle + ?.set("scan_result", rawValue) + Toast.makeText(context, rawValue, Toast.LENGTH_SHORT).show() + navController.popBackStack() + } + } + } + .addOnFailureListener { + // 忽略单帧失败 + } + .addOnCompleteListener { + imageProxy.close() + } + } + } + + try { + cameraProvider.unbindAll() + cameraProvider.bindToLifecycle( + lifecycleOwner, + CameraSelector.DEFAULT_BACK_CAMERA, + preview, + analysis + ) + } catch (_: Exception) { + // 绑定失败,忽略 + } + }, ContextCompat.getMainExecutor(context)) + } + ) + + // 简单文案提示 + Text( + text = "对准二维码进行扫描", + color = Color.White, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 32.dp) + ) + } + } + } +} + +