How to Create an Animated Gender Selector in Jetpack Compose

Kappdev
6 min readJul 27, 2024

--

Welcome ๐Ÿ™‹

In this article, weโ€™ll create a cool animated Gender Selector in Jetpack Compose by leveraging Canvas and custom Shapes.

Letโ€™s dive in ๐Ÿš€

Shapes

First of all, we need to define the shapes that will be rendered as options.

Male ๐Ÿšน

object MaleShape : Shape {

override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline {
val scale = size.minDimension / 24f

val path = Path().apply {
// Head
addOval(Rect(Offset(12f * scale, 4f * scale), 2f * scale))

// Body and limbs
moveTo(14.0f * scale, 7.0f * scale)
lineTo(10.0f * scale, 7.0f * scale)
cubicTo(8.9f * scale, 7.0f * scale, 8.0f * scale, 7.9f * scale, 8.0f * scale, 9.0f * scale)
lineTo(8.0f * scale, 14.0f * scale)
cubicTo(8.0f * scale, 14.55f * scale, 8.45f * scale, 15.0f * scale, 9.0f * scale, 15.0f * scale)
lineTo(10.0f * scale, 15.0f * scale)
lineTo(10.0f * scale, 21.0f * scale)
cubicTo(10.0f * scale, 21.55f * scale, 10.45f * scale, 22.0f * scale, 11.0f * scale, 22.0f * scale)
lineTo(13.0f * scale, 22.0f * scale)
cubicTo(13.55f * scale, 22.0f * scale, 14.0f * scale, 21.55f * scale, 14.0f * scale, 21.0f * scale)
lineTo(14.0f * scale, 15.0f * scale)
lineTo(15.0f * scale, 15.0f * scale)
cubicTo(15.55f * scale, 15.0f * scale, 16.0f * scale, 14.55f * scale, 16.0f * scale, 14.0f * scale)
lineTo(16.0f * scale, 9.0f * scale)
cubicTo(16.0f * scale, 7.9f * scale, 15.1f * scale, 7.0f * scale, 14.0f * scale, 7.0f * scale)
close()
}

return Outline.Generic(path)
}
}

Female ๐Ÿšบ

object FemaleShape : Shape {

override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline {
val scale = size.minDimension / 24f

val path = Path().apply {
// Head
addOval(Rect(Offset(12.0f * scale, 4.0f * scale), 2.0f * scale))

// Body and limbs
moveTo(16.45f * scale, 14.63f * scale)
lineTo(13.93f * scale, 8.31f * scale)
cubicTo(13.61f * scale, 7.52f * scale, 12.85f * scale, 7.01f * scale, 12.0f * scale, 7.0f * scale)
cubicTo(11.15f * scale, 7.01f * scale, 10.38f * scale, 7.52f * scale, 10.07f * scale, 8.31f * scale)
lineTo(7.55f * scale, 14.63f * scale)
cubicTo(7.28f * scale, 15.29f * scale, 7.77f * scale, 16.0f * scale, 8.47f * scale, 16.0f * scale)
lineTo(10.0f * scale, 16.0f * scale)
lineTo(10.0f * scale, 21.0f * scale)
cubicTo(10.0f * scale, 21.55f * scale, 10.45f * scale, 22.0f * scale, 11.0f * scale, 22.0f * scale)
lineTo(13.0f * scale, 22.0f * scale)
cubicTo(13.55f * scale, 22.0f * scale, 14.0f * scale, 21.55f * scale, 14.0f * scale, 21.0f * scale)
lineTo(14.0f * scale, 16.0f * scale)
lineTo(15.53f * scale, 16.0f * scale)
cubicTo(16.23f * scale, 16.0f * scale, 16.72f * scale, 15.29f * scale, 16.45f * scale, 14.63f * scale)
close()
}

return Outline.Generic(path)
}
}

Gender Option

Now that we have defined the shapes, we can proceed to create a composable to render those shapes as option buttons.

GenderOptionStyle

Before creating the composable, we need to define a data class to represent the style of the option:

data class GenderOptionStyle(
val backgroundColor: Color,
val fillColor: Color,
val effectOrigin: TransformOrigin
) {
companion object {
val MaleDefault = GenderOptionStyle(Color.LightGray, Color.Blue, TransformOrigin(0f, 0f))
val FemaleDefault = GenderOptionStyle(Color.LightGray, Color.Red, TransformOrigin(1f, 0f))
}
}

The effectOrigin determines the center point for the fill effect animation, which is the origin of the growing circle effect.

Option code

@Composable
fun GenderOption(
shape: Shape,
selected: Boolean,
style: GenderOptionStyle,
modifier: Modifier = Modifier,
animationSpec: AnimationSpec<Float> = tween(400, easing = LinearEasing)
) {
// Animate the progress of the selection
val progress by animateFloatAsState(
targetValue = if (selected) 1f else 0f,
animationSpec = animationSpec
)

Canvas(
modifier = modifier.size(48.dp)
) {
// Turn the shape into an outline
val outline = shape.createOutline(size, layoutDirection, this)
// Create a path from the outline
val path = Path().apply { addOutline(outline) }

// Calculate the diagonal of the Canvas size
val diagonal = sqrt(size.width.pow(2) + size.height.pow(2))
// Calculate the radius of the fill circle based on the selection progress
val radius = diagonal * progress
// Determine the center of the fill effect based on the effectOrigin
val circleCenter = Offset(
x = size.width * style.effectOrigin.pivotFractionX,
y = size.height * style.effectOrigin.pivotFractionY
)

// Clip the drawing area to the shape path
clipPath(path) {
// Draw the background color
drawRect(style.backgroundColor)
// Draw the fill circle
drawCircle(
color = style.fillColor,
radius = radius,
center = circleCenter
)
}
}
}

Gender Selector

In this section, we will construct the selector composable.

Options

Before implementing the selector, letโ€™s define an enum class that represents the available options:

enum class Gender { MALE, FEMALE }

Utility

To make the code cleaner, letโ€™s define the clickableGenderOption modifier. This modifier will handle clicks on gender options without any visual indication:

fun Modifier.clickableGenderOption(
gender: Gender,
onGenderChange: (newGender: Gender) -> Unit
) = composed {
this.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { onGenderChange(gender) }
)
}

Selector Code

@Composable
fun GenderSelector(
selected: Gender,
modifier: Modifier = Modifier,
iconSize: Dp = 48.dp,
maleStyle: GenderOptionStyle = GenderOptionStyle.MaleDefault,
femaleStyle: GenderOptionStyle = GenderOptionStyle.FemaleDefault,
animationSpec: AnimationSpec<Float> = tween(400, easing = LinearEasing),
onGenderChange: (newGender: Gender) -> Unit
) {
// Arrange the gender options in a horizontal row
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) {
// Female gender option
GenderOption(
shape = FemaleShape,
style = femaleStyle,
selected = (selected == Gender.FEMALE),
animationSpec = animationSpec,
modifier = Modifier
.size(iconSize)
.clickableGenderOption(Gender.FEMALE, onGenderChange)
)
// Male gender option
GenderOption(
shape = MaleShape,
style = maleStyle,
selected = (selected == Gender.MALE),
animationSpec = animationSpec,
modifier = Modifier
.size(iconSize)
.clickableGenderOption(Gender.MALE, onGenderChange)
)
}
}

Congratulations ๐Ÿฅณ! Weโ€™ve successfully built it ๐Ÿ‘. You can find the full code on GitHub Gist ๐Ÿง‘โ€๐Ÿ’ป. Letโ€™s explore the usage ๐Ÿ‘‡

Practical Usage ๐Ÿ’โ€โ™‚๏ธ

There are plenty of interesting combinations of different effectOrigin. Let's consider a few of them ๐Ÿ‘€

State

All of the examples use this state:

var currentGender by remember { mutableStateOf(Gender.MALE) }

Default

GenderSelector(
selected = currentGender,
iconSize = 100.dp,
onGenderChange = {
currentGender = it
}
)

Bottom Rise

GenderSelector(
selected = currentGender,
iconSize = 100.dp,
femaleStyle = GenderOptionStyle.FemaleDefault.copy(
effectOrigin = TransformOrigin(0.5f, 1f)
),
maleStyle = GenderOptionStyle.MaleDefault.copy(
effectOrigin = TransformOrigin(0.5f, 1f)
),
onGenderChange = {
currentGender = it
}
)

Side Shifting

GenderSelector(
selected = currentGender,
iconSize = 100.dp,
femaleStyle = GenderOptionStyle.FemaleDefault.copy(
effectOrigin = TransformOrigin(1f, 0.5f)
),
maleStyle = GenderOptionStyle.MaleDefault.copy(
effectOrigin = TransformOrigin(0f, 0.5f)
),
onGenderChange = {
currentGender = it
}
)

You might also like ๐Ÿ‘‡

Thank you for reading this article! โค๏ธ If you found it enjoyable and valuable, show your appreciation by clapping ๐Ÿ‘ and following Kappdev for more exciting articles ๐Ÿ˜Š

๐Ÿ”” Subscribe to my ๐Ÿ‘‰ Email Notifications to stay updated with my latest content.

Happy coding!

--

--

Kappdev

๐Ÿ’ก Curious Explorer ๐Ÿงญ Kotlin and Compose enthusiast ๐Ÿ‘จโ€๐Ÿ’ป Passionate about self-development and growth โค๏ธโ€๐Ÿ”ฅ Push your boundaries ๐Ÿš€