新增扫码功能
- 新增 `ScanQrScreen.kt` 文件,用于实现二维码扫描界面。 - 使用 CameraX 和 ML Kit Barcode Scanning 实现二维码识别。 - 请求相机权限,并在权限被拒绝时显示提示信息。 - 扫描成功后,通过 `savedStateHandle` 将结果返回给上一个界面并关闭当前屏幕。
This commit is contained in:
171
app/src/main/java/com/aiosman/ravenow/ui/scan/ScanQrScreen.kt
Normal file
171
app/src/main/java/com/aiosman/ravenow/ui/scan/ScanQrScreen.kt
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user