Hey people ! Welcome to my blog on animating custom shapes, pulsating circles, draw shapes on canvas and animate them with compose.
This is the preview of what we will build. Let’s get started.

You can take reference of the source code from GitHub repository and above preview image is for the app which I will take as an example.
Dependency
Right now I’m using compose version 1.2.0-beta02 which is latest release.
implementation "androidx.compose.ui:ui:1.2.0-beta02"
implementation "androidx.compose.runtime:runtime-livedata:1.2.0-beta02"
1. Animate circle on canvas
First we will animate circle on canvas using drawArc().
This is how it should look.

We have to remember the initial float value in Animatable function that automatically animates its value.
val animateCircle = remember { Animatable(0f) }
In composable we launch coroutine with LaunchedEffect and animate the shape.
animateTo will starts an animation to animate from initialValue to the provided targetValue.
LaunchedEffect(animateCircle) {
animateCircle.animateTo(
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
easing = LinearEasing
),
repeatMode = RepeatMode.Restart
)
)
}
Animation properties
- animateValue – Animate from initial (0f) to target (1f) value.
- infiniteRepeatable – Creates duration based animation for infinite amount of times.
- tween – Animation type, alternative is spring, keyframes.
- durationMillis – Time taken for one animation cycle to complete.
- easing – Default is FastOutSlowInEasing, easing is just interpolation.
- repeatMode – Default is Restart, whether animation should begin from end or beginning(Reverse)
Now lets use drawArc() to draw animated circle on canvas.
Canvas(
modifier = Modifier
) {
drawArc(
color = Color(0xFF302522),
startAngle = 45f,
sweepAngle = 360f * animateCircle.value,
useCenter = false,
size = Size(80f, 80f),
style = Stroke(16f, cap = StrokeCap.Round)
)
}
drawArc( ) properties
- drawArc – Draws an arc scaled to fit inside the given rectangle.
- color – Color to be applied to the arc
- startAngle – 0 represents 3 o’clock. We will start at 45 -> 4:30.
- sweepAngle – We provide 360f to form full circle.
- useCenter – I kept it false because I don’t want a sector to form.
- size – Size taken by arc in canvas.
- style – DrawStyle default is Fill, I kept it stroke with 16 width and rounded.
At this a circle with border will be drawn on canvas, but we want its path to be animated from initial to target value, for that we have to pass the initial value to the drawArc() while it starts laying on Canvas.
We can multiply the initial float value with sweepAngle while it draws the arc. This will lay the arc with the progressive looking circle in canvas or like a loading circular progress bar.
drawArc(
sweepAngle = 360f * animateCircle.value
)
Here’s the combined code changes from above to animate circle.
@Composable
fun AnimateCircle() {
// Initial float value for animation start.
val animateCircle = remember { Animatable(0f) }
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
// Animation properties
LaunchedEffect(animateCircle) {
animateCircle.animateTo(
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
easing = LinearEasing
),
repeatMode = RepeatMode.Restart
)
)
}
// Draw arc in canvas which forms animated circle, repeatable.
Canvas(
modifier = Modifier
) {
drawArc(
color = Color(0xFF302522),
startAngle = 45f,
sweepAngle = 360f * animateCircle.value,
useCenter = false,
size = Size(80f, 80f),
style = Stroke(16f, cap = StrokeCap.Round)
)
}
}
}
Check whether your circle is animating as expected compared to my above preview. Now let’s move to animating a simple line.
2. Animate line on canvas
This is what we should try to animate, line expands and moves to target float.

Nothing changes much as compared to previous.
- Need a new Animatable property with different initial float value.
- Properties for animation remains same except initial float value.
- To animate line we will use drawLine().
We have to remember the initial float value in Animatable function that automatically animates its value.
val animateLine = remember { Animatable(0.6f) }
Launch coroutine and animate line with LaunchedEffect.
LaunchedEffect(animateLine) {
animateLine.animateTo(
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
easing = LinearEasing
),
repeatMode = RepeatMode.Restart
)
)
}
I’m using initial value 0.6f to reduce floating time of line to reach it’s final state. Setting it to 0f will make final animation output to look kind of aggressive. Change its value for your convenience.
Now draw a line on Canvas with drawLine().
Line has start and end offset points. So give (x, y) start and end offsets to line. Also multiply those offsets x, y with initial float value for animation to start.
drawLine(
color = Color(0xFF302522),
strokeWidth = 16f,
cap = StrokeCap.Round,
start = Offset(
80f * animateLine.value,
80f * animateLine.value
),
end = Offset(
110f * animateLine.value,
110f * animateLine.value
)
)
Combine circle and line
Brining all the changes in single composable, since we are using same animationSpec properties, we can reuse it by moving to separate composable.
fun AnimateShapeInfinitely(
animatableShape: Animatable<Float, AnimationVector1D>
) {
LaunchedEffect(animatableShape) {
animatableShape.animateTo(
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
easing = LinearEasing
),
repeatMode = RepeatMode.Restart
)
)
}
}
Finally this composable function draws circle and line in single canvas.
At this point you should be able to see both line and circle border animation.
@Composable
fun AnimateSearch() {
// Initial float value for circle.
val animateCircle = remember { Animatable(0f) }.apply {
AnimateShapeInfinitely(this)
}
// Initial float value for line.
val animateLine = remember { Animatable(0.6f) }.apply {
AnimateShapeInfinitely(this)
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
// Draw arc in canvas which forms animated circle, repeatable.
Canvas(
modifier = Modifier
) {
drawArc(
color = Color(0xFF302522),
startAngle = 45f,
sweepAngle = 360f * animateCircle.value,
useCenter = false,
size = Size(80f, 80f),
style = Stroke(16f, cap = StrokeCap.Round)
)
// Draw diagonal line in canvas.
drawLine(
color = Color(0xFF302522),
strokeWidth = 16f,
cap = StrokeCap.Round,
start = Offset(
animateLine.value * 80f,
animateLine.value * 80f
),
end = Offset(
animateLine.value * 110f,
animateLine.value * 110f
)
)
}
}
}
We have completed animating search look-a-like shape.
3. Pulsating circle animation
This is what we want to create.

Let’s start by drawing a circle in canvas.
Canvas(
modifier = Modifier.fillMaxSize()
) {
val canvasWidth = size.width
val canvasHeight = size.height
drawCircle(
color = Color(0xFFffb59c),
center = Offset(
x = canvasWidth / 2,
y = canvasHeight / 2
),
radius = size.minDimension / 4f,
)
}
GraphicsLayer
From Android Documentation
Can be used to apply effects to content, such as scaling, rotation, opacity, shadow, and clipping. Prefer this version when you have layer properties or an animated value.
Canvas modifier allows us to apply graphics scale properties. But we also want to animate the scaling of the circle on canvas between initial and target float values. So we should use animateFloat and create a infinite transition.
Create a InfiniteTransition that runs infinite child animations. Child animations will start running as soon as they enter the composition, and will not stop until they are removed from the composition.
val infiniteTransition = rememberInfiniteTransition()
With infinite transition you can animate float changes.
val scale by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1.2f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 600,
easing = LinearEasing
),
repeatMode = RepeatMode.Reverse
)
)
- initialValue – Float value where the animations starts.
- targetValue – Final float value where animation ends.
- infiniteRepeatable – Animations that plays infinite amount of times.
Now apply that scale float value graphics layer of canvas.
Canvas( modifier = Modifier .fillMaxSize() .graphicsLayer { scaleX = scale scaleY = scale } )
These are the final changes for single pulsating circle on canvas.
@Composable
fun PulsatingCircle() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
val infiniteTransition = rememberInfiniteTransition()
val scale by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1.2f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 600,
easing = LinearEasing
),
repeatMode = RepeatMode.Reverse
)
)
Canvas(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
scaleX = scale
scaleY = scale
}
) {
val canvasWidth = size.width
val canvasHeight = size.height
drawCircle(
color = Color(0xFFffb59c),
center = Offset(
x = canvasWidth / 2,
y = canvasHeight / 2
),
radius = size.minDimension / 4f,
)
}
}
}
Now let’s add 2 more circles with canvas.
4. Infinite repeatable pulsating animation
As shown in preview, we need 3 circles total with different animationSpec properties. This way we can make sure that the 3 circles won’t look same. So we will pass different values for below properties.
- Target float value
- Circle background color
- Animation duration
- Radius ratio
Let us create a reusable composable for 3 circles for infinite transition with function name scaleInfiniteTransition.
I have kept all initial states of 3 circles to 0f to make end result much smoother. Random or uneven gaps between initial, target, durationMillis will make end animation look more abrupt and aggressively pushing it’s bounds.
fun scaleInfiniteTransition(
initialValue: Float = 0f,
targetValue: Float,
durationMillis: Int,
): Float {
val infiniteTransition = rememberInfiniteTransition()
val scale: Float by infiniteTransition.animateFloat(
initialValue = initialValue,
targetValue = targetValue,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = durationMillis,
easing = LinearEasing
),
repeatMode = RepeatMode.Reverse
)
)
return scale
}
Let’s create one more reusable composable which draws single circle on canvas every time its called.
The function parameters allows us to pass different scale, color and radius ratio for us.
fun DrawCircleOnCanvas(
scale: Float,
color: Color,
radiusRatio: Float
) {
Canvas(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
scaleX = scale
scaleY = scale
}
) {
drawCircle(
color = color,
center = Offset(
x = size.width / 2,
y = size.height / 2
),
radius = size.minDimension / radiusRatio
)
}
}
Now we need 3 different circle background color. But instead use only one color with variable alpha to make animation look smoother and with less resources.
val circleColor = Color(0xFFffb59c)
val frontCircle = circleColor.copy(0.75f)
val midCircle = circleColor.copy(0.50f)
val backCircle = circleColor.copy(0.25f)
I have given different names for each variant colors, making easy to identify circles drawn on top of each other.
Now invoke the function DrawCircleOnCanvas( ) three times with different animation properties each time.
DrawCircleOnCanvas(
scale = scaleInfiniteTransition(targetValue = 2f, durationMillis = 600),
color = backCircle,
radiusRatio = 4f
)
DrawCircleOnCanvas(
scale = scaleInfiniteTransition(targetValue = 2.5f, durationMillis = 800),
color = midCircle,
radiusRatio = 6f
)
DrawCircleOnCanvas(
scale = scaleInfiniteTransition(targetValue = 3f, durationMillis = 1000),
color = frontCircle,
radiusRatio = 12f
)
Combining all the above code, this is the final code changes.
@Composable
fun InfinitelyFlowingCircles() {
val circleColor = Color(0xFFffb59c)
val frontCircle = circleColor.copy(0.75f)
val midCircle = circleColor.copy(0.50f)
val backCircle = circleColor.copy(0.25f)
// back circle
DrawCircleOnCanvas()
// middle circle
DrawCircleOnCanvas()
// front circle
DrawCircleOnCanvas()
}
fun DrawCircleOnCanvas(
scale: Float,
color: Color,
radiusRatio: Float
) {
Canvas() { drawCircle() }
}
fun scaleInfiniteTransition(
initialValue: Float = 0f,
targetValue: Float,
durationMillis: Int,
): Float {
val infiniteTransition = rememberInfiniteTransition()
val scale: Float by infiniteTransition.animateFloat()
return scale
}
Final changes
Combine search animation and circles in one composable.
We have earlier completed animating search in composable function AnimateSearch.
Now 3 pulsating circles are in other composable function InfinitelyFlowingCircles.
Bring both in same composable and center align them to achieve the final state of animation.
@Composable
fun AnimatedCirclesAndSearch() {
// Pulsating circles
InfinitelyFlowingCircles()
// Search look-a-like animation
Box(
modifier = Modifier
.fillMaxSize()
.padding(bottom = 28.dp, end = 28.dp),
contentAlignment = Alignment.Center
) {
AnimateSearch()
}
}
I have added extra Box and padding to AnimateSearch( ) so that I can make adjustments to center align them.
That’s it !
Take a look at GitHub repository which I have linked below has complete code example on building advanced layouts, fetching data from api, animations, draw on canvas, search functionality, loader, UI state handling, Image loading, light/dark theme with MD3, palette usage etc.
Read all the detailed guidelines from Android documentation, it covers everything in much depth.
5. Project code and Resources
Android Documentation on Architecture.
1. Animations with jetpack compose
2. Codelab on compose animations
3. Core compose animations android

Rajasekhar K E
Hi ! I’m Rajasekhar a Programmer who does Android Development, Creative & Technical writing, Kotlin enthusiast and Engineering graduate. I learn from Open Source and always happy to assist others with my work. I spend most of time Training, Assisting & Mentoring students who are absolute Beginners in android development. I’m also running my startup named Developers Breach which mostly works on contributing to open source.
Here We Go Again : (
if (article == helpful) {
println("Like and subscribe to blog newsletter.")
} else {
println("Let me know what i should blog on.")
}
You must be logged in to post a comment.