ListeView recycling Views causing problems - android

When I click a list item, every 12th item is also selected. ListViews recycle views and therefore many items are being selected, any idea how I overcome the problem so just the items I click are marked as checked.
#Override
public void onListItemClick(ListView l, View v, int position, long id){
CheckedTextView check = (CheckedTextView) v;
if (check.isChecked()){
check.setChecked(false);
selections.remove((Integer) position);
}
else{
check.setChecked(true);
selections.add((Integer) position);
}
}
I use an ArrayAdapter. Names is a String[] of about 1000 options.
adapter = new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_multiple_choice, names);
setListAdapter(adapter);

As this answer CheckBox in ListView says maintain a list of your checked items and then check your CheckBox in getView based on that list.
Also set the onCheckedChangeListener for that CheckBox null before checking it or else the corresponding listener will be called.

In lisview, recycling is very tricky concept, first by recycled view android means view which were used earlier, so basically this view will contain all the parameter which you or user set earlier.
This view is now returned to your adapter, what you do is to check if its null inflate a new view set new values supply it back, if its not then refresh the widgets view contains with new values and then supply it back.
Now coming back to your code, you are simply checking if this view was checked or not, say suppose you checked first view in you list, now when you scroll down further this same view will be returned to you through recycling and guess what it will be checked again, remember it was the same first view you used, this is reason for your problem for founding a unchecked view checked.
Now to your resolution, maintain a separate list somewhere globally to hold on checked index, and from your adapter just check or uncheck the checkbox before sending it to list depending about its index presence in maintained list. this will resolve your probelm.
Please try something like this.
-- Have a static HashMap on your main activity.
--Implement two methods over you activity, AddSelection and RemoveSelection
--On your adapter, implement onCheckedStateChanged listener and, perform either addition or deletion from the map, based on position in list.
In Your Activity:
private static HashMap<Integer,Integer> selectedPositions = new HashMap<Integer,Integer>();
public static boolean isSelected(int position){
System.out.println("#### Position: " + position + " Value: " + selectedPositions.containsKey(position));
return selectedPositions.containsKey(position);
}
public static void addSelection(int position){
System.out.println("#### Puttin Position: " + position);
selectedPositions.put(position,position);
}
public static void removeSelection(int position){
System.out.println("#### Removing Position: " + position);
selectedPositions.remove(position);
}
In your adapter:
checkbox.setChecked(MainActivity.isSelected(position));
checkbox.setOnCheckedChangeListener(new OnCheckedChangeListener(){
#Override
public void onCheckedChanged(CompoundButton view, boolean state) {
// TODO Auto-generated method stub
if(state == true){
MainActivity.addSelection(position);
}else{
MainActivity.removeSelection(position);
}
}
});
thats it now, whenever you want iterate through the hasmap for finding out selected items, make sure you clear it when job is done, else u will end up piling selected positions again and again.

Related

Get spinner selected item inside RecyclerView

I have added Spinner inside RecyclerView , when i am trying to get spinner selected item data, its getting another/wrong position data, any one suggest me to get correct selected item and position from Spinner onItemSelected
Here is my code
#Override
public void onBindViewHolder(final QuestionHolder holder, final int position) {
if (position % 2 == 1)
holder.itemView.setBackgroundColor(Color.parseColor("#F8F8F8"));
adapter = new ArrayAdapter<Option>(binding.getRoot().getContext(),
R.layout.item_spinner, questionList.get(position).getOptions());
adapter.setDropDownViewResource(R.layout.item_spinner);
binding.optionSpinner.setAdapter(adapter);
binding.serialNo.setText((position + 1) + ".");
binding.setQuestion(questionList.get(position));
binding.executePendingBindings();
binding.optionSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
#Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
Toast.makeText(holder.itemView.getContext(), position+" : "+binding.optionSpinner.getSelectedItem().toString(), Toast.LENGTH_SHORT).show();
spinnerData.setSelectedData(position, binding.optionSpinner.getSelectedItem().toString());
}
#Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
}
I think you have to try this
showSpinnerList.setOnItemSelectedListener(new
AdapterView.OnItemSelectedListener() {
#Override
public void onItemSelected(AdapterView<?> parent, View view, int
position, long id) {
// On selecting a spinner item
String item = parent.getItemAtPosition(position).toString();
}
#Override
public void onNothingSelected(AdapterView<?> parent) {
// todo for nothing selected
}
});
Check this, can be helpful and may fix your problem. If this won't fix your problem at least you get rid of Lint error. Lint error “Do not treat position as fixed; only use immediately…”. So everywhere you are using final int position** change it to getAdapterPosition();
The documentation of RecyclerView.Adapter.onBindViewHolder()
states:
Note that unlike ListView, RecyclerView will not call this method
again if the position of the item changes in the data set unless the
item itself is invalidated or the new position cannot be determined.
For this reason, you should only use the position parameter while
acquiring the related data item inside this method and should not keep
a copy of it. If you need the position of an item later on (e.g. in a
click listener), use getAdapterPosition() which will have the updated
adapter position
So, technically items may be re-arranged and binding will not be
necessary because items are not invalidated yet. The position
variable received holds true only for the scope of bind function and
will not always point to correct position in the data set. That's why
the function getAdapterPosition() must be called everytime updated
position is needed.
IMHO, mLastPosition = holder.getAdapterPosition(); is still
potentially wrong. Because item may be re-arranged and mLastPosition
still points to old position.
About why lint is silent, perhaps Lint rule is not that thorough. Its
only checking whether position parameter is being copied or not.

Strange RecyclerView Checkbox onCheckChanged Behavior

I am working where I have checkboxe inside RecyclerView items. I am aware of some of its issues. But, with only 10 items and no scrolling wrong listener is called.
I have something like this:
private class MyAdapter extends RecyclerView.Adapter<MyViewHodler> {
ArrayList<String> items;
ArrayList<String> checkedList = new ArrayList<>();
...
public void onBindViewHolder(final MyViewHolder holder, int position) {
String item = items.get(position);
String check = checkedList.get(item);
if(check != null)
holder.vChecked.setChecked(true);
else
holder.vChecked.setChecked(false);
....
holder.vChecked.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
if (isChecked) {
checkedList.add(item);
} else {
checkedList.remove(item);
}
}
}
}
Now, strangely this checkedChange calls are made on different views. I have a list of only 10 items and are fit in the recyclerview, no scrolling. I externally change the data (items and checked list), set the new data and calling myAdapter.notifyDatasetChanged()
This call removes one more item from the checked state.
For example, I have 10 items all checked. I uncheck 1 item, and doing some other calculations based on that and calling myAdapter.notifyDatasetChanged(). It also unchecks one more element from the other 9 items.
So even without scrolling why onCheckedChange listener is called for the wrong view?
I have found out the problem. I thought onBindViewHolder is called for every view to change the data. But I was wrong.
notifyDatasetChanged() recycles all the view holders on screen and from the recycled view holder pool randomly viewholders are picked to bind the new data. So when I called the notifyDatasetChanged, the viewholder was used at the different position, and thus changed one more item.
So, I removed the listener to null in onViewRecycled method of the recycler view. Like this:
#Override
public void onViewRecycled(MyViewHolder holder) {
super.onViewRecycled(holder);
holder.vChecked.setOnCheckedChangeListener(null);
}
This, did solved the issue. I was aware of this, but I thought viewholders are reused for the same position if there is no scrolling. But onDatasetChanged recycles all currently used view holders and uses it again from the random pool.
This line: String item = items.get(item);
It bothers me. Shouldn't it be String item = items.get(position);
Edit: If you change one position of the dataset, call notifyItemChanged. Don't notify the whole dataset unless it has changed entirely. Could be this.
Edit 2: Well then the problem is not that onCheckedChanged is being called on the wrong views. It is called in all of the views because you always change the checkbox state at onBindViewHolder

How to disable all elements (also the ones not currently visible in the screen) in a listview?

I have a ListView with a set of elements. When I click one of the I would like to disable all the others. If I click again on the selected item all the other items are enabled again.
I tried the different proposed solution without success. I hope someone can help me.
This is my code:
//action to take when a presentation is selected
listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, final View view,
int position, long id) {
//disable the other items when one is selected
if(position!=selectedPresentation){
for(int i=0;i<parent.getCount();i++){
if(i!=position)//disable all the items except the select one
parent.getChildAt(i).setEnabled(false);
}
selectedPresentation=position;
}else if(selectedPresentation==position){
//enable again all the items
for(int i=0;i<parent.getCount();i++){
parent.getChildAt(i).setEnabled(true);
}
selectedPresentation=-1;
}
Where selectedPresentation is a global variable storing the selected item. If no item is selected its value is -1.
Thank you for your help!
Make your own subclass of ArrayAdapter that has AreAllItemsEnabled() return false, and define isEnabled(int position) to return false for a given item in your the ones you want to disable.
In your custom ArrayAdapter overide isEnabled method as following
#Override
public boolean isEnabled(int position) {
return false;
}
other option
Don't implement onItemclickListener. This will not give you any update of item click. Only register onClick listener to views.
You may take another object of the List(Arraylist) which is populating elements in listview then on click copy the corresponding position data to new arraylist you made and then notifyDataSet and when you click again you populate listview with original list so it will apear again....Just do this trick it might work for you
parent.getChildeAt() only work for visible Item.
you should change something in adapter.
if make custom adapter then you can do some thing like #ṁᾶƔƏň ツ answer, but if you use default adapter you can change adapter's arraylist that before you pass it to adapter.
put boolean in it (in arraylist) and in on item click true/false it for all item.
I wrote this solution and seems it works fine!
listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, final View view,
int position, long id) {
//disable the other items when one is selected
Toast.makeText(context, "onClick called",Toast.LENGTH_SHORT).show(); // this will flash up twice
if(position!=selectedPresentation){
selectedPresentation=position;
for(int i=0;i<adapter.getCount();i++){
if(i!=position)//disable all the items except the select one
adapter.isEnabled(i);
}
}else if(selectedPresentation==position){
//enable again all the items
selectedPresentation=-1;
for(int i=0;i<adapter.getCount();i++){
adapter.isEnabled(i);
}
}
And in my adapter I wrote:
public boolean isEnabled(int position) {
if(selectedPresentation==-1 || selectedPresentation==position)
return true;
return false;
Now my concern is how to show the items in the listView as disabled

listview getting new instance when swiping the listview

I have one listview in my application,it contains two rows one for task and another one for alarm,date,severity. Initially first row of the list item only displayed for all list item and the second one is invisible. When i click the list item the second row displayed for that item as well as click another list item at that time the above list item closed that second row. Its working fine for me...My problem is if i open one list item and then swipe the listview at then i click the another list item at that time the above one cannot be closed because the above list item instance will be chnaged.please any one help me how to solve this problem...
int lastselectedPosition == -1
#Override
public void onItemClick(AdapterView<?> arg0, View view, int position,
long id) {
TextView textviewDate=(TextView)view.findViewById(R.id.taskTimeidDaytoDay);
selectedtaskDate=textviewDate.getText().toString().trim();
if (lastselectedPosition == -1) {
Log.i(TAG,"Loopif:"+lastselectedPosition);
TextView twTaskTime = (TextView) view
.findViewById(R.id.taskTimeidDaytoDay);
TextView twSeverity = (TextView) view
.findViewById(R.id.severityidDaytoDay);
TextView twAlarm = (TextView) view
.findViewById(R.id.alarmidDaytoDay);
twAlarm.setVisibility(view.VISIBLE);
twSeverity.setVisibility(view.VISIBLE);
twTaskTime.setVisibility(view.VISIBLE);
lastselectedPosition = position;
lastSelectedItem = arg0.getChildAt(position);
} else {
// Log.i(TAG,"LoopElse:"+lastselectedPosition);
lastSelectedItem.findViewById(R.id.taskTimeidDaytoDay)
.setVisibility(lastSelectedItem.GONE);
lastSelectedItem.findViewById(R.id.severityidDaytoDay)
.setVisibility(lastSelectedItem.GONE);
lastSelectedItem.findViewById(R.id.alarmidDaytoDay).setVisibility(
lastSelectedItem.GONE);
if (lastselectedPosition != position) {
view.findViewById(R.id.taskTimeidDaytoDay).setVisibility(
view.VISIBLE);
view.findViewById(R.id.severityidDaytoDay).setVisibility(
view.VISIBLE);
view.findViewById(R.id.alarmidDaytoDay).setVisibility(
view.VISIBLE);
lastselectedPosition = position;
lastSelectedItem = arg0.getChildAt(position);
} else {
lastselectedPosition = -1;
lastSelectedItem = null;
}
}
GetView():
#Override
public View getView(int position, View view, ViewGroup parent) {
Log.i("XXXX", "Inside getView");
final DaytoDayTaskGetterSetter objDaytoDaygetset=getItem(position);
TextView textviewTask;
TextView txtviewAlarm ,txtviewTaskTime ,txtviewSeverity;
Log.i(TAG,"InsideGetView:"+position);
LayoutInflater inflater=(LayoutInflater)context.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
if(view==null)
{
view=inflater.inflate(R.layout.daytodaylistlayout,null);
}
Log.i("XXXX", "before first test");
textviewTask=(TextView)view.findViewById(R.id.tasknameidDaytoDay);
txtviewAlarm=(TextView)view.findViewById(R.id.alarmidDaytoDay);
txtviewSeverity=(TextView)view.findViewById(R.id.severityidDaytoDay);
txtviewTaskTime=(TextView)view.findViewById(R.id.taskTimeidDaytoDay);
return view;
}
In first i click the "gdfgdtet" list item it show another row and then i click the second list item "dfgsdgsd" at that time the above list item "gdfgdtet" closed the second row.This is a normal output.Suppose if i open the "gdfgdtet" list item and then swipe the listview at that time both of "gdfgdtet" "dfgsdgsd" will be opened and crashed...because the above one list item reference changed when i am swiping please how to solve this problem...
I'll try to provide you a good answer that explains why you are having this problems, but the general idea is that you have to see this video - http://www.youtube.com/watch?v=wDBM6wVEO70
please take my words kindly - you don't seems to understand what ListView + BaseAdapter recycling mechanism is all about, and I strongly recommend you see the full video I linked you to, and read more about that.
in general, the specific problem in your code is that you are holding reference to listview item (lastSelectedItem), then trying to use it latter assuming it's still representing same list item. that's wrong. in that stage (after scrolling) the view already been recycled to represent another item in the list (based on the adapter implementation).
listView's number of childs is not the size of adapter.getCount()!!!!!!!!
listViews's number of childs = number of visible list items on screen + 1 + headers + footers
let's say you have the 5 first items visible on screen, then you are scrolling down. when you see the 7 item you actually see the same view instance that used to show the first list item and been recycled.
getView will call in this stage with convertView != null and position in the adapter to let you reuse the item by putting new values such different text/image to the same instance
this mechanism provides ability to display list of "infinite" number of items in the list, and holding in memory only a few number of views. imagine that you have list of 5000 items in the list, and each one of them have different view instance - you would get outOfMemory exception in a sec!
complete explanation about that would be hard to write in stackoverflow answer's context.
it just too long trying to explain one of the most important and complex UI components in android, but this links would be a good start:
http://www.youtube.com/watch?v=wDBM6wVEO70
How ListView's recycling mechanism works
http://mobile.cs.fsu.edu/the-nuance-of-android-listview-recycling-for-n00bs/
if you are interstead in "quick" fix for your specific problem, the solution would be:
hold in the data structure represents your list item additional field indicating if it in "close" or "open state. when item been clicked - change the data accordinly and call notifyDatasetChanged(). inside the getView() check if item is open or close and populate it accordinly
by the way - it's not only "quick fix" solution, but also the right thing to do anyway
You should pay attention to Tal Kanel's answer and consider this one to be an extension to it. His advice will help you in the long run.
Add a boolean field to DaytoDayTaskGetterSetter class:
public class DaytoDayTaskGetterSetter {
....
....
boolean open;
public DaytoDayTaskGetterSetter (.., .., boolean o) {
....
....
open = o;
}
....
....
public boolean shouldOpen() {
return open;
}
public void setOpen(boolean o) {
open = o;
}
}
In your getView(), check if the object has its open value set:
DaytoDayTaskGetterSetter obj = (DaytoDayTaskGetterSetter) getItem(position);
if (obj.shouldOpen()) {
// Set visibility to true for the items
} else {
// Set visibility to false for the items
}
On list item click, traverse the list and set open for all list items to false. Use the position to retrieve DaytoDayTaskGetterSetter and set its open to true:
#Override
public void onItemClick(AdapterView<?> arg0, View view, int position, long id) {
for (DaytoDayTaskGetterSetter obj : listContainingObjects) {
obj.setOpen(false);
}
DaytoDayTaskGetterSetter clickedItem = (DaytoDayTaskGetterSetter)
yourAdapter.getItem(position);
clickedItem.setOpen(true);
yourAdapter.notifyDataSetChanged();
}
Edit 1:
#Override
public void onItemClick(AdapterView<?> arg0, View view, int position, long id) {
DaytoDayTaskGetterSetter clickedItem = (DaytoDayTaskGetterSetter)
yourAdapter.getItem(position);
if (clickedItem.shouldOpen()) {
clickedItem.setOpen(false);
} else {
for (DaytoDayTaskGetterSetter obj : listContainingObjects) {
obj.setOpen(false);
}
clickedItem.setOpen(true);
}
yourAdapter.notifyDataSetChanged();
}

How to identify reused widgets in ListViews

I have what I consider to be a strange dilemma, although YMMV.
I'm using a layout file that describes each line/row in a ListView (nothing too exotic about that). I have an id assigned to each one, such as:
android:id="#+id/checkBox1"
android:id="#+id/checkBox2"
android:id="#+id/checkBox3"
android:id="#+id/contactLabel" // a TextView
Now this doesn't seem to make sense, as these ids should be unique, so what is the id of the second
row? That is, if "row 1" honors the specified ids of checkbox1, checkbox2, checkbox3, and contactLabel, what would the "row 2" ids be?
I'm curious, but also I need to know because I want to save the values of the checkboxes to a SharedPreferences object.
Who has a clue about how to get around this?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Update
The first thing I need to solve is how to respond to a click on the ListView. This is my current conundrum related to all of this:
ListView doesn't know it's been clicked, or won't tell me
I've added this event handler to my ListActivity:
#Override
protected void onListItemClick(ListView l, View v, int position, long id) {
String item = (String) getListAdapter().getItem(position);
Toast.makeText(this, item + " selected", Toast.LENGTH_LONG).show();
}
...but it's not getting called. I click on the Contacts that display, but no go.
I also tried it this way:
lv.setOnItemClickListener(new OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
Toast.makeText(getApplicationContext(),
"Click ListItem Number " + position, Toast.LENGTH_LONG)
.show();
}
});
...still no joy...I put breakpoints on both "Toast" lines, and they never get reached.
I read here:
http://www.vogella.de/articles/AndroidListView/article.html#listsactivity_layout
...that, "In case you need more the just a ListView in your Activity, you can use you own layout for
ListActivity."
...which I do, because I add a header and a footer in addition to the listview.
It goes on to say, "In this case your layout must have an ListView element with the android:id
attribute set to #android:id/list."
So I added this to my layout:
...but it makes no difference one way or the other.
The ID's for the items within the ListView widget are referenced through their parent view when you inflate it in your getView() method.
To elaborate, you would have something like this is you ListView adapter.
public View getView(int position, View convertView, ViewGroup viewGroup) {
if (convertView == null) {
convertView = LayoutInflater.from(ctx).inflate(R.layout.list_view_item, null);
}
Nown, a new view instance exists as the convertView. You can access your widgets using covertView.findViewById(R.id.checkBox1), convertView.findViewById(R.id.checkBox2), etc.
Each of these views is a child of your ListView. You can reference each individual view from your ListView using the getChildCount() and getChildAt() methods from the ListView object.
However, since it is recommended to use the convertView view to recycle views, in that case you will only have reference to the views on screen at a time.
Also, with regards to the SharedPreferences, all the views in your ListView are populated by an Adapter subclass which would be the actual object that puts the values in the Checkbox and TextView widgets. This Adapter has a dataset that you provide it. Why not reference the values from the dataset directly, instead of trying to find them from the list items which are populated from the dataset in any case ? You can write to the dataset from the ListView when someone clicks a CheckBox so you have an easy ordered reference to all the items in the ListView.
UPDATE: Added dummy source code
OK. Let's start with a hypothical list. We want to display say five items on the list. For simplicity, I'll assume each has a TextView and a Checkbox. So my container class is:
class Item {
String textView;
boolean checked;
}
Now in my Activity where I want to display this list, I put an ArrayList of items (you can use just about any datastructure) as a class variable. Then I get the ListView reference and assign it an adapter.
class MyActivity extends Activity {
ArrayList<Item> listItems;
.....
onCreate(Bundle icicle) {
.....
ListView listView = (ListView) findViewById(R.id.listView1); // this will be you list view
MyAdapter listAdapter = new MyAdapter();
listView.setAdapter(listAdapter);
....
// Rest of your Activity
....
MyAdapter extends BaseAdapter {
int getItemCount() {
return listItems.size();
}
Item getItem(int position) {
return listItems.get(position);
}
View getView(int position, View convertView, ViewGroup parent) {
// Here's the important part
Item currentItem = listItems.getItem(position); // Since the array is a class variable, you can do either get or getItem
..... // do the standard individual item inflating ....
checkbox = convertView.findViewById(R.id.checkbox);
checkbox.OnItemSelectedListener( new OnItemSelectedListener() { // or whatever listener there should be... I didn't check
... // do whatever...
currentItem.setChecked(true);
}
When you want to retrieve what items were clicked, just iterate through the Item class and find which ones are true or you perform whatever action you want within the Listener since you have a reference identifying individual members of the ListView dataset (here listItems ArrayList).
Apologies for any errors. Didn't do any checking.

Categories

Resources