Reputation: 666
I'm using a BottomNavigationView
in my app. Right now my navigation view looks like this:
but I want it to be with underlined selected item, like this:
Are there any ways to do this with some standard attributes?
Upvotes: 4
Views: 3526
Reputation: 40810
This could be a simpler & better solution than my other answer; it also could have a variety of capabilities like the thickness of the width/height, corners, shapes, padding..etc stuff of drawable capabilities.
You can create a selector (only with a checked state) that has a gravity set to the bottom
:
item_background.xml:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true">
<layer-list>
<item android:gravity="bottom|center_horizontal">
<shape android:shape="rectangle">
<size android:width="100dp" android:height="5dp" />
<solid android:color="#03DAC5" />
<corners android:bottomLeftRadius="3dp" android:bottomRightRadius="3dp" />
</shape>
</item>
</layer-list>
</item>
</selector>
Set this to app:itemBackground
:
<com.google.android.material.bottomnavigation.BottomNavigationView
....
app:itemBackground="@drawable/item_background"
Upvotes: 2
Reputation: 71
I know I'm late to the party, but for the next generations - this is a solution with more control + animation:) using constraint layout.
the example is for 4 items, adjust the numbers.
first, create a view with the desired characteristics in the (constraint) layout that contains the BottomNavigationView. set app:layout_constraintWidth_percent
to 1/number of items
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/mainTabBottomNavigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@android:color/white"
android:nestedScrollingEnabled="true"
app:elevation="16dp"
app:itemIconTint="@drawable/nav_account_item"
app:labelVisibilityMode="unlabeled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/main_bottom_navigation"
/>
<View
android:id="@+id/underline"
android:layout_width="0dp"
android:layout_height="3dp"
android:background="@color/underlineColor"
android:elevation="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintWidth_percent="0.25" />
</androidx.constraintlayout.widget.ConstraintLayout>
Then, use this function inside OnNavigationItemSelectedListener
:
private fun underlineSelectedItem(view: View, itemId: Int) {
val constraintLayout: ConstraintLayout = view as ConstraintLayout
TransitionManager.beginDelayedTransition(constraintLayout)
val constraintSet = ConstraintSet()
constraintSet.clone(constraintLayout)
constraintSet.setHorizontalBias(
R.id.underline,
getItemPosition(itemId) * 0.33f
)
constraintSet.applyTo(constraintLayout)
}
complete code (inside a fragment):
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val navController = Navigation.findNavController(
requireActivity(),
R.id.mainNavigationFragment
)
mainTabBottomNavigation.setupWithNavController(navController)
underlineSelectedItem(view, R.id.bottomNavFragmentHome) //select first item
mainTabBottomNavigation.setOnNavigationItemSelectedListener { item ->
underlineSelectedItem(view, item.itemId)
true
}
}
private fun underlineSelectedItem(view: View, itemId: Int) {
val constraintLayout: ConstraintLayout = view as ConstraintLayout
TransitionManager.beginDelayedTransition(constraintLayout)
val constraintSet = ConstraintSet()
constraintSet.clone(constraintLayout)
constraintSet.setHorizontalBias(
R.id.underline,
getItemPosition(itemId) * 0.33f
)
constraintSet.applyTo(constraintLayout)
}
private fun getItemPosition(itemId: Int): Int {
return when (itemId) {
R.id.bottomNavFragmentHome -> 0
R.id.bottomNavFragmentMyAccount -> 1
R.id.bottomNavFragmentCoupon -> 2
R.id.bottomNavFragmentSettings -> 3
else -> 0
}
}
Notice that this implementation overrides the navigation functionality.
In order to maintain this functionality, you'll need to use NavigationUI.onNavDestinationSelected(item, navController)
at the end of the transition animation.
complete code:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val navController = Navigation.findNavController(
requireActivity(),
R.id.mainNavigationFragment
)
mainTabBottomNavigation.setupWithNavController(navController)
underlineSelectedItem(view, R.id.bottomNavFragmentHome, null, null, null)
mainTabBottomNavigation.setOnNavigationItemSelectedListener { item ->
underlineSelectedItem(view, item.itemId, item, navController) { item1, navController1 ->
safeLet(item1, navController1) { a, b->
NavigationUI.onNavDestinationSelected(a, b)
}
}
true
}
}
private fun underlineSelectedItem(
view: View,
itemId: Int,
item: MenuItem?,
navController: NavController?,
onAnimationEnd: ((item: MenuItem?, navController: NavController?) -> Unit)?
) {
val constraintLayout: ConstraintLayout = view as ConstraintLayout
val transition: Transition = ChangeBounds()
transition.addListener(object : Transition.TransitionListener {
override fun onTransitionStart(transition: Transition?) {
}
override fun onTransitionEnd(transition: Transition?) {
onAnimationEnd?.invoke(item, navController)
}
override fun onTransitionCancel(transition: Transition?) {
}
override fun onTransitionPause(transition: Transition?) {
}
override fun onTransitionResume(transition: Transition?) {
}
})
TransitionManager.beginDelayedTransition(constraintLayout, transition)
val constraintSet = ConstraintSet()
constraintSet.clone(constraintLayout)
constraintSet.setHorizontalBias(
R.id.underline,
getItemPosition(itemId) * 0.33f
)
constraintSet.applyTo(constraintLayout)
}
private fun getItemPosition(itemId: Int): Int {
return when (itemId) {
R.id.bottomNavFragmentHome -> 0
R.id.bottomNavFragmentMyAccount -> 1
R.id.bottomNavFragmentCoupon -> 2
R.id.bottomNavFragmentSettings -> 3
else -> 0
}
}
(safeLet
is a Kotlin helper function for checking two variables nullabilty:
fun <T1 : Any, T2 : Any, R : Any> safeLet(p1: T1?, p2: T2?, block: (T1, T2) -> R?): R? {
return if (p1 != null && p2 != null) block(p1, p2) else null
}
)
Upvotes: 3
Reputation: 40810
You can do that using a SpannableString
with UnderlineSpan
to the item title when this item is selected by the user by setting OnNavigationItemSelectedListener
listener to the BottomNavigationView
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
BottomNavigationView bottomNavigationView = (BottomNavigationView)
findViewById(R.id.bottom_navigation);
underlineMenuItem(bottomNavigationView.getMenu().getItem(0)); // underline the default selected item when the activity is launched
bottomNavigationView.setOnNavigationItemSelectedListener(
new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
removeItemsUnderline(bottomNavigationView); // remove underline from all items
underlineMenuItem(item); // underline selected item
switch (item.getItemId()) {
// handle item clicks
}
return false;
}
});
}
private void removeItemsUnderline(BottomNavigationView bottomNavigationView) {
for (int i = 0; i < bottomNavigationView.getMenu().size(); i++) {
MenuItem item = bottomNavigationView.getMenu().getItem(i);
item.setTitle(item.getTitle().toString());
}
}
private void underlineMenuItem(MenuItem item) {
SpannableString content = new SpannableString(item.getTitle());
content.setSpan(new UnderlineSpan(), 0, content.length(), 0);
item.setTitle(content);
}
This works exactly if you're using text based items, but in your case you're just using icons in your menu, and to resolve this issue; you have to utilize the android:title
of menu items in menu.xml with white spaces as follows
bottom_nav_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_favorites"
android:enabled="true"
android:icon="@drawable/ic_favorite_white_24dp"
android:title="@string/text_spaces"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_schedules"
android:enabled="true"
android:icon="@drawable/ic_access_time_white_24dp"
android:title="@string/text_spaces"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_music"
android:enabled="true"
android:icon="@drawable/ic_audiotrack_white_24dp"
android:title="@string/text_spaces"
app:showAsAction="ifRoom" />
</menu>
And use  
in your text as many times as you need spaces which will reflect on the length of the the line under each item
strings.xml
<resources>
...
<string name="text_spaces">          </string>
This is a preview
hope this solves your issue, and happy for any queries.
Upvotes: 4