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!