收录日期:2020/10/23 10:22:17 时间:2010-09-07 02:38:10 标签:java,android,listview,checkbox

I have a ListView, and within each list item I have some TextViews and a CheckBox. When I check a CheckBox and my onCheckedChangeListener fires, everything works as it should. However, random other checkboxes get checked once one is checked. Here is an example.

If I click on the first CheckBox: 8 is checked. 15 is checked. 21 is checked. 27 is checked. 33 is checked. 41 is checked. Then if I scroll all the way up, none are checked until 6. The next being 13.

Basically... what is going on?

It seems that you are reusing the convertView that is passed on the getView() method that you implement.

Android will try to use the same view for different items in a ListView. You will either need to (1) uncheck/check manually the checkbox that is inside the returned item (always call setChecked before returning on getView or (2) don't use convertView, but return a new View from getView.

(1) is recommended, I think.

works fine for me

public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {

        final ViewHolder holder;
        final Season season = (Season) getGroup(groupPosition);
        if (convertView == null) {
            LayoutInflater vi = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = vi.inflate(R.layout.season, parent, false);
            holder = new ViewHolder();
            holder.title = (TextView) convertView.findViewById(R.id.season_title);
            holder.checkBox = (CheckBox) convertView.findViewById(R.id.season_check_box);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        holder.title.setText(season.getTitle());
        holder.checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                season.setChecked(isChecked);
                adapter.notifyDataSetChanged();
            }
        });

        holder.checkBox.setChecked(season.isChecked()); // position is important! Must be before return statement!
        return convertView;
    }

    protected class ViewHolder {
        protected TextView title;
        protected CheckBox checkBox;
    }

I was also facing a similar king of problem, so after lot of reading I solved this problem like this:

@Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder = null;
        if (convertView == null) {
            convertView = mInflater.inflate(R.layout.listview, null);
            holder = new ViewHolder();
            holder.nameView = (TextView)convertView.findViewById(R.id.textView1);
            holder.numberView = (TextView)convertView.findViewById(R.id.textView2);
            holder.cb = (CheckBox)convertView.findViewById(R.id.checkBox1);
            convertView.setTag(holder);                
        } else {
            holder = (ViewHolder)convertView.getTag();
        }
        holder.nameView.setText(mData.get(position).toString());
        holder.numberView.setText(mNumber.get(position).toString());
        holder.cb.setChecked(false);
        holder.cb.setTag(position);



        if(selected.indexOf(mNumber.get(position).toString()) >= 0) // Check whether this row checkbox was checked, for that we previously stored the textview text in selected variable
        {

        holder.cb.setChecked(true);
        }

        return convertView;
    }

}

Here what I am doing that on getView() I am unchecking all the checkboxes and checking again manually those which I need to be checked according to the textview it corresponds. So if the user scroll down after checking the first checkbox, all the checkbox in the view will get unchecked and if he again scrolls up then also all the checkboxes will be unchecked but then the one he clicked before will be again rechecked.

The problem can be solve easily by keeping CheckBox states. A self defined and explained complete Sample Class code for the same is as below_

 public class AreaDataAdapter extends ArrayAdapter<String> {
    private List<String> areaList;
    private Activity context;
    ArrayList<Boolean> positionArray;

    public AreaDataAdapter(Context context, int textViewResourceId,
            List<String> offersAreaList) {
        super(context, textViewResourceId, offersAreaList);
        // TODO Auto-generated constructor stub
        this.context=context;
        this.areaList = offersAreaList;

         positionArray = new ArrayList<Boolean>(offersAreaList.size());
            for(int i =0;i<offersAreaList.size();i++){
                positionArray.add(false);
          }

    }

    public AreaDataAdapter(Activity context, List<String> offersAreaList) {
        super(ShowOffersActivity.this, R.layout.filterlist_row, offersAreaList);
        this.context=context;
        this.areaList=offersAreaList;

         positionArray = new ArrayList<Boolean>(offersAreaList.size());
            for(int i =0;i<offersAreaList.size();i++){
                positionArray.add(false);
          }

    }

    public View getView(final int position, View convertView,ViewGroup parent) {
        View row=convertView;
        FilterViewHolder holder;
        if (row==null) {
            LayoutInflater inflater=getLayoutInflater();

            row=inflater.inflate(R.layout.filterlist_row, parent, false);
            holder = new FilterViewHolder();
            holder.filterCheckBox = (CheckBox)row.findViewById(R.id.filter_checkbox);
            holder.filterCheckBox.setTypeface(fontFace);

            row.setTag(holder);
        } else {
            //holder = (View) row;
            holder = (FilterViewHolder) row.getTag();

            /* When a listview recycles views , it recycles its present state as well as listeners attached to it.
             * if the checkbox was checked and has a onCheckedChangeListener set, both will remain a part of 
             * recycled view based on position. So it is our responsibility to reset all states and remove
             *  previous listeners.
             *  The listener was removed as below:-
             */
            holder.filterCheckBox.setOnCheckedChangeListener(null);

        }

            holder.filterCheckBox.setText(areaList.get(position));
            holder.filterCheckBox.setFocusable(false);
            holder.filterCheckBox.setChecked(positionArray.get(position));
            holder.filterCheckBox.setText(areaList.get(position));

            holder.filterCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {

                @Override
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    // TODO Auto-generated method stub
                    if (isChecked) {
                        //change state of concern item in 'positionArray' array to 'True'
                         positionArray.set(position, true);
                        //add the checked item in to area list which used to filter offers.
                        filterOffersByThisAreaList.add(areaList.get(position));

                    } else {
                       //change state of concern item in 'positionArray' array to 'True'
                         positionArray.set(position, true);
                        //remove the unchecked item in to area list which used to filter offers.
                        filterOffersByThisAreaList.remove(areaList.get(position));

                    }
                }
            });
        return row;
    }

}

Custom Row (filterlist_row) for the ListView Containing these CheckBoxs is as below_

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >

<CheckBox
    android:id="@+id/filter_checkbox"
    style="@style/CodeFont"
    android:button="@drawable/bg_custom_checkbox"
    android:text="Indian" />

<View
    android:layout_width="match_parent"
    android:layout_height="1dp"
    android:background="#c9c9c3" />

</LinearLayout>

customize CheckBox backgroung is as below_

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
 <item android:drawable="@drawable/checkbox_s" android:state_checked="true"></item>
 <item android:drawable="@drawable/checkbox_ns" android:state_checked="false"></item>

</selector>

I had the same kind of problem with my switch view. After a lot of search and reading, I figure out that this problem is result of a sort of optimization of the views promoted by Android. It try to reuse the views objects. So, basically, when you click on a switch or a checkbox you trigger the callback of change in another view too. The optimization really works, but if you do not pay attention, or just don't know the behavior (like me), some pretty weird results happens.

Anyway, I find this simple solution:

The best practice is to reuse the convertView passed on the getView() method. This way you optimize the amount of Ram used by the listview. So I store my views in a ViewHolder and programmed the onCheckedChanged event. The trick is, before set if the switch is checked or not (in your case, check the checkbox) and define the CheckedChange listener, I simply reset the callback with null, this ensures that the event of another switch isn't triggered.

Here is the snippet:

...
viewHolder.switchView.setOnCheckedChangeListener(null);
viewHolder.switchView.setChecked(myObject.getStatus() > 0);
...
viewHolder.switchView.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                ...
            }
        });

myObject is a final object that has the Cursor data stored.

And regardless of the switch will be checked or not, you have to call the setChecked method.

Hope it helps someone!