Skip to content

Commit 9e58ac8

Browse files
authored
Merge pull request #441 from synonymdev/feat/qr-quiet-zone
feat: receive QR code quiet zone and polish
2 parents a18e264 + 71af030 commit 9e58ac8

File tree

2 files changed

+74
-66
lines changed

2 files changed

+74
-66
lines changed

app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt

Lines changed: 72 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package to.bitkit.ui.components
22

33
import android.graphics.Bitmap
4+
import androidx.compose.animation.Crossfade
5+
import androidx.compose.animation.core.tween
46
import androidx.compose.foundation.Image
57
import androidx.compose.foundation.background
68
import androidx.compose.foundation.clickable
@@ -21,6 +23,7 @@ import androidx.compose.runtime.rememberCoroutineScope
2123
import androidx.compose.runtime.setValue
2224
import androidx.compose.ui.Alignment
2325
import androidx.compose.ui.Modifier
26+
import androidx.compose.ui.draw.clip
2427
import androidx.compose.ui.graphics.Color
2528
import androidx.compose.ui.graphics.asImageBitmap
2629
import androidx.compose.ui.graphics.painter.BitmapPainter
@@ -37,7 +40,6 @@ import androidx.compose.ui.unit.dp
3740
import androidx.core.graphics.createBitmap
3841
import com.google.zxing.BarcodeFormat
3942
import com.google.zxing.EncodeHintType
40-
import com.google.zxing.WriterException
4143
import com.google.zxing.qrcode.QRCodeWriter
4244
import kotlinx.coroutines.Dispatchers
4345
import kotlinx.coroutines.launch
@@ -47,6 +49,10 @@ import to.bitkit.ui.theme.AppShapes
4749
import to.bitkit.ui.theme.AppThemeSurface
4850
import to.bitkit.ui.theme.Colors
4951

52+
private const val QUIET_ZONE_MIN = 2
53+
private const val QUIET_ZONE_MAX = 4
54+
private const val QUIET_ZONE_RATIO = 150
55+
5056
@OptIn(ExperimentalMaterial3Api::class)
5157
@Composable
5258
fun QrCodeImage(
@@ -63,65 +69,73 @@ fun QrCodeImage(
6369
val coroutineScope = rememberCoroutineScope()
6470

6571
Box(
66-
contentAlignment = Alignment.TopCenter,
72+
contentAlignment = Alignment.Center,
6773
modifier = modifier
68-
.background(Color.White, AppShapes.small)
6974
.aspectRatio(1f)
70-
.padding(8.dp)
75+
.clip(AppShapes.small)
76+
.background(Color.White)
7177
) {
7278
val bitmap = rememberQrBitmap(content, size)
7379

7480
LaunchedEffect(bitmap) {
7581
onBitmapGenerated(bitmap)
7682
}
7783

78-
if (bitmap != null) {
79-
val imageComposable = @Composable {
80-
Image(
81-
painter = remember(bitmap) { BitmapPainter(bitmap.asImageBitmap()) },
82-
contentDescription = content,
83-
contentScale = ContentScale.Inside,
84-
modifier = Modifier
85-
.clickable(enabled = tipMessage.isNotBlank()) {
86-
coroutineScope.launch {
87-
context.setClipboardText(content)
88-
tooltipState.show()
84+
Crossfade(
85+
targetState = bitmap,
86+
animationSpec = tween(durationMillis = 200),
87+
label = "QR Code Crossfade"
88+
) { currentBitmap ->
89+
if (currentBitmap != null) {
90+
val imageComposable = @Composable {
91+
Image(
92+
painter = remember(currentBitmap) { BitmapPainter(currentBitmap.asImageBitmap()) },
93+
contentDescription = content,
94+
contentScale = ContentScale.Inside,
95+
modifier = Modifier
96+
.clickable(enabled = tipMessage.isNotBlank()) {
97+
coroutineScope.launch {
98+
context.setClipboardText(content)
99+
tooltipState.show()
100+
}
89101
}
90-
}
91-
.then(testTag?.let { Modifier.testTag(it) } ?: Modifier)
92-
)
102+
.then(testTag?.let { Modifier.testTag(it) } ?: Modifier)
103+
)
104+
}
105+
106+
if (tipMessage.isNotBlank()) {
107+
Tooltip(
108+
text = tipMessage,
109+
tooltipState = tooltipState,
110+
content = imageComposable,
111+
)
112+
} else {
113+
imageComposable()
114+
}
93115
}
116+
}
94117

95-
if (tipMessage.isNotBlank()) {
96-
Tooltip(
97-
text = tipMessage,
98-
tooltipState = tooltipState,
99-
content = imageComposable,
118+
logoPainter?.let {
119+
Box(
120+
contentAlignment = Alignment.Center,
121+
modifier = Modifier
122+
.size(68.dp)
123+
.background(Color.White, shape = CircleShape)
124+
.align(Alignment.Center)
125+
) {
126+
Image(
127+
painter = it,
128+
contentDescription = null,
129+
modifier = Modifier.size(50.dp)
100130
)
101-
} else {
102-
imageComposable()
103131
}
132+
}
104133

105-
logoPainter?.let {
106-
Box(
107-
contentAlignment = Alignment.Center,
108-
modifier = Modifier
109-
.size(68.dp)
110-
.background(Color.White, shape = CircleShape)
111-
.align(Alignment.Center)
112-
) {
113-
Image(
114-
painter = it,
115-
contentDescription = null,
116-
modifier = Modifier.size(50.dp)
117-
)
118-
}
119-
}
120-
} else {
134+
if (bitmap == null) {
121135
CircularProgressIndicator(
122136
color = Colors.Black,
123-
strokeWidth = 2.dp,
124-
modifier = Modifier.align(Alignment.Center)
137+
strokeWidth = 4.dp,
138+
modifier = Modifier.size(68.dp)
125139
)
126140
}
127141
}
@@ -135,46 +149,39 @@ private fun rememberQrBitmap(content: String, size: Dp): Bitmap? {
135149
val sizePx = with(LocalDensity.current) { size.roundToPx() }
136150

137151
LaunchedEffect(content, size) {
138-
if (bitmap != null) return@LaunchedEffect
152+
bitmap = null // Always reset to show loading indicator
139153

140154
launch(Dispatchers.Default) {
141155
val qrCodeWriter = QRCodeWriter()
142156

143-
val encodeHints = mutableMapOf<EncodeHintType, Any?>().apply {
144-
this[EncodeHintType.MARGIN] = 0
145-
}
157+
val quietZoneModules = (content.length / QUIET_ZONE_RATIO + 1).coerceIn(QUIET_ZONE_MIN, QUIET_ZONE_MAX)
158+
159+
val encodeHints = mapOf(EncodeHintType.MARGIN to quietZoneModules)
146160

147-
val bitmapMatrix = try {
161+
val bitmapMatrix = runCatching {
148162
qrCodeWriter.encode(
149163
content,
150164
BarcodeFormat.QR_CODE,
151165
sizePx,
152166
sizePx,
153167
encodeHints,
154168
)
155-
} catch (_: WriterException) {
156-
null
157-
}
169+
}.getOrElse { return@launch }
158170

159-
val matrixWidth = bitmapMatrix?.width ?: sizePx
160-
val matrixHeight = bitmapMatrix?.height ?: sizePx
161-
162-
val newBitmap = createBitmap(
163-
width = bitmapMatrix?.width ?: sizePx,
164-
height = bitmapMatrix?.height ?: sizePx
165-
)
171+
val matrixWidth = bitmapMatrix.width
172+
val matrixHeight = bitmapMatrix.height
166173

174+
val newBitmap = createBitmap(width = matrixWidth, height = matrixHeight)
167175
val pixels = IntArray(matrixWidth * matrixHeight)
168176

169177
for (x in 0 until matrixWidth) {
170178
for (y in 0 until matrixHeight) {
171-
val shouldColorPixel = bitmapMatrix?.get(x, y) ?: false
172-
val pixelColor =
173-
if (shouldColorPixel) {
174-
android.graphics.Color.BLACK
175-
} else {
176-
android.graphics.Color.WHITE
177-
}
179+
val shouldColorPixel = bitmapMatrix[x, y]
180+
val pixelColor = if (shouldColorPixel) {
181+
android.graphics.Color.BLACK
182+
} else {
183+
android.graphics.Color.WHITE
184+
}
178185

179186
pixels[y * matrixWidth + x] = pixelColor
180187
}

app/src/main/java/to/bitkit/ui/components/Tooltip.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ fun Tooltip(
2020
text: String,
2121
tooltipState: TooltipState,
2222
modifier: Modifier = Modifier,
23-
content: @Composable (() -> Unit)
23+
content: @Composable () -> Unit
2424
) {
2525
TooltipBox(
2626
modifier = modifier,
@@ -48,6 +48,7 @@ fun Tooltip(
4848
}
4949
},
5050
state = tooltipState,
51+
focusable = false,
5152
content = content
5253
)
5354
}

0 commit comments

Comments
 (0)