A QR Code (quick repose code) is a type of barcode represented as a matrix of black and white squares. QR codes make it very convenient to represent text such as phone numbers, URLs and other information in an image format.
In Jetpack Compose (Google's new UI toolkit for android apps), it is very easy to create an instance of Painter
which is used to render images and UI elements that can be drawn into a specified bounded area. There are number of Painter
subclasses provided in compose by default but we will be focusing on the BitmapPainter
.
Adding Zxing Core Dependency
The first step is to generate the QR code Bitmap image that will be rendered in our UI. To do this we will be using zxing-core
library by Google. Zxing is an open-source barcode scanning libary for Java and Android. Add the dependency to the module level (in a simple project this would be app) build.gradle
file:
dependencies {
...
implementation "com.google.zxing:core:3.5.1"
}
Defining the Painter function
Let's define a special type of composable function that returns a value and has a camelCase naming as opposed to our usual composable functions that emit UI with a PascalCase naming and return no value (Unit). This returns the BitmapPainter
that will render the QR code when passed to our Image
composable function:
@Composable
fun rememberQrBitmapPainter(
content: String,
size: Dp = 150.dp,
padding: Dp = 0.dp
): BitmapPainter {
return BitmapPainter()
}
Our rememberQrBitmapPainter
takes a content: String
which represents the text value we want to encode in our QR code, size:Dp
which represent the size(width x height) of our QR specified as a device-independent pixel value (to ensure we have a sharp image on different device with different pixel densities) and the padding:Dp
represent the amount of white space border we want around the generated code. The padding is set to a default of 0 to override/remove the default padding that the zxing library adds when generating a QR bitmap.
Let's add 4 new variables to our function;
density
to retrieve the current screen density information of the device using the LocalDensity
composition local. The Density
interface provides useful extension functions for performing simple operations like getting the device pixel equivalent of the .dp
values we specified earlier.
The sizePx
and paddingPx
variables are used to represent the pixel equivalent of the size and padding respectively.
A bitmap
mutable state variable to hold the bitmap of the QR code. In order to prevent our bitmap from being recalculated any time the UI is redrawn due to state changes (re-composition) we wrap it with a remember function with a key of content. The content key allows us to re-calculate the bitmap when the value our QR contains changes.
@Composable
fun rememberQrBitmapPainter(
content: String,
size: Dp = 150.dp,
padding: Dp = 0.dp
): BitmapPainter {
val density = LocalDensity.current
val sizePx = with(density) { size.roundToPx() }
val paddingPx = with(density) { padding.roundToPx() }
var bitmap by remember(content) {
mutableStateOf<Bitmap?>(null)
}
return BitmapPainter()
}
The QR code is a matrix which consists of multiple pixels whose values if computed on the UI thread, can cause your UI to stutter or hang; for this reason we will make use of a LaunchedEffect
and IO
CoroutineDispatcher
to offload the work required to compute the Bitmap of the QR code off the UI thread:
LaunchedEffect(bitmap) {
if (bitmap != null) return@LaunchedEffect
launch(Dispatchers.IO) {
val qrCodeWriter = QRCodeWriter()
val encodeHints = mutableMapOf<EncodeHintType, Any?>()
.apply {
this[EncodeHintType.MARGIN] = paddingPx
}
val bitmapMatrix = try {
qrCodeWriter.encode(
content, BarcodeFormat.QR_CODE,
sizePx, sizePx, encodeHints
)
} catch (ex: WriterException) {
null
}
}
}
Here we create a QRCodeWriter
instance which would be used to encode the QR. The encodeHints
variable is a map of EncodeHintType
to values which is used to configure the QRCodeWriter
. A bitmapMatrix
is created by encoding the content string and specifying the height and width of the QR.
We need to create a bitmap from the bitmapMatrix and set the color for each pixel (either white or black):
val matrixWidth = bitmapMatrix?.width ?: sizePx
val matrixHeight = bitmapMatrix?.height ?: sizePx
val newBitmap = Bitmap.createBitmap(
bitmapMatrix?.width ?: sizePx,
bitmapMatrix?.height ?: sizePx,
Bitmap.Config.ARGB_8888,
)
for (x in 0 until matrixWidth) {
for (y in 0 until matrixHeight) {
val shouldColorPixel = bitmapMatrix?.get(x, y) ?: false
val pixelColor = if (shouldColorPixel) Color.BLACK else Color.WHITE
newBitmap.setPixel(x, y, pixelColor)
}
}
bitmap = newBitmap
In our final step we return an instance of BitmapPainter
with the computed bitmap passed as an argument to it. If the bitmap is yet to be computed, we pass an empty bitmap of equal size with a transparent color. The bitmap is then converted to an image bitmap using the Bitmap.asImagePainter()
extension function.
return remember(bitmap) {
val currentBitmap = bitmap ?: Bitmap.createBitmap(
sizePx, sizePx,
Bitmap.Config.ARGB_8888,
).apply { eraseColor(Color.TRANSPARENT) }
BitmapPainter(currentBitmap.asImageBitmap())
}
How you would use it:
Image(
painter = rememberQrBitmapPainter("https://dev.to"),
contentDescription = "DEV Communit Code",
contentScale = ContentScale.FillBounds,
modifier = Modifier.size(135.dp),
)
Putting everything together we get: QRPainter.kt
I hope you find this article useful and thanks for reading.
Top comments (0)