新增扫码功能
- 新增 `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