在ListView上使用CheckBox的自定义Adapter写法

这似乎是一个经常的UI设计,所以值得记录一下。

写自定义的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 listItems; // 用这个list来指向最初的数据list

    private class ViewHolder {
        int position;
    }

    public MyArrayAdapter(Context context, int textViewResourceId, List 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 selectedItems = new ArrayList();

        // 这个viewListItems就是最初构建Adapter的数据list
        // 在那边修改后就反映到这里
        int size = viewListItems.size();

        for(int count = 0;count

下面是改进方案,也即采用CheckBox增加tag来保存position信息,而不用隐晦的final变量:
public class MyArrayAdapter extends ArrayAdapter{
    int resource;
    final List listItems;

    private class ViewHolder {
        int position;
    }

    public MyArrayAdapter(Context context, int textViewResourceId, List 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。


在ListView上使用CheckBox的自定义Adapter写法_第1张图片


测试的时候要小心,在选择了若干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/




你可能感兴趣的:(Android)