完美解决ListView和CheckBox焦点冲突及复用时CheckBox错位等一系列问题

时间:2022-09-22 18:56:03

前段时间项目进行版本升级的时候遇到这么一个需求:当点击界面右上角的“编辑”按钮时,会在列表的每个item的左边显示一个用于选中进行删除的CheckBox,刚开始的时候觉得非常容易,但是越往下问题也越多,并且由于ListView具有下拉刷新和上拉加载的功能,因此实现起来也更加的困难,比如处于编辑状态时,下拉刷新新增item时也要保持已选中的item及上拉加载更多时也要保持已选中的item,同时对下拉刷新和上拉加载新增的item也要具有可选操作。因此借这篇博客向大家分享一下我的实现方式,首先让大家看一下效果图:

 完美解决ListView和CheckBox焦点冲突及复用时CheckBox错位等一系列问题

由于布局中含有CheckBox,因此首先要做的是解决焦点问题,在这里就需要用到android中的一个descendantFocusability属性,该属性值也有如下三种:

beforeDescendants:表示ViewGroup会优先其子类控件而获取到焦点;

afterDescendants:表示ViewGroup只有当其子类控件不需要获取焦点时才获取焦点;

blocksDescendants:表示ViewGroup会覆盖子类控件而直接获得焦点。

通常我们用到的是第三种,即在item布局的根布局中添加android:descendantFocusability = “blocksDescendants”,通过此种方式即可解决ListViewitem布局中含有CheckBox时所产生的焦点冲突问题。

焦点冲突问题解决了,接下来需要实现的是如何处理CheckBox的状态,即默认状态为未选中,其次当选中的时候不管是下拉刷新增加新的item还是上拉加载出更多的item都需要保持原来选中的状态,另外新增加的item和加载的item都需要像其它item一样可以对CheckBox进行操作,最后,由于在使用ListView时为了减少对内存的消耗,因此在自定义适配器的时候为了优化ListView都会复用View,这样的话就会造成前面选中CheckBox时后面复用的itemCheckBox也会被选中的问题。

为了满足这一系列的要求,首先需要定义一个用来保存选中位置和对应状态的Map集合并且在ListViewAdapter的构造函数中对其进行初始化,代码如下所示:

/**
 * 用来保存选中状态和对应的位置,用于解决item的复用问题
 */
public static Map<Integer, Boolean> isSelected;
 

Map集合进行初始化,代码如下: 

 

/**
 * 初始选中状态
 *
 * @param size 表示数据的长度,是为了解决下拉刷新和上拉加载时产生新的item时能够都有默认初始值
 */
private void initSelected(int size) {
    //判断isSelected是否已经存在
    if (isSelected == null) {
        isSelected = new HashMap<>();
        for (int i = 0; i < size; i++) {
            isSelected.put(i, false);
        }
    }
}

Map集合初始完毕之后,就可以在getView()方法中对CheckBox进行状态的设置,如CheckBox显示时默认为未选中状态,代码如下:

//判断是否处于编辑状态
if (isVisible) {
    holder.llayout_parent.setVisibility(View.VISIBLE);
    //设置CheckBox默认状态为未选中
    holder.cb_checkbox.setChecked(isSelected.get(position));
} else {//如果CheckBox为不可见,则设置CheckBox为未选中状态
    holder.llayout_parent.setVisibility(View.GONE);
    holder.cb_checkbox.setChecked(false);
}
至此, CheckBox 的默认状态就初始完了,接下来要做的是定义一个用来保存之前选中状态位置的 List 集合,用于加载更多数据后恢复先前已选中的位置,代码如下:
// 用来保存之前选中状态的位置,用于下拉刷新和上拉加载更多数据时恢复已选中的位置
public static List<Integer> hasSelected = new ArrayList<>();

同时也需要在初始化用于设置CheckBox默认状态的Map集合中进行初始化List集合,添加后的代码如下所示:

/**
 * 初始选中状态
 *
 * @param size
 */
private void initSelected(int size) {
    //判断isSelected是否已经存在
    if (isSelected == null) {
        isSelected = new HashMap<>();
        for (int i = 0; i < size; i++) {
            isSelected.put(i, false);
        }
    }else{//此部分适用于具有上拉加载功能的ListView
        for (int i = 0; i < size; i++) {
            isSelected.put(i,false);
            //遍历加载之前所保存的选中的位置
            int length = hasSelected.size();
            for (int j = 0; j < length; j++) {
                if(i==hasSelected.get(j)){
                    isSelected.put(i,true);
                }
            }
        }
    }
}

到这里,在Adapter中对CheckBox的一系列操作就结束了,ListView适配器的完整代码如下所示:

package abner.listview.with.checkbox;

import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.TextView;


import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class CollectionAdapter extends BaseAdapter {

    /**
     * 用来保存选中状态和对应的位置,用于解决item的复用问题
     */
    public static Map<Integer, Boolean> isSelected;
    /**
     * 用来保存之前选中状态的位置,用于加载更多数据时恢复已选中的位置
     */
    public static List<Integer> hasSelected = new ArrayList<>();
    private Context context;
    private List<Collection> collectionList;
    private boolean isVisible = false;

    public CollectionAdapter(Context context, List<Collection> messageList) {
        this.context = context;
        this.collectionList = messageList;
        int size = messageList.size();
        initSelected(size);
    }

    public void setList(List<Collection> messageList) {
        this.collectionList = messageList;
        int size = messageList.size();
        initSelected(size);
    }

    /**
     * 初始选中状态
     *
     * @param size
     */
    private void initSelected(int size) {
        //判断isSelected是否已经存在
        if (isSelected == null) {
            isSelected = new HashMap<>();
            for (int i = 0; i < size; i++) {
                isSelected.put(i, false);
            }
        }else{//此部分适用于具有上拉加载功能的ListView
            for (int i = 0; i < size; i++) {
                isSelected.put(i,false);
                //遍历加载之前所保存的选中的位置
                int length = hasSelected.size();
                for (int j = 0; j < length; j++) {
                    if(i==hasSelected.get(j)){
                        isSelected.put(i,true);
                    }
                }
            }
        }
    }

    public void setVisible(boolean visible) {
        this.isVisible = visible;
    }

    public boolean isVisible() {
        return isVisible;
    }

    @Override
    public int getCount() {
        return collectionList.size();
    }

    @Override
    public Object getItem(int position) {
        return collectionList.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (convertView == null) {
            holder = new ViewHolder();
            convertView = View.inflate(context, R.layout.item_collection, null);
            holder.tv_title = (TextView) convertView.findViewById(R.id.tv_title);
            holder.tv_description = (TextView) convertView.findViewById(R.id.tv_description);
            holder.llayout_parent = (LinearLayout) convertView.findViewById(R.id.llayout_parent);
            holder.cb_checkbox = (CheckBox) convertView.findViewById(R.id.cb_checkbox);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }
        final Collection collection = collectionList.get(position);
        holder.tv_title.setText(collection.getTitle());
        holder.tv_description.setText(collection.getDescription());

        //判断是否处于编辑状态
        if (isVisible) {
            holder.llayout_parent.setVisibility(View.VISIBLE);
            holder.cb_checkbox.setChecked(isSelected.get(position));
        } else {
            holder.llayout_parent.setVisibility(View.GONE);
            holder.cb_checkbox.setChecked(false);
        }
        return convertView;
    }

    class ViewHolder {
        TextView tv_title;
        TextView tv_description;
        LinearLayout llayout_parent;
        CheckBox cb_checkbox;
    }
}

接下来,就来讲解如何在点击编辑按钮时让CheckBox显示并进行选择、全选、反选、删除等功能的实现,首先就是在编辑按钮的点击事件中判断Adapter适配器中的isVisible的值是否为true,如果是则设置CheckBox为不可见,反之为可见,关键代码如下所示:

if (!adapter.isVisible()) {
    btn_edit.setText("取消");
    //显示CheckBox
    adapter.setVisible(true);
    adapter.notifyDataSetChanged();
}else{
    btn_edit.setText("编辑");
    //隐藏CheckBox
    adapter.setVisible(false);
}

至于对CheckBox的选择、全选、反选、删除的实现则需要先在ListViewonItemClick事件中对CheckBox的状态进行一些判断和值的处理,如:在编辑状态下,点击item的时候需要对CheckBox的状态进行切换(即CheckBox为选中时需要切换到未选中状态,反之亦然),其次是需要将点击的item的位置及对应的CheckBox的状态值保存到Adapter适配器中定义好的Map<IntegerBoolean>集合中,最后需要对在Adapter适配器定义好的用来保存点击位置的List集合进行判断,判断点击的位置是否已经存在,如果已经存在则移除,否则添加至List集合中,主要代码如下:

//判断CheckBox是否处于可见状态
if (adapter.isVisible()) {
    CollectionAdapter.ViewHolder holder = (CollectionAdapter.ViewHolder) view.getTag();
    //每次点击item都对checkbox的状态进行改变
    holder.cb_checkbox.toggle();
    //同时将CheckBox的状态保存到HashMap中,其中key为点击的位置,value为状态
    adapter.isSelected.put(position, holder.cb_checkbox.isChecked());

    //判断是否已经存在,如果已经存在,则移除,否则添加
    if (adapter.hasSelected.contains(position)) {
        adapter.hasSelected.remove(position);
    } else {
        adapter.hasSelected.add(position);
    }
}

再接下来的就是对CheckBox的全选和反选功能的实现了,对于全选,只需对数据源进行遍历,然后在其中对在Adapter适配器中定义好的Map<Integer,Boolean> isSelectedList<Integer> hasSelected集合重新进行赋值即可,主要代码如下所示:

/**
 * 全选
 */
public void selectAll() {
    for (int i = 0; i < collectionList.size(); i++) {
        adapter.isSelected.put(i, true);
        adapter.hasSelected.add(i);
        collectionList.get(i).setSelect(true);
        adapter.notifyDataSetChanged();
    }
}

/**
 * 反选
 */
public void cancelAll() {
    for (int i = 0; i < collectionList.size(); i++) {
        adapter.isSelected.put(i, false);
        adapter.hasSelected.clear();
        collectionList.get(i).setSelect(false);
        adapter.notifyDataSetChanged();
    }
}

最后,就是对CheckBox的删除功能的实现了,由于数据源都是本地数据,因此实现起来反而比较的麻烦点,如果是服务器的数据,如果需要实现对CheckBox的删除,只需将需要删除的id等上传到服务器,由后台进行删除操作,对于本地数据的删除,首先需要判断是否有选择要删除的数据,如果有则对数据源进行遍历并判断是哪个对象被选中,然后为其设置一个boolean类型的状态值,最后就可以通过Iterator来删除List集合中某个对象了,主要代码如下所示:

/**
 * 删除所选数据
 */
public void delete() {
    //如果有选择要删除的内容才进行删除,否则提示用户还没有选择要删除的内容
    if(adapter.hasSelected.size()>0) {
        //此处删除的是自己定义好的本地数据
        int size = collectionList.size();
        for (int i = 0; i < size; i++) {
            if (adapter.isSelected.get(i)) {
                collectionList.get(i).setSelect(true);
            } else {
                collectionList.get(i).setSelect(false);
            }
        }

        //此处借助Iterator来删除List集合中的某个对象
        Iterator<Collection> iter = collectionList.iterator();
        while (iter.hasNext()) {
            Collection massage = iter.next();
            //如果是选中状态,则将其从集合中移除
            if (massage.isSelect()) {
                iter.remove();
            }
        }
        //删除完后,重置CheckBox的状态
        resetState();
    }else{
        Toast.makeText(this,"还没有选择要删除的内容",Toast.LENGTH_SHORT).show();
    }

}

至于这里为什么要借助Iterator来删除List集合中的对象可以参考这篇博客:http://blog.csdn.net/wangpeng047/article/details/7590555

到此,ListViewCheckBox的焦点冲突及CheckBox复用时产生的问题就告一段落了,以后再也不用郁闷在Adapter适配器中复用View时为什么上面选中的CheckBox在往下拉时下面item中的CheckBox也会选中了。

 Activity的完整代码如下所示:

package abner.listview.with.checkbox;

import android.app.Activity;
import android.os.Bundle;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.ListView;
import android.widget.PopupWindow;
import android.widget.TextView;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class MainActivity extends Activity {
    private ListView lv_content;
    private CollectionAdapter adapter;
    private Button btn_edit,btn_back;
    private List<Collection> collectionList;
    private int lastVisibleItem;
    private int totalItemCount;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);
        initViews();
        initEvents();
    }

    private void initEvents() {
        btn_edit.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!adapter.isVisible()) {
                    btn_edit.setText("取消");
                    btn_back.setVisibility(View.VISIBLE);
                    adapter.setVisible(true);
                    showDeletePopupWindow();
                    adapter.notifyDataSetChanged();
                }else{
                    btn_back.setVisibility(View.GONE);
                    btn_edit.setText("编辑");
                    //处理组件的显示状态
                    handleComponentState();
                }
            }
        });

        lv_content.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> arg0, View view, int position, long id) {

                //判断CheckBox是否处于可见状态
                if (adapter.isVisible()) {
                    CollectionAdapter.ViewHolder holder = (CollectionAdapter.ViewHolder) view.getTag();
                    //每次点击item都对checkbox的状态进行改变
                    holder.cb_checkbox.toggle();
                    //同时将CheckBox的状态保存到HashMap中,其中key为点击的位置,value为状态
                    adapter.isSelected.put(position, holder.cb_checkbox.isChecked());

                    //判断是否已经存在,如果已经,则移除,否则添加
                    if (adapter.hasSelected.contains(position)) {
                        adapter.hasSelected.remove(position);
                    } else {
                        adapter.hasSelected.add(position);
                    }
                }
            }
        });

        btn_back.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                //处理组件的显示状态
                handleComponentState();
            }
        });



        lv_content.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {
                if (totalItemCount == lastVisibleItem && scrollState == SCROLL_STATE_IDLE) {
                    addLoadMore();
                }
            }

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                lastVisibleItem = firstVisibleItem + visibleItemCount;
                MainActivity.this.totalItemCount = totalItemCount;
            }
        });

    }

    /**
     * 处理组件的显示状态
     */
    private void handleComponentState() {
        //隐藏删除的PopupWindow
        dismissDeletePopupWindow();
        //返回时重置CheckBox的状态
        resetState();
        btn_edit.setText("编辑");
        btn_back.setVisibility(View.GONE);
        //设置CheckBox不可见
        adapter.setVisible(false);
        adapter.notifyDataSetChanged();
    }

    private PopupWindow pw_delete;
    private TextView tv_delete;
    private TextView tv_selectAll;
    /**
     * 弹出删除的PopupWindow
     */
    private void showDeletePopupWindow(){
        //加载PopupWindow的布局文件
        View view = LayoutInflater.from(this).inflate(R.layout.delete_popupwindow, null);
        tv_delete = (TextView) view.findViewById(R.id.tv_delete);
        tv_selectAll = (TextView) view.findViewById(R.id.tv_selectAll);
        //实例化PopupWindow
        pw_delete = new PopupWindow(view, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        //设置PopupWindow弹出时的动画
        pw_delete.setAnimationStyle(R.style.PopupWindowAnimation);
        //PopupWindow的显示问题
        pw_delete.showAtLocation(findViewById(R.id.rlayout_root), Gravity.BOTTOM, 0, 0);
        //为删除和全选绑定事件
        tv_delete.setOnClickListener(listener);
        tv_selectAll.setOnClickListener(listener);
    }

    private OnClickListener listener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            switch (v.getId()){
                case R.id.tv_delete:
                    delete();
                    break;
                case R.id.tv_selectAll:
                    if ("全选".equals(tv_selectAll.getText())) {
                        selectAll();
                        tv_selectAll.setText("反选");
                    } else {
                        cancelAll();
                        tv_selectAll.setText("全选");
                    }
                    break;
            }
        }
    };

    /**
     * 隐藏删除的PopupWindow
     */
    private void dismissDeletePopupWindow(){
        if(pw_delete!=null&&pw_delete.isShowing()){
            pw_delete.dismiss();
            pw_delete = null;
        }
    }

    /**
     * 加载更多
     */
    private void addLoadMore() {
        for (int i = 0; i < 5; i++) {
            Collection collection = new Collection("这是加载的收藏的标题"+i,"这是加载的收藏的描述"+i);
            collectionList.add(collection);
            adapter.setList(collectionList);
            adapter.notifyDataSetChanged();
        }
    }

    /**
     * 重置CheckBox的状态
     */
    private void resetState() {
        for (int i = 0; i < collectionList.size(); i++) {
            adapter.isSelected.put(i, false);
            adapter.hasSelected.clear();
        }
        tv_selectAll.setText("全选");
        adapter.notifyDataSetChanged();
    }

    /**
     * 初始化组件
     */
    public void initViews() {
        lv_content = (ListView) findViewById(R.id.list_view);
        btn_edit = (Button) findViewById(R.id.btn_edit);
        btn_back = (Button) findViewById(R.id.btn_back);
        adapter = new CollectionAdapter(this, initDatas());
        lv_content.setAdapter(adapter);

    }

    /**
     * 初始化数据
     *
     * @return
     */
    public List<Collection> initDatas() {
        collectionList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Collection mas = new Collection("这是收藏的标题"+i,"这是收藏的描述"+i);
            collectionList.add(mas);
        }
        return collectionList;
    }

    /**
     * 删除所选数据
     */
    public void delete() {
        //如果有选择要删除的内容才进行删除,否则提示用户还没有选择要删除的内容
        if(adapter.hasSelected.size()>0) {
            //此处删除的是自己定义好的本地数据
            int size = collectionList.size();
            for (int i = 0; i < size; i++) {
                if (adapter.isSelected.get(i)) {
                    collectionList.get(i).setSelect(true);
                } else {
                    collectionList.get(i).setSelect(false);
                }
            }

            //此处借助Iterator来删除List集合中的某个对象
            Iterator<Collection> iter = collectionList.iterator();
            while (iter.hasNext()) {
                Collection massage = iter.next();
                //如果是选中状态,则将其从集合中移除
                if (massage.isSelect()) {
                    iter.remove();
                }
            }
            //删除完后,重置CheckBox的状态
            resetState();
        }else{
            Toast.makeText(this,"还没有选择要删除的内容",Toast.LENGTH_SHORT).show();
        }

    }

    /**
     * 全选
     */
    public void selectAll() {
        for (int i = 0; i < collectionList.size(); i++) {
            adapter.isSelected.put(i, true);
            adapter.hasSelected.add(i);
            collectionList.get(i).setSelect(true);
            adapter.notifyDataSetChanged();
        }
    }

    /**
     * 取消全选
     */
    public void cancelAll() {
        for (int i = 0; i < collectionList.size(); i++) {
            adapter.isSelected.put(i, false);
            adapter.hasSelected.clear();
            collectionList.get(i).setSelect(false);
            adapter.notifyDataSetChanged();
        }
    }
}

源码