NavigationView custom menu item

Hello,

I am trying to create a Drawer with a header and some menu items in it. I have created the header as a separate layout, and put it into the NavigationView like this:

<com.google.android.material.navigation.NavigationView
    android:id="@+id/navView"
    android:layout_width="312dp"
    android:layout_height="match_parent"
    android:layout_gravity="start"
    app:headerLayout="@layout/nav_header"
    app:menu="@menu/menu_drawer">

The header appears correctly. The problem is that the menu is not customizable, by default. Therefore, I put the app:actionLayout="@layout/nav_menu_item" attribute for each item in the menu, like this:

<?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">
<group>
    <item android:id="@+id/navDocumentScanItem"
        app:actionLayout="@layout/nav_menu_item"
        android:title="@null"/>
    <item android:id="@+id/navReportItem"
        app:actionLayout="@layout/nav_menu_item"
        android:title="@null"/>
    <item android:id="@+id/navHallOfFameItem"
        app:actionLayout="@layout/nav_menu_item"
        android:title="@null"/>
</group>

In my nav_menu_item I have the layout that I want to use for each item in my drawer menu:

<?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"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/navItem"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="56dp">

<ImageView
    android:id="@+id/navItemIcon"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    android:layout_marginTop="6dp"
    android:layout_marginStart="8dp"
    android:layout_width="44dp"
    android:layout_height="44dp"
    tools:src="@drawable/nav_icon_qr_scan"/>

<TextView
    android:id="@+id/navItemText"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toEndOf="@id/navItemIcon"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    style="@style/menu_item_text_style"
    tools:text="Document Scanning"/>

</androidx.constraintlayout.widget.ConstraintLayout>

This appears reasonably well (if I hardcode it), but the problem is that I can’t access the views in this custom layout. How do I get a handle of the “navItemIcon” and “navItemText” so that I can set the icon and text programatically, depending on which item position is for that particular item?

Thanks!

You could try:

  • Calling getMenu() on the NavigationView to get your inflated Menu resource
  • Calling findItem() on the Menu to get the MenuItem associated with a given item ID (e.g., R.id.navDocumentScanItem)
  • Calling getActionView() on the MenuItem to get the inflated nav_menu_item view hierarchy

I have not tried this with NavigationView, but if I were in your shoes, it is what I would try.

1 Like

I have experimented with these in onCreate() but they return null, since they haven’t been created yet. What would be the right way to get them when they’re already created? In the past I’ve been using ViewTreeObserver or something like that - is that the correct way or is there another one?

What specifically are you referring to as “they”? IOW, where is the null showing up?

Beats me. I would have expected them to be ready after the view has been inflated.

Right now, your guess is as good as (or better than) mine.

I fixed it with ViewTreeObserver:

//handle the navigation drawer item
    val navViewTreeObserver = navView.viewTreeObserver
    navViewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
        override fun onGlobalLayout() {
            navView.viewTreeObserver.removeOnGlobalLayoutListener(this)
            val menu = navView.menu
            //get the menu items of the navigation view
            for (i in 0 until menu.size()){
                val menuItem = menu.getItem(i)
                //get the relevant views that we want to change programmatically
                val menuItemText = menuItem.actionView.findViewById<TextView>(R.id.navItemText)
                val menuItemIcon = menuItem.actionView.findViewById<ImageView>(R.id.navItemIcon)
                //change the menu item's text and icon depending on the position in the drawer
                when (i){
                    0 -> {
                        menuItemText.text = "QR Scan"
                        menuItemIcon.setImageResource(R.drawable.nav_icon_qr_scan)
                    }
                    1 -> {
                        menuItemText.text = "Activity Reports"
                        menuItemIcon.setImageResource(R.drawable.nav_icon_report_inactive)
                    }
                    2 -> {
                        menuItemText.text = "Hall of fame"
                        menuItemIcon.setImageResource(R.drawable.nav_icon_hall_of_fame_inactive)
                    }
                }
            }
        }
    })

Glad you got it working! Here are some ways you might simplify that:

  • Use findItem() on the Menu rather than iteration
  • Use doOnNextLayout() instead of rolling a full ViewTreeObserver (this is in one of the -ktx libraries)