您的位置:首页 > 其它

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

2013-09-19 18:56 197 查看
这似乎是一个经常的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<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/


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: