Uncheck ALL items in BottomNavigationView without Dummy Item or Reflection - android

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:
I am using a dummy menu item to allow no visible item to be checked
It adds quite a bit of code for what should be a small visual fix.
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.)

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
}

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

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:
Use Fragments not Activities (Recommended)
They the BNV stays on one activity, the fragment within the activity changes
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;
}
});

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

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

Related

How can I programmatically change each item of a RecyclerView all at once? (See post)

everyone.
I'm currently writing a vocabulary app ("Vocabulator) by using a room database and a RecyclerView to display the information.
My problem is that the way I implemented the feature to delete data from the RecyclerView (and the database) does not work on a dataset>10 items.
gif of how the delete function works
On the gif you can see that whenever an item is longClicked, a radio button for each item appears.
Upon clicking the button, the respective item is removed from the RecyclerView (and the db).
As you can see, this works on this very small dataset, but once I add more items the app starts to crash when performing the longClick.
Under the hood, it is implemented using a for loop to reiterate over each item like this:
#Override
public void onLongItemClick(int adapterPosition) {
//wordList.get(position);
// Show radio buttons
for (int i = 0; i < adapter.getItemCount(); i++)
{
vh = recyclerView.findViewHolderForAdapterPosition(i);
v = ((RecyclerAdapter.MyViewHolder) vh).getView();
rb = v.findViewById(R.id.delete_selector);
if (rb.getVisibility() == View.INVISIBLE)
{
rb.setVisibility(View.VISIBLE);
}
else
{
rb.setVisibility(View.INVISIBLE);
}
}
longClicked = true;
}
I can imagine this isn't among the most performant lines of code ever written.
However, that's the solution a beginner like me came up with.
I'd love to implement my delete feature in terms of its functionality exactly as demonstrated in the gif.
However, I can't find a way to make this work.
Can you give me any advice on the issue? :)
Thank you for reading.

Performance: How to select all ListView elements programmatically in efficient way?

In my application I use the ListView container with quite a lot of data. In order to provide the possiblity of item management, I use the MultipleChoiceMode to let user choose several items and copy/delete them.
I do it by setting the:
listView.setChoiceMode(GridView.CHOICE_MODE_MULTIPLE_MODAL);
listView.setMultiChoiceModeListener(listener);
Where listener implements a AbsListView.MultiChoiceModeListener.
Since it is a useful feature, I decided to add the possibility to select/deselect all list elements at once.
Currently it is done with following code (I simplified the code to show the main concept):
private void selectAll() {
for(int i = 0; i < listView.getCount(); i++) {
listView.setItemChecked(i, true);
}
}
Unfortunatelly this solution's performance is strictly item amount-dependent.
Could you, please advise me how to do it better/more scallable?
For, let's say 15000 elements, current solution is extremely laggy.
PS. I can't change the container type (e.g. to RecyclerView)... :(

How to override the Android ListView fast scroller appearance

I'm trying to implement a ListView with a FastScroll mechanism which uses time rather than A-Z
Unfortunately I can't seem to find a way into the layout used by the FastScroller index - it seems determined to show a small black square with very large white text
I've looked at the source:
http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.0_r1/android/widget/FastScroller.java/
http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.3.3_r1/android/widget/AbsListView.java#AbsListView
Both of these seem to show the key fast scroll member variables are private, and that the resource id used is fixed:
mOverlayDrawable = res.getDrawable(com.android.internal.R.drawable.menu_submenu_background);
Is there any way to override this? Ideally I'm targeting 2.2. and above.
You can try with reflection with something like this:
try {
Field scrollerField = AbsListView.class.getDeclaredField("mFastScroller"); //java.lang.reflect.Field
scrollerField.setAccessible(true);
FastScroller instance = scrollerField.get(listViewInstance);
Field overlayField = instance.getClass().getDeclaredField("mOverlayDrawable");
overlayField.setAccessible(true);
overlayField.set(instance, yourValueHere);
} catch (Exception e) {
Log.d("Error", "Could not get fast scroller");
}
I just typed it out so it might or might not compile straight off the bat, but that's the idea. I didn't check if the fields were called the same in all of the versions, you might have to adjust.

How to use Android Spinner like a drop-down list

It's taken me quite a while to get my head around the Android Spinner. After several failed implementation attempts, and after reading many questions partially similar to my own but without satisfactory answers, and some without any answers at all, e.g. here and here, I finally get that a "spinner" in Android isn't meant to be the same thing as a "drop-down list" from desktop apps, or a select in HTML. However, what my app (and I'm guessing the apps of all the other posters whose questions are similar) needs is something that works like a drop-down box, not like a spinner.
My two problems are with what I first considered to be idiosynchrasies the OnItemSelectedListener (I've seen these as separate questions on this site but not as one):
An initial selection of the first list item is triggered automatically without the user's interaction.
When the item that was already selected is selected again by the user, it is ignored.
Now I realise that, when you think about it, it makes sense for this to happen on a spinner - it has to start with a default value selected, and you spin it only to change that value, not to "re-select" a value - the documentation actually says: "This callback is invoked only when the newly selected position is different from the previously selected position". And I've seen answers suggesting that you set up a flag to ignore the first automatic selection - I guess I could live with that if there's no other way.
But since what I really want is a drop-down list which behaves as a drop-down list should (and as users can and should expect), what I need is something like a Spinner that behaves like a drop-down, like a combo-box. I don't care about any automatic pre-selection (that should happen without triggering my listener), and I want to know about every selection, even if it's the same one as previously (after all, the user selected the same item again).
So... is there something in Android that can do that, or some workaround to make a Spinner behave like a drop-down list? If there is a question like this one on this site that I haven't found, and which has a satisfactory answer, please let me know (in which case I sincerely apologise for repeating the question).
+1 to David's answer. However, here's an implementation suggestion that does not involve copy-pasting code from the source (which, by the way, looks exactly the same as David posted in 2.3 as well):
#Override
void setSelectionInt(int position, boolean animate) {
mOldSelectedPosition = INVALID_POSITION;
super.setSelectionInt(position, animate);
}
This way you'll trick the parent method into thinking it's a new position every time.
Alternatively, you could try setting the position to invalid when the spinner is clicked and setting it back in onNothingSelected. This is not as nice, because the user will not see what item is selected while the dialog is up.
Ok, I think I've come up with a solution for my own situation with the help of both David's and Felix' answer (I believe David's helped Felix', which in turn helped mine). I thought I'd post it here together with a code sample in case someone else finds this approach useful as well. It also solves both of my problems (both the unwanted automatic selection and the desired re-selection trigger).
What I've done is added a "please select" dummy item as the first item in my list (initially just to get around the automatic selection problem so that I could ignore when it was selected without user interaction), and then, when another item is selected and I've handled the selection, I simply reset the spinner to the dummy item (which gets ignored). Come to think of it, I should've thought of this long ago before deciding to post my question on this site, but things are always more obvious in hindsight... and I found that writing my question actually helped me to think about what I wanted to achieve.
Obviously, if having a dummy item doesn't fit your situation, this might not be the ideal solution for you, but since what I wanted was to trigger an action when the user selected a value (and having the value remain selected is not required in my specific case), this works just fine. I'll try to add a simplified code example (may not compile as is, I've ripped out a few bits from my working code and renamed things before pasting, but hopefully you'll get the idea) below.
First, the list activity (in my case) containing the spinner, let's call it MyListActivity:
public class MyListActivity extends ListActivity {
private Spinner mySpinner;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// TODO: other code as required...
mySpinner = (Spinner) findViewById(R.id.mySpinner);
mySpinner.setAdapter(new MySpinnerAdapter(this));
mySpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
#Override
public void onItemSelected(AdapterView<?> aParentView,
View aView, int aPosition, long anId) {
if (aPosition == 0) {
Log.d(getClass().getName(), "Ignoring selection of dummy list item...");
} else {
Log.d(getClass().getName(), "Handling selection of actual list item...");
// TODO: insert code to handle selection
resetSelection();
}
}
#Override
public void onNothingSelected(AdapterView<?> anAdapterView) {
// do nothing
}
});
}
/**
* Reset the filter spinner selection to 0 - which is ignored in
* onItemSelected() - so that a subsequent selection of another item is
* triggered, regardless of whether it's the same item that was selected
* previously.
*/
protected void resetSelection() {
Log.d(getClass().getName(), "Resetting selection to 0 (i.e. 'please select' item).");
mySpinner.setSelection(0);
}
}
And the spinner adapter code could look something like this (could in fact be an inner class in the above list activity if you prefer):
public class MySpinnerAdapter extends BaseAdapter implements SpinnerAdapter {
private List<MyListItem> items; // replace MyListItem with your model object type
private Context context;
public MySpinnerAdapter(Context aContext) {
context = aContext;
items = new ArrayList<MyListItem>();
items.add(null); // add first dummy item - selection of this will be ignored
// TODO: add other items;
}
#Override
public int getCount() {
return items.size();
}
#Override
public Object getItem(int aPosition) {
return items.get(aPosition);
}
#Override
public long getItemId(int aPosition) {
return aPosition;
}
#Override
public View getView(int aPosition, View aView, ViewGroup aParent) {
TextView text = new TextView(context);
if (aPosition == 0) {
text.setText("-- Please select --"); // text for first dummy item
} else {
text.setText(items.get(aPosition).toString());
// or use whatever model attribute you'd like displayed instead of toString()
}
return text;
}
}
I guess (haven't tried this) the same effect could be achieved using setSelected(false) instead of setSelection(0), but re-setting to "please select" suits my purposes fine. And, "look, Ma, no flag!" (Although I guess ignoring 0 selections is not that dissimilar.)
Hopefully, this can help someone else out there with a similar use case. :-) For other use cases, Felix' answer may be more suitable (thanks Felix!).
Look. I don't know if this will help you, but since you seem tired of looking for an answer without much success, this idea may help you, who knows...
The Spinner class is derived from AbsSpinner. Inside this, there is this method:
void setSelectionInt(int position, boolean animate) {
if (position != mOldSelectedPosition) {
mBlockLayoutRequests = true;
int delta = position - mSelectedPosition;
setNextSelectedPositionInt(position);
layout(delta, animate);
mBlockLayoutRequests = false;
}
}
This is AFAIK taken from 1.5 source. Perhaps you could check that source, see how Spinner/AbsSpinner works, and maybe extend that class just enough to catch the proper method and not check if position != mOldSelectedPosition.
I mean... that's a huge "maybe" with a lot of "ifs" (android versioning comes to mind etc.), but since you seem frustrated (and I've been there with Android many times), maybe this can give you some "light". And I assume that there are no other obvious answers by looking at your previous research.
I wish you good luck!
Here is an alternative solution to differentiate between any (intended or unintended) programmatic and user-initiated changes:
Create your listener for the spinner as both an OnTouchListener and OnItemSelectedListener
public class SpinnerInteractionListener implements AdapterView.OnItemSelectedListener, View.OnTouchListener {
boolean userSelect = false;
#Override
public boolean onTouch(View v, MotionEvent event) {
userSelect = true;
return false;
}
#Override
public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
if (userSelect) {
// Your selection handling code here
userSelect = false;
}
}
}
Add the listener to the spinner registering for both event types
SpinnerInteractionListener listener = new SpinnerInteractionListener();
mSpinnerView.setOnTouchListener(listener);
mSpinnerView.setOnItemSelectedListener(listener);
This wouldn't handle the case in which the re-selection of the same item by the user doesn't trigger the onItemSelected method (which I have not observed), but I guess that could be handled by adding some code to the onTouch method.
Anyway, the problems Amos pointed out were driving me crazy before thinking of this solution, so I thought I'd share as widely as possible. There are many threads that discuss this, but I've only seen one other solution so far that is similar to this: https://stackoverflow.com/a/25070696/4556980.
Modifying the Spinner is useful if you want to have multiple selections simultaneously in the same activity.
If you only desire the user to have a hierarchical selection, for example:
What do you want to eat?
Fruit
Apples
Bananas
Oranges
Fast Food
Burgers
Fries
Hot dogs,
then the ExpandableListView might be better for you. It allows the user to navigate a hierarchy of different groups and choose a child element. This would be similar to having several Spinners for the user to choose from - if you do not desire a simultaneous selection, that is.
I worked through several of the issues mentioned in this thread before I realized that the PopupMenu widget is what I really wanted. That was easy to implement without the hacks and workarounds needed to change the functionality of a Spinner. PopupMenu was relatively new when this thread was started in 2011, but I hope this helps someone searching for similar functionality now.

ListView item backgrounds not changing

I have an unusual issue with my ListView. I currently have a "deselectAll()" method which iterates through the items in my ListView and sets them to unchecked (the items implement the Checkable interface). The "checked" variable gets changed correctly (the view reports as not being checked), but the visual indicator (in this case, a background change) does not show the view as unchecked (the background stays the color of a checked item).
I am iterating and deselecting through my listview like so (I also added my declerations):
private ListView vw_entryList;
private void deselectAll() {
for (int i = 0; i < sAdapter.getCount(); i++) {
((Entry)vw_entryList.getItemAtPosition(i)).setChecked(false);
}
}
The code for my implemented setChecked() is as follows:
public void setChecked(boolean checked) {
_checked = checked;
if (checked) {
setBackgroundResource(R.drawable.listview_checked);
}
else {
setBackgroundResource(R.drawable.listview_unchecked);
}
invalidate();
}
It should be noted that when the items are clicked, they are toggled between checked and unchecked in the OnItemClickListener, and this works ok, with the background change and everything. The code for toggling is very similar:
public void toggle() {
_checked = !_checked;
setBackgroundResource(_checked ?
R.drawable.listview_checked : R.drawable.listview_unchecked);
invalidate();
}
The only difference I can see is where the methods are called from. toggle() is called from within the OnItemClickListener.onClick() method, while my deselectAll() is called from within a button's standard OnClickListener, both in the same class. Does anyone have any ideas as to why the background doesn't change when I call my deselectAll() function?
Do you have custom, non-standard color for the background? If so you might take a look at http://www.curious-creature.org/2008/12/22/why-is-my-list-black-an-android-optimization/ - it boils down to setting android:cacheColorHint attribute of your list to the background color. Maybe that will help.
Edited after further discussion:
I think you need to call getAdapter().notifyDataSetChanged() on the List rather than invalidate(). List is really build in the way that it is relying on adapter to provide the data. What you are doing in fact you have an implicit adapter - Entry is really kept in the adapter and by setting checked, you are changing the data model really, but if you do not call notifyDataSetChanged() the list does not really know that the model has changed and will not recreate the views (invalidate() will only redraw the existing ones).
After trying everything (thanks for your help Jarek), I found a solution that works for my purposes. Instead of implicitly calling the setChecked() within the view that was clicked, I leave it up to the setItemChecked() method within the ListView class.
My updated code:
private void deselectAll() {
for (int i = 0; i < sAdapter.getCount(); i++) {
vw_entryList.setItemChecked(i, false);
}
}
My best guess is that the ListView knows that its items implement the Checkable class, and thus requires itself to be the handler of all item operations. Something along those lines. If anyone can explain in more detail why this solution works while the others did not, I'll reward them with the answer and an upvote.

Categories

Resources