Add a custom badge to your BottomNavigationView tabs, because sometimes you need a square instead of a circle
Bottom Navigation Bar is probably one of the most common navigation patterns around and the view at Material components is an incredible one. It has lot of customisation options, but badges has always to be a circle. If circles does not match your app’s brand identity or you just want to add crazy stuff (Lottie animation anyone?) in your bottom tabs, there is one pretty decent way to do it.
Main goals
- Add a squared badge (well, or anything you’d like to put it there) to the bottom tabs. The badge should be different when the tab is selected or deselected.
- Try to keep BottomBar as standard as possible
Main approach
We’ll be adding a new method setBadge() to BottomNavigationBar wich will allow setting a badge. There will be some logic to inflate extra views in the tabs that serve as our custom badge.
OK, show me the code
First we will create a xml which will be our badge.
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<TextView
android:id="@+id/menuItemBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:background="#000"
android:backgroundTint="@color/bottom_bar_badge_background"
android:textColor="?colorPrimary"
android:textStyle="bold"
android:duplicateParentState="true"
android:gravity="center"
android:layout_gravity="center"
android:includeFontPadding="false"
android:minWidth="16dp"
android:paddingBottom="1dp"
android:paddingEnd="2dp"
android:paddingStart="2dp"
android:paddingTop="2dp"
tools:text="11"
/>
</merge>
To get different background color depending on if the tab is selected or not, we used the xml att android:duplicateParentState=”true”. This will make that, when the parent state change to selected (well, state is checked to be accurate), this change will propagate to the children.
Then, we define a color with depends on the state as below:
<selector
<item android:color="@color/teal_200" android:state_checked="true"/>
<item android:color="#aaa"/>
</selector>
We have done all the xml work that we need, now let’s check the code to inflate it, which comes in form of extension function:
fun BottomNavigationView.setBadge(tabResId: Int, badgeValue: Int) {
getOrCreateBadge(this, tabResId)?.let { badge ->
badge.visibility = if (badgeValue > 0) {
badge.text = "$badgeValue"
View.VISIBLE
} else {
View.GONE
}
}
}
private fun getOrCreateBadge(bottomBar: View, tabResId: Int): TextView? {
val parentView = bottomBar.findViewById<ViewGroup>(tabResId)
return parentView?.let {
var badge = parentView.findViewById<TextView>(R.id.menuItemBadge)
if (badge == null) {
LayoutInflater.from(parentView.context).inflate(R.layout.bottom_nav_badge, parentView, true)
badge = parentView.findViewById(R.id.menuItemBadge)
}
badge
}
}
setBadge() receives the id of the tab where we wan’t a badge with a value. It calls getOrCreateBadge() which inflates a new badge if none can be found and sets the value to the one given. The trick here is that we know the the parent id is R.id.menuItemBadge, which is defined inside BottomNavigationView. This is an internal detail of that particular view and we have no control on if that’s gonna change in the future or not, breaking this. So far it’s been stable for a long time.
Now we just need to call our extension method and set eh value we want.
val navView: BottomNavigationView = findViewById(R.id.nav_view)
navView.setBadge(R.id.navigation_dashboard, 99)
R.id.navigation_dashboard is the one defined in menu.xml used to configure BottomNavigationTab.
And thats all, you can check a the working example here: