Android开发中ListView多屏的全选、反选功能
鄙人最近刚开始学习Android,在练习的时候写到一个ListView的全选反选功能。本来以为这个功能很简单,随随便便就能搞定,结果真的下手去做的时候被虐的死去活来,不知道为什么在做全选和反选的操作是,总有几个选项在意料之外被选上或者取消,导致每次反选的结果都不是我预期的结果。而且在全选或者反选之后再去点checkbox,会出现操作无效的情况,一滚动屏幕,选项就又回到全选的状态了。
后来问了一些以前开发过Android的人才知道,原来Android里面的ListView是基于这样一种工作原理:当数据超过一屏的时候,每次滚动都会重新加载ListView里面每一行的View。而新加进来的View实际上是之前的View重新加载。这样ListView就只需要加载一个屏幕所展示数量的View就可以了,但是副作用就是消失到屏幕外的View系统不会为你保存。举个例子,一屏能显示7行数据,要显示的数据总共有10行。那头一屏必然是0~6,当滚到第二屏的时候,就是6、7、8、9、0、1。头几个被滚出屏幕的View又重新被加载了回来。我之前用的方法是通过for循环获取ListView里面的子元素然后改变CheckBox的状态,由于刚才那个原因,总是存在序号错位的问题,所以才会总是出现刚才说的全选和反选时的诡异现象。
所以解决办法除了要把当前屏幕内的checkBox的状态按照原来数据的序号进行改变以外,还要同时修改adapter里面的数据,以确保滚屏的时候数据一致。我在看书的时候基本上所有的例子都是把adapter写成一个匿名内部类,这样adapter的数据一定与activity里面的数据是同步的。但是如果adapter里面要实现很多功能的话,就会使activity这个类过于庞大,违反了“指责单一化原则”。而如果把Adapter单独写出来,数据同步就不太好处理。
所以我抽离了一个中间的专门管理数据的类InAccountListManager(InAccount是我这个工程的某个bean,可以忽略不计),来同步activity与adapter之间数据的差异。package com.accountms.activity.util; import java.util.HashSet; import java.util.List; import java.util.Set; import android.app.Activity; import android.util.Log; import android.view.LayoutInflater; import android.widget.CheckBox; import android.widget.LinearLayout; import android.widget.ListView; import com.accountms.activity.R; import com.accountms.databean.BaseBean; import com.accountms.databean.InAccountBean; import com.accountms.functioninterface.IListHandler; import com.example.util.IBaseInfo; /** * ListView数据管理类 * * @author Treagzhao 用来同步Activity与Adapter之间的数据,同时管理正向同步和反向同步的互斥。 * */ public class InAccountListManager implements IBaseInfo, IListAdapter, IListActivity { private IListActivity activity; private IListAdapter adapter; // list用来保存要展示的数据(InAccountBean是我要操作的bean类,可以换成任意数据) private List<InAccountBean> list; // selectedList 用来保存已经选中的数据 private Set<InAccountBean> selectedList = new HashSet(); // posList与selectedList数据一致,用以记录所保存的数据在源数据中的位置 private Set<Integer> posList = new HashSet(); // 如果正在执行“全选”或者“反选”操作时,用于取消checkbox的onCheckedChagne事件(下面会有详细说明) private boolean operatingMutex = false; private ListView listView; /** * * @param list * 源数据 * @param activity * 具体的activity * @param adapter * listView的adapter * @param listView * 展示数据的listView */ public InAccountListManager(List<InAccountBean> list, IListActivity activity, IListAdapter adapter, ListView listView) { this.activity = activity; this.adapter = adapter; this.list = list; this.listView = listView; } public Set<InAccountBean> getSelectedList() { return selectedList; } public void setSelectedList(Set<InAccountBean> selectedList) { this.selectedList = selectedList; } public Set<Integer> getPosList() { return posList; } public void setPosList(Set<Integer> posList) { this.posList = posList; } /** * 继承IListAdapter接口的方法,用以处理用户点击“全选”后的操作 */ @Override public void onSelectedAll() { // “全选” // 是一个activity向adapter单向同步数据的操作,可是在执行checkbox.setChecked时,会触发checkbox的onCheckedChange事件,会把adapter的数据重新发回来,造成干扰。所以要设定一个标记量,activity向adapter单向同步的时候,不接受反向的同步。 operatingMutex = true; // 将所有数据都放在已选数据中 for (int i = 0; i < list.size(); i++) { InAccountBean bean = list.get(i); selectedList.add(bean); posList.add(i); } // 改变checkbox的状态 for (int i = 0; i < listView.getChildCount(); i++) { LinearLayout linear = (LinearLayout) listView.getChildAt(i); CheckBox checkbox = (CheckBox) linear .findViewById(R.id.inaccount_list_checkbox); checkbox.setChecked(true); } this.adapter.onSelectedAll(); operatingMutex = false; } @Override public void onSelectedAllCancle() { // TODO Auto-generated method stub } /** * 继承自IListActivity的方法,用以处理checkbox的状态点击事件 */ @Override public void onItemCheckedChanged(int pos, boolean checked) { //如果是“全选”或者“反选”类的操作则退出,只执行用户点击checkbox这一类的adapter到activity的数据同步 if (operatingMutex) return; if (checked) { selectedList.add(list.get(pos)); posList.add(pos); } else { selectedList.remove(list.get(pos)); posList.remove(pos); } this.activity.onItemCheckedChanged(pos, checked); } /** * 继承自IListAdapter的方法,用以处理用户点击“反选”的操作 */ @Override public void onReverseSelected() { //原理同上 operatingMutex = true; //将选中的数据逆置 for (int i = 0; i < list.size(); i++) { InAccountBean bean = list.get(i); if (selectedList.contains(bean)) { selectedList.remove(bean); posList.remove(i); } else { selectedList.add(bean); posList.add(i); } } //设定checkbox的状态 for (int i = 0; i < listView.getChildCount(); i++) { LinearLayout linear = (LinearLayout) listView.getChildAt(i); CheckBox checkbox = (CheckBox) linear .findViewById(R.id.inaccount_list_checkbox); //注意这里获取的是checkbox的tag,不是最外层的linear的tag,具体的原理在adapter类里面会提到 int pos = (Integer) checkbox.getTag(); if (posList.contains(pos)) checkbox.setChecked(true); else checkbox.setChecked(false); } operatingMutex = false; } }这里面用到两个接口IListActivity和IListAdapter,前者的作用是接受adapter发出的onCheckedChange事件,后者是响应“全选”和“反选”。由于这个Manager类相当于activity和adapter之间的中间件,它的作用是把activity的操作指令预处理后传递给adapter,反之亦然,所以Manager类同时实现这两个接口。
package com.accountms.activity.util; public interface IListActivity { public void onItemCheckedChanged(int pos, boolean checked); }
package com.accountms.activity.util; public interface IListAdapter { public void onSelectedAll(); public void onSelectedAllCancle(); public void onReverseSelected(); }以下是Activity的代码
package com.accountms.activity; import java.util.HashMap; import java.util.List; import java.util.Map; import com.accountms.activity.util.IListActivity; import com.accountms.activity.util.InAccountListManager; import com.accountms.adapter.InAccountListAdapter; import com.accountms.databean.InAccountBean; import com.accountms.functioninterface.IThreadHandler; import com.accountms.thread.InAccountThreadFactory; import com.example.util.BaseActivity; import com.example.util.IBaseInfo; import android.os.Bundle; import android.app.ProgressDialog; import android.view.Menu; import android.view.View; import android.widget.Button; import android.widget.ListView; import android.widget.TextView; public class InAccountListActivity extends BaseActivity implements IBaseInfo, IThreadHandler, IListActivity { private TextView selectAll, reverseSelect, removeBtn; private ListView listView; private Button addBtn; private int page = 1; private ProgressDialog dialog; //用以保存从数据库中读出的信息 private List<InAccountBean> list; //“哒啦啦!”主角出现了 private InAccountListManager manager; //listView 的adapter private InAccountListAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_in_account_list); init(); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.in_account_list, menu); return true; } /** * 初始化View */ @Override protected void findView() { listView = (ListView) findViewById(R.id.info_list); selectAll = (TextView) findViewById(R.id.text_selecteAll); reverseSelect = (TextView) findViewById(R.id.text_reverseSelect); removeBtn = (TextView) findViewById(R.id.text_remove); } /** * 绑定事件 */ @Override protected void bindListener() { selectAll.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (manager == null) return; manager.onSelectedAll(); } }); reverseSelect.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { manager.onReverseSelected(); } }); } /** * 初始化各个功能 */ @Override protected void initPlugins() { initList(); } /** * 初始化数据List,新建一个线程从数据库中获取数据 */ private void initList() { dialog = new ProgressDialog(this); dialog.setTitle("提醒"); dialog.setProgress(0); dialog.setMax(100); dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); dialog.setMessage("读取中····"); dialog.setCancelable(false); dialog.setIndeterminate(false); dialog.show(); Map map = new HashMap(); map.put("page", page); Thread thread = new Thread(InAccountThreadFactory.getThread(this, InAccountThreadFactory.TYPE_LIST, map, this, dialog)); thread.start(); } /** * 获取数据库的线程回调函数,本文所说的内容基本就是从这里开始的 */ @Override public void handle(Map map) { list = (List<InAccountBean>) map.get("list"); adapter = new InAccountListAdapter(InAccountListActivity.this, list); manager = new InAccountListManager(list, this, adapter, listView); adapter.setManager(manager); listView.setAdapter(adapter); if (dialog != null) dialog.cancel(); } @Override public void onItemCheckedChanged(int pos, boolean checked) { // TODO 做爱做的事 } }
接下来是adapter的代码
package com.accountms.adapter; import java.text.SimpleDateFormat; import java.util.List; import java.util.Set; import android.app.Activity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.LinearLayout; import android.widget.TextView; import com.accountms.activity.R; import com.accountms.activity.util.IListAdapter; import com.accountms.activity.util.InAccountListManager; import com.accountms.databean.InAccountBean; import com.example.util.IBaseInfo; /** * * @author Treagzhao 设定ListView的Adapter * */ public class InAccountListAdapter extends BaseAdapter implements IBaseInfo, IListAdapter { private List<InAccountBean> list; public Activity activity; private SimpleDateFormat format; // 我们的主角在这里 private InAccountListManager manager; public InAccountListManager getManager() { return manager; } public void setManager(InAccountListManager manager) { this.manager = manager; } public InAccountListAdapter(Activity activity, List<InAccountBean> list) { this.list = list; this.activity = activity; format = new SimpleDateFormat("yyyy-MM-dd"); } @Override public int getCount() { // TODO Auto-generated method stub return list.size(); } @Override public Object getItem(int pos) { // TODO Auto-generated method stub return pos; } @Override public long getItemId(int pos) { // TODO Auto-generated method stub return pos; } /** * 看这里看这里看这里 * * @deprecated 这个函数两个情况下会被调用,除了一开始的初始化以外,每次listView滚动时也会调用这个函数。 */ @Override public View getView(int pos, View convertView, ViewGroup arg2) { LayoutInflater inflater = this.activity.getLayoutInflater(); LinearLayout linear = null; if (convertView == null) { linear = (LinearLayout) inflater.inflate( R.layout.inaccountlist_layout, null); linear.setTag(pos); } else { linear = (LinearLayout) convertView; } // 注意这个地方,除了LinearLayout的初始化以外,其内部的所有子View在所有情况下均被执行。 CheckBox checkbox = (CheckBox) linear .findViewById(R.id.inaccount_list_checkbox); // 由于checkbox的tag是每次随着list的顺序设定的,所以checkbox的tag一定是与数据一致的 checkbox.setTag(pos); TextView mark = (TextView) linear .findViewById(R.id.text_inaccount_mark); TextView time = (TextView) linear .findViewById(R.id.text_inaccount_time); mark.setText(list.get(pos).getMark()); time.setText(format.format(list.get(pos).getTime())); // 获取已经被选中的数据,由于这个数据是每次加载时从manager对象获取,所以一定是同步的数据 Set<Integer> posList = manager.getPosList(); if (posList.contains(pos)) { checkbox.setChecked(true); } else { checkbox.setChecked(false); } checkbox.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { int pos = (Integer) buttonView.getTag(); // 触发manager的onItemCheckedChanged事件 manager.onItemCheckedChanged(pos, isChecked); } }); return linear; } @Override public void onSelectedAll() { } @Override public void onSelectedAllCancle() { } @Override public void onReverseSelected() { } }
由于activity和adapter的操作都是对manager的操作,而且获取数据是都是从manager里面获取,所以双方的数据一定是一致的。而且有manager在中间进行管理,就会让activity的操作不会返回来对数据造成影响。同时因为有manager的存在,大大减少了activity和adapter的工作量。解决了ListView在超过一屏是的全选和反选功能。
鄙人刚刚开始学习Android开发,如果各位看官有更好的解决方案,请一定告知我