android BottomNavigationView underline item

I'm using a BottomNavigationView in my app. Right now my navigation view looks like this:

current state

but I want it to be with underlined selected item, like this:

desired state

Are there any ways to do this with some standard attributes?

Upvotes: 4

Views: 3526

Answers (3)

Zain
Zain

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

shanih
shanih

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
}

)

final result: final result gif

Upvotes: 3

Zain
Zain

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 &#160; 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">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</string>

This is a preview

hope this solves your issue, and happy for any queries.

Upvotes: 4

Related Questions