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