Today I’ll show you how to create a curved bottom bar in Jetpack Compose.
To start let’s create a class that defines a route.
data class Screen(
val route: String,
@DrawableRes val icon: Int,
@DrawableRes val selectedIcon: Int,
)
I’m using icon
and selectedIcon
because in my implementation they change but you can change the tint instead of whatever you want.
Now let’s define the bottom bar.
@Composable
fun BottomMenuBar(
screens: List<Screen>,
currentScreen: Screen?,
onNavigateTo: (Screen) -> Unit,
)
screens
is the routes you want to show in the bottom bar. You need to specify 5 screens for this bottom bar to work.
currentScreen
is the currently selected screen so we can change the icon.
onNavigateTo
is called when the icon for a screen is tapped in the bottom bar.
Before we continue let’s build the rounded shape that’s used for the background.
private fun menuBarShape() = GenericShape { size, _ ->
reset()
moveTo(0f, 0f)
val width = 150f
val height = 90f
val point1 = 75f
val point2 = 85f
lineTo(size.width / 2 - width, 0f)
cubicTo(
size.width / 2 - point1, 0f,
size.width / 2 - point2, height,
size.width / 2, height
)
cubicTo(
size.width / 2 + point2, height,
size.width / 2 + point1, 0f,
size.width / 2 + width, 0f
)
lineTo(size.width / 2 + width, 0f)
lineTo(size.width, 0f)
lineTo(size.width, size.height)
lineTo(0f, size.height)
close()
}
We use cubic bezier curves to get that rounded shape. If you want to change the rounded part you can change width
, height
, point1
and point2
. By changing these variables the curvature will change.
Now let’s define the preview so we can see the bottom bar as we change it.
@Preview(showSystemUi = true)
@Composable
private fun Preview() {
var currentScreen by remember { mutableStateOf<Screen?>(null) }
Box(
contentAlignment = Alignment.BottomCenter,
modifier = Modifier.fillMaxSize()
) {
BottomMenuBar(
screens = listOf(
Screen(
route = "home",
icon = R.drawable.outline_home_24,
selectedIcon = R.drawable.baseline_home_24,
),
Screen(
route = "products",
icon = R.drawable.outline_collections_24,
selectedIcon = R.drawable.baseline_collections_24,
),
Screen(
route = "cart",
icon = R.drawable.outline_shopping_cart_24,
selectedIcon = R.drawable.baseline_shopping_cart_24,
),
Screen(
route = "profile",
icon = R.drawable.outline_person_24,
selectedIcon = R.drawable.baseline_person_24,
),
Screen(
route = "chat",
icon = R.drawable.outline_chat_24,
selectedIcon = R.drawable.baseline_chat_24,
),
),
currentScreen = currentScreen,
onNavigateTo = { currentScreen = it },
)
}
}
The currentScreen
variable in the preview is used so we can play with the bottom bar and see it changing.
Now let’s actually implement the bottom bar.
@Composable
fun BottomMenuBar(
screens: List<Screen>,
currentScreen: Screen?,
onNavigateTo: (Screen) -> Unit,
) {
val backgroundShape = remember { menuBarShape() }
Box {
Box(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.background(Color.White, backgroundShape)
.align(Alignment.BottomCenter)
)
Column(
modifier = Modifier
.align(Alignment.TopCenter)
) {
FloatingActionButton(
shape = RoundedCornerShape(50),
containerColor = Color.White,
contentColor = Color.Gray,
onClick = {},
modifier = Modifier.clip(RoundedCornerShape(50))
) {
Row(
modifier = Modifier.size(64.dp)
) {
BottomBarItem(screens[2], currentScreen, onNavigateTo)
}
}
Spacer(modifier = Modifier.height(30.dp))
}
Row(
modifier = Modifier
.height(56.dp)
.align(Alignment.BottomCenter)
) {
BottomBarItem(screens[0], currentScreen, onNavigateTo)
BottomBarItem(screens[1], currentScreen, onNavigateTo)
Spacer(modifier = Modifier.width(72.dp))
BottomBarItem(screens[3], currentScreen, onNavigateTo)
BottomBarItem(screens[4], currentScreen, onNavigateTo)
}
}
}
We also need to implement a component for the menu items,
@Composable
private fun RowScope.BottomBarItem(
screen: Screen,
currentScreen: Screen?,
onNavigateTo: (Screen) -> Unit,
) {
val selected = currentScreen?.route == screen.route
Box(
Modifier
.selectable(
selected = selected,
onClick = { onNavigateTo(screen) },
role = Role.Tab,
interactionSource = remember { MutableInteractionSource() },
indication = remember { ripple(radius = 32.dp) }
)
.fillMaxHeight()
.weight(1f),
contentAlignment = Alignment.Center
) {
BadgedBox(
badge = {},
content = {
Image(
painter = painterResource(
id = when {
selected -> screen.selectedIcon
else -> screen.icon
}
),
contentDescription = null
)
},
)
}
}
Well, that’s it. You have a working curved bottom bar.
If you have any comments or suggestions, please reach out to me on X.
Photo by CHUTTERSNAP on Unsplash