ListView里的CheckBox错乱问题的两种解决方式

ListView里面如果有CheckBox跟RadioButton的话,而且Adapter复用了convertView的话,基本上是会100%出现滑动错乱的,比如像下面这样的

出现这个问题的根本原因还是在convertView复用的时候没有对CheckBox做处理,我们知道,假设手机屏幕一屏能显示7个item,则刚开始会一次性创建7个convertView,当屏幕往上滑的时候,第1个item不可见时,系统会将它缓存起来复用给后面的item,所以在滑动到第8个item的时候,getView()方法中的convertView就是第1个item的。重用convertView可以避免大量重复的inflate跟findViewById,会使ListView的性能大大提升,但是带来的问题就是,第8个item的convertView其实是第1个复用下来的,如果我们在第1个item上做了操作,而在第8个item上没有刷新数据,就会导致上面的错乱问题。

解决这个问题有两种办法:

  1. 在Adapter中使用ArrayList保存所有CheckBox的选中状态,然后在OnCheckedChangeListener中监听CheckBox的状态,对状态进行相应的保存,代码很简单,相信大家看看就明白了,直接上代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    public class ListViewAdapter1 extends BaseAdapter {
    private List<String> cities;
    private LayoutInflater mInflater;
    // 创建一个List用于保存CheckBox的选中状态
    private List<Boolean> checks = new ArrayList<>();
    public ListViewAdapter1(Context context, List<String> cities) {
    this.cities = cities;
    mInflater = LayoutInflater.from(context);
    for (int i = 0; i < cities.size(); i++) {
    // 刚开始,所有的CheckBox都是未选中的
    checks.add(false);
    }
    }
    @Override
    public int getCount() {
    return cities.size();
    }
    @Override
    public Object getItem(int position) {
    return cities.get(position);
    }
    @Override
    public long getItemId(int position) {
    return position;
    }
    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
    ViewHolder holder = null;
    if (convertView == null) {
    holder = new ViewHolder();
    convertView = mInflater.inflate(R.layout.listview_item, parent, false);
    holder.cbListView = (CheckBox) convertView.findViewById(R.id.cb_listView);
    holder.tvListView = (TextView) convertView.findViewById(R.id.tv_listView);
    convertView.setTag(holder);
    } else {
    holder = (ViewHolder) convertView.getTag();
    }
    holder.tvListView.setText(cities.get(position));
    holder.cbListView.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    if (isChecked) {
    // 当CheckBox被选中,将相应position的值设置为true
    checks.set(position, true);
    } else {
    // 当CheckBox取消选中,将对应position的值设置为false
    checks.set(position,false);
    }
    }
    });
    // 根据相应位置的checks值设置CheckBox的选中状态
    holder.cbListView.setChecked(checks.get(position));
    return convertView;
    }
    class ViewHolder {
    CheckBox cbListView;
    TextView tvListView;
    }
    }
  2. 第二种方法原理跟第一种一样,也是监听CheckBox的选中状态然后进行保存,只不错用的是Map<Integer,Boolean>保存,下面是代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    public class ListViewAdapter extends BaseAdapter {
    private List<String> cities;
    private LayoutInflater mInflater;
    private Map<Integer,Boolean> checkBoxState = new HashMap<>();
    public ListViewAdapter(Context context, List<String> cities) {
    this.cities = cities;
    mInflater = LayoutInflater.from(context);
    }
    @Override
    public int getCount() {
    return cities.size();
    }
    @Override
    public Object getItem(int position) {
    return cities.get(position);
    }
    @Override
    public long getItemId(int position) {
    return position;
    }
    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
    ViewHolder holder = null;
    if (convertView == null){
    holder = new ViewHolder();
    convertView = mInflater.inflate(R.layout.listview_item,parent,false);
    holder.cbListView = (CheckBox) convertView.findViewById(R.id.cb_listView);
    holder.tvListView = (TextView) convertView.findViewById(R.id.tv_listView);
    convertView.setTag(holder);
    } else {
    holder = (ViewHolder) convertView.getTag();
    }
    holder.tvListView.setText(cities.get(position));
    holder.cbListView.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    if (isChecked){
    // 若CheckBox为选中状态,保存当前位置的选中信息
    checkBoxState.put(position,true);
    } else {
    // 若CheckBox取消选中,则将其从HashMap中移除
    checkBoxState.remove(position);
    }
    }
    });
    if (checkBoxState.size() != 0 && checkBoxState.containsKey(position)){
    holder.cbListView.setChecked(true);
    } else {
    holder.cbListView.setChecked(false);
    }
    return convertView;
    }
    class ViewHolder{
    CheckBox cbListView;
    TextView tvListView;
    }
    }

处理之后就不会出现滑动错乱了

以上就是解决这个问题的两种方式,此方法同样也适用于RecyclerView。

其实在这里还会有另一个隐藏的问题,那就是如果ListView中包含CheckBox跟Button等,则ListView默认会失去焦点,这个时候设置了setOnItemClickListener()即item的点击监听是不起作用的,点击ListView的item是不响应的,解决这个问题的办法就是在item的根布局中设置descendantFocusability属性,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?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="horizontal"
android:gravity="center"
android:paddingLeft="10dp"
android:paddingStart="10dp"
android:paddingRight="10dp"
android:paddingEnd="10dp"
android:paddingTop="10dp"
android:descendantFocusability="blocksDescendants">
<CheckBox
android:id="@+id/cb_listView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tv_listView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="30sp"
android:layout_marginLeft="5dp"
android:layout_marginStart="5dp"/>
</LinearLayout>

该属性是当一个为view获取焦点时,定义viewGroup和其子控件两者之间的关系。

属性的值有三种:

  • beforeDescendants:viewgroup会优先其子类控件而获取到焦点
  • afterDescendants:viewgroup只有当其子类控件不需要获取焦点时才获取焦点
  • blocksDescendants:viewgroup会覆盖子类控件而直接获得焦点
如果觉得本文对你有帮助,请支持我!