Android ListView Checkbox 混乱

这是一个很坑的问题,也是一个很经典的问题,百度搜索了下,发现问这个问题的人还蛮多的,下面我们就来尝试解决这个问题吧

复现问题

  1. 创建一个 空的 Android 项目 cn.twle.android.ListViewCheckBox

  2. 修改 activity_main.xml 添加一个 ListView

    <?xml version="1.0" encoding="utf-8" ?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="8dp" 
        android:orientation="vertical" >
    
        <ListView
            android:id="@+id/listview"
            android:layout_width="match_parent"
            android:layout_height="300dp" />
    </LinearLayout>
    

    限制下高度,不然要设置的数据太多

  3. 定义列表中每一行的布局,在 res/layout 目录下新建一个文件 listview_item.xml

    <?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="match_parent"
        android:padding="8dp"
        android:orientation="horizontal">
    
        <TextView
            android:id="@+id/name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingLeft="8dp"
            android:textColor="#1D1D1C"
            android:textSize="20sp" 
            android:layout_weight="3" />
    
        <CheckBox
            android:id="@+id/checked"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:focusable="false" 
            android:layout_weight="1" />
    
        </LinearLayout>
    

    CheckBox 只是一个状态指示器

  4. MainActivity.java 目录下创建一个 LanguageBean.java

    package cn.twle.android.listviewcheckbox;
    
    public class LanguageBean  {
    
        private String name;
        private Boolean checked;
    
        public LanguageBean () {
        }
    
        public LanguageBean (String name, Boolean checked) {
            this.name = name;
            this.checked = checked;
        }
    
        public String getName() {
            return name;
        }
    
        public Boolean getChecked() {
            return checked;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public void setChecked(Boolean checked) {
            this.checked = checked;
        }
    }
    
  5. MainActivity.java 目录下创建一个适配器 LanguageAdapter.java

    package cn.twle.android.listviewcheckbox;
    
    import android.content.Context;
    import android.widget.BaseAdapter;
    
    import android.util.Log;
    
    import android.widget.CompoundButton;
    import android.widget.TextView;
    import android.widget.CheckBox;
    
    import android.view.View;
    import android.view.ViewGroup;
    import android.view.LayoutInflater;
    
    import java.util.LinkedList;
    
    public class LanguageAdapter extends BaseAdapter {
    
        private LinkedList<LanguageBean> mData;
        private Context mContext;
    
        public LanguageAdapter(LinkedList<LanguageBean> mData, Context mContext) {
    
            this.mData = mData;
            this.mContext = mContext;
        }
    
        @Override
        public int getCount() {
            return mData.size();
        }
    
        @Override
        public Object getItem(int position) {
            return null;
        }
    
        @Override
        public long getItemId(int position) {
            return position;
        }
    
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
    
            final int index = position;
            ViewHolder holder = null;
    
            if(convertView == null){
    
                convertView = LayoutInflater.from(mContext).inflate(R.layout.listview_item,parent,false);
    
                holder = new ViewHolder();
    
                holder.name= (TextView) convertView.findViewById(R.id.name);
                holder.checked = (CheckBox) convertView.findViewById(R.id.checked);
    
                convertView.setTag(holder);   //Holder存储到convertView中
            }else{
                holder = (ViewHolder) convertView.getTag();
            }
    
            holder.name.setText(mData.get(index).getName());
            holder.checked.setChecked(mData.get(index).getChecked());
    
            holder.checked.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
                            @Override
                            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    
                                mData.get(index).setChecked(isChecked);
                            }
                        });
    
            return convertView;
        }
    
        static class ViewHolder{
            TextView name;
            CheckBox checked;
        }
    }
    
  6. 修改 MainActivity.java

    package cn.twle.android.listviewcheckbox;
    
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    
    import android.widget.ListView;
    import android.widget.Toast;
    import android.widget.AdapterView;
    
    import android.view.View;
    
    import java.util.LinkedList;
    import java.util.List;
    
    public class MainActivity extends AppCompatActivity implements AdapterView.OnItemClickListener  {
    
        private String[] langs = new String[]{
            "Kotlin", 
            "Scala", 
            "Swift",
            "TypeScript",
            "Java",
            "Python",
            "PHP",
            "Perl",
        };
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            List<LanguageBean>  mData = new LinkedList<LanguageBean>();
    
            mData.add(new LanguageBean("Kotlin",true));
            mData.add(new LanguageBean("Scala", false));
            mData.add(new LanguageBean("Swift",false));
            mData.add(new LanguageBean("TypeScript", false));
            mData.add(new LanguageBean("java",false));
            mData.add(new LanguageBean("Python", false));
            mData.add(new LanguageBean("PHP",true));
            mData.add(new LanguageBean("Perl", true));
    
            //创建一个 YetAdapter
            LanguageAdapter languageAdapter = new LanguageAdapter((LinkedList<LanguageBean>) mData,getApplicationContext());
    
            ListView listView = (ListView) findViewById(R.id.listview);
    
            listView.setAdapter(languageAdapter);
            listView.setOnItemClickListener(this);
        }
    
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            Toast.makeText(getApplicationContext(),"你点击了第" + position + "项",Toast.LENGTH_SHORT).show();
        }
    
    }
    

运行范例,先把第一个 CheckBox 取消选中然后拉到列表底部,最后再回到顶部,what ,顶部的 CheckBox 怎么选中了?

问题发生的原因

这是因为 ListViewItem 的复用造成的

我们先来看一下 ListView 方法 getView() 的调用机制

上图左下角的 Recycler ,ListView上 可见的 Item 放在内存中,不可见的 Item 则放在 Recycler

第一次加载 item 时,当前页面中的 convertView 都为 NULL,当滚出屏幕后,convertView 就可能不为空,因为新的一项会复用这个 convertView

我们复用上面的 demo ,打上 Log ,修改 LanguageAdapter.java 中的 getView() 方法

@Override
public View getView(int position, View convertView, ViewGroup parent) {

    final int index = position;
    ViewHolder holder = null;

    // 调试输出信息的时候不推荐用 info 级别,信息太多太杂
    Log.d("LanguageAdapter.getView()" ,String.valueOf(index) + " " + String.valueOf(convertView) );

    if(convertView == null){

        convertView = LayoutInflater.from(mContext).inflate(R.layout.listview_item,parent,false);

        holder = new ViewHolder();

        holder.name= (TextView) convertView.findViewById(R.id.name);
        holder.checked = (CheckBox) convertView.findViewById(R.id.checked);

        convertView.setTag(holder);   //将Holder存储到convertView中
    }else{
        holder = (ViewHolder) convertView.getTag();
    }


    holder.name.setText(mData.get(index).getName());
    holder.checked.setChecked(mData.get(index).getChecked());

    holder.checked.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
                    @Override
                    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {

                        mData.get(index).setChecked(isChecked);
                    }
                });


    return convertView;
}

下面是运行后的一些 Log 信息

从图中看出,Postion7 开始,convertView 就不为空了,而 7 正好是一屏 item 的数量 + 1

然后看淡绿色的框,发现回到顶部后,0 和刚刚的 7 复用的同一个

就是因为这个 convertView 缓存的原因,造成了 CheckBox 混乱

要解决这个问题,一般都会想,要不我就不重用 convertView,或者 说每次 getView() 都将这个 convertView 设置为 null,如果这样,那么我们之前做的优化工作都白费了

当然了,真正的原因是这个吗? 是也不是,说是是因为确实是缓存造成的,说不是,是因为在于缓存的时候,把 onCheckedChanged() 方法里的那个 index 也缓存了

其实这也不是问题的本质所在,本质的原因,是 CheckBox 任何状态的改变都会触发 onCheckedChanged() 事件,所以,有可能在设置 CheckBox 属性的时候刚好就触发了 onCheckedChanged() 属性

所以,要解决这个问题,就要先添加 onCheckedChanged() 事件,然后再设置属性

解决办法

解决办法就是将 holder.checked.setChecked(mData.get(index).getChecked()); 语句移到 holder.checked.setOnCheckedChangeListener 之后

@Override
public View getView(int position, View convertView, ViewGroup parent) {

    final int index = position;
    ViewHolder holder = null;

    // 调试输出信息的时候不推荐用 info 级别,信息太多太杂
    Log.d("LanguageAdapter.getView()" ,String.valueOf(index) + " " + String.valueOf(convertView) );


    if(convertView == null){

        convertView = LayoutInflater.from(mContext).inflate(R.layout.listview_item,parent,false);

        holder = new ViewHolder();

        holder.name= (TextView) convertView.findViewById(R.id.name);
        holder.checked = (CheckBox) convertView.findViewById(R.id.checked);

        convertView.setTag(holder);   //将Holder存储到convertView中
    }else{
        holder = (ViewHolder) convertView.getTag();
    }

    holder.checked.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
                    @Override
                    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {

                        mData.get(index).setChecked(isChecked);
                    }
                });


    holder.name.setText(mData.get(index).getName());
    holder.checked.setChecked(mData.get(index).getChecked());


    return convertView;
}

后面想了想,本质原因应该是写代码习惯问题,哎

注意: CheckBox 监听器的方法要添加在初始化 Checkbox 状态的代码之前

Android 基础教程

关于   |   FAQ   |   我们的愿景   |   广告投放   |  博客

  简单教程,简单编程 - IT 入门首选站

Copyright © 2013-2022 简单教程 twle.cn All Rights Reserved.