[#1473] Dark theme QR codes

- Closes #1473
- These changes add dark theme QR codes to `QrCodeView` and `RequestQrCodeVew` for all use cases
- Changelogs updated
This commit is contained in:
Honza Rychnovský 2025-01-09 17:46:11 +01:00 committed by GitHub
parent 805a1b26b7
commit 37a7a1e334
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 65 additions and 23 deletions

View File

@ -10,6 +10,7 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2
- Send Confirmation & Send Progress screens have been refactored
- ZXing QR codes scanning library has been replaced with a more recent MLkit Barcodes scanning library, which gives
us better results in testing
- Zashi now displays dark version of QR code in the dark theme on the QR Code and Request screens
### Fixed
- The way how Zashi treats ZIP 321 single address within URIs results has been fixed

View File

@ -16,6 +16,7 @@ directly impact users rather than highlighting other key architectural updates.*
- Send Confirmation & Send Progress screens have been refactored with bugfixes and optimizations
- ZXing QR codes scanning library has been replaced with a more recent MLkit Barcodes scanning library, which gives
us better results in testing
- Zashi now displays dark version of QR code in the dark theme on the QR Code and Request screens
### Fixed
- The way how Zashi treats ZIP 321 single address within URIs results has been fixed

View File

@ -16,6 +16,7 @@ directly impact users rather than highlighting other key architectural updates.*
- Send Confirmation & Send Progress screens have been refactored with bugfixes and optimizations
- ZXing QR codes scanning library has been replaced with a more recent MLkit Barcodes scanning library, which gives
us better results in testing
- Zashi now displays dark version of QR code in the dark theme on the QR Code and Request screens
### Fixed
- The way how Zashi treats ZIP 321 single address within URIs results has been fixed

View File

@ -8,6 +8,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
@ -17,16 +18,18 @@ import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions
import co.electriccoin.zcash.ui.design.util.AndroidQrCodeImageGenerator
import co.electriccoin.zcash.ui.design.util.JvmQrCodeGenerator
import co.electriccoin.zcash.ui.design.util.QrCodeColors
import co.electriccoin.zcash.ui.design.util.orDark
@Composable
fun ZashiQr(
qrData: String,
modifier: Modifier = Modifier,
qrSize: Dp = ZashiQrDefaults.width
qrSize: Dp = ZashiQrDefaults.width,
colors: QrCodeColors = QrCodeDefaults.colors()
) {
val qrSizePx = with(LocalDensity.current) { qrSize.roundToPx() }
val bitmap = getQrCode(qrData, qrSizePx)
val bitmap = getQrCode(qrData, qrSizePx, colors)
Surface(
modifier = modifier,
@ -47,10 +50,11 @@ fun ZashiQr(
private fun getQrCode(
address: String,
size: Int
size: Int,
colors: QrCodeColors
): ImageBitmap {
val qrCodePixelArray = JvmQrCodeGenerator.generate(address, size)
return AndroidQrCodeImageGenerator.generate(qrCodePixelArray, size)
return AndroidQrCodeImageGenerator.generate(qrCodePixelArray, size, colors)
}
object ZashiQrDefaults {
@ -60,3 +64,14 @@ object ZashiQrDefaults {
}
private const val WIDTH_RATIO = 0.66
object QrCodeDefaults {
@Composable
fun colors(
background: Color = Color.White orDark ZashiColors.Surfaces.bgPrimary,
foreground: Color = Color.Black orDark Color.White
) = QrCodeColors(
background = background,
foreground = foreground
)
}

View File

@ -1,29 +1,34 @@
package co.electriccoin.zcash.ui.design.util
import android.graphics.Bitmap
import android.graphics.Color
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.toArgb
object AndroidQrCodeImageGenerator : QrCodeImageGenerator {
override fun generate(
bitArray: BooleanArray,
sizePixels: Int
sizePixels: Int,
colors: QrCodeColors
): ImageBitmap {
val colorArray = bitArray.toBlackAndWhiteColorArray()
val colorArray = bitArray.toThemeColorArray(colors)
return Bitmap.createBitmap(colorArray, sizePixels, sizePixels, Bitmap.Config.ARGB_8888)
.asImageBitmap()
}
}
// TODO [#1473]: Dark mode QR codes for Receive screen
// TODO [#1473]: https://github.com/Electric-Coin-Company/zashi-android/issues/1473
private fun BooleanArray.toBlackAndWhiteColorArray() =
private fun BooleanArray.toThemeColorArray(colors: QrCodeColors) =
IntArray(size) {
if (this[it]) {
Color.BLACK
colors.foreground.toArgb()
} else {
Color.WHITE
colors.background.toArgb()
}
}
data class QrCodeColors(
val background: Color,
val foreground: Color
)

View File

@ -5,6 +5,7 @@ import androidx.compose.ui.graphics.ImageBitmap
interface QrCodeImageGenerator {
fun generate(
bitArray: BooleanArray,
sizePixels: Int
sizePixels: Int,
colors: QrCodeColors
): ImageBitmap
}

View File

@ -50,6 +50,7 @@ import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.design.component.QrCodeDefaults
import co.electriccoin.zcash.ui.design.component.ZashiBadge
import co.electriccoin.zcash.ui.design.component.ZashiBadgeColors
import co.electriccoin.zcash.ui.design.component.ZashiBottomBar
@ -63,6 +64,7 @@ import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
import co.electriccoin.zcash.ui.design.util.AndroidQrCodeImageGenerator
import co.electriccoin.zcash.ui.design.util.JvmQrCodeGenerator
import co.electriccoin.zcash.ui.design.util.QrCodeColors
import co.electriccoin.zcash.ui.screen.qrcode.model.QrCodeState
import co.electriccoin.zcash.ui.screen.qrcode.model.QrCodeType
import kotlinx.coroutines.runBlocking
@ -127,11 +129,13 @@ internal fun QrCodeView(
}
is QrCodeState.Prepared -> {
val sizePixels = with(LocalDensity.current) { DEFAULT_QR_CODE_SIZE.toPx() }.roundToInt()
val colors = QrCodeDefaults.colors()
val qrCodeImage =
remember {
qrCodeForAddress(
address = state.walletAddress.address,
size = sizePixels
size = sizePixels,
colors = colors
)
}
@ -441,11 +445,13 @@ private fun ColumnScope.QrCode(
modifier: Modifier = Modifier
) {
val sizePixels = with(LocalDensity.current) { DEFAULT_QR_CODE_SIZE.toPx() }.roundToInt()
val colors = QrCodeDefaults.colors()
val qrCodeImage =
remember {
qrCodeForAddress(
address = walletAddress.address,
size = sizePixels
size = sizePixels,
colors = colors
)
}
@ -474,7 +480,7 @@ private fun ColumnScope.QrCode(
)
.background(
if (isSystemInDarkTheme()) {
ZashiColors.Surfaces.bgAlt
ZashiColors.Surfaces.bgPrimary
} else {
ZashiColors.Surfaces.bgPrimary
},
@ -488,6 +494,7 @@ private fun ColumnScope.QrCode(
private fun qrCodeForAddress(
address: String,
size: Int,
colors: QrCodeColors,
): ImageBitmap {
// In the future, use actual/expect to switch QR code generator implementations for multiplatform
@ -497,7 +504,7 @@ private fun qrCodeForAddress(
val qrCodePixelArray = JvmQrCodeGenerator.generate(address, size)
return AndroidQrCodeImageGenerator.generate(qrCodePixelArray, size)
return AndroidQrCodeImageGenerator.generate(qrCodePixelArray, size, colors)
}
@Composable

View File

@ -5,6 +5,7 @@ import cash.z.ecc.android.sdk.model.MonetarySeparators
import cash.z.ecc.android.sdk.model.WalletAddress
import cash.z.ecc.sdk.type.ZcashCurrency
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
import co.electriccoin.zcash.ui.design.util.QrCodeColors
internal sealed class RequestState {
data object Loading : RequestState()
@ -37,7 +38,7 @@ internal sealed class RequestState {
val request: Request,
val walletAddress: WalletAddress,
val onQrCodeShare: (ImageBitmap) -> Unit,
val onQrCodeGenerate: (pixels: Int) -> Unit,
val onQrCodeGenerate: (pixels: Int, colors: QrCodeColors) -> Unit,
override val onBack: () -> Unit,
val onClose: () -> Unit,
val zcashCurrency: ZcashCurrency,

View File

@ -34,6 +34,7 @@ import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.model.WalletAddress
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.design.component.QrCodeDefaults
import co.electriccoin.zcash.ui.design.component.ZashiBadge
import co.electriccoin.zcash.ui.design.component.ZashiBadgeColors
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@ -112,7 +113,8 @@ private fun ColumnScope.QrCode(
val sizePixels = with(LocalDensity.current) { DEFAULT_QR_CODE_SIZE.toPx() }.roundToInt()
if (state.request.qrCodeState.bitmap == null) {
state.onQrCodeGenerate(sizePixels)
val colors = QrCodeDefaults.colors()
state.onQrCodeGenerate(sizePixels, colors)
}
QrCode(
@ -131,7 +133,7 @@ private fun ColumnScope.QrCode(
)
.background(
if (isSystemInDarkTheme()) {
ZashiColors.Surfaces.bgAlt
ZashiColors.Surfaces.bgPrimary
} else {
ZashiColors.Surfaces.bgPrimary
},

View File

@ -22,6 +22,7 @@ import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
import co.electriccoin.zcash.ui.design.util.AndroidQrCodeImageGenerator
import co.electriccoin.zcash.ui.design.util.JvmQrCodeGenerator
import co.electriccoin.zcash.ui.design.util.QrCodeColors
import co.electriccoin.zcash.ui.screen.qrcode.ext.fromReceiveAddressType
import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType
import co.electriccoin.zcash.ui.screen.request.ext.convertToDouble
@ -136,7 +137,13 @@ class RequestViewModel(
},
walletAddress = walletAddress,
request = request,
onQrCodeGenerate = { qrCodeForValue(request.qrCodeState.requestUri, it) },
onQrCodeGenerate = { pixels, colors ->
qrCodeForValue(
value = request.qrCodeState.requestUri,
size = pixels,
colors = colors,
)
},
onQrCodeShare = { onRequestQrCodeShare(it, shareImageBitmap) },
onBack = ::onBack,
onClose = ::onClose,
@ -444,6 +451,7 @@ class RequestViewModel(
private fun qrCodeForValue(
value: String,
size: Int,
colors: QrCodeColors
) = viewModelScope.launch {
// In the future, use actual/expect to switch QR code generator implementations for multiplatform
@ -452,7 +460,7 @@ class RequestViewModel(
// small and we only generate QR codes infrequently.
val qrCodePixelArray = JvmQrCodeGenerator.generate(value, size)
val bitmap = AndroidQrCodeImageGenerator.generate(qrCodePixelArray, size)
val bitmap = AndroidQrCodeImageGenerator.generate(qrCodePixelArray, size, colors)
val newQrCodeState = request.value.qrCodeState.copy(bitmap = bitmap)
request.emit(request.value.copy(qrCodeState = newQrCodeState))

View File

@ -95,7 +95,7 @@ private fun Bitmap.rotate(rotationDegrees: Int): Bitmap {
width,
// height
height,
// m
// matrix
matrix,
// filter (Filter for better quality)
true