How to Create a Cute 3D Radio Button in Jetpack Compose

Kappdev
5 min readJun 15, 2024

--

Welcome 👋

In this article, we’ll create a pretty cute 3D Radio Button with Jetpack Compose, enhancing your app’s appearance in just 5 minutes.

Let’s explore the code! 🔎

Defining Constants

Let’s start our journey by defining the constants that we’ll use later.

// Duration for radio button animation
private const val RadioAnimationDuration = 100

// Offset and blur radius for shadow effects
private val RadioShadowOffset = 1.dp
private val RadioShadowBlur = 2.dp

// Colors for shadow and glare effects
private val RadioShadowColor = Color.Black.copy(0.54f)
private val RadioGlareColor = Color.White.copy(0.64f)

// Size constants for the radio button and its dot
private val RadioButtonDotSize = 12.dp
private val RadioStrokeWidth = 3.dp
private val RadioButtonSize = 22.dp

Defining Colors for Different States

The next step is to define the colors the radio button uses in various states (selected, unselected, enabled, disabled). We’ll create a class ConvexRadioButtonColors to manage these colors.

class ConvexRadioButtonColors(
private val selectedColor: Color,
private val unselectedColor: Color,
private val disabledSelectedColor: Color,
private val disabledUnselectedColor: Color
) {
@Composable
fun radioColorAsState(enabled: Boolean, selected: Boolean): State<Color> {
// Determine the target color based on the current state
val target = when {
enabled && selected -> selectedColor
enabled && !selected -> unselectedColor
!enabled && selected -> disabledSelectedColor
else -> disabledUnselectedColor
}
// Animate the color if enabled, otherwise return the updated state directly
return if (enabled) {
animateColorAsState(target, tween(durationMillis = RadioAnimationDuration))
} else {
rememberUpdatedState(target)
}
}
}

This class takes four colors as parameters to represent the radio button’s different states and provides a composable function radioColorAsState that returns the appropriate color based on the radio button's current state.

Providing Default Colors

Next, we’ll create an object ConvexRadioButtonDefaults to provide default color values for the radio button.

object ConvexRadioButtonDefaults {
@Composable
fun colors(
selectedColor: Color = MaterialTheme.colorScheme.primary,
unselectedColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
disabledSelectedColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = DisabledAlpha),
disabledUnselectedColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = DisabledAlpha)
): ConvexRadioButtonColors = ConvexRadioButtonColors(selectedColor, unselectedColor, disabledSelectedColor, disabledUnselectedColor)
}

This object provides a composable function colors that returns a ConvexRadioButtonColors instance with default or specified colors. These defaults are based on the current material theme's color scheme.

Defining the Convex Radio Button

Now we can declare the main ConvexRadioButton composable function and go through its parameters.

@Composable
fun ConvexRadioButton(
selected: Boolean,
modifier: Modifier = Modifier,
enabled: Boolean = true,
colors: ConvexRadioButtonColors = ConvexRadioButtonDefaults.colors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
onClick: () -> Unit
)

💎 selected ➜ Indicates whether the radio button is selected or not.

💎 modifier ➜ The Modifier to be applied to this radio button.

💎 enabled ➜ Indicates whether the radio button is enabled for user interaction.

💎 colors ➜ An instance of ConvexRadioButtonColors which defines the colors for the different states of the radio button.

💎 interactionSource ➜ The interaction source responsible for handling user interaction events.

💎 onClick ➜ A lambda function that is called when the radio button is clicked.

The Convex Radio Button Implementation

We are stepping into the most exciting part of the journey: crafting the Convex Radio Button itself.

Preparation

To implement this function, we will need to utilize the innerShadow and convexBorder modifiers. For a detailed explanation, refer to my related articles provided below 👇 or grab the code from👉 InnerShadow Gist, ConvexBorder Gist.

Implementation

Now, let’s dive into the code 👀

@Composable
fun ConvexRadioButton(
/* Parameters... */
) {
// Animate the size of the dot based on the selected state
val dotSize = animateDpAsState(
targetValue = if (selected) RadioButtonDotSize else 0.dp,
animationSpec = tween(durationMillis = RadioAnimationDuration)
)
// Get the appropriate color for the current state
val radioColor = colors.radioColorAsState(enabled, selected)

// Modifier for handling the selection and click interaction
val selectableModifier = Modifier.selectable(
selected = selected,
onClick = onClick,
enabled = enabled,
role = Role.RadioButton,
interactionSource = interactionSource,
indication = rememberRipple(bounded = false, radius = RadioButtonSize)
)

// Modifier for applying the convex border
// This represents the outer circle or the unselected state
val convexBorderModifier = Modifier.convexBorder(
color = radioColor.value,
shape = CircleShape,
strokeWidth = RadioStrokeWidth,
convexStyle = ConvexStyle(RadioShadowBlur, RadioShadowOffset, RadioGlareColor, RadioShadowColor)
)

// Main container for the radio button
Box(
modifier
.minimumInteractiveComponentSize() // Ensure minimum touch target size
.then(selectableModifier) // Add selectable behavior
.size(RadioButtonSize) // Set size of the radio button
.then(convexBorderModifier), // Apply convex border
contentAlignment = Alignment.Center
) {
// Conditionally display the inner dot if the size is greater than zero
if (dotSize.value > 0.dp) {
Box(
modifier = Modifier
.size(dotSize.value)
.background(radioColor.value, CircleShape)
// Create a convex effect by utilizing two inner shadows
.innerShadow(CircleShape, RadioShadowColor, RadioShadowBlur, -RadioShadowOffset, -RadioShadowOffset)
.innerShadow(CircleShape, RadioGlareColor, RadioShadowBlur, RadioShadowOffset, RadioShadowOffset)
)
}
}
}

Congratulations🥳! We’ve successfully built it👏. For the complete code implementation, you can access it on GitHub Gist🧑‍💻. Now, let’s explore how we can put it to use.

Advertisement

Are you learning a foreign language and struggling with new vocabulary? Then, I strongly recommend you check out this words-learning app, which will make your journey easy and convenient!

WordBook

Practical Example 💁

Alright, let’s craft a simple example for choosing a payment option.

First, create an enum class PaymentOption to represent the options:

enum class PaymentOption(val displayName: String) {
CreditCard("Credit Card"),
PayPal("PayPal"),
BankTransfer("Bank Transfer"),
}

Next, we need a state variable to hold the currently selected option:

var selectedOption by remember { mutableStateOf(PaymentOption.CreditCard) }

Finally, put all available options into a column:

Column {
PaymentOption.entries.forEach { option ->
Row(verticalAlignment = Alignment.CenterVertically) {
ConvexRadioButton(
selected = (option == selectedOption),
onClick = { selectedOption = option }
)
Text(option.displayName)
}
}
}

The output:

You might also like 👇

Thank you for reading this article! ❤️ I hope you’ve found it enjoyable and valuable. Feel free to show your appreciation by hitting the clap 👏 if you liked it and follow Kappdev for more exciting articles 😊

Happy coding!

--

--

Kappdev

💡 Curious Explorer 🧭 Kotlin and Compose enthusiast 👨‍💻 Passionate about self-development and growth ❤️‍🔥 Push your boundaries 🚀