Trouble with Android ListView selection changes when switching ChoiceMode - android

I'm facing an odd behavior while trying to clear the choices (the selection) of items in my ListView. The code works pretty good, with functions that allow (once in CHOICE_MODE_MULTIPLE) to select single items by tapping, select all, select none and invert the current selection. Since the ListView is supposed to work in both modes (_NONE and _MULTIPLE) I have this menu item that switches between those modes allowing the user to "open" an item or select several items at once for batch operations.
The issue I'm facing shows up ONLY while changing choice mode from CHOICE_MODE_MULTIPLE back to CHOICE_MODE_NONE. What I'm trying to do is to not only revert back to CHOICE_MODE_NONE, but also clear any choices. The odd thing is that while all functions work reliably, when I call the "select non" function just before changing back to CHOICE_MODE_NONE, all items stay checked, no matter where or when I call the "select none" function inside my code.
The function that handles the selection changes is the following:
private void changeItemSelection(int selection) {
NotesAdapter adapter = (NotesAdapter)listView.getAdapter();
if (selection == SELECT_ALL) {
for(int iCount = 0; iCount < adapter.getCount(); iCount++) {
listView.setItemChecked(iCount, true);
}
}
else if (selection == SELECT_NONE) {
for(int iCount = 0; iCount < adapter.getCount(); iCount++) {
listView.setItemChecked(iCount, false);
}
}
else if (selection == SELECT_INVERT) {
for(int iCount = 0; iCount < adapter.getCount(); iCount++) {
listView.setItemChecked(iCount, !listView.isItemChecked(iCount));
}
}
adapter.notifyDataSetChanged();
checkedItemCountInvalid = true; // Invalidate checked indices cache
}
This is what happens when the user taps the menu item that switches selection modes:
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_multisel:
toggleSelectionMode();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void toggleSelectionMode() {
if (listView.getChoiceMode() == ListView.CHOICE_MODE_NONE) {
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
}
else {
listView.setChoiceMode(ListView.CHOICE_MODE_NONE);
}
changeItemSelection(SELECT_NONE);
}
No matter where I put "changeItemSelection(SELECT_NONE)" in the above code block, it simply doesn't work. But when I remove the "listView.setChoiceMode(ListView.CHOICE_MODE_NONE)" it magically starts to work as expected.
It's driving me crazy...
I really would appreciate any insights on this.
Thanks for reading!
EDIT:
What I mean by "not working" is that the selection remains unchanged. Thus, if item1 and item3 where selected, calling select none doesn't uncheck them, but only when I call the function in the part of code mentioned above. Calling select none without trying to change choice mode works perfectly fine.

I experience the same problem. It seems that there already is related question having an answer that states that it is a bug in Android.
I suppose this should be a comment, not answer. But I just do not have enough reputation to comment here.

I don't know if you've solved this yet, so maybe this answer is too late.
I think if you try using clearChoices() before you set the choice mode back to none (and then call your method to clear the checks) it will work as you desire.
private void toggleSelectionMode() {
if (listView.getChoiceMode() == ListView.CHOICE_MODE_NONE) {
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
}
else {
listview.clearChoices();
listView.setChoiceMode(ListView.CHOICE_MODE_NONE);
}
changeItemSelection(SELECT_NONE);
}

Related

How to make all checkboxes unchecked with a certain condition? Android

I have 45 check boxes that you can check. What I want to do is if you try to select more than 6 check boxes it will automatically disable all the buttons, however, if you tap the even one of the checked checkbox ,it will make all the checkbox checkable. This sounds simple ,but I cannot implements this method. I would be grateful if the pros here can help a noob like me. Here is the sample code.
checkbox[i].setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) { buttonView.setTextColor(Color.GREEN);
buttonClicked.add(buttonView.getText().toString());
buttonView.setTextSize(18);
count+=1;
if(count>=6){
for(int i = 0; i< 43;i++){
checkbox[i].setEnabled(false);
stopped = checkbox[i].isChecked();
if(stopped==true){
for(int a = 0; a < checkbox.length;a++){
checkbox[a].setEnabled(true);
}
}
}
}
}
if (!isChecked) {buttonView.setTextColor(Color.BLACK);
buttonClicked.remove(buttonView.getText().toString());
buttonView.setTextSize(15);
count-=1;
Your issue here is your reaction to finding a checked checkbox. We need to look at removing the internal loop under if(stopped==true)(Note 2).
You simply need
if(stopped){
checkbox[i].setEnabled(true);
}
Then in your if(!isChecked)(Note 3) you add your loop back in to reenable all the checkboxes so it will look like
if(!isChecked){
//your existing code
for(int i=0;i<checkbox.length;i++){
checkbox[i].setEnabled(true);
}
}
Note 1: I would advise that you swap your hardcoded "43"to checkbox.length just to keep things cleaner.
Note 2: You don't need to put ==true, it's already a boolean so this can just be if(stopped)
Note 3: This is what "else" was designed for. if(...){} if(!...){} is synonymous with if(...){}else{}.
Note 4: To void unnecessary looping (always good practice) we should maybe add another check here before the for loop to ensure that there were 6 boxes active.
if(count>=6){
for(int i=0;i<checkbox.length;i++){
checkbox[i].setEnabled(true);
}
}
count--;
Note 5: x+=1; can be replaced by x++; and similarly for x-=1; x--;(as said in the comments to your question)

Changing ListPreference entries by code working only the second time

I want to change dinamically the list entries and entryvalues of a listpreference. Well, finally it works, the list changes, but the problem is that I can see the new values only when I click the second time or more in the listpreference in the preference screen.
The first time, the list is always the original list, from 1 to 10.
numeroIntentosLP.setOnPreferenceClickListener(new OnPreferenceClickListener()
{
#Override
public boolean onPreferenceClick(Preference preference)
{
CharSequence[] NuevosValores = new String[10 - LineaActual];
int Indice = 0;
for(int i = LineaActual + 1; i <= 10; i++)
{
NuevosValores[Indice++] = String.valueOf(i);
}
numeroIntentosLP.setEntries(NuevosValores);
numeroIntentosLP.setEntryValues(NuevosValores);
return true;
}
});
I have tried with numeroIntentosLP.setOnPreferenceChangeListener, but the same result.
LineaActual is an integer value that I passed from MainActivity to PreferenceActivity. This is useful to me to know the new start value for the list.
So, everytime I open the preference screen and I click for the first time in the listpreference, I always get numbers 1 to 10, but when I click again, no matter how many times, I get what I want, I mean the list from LineaActual to 10.
Thanks in advance.
Ok, I found the solution. It was as easy as put the code directly in the onCreate, not in the listeners.
Thank you samgak, I saw your answer a little bit late and you're right, thank you anyway.

ListView with date as section headers position varies onScroll

Im trying to load custom callLogs in a listView based on date as section header.In ListAdapter i compare each date with the previous date and set SectionHeaderLayout Visible/Invisible. When the ListView has been loaded the section header are displayed correctly but when i scroll the section headers are set Visible to wrong ListItems.
Please help me to figure out a solution.
This is how im trying to set SectionHeader through the adapter.
if (position == 0) {
checkDate = mDateStr;
holder.sectionHeaderDate.setVisibility(View.VISIBLE);
holder.sectionHeaderText.setText(mDateStr);
}
} else if (checkDate == null || !checkDate.equals(mDateStr)) {
checkDate = mDateStr;
holder.sectionHeaderDate.setVisibility(View.VISIBLE);
holder.sectionHeaderText.setText(mDateStr);
} else {
holder.sectionHeaderDate.setVisibility(View.GONE);
}
Thanks in Advance
I see this is old question, you have probably solved your problem, but I'll answer for others who will have the same problem.
If you want to show header based on previous date you can't do that by remembering last item that was passed to getView function.
The reason is scrolling, i.e. different direction when going up and down.
For example, if you have items
1,
2,
3,
4,
5
when you're going down, and current item is 3, previous was 2, and all will work.
But if you are going up, your previous item for 3 was actually 4, and that's where your problem happens.
so instead of keeping item, you should use positions.
this would be the sketch of solution that you can call inside of your getView function:
private void showHeader(ViewHolder holder, Call item, int position) {
boolean shouldShowHeader = false;
if (position == 0
|| !DateHelper.isSameDay(item.getDateTime(),
items.get(position - 1).getDateTime()))
shouldShowHeader = true;
if (shouldShowHeader) {
holder.header.setVisibility(View.VISIBLE);
holder.date.setText(DateHelper.getSimpleDate(item.getDateTime()));
} else {
holder.header.setVisibility(View.GONE);
}
}

Adding items to ListView, maintaining scroll position and NOT seeing a scroll jump

I'm building an interface similar to the Google Hangouts chat interface. New messages are added to the bottom of the list. Scrolling up to the top of the list will trigger a load of previous message history. When the history comes in from the network, those messages are added to the top of the list and should not trigger any kind of scroll from the position the user had stopped when the load was triggered. In other words, a "loading indicator" is shown at the top of the list:
Which is then replaced in-situ with any loaded history.
I have all of this working... except one thing that I've had to resort to reflection to accomplish. There are plenty of questions and answers involving merely saving and restoring a scroll position when adding items to the adapter attached to a ListView. My problem is that when I do something like the following (simplified but should be self-explanatory):
public void addNewItems(List<Item> items) {
final int positionToSave = listView.getFirstVisiblePosition();
adapter.addAll(items);
listView.post(new Runnable() {
#Override
public void run() {
listView.setSelection(positionToSave);
}
});
}
Then what the user will see is a quick flash to the top of the ListView, then a quick flash back to the right location. The problem is fairly obvious and discovered by many people: setSelection() is unhappy until after notifyDataSetChanged() and a redraw of ListView. So we have to post() to the view to give it a chance to draw. But that looks terrible.
I've "fixed" it by using reflection. I hate it. At its core, what I want to accomplish is reset the first position of the ListView without going through the rigamarole of the draw cycle until after I've set the position. To do that, there's a helpful field of ListView: mFirstPosition. By gawd, that's exactly what I need to adjust! Unfortunately, it's package-private. Also unfortunately, there doesn't appear to be any way to set it programmatically or influence it in any way that doesn't involve an invalidate cycle... yielding the ugly behavior.
So, reflection with a fallback on failure:
try {
Field field = AdapterView.class.getDeclaredField("mFirstPosition");
field.setAccessible(true);
field.setInt(listView, positionToSave);
}
catch (Exception e) { // CATCH ALL THE EXCEPTIONS </meme>
e.printStackTrace();
listView.post(new Runnable() {
#Override
public void run() {
listView.setSelection(positionToSave);
}
});
}
}
Does it work? Yes. Is it hideous? Yes. Will it work in the future? Who knows? Is there a better way? That's my question.
How do I accomplish this without reflection?
An answer might be "write your own ListView that can handle this." I'll merely ask whether you've seen the code for ListView.
EDIT: Working solution with no reflection based on Luksprog's comment/answer.
Luksprog recommended an OnPreDrawListener(). Fascinating! I've messed with ViewTreeObservers before, but never one of these. After some messing around, the following type of thing appears to work quite perfectly.
public void addNewItems(List<Item> items) {
final int positionToSave = listView.getFirstVisiblePosition();
adapter.addAll(items);
listView.post(new Runnable() {
#Override
public void run() {
listView.setSelection(positionToSave);
}
});
listView.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
#Override
public boolean onPreDraw() {
if(listView.getFirstVisiblePosition() == positionToSave) {
listView.getViewTreeObserver().removeOnPreDrawListener(this);
return true;
}
else {
return false;
}
}
});
}
Very cool.
As I said in my comment, a OnPreDrawlistener could be another option to solve the problem. The idea of using the listener is to skip showing the ListView between the two states(after adding the data and after setting the selection to the right position). In the OnPreDrawListener(set with listViewReference.getViewTreeObserver().addOnPreDrawListener(listener);) you'll check the current visible position of the ListView and test it against the position which the ListView should show. If those don't match then make the listener's method return false to skip the frame and set the selection on the ListView to the right position. Setting the proper selection will trigger the draw listener again, this time the positions will match, in which case you'd unregister the OnPreDrawlistener and return true.
I was breaking up my head until I found a solution similar to this.
Before adding a set of items you have to save top distance of the firstVisible item and after adding the items do setSelectionFromTop().
Here is the code:
// save index and top position
int index = mList.getFirstVisiblePosition();
View v = mList.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
// for (Item item : items){
mListAdapter.add(item);
}
// restore index and top position
mList.setSelectionFromTop(index, top);
It works without any jump for me with a list of about 500 items :)
I took this code from this SO post: Retaining position in ListView after calling notifyDataSetChanged
The code suggested by the question author works, but it's dangerous.
For instance, this condition:
listView.getFirstVisiblePosition() == positionToSave
may always be true if no items were changed.
I had some problems with this aproach in a situation where any number of elements were added both above and below the current element. So I came up with a sligtly improved version:
/* This listener will block any listView redraws utils unlock() is called */
private class ListViewPredrawListener implements OnPreDrawListener {
private View view;
private boolean locked;
private ListViewPredrawListener(View view) {
this.view = view;
}
public void lock() {
if (!locked) {
locked = true;
view.getViewTreeObserver().addOnPreDrawListener(this);
}
}
public void unlock() {
if (locked) {
locked = false;
view.getViewTreeObserver().removeOnPreDrawListener(this);
}
}
#Override
public boolean onPreDraw() {
return false;
}
}
/* Method inside our BaseAdapter */
private updateList(List<Item> newItems) {
int pos = listView.getFirstVisiblePosition();
View cell = listView.getChildAt(pos);
String savedId = adapter.getItemId(pos); // item the user is currently looking at
savedPositionOffset = cell == null ? 0 : cell.getTop(); // current item top offset
// Now we block listView drawing until after setSelectionFromTop() is called
final ListViewPredrawListener predrawListener = new ListViewPredrawListener(listView);
predrawListener.lock();
// We have no idea what changed between items and newItems, the only assumption
// that we make is that item with savedId is still in the newItems list
items = newItems;
notifyDataSetChanged();
// or for ArrayAdapter:
//clear();
//addAll(newItems);
listView.post(new Runnable() {
#Override
public void run() {
// Now we can finally unlock listView drawing
// Note that this code will always be executed
predrawListener.unlock();
int newPosition = ...; // Calculate new position based on the savedId
listView.setSelectionFromTop(newPosition, savedPositionOffset);
}
});
}

Mono for Android: Spinner ItemSelected event triggers on load but shouldn't?

I have a spinner with a few values and I fill it from my webservice.
Filling the spinner
int i = 0;
var dropItems = new List<SpinItem2>();
DataRow[] result = myOPTvalues.Tables[0].Select("FieldValue=" + item.FieldValue);
foreach (DataRow row in result)
{
var optItem = new PrevzemSpin();
optItem.FieldValue = row["FieldValue"].ToString();
if (optItem.FieldValue.Equals(""))
optItem.FieldValue = null;
optItem.FieldTextValue = row["FieldTextValue"].ToString();
if (optItem.FieldTextValue.Equals(""))
optItem.FieldTextValue = null;
dropItems.Add(new SpinItem2(i, optItem.FieldValue.ToString(), optItem.FieldTextValue.ToString()));
}
i = 1;
foreach (DataRow row in myOPTvalues.Tables[0].Rows)
{
var optItem = new PrevzemSpin();
optItem.FieldValue = row["FieldValue"].ToString();
if (optItem.FieldValue.Equals(""))
optItem.FieldValue = null;
optItem.FieldTextValue = row["FieldTextValue"].ToString();
if (optItem.FieldTextValue.Equals(""))
optItem.FieldTextValue = null;
if (optItem.FieldValue != item.FieldValue)
{
dropItems.Add(new SpinItem2(i, optItem.FieldValue.ToString(), optItem.FieldTextValue.ToString()));
}
++i;
}
For some reason it acts like the item that was inserted first is "selected" on default and then triggers the ItemSelected event which I use to send the selected but I don't want that.
Since there's quite a number of these spinners on my screen it really slows down the activity plus it also sends the incorrect values to the field and since I use the ItemSelect to detect if everything went OK (let's say the service fell or the values themselves changed on server (someone added a new field on the server application) while the user is completing the form etc.)
Is there someway to tell the app not to trigger that on activity load but on actual user interaction?
I can't speak for Android specifically, but I have encountered this many times with Windows.
The solution I usually use is to simply add a boolean loading variable. Set it to true at the beginning of your initialisation and then clear it at the end.
In your event handlers like ItemSelected you can simply check if this is being triggered as the result of the initial load.
private void onItemSelected(....)
{
if(loading)
{
return; //Ignore as form is still loading
}
//Normal event handling logic goes here
....
}
Before I declared GetView:
int LastSpinnerSelectedPosition;
Inside my spinner definition:
LastSpinnerSelectedPosition = 0;
My spinner ItemSelected event:
var CurrentSelectedIndex = SpinnerValue.SelectedItemPosition;
if (CurrentSelectedIndex != LastSpinnerSelectedPosition)
{
// WHATEVER I WANTED TO DO ON ITEM SELECT ANYWAY
// Fix the LastSpinnerSelectedPosition ;)
LastSpinnerSelectedPosition = CurrentSelectedIndex;
}
Simple ;D
Just for clarification, the event fires when an item is selected. The semantics are obviously flawed, but technically the item IS selected when it initially loads since you can then immediately ask the spinner for which item is selected, so as the other answers say, just ignore the first time it is selected since it's guaranteed to be the loading select, and then proceed as normal after that.

Categories

Resources