I have a listview with a checked textview and two textviews,however, my getView method keeps changing the listview items while scrolling, the values and checkbox states are both saved into sqlite database. I tried every possible solution and spent 4 hours trying to fix that.
Any help appreciated.The only solution that worked was setting convertview to null at beginning of getView() which lags the listview.
GOAL:to make listview display items properly without changing its positions randomly.
Final working code for anyone in need:
#Override
public View getView( final int position, View convertView, ViewGroup parent) {
viewHolder = null;
if(convertView == null){
convertView = inflater.inflate(R.layout.sin_item,null);
viewHolder = new HolderCo();
viewHolder.box = (CheckBox)convertView.findViewById(R.id.coco);
viewHolder.subject = (TextView)convertView.findViewById(R.id.subject_com);
viewHolder.date = (TextView)convertView.findViewById(R.id.date_co);
convertView.setTag(viewHolder);
}
else{
viewHolder = (HolderCo)convertView.getTag();
}
viewHolder.position = position;
viewHolder.box.setText(list.get(viewHolder.position).getWhats());
viewHolder.subject.setText(list.get(viewHolder.position).getSubject());
if(list.get(viewHolder.position).isSelected()) {
viewHolder.box.setOnCheckedChangeListener(null);
viewHolder.box.setChecked(true);
viewHolder.box.setPaintFlags(viewHolder.box.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
}else{
viewHolder.box.setOnCheckedChangeListener(null);
viewHolder.box.setChecked(false);
viewHolder.box.setPaintFlags(viewHolder.box.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG));
}
if(dator.equals("d"))
viewHolder.date.setText(list.get(viewHolder.position).getDay()+"/"+list.get(viewHolder.position).getMonth()+"/"+list.get(viewHolder.position).getYear());
if(dator.equals("m"))
viewHolder.date.setText(list.get(viewHolder.position).getMonth()+"/"+list.get(viewHolder.position).getDay()+"/"+list.get(viewHolder.position).getYear());
if(dator.equals("y"))
viewHolder.date.setText(list.get(viewHolder.position).getYear()+"/"+list.get(viewHolder.position).getMonth()+"/"+list.get(viewHolder.position).getDay());
viewHolder.box.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if(buttonView.isChecked()) {
list.get(position).setSelected(true);
db.updateState(list.get(position),true);
buttonView.setPaintFlags(buttonView.getPaintFlags()| Paint.STRIKE_THRU_TEXT_FLAG);
if(PreferenceManager.getDefaultSharedPreferences(ctx).getBoolean("add_mark_dialog",true))
buttonView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
dialoging(viewHolder.position);
}
});
}else{
buttonView.setOnClickListener(null);
list.get(position).setSelected(false);
db.updateState(list.get(position), false);
buttonView.setPaintFlags(buttonView.getPaintFlags()&(~Paint.STRIKE_THRU_TEXT_FLAG));
}
}
});
return convertView;
}
By doing this:
viewHolder.box.setTag(position);
viewHolder.date.setTag(position);
viewHolder.subject.setTag(position);
you set the tags to the views to the first position they were created with.
So when getView() is called with non-null convertView (previously recycled), the tags in its viewHolder still point to that position.
Move these setTag() calls outside if(), to set new position to recycled view.
BTW I would rather replace all this with
viewHolder.position = position; // outside if()
and using it everywhere you use (Integer)x.getTag()
UPDATE: Also you have to do this:
viewHolder.box.setOnCheckedChangeListener(null);
before this:
viewHolder.box.setChecked(...);
Because otherwise it can trigger previous listener which most likely you don't want.
You're updating the view conditionally with if conditions. You need to provide corresponding else blocks where you reset the view to their default values.
For example,
if(dator.equals("d"))
viewHolder.date.setText(...);
if(dator.equals("m"))
viewHolder.date.setText(...);
if(dator.equals("y"))
viewHolder.date.setText(...);
needs to be something like
if(dator.equals("d"))
viewHolder.date.setText(...);
else if(dator.equals("m"))
viewHolder.date.setText(...);
else if(dator.equals("y"))
viewHolder.date.setText(...);
else
viewHolder.date.setText("some default value");
Similarly reset defaults in viewHolder.box.setPaintFlags().
The reason is that ListView views are recycled. Recycled views are not in their pristine state like they were immediately after inflation. Instead they will be in a state they were before they were recycled, possibly containing data from the list row previously using that view.
Related
I know about recycling rows in a listview. I have a listview with toggle buttons. I'm saving the states of the toggle buttons in a SparseBooleanArray as lot of posts suggest. My problem is the toggle button row gets on and off on scroll anyway. In my code I have saved the state of togglebutton and their respective position in the array and them I get their states from the same array.
Thanks.
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
View row = convertView;
final ViewHolderBrandAvailability holder;
if(row == null){
dbHelper = new DBHelper(con);
database = dbHelper.getWritableDatabase();
LayoutInflater mInflater = (LayoutInflater) con.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
//viewHolderBrandAvailability = new ViewHolderBrandAvailability();
row = mInflater.inflate(R.layout.brand_availability_listview, parent, false);
holder = new ViewHolderBrandAvailability();
holder.brandNameTextView = (TextView) row.findViewById(R.id.brandAvailabilityNameText);
holder.radioGroup = (ToggleButton) row.findViewById(R.id.brandAvailable);
/*viewHolderBrandAvailability.unavailableRadioBtn = (RadioButton) convertView.findViewById(R.id.brandUnavailable);*/
row.setTag(holder);
}else {
holder = (ViewHolderBrandAvailability) row.getTag();
}
holder.radioGroup.setTag(position);
holder.radioGroup.setChecked(mCheckStates.get(position, false));
holder.radioGroup.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
if(isChecked){
selectedBrandStatus.put(((BrandAvailability)list.get(position)).getBrand_id(), "Yes");
}else {
selectedBrandStatus.put(((BrandAvailability)list.get(position)).getBrand_id(), "No");
}
mCheckStates.put((Integer) compoundButton.getTag(), isChecked);
}
});
String brandTitle = ((BrandAvailability)list.get(position)).getBrand_title();
holder.brandNameTextView.setText(brandTitle);
//holder.radioGroup.setChecked();
return row;
}
static class ViewHolderBrandAvailability {
private TextView brandNameTextView;
private ToggleButton radioGroup;
//RadioButton unavailableRadioBtn;
//int position;
}
From the code you've posted, I see nothing that indicates a problem with the checked state of the ToggleButton changing on scroll. In fact, I created my own adapter to test out your code, and it worked perfectly fine for me.
Often, people run into a similar issue because they don't realize that the OnCheckedChangeListener they set in one call to getView() will stick around during the next call to getView(), and so the setChecked() call winds up triggering it. However, in your case, you're using compoundButton.getTag() to determine the index into the mCheckStates array, so there's no problem here.
That being said, you are using the getView() position argument for other operations in your listener, and this will cause the issue I described above.
selectedBrandStatus.put(((BrandAvailability)list.get(position)).getBrand_id(), "Yes");
Imagine getView() is called for the very first time, for position 0. Your code runs, and assigns the listener. You wind up checking the ToggleButton at position 0, so you call selectedBrandStatus.put(list.get(0).getBrand_id(), "Yes"). Now you scroll the list until this view is recycled: getView() is called again and this view is passed as convertView. Let's call this position 20. Your listener is still in place, so when the ToggleButton is unchecked by holder.radioGroup.setChecked(mCheckStates.get(20, false)), it is triggered again. Now it will overwrite the previous "Yes" with "No", since the listener was created using position = 0.
You can just change all of the indexes in your listener to be (Integer) compoundButton.getTag() and that will fix this problem.
I have a custom row_item for ListViews with an image, a pair of TextViews and a checkBox.
From what I have understood, as checkBox is a focusable element it steals the focus from the listView so the OnListItemClicked is never fired unless I set every row_item as not clickable and I block descendants focusability.
Then for managing the checkBoxes changes I set in my adapter getView method an "OnCheckedChangeListener" for my checkBoxes.
Is this a bad way of doing this? (Because I am creating new Listeners every time getView is called)
Is there an other way of doing the same?
I attach some code so it's easier to understand what I mean.
getView method: (inside arrayAdapter)
#Override
public View getView(int position, View convertView, ViewGroup parent) {
/** recycling views */
View row = convertView;
PregoHolder holder = null;
if(row == null)
{
LayoutInflater inflater = ((Activity)context).getLayoutInflater();
row = inflater.inflate(layoutResourceId, parent, false);
holder = new PregoHolder();
holder.imgIcon = (ImageView)row.findViewById(R.id.thumbnail);
holder.txtTitle = (TextView)row.findViewById(R.id.txtTitle);
holder.txtNews = (TextView)row.findViewById(R.id.txtNews);
holder.chBox = (CheckBox) row.findViewById(R.id.checkBox);
row.setTag(holder);
}
else
{
holder = (PregoHolder)row.getTag();
}
Prego Prego = data[position];
holder.txtTitle.setText(Prego.title);
holder.imgIcon.setImageResource(Prego.icon);
holder.txtNews.setText(Prego.news);
holder.chBox.setChecked(Prego.checked);
/** wiring up Listeners...
* (works fine but we are creating new listener each time)
* (Done like this because of the custom list view focusable issue)
*/
holder.chBox.setOnCheckedChangeListener(new OnCheckedChangeListener(){
#Override
public void onCheckedChanged(CompoundButton arg0, boolean checked) {
/** Getting the view position in the ListView */
ListView parent = (ListView)(arg0.getParent()).getParent();
int pos = parent.getPositionForView(arg0);
if (checked){
checkedPregons[pos] = true;
pregonsChecked++;
}
else if (!checked){
checkedPregons[pos] = false;
pregonsChecked--;
}
Toast.makeText(context, pregonsChecked+" is/are checked", Toast.LENGTH_SHORT).show();
}
});
row.setOnClickListener(new OnClickListener(){
#Override
public void onClick(View v) {
//mMode = ((Activity)context).startActionMode(new PregonsActionModes());
ListView parent = (ListView)v.getParent();
int pos = parent.getPositionForView(v);
Toast.makeText(context, "getView should show prego "+pos,Toast.LENGTH_SHORT).show();
}
});
return row;
}
Thanks in advance.
i suppose there could be a better way to go about it, but this isn't something i'd lose sleep over. Your method is wildly implemented by many android developers new and old and it's just a plain natural way to go about it. Moreover, keep in mind that the ListView only has as many rows as you can see on the screen at one time (and constantly is calling getView when you scroll), so you're talking about 8 - 12 made objects at any given time (obviously totally guessing), so your bottle-neck won't be here. In fact, depending on where the objects that I need to modify are, i don't even think about it.
But using OnCheckedChangeListener in a ListView is something i'd heavily warn against. like i said, getView is called for every row as it's being scrolled and made. This checked changed listener is being fired just as it's being made and notwithstanding there's absolutely no guarantees that getView is being called just once for each row, in fact i guarantee otherwise. Thus those code blocks are probably getting fired up the wazoo, even though you probably only intended for if the CheckBox box tick was handled manually. Put a relatively more intensive command like notifyDataSetChanged() and you'll see your ListView lock up from this phenomenom.
The solution is to simply use a checkbox onClickListener instead and check for isChecked() inside.
I had a ListView with images that are loading by using ImageLoader(framework) and checkbox for each image.I had a adapter that is extending BaseAdapter for this ListView,when i'm checking one of the checkbox and scrolling it then i see other checkboxes which are automatically checked. Is there any way to resolve my issue. Please someone give me a clue or example.
This is because the rows are being re-used, so you need to set some attribute for each row telling it whether that row is checked or not. And then make "if and else" statement in your getView() to look if that row is checked or not, and if it is just check it, otherwise leave it unchecked.
I would suggest you use an array of booleans where you keep your checked state for every position in the list. ListView recreates list elements on scroll, so your checks could be messed up. This means that you have to check if boolean value for position of the element you're instantiating is true and then use checkBox.setChecked(true), or checkBox.setChecked(false).
Simple,
Set<String> bs = new HashSet(String);
String[] bid;
And write this in you getView method, Take it as a template. Not as a solution
public View getView(final int position, View convertView, ViewGroup arg2) {
row = convertView;
if (row == null)
{
LayoutInflater vi = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
row = vi.inflate(R.layout.your_listview,null);
}
check = (CheckBox) row.findViewById(R.id.your_checkboxid);
check.setOnCheckedChangeListener(new OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if(isChecked)
{
bs.add(bid[position]);
}
else
{
if(bs.contains(bid[position]))
{
bs.remove(bid[position]);
}
}
}
});
if(bs.contains(bid[position]))
{
check.setChecked(true);
}
else
{
check.setChecked(false);
}
return row;
}
Apparently my thinking has some flaw as it's not working correctly. Basically, I'm trying to solve this problem: listview with checkbox
In the answer, people suggested to create a global data structure to hold the state, which made sense. However, I thought if I'm using ViewHolder pattern, I could use the tag as the structure to store state information?
cbox.setOnClickListener(new OnClickListener() {
public void onClick(View v)
{
P tag = (P) ((View) v.getParent()).getTag();
if(tag.cbox.isChecked())
tag.cbox.setChecked(true);
else
tag.cbox.setChecked(false);
//tag.cbox.toggle();
Log.d("YoYo", Boolean.toString(tag.cbox.isChecked()));
}
});
The code above did not toggle my checkbox in the rows. What did I do wrong?
Update: Rather then toggle, manual if statement seemed to work. Though, I'm running into another problem, where the checked state mess up after I scroll to different places. Why is that? If I set the checked state in the tag.cbox, isn't the check state unique to that object only?
Update2: I follow other's suggestion and got a working version, but I'm still wondering why setTag/getTag not working?
Working:
public View getView(final int position, View view, ViewGroup parent)
{
Plurker tag = getItem(position);
if (view == null)
view = adapterLayoutInflater.inflate(R.layout.adapter, null);
tag.avatar = (ImageView) view.findViewById(R.id.imgAvatar);
tag.cbox = (CheckBox) view.findViewById(R.id.cBox);
tag.cbox.setOnCheckedChangeListener(new OnCheckedChangeListener()
{
public void onCheckedChanged(CompoundButton btn, boolean isChecked)
{
int pos = position;
Plurker p = getItem(pos);
p.isChecked = isChecked;
Log.d("PLURK", "Listener:" + p.toString());
}
});
tag.update(getItem(position));
Log.d("PLURK", tag.cbox.getText() + ":" + Integer.toString(position));
return view;
}
Not working:
public View getView(final int position, View view, ViewGroup parent)
{
Plurker tag = getItem(position);
if (view == null)
{
view = adapterLayoutInflater.inflate(R.layout.adapter, null);
tag.avatar = (ImageView) view.findViewById(R.id.imgAvatar);
tag.cbox = (CheckBox) view.findViewById(R.id.cBox);
tag.cbox.setOnClickListener(new View.OnClickListener() {
public void onClick(View arg0)
{
int pos = position;
Plurker p = getItem(pos);
p.isChecked = !p.isChecked;
Log.d("PLURK", "Listener:" + p.toString());
}
});
view.setTag(tag);
}
else
tag = (Plurker) view.getTag();
tag.update(getItem(position));
Log.d("PLURK", tag.cbox.getText() + ":" + Integer.toString(position));
return view;
}
In the "not working" version, I need to use onClick instead of onCheckedChanged event, because when the view from hidden to reappear, it called the event listener, so it would falsely triggered.
Are you inflating a new view each time or making use of the convertView that is passed in?
Normally the Adapter tries to recycle views, only creating enough to provide smooth scrolling. The existing recycled views are passed in as convertView. You can either inflate and return a new view every time (expensive) or just re-setup the convertView (if it exists) based on position. If recycling you need to re-set all the view attributes, as there is no guarantee that the recycled view you get is the same one used for this position in the past.
It sounds like your bug is that you are not correctly re-setting all the attributes of the recycled view (convertView) to match the data for the current position.
I've got a ListView, each of item of which contains a ToggleButton. After I toggle it and then scroll up or down, the ListView is recycling the Views and so some of the others are mirroring the checked state of the ToggleButton. I don't want this. How can I prevent it?
Add this two methods to your Adapter.
#Override
public int getViewTypeCount() {
return getCount();
}
#Override
public int getItemViewType(int position) {
return position;
}
Android recycles list items for performance purposes. It is highly recommended to reuse them if you want your ListView to scroll smoothly.
For each list item the getView function of your adapter is called. There, is where you have to assign the values for the item the ListView is asking for.
Have a look at this example:
#Override
public View getView(int position, View convertView, ViewGroup parent)
{
ViewHolder holder = null;
if ( convertView == null )
{
/* There is no view at this position, we create a new one.
In this case by inflating an xml layout */
convertView = mInflater.inflate(R.layout.listview_item, null);
holder = new ViewHolder();
holder.toggleOk = (ToggleButton) convertView.findViewById( R.id.togOk );
convertView.setTag (holder);
}
else
{
/* We recycle a View that already exists */
holder = (ViewHolder) convertView.getTag ();
}
// Once we have a reference to the View we are returning, we set its values.
// Here is where you should set the ToggleButton value for this item!!!
holder.toggleOk.setChecked( mToggles.get( position ) );
return convertView;
}
Notice that ViewHolder is a static class we use to recycle that view. Its properties are the views your list item has. It is declared in your adapter.
static class ViewHolder{
ToggleButton toggleOk;
}
mToggles is declared as a private property in your adapter and set with a public method like this:
public void setToggleList( ArrayList<Boolean> list ){
this.mToggles = list;
notifyDataSetChanged();
}
Have a look at other custom ListView examples for more information.
Hope it helps.
You could use a HashMap to save your buttons state:
private Map<Integer,Boolean> listMapBoolean = new HashMap<Integer,Boolean>();
toggleButton.setOnCheckedChangeListener(new OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
listMapBoolean.put(position, true);
} else {
listMapBoolean.put(position, false);
}
}
});
and after inflating the view you read the HashMap to see if it was checked or not:
for (Entry<Integer, Boolean> entry : listMapBoolean.entrySet()) {
if (entry.getKey().equals(i)) {
if(entry.getValue()) {
System.out.println("ToggleButton is checked!");
} else {
System.out.println("ToggleButton is not checked!");
}
}
}
Not sure if it helps in your way. I had also problems with recycling my EditText in my ListView.
This would make it so slow for large lists. But inside getView(), you can use:
if (listItemView == null || ((int)listItemView.getTag()!=position)) {
listItemView = LayoutInflater.from(context).inflate(R.layout.edit_text_list_item,
parent, false);
}
listItemView.setTag(position);
// set inner Views data from ArrayList
...
The tag is an Object that is associated with the View. And you check whenever you recycle it if you can recycle it or not. This makes each list item be inflated and nothing will be recycled.
This also should prevent deleting text from EditText inside the ListView and also prevent images from being reordered or messed up if your ListView has images in it.
May be you should try creating your own list view with scroll view and a container that holds the children that are added to the container programatically. set the tag for identifying the child or you could use the order of the child for that