Android ListView Checkbox 混乱
这是一个很坑的问题,也是一个很经典的问题,百度搜索了下,发现问这个问题的人还蛮多的,下面我们就来尝试解决这个问题吧
复现问题
-
创建一个 空的 Android 项目
cn.twle.android.ListViewCheckBox
-
修改
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>
限制下高度,不然要设置的数据太多
-
定义列表中每一行的布局,在
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
只是一个状态指示器 -
在
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; } }
-
在
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; } }
-
修改
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
怎么选中了?
问题发生的原因
这是因为 ListView
中 Item
的复用造成的
我们先来看一下 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 信息
从图中看出,Postion
从 7
开始,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 状态的代码之前