Skip to content

Commit 46a6256

Browse files
committed
Fix QrCode Scan
1 parent 8c2f246 commit 46a6256

5 files changed

Lines changed: 168 additions & 20 deletions

File tree

25 KB
Loading

app/src/main/java/com/ismartcoding/plain/ui/base/PDonationBanner.kt

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.padding
1010
import androidx.compose.foundation.layout.size
1111
import androidx.compose.foundation.layout.width
1212
import androidx.compose.foundation.shape.RoundedCornerShape
13+
import androidx.compose.material3.AlertDialog
1314
import androidx.compose.material3.Button
1415
import androidx.compose.material3.ButtonDefaults
1516
import androidx.compose.material3.CardDefaults
@@ -18,20 +19,42 @@ import androidx.compose.material3.Icon
1819
import androidx.compose.material3.MaterialTheme
1920
import androidx.compose.material3.Surface
2021
import androidx.compose.material3.Text
22+
import androidx.compose.material3.TextButton
2123
import androidx.compose.runtime.Composable
24+
import androidx.compose.runtime.getValue
25+
import androidx.compose.runtime.mutableStateOf
26+
import androidx.compose.runtime.remember
27+
import androidx.compose.runtime.setValue
2228
import androidx.compose.ui.Alignment
2329
import androidx.compose.ui.Modifier
30+
import androidx.compose.ui.platform.LocalContext
2431
import androidx.compose.ui.res.painterResource
2532
import androidx.compose.ui.res.stringResource
2633
import androidx.compose.ui.text.font.FontWeight
2734
import androidx.compose.ui.unit.dp
35+
import coil3.compose.AsyncImage
2836
import com.ismartcoding.plain.R
2937

3038
@Composable
3139
fun PDonationBanner(
3240
modifier: Modifier = Modifier,
3341
onClick: () -> Unit,
3442
) {
43+
val context = LocalContext.current
44+
val locale = context.resources.configuration.locales.get(0)
45+
val isZhCN = locale.language == "zh" && locale.country == "CN"
46+
var showWeChatDialog by remember { mutableStateOf(false) }
47+
48+
if (showWeChatDialog) {
49+
WeChatDonateDialog { showWeChatDialog = false }
50+
}
51+
52+
val effectiveOnClick: () -> Unit = if (isZhCN) {
53+
{ showWeChatDialog = true }
54+
} else {
55+
onClick
56+
}
57+
3558
ElevatedCard(
3659
modifier = modifier
3760
.fillMaxWidth()
@@ -41,18 +64,49 @@ fun PDonationBanner(
4164
containerColor = MaterialTheme.colorScheme.primaryContainer,
4265
),
4366
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp),
44-
onClick = onClick,
67+
onClick = effectiveOnClick,
4568
) {
4669
Column(modifier = Modifier.padding(24.dp)) {
4770
DonationLabel()
4871
Spacer(Modifier.height(16.dp))
4972
DonationTexts()
5073
Spacer(Modifier.height(20.dp))
51-
DonationButton(onClick)
74+
DonationButton(effectiveOnClick)
5275
}
5376
}
5477
}
5578

79+
@Composable
80+
private fun WeChatDonateDialog(onDismiss: () -> Unit) {
81+
AlertDialog(
82+
onDismissRequest = onDismiss,
83+
title = { Text("微信赞赏") },
84+
text = {
85+
Column(
86+
modifier = Modifier.fillMaxWidth(),
87+
horizontalAlignment = Alignment.CenterHorizontally,
88+
) {
89+
AsyncImage(
90+
model = "file:///android_asset/donate_wechat.webp",
91+
contentDescription = null,
92+
modifier = Modifier.fillMaxWidth(),
93+
)
94+
Spacer(Modifier.height(12.dp))
95+
Text(
96+
text = "请用微信扫码",
97+
style = MaterialTheme.typography.bodyMedium,
98+
color = MaterialTheme.colorScheme.onSurface,
99+
)
100+
}
101+
},
102+
confirmButton = {
103+
TextButton(onClick = onDismiss) {
104+
Text("关闭")
105+
}
106+
},
107+
)
108+
}
109+
56110
@Composable
57111
private fun DonationLabel() {
58112
Surface(

app/src/main/java/com/ismartcoding/plain/ui/page/home/TopBarHome.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import androidx.compose.material3.IconButton
1111
import androidx.compose.material3.MaterialTheme
1212
import androidx.compose.material3.Text
1313
import androidx.compose.material3.TopAppBar
14+
import androidx.compose.material3.TopAppBarDefaults
1415
import androidx.compose.runtime.Composable
1516
import androidx.compose.runtime.LaunchedEffect
1617
import androidx.compose.runtime.getValue
@@ -95,5 +96,10 @@ fun TopBarHome(navController: NavHostController) {
9596
onDismiss = { dismiss() },
9697
)
9798
}
98-
})
99+
},
100+
colors = TopAppBarDefaults.topAppBarColors(
101+
containerColor = MaterialTheme.colorScheme.background,
102+
scrolledContainerColor = MaterialTheme.colorScheme.background,
103+
),
104+
)
99105
}

app/src/main/java/com/ismartcoding/plain/ui/page/scan/ScanCameraView.kt

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import androidx.camera.lifecycle.ProcessCameraProvider
99
import androidx.camera.view.PreviewView
1010
import androidx.compose.foundation.layout.fillMaxSize
1111
import androidx.compose.runtime.Composable
12+
import androidx.compose.runtime.DisposableEffect
1213
import androidx.compose.runtime.MutableState
14+
import androidx.compose.runtime.remember
1315
import androidx.compose.ui.Modifier
1416
import androidx.compose.ui.platform.LocalContext
1517
import androidx.compose.ui.viewinterop.AndroidView
@@ -24,6 +26,7 @@ import com.google.zxing.common.HybridBinarizer
2426
import com.ismartcoding.lib.logcat.LogCat
2527
import com.ismartcoding.plain.helpers.QrCodeScanHelper
2628
import java.nio.ByteBuffer
29+
import java.util.concurrent.Executors
2730

2831
@Composable
2932
internal fun ScanCameraView(
@@ -33,31 +36,44 @@ internal fun ScanCameraView(
3336
onScanResult: (String) -> Unit,
3437
) {
3538
val context = LocalContext.current
39+
val executor = remember { Executors.newSingleThreadExecutor() }
40+
val mainExecutor = remember { ContextCompat.getMainExecutor(context) }
41+
val reader = remember { QrCodeScanHelper.createReader() }
42+
43+
DisposableEffect(Unit) {
44+
onDispose { executor.shutdown() }
45+
}
46+
3647
AndroidView(factory = { ctx ->
3748
val previewView = PreviewView(ctx)
3849
val preview = Preview.Builder().build()
3950
val selector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
4051
preview.surfaceProvider = previewView.surfaceProvider
41-
val imageAnalysis = ImageAnalysis.Builder().setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).build()
42-
imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(ctx), object : ImageAnalysis.Analyzer {
52+
val imageAnalysis = ImageAnalysis.Builder()
53+
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
54+
.build()
55+
imageAnalysis.setAnalyzer(executor, object : ImageAnalysis.Analyzer {
4356
private val supportedImageFormats = listOf(ImageFormat.YUV_420_888, ImageFormat.YUV_422_888, ImageFormat.YUV_444_888)
4457
override fun analyze(imageProxy: ImageProxy) {
45-
if (cameraDetecting.value) {
46-
if (imageProxy.format in supportedImageFormats && imageProxy.planes.size == 3) {
47-
val data = imageProxy.planes[0].buffer.toByteArray()
48-
try {
49-
cameraDetecting.value = false
50-
val reader = QrCodeScanHelper.createReader()
51-
try {
52-
onScanResult(decode(reader, imageProxy, data).text)
53-
} catch (e: NotFoundException) {
54-
for (i in data.indices) data[i] = (255 - (data[i].toInt() and 0xff)).toByte()
55-
onScanResult(decode(reader, imageProxy, data).text)
56-
}
57-
} catch (e: Exception) { cameraDetecting.value = true; e.printStackTrace() }
58-
finally { imageProxy.close() }
58+
if (!cameraDetecting.value) { imageProxy.close(); return }
59+
if (imageProxy.format !in supportedImageFormats || imageProxy.planes.size != 3) { imageProxy.close(); return }
60+
val data = imageProxy.planes[0].buffer.toByteArray()
61+
try {
62+
cameraDetecting.value = false
63+
try {
64+
val text = decode(reader, imageProxy, data).text
65+
mainExecutor.execute { onScanResult(text) }
66+
} catch (e: NotFoundException) {
67+
for (i in data.indices) data[i] = (255 - (data[i].toInt() and 0xff)).toByte()
68+
val text = decode(reader, imageProxy, data).text
69+
mainExecutor.execute { onScanResult(text) }
5970
}
60-
} else { imageProxy.close() }
71+
} catch (e: Exception) {
72+
cameraDetecting.value = true
73+
e.printStackTrace()
74+
} finally {
75+
imageProxy.close()
76+
}
6177
}
6278
private fun ByteBuffer.toByteArray(): ByteArray { rewind(); return ByteArray(remaining()).also { get(it) } }
6379
})
@@ -78,3 +94,4 @@ internal fun decode(reader: MultiFormatReader, imageProxy: ImageProxy, data: Byt
7894
val source = PlanarYUVLuminanceSource(data, imageProxy.planes[0].rowStride, imageProxy.height, 0, 0, imageProxy.width, imageProxy.height, false)
7995
return reader.decodeWithState(BinaryBitmap(HybridBinarizer(source)))
8096
}
97+

app/src/main/java/com/ismartcoding/plain/ui/page/scan/ScanPage.kt

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ package com.ismartcoding.plain.ui.page.scan
33
import android.annotation.SuppressLint
44
import android.content.Context
55
import androidx.camera.lifecycle.ProcessCameraProvider
6+
import androidx.compose.animation.core.LinearEasing
7+
import androidx.compose.animation.core.RepeatMode
8+
import androidx.compose.animation.core.animateFloat
9+
import androidx.compose.animation.core.infiniteRepeatable
10+
import androidx.compose.animation.core.rememberInfiniteTransition
11+
import androidx.compose.animation.core.tween
12+
import androidx.compose.foundation.Canvas
613
import androidx.compose.foundation.background
714
import androidx.compose.foundation.clickable
815
import androidx.compose.foundation.layout.Arrangement
@@ -31,6 +38,9 @@ import androidx.compose.runtime.setValue
3138
import androidx.compose.ui.Alignment
3239
import androidx.compose.ui.Modifier
3340
import androidx.compose.ui.draw.clip
41+
import androidx.compose.ui.geometry.Offset
42+
import androidx.compose.ui.geometry.Size
43+
import androidx.compose.ui.graphics.Brush
3444
import androidx.compose.ui.graphics.Color
3545
import androidx.compose.ui.platform.LocalContext
3646
import androidx.compose.ui.res.painterResource
@@ -123,6 +133,7 @@ fun ScanPage(navController: NavHostController) {
123133
}, content = { paddingValues ->
124134
Box(modifier = Modifier.fillMaxSize().padding(top = paddingValues.calculateTopPadding())) {
125135
if (hasCamPermission) ScanCameraView(lifecycleOwner, cameraDetecting, onCameraProvider = { cameraProvider = it }, onScanResult = { handleScanResult(it) })
136+
if (hasCamPermission) ScanOverlay()
126137
Row(modifier = Modifier.fillMaxWidth().padding(start = 24.dp, end = 24.dp, bottom = 64.dp).align(Alignment.BottomCenter), horizontalArrangement = Arrangement.End) {
127138
Box(modifier = Modifier.size(56.dp).clip(CircleShape).background(MaterialTheme.colorScheme.darkMask(0.2f)).clickable { sendEvent(PickFileEvent(PickFileTag.SCAN, PickFileType.IMAGE, multiple = false)) }, contentAlignment = Alignment.Center) {
128139
Icon(painter = painterResource(R.drawable.image), contentDescription = stringResource(R.string.images), tint = Color.White)
@@ -139,3 +150,63 @@ private fun addScanResult(context: Context, scope: CoroutineScope, value: String
139150
withIO { ScanHistoryPreference.putAsync(context, results) }
140151
}
141152
}
153+
154+
@Composable
155+
private fun ScanOverlay(modifier: Modifier = Modifier) {
156+
val infiniteTransition = rememberInfiniteTransition(label = "scan")
157+
val scanProgress by infiniteTransition.animateFloat(
158+
initialValue = 0f,
159+
targetValue = 1f,
160+
animationSpec = infiniteRepeatable(
161+
animation = tween(2000, easing = LinearEasing),
162+
repeatMode = RepeatMode.Reverse,
163+
),
164+
label = "scan_line",
165+
)
166+
Canvas(modifier = modifier.fillMaxSize()) {
167+
val boxSize = minOf(size.width, size.height) * 0.65f
168+
val left = (size.width - boxSize) / 2f
169+
val top = (size.height - boxSize) / 2f
170+
171+
// Dark overlay around scan area
172+
drawRect(color = Color.Black.copy(alpha = 0.5f), topLeft = Offset(0f, 0f), size = Size(size.width, top))
173+
drawRect(color = Color.Black.copy(alpha = 0.5f), topLeft = Offset(0f, top + boxSize), size = Size(size.width, size.height - top - boxSize))
174+
drawRect(color = Color.Black.copy(alpha = 0.5f), topLeft = Offset(0f, top), size = Size(left, boxSize))
175+
drawRect(color = Color.Black.copy(alpha = 0.5f), topLeft = Offset(left + boxSize, top), size = Size(size.width - left - boxSize, boxSize))
176+
177+
// Corner decorations
178+
val cornerLen = 40.dp.toPx()
179+
val cornerStroke = 3.dp.toPx()
180+
val white = Color.White
181+
// top-left
182+
drawLine(white, Offset(left, top), Offset(left + cornerLen, top), cornerStroke)
183+
drawLine(white, Offset(left, top), Offset(left, top + cornerLen), cornerStroke)
184+
// top-right
185+
drawLine(white, Offset(left + boxSize, top), Offset(left + boxSize - cornerLen, top), cornerStroke)
186+
drawLine(white, Offset(left + boxSize, top), Offset(left + boxSize, top + cornerLen), cornerStroke)
187+
// bottom-left
188+
drawLine(white, Offset(left, top + boxSize), Offset(left + cornerLen, top + boxSize), cornerStroke)
189+
drawLine(white, Offset(left, top + boxSize), Offset(left, top + boxSize - cornerLen), cornerStroke)
190+
// bottom-right
191+
drawLine(white, Offset(left + boxSize, top + boxSize), Offset(left + boxSize - cornerLen, top + boxSize), cornerStroke)
192+
drawLine(white, Offset(left + boxSize, top + boxSize), Offset(left + boxSize, top + boxSize - cornerLen), cornerStroke)
193+
194+
// Animated scan line
195+
val lineY = top + boxSize * scanProgress
196+
drawRect(
197+
brush = Brush.horizontalGradient(
198+
colors = listOf(
199+
Color.Transparent,
200+
Color(0xFF00E676).copy(alpha = 0.9f),
201+
Color(0xFF00E676),
202+
Color(0xFF00E676).copy(alpha = 0.9f),
203+
Color.Transparent,
204+
),
205+
startX = left,
206+
endX = left + boxSize,
207+
),
208+
topLeft = Offset(left, lineY - 1.5.dp.toPx()),
209+
size = Size(boxSize, 3.dp.toPx()),
210+
)
211+
}
212+
}

0 commit comments

Comments
 (0)