Welcome to the second installment of Cartographing Jetpack Compose. In the first part we looked at androidx.compose.compiler
and androidx.compose.runtime
. Both provide essential groundwork and keep the compose machinery working, for example by modifying or amending composable functions during compile time, and by bringing on screen what has been emitted by a composable during runtime.
This time we will be looking at androidx.compose.foundation
. The docs describe is like this:
Write Jetpack Compose applications with ready to use building
blocks and extend foundation to build your own design system
pieces.
As of May 2021 we see quite a few subpackages: layout, shape, gestures, selection, lazy, interaction and text. Before we turn to them in the next episode, let's look at the base package, androidx.compose.foundation
. We have a few interfaces, Indication
and IndicationInstance
. They deal with visual effects that occur when certain interactions happens. Both sort of accompany a Top-level property called LocalIndication
, a CompositionLocal
that provides an Indication through the hierarchy.
We also see a few classes, for example BorderStroke
(to specify the stroke to draw borders with) and ScrollState
(state of the scroll). Here's how to use them.
@Composable
@Preview
fun Sandbox() {
Text(
modifier = Modifier
.border(
BorderStroke(4.dp, Color.Red)
)
.padding(8.dp),
text = "Hello Compose"
)
}
As you can see I use BorderStroke
to create a simple border around a Text()
. Please be aware that BorderStroke
is both a class and a Top-level function.
Next: ScrollState
. The docs say:
Create and
remember
theScrollState
based on the currently
appropriate scroll configuration to allow changing scroll
position or observing scroll behavior.
@Composable
@Preview
fun Sandbox() {
val state = rememberScrollState()
val text = "${(1..100).joinToString("") { "${it}\n" }}- end -"
Text(
text = text,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxSize()
.verticalScroll(state)
)
}
My composable first creates a string consisting of 100 lines with ascending numbers followed by a final line - end -
. This string is passed to Text()
. Spot the modifier
. The text wants all available size (fillMaxSize()
). Also, verticalScroll()
makes it scrollable vertically. The docs say:
Modify element to allow to scroll vertically when height of the
content is bigger than max constraints allow.
Naturally, verticalScroll()
is an extension function to Modifier
. It receives the state of the scroll, which is created using rememberScrollState()
.
Now let's look at the Top-level functions. They include very important basic composables, for example Image()
and Canvas()
.
Displaying images
Displaying static images is super easy:
@Composable
fun Sandbox() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(R.drawable.ic_launcher_foreground),
"An image",
modifier = Modifier.requiredSize(96.dp)
)
Text("Hello Image")
}
}
As you can see I obtain a painter
through painterResource()
. The docs say:
Create a
Painter
from an Android resource id. This can load
either an instance ofBitmapPainter
orVectorPainter
for
ImageBitmap
based assets or vector based assets respectively.
The resources with the given id must point to either fully
rasterized images (ex. PNG or JPG files) or VectorDrawable xml
assets. API based xml Drawables are not supported here.
There is one caveat, though.
If you try this:
Image(
painter = painterResource(R.mipmap.ic_launcher),
"An image",
modifier = Modifier.requiredSize(96.dp)
)
you will get a IllegalArgumentException: Only VectorDrawables and rasterized asset types are supported ex. PNG, JPG
at runtime if your app icon is an adaptive icon. This is the case if your have a file ic_launcher.xml in mipmap-anydpi-v26. Yet, it's easy to load adaptive icons:
@Composable
fun Sandbox() {
ResourcesCompat.getDrawable(
LocalContext.current.resources,
R.mipmap.ic_launcher, LocalContext.current.theme
)?.let { drawable ->
val bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth, drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
// painter = painterResource(R.mipmap.ic_launcher),
bitmap = bitmap.asImageBitmap(),
"An image",
modifier = Modifier.requiredSize(96.dp)
)
Text("Hello Image")
}
}
}
So, we just need to...
- get a
Drawable
usingResourcesCompat.getDrawable()
- create an empty
android.graphics.Bitmap
- create an
android.graphics.Canvas
that operates on this bitmap - draw the drawable on the canvas (into our bitmap)
- get an
androidx.compose.ui.graphics.ImageBitmap
instance using the extension functionasImageBitmap()
Quite easy, isn't it? 😎
Drawing with Canvas()
Just like Image()
, Canvas()
is a Top-level function in androidx.compose.foundation
. Please do not confuse it with the class we saw moments ago, android.graphics.Canvas
(which belongs to the Android framework). I have written a complete series called Drawing and painting in Jetpack Compose, so here I will show you just one example.
@Composable
@Preview
fun Sandbox() {
Canvas(modifier = Modifier.fillMaxSize(),
onDraw = {
drawLine(
Color.Black, Offset(0f, 0f),
Offset(size.width - 1, size.height - 1)
)
drawLine(
Color.Black, Offset(0f, size.height - 1),
Offset(size.width - 1, 0f)
)
drawCircle(
Color.Red, 64f,
Offset(size.width / 2, size.height / 2)
)
})
}
You specify an area on the screen and perform drawing operations on it. These instructions are given inside onDraw()
. My example produces two lines and a filled circle.
Wrap up
I hope you like this series. Please share your thoughts in the comments. In the next installment we will turn to the subpackages of androidx.compose.foundation
. So stay tuned.
Top comments (0)