DEV Community

Cover image for Cartographing Jetpack Compose: foundation
Thomas Künneth
Thomas Künneth

Posted on

Cartographing Jetpack Compose: foundation

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"
  )
}
Enter fullscreen mode Exit fullscreen mode

A simple border

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 the ScrollState 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)
  )
}
Enter fullscreen mode Exit fullscreen mode

App showing a scrollable text

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")
    }
}
Enter fullscreen mode Exit fullscreen mode

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 of BitmapPainter or VectorPainter 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)
)
Enter fullscreen mode Exit fullscreen mode

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")
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

So, we just need to...

  • get a Drawable using ResourcesCompat.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 function asImageBitmap()

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)
      )
    })
}
Enter fullscreen mode Exit fullscreen mode

Drawing on a canvas

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)