Drawing and painting in Jetpack Compose #1
Tags: android, kotlin, jetpackcompose
Published: 2021-01-04 11:12:45

Recently I blogged about [using shapes in Jetpack Compose](https://dev.to/tkuenneth/using-shapes-in-jetpack-compose-3ggb). As you have seen, `RectangleShape`, `CircleShape` and `GenericShape` are great for applying simple forms (shapes) to composables. But what about drawing lines, dots or circles that are not filled or have open sides? Let's investigate how we can become an artist. Now, that may sound a little over the top. But I did it on purpose. Because artists often paint on canvas, and `Canvas` is very important for us, too. Take a look:

![Two lines and a filled circle](https://dev-to-uploads.s3.amazonaws.com/i/06z4rwf4u8odn2j8tb8c.png)

```kotlin
@Composable
fun SimpleCanvas() {
  Canvas(modifier = Modifier.fillMaxWidth().preferredHeight(128.dp),
    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)
      )
    })
}
```

`Canvas` is a composable that [allows you to](https://developer.android.com/reference/kotlin/androidx/compose/foundation/package-summary#Canvas(androidx.compose.ui.Modifier,%20kotlin.Function1))

> specify an area on the screen and perform canvas drawing 
> on this area. You MUST specify size with modifier, whether
> with exact sizes via `Modifier.size` modifier, or relative to
> parent, via `Modifier.fillMaxSize`, `ColumnScope.weight`, etc.
> If parent wraps this child, only exact sizes must be specified.

Drawing instructions are given inside `onDraw`. My example produces two lines and a filled circle. Instead of solid red we could for example use a linear gradient. But before I show you how to achieve this, let's create an outline instead.

![Circle with a dashed red line](https://dev-to-uploads.s3.amazonaws.com/i/5mqz1svrjdm6h5oduzga.png)

```kotlin
drawCircle(
  Color.Red, 64f,
  Offset(size.width / 2, size.height / 2),
  style = Stroke(width = 8f,
    pathEffect = DashPathEffect(floatArrayOf(10f, 10f), 0f)
  ),
)
```

`Stroke` is no composable but a class. Its `pathEffect` receives a `NativePathEffect`, which is a typealias for `android.graphics.PathEffect`. It is extended by (among others) `android.graphics.DashPathEffect`. [Its documentation explains](https://developer.android.com/reference/android/graphics/DashPathEffect):

> The intervals array must contain an even number of entries
> (>=2), with the even indices specifying the "on" intervals, 
> and the odd indices specifying the "off" intervals.

 So, in my example the distances for "on" and "off" are equal (`10f`). The `Stroke` has a width (or thickness) of `8f`. How big this is depends on the device configuration. Now, let's turn to gradients, shall we?

![Circle filled with a linear gradient](https://dev-to-uploads.s3.amazonaws.com/i/yveri8ec5g0bwyu3t9ic.png)

```kotlin
@Composable
fun CanvasWithGradient() {
  Canvas(modifier = Modifier.fillMaxWidth().preferredHeight(128.dp),
    onDraw = {
      val gradient = LinearGradient(
        listOf(Color.Blue, Color.Black),
        startX = size.width / 2 - 64, startY = size.height / 2 - 64,
        endX = size.width / 2 + 64, endY = size.height / 2 + 64,
        tileMode = TileMode.Clamp
      )
      drawCircle(
        gradient, 64f,
      )
    })
}
```

Have you noticed that I did not provide the center of the circle? Thanks to the default value in `drawCircle()` this is not necessary. Nonetheless I am using (that is, computing) the values (`size.width / 2` and `size.height / 2`), because I need them for the definition of my linear gradient. I want it to cover only the area of the circle. Its upper left corner is `startX`, `startY` and the lower right corner is `endX`, `endY`.

A radial gradient looks like this:

![Circle with a radial gradient](https://dev-to-uploads.s3.amazonaws.com/i/zy8urmd27ll5nhcklnvf.png)

```kotlin
val gradient = RadialGradient(
  listOf(Color.Black, Color.Blue),
  centerX = center.x, centerY = center.y,
  radius = 64f
)
drawCircle(
  gradient, 64f,
)
```

`center.x` and `center.y` specify the center of my canvas. This is used for the center of the circle (its default value), so I can reuse it for my radial gradient. There are more gradients. You may, for example, want to take a look at `VerticalGradient` or `HorizontalGradient`.

To conclude this post, let's draw some individual pixels. Here is a composable that draws a sinus curve in a carthesian coordinate system.

![a sinus curve in a carthesian coordinate system](https://dev-to-uploads.s3.amazonaws.com/i/w0mqn9im9rpdyixefmif.png)

```kotlin
@Composable
fun SinusPlotter() {
  Canvas(modifier = Modifier.fillMaxSize(),
    onDraw = {
      val middleW = size.width / 2
      val middleH = size.height / 2
      drawLine(Color.Gray, Offset(0f, middleH), Offset(size.width - 1, middleH))
      drawLine(Color.Gray, Offset(middleW, 0f), Offset(middleW, size.height - 1))
      val points = mutableListOf<Offset>()
      for (x in 0 until size.width.toInt()) {
        val y = (sin(x * (2f * PI / size.width)) * middleH + middleH).toFloat()
        points.add(Offset(x.toFloat(), y))
      }
      drawPoints(
        points = points,
        strokeWidth = 4f,
        pointMode = PointMode.Points,
        color = Color.Blue
      )
    }
  )
}
```

`drawPoints()` receives a list if `Offset` instances. `PointMode.Points` means: draw individual points. Please note that I deliberately set `strokeWidth` because its default `Stroke.HairlineWidth` led to no visible output.

One more thing... You may be wondering how the axes of the coordinate system can look like arrows. While `drawLine()` can recieve both `strokeWidth` and `cap`, the latter one is used for both ends. Also, while there is `StrokeCap.Round`, `Square` and `Butt`, there seems to be no `Arrow`. So for now my resolution would be to draw the arrow by myself. Here is how this might look like for the horizontal axis:

```kotlin
drawPath(
  path = Path().apply {
    moveTo(size.width - 1, middleH)
    relativeLineTo(-20f, 20f)
    relativeLineTo(0f, -40F)
    close()
  },
  Color.Gray,
)
```

As you can see, I am using an old friend, `Path`, which is then passed to `drawPath()`. The vertical axis has just a slightly changed set of drawing instructions:

```kotlin
moveTo(size.width - 1, middleH)
relativeLineTo(-20f, 20f)
relativeLineTo(0f, -40F)
```

That's it for today. I hope to follow up on this soon. Kindly share your thoughts in the comments.

---

[source](https://github.com/tkuenneth/android-demos/tree/master/compose/DrawingDemo)