新增扫码功能

- 新增 `ScanQrScreen.kt` 文件,用于实现二维码扫描界面。
- 使用 CameraX 和 ML Kit Barcode Scanning 实现二维码识别。
- 请求相机权限,并在权限被拒绝时显示提示信息。
- 扫描成功后,通过 `savedStateHandle` 将结果返回给上一个界面并关闭当前屏幕。
This commit is contained in:
2025-11-11 10:13:00 +08:00
parent e01b2d9e8f
commit f63b421915

View File

@@ -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<Boolean?>(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)
)
}
}
}
}