写自定义的Adapter的主要麻烦在于其中的getView()方法,又特别在于其中CheckBox状态的保存和恢复。编写的一个原则是,尽可能吻合软件工程需求,比如耦合与分离,尽量简单。
标准的ListView构建过程是:
1、首先构建一个ArrayList(或其他List)来保存数据
2、用上述List构建ArrayAdapter
3、将ArrayAdapter传递给ListView
4、ArrayAdapter提供构建行View的方法给ListView
5、ListView在显示item时调用上述方法构建行View
6、现实行View,其中有CheckBox
7、ListView在不显示item时(比如移除屏幕外)时清除行View
最终目标是统计各行的checked状态。主要的问题在于:在CheckBox的View中虽然有当前的checked状态,但一旦其所在的行View被拖出屏幕外,则CheckBox的checked状态立即丢失。那么,怎样保存和恢复CheckBox的checked状态?在哪里保存?
在官网及其他地方搜罗了很久,看到多个方案,根据自己的需求,设计了一个比较简单的解决方案,要先说明的是:
1、checked状态保存位置是问题之一。我的方案是在最初的数据list中增加保存checked信息的位置。我没看过Android源码,但由于list可能很大,所以不大可能有list的拷贝复制操作,所以这个list相当于一个全局变量,可以用来在多个命名域空间之间传递信息。感觉是一个大胆创举,不过我并不保证肯定正确,也不保证将来正确。
2、匿名内部类在访问外部类变量时有限制,这是问题关键之二。
下面是解决方案,也即在数据list中增加checked标志位,在adapter中使用这个位置,并采用final变量中转,使得匿名内部类访问父类中的position信息,下面是代码和注释。
public class MyArrayAdapter extends ArrayAdapter{
int resource;
final List<LvRow> listItems; // 用这个list来指向最初的数据list
private class ViewHolder {
int position;
}
public MyArrayAdapter(Context context, int textViewResourceId, List<LvRow> objects) {
super(context, textViewResourceId, objects);
resource = textViewResourceId;
listItems = objects; // 相信这个objects就是构建Adapter的最初的数据list,赶紧保存
}
public View getView (int position, View convertView, ViewGroup parent){
LinearLayout fileView;
// 方案一,使用final变量
// 进入方法都会生成一个final p,原本方法退出之后会被回收,
// 但由于后面兼听类的引用,使其不会被回收,除非兼听类先被回收。
// 下次进入本方法会生成另外一个final p。
// 因此,我们可以用final p来保存position。
// 这是我的理解,不肯定对。
final Integer p;
p = position;
LvRow lr = (LvRow)getItem(position);
if (convertView == null){
fileView = new LinearLayout(getContext());
String inflater = Context.LAYOUT_INFLATER_SERVICE;
LayoutInflater li;
li = (LayoutInflater)getContext().getSystemService(inflater);
li.inflate(resource, fileView, true);
}else{
fileView = (LinearLayout)convertView;
}
TextView name = (TextView)fileView.findViewById(R.id.text1);
name.setText(lr.getName());
TextView length = (TextView)fileView.findViewById(R.id.text2);
length.setText(lr.getLength());
TextView date = (TextView)fileView.findViewById(R.id.text3);
date.setText(lr.getDate());
// 这是我们ListView的宝贝CheckBox
CheckBox cb = (CheckBox)fileView.findViewById(R.id.checkbox);
if (lr.getName().charAt(0) == '/') // 这是一个文件管理项目的代码,不允许check目录,故隐藏CheckBox
{
cb.setVisibility(cb.INVISIBLE);
}else{
cb.setVisibility(cb.VISIBLE);
cb.setChecked(listItems.get(position).getSelected()); // 从数据list中恢复checked状态
cb.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
CheckBox cb = (CheckBox)view;
listItems.get(p).setSelected(cb.isChecked()); // 保持对final p的引用,使其不被回收
}
});
}
return fileView;
}
}
// 注意增加了selected标志位
public class LvRow{
private String name;
private String length;
private String date;
private File file;
private boolean selected;
// 各种set/get/constructer略掉
}
在主Activity中统计checked信息。
private void shareFiles() { // 共享被选择的文件
List<LvRow> selectedItems = new ArrayList<LvRow>();
// 这个viewListItems就是最初构建Adapter的数据list
// 在那边修改后就反映到这里
int size = viewListItems.size();
for(int count = 0;count<size;count++){
if (viewListItems.get(count).getSelected() == true){
selectedItems.add(viewListItems.get(count));
}
}
// share selectedItems
}
下面是改进方案,也即采用CheckBox增加tag来保存position信息,而不用隐晦的final变量:
public class MyArrayAdapter extends ArrayAdapter{
int resource;
final List<LvRow> listItems;
private class ViewHolder {
int position;
}
public MyArrayAdapter(Context context, int textViewResourceId, List<LvRow> objects) {
super(context, textViewResourceId, objects);
resource = textViewResourceId;
listItems = objects;
}
public View getView (int position, View convertView, ViewGroup parent){
LinearLayout fileView;
LvRow lr = (LvRow)getItem(position);
if (convertView == null){
fileView = new LinearLayout(getContext());
String inflater = Context.LAYOUT_INFLATER_SERVICE;
LayoutInflater li;
li = (LayoutInflater)getContext().getSystemService(inflater);
li.inflate(resource, fileView, true);
}else{
fileView = (LinearLayout)convertView;
}
TextView name = (TextView)fileView.findViewById(R.id.text1);
name.setText(lr.getName());
TextView length = (TextView)fileView.findViewById(R.id.text2);
length.setText(lr.getLength());
TextView date = (TextView)fileView.findViewById(R.id.text3);
date.setText(lr.getDate());
CheckBox cb = (CheckBox)fileView.findViewById(R.id.checkbox);
// 方案二,直接在View中添加tag来保存position信息
cb.setTag(position);
if (lr.getName().charAt(0) == '/')
{
cb.setVisibility(cb.INVISIBLE);
}else{
cb.setVisibility(cb.VISIBLE);
cb.setChecked(listItems.get(position).getSelected()); // 恢复checked状态
cb.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
CheckBox cb = (CheckBox)view;
// 下面直接用View的tag中获取position信息,并修改对应的最初的数据list
listItems.get((Integer)view.getTag()).setSelected(cb.isChecked());
}
});
}
return fileView;
}
}
感觉View可以增加tag是一件比较奇葩的事,让我进一步思考:其他对象是不是也可以随时增加tag?
考虑到模块之间尽量减低耦合,所以推荐方案二。
以下是截图。注意在行View布局中我设置了android:focusable="false",使得可以分别点击List的item和CheckBox。
测试的时候要小心,在选择了若干CheckBox之后要将其来回拖出屏幕,检查checked状态是否正常,最后再在最初的数据list中统计checked状态。
---------修正------------------------
2014年4月14日
写本文时对某些地方理解不清,并没说到要点,这里给予补充。
主要是两个问题。问题之一题是View被复用,但却没有被清空:
1、当ListView中的一行条目移出显示屏时,其View并未被回收,而是放在旁边,准备复用。注意此刻并不清空View里面的垃圾数据。
2、当有一行条目要进入显示屏时,刚才被回收的View立即被取过来重用,但是由于之前被用过,且没有重新清空的步骤,所以凡是没有被设置的地方均是View上次的数据。
解决方案是,任何表示数据的地方都要做清空或数据设置。
问题之二是,怎样向View的监听函数传递数据,方式之一是通过final类型变量,方式之二是设置在View里面。后者好。
在本例中,向监听函数传递CheckButton所在item在list列表中的位置,于是CheckButton被点击时能够将变动修改到list列表。
2015年1月1日
ListView里面常用findViewById()来寻找view中的子view,这种操作往往很费时,通常可以使用一个静态类来标识View的结构:
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.your_layout, null);
holder = new ViewHolder();
holder.text = (TextView) convertView.findViewById(R.id.text);
convertView.setTag(holder);
} else {
holder = convertView.getTag();
}
holder.text.setText("Position " + position);
return convertView;
}
private static class ViewHolder {
public TextView text;
}
以上例子来自:http://lucasr.org/2012/04/05/performance-tips-for-androids-listview/