More on Canvas

tkuenneth

Thomas Künneth

Posted on January 15, 2021

More on Canvas

In the previous posts of this series we have taken a closer look at how to use shapes and canvases. In this installment I will be focussing on when to draw something. Or why.

Sounds weird? Hang on.

Composables are the building blocks of our user interface. We can either use them as is or amend them with prebuilt functionaly. First, as is:

Text("Hello Compose")
Enter fullscreen mode Exit fullscreen mode

A simple text

To customize both look and behavior of a composable we use modifier:

Text(
  modifier = Modifier.border(
    1.dp,
    MaterialTheme.colors.primary
  )
    .padding(8.dp)
    .shadow(4.dp, shape = CircleShape, clip = true),
  text = "Hello Compose"
)
Enter fullscreen mode Exit fullscreen mode

A modified composable

As you can see, we have amended, modified, our composable quite a bit by adding a border, a shadow and a padding. There are a lot of prebuilt modifiers. But what if none of these suits your needs? Just roll your own. As you will see shortly, this is very easy with Compose. But let's take a look at the inner workings first.

A modifier is an ordered, immutable collection of modifier elements. androidx.compose.ui.Modifier is an interface, that helps you define such a collection. then() returns a Modifier representing the current modifier followed by another one in sequence. There is also a companion object that implements this interface. It may be used as the start of a modifier extension factory expression. We'll see soon what this means. For now, please take a look at padding():

The padding() modifier

As you can see, then() receives a PaddingModifier (a private class inside androidx.compose.foundation.layout.Padding.kt). Since this affects more layout than drawing I'll not elaborate on this. What's important: this is another implementation of the Modifier interface. Which one, depends on what other extension functions have been called before. In my example border() is invoked first. Now let's tackle this modifier extension factory expression thing. Here's how some lines of border() look like.

The border() modifier (beginning)

The border() modifier (end)

It returns the result of the composed() modifier, which receives a factory and a inspectorInfo.

The composed() modifier

The factory is both a composable and an extension function of Modifier. It is wrapped in a ComposedModifier which in turn is passed to then(). ComposedModifier is a private class that extends abstract InspectorValueInfo and implements Modifier.Element, a single element contained within a Modifier chain. But when do you use then()? And when is a factory preferrable? Fortunately, most of the time you won't need to deal with these questions. That's because there are a few Modifier extension functions that allow us to draw: drawWithContent(), drawBehind() and drawWithCache().

fun Modifier.drawOnYellow() = this.drawBehind {
  drawRect(Color.Yellow)
}
Enter fullscreen mode Exit fullscreen mode

This modifier draws a yellow rectangle behind the composable that is amended with it.

Text(
  modifier = Modifier.drawOnYellow(),
  text = "Hello Compose"
)
Enter fullscreen mode Exit fullscreen mode

Text() with a yellow background

Your lambda receives an instance of androidx.compose.ui.graphics.drawscope.DrawScope. This interface defines

a scoped drawing environment with the provided Canvas.
This provides a declarative, stateless API to draw shapes
and paths without requiring consumers to maintain
underlying Canvas state information.

Next.

drawWithContent() allows us to draw before or after the layout's contents.

fun Modifier.drawRedCross() = this.drawWithContent {
  drawContent()
  drawLine(
    Color.Red, Offset(0f, 0f),
    Offset(size.width - 1, size.height - 1),
    blendMode = BlendMode.SrcAtop,
    strokeWidth = 8f
  )
  drawLine(
    Color.Red, Offset(0f, size.height - 1),
    Offset(size.width - 1, 0f),
    blendMode = BlendMode.SrcAtop,
    strokeWidth = 8f
  )
}
Enter fullscreen mode Exit fullscreen mode
Text(
  modifier = Modifier.drawRedCross(),
  text = "Imperative"
)
Enter fullscreen mode Exit fullscreen mode

Using drawWithContent()

The location of drawContent() in your lambda defines when the content is drawn. Finally, drawWithCache() draws into

a DrawScope with content that is persisted across
draw calls as long as the size of the drawing area is
the same or any state objects that are read have not
changed. In the event that the drawing area changes, or
the underlying state values that are being read
change, this method is invoked again to recreate objects
to be used during drawing

As you have seen in this post, it is very easy to write extension functions that can modify your composables by drawing on a canvas. Are there other topics related to Canvas you would like me to cover? Please share your thoughts in the comments.

💖 💪 🙅 🚩
tkuenneth
Thomas Künneth

Posted on January 15, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

More on Canvas
kotlin More on Canvas

January 15, 2021