Custom UI Components using Jetpack Compose Canvas based on NeoPoP Design — Part 1 (Radio Button)

Make Apps Simple
Published in
3 min readAug 14, 2022


An exploration into Jetpack compose animation and canvas to create custom UI components

What we would be creating

End result UI

The UI components created will be based on NeoPop UI from Cred. It will be used as a reference and not everything would be exactly the same.

I would be writing separate articles for each component.

This is the first one for Radio Button.

Project Creation

Let’s start by creating a new Android project using the Empty Compose Activity template.


Added the dependencies used here as some are alpha/beta and they might be changed in the future.

Now that project is set up, let's start with the Radio Button.


  1. A default state UI:
NeoPop Default State UI
Default State

2. A selected state UI:

NeoPop Selected State UI
Selected State

3. UI to be created using Jetpack compose canvas

4. Animations when switching states

5. Cancellable Animations

As we would be animating between the selection and default states, I am creating a single composable with parameters to define the state at any given point.


  • innerRadius: The radius of the white inner circle
  • outerWidth: Width of the black circle

At any given point in time innerRadius + outerWidth will be equal to the radius of the radio button. Hence size = (innerRadius + outerWidth) * 2 .

Things to notice

  1. toPx() extension function from Dp can be used for Dp to Px conversion.
  2. The order of drawing inside the Canvas is important. Thought in this case, both the circles are not overlapping, if there are overlapping drawings on canvas the latest drawing will be drawn on top of the previous one.
  3. When drawing a circle with stroke style, the radius should be the center as the stroke would be on both sides of the circle equally.

Now that we have completed the UI using Canvas, we can proceed to use them along with Animations.


The code seems like a lot 😅, but most of it is only for making smoother animation.

The borderWidthValue and selectionWidthValue is written like this as a safety measure in case the passed borderWidth or selectionWidth is too large.

The division by 10 & 2.dp are random values.

  • .value is used to convert Dp to Float .
  • The code inside LaunchedEffect is triggered every time the radio button is clicked.
  • Simultaneous animations: Both the outer circle and inner should animate in parallel, so we have to use separate launch{} to start them at the same time.


For cancellable animation, we have to use the coroutine scope of the LaunchedEffect . If you use coroutine from rememberCoroutineScope() , it gets canceled only when the composable is disposed.


You can add more customization as required.

Thanks for reading.

Please comment with your valuable feedback.
It would help me to improvise.

If you like this article and you would be interested to know about the other UI components given at the starting please 👏👏👏 .



Make Apps Simple