Search code examples
androidandroid-layoutnavigationviewandroid-navigationview

Override height of a single MenuItem in a NavigationView


I'd like to increase the height of a single MenuItem inside a NavigationView.

I am trying to put a Google MapFragment inside the item's actionLayout, as shown below, but I want the height of the map to be larger.

Any ideas?

Note:

  • I want to utilise the standard menu layout of the NavigationView (I'd prefer not to have to implement a whole custom menu layout as using this answer).
  • I don't want to change the height of all items like this answer, only the map item.

Screenshot showing map inside NavigationView MenuItem

activity_main_drawer.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:showIn="navigation_view">

    ...

    <item
        android:id="@+id/nav_map"
        app:showAsAction="always"
        android:title="@string/nav_map"
        app:actionLayout="@layout/fragment_map" />

    ...

</menu>

fragment_map.xml

Adjusting android:layout_height here has no effect.

<?xml version="1.0" encoding="utf-8"?>
<fragment
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:map="http://schemas.android.com/apk/res-auto"
    android:name="com.google.android.gms.maps.SupportMapFragment"
    android:id="@+id/nav_map_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    map:mapType="normal"
    map:liteMode="true"/>

In Activity.onCreate(...) in MainActivity.kt

Setting the height on the action view has no effect here.

val mapItem = nav.menu.findItem(R.id.nav_map)

// Has no effect: trying to set height to 128dp
mapItem.actionView.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 128)

// Hack: hide title
mapItem.title = null

val mapView = supportFragmentManager.findFragmentById(R.id.nav_map_fragment) as SupportMapFragment
mapView.getMapAsync { map ->
    ...
}

Solution

  • I ended up solving this problem by creating an extension function that will do two things:

    1. Remove the title of the MenuItem (i.e. title = null)
    2. Attach a state change listener to the MenuItem

    The state change listener has the purpose of, when the item is attached to the window, finding the parent of the action view and setting it's height to WRAP_CONTENT (and storing the original height). When the view is detached, it restores the original height. This is because it seems like the views are actually recycled between different items as you scroll... so you'd otherwise find that gradually all of the items in the menu are affected, not just your target item.

    The extension function is implemented below:

    fun MenuItem.fillActionView() {
        // Hide the title text
        title = null
    
        // Fill the action view
        val thisView = actionView
        thisView.addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener {
            private var originalHeight: Int? = null
    
            override fun onViewAttachedToWindow(view: View?) {
                if (view === thisView) {
                    val v = view?.parent?.parent as? View
                    if (v != null) {
                        v.layoutParams = v.layoutParams
                                .apply {
                                    originalHeight = height
                                    height = ViewGroup.LayoutParams.WRAP_CONTENT
                                }
                    }
                }
            }
    
            override fun onViewDetachedFromWindow(view: View?) {
                val h = originalHeight
                val v = view?.parent?.parent as? View
                if ((h != null) && (v != null)) {
                    v.layoutParams = v.layoutParams
                            .apply {
                                height = h
                                originalHeight = null
                            }
                }
            }
        })
    
    }
    

    You can use it in your activity's onCreate() like so:

    val mapItem = nav.menu.findItem(R.id.nav_map) // Find your menu item which has the action view set
    mapItem.fillActionView()
    

    Java code:

    View.OnAttachStateChangeListener menuItemActionViewStateListener = new View.OnAttachStateChangeListener() {
        int originalHeight = 0;
    
        @Override
        public void onViewAttachedToWindow(View v) {
            View parent = (View) v.getParent();
            if (parent != null)
                parent = (View)parent.getParent();
    
            if (parent != null) {
                ViewGroup.LayoutParams p = parent.getLayoutParams();
                originalHeight = p.height;
                p.height = ViewGroup.LayoutParams.WRAP_CONTENT;
                parent.requestLayout();
            }
        }
    
        @Override
        public void onViewDetachedFromWindow(View v) {
            if (originalHeight != 0) {
                View parent = (View) v.getParent();
                if (parent != null)
                    parent = (View)parent.getParent();
    
                if (parent != null) {
                    ViewGroup.LayoutParams p = parent.getLayoutParams();
                    p.height = originalHeight;
                }
            }
        }
    };
    

    And just use it like here:

    final View v = inflater.inflate(R.layout.main_menu_item, null);
    v.addOnAttachStateChangeListener(menuItemActionViewStateListener);
    
    menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
    menuItem.setActionView(v);
    menuItem.setIcon(null);
    menuItem.setTitle(null);
    

    I hope this is helpful to others.