DEV Community

Akash Kamble
Akash Kamble

Posted on

7

Create art using Jetpack Compose

Inspired by recent amazing tweets of @alexlockwood about jetpack compose I started playing with jetpack compose canvas and want to share what I learned so far.

Note: To understand the below code you must have basic knowledge of jetpack compose.

In this article, we will look at how to make the below animation in jetpack compose.

Alt Text

This article is based on jetpack compose version 1.0.0-alpha05

To add jetpack compose in your project go to your app-level build.gradle file and add below code

Alt Text

Now, we will start writing our composables.

Alt Text

To create a container to hold your canvas
In your setContent composable add the below code.

Alt Text

Write code to draw a single rectangle first.

To draw a rectangle on the canvas, canvas provides drawRect function.

package com.akash.canvasplayground.beesandbombs
import androidx.compose.foundation.Canvas
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.unit.dp
/**
* Created by Akash on 20/10/20
*/
@Composable
fun Squares(modifier: Modifier) {
val rectSize = 40.dp
Canvas(modifier = modifier.clipToBounds()) {
translate(size.width / 2, size.height / 2) {
drawRect(
brush = SolidColor(Color.Black),
Offset(-(rectSize / 2).toPx(), -(rectSize / 2).toPx()),
Size(rectSize.toPx(), rectSize.toPx()),
style = Stroke(width = 2f)
)
}
}
}
view raw Temp.kt hosted with ❤ by GitHub

Alt Text

Let's understand the above code

  1. The coordinates start from the top left corner of the canvas and the translate function moves the canvas at a given point. With the above code, our (0,0) coordinates are now at the center of the canvas.
  2. drawRect function requires brush, topLeft offset point on the canvas, size, style, alpha, colorFilter and blendMode as arguments. For now we will focus only on brush, topLeft Offset, size and style.

As you have learned to draw a single rectangle on canvas now, we will repeat the same process to draw multiple rectangles of different sizes.
For simplicity and Maintainability, we will create a data class RectData which takes size, OffsetX, offsetY for now (later in this article we will add color as a property). Make a list of RectData and iterate over it as shown in the code below.

package com.akash.canvasplayground.beesandbombs
import androidx.compose.foundation.Canvas
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/**
* Created by Akash on 17/10/20
*/
@Composable
fun Squares(modifier: Modifier) {
val n = 60
val rectSize = 20.dp
val rectList = mutableListOf<RectData>()
for (i in 0..n) {
val s = rectSize * i
val offsetX = -s / 2
val offsetY = -s / 2
val rect = RectData(s, offsetX, offsetY)
rectList.add(rect)
}
Canvas(modifier = modifier.clipToBounds()) {
translate(size.width / 2, size.height / 2) {
for (i in 0..n) {
drawRect(
brush = SolidColor(Color.Black),
Offset(rectList[i].offsetX.toPx(), rectList[i].offsetY.toPx()),
Size(rectList[i].size.toPx(), rectList[i].size.toPx()),
style = Stroke(width = 2f)
)
}
}
}
}
data class RectData(
val size: Dp,
val offsetX: Dp,
val offsetY: Dp
)
view raw Temp.kt hosted with ❤ by GitHub

Run the app and you will see an output as shown in the below image.

Alt Text

Now we are moving towards an interesting part of this article.
For the animation, we need to rotate the rectangles at a particular angle in every frame.
In order to this, we must get the time taken by our composable till now and multiply it with 360f.
The below code gives us a state with animation time taken by our composable till now in milliseconds and every time the state value changes the rectangles will be drawn at a new angle.

package com.akash.canvasplayground.utils
import androidx.compose.runtime.*
import androidx.compose.runtime.dispatch.withFrameMillis
import androidx.compose.ui.platform.LifecycleOwnerAmbient
import androidx.lifecycle.whenStarted
/**
* Created by Akash on 16/10/20
* Copied From Alex LockWoods Compose bees-and-bombs compose samples
* (https://github.com/alexjlockwood/bees-and-bombs-compose)
* Returns a [State] holding a local animation time in milliseconds. The value always starts
* at `0L` and stops updating when the call leaves the composition.
*/
@Composable
fun animationTimeMillis(): State<Long> {
val millisState = mutableStateOf(0L)
val lifecycleOwner = LifecycleOwnerAmbient.current
LaunchedTask {
val startTime = withFrameMillis { it }
lifecycleOwner.whenStarted {
while (true) {
withFrameMillis { frameTime ->
millisState.value = frameTime - startTime
}
}
}
}
return millisState
}

To rotate anything on the canvas we have to use rotate function which takes angle and Offset(x, y) point.
We will wrap drawRect function in rotate function as shown in the below code.

package com.akash.canvasplayground.beesandbombs
import androidx.compose.foundation.Canvas
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.akash.canvasplayground.utils.animationTimeMillis
/**
* Created by Akash on 17/10/20
*/
@Composable
fun Squares(modifier: Modifier) {
val animatedProgress = animationTimeMillis()
val n = 30
val rectSize = 20.dp
val rectList = mutableListOf<RectData>()
for (i in 0..n) {
val s = rectSize * i
val offsetX = -s / 2
val offsetY = -s / 2
val rect = RectData(s, offsetX, offsetY)
rectList.add(rect)
}
Canvas(modifier = modifier.clipToBounds()) {
translate(size.width / 2, size.height / 2) {
val angle = (animatedProgress.value) * 0.0001f * 360f
for (i in 0..n) {
rotate(angle, Offset(0f, 0f)) {
drawRect(
brush = SolidColor(Color.Black),
Offset(rectList[i].offsetX.toPx(), rectList[i].offsetY.toPx()),
Size(rectList[i].size.toPx(), rectList[i].size.toPx()),
style = Stroke(width = 2f)
)
}
}
}
}
}
data class RectData(
val size: Dp,
val offsetX: Dp,
val offsetY: Dp
)
view raw Temp.kt hosted with ❤ by GitHub

And if we run the app we will get an output as below.

Alt Text

But, that's not what we wanted. We want our inner rectangle to be rotated at a faster speed and gradually decrease the speed as we go towards outer rectangles.
For this, we will change the value of the angle depending on the index of the rectangle in the list.

package com.akash.canvasplayground.beesandbombs
import androidx.compose.foundation.Canvas
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.akash.canvasplayground.utils.animationTimeMillis
/**
* Created by Akash on 17/10/20
*/
@Composable
fun Squares(modifier: Modifier) {
val animatedProgress = animationTimeMillis()
val n = 30
val rectSize = 20.dp
val rectList = mutableListOf<RectData>()
for (i in 0..n) {
val s = rectSize * i
val offsetX = -s / 2
val offsetY = -s / 2
val rect = RectData(s, offsetX, offsetY)
rectList.add(rect)
}
Canvas(modifier = modifier.clipToBounds()) {
translate(size.width / 2, size.height / 2) {
val angle = (animatedProgress.value) * 0.0001f * 360f
for (i in 0..n) {
rotate(angle * (n - i + 1) * 0.07f, Offset(0f, 0f)) {
drawRect(
brush = SolidColor(Color.Black),
Offset(rectList[i].offsetX.toPx(), rectList[i].offsetY.toPx()),
Size(rectList[i].size.toPx(), rectList[i].size.toPx()),
style = Stroke(width = 2f)
)
}
}
}
}
}
data class RectData(
val size: Dp,
val offsetX: Dp,
val offsetY: Dp
)
view raw Temp.kt hosted with ❤ by GitHub

And the output will be as below.

Alt Text

It's time to add colors now.
Make colorList, add a new property in RectData as color, and remove the style of drawRect as default style is of Type Fill.

package com.akash.canvasplayground.beesandbombs
import androidx.compose.foundation.Canvas
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.akash.canvasplayground.utils.animationTimeMillis
/**
* Created by Akash on 17/10/20
*/
@Composable
fun Squares(modifier: Modifier) {
val animatedProgress = animationTimeMillis()
val n = 30
val rectSize = 20.dp
val rectList = mutableListOf<RectData>()
val colorList = listOf(
Color(red = 40, green = 223, blue = 153),
Color(red = 210, green = 246, blue = 197),
Color(red = 246, green = 247, blue = 212)
)
for (i in 0..n) {
val s = rectSize * i
val offsetX = -s / 2
val offsetY = -s / 2
val rect = RectData(s, offsetX, offsetY, colorList[i % 3])
rectList.add(rect)
}
Canvas(modifier = modifier.clipToBounds()) {
translate(size.width / 2, size.height / 2) {
val angle = (animatedProgress.value) * 0.0001f * 360f
for (i in 0..n) {
rotate(angle * (n - i + 1) * 0.07f, Offset(0f, 0f)) {
drawRect(
brush = SolidColor(rectList[i].color),
Offset(rectList[i].offsetX.toPx(), rectList[i].offsetY.toPx()),
Size(rectList[i].size.toPx(), rectList[i].size.toPx()),
)
}
}
}
}
}
data class RectData(
val size: Dp,
val offsetX: Dp,
val offsetY: Dp,
val color: Color
)
view raw Temp.kt hosted with ❤ by GitHub

And the output is as below

Alt Text

In the above image, the outer rectangles are getting drawn on top of the inner rectangles, so we draw them in reverse order.

And the output will be as below.

Alt Text

For final code and many more canvas animations checkout CanvasPlayGround.

Sentry mobile image

Improving mobile performance, from slow screens to app start time

Based on our experience working with thousands of mobile developer teams, we developed a mobile monitoring maturity curve.

Read more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please consider leaving a ❤️ or a friendly comment if you found this post helpful!

Okay