I want to add this dot icon over the selected tab icon in the bottom nav bar. I don’t want to use images for this. I am using Jetpack Compose.
That design which is outside the bottom nav bar is required.
How can I achieve this in jetpack composition? Also if I want to fill the icon with custom color how can I accomplish that?
Here is my code:
@Composable
fun BottomBar(modifier: Modifier = Modifier) {
var navNum by remember { mutableStateOf(0) }
Row(
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(topStart = 30.dp, topEnd = 30.dp))
.background(CardColor)
.padding(vertical = 15.dp, horizontal = 15.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (navNum == 0) {
IconButton(onClick = { /*TODO*/ }) {
Icon(
painter = painterResource(id = R.drawable.home_filled),
contentDescription = "home",
tint = SecondaryColor,
modifier = Modifier.size(25.dp),
)
}
} else {
IconButton(onClick = { navNum = 0 }) {
Icon(
painter = painterResource(id = R.drawable.home_light),
contentDescription = "home",
tint = ThinTextColor,
modifier = Modifier.size(25.dp),
)
}
}
Spacer(modifier = Modifier.width(8.dp))
if (navNum == 1) {
IconButton(onClick = { /*TODO*/ }) {
Icon(
painter = painterResource(id = R.drawable.calendar_filled),
contentDescription = "home",
tint = SecondaryColor,
modifier = Modifier.size(25.dp)
)
}
} else {
IconButton(onClick = { navNum = 1 }) {
Icon(
painter = painterResource(id = R.drawable.calendar_light),
contentDescription = "home",
tint = ThinTextColor,
modifier = Modifier.size(25.dp),
)
}
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
if (navNum == 2) {
IconButton(onClick = { /*TODO*/ }) {
Icon(
painter = painterResource(id = R.drawable.message_filled),
contentDescription = "home",
tint = SecondaryColor,
modifier = Modifier.size(25.dp),
)
}
} else {
IconButton(onClick = { navNum = 2 }) {
Icon(
painter = painterResource(id = R.drawable.message_light),
contentDescription = "home",
tint = ThinTextColor,
modifier = Modifier.size(25.dp),
)
}
}
Spacer(modifier = Modifier.width(8.dp))
if (navNum == 3) {
IconButton(onClick = { /*TODO*/ }) {
Icon(
painter = painterResource(id = R.drawable.user_filled),
contentDescription = "home",
tint = SecondaryColor,
modifier = Modifier.size(25.dp),
)
}
} else {
IconButton(onClick = { navNum = 3 }) {
Icon(
painter = painterResource(id = R.drawable.user_light),
contentDescription = "home",
tint = ThinTextColor,
modifier = Modifier.size(25.dp),
)
}
}
}
}
}
To achieve this in compose, you can use NavigationBar
for material 3 or BottomNavigation
for material 2. In my example, i’ll be using material 3 and the NavigationBar
.
To replicate the design with a dot over the selected item, we need to:
Create a custom shape for the bottom navigation item, which includes a curved border.
Use drawWithContent
to add the dot above the selected icon.
First, we need to define a custom shape using a cubic bezier curve. This shape will give the selected tab that curved look:
val CurvedShape = remember {
GenericShape { size, _ ->
val width = size.width
val height = size.height
// Move to the top left corner
moveTo(0f, 0f)
lineTo(width - width / 7f, 0f)
val start = Offset(width - width / 7f, height / 3.5f) // start pos of the curve
val center = Offset(width, height/2f) // center pos of the curve
val end = Offset(width - width / 7f, height - height / 3.5f) // end pos of the curve
val controlDistance = 20 // length of control points of the bezier curve
val c1 = Offset(start.x, start.y + controlDistance) // control points of the bezier curve
val c2 = Offset(center.x, center.y - controlDistance)
val c3 = Offset(center.x, center.y + controlDistance)
val c4 = Offset(end.x, end.y - controlDistance)
lineTo(start.x, start.y)
cubicTo(
x1 = c1.x,
y1 = c1.y,
x2 = c2.x,
y2 = c2.y,
x3 = center.x,
y3 = center.y
)
cubicTo(
x1 = c3.x,
y1 = c3.y,
x2 = c4.x,
y2 = c4.y,
x3 = end.x,
y3 = end.y
)
lineTo(width - width / 7f, height)
lineTo(0f, height)
}
}
Now, you can conditionally apply this CurvedShape
when the item is selected, and use drawWithContent
to draw the dot above the selected icon.
modifier = Modifier
.background(
color = backgroundColor,
shape = if (selectedIndex == 0) CurvedShape else RectangleShape
)
.then(
if (selectedIndex == 0) {
Modifier.drawWithContent {
val radius = dotRadius.toPx()
drawContent()
drawCircle(
color = selectedIconColor,
radius = radius,
center = Offset(size.width - 3 * radius / 2, size.height / 2)
)
}
} else {
Modifier
}
)
You can easily change the colors of the selected and unselected icon using this:
colors = NavigationBarItemDefaults.colors().copy(
selectedIconColor = selectedIconColor,
unselectedIconColor = unselectedIconColor,
selectedIndicatorColor = backgroundColor
)
Here’s the complete example:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Scaffold(
modifier = Modifier.fillMaxSize(),
bottomBar = { BottomBar() },
) { innerPadding ->
// Your content goes here
}
}
}
}
@Composable
fun BottomBar(modifier: Modifier = Modifier) {
var selectedIndex by remember { mutableIntStateOf(0) }
val CurvedShape = remember {
GenericShape { size, _ ->
val width = size.width
val height = size.height
// Move to the top left corner
moveTo(0f, 0f)
lineTo(width - width / 7f, 0f)
val start = Offset(width - width / 7f, height / 3.5f) // start pos of the curve
val center = Offset(width, height/2f) // center pos of the curve
val end = Offset(width - width / 7f, height - height / 3.5f) // end pos of the curve
val controlDistance = 20 // length of control points of the bezier curve
val c1 = Offset(start.x, start.y + controlDistance) // control points of the bezier curve
val c2 = Offset(center.x, center.y - controlDistance)
val c3 = Offset(center.x, center.y + controlDistance)
val c4 = Offset(end.x, end.y - controlDistance)
lineTo(start.x, start.y)
cubicTo(
x1 = c1.x,
y1 = c1.y,
x2 = c2.x,
y2 = c2.y,
x3 = center.x,
y3 = center.y
)
cubicTo(
x1 = c3.x,
y1 = c3.y,
x2 = c4.x,
y2 = c4.y,
x3 = end.x,
y3 = end.y
)
lineTo(width - width / 7f, height)
lineTo(0f, height)
}
}
val backgroundColor = Color.LightGray
val selectedIconColor = Color.Red
val unselectedIconColor = Color.Black
val dotRadius = 5.dp
NavigationBar(
modifier = modifier
.fillMaxWidth(),
tonalElevation = 2.dp
) {
NavigationBarItem(
icon = {
Icon(
imageVector = Icons.Filled.Home,
contentDescription = null,
)
},
label = null,
selected = selectedIndex == 0,
onClick = {
selectedIndex = 0
},
modifier = Modifier
.background(
color = backgroundColor,
shape = if (selectedIndex == 0) CurvedShape else RectangleShape
)
.then(
if (selectedIndex == 0) {
Modifier.drawWithContent {
val radius = dotRadius.toPx()
drawContent()
drawCircle(
color = selectedIconColor,
radius = radius,
center = Offset(size.width - 3 * radius / 2, size.height / 2)
)
}
} else {
Modifier
}
),
colors = NavigationBarItemDefaults.colors().copy(
selectedIconColor = selectedIconColor,
unselectedIconColor = unselectedIconColor,
selectedIndicatorColor = backgroundColor
)
)
NavigationBarItem(
icon = {
Icon(
imageVector = Icons.Filled.Call,
contentDescription = null,
)
},
label = null,
selected = selectedIndex == 1,
onClick = {
selectedIndex = 1
},
modifier = Modifier
.background(
color = backgroundColor,
shape = if (selectedIndex == 1) CurvedShape else RectangleShape
)
.then(
if (selectedIndex == 1) {
Modifier.drawWithContent {
val radius = dotRadius.toPx()
drawContent()
drawCircle(
color = selectedIconColor,
radius = radius,
center = Offset(size.width - 3 * radius / 2, size.height / 2)
)
}
} else {
Modifier
}
),
colors = NavigationBarItemDefaults.colors().copy(
selectedIconColor = selectedIconColor,
unselectedIconColor = unselectedIconColor,
selectedIndicatorColor = backgroundColor
)
)
Spacer(modifier = Modifier.weight(1f))
NavigationBarItem(
icon = {
Icon(
imageVector = Icons.Filled.ShoppingCart,
contentDescription = null,
)
},
label = null,
selected = selectedIndex == 2,
onClick = {
selectedIndex = 2
},
modifier = Modifier
.background(
color = backgroundColor,
shape = if (selectedIndex == 2) CurvedShape else RectangleShape
)
.then(
if (selectedIndex == 2) {
Modifier.drawWithContent {
val radius = dotRadius.toPx()
drawContent()
drawCircle(
color = selectedIconColor,
radius = radius,
center = Offset(size.width - 3 * radius / 2, size.height / 2)
)
}
} else {
Modifier
}
),
colors = NavigationBarItemDefaults.colors().copy(
selectedIconColor = selectedIconColor,
unselectedIconColor = unselectedIconColor,
selectedIndicatorColor = backgroundColor
)
)
NavigationBarItem(
icon = {
Icon(
imageVector = Icons.Filled.Menu,
contentDescription = null,
)
},
label = null,
selected = selectedIndex == 3,
onClick = {
selectedIndex = 3
},
modifier = Modifier
.background(
color = backgroundColor,
shape = if (selectedIndex == 3) CurvedShape else RectangleShape
)
.then(
if (selectedIndex == 3) {
Modifier.drawWithContent {
val radius = dotRadius.toPx()
drawContent()
drawCircle(
color = selectedIconColor,
radius = radius,
center = Offset(size.width - 3 * radius / 2, size.height / 2)
)
}
} else {
Modifier
}
),
colors = NavigationBarItemDefaults.colors().copy(
selectedIconColor = selectedIconColor,
unselectedIconColor = unselectedIconColor,
selectedIndicatorColor = backgroundColor
)
)
}
}
Demo: