5

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.)

Community
  • 1
  • 1
drawinfinity
  • 315
  • 4
  • 21
  • 3
    a screenshot would really improve this question – Nick Cardoso Mar 17 '17 at 13:58
  • Can I ask you why you want to avoid reflection? There is a reason that it is there and it can be used efficiently and effectively. Admittedly, I will avoid it if I can, but sometimes that option just doesn't exist, and it's easier to use reflection than it is to try and find a work-around that doesn't. – BlackHatSamurai Mar 23 '17 at 21:56

5 Answers5

6

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
}
3

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
        }
    }
}
Michał Ziobro
  • 10,759
  • 11
  • 88
  • 143
1

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;
     }
});
Nick Cardoso
  • 20,807
  • 14
  • 73
  • 124
  • Your assumption is incorrect. The bottom navigation is a list of 3 choices only available in this activity. They do not correspond to the current activity at all, and the future activities do not have similar navigation (in fact they only move backwards to this activity). Also I am fully aware this is not exactly the typical use for this navigation in the material design spec, but it is the best option for our user experience as they need an obvious indicator of what their 3 options are and in this case would not likely think to search for a menu. – drawinfinity Mar 17 '17 at 14:25
  • what does "I want to uncheck all items in the nav so they look the same" mean? – Nick Cardoso Mar 17 '17 at 14:28
1

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");
    }

}
tim.paetz
  • 2,635
  • 20
  • 23
1

I just run into this problem. I found working solution here with casting menuItem to MenuItemImp which is annotated with @RestrictTo(LIBRARY_GROUP_PREFIX). If you don't want to use this restricted class, you can stick to standard MenuItem and instead of using isExclusiveCheckable just use isCheckable.

Example:

navigation_view?.menu?.let {
                for(menuItem in it.iterator()){
                    menuItem.isCheckable = false
                    menuItem.isChecked = false
                    menuItem.isCheckable = true
                }
            }