ListView selection remains persistent after exiting choice mode - java

I have a ListView subclass that I allow selections on when the context action bar (CAB) is active. The CAB is set as a callback to the onItemLongClick event:
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Inflate a menu resource providing context menu items
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(context_menu, menu);
getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
return true;
}
This is fine, and the ListView works as expected, with the currently selected item staying highlighted when touched.
When I close the CAB, I want the ListView to return to normal (i.e. Touch mode). The problem is that the last selected item remains highlighted indefinitely, regardless of what methods I try to clear it:
public void onDestroyActionMode(ActionMode mode) {
//Unselect any rows
ListView lv = getListView();
lv.clearChoices(); // Has no effect
lv.setChoiceMode(ListView.CHOICE_MODE_NONE); // Has no effect on the highlighted item
lv.setFocusable(false); // Has no effect
lv.setSelection(0); // Has no effect
mActionMode = null;
}
Any suggestions?

The main reason for the problem is that once the ListView selection mode is switched to CHOICE_MODE_NONE, the framework optimizes out the clear operation as it is no longer supporting 'selections'. I have improved the above workarounds a bit by clearing the selection state manually and then setting the mode in a delayed manner so the framework will have its turn to clear the state before turning the mode to CHOICE_MODE_NONE.
final ListView lv = getListView();
lv.clearChoices();
for (int i = 0; i < lv.getCount(); i++)
lv.setItemChecked(i, false);
lv.post(new Runnable() {
#Override
public void run() {
lv.setChoiceMode(ListView.CHOICE_MODE_NONE);
}
});

I faced the same issue and since requesting layout doesn't solve the problem for me either I implemented a little hack which works for me. Maybe this is the same issue because I'm switching between CHOICE_MODE_SINGLE and CHOICE_MODE_NONE.
When the action mode ends I'm calling this code snippet. clearChoices makes sure that all items are not checked anymore (internally). The iteration over the views makes sure that all currently visible views are reset and not checked anymore.
mListView.clearChoices();
for (int i = 0; i < mListView.getChildCount(); i++) {
((Checkable) mListView.getChildAt(i)).setChecked(false);
}
mListView.setChoiceMode(ListView.CHOICE_MODE_NONE);

Looking at the ListView sourcecode, the only way to work around this is to set the ListView to CHOICE_MODE_NONE, then re-assign the ListAdapter (which clears the internal selection list regardless of choice mode)
i.e. in a ListFragment/ListActivity
getListView().setChoiceMode(ListView.CHOICE_MODE_NONE);
getListView().setAdapter(getListAdapter())

I was having this issue in API Level 17 and solved it by doing:
listView.clearChoices();
listView.invalidateViews();

For me, it seems the accepted answer is not working for invisible items, and it's no need to call
for (int i = 0; i < lv.getCount(); i++)
lv.setItemChecked(i, false);
instead, just call
lv.requestLayout();
To completely solve my issue, I call
lv.clearChoices();
lv.requestLayout();
in onDestroyActionMode()
and call
lv.setItemChecked(position, false)
in onItemClick() when it's not in ActionMode
However, I did not confirm whether call setItemChecked() will result some performance issues

This has been logged as an AOSP bug, but marked as obsolete for whatever reason.
Normally you would expect this to work:
getListView().clearChoices();
getListView().setChoiceMode(ListView.CHOICE_MODE_NONE);
Unfortunately it does not. Deferring setting choice mode to none in the next layout pass would work:
getListView().clearChoices();
getListView().post(new Runnable() {
#Override
public void run() {
getListView().setChoiceMode(ListView.CHOICE_MODE_NONE);
}
});

I had tried all the approaches discussed above but none of them work for me. Finally, I decide to apply the following workaround. The key idea is that,
During multimode, instead of reusing the "cached" view, we will create a completely new view. Not efficient, but at least "partially" solve my problem.
Here is the code of my customized ArrayAdapter
#Override
public View getView(int position, View convertView, ViewGroup parent) {
// Key to solve this problem. When we are in multimode, we will not reusing the cached view.
View rowView = this.multimode ? null : convertView;
if (rowView == null) {
LayoutInflater inflater = activity.getLayoutInflater();
rowView = inflater.inflate(R.layout.watchlist_row_layout, null);
ViewHolder viewHolder = new ViewHolder();
viewHolder.textView0 = (TextView) rowView.findViewById(R.id.text_view_0);
viewHolder.textView1 = (TextView) rowView.findViewById(R.id.text_view_1);
viewHolder.textView2 = (TextView) rowView.findViewById(R.id.text_view_2);
rowView.setTag(viewHolder);
}
Also, I feel safer to have the following code in ActionMode.Callback, although I'm not sure how much it helps.
#Override
public void onDestroyActionMode(ActionMode mode) {
MyFragment.this.myArrayAdapter.setMultimode(false);
// http://stackoverflow.com/questions/9754170/listview-selection-remains-persistent-after-exiting-choice-mode
// Using View.post is the key to solve the problem.
final ListView listView = MyFragment.this.getListView();
listView.clearChoices();
for (int i = 0, ei = listView.getChildCount(); i < ei; i++) {
listView.setItemChecked(i, false);
}
listView.post(new Runnable() {
#Override
public void run() {
listView.setChoiceMode(ListView.CHOICE_MODE_NONE);
}
});
actionMode = null;
}
Side Note
Using MultiChoiceModeListener couple with CHOICE_MODE_MULTIPLE_MODAL will make this bug gone. However, for device below API level 11 will not able to use this solution.

I know this has been answered, but above answers still gave me problems with the cached/recycled views that ListView maintains, that didn't update it's state when scrolled back into view.
So, the above solution changes slightly to:
lv.clearChoices();
ArrayList<View> list = new ArrayList<View>();
lv.reclaimViews(list);
for (View view : list) {
((Checkable) view).setChecked(false);
}
lv.setChoiceMode(lv.CHOICE_MODE_NONE);
This is better than using getChildAt(i) because that method jusg gives you the currently visble views and does not account for the internal cached views, that are not visible.

I have found that the only two methods that work here (API 19) are:
Resetting the list adapter, which is undesirable because it goes back to the top of the list;
Setting the choice mode to CHOICE_MODE_NONE in a new Runnable
If the choice mode is changed without using listView.post(new Runnable()), it doesn't work. Can anyone explain to me why this is?
Apologies for not commenting; I have no reputation.
Thanks.

Not sure if this is too late just wanted to share. I created an intent to the same page so that once the clicked data is captured it recreates a fresh page without any clicked persistence.

Is not a bug. That behavior is required to support multiple HID for Android.
So to show the selection state you only need set the choice mode of the listview and a background to support the selected state for the "list item layout", like:
android:background="?android:attr/activatedBackgroundIndicator"
FYI: http://android-developers.blogspot.mx/2008/12/touch-mode.html

Related

Spinner is not showing any selected Item

this is my first question here as a Android beginner. I am also not an native speaker, so please excuse my bad english skills.
I want to implement an Android App for renting machines. To add a new contract, i created an AddVertragActivity.java with a spinner. This spinner should be filled with data from a Room database. The problem is, that i can populate the spinner to view all items, but when I click on any item nothing happens. I discovered in debug mode, that the onItemSelected method is never called. I have read many other post here, but nothing solved my problem. Due to the fact, that no error message is shown, I have no idea what to do next. Every time I select an item in the dropdown list, there is a warning in the logfile:D/OpenGLRenderer: endAllActiveAnimators on 0xe54e7a70 (DropDownListView) with handle 0xbc8d3a30.
So I also do some research and only find a few answers, that didn't help me.
Every time I start the activity there is also an warning about HiddenField. But I decreased the android version to Nougat and this warning was obsolet. So that didn't solved the problem either:W/.viewpager_tes: Accessing hidden field Landroid/widget/AbsListView;->mIsChildViewEnabled:Z (greylist, reflection, allowed)
When you need some extra information, let me know. Thanks in advance for any advise.
Activity
public class AddVertragActivity extends AppCompatActivity implements AdapterView.OnItemSelectedListener {
private Spinner spinnerB;
private BaumaschinenViewModel bViewModel;
private List<String> baumaschineList = new ArrayList<String>();
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_add_vertrag);
spinnerB = findViewById(R.id.spinnerBaumaschine);
bViewModel = new ViewModelProvider(this).get(BaumaschinenViewModel.class);
bViewModel.getAllBaumaschinen().observe(this, baumaschines -> {
for (int i = 0; i < baumaschines.size(); i++) {
baumaschineList.add(baumaschines.get(i).getMachineName());
System.out.println(baumaschines.get(i).getRowid());
}
});
ArrayAdapter<String> spinnerBaumaschinenAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, baumaschineList);
spinnerBaumaschinenAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerBaumaschinenAdapter.notifyDataSetChanged();
spinnerBaumaschinenAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerB.setAdapter(spinnerBaumaschinenAdapter);
spinnerB.setOnItemSelectedListener(this);
}
#Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (parent.getItemAtPosition(position).equals("Baumaschine auswählen")) {
} else {
String item = parent.getItemAtPosition(position).toString();
Toast.makeText(parent.getContext(), "Selected: " + item, Toast.LENGTH_SHORT).show();
}
}
#Override
public void onNothingSelected(AdapterView<?> parent) {
}
}
You are missing a call to set the item listener that is the reason it is not been called when you clicked any item.
Do something like below. After setting adapter spinnerB.setAdapter(spinnerBaumaschinenAdapter);
spinnerB.setOnItemSelectedListener(this);
Try to use spinnerB.setOnItemClickListener instead of spinnerB.setOnItemSelectedListener.
Hope this helps. Thank you...
Kindly let me know whether it is working or not. Thank you...

Removing item from list in the OnBindViewHolder method for RecyclerView Android

Ok so it is a bit complicated, I have a custom RecyclerView Adapter and in the OnBindViewHolder method I would like to remove the current item from the recyclerview depending on some different variables but when I remove the item from the ArrayList and call notifyDataSetChanged(); I get :
java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling.
How can I remove the item from the RecyclerView in the onBindViewHolder ? , I do not want to do it before setting the adapter because each item in the recyclerview has a sublist and I want to remove the item if the sublist is empty. Thanks for any ideas and sorry for bad english.
Please follow the method used below as shown in this answer.
private List<DetectedIssue> issues = new ArrayList<DetectedIssue>();
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
// - get element from your dataset at this position
// - replace the contents of the view with that element
holder.button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
try {
issues.remove(position);
notifyItemRemoved(position);
//this line below gives you the animation and also updates the
//list items after the deleted item
notifyItemRangeChanged(position, getItemCount());
} catch (SQLException e) {
e.printStackTrace();
}
}
});
}
#Override
public int getItemCount() {
return issues.size();
}
PS: As can be seen from the documentation it is bad practice to use notifyDataSetChanged() if only an item is added, moved or removed as unnecessary processes are run.
This event does not specify what about the data set has changed, forcing any observers to assume that all existing items and structure may no longer be valid. LayoutManagers will be forced to fully rebind and relayout all visible views.
If you want the underlying arraylist in some activity be changed then you will have to use an interface to communicate with the activity to change the arraylist.
I know its a bit late (4 years, heh) but I'm in the same solution. As the error states it is actually impossible to call methods such as adapter.notifyItemRemoved(position); or notifyItemRangeChanged(position, getItemCount()); and even the more intensive notifyDataSetChanged(); from within the onBindViewHolder method unless it is within an onClickListener whereby it will be called after the layout is built and not during the build.
A simple workaround for me was to call datalist.remove(position); so if the layout is rebuilt the view is omitted, then simply call holder.itemView.setVisibility(View.GONE);. It's not as clean, but it gets the job done for views you wish to hide.

Fast scoll disabled after changing list view adapter

I have a ListView with fastscroll enabled:
mList.setFastScrollEnabled(true);
This works very well. But when I change the adapter, I cannot fast scroll anymore.
mList.setAdapter(mAdapter);
My adapter has getCount() properly implemented. Calling
mList.setFastScrollEnabled(true);
after setting the new adapter doesn't work either nor adding android:fastScrollEnabled="true" to the ListView XML works.
Is there any way to reenable fast scrolling?
After digging in the depths of the Android sourcecode, I finally found a solution:
mAdapter = new ArrayAdapter....
mList.setAdapter(mAdapter);
// We have to post notifyDataSetChanged() here, so fast scroll is set correctly.
// Without it, the fast scroll cannot get any child views as the adapter is not yet fully
// attached to the view and getChildCount() returns 0. Therefore, fast scroll won't
// be enabled.
// notifyDataSetChanged() forces the listview to recheck the fast scroll preconditions.
mList.post(new Runnable() {
#Override
public void run() {
mAdapter.notifyDataSetChanged();
}
});
FastScroller.java has the following method:
public void onItemCountChanged(int totalItemCount) {
final int visibleItemCount = mList.getChildCount();
....
updateLongList(visibleItemCount, totalItemCount);
}
When setAdapter() is called, FastScroller re-checks the conditions for enabling fast scrolling. But since the new adapter is not fully shown yet, mList.getChildCount() returns zero and updateLongList() won't enable fast scrolling.
With my fix, the ListView gets notified that the underlaying data was changed after the new adapter was fully attached to the view and the preconditions for fast scrolling are now satisfied.

Why does LinearLayout child getWidth method return 0?

I have a LinearLayout, and this LinearLayout will hold dynamically placed views. I need to find out what the width of the children of LinearLayout, however this has to be done in onCreate method. From researching I've found out that you can't use getWidth from this method. So instead I'm using onWindowFocusChanged, which works for the parent LinearLayout (returning me the actual size), but it doesn't work with its children.
Another thing I noticed is that when the screen is fading away and the screen is locked, I can see at the logs the actual width of the children being returned (I think the activity is being paused).
I'm really stuck and this is needed because I need to dynamically place those views depending on the children width.
You might be able to get with the below. But as others pointed out, this probably isn't a great idea.
LinearLayout.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
LinearLayout.getMeasuredWidth();
inside the onCreate , views still can't know the state of the nearby views and the children ,etc... so only after all is prepared and the layout process is done , you can get the size of views .
here's a quick code for getting the size of the view just before it's being drawn:
private static void runJustBeforeBeingDrawn(final View view, final Runnable runnable)
{
final ViewTreeObserver vto = view.getViewTreeObserver();
final OnPreDrawListener preDrawListener = new OnPreDrawListener()
{
#Override
public boolean onPreDraw()
{
Log.d(App.APPLICATION_TAG, CLASS_TAG + "onpredraw");
runnable.run();
final ViewTreeObserver vto = view.getViewTreeObserver();
vto.removeOnPreDrawListener(this);
return true;
}
};
vto.addOnPreDrawListener(preDrawListener);
}
alternatively , you can use addOnGlobalLayoutListener instead of addOnPreDrawListener if you wish.
example of usage :
runJustBeforeBeingDrawn(view,new Runnable()
{
#Override
public void run()
{
int width=view.getWidth();
int height=view.getHeight();
}
});
another approach is to use onWindowFocusChanged (and check that hasFocus==true) , but that's not always the best way ( only use for simple views-creation, not for dynamic creations)
EDIT: Alternative to runJustBeforeBeingDrawn: https://stackoverflow.com/a/28136027/878126
https://stackoverflow.com/a/3594216/1397218
So you should somehow change your logic.

How can I update multi select in android dialog window

I have been unable to find a tutorial helping with multi-selects using cursors. As of right now my logic is working the way I want but the check boxes will not update properly. What am I overlooking?
return new AlertDialog.Builder(this).setTitle("Items")
.setMultiChoiceItems(cur, CHECK, EDATE, new DialogInterface.OnMultiChoiceClickListener() {
#Override
public void onClick(DialogInterface dialog, int position, boolean checked)
{
DBM.open();
AlertDialog AD = (AlertDialog) dialog;
ListView list = AD.getListView();
list.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
itemCur = (Cursor) list.getItemAtPosition(position);
if (checked)
{
//update query
DBM.setChecked(checkCur.getInt(checkCur.getColumnIndex(ID)), itemId, userId, 1);
list.setItemChecked(1, true);
} else
{
DBM.setChecked(checkCur.getInt(checkCur.getColumnIndex(ID)), itemId, userId, 0);
list.setItemChecked(1, false);
}
DBM.close();
}
}).setPositiveButton("OK", new DialogButtonClickHandler()).create();
Dialogs on android can't be modified. If you look at the source code you will see that dialogbuilder delegates all the presentation work to some components and you don't have access to them after creation. Thus changing the state of the components you use for building the dialog won't update the dialog components afterwards.
You can see this mechanism here and here : you don't have access to the access controller after onCreate has been called on the alert controller.
The best if you want to achieve this is to rebuild a new activity and give it a dialog theme.
You can just use the setCursor() method for AlertDialog. Its pretty simple so you probably wouldn't need a tutorial.
A relevant SO questions is here and the docs for it are here
So after digging into the issue a bit and going through a couple different iterations I finally found a solution that I am fairly happy with. With school and work pushing hard I have had little time outside to work on extra projects and I have been sitting with this solution for while now but unable to get it posted.
The final piece to my puzzle was finding the changeCursor function, this fixed the issue of the old data that no longer matched the DB to load. My current hurdle is the time it takes to check a box, there is an obvious lag from clicked to updated. I have found that mutliple records update when one is clicked. I have not been able to find a valid reason for these extra updates.
Below is the code I currently have implemented to have the multi-select working. This just the dialog code, for a working demo I will be posting a project on GitHub for a working prototype of it all in action. (Now made public, Multiselect Dialog)
I am a fairly new Android developer, majority of my Android knowledge has been self taught and learned through the knowledge of online resources. I was working on a school project and wanted to implement a multiselect in a dialog that would update the main activity with the selected choices. Please lend any advice you can on how to improve this.
Pros:
- Populates check boxes properly on load.
- Updates database when check is clicked.
- Keeps display updated after data change.
Cons:
- Must click check box to update value.
- Unable to undo changes made while in dialog. The values save onClick, I have not been able to think of a way to temporarily store the new values until confirmed by the user.
- A single click updates multiple records, also sometimes when choices scroll off the screen values update
#Override
protected Dialog onCreateDialog(int id)
{
switch (id) {
case 0:
LayoutInflater factory = LayoutInflater.from(this);
// Setup of the view for the dialog
final View bindListDialog = factory.inflate(R.layout.multi_list_layout, null);
multiListView = (ListView) bindListDialog.findViewById(R.id.multiList);
// Because I do not know how to properly handle an undo in this situation
// I make the dialog only close if the button is pressed and confirms the changes
return new AlertDialog.Builder(MultiSelectDemoActivity.this).setTitle(R.string.multiSelectTitle)
.setCancelable(false).setView(bindListDialog)
.setPositiveButton(R.string.btnClose, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton)
{
updateItemList(); // In my implementation there is a list view
// that shows what has been selected.
}
}).create();
default:
return null;
}
}
private static final boolean ONCREATE = true;
private static final boolean ONUPDATE = false;
private void setupMultiList(Boolean newList)
{
demoDBM.open();
multiCur = demoDBM.getList(userId); // Gets all items tied to the user.
startManagingCursor(multiCur);
// Uses the cursor to populate a List item with an invisible ID column,
// a name column, and the checkbox
demoDBM.close();
if (newList)
{
// Creates a new adapter to populate the list view on the dialog
multiAdapter = new SimpleCursorAdapter(this, R.layout.check_list_item, multiCur, new String[] { DemoDBM.ID,
DemoDBM.NAME, DemoDBM.SEL }, new int[] { R.id.itemId, R.id.itemName, R.id.itemCheck });
multiAdapter.setViewBinder(new MyViewBinder());
multiListView.setAdapter(multiAdapter);
} else
{
// updates the previously made adapter with the new cursor, without changing position
multiAdapter.changeCursor(multiCur);
}
}
#Override
protected void onPrepareDialog(final int id, final Dialog dialog, Bundle args)
{
setupMultiList(ONCREATE);
}
public class MyViewBinder implements ViewBinder
{
#Override
public boolean setViewValue(View view, Cursor cursor, int columnIndex)
{
int checkId = cursor.getColumnIndex(DemoDBM.SEL);
if (columnIndex == checkId)
{
CheckBox cb = (CheckBox) view;
// Sets checkbox to the value in the cursor
boolean bChecked = (cursor.getInt(checkId) != 0);
cb.setChecked(bChecked); // Switches the visual checkbox.
cb.setOnCheckedChangeListener(new MyOnCheckedChangeListener());
return true;
}
return false;
}
}
public class MyOnCheckedChangeListener implements OnCheckedChangeListener
{
#Override
public void onCheckedChanged(CompoundButton checkBox, boolean newVal)
{
View item = (View) checkBox.getParent(); // Gets the plain_list_item(Parent) of the Check Box
// Gets the DB _id value of the row clicked and updates the Database appropriately.
int itemId = Integer.valueOf(((TextView) item.findViewById(R.id.itemId)).getText().toString());
demoDBM.open();
demoDBM.setChecked(itemId, userId, newVal);
demoDBM.close();
setupMultiList(ONUPDATE);
}
}

Categories