7

I want to set indicator on bottomNavigationView.You can see that orange line at bottom side
How to set indicator?
enter image description here

I didn't find any example for indicator.

 <android.support.design.widget.BottomNavigationView
            android:id="@+id/bottom_navigation_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:background="@drawable/bottom_navigation_bg"
            app:labelVisibilityMode="unlabeled"
            app:menu="@menu/bottom_menu_main"/>
Sanjayrajsinh
  • 15,014
  • 7
  • 73
  • 78
Khashchuluun
  • 169
  • 1
  • 13
  • @NileshRathod He/She wants indicator (bottom line as TabLayout), not badge. – Pratik Butani Feb 18 '19 at 06:44
  • I didn't ask about badge. I asked about indicator. – Khashchuluun Feb 18 '19 at 06:44
  • @PratikButani sorry my bad – AskNilesh Feb 18 '19 at 06:46
  • @Kvoid I think it is bad practice, As you will find some library or Google recommended codes, It will gives you to change icon color or text color to identify current selected item. Even If you want to do same then you can create custom view for that. Thanks. – Pratik Butani Feb 18 '19 at 06:51
  • 3
    I don't understand why this question's closed and it's pointed to an unrelated question. Someone who has enough reputation must reopen this question. – twenk11k Apr 29 '20 at 08:25

4 Answers4

14

My previous answer offers a lot of flexibility and animates nicely between selected states but involves a relatively large amount of code. A simpler solution that comes without animation, would be to use the itemBackground attribute of the BottomNavigationView:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_nav"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:itemBackground="@drawable/bottom_nav_tab_background"
        android:layout_gravity="bottom"
        app:menu="@menu/menu_bottom_nav" />

</LinearLayout>

bottom_nav_tab_background

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_checked="true">
        <layer-list>
            <item android:gravity="bottom">
                <shape android:shape="rectangle">
                    <size android:height="4dp" />
                    <solid android:color="?attr/colorPrimary" />
                </shape>
            </item>
        </layer-list>
    </item>
</selector>
HBG
  • 1,731
  • 2
  • 23
  • 35
  • Instead of app:itemBackground="@drawable/bottom_nav_tab_background" my code worked with app:itemIconTint="@drawable/bottom_nav_tab_background" i dont know why – Haseeb Hassan Asif Mar 23 '21 at 12:44
4

Here's something I whipped up to add an indicator and move it around.

class IndicatorBottomNavigationView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = R.attr.bottomNavigationStyle
) : BottomNavigationView(context, attrs, defStyleAttr) {

    private val indicator: View = View(context).apply {
        layoutParams = LayoutParams(0, 5)
        setBackgroundColor(ContextCompat.getColor(context, R.color.carecredit_green))
    }

    init {
        addView(indicator)
    }

    var animateIndicator = true

    override fun setOnNavigationItemSelectedListener(listener: OnNavigationItemSelectedListener?) {
        OnNavigationItemSelectedListener { selectedItem ->

            menu
                .children
                .first { item ->
                    item.itemId == selectedItem.itemId
                }
                .itemId
                .let {
                    findViewById<View>(it)
                }
                .let { view ->
                    this.post {
                        indicator.layoutParams = LayoutParams(view.width, 5)

                        if (animateIndicator) {
                            indicator
                                .animate()
                                .x(view.x)
                                .start()
                        } else {
                            indicator.x = view.x
                        }

                        indicator.y = view.y
                    }
                }

            listener?.onNavigationItemSelected(selectedItem) ?: false
        }.let {
            super.setOnNavigationItemSelectedListener(it)
        }
    }
}
Carter Hudson
  • 1,176
  • 1
  • 11
  • 23
0

To get this working you need to extend the BottomNavigationView to be able to have multiple listeners. The original implementation only allows for one but you need another to be able to tell the indicator when to move.

class CustomBottomNavigationView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : BottomNavigationView(context, attrs, defStyleAttr), OnNavigationItemSelectedListener {

    private val onNavigationItemSelectedListeners =
        mutableListOf<OnNavigationItemSelectedListener>()

    init {
        super.setOnNavigationItemSelectedListener(this)

        itemIconTintList = null
    }

    override fun setOnNavigationItemSelectedListener(listener: OnNavigationItemSelectedListener?) {
        if (listener != null) addOnNavigationItemSelectedListener(listener)
    }

    fun addOnNavigationItemSelectedListener(listener: OnNavigationItemSelectedListener) {
        onNavigationItemSelectedListeners.add(listener)
    }

    fun addOnNavigationItemSelectedListener(listener: (Int) -> Unit) {
        addOnNavigationItemSelectedListener(OnNavigationItemSelectedListener {
            for (i in 0 until menu.size()) if (menu.getItem(i) == it) listener(i)
            false
        })
    }

    override fun onNavigationItemSelected(item: MenuItem) = onNavigationItemSelectedListeners
        .map { it.onNavigationItemSelected(item) }
        .fold(false) { acc, it -> acc || it }
}

Then create an indicator item:

class BottomNavigationViewIndicator @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    val size = 8f
    private val targetId: Int
    private var target: BottomNavigationMenuView? = null

    private var rect = Rect()
    //    val drawPaint = Paint()
    private val backgroundDrawable: Drawable

    private var index = 0
    private var animator: AnimatorSet? = null

    init {
        if (attrs == null) {
            targetId = NO_ID
            backgroundDrawable = ColorDrawable(Color.TRANSPARENT)
        } else {
            with(context.obtainStyledAttributes(attrs, BottomNavigationViewIndicator)) {
                targetId =
                    getResourceId(BottomNavigationViewIndicator_targetBottomNavigation, NO_ID)
                val clippableId =
                    getResourceId(BottomNavigationViewIndicator_clippableBackground, NO_ID)
                backgroundDrawable = if (clippableId != NO_ID) {
                    androidx.appcompat.content.res.AppCompatResources.getDrawable(
                        context,
                        clippableId
                    ) ?: ColorDrawable(Color.TRANSPARENT)
                } else {
                    ColorDrawable(
                        getColor(
                            BottomNavigationViewIndicator_clippableBackground,
                            Color.TRANSPARENT
                        )
                    )
                }
                recycle()
            }
        }
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        if (targetId == NO_ID) return attachedError("invalid target id $targetId, did you set the app:targetBottomNavigation attribute?")
        val parentView =
            parent as? View ?: return attachedError("Impossible to find the view using $parent")
        val child = parentView.findViewById<View?>(targetId)
        if (child !is CustomBottomNavigationView) return attachedError("Invalid view $child, the app:targetBottomNavigation has to be n ListenableBottomNavigationView")
        for (i in 0 until child.childCount) {
            val subView = child.getChildAt(i)
            if (subView is BottomNavigationMenuView) target = subView
        }
        elevation = child.elevation
        child.addOnNavigationItemSelectedListener { updateRectByIndex(it, true) }
        post { updateRectByIndex(index, false) }
    }

    private fun attachedError(message: String) {
        Log.e("BNVIndicator", message)
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        target = null
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.clipRect(rect)

//        canvas.drawCircle(size,size,size, drawPaint)
        backgroundDrawable.draw(canvas)
    }

    private fun updateRectByIndex(index: Int, animated: Boolean) {
        this.index = index
        target?.apply {
            if (childCount < 1 || index >= childCount) return
            val reference = getChildAt(index)

            val start = reference.left + left
            val end = reference.right + left

            backgroundDrawable.setBounds(left, top, right, bottom)
            val newRect = Rect(start, 0, end, height)
            if (animated) startUpdateRectAnimation(newRect) else updateRect(newRect)
        }
    }

    private fun startUpdateRectAnimation(rect: Rect) {
        animator?.cancel()
        animator = AnimatorSet().also {
            it.playTogether(
                ofInt(this, "rectLeft", this.rect.left, rect.left),
                ofInt(this, "rectRight", this.rect.right, rect.right),
                ofInt(this, "rectTop", this.rect.top, rect.top),
                ofInt(this, "rectBottom", this.rect.bottom, rect.bottom)
            )
            it.interpolator = FastOutSlowInInterpolator()
            it.duration = resources.getInteger(android.R.integer.config_shortAnimTime).toLong()
            it.start()
        }
    }

    private fun updateRect(rect: Rect) {
        this.rect = rect
        postInvalidate()
    }

    @Keep
    fun setRectLeft(left: Int) = updateRect(rect.apply { this.left = left })

    @Keep
    fun setRectRight(right: Int) = updateRect(rect.apply { this.right = right })

    @Keep
    fun setRectTop(top: Int) = updateRect(rect.apply { this.top = top })

    @Keep
    fun setRectBottom(bottom: Int) = updateRect(rect.apply { this.bottom = bottom })

}

Put some attributes into attrs.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="BottomNavigationViewIndicator">
        <attr name="targetBottomNavigation" format="reference"/>
        <attr name="clippableBackground" format="reference|color"/>
    </declare-styleable>
</resources>

Create a background drawable that'll be clipped as the indicator moves: my_orange_background.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="360"
    android:viewportHeight="4">
    <path
        android:fillType="nonZero"
        android:pathData="M0,0h359.899v3.933H0z">
        <aapt:attr name="android:fillColor">
            <gradient
                android:endX="360"
                android:endY="2"
                android:startX="0"
                android:startY="2"
                android:type="linear">
                <item
                    android:color="@color/orange"
                    android:offset="0" />
                <item
                    android:color="@color/orange"
                    android:offset="0.55" />
            </gradient>
        </aapt:attr>
    </path>
</vector>

Create your activity/ fragment layout:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintBottom_toTopOf="@+id/bottom_nav"
        app:layout_constraintTop_toTopOf="parent" />

    <com.example.app.view.CustomBottomNavigationView
        android:id="@+id/bottom_nav"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="0dp"
        android:layout_marginEnd="0dp"
        android:background="?attr/colorSurface"
        app:labelVisibilityMode="unlabeled"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:menu="@menu/menu_bottom_nav" />

    <com.example.app.view.BottomNavigationViewIndicator
        android:layout_width="0dp"
        android:layout_height="4dp"
        app:clippableBackground="@drawable/my_orange_background"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="@+id/bottom_nav"
        app:targetBottomNavigation="@+id/bottom_nav" />

</androidx.constraintlayout.widget.ConstraintLayout>

Sources: Medium article GitHub project

HBG
  • 1,731
  • 2
  • 23
  • 35
-3

It is bad practice but if you wish to do like this you can take Tab layout.

here is the code

<android.support.design.widget.CoordinatorLayout 
 xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|enterAlways"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

    </android.support.design.widget.AppBarLayout>



        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <android.support.v4.view.ViewPager
                android:id="@+id/viewpager"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_above="@+id/tabs"
                />


            <android.support.design.widget.TabLayout
                android:id="@+id/tabs"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_alignParentBottom="true"
                app:tabBackground="@color/colorPrimary"
                app:tabIndicatorColor="@android:color/white"
                app:tabMode="fixed"
                app:tabGravity="fill"/>


        </RelativeLayout>


     </android.support.design.widget.CoordinatorLayout>
Vasudev Vyas
  • 726
  • 1
  • 10
  • 28