androidandroid-menudrawerlayoutandroid-navigationview

Why do checkable menu items not display checkboxes


NavigationView Menu:

<?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 android:checkableBehavior="all">
        <item
            android:id="@+id/itemFoo1"
            android:checkable="true"
            android:title="Foo1" />
        <item
            android:id="@+id/itemFoo2"
            android:checkable="true"
            android:title="Foo2" />
    </group>
</menu>

The designer in Android Studio shows checkboxes: enter image description here

However, the app does not display checkboxes:

enter image description here

Could anyone offer a clue about this?

The layout using the menu:

<com.google.android.material.navigation.NavigationView
    android:id="@+id/navigationView"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_gravity="end"
    app:menu="@menu/activity_foo_view"/>

Solution

  • The default behavior for the NavigationView is to select an item at a time; this typically picks a fragment in a drawer layout for instance.

    And as Mike M pointed out in comments; making an item checkable doesn't mean that it's a CheckBox.

    So, you have to add a CheckBox to the menu item, and there are two options to do that:

    Using a CheckBox as app:actionViewClass:

    <?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 android:checkableBehavior="all">
            <item
                android:id="@+id/itemFoo1"
                android:checkable="true"
                app:actionViewClass="android.widget.CheckBox"
                android:title="Foo1" />
            <item
                android:id="@+id/itemFoo2"
                android:checkable="true"
                app:actionViewClass="android.widget.CheckBox"
                android:title="Foo2" />
        </group>
    </menu>
    

    Hitting the checkBox will toggle its state, but hitting the item text will not; this can be fixed programmatically by getting the menuItem ActionView and toggle the CheckBox using setChecked():

    private final NavigationView.OnNavigationItemSelectedListener navViewlistener = new NavigationView.OnNavigationItemSelectedListener() {
        @Override
        public boolean onNavigationItemSelected(MenuItem item) {
    
            int id = item.getItemId();
    
            if (id == R.id.itemFoo1 || id == R.id.itemFoo2) {
                CheckBox actionView = (CheckBox) item.getActionView();
                actionView.setChecked(!actionView.isChecked()); // Toggle the CheckBox
            }
    
            return true;
        }
    };
    
    navView.setNavigationItemSelectedListener(navViewlistener);
    

    UPDATE

    I used app:actionViewClass="android.widget.CheckBox" for a while. The main issue is the the checkbox is independent of the item. Clicking it does not trigger NavigationItemSelectedListener. That is the main reason I am seeking a new way.

    This is right; we could fix this programmatically by triggering a MenuItem click event whenever the holding CheckBox is checked/unchecked.

    But the main issue of that, it can cause infinite loop; as we check/uncheck the CheckBox within onNavigationItemSelected; and that will trigger another MenuItem click and so on.

    So, here a mIsCheckBoxClick boolean and also stopping CheckBox listeners before calling setChecked(), and reattaching them after that; all that to avoid the loop:

    private boolean mIsCheckBoxClick;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ....... add your code
    
        // Enable CheckBox listeners at the NavView menu items by default 
        addFooListeners(navView, true);
    }
    
    private void addFooListeners(final NavigationView navView, boolean enabled) {
        adjustNavViewItemListener(navView.getMenu().findItem(R.id.itemFoo1), navView, enabled);
        adjustNavViewItemListener(navView.getMenu().findItem(R.id.itemFoo2), navView, enabled);
    }
    
    private void adjustNavViewItemListener(final MenuItem item, final NavigationView navView, boolean enabled) {
        final CheckBox fooChBox = (CheckBox) item.getActionView();
        CompoundButton.OnCheckedChangeListener listener = new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                mIsCheckBoxClick = true;
                navView.getMenu().performIdentifierAction(item.getItemId(), 0); // perform a click on MenuItem to trigger OnNavigationItemSelectedListener
            }
        };
        fooChBox.setOnCheckedChangeListener(enabled ? listener : null);
    }
    
    
    private final NavigationView.OnNavigationItemSelectedListener navViewlistener = new NavigationView.OnNavigationItemSelectedListener() {
        @Override
        public boolean onNavigationItemSelected(MenuItem item) {
    
            int id = item.getItemId();
    
            if (id == R.id.itemFoo1 || id == R.id.itemFoo2) {
                
                // Loop prevention check
                if (!mIsCheckBoxClick) {
                    CheckBox actionView = (CheckBox) item.getActionView();
                
                    // Stop the CheckBox listeners
                    addFooListeners(navView, false);
                    
                    actionView.setChecked(!actionView.isChecked());
                    
                    // Reattach the CheckBox listeners
                    addFooListeners(navView, true);
                }
                mIsCheckBoxClick = false;           
            }
    
            return true;
        }
    };