Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Uncheck ALL items in BottomNavigationView without Dummy Item or Reflection

I am attempting to use the BottomNavigationView from the design library. Everything is working except I want each navigation item to start an activity, and therefore I want to uncheck all items in the nav so they look the same. I have tried several solutions, most of which do not work, and the last of which does work but feels very hacky.

First I did this:

ViewGroup nav = (ViewGroup) bottomNav;
for(int i=0; i < nav.getChildCount(); i++) {
    nav.getChildAt(i).setSelected(false);
}

Which seemed to do nothing.

Then I tried:

int size = bottomNav.getMenu().size();
for (int i = 0; i < size; i++) {
    bottomNav.getMenu().getItem(i).setChecked(false);
}

Which only made the last item checked instead of the first.

And finally I tried adding a dummy item to the menu and doing:

bottomNav.getMenu().findItem(R.id.dummmy_item).setChecked(true);
bottomNav.findViewById(R.id.dummmy_item).setVisibility(View.GONE);

Which almost works, but it hides the title underneath, which are important for context in my case.

Then I found this answer: https://stackoverflow.com/a/41372325/4888701 and edited my above solution to include that. Specifically I added the proguard rule, and I used that exact helper class and called the method. It looks correct, seems to work. But it feels very hacky to me because:

  1. I am using a dummy menu item to allow no visible item to be checked
  2. It adds quite a bit of code for what should be a small visual fix.
  3. I have read before that reflection should be avoided if at all possible.

Is there any other, preferably simpler way to achieve this, or is this the best we have with the current version of the library?

(As a side note, I am wondering if the proguard rule in this solution is necessary and what it does? I don't know really anything about proguard, but this project is inherited from someone else who had enabled it.)

like image 735
drawinfinity Avatar asked Mar 14 '17 20:03

drawinfinity


4 Answers

After plenty of trial and error, this worked for me (using Kotlin)

(menu.getItem(i) as? MenuItemImpl)?.let {
    it.isExclusiveCheckable = false
    it.isChecked = it.itemId == actionId
    it.isExclusiveCheckable = true
}
like image 120
Joe Van der Wee Avatar answered Oct 23 '22 19:10

Joe Van der Wee


The @Joe Van der Vee solutions works for me. I have made extension methods from it. But I consider whether this doesn't have some downsides like @RestirctedApi suppressing!

@SuppressLint("RestrictedApi")
fun BottomNavigationView.deselectAllItems() {
    val menu = this.menu

    for(i in 0 until menu.size()) {
        (menu.getItem(i) as? MenuItemImpl)?.let {
            it.isExclusiveCheckable = false
            it.isChecked = false
            it.isExclusiveCheckable = true
        }
    }
}
like image 28
Michał Ziobro Avatar answered Oct 23 '22 19:10

Michał Ziobro


If I've understood your question correctly (which it's possible I haven't) then a better solution might be to flip this problem around. These are my assumptions about your question:

  • You have a set of activities
  • Each Activity has its own BottomNavigationView
  • When you click the BNV on one activity, the item clicked becomes selected
  • You want to deselect the clicked item because when the new Activity starts nothing is selected

If my assumptions are correct there are two better solutions:

  1. Use Fragments not Activities (Recommended)
    • They the BNV stays on one activity, the fragment within the activity changes
  2. Don't deselect clicked item
    • Each activity when started selects the correct tile to match

That said, if you do want to do it your way I think the code below will achieve it, by just changing the affected item when it changes. (You should avoid Reflection whenever possible, it's generally indicative of another architectural problem with your design)

bnv.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
     @Override
     public boolean onNavigationItemSelected(@NonNull MenuItem item) {
         item.getActionView().setSelected(false);
         return false;
     }
});
like image 1
Nick Cardoso Avatar answered Oct 23 '22 19:10

Nick Cardoso


I know the question asked to not use reflection, however, I have not found another way to get the desired effect without using it. There is a code fix for allowing to disable the shifting mode in the git repo but who knows when that will be released. So for the time being (26.0.1), this code works for me. Also the reason people say don't use reflection is because it is slow on Android (especially on older devices). However, for this one call it won't be an impact on performance. You should avoid it when parsing/serializing a large amount of data though.

The reason for needing the proguard rule is because proguard obfuscates your code. Which means it can change method names, truncate names, break up classes and whatever else it sees fit to prevent someone from being able to read your source code. This rule prevents this field variable name from changing so that when you call it via reflection, it still exists.

Proguard rule:

  -keepclassmembers class android.support.design.internal.BottomNavigationMenuView {
     boolean mShiftingMode;
  }

Updated method:

static void removeShiftMode(BottomNavigationView view)
{
    BottomNavigationMenuView menuView = (BottomNavigationMenuView) view.getChildAt(0);
    try
    {
        Field shiftingMode = menuView.getClass().getDeclaredField("mShiftingMode");
        shiftingMode.setAccessible(true);
        shiftingMode.setBoolean(menuView, false);
        shiftingMode.setAccessible(false);
        for (int i = 0; i < menuView.getChildCount(); i++)
        {
            BottomNavigationItemView item = (BottomNavigationItemView) menuView.getChildAt(i);
            item.setShiftingMode(false);
            item.setChecked(false); // <-- Changed this line
            item.setCheckable(false); // <-- Added this line
        }
    }
    catch (NoSuchFieldException e)
    {
        Log.e("ERROR NO SUCH FIELD", "Unable to get shift mode field");
    }
    catch (IllegalAccessException e)
    {
        Log.e("ERROR ILLEGAL ALG", "Unable to change value of shift mode");
    }

}
like image 1
tim.paetz Avatar answered Oct 23 '22 17:10

tim.paetz