马上就要告别2017了,突然发现自己两个三月没有写博客了,今天决定整理一下最近用到的这个ExpandableListView控件,以后要是忘了也可以看看自己的博客,如果对各位有所帮助,那样不错。就作为2017年最后一篇博客吧。(虽然我也没有写几篇,有时候太懒不想写,自我鄙视一下)ExpandableListView可扩展列表,顾名思义就是一个可以收缩展开的控件。主要实现的功能是所有组元素以及组元素下面的子元素的全选和反选,以及组元素的复选实现下面的子元素的全选。这句话可能比较绕,不明白的可以去玩一下淘宝购物车的那个全选就知道了。好了先来看两张效果图。
先看下布局吧。
//activity布局很简单,就是一个ExpandableListView控件,然后下面放了一个全选的checkbox按钮
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.lcsb.shoppingcheckboxdemo.MainActivity"
android:orientation="vertical">
<ExpandableListView
android:id="@+id/elv_card_plane_num"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<CheckBox
android:id="@+id/ck_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
//组元素的布局(一级列表的布局)同样很简单,就是一个checkbox按钮和一个TextView
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:descendantFocusability="blocksDescendants"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="26dp"
android:gravity="center_vertical"
android:padding="10dp">
<CheckBox
android:id="@+id/ck_shop_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tv_shop_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="一号商店"
android:textColor="@color/colorAccent"/>
</LinearLayout>
</LinearLayout>
//子元素的布局 (二级列表的布局)一个checkbox按钮和一个TextView
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="46dp"
android:padding="10dp">
<CheckBox
android:id="@+id/ck_shop_product"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"/>
<TextView
android:id="@+id/tv_shop_product_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="@color/backgray"
android:text="键盘"/>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="@color/backgray"/>
</LinearLayout>
三个布局已经做好了,现在我们来看一下父元素和子元素对应的数据实体类。
首先父元素的实体类,有三个参数,父元素的ID,name,以及isStoreChoosed字段,判断父元素是否选中的boolean值。ID和name字段只是这里demo测试,实际开发时已实际接口参数为准,下面的子元素中的参数同理。
public class FatherInfo {
private String id;
private String name;
private boolean isStoreChoosed;
public FatherInfo(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isStoreChoosed() {
return isStoreChoosed;
}
public void setStoreChoosed(boolean storeChoosed) {
isStoreChoosed = storeChoosed;
}
}
//子元素实体类
public class ChildInfo {
private String id;
private String name;
private boolean isGoodsChoosed;
public ChildInfo(String id, String name){
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isGoodsChoosed() {
return isGoodsChoosed;
}
public void setGoodsChoosed(boolean goodsChoosed) {
isGoodsChoosed = goodsChoosed;
}
}
//接下来是最重要的适配器代码,完整代码如下:
每个方法都添加了注释,应该很好理解,这里就不再啰嗦了。
public class ShopAdapter extends BaseExpandableListAdapter {
private List<FatherInfo>mslist;//组元素列表
private Map<String, List<ChildInfo>> mglist; //这个String对应着StoreInfo的Id,也就是店铺的Id
private Context mcontext;
private CheckInterface checkInterface;
public ShopAdapter(List<FatherInfo>slist, Map<String,List<ChildInfo>>glist, Context context) {
this.mslist=slist;
this.mglist=glist;
this.mcontext=context;
}
//父列表项的数目
@Override
public int getGroupCount() {
return mslist.size();
}
//子列表项的数目
@Override
public int getChildrenCount(int groupPosition) {
String groupId = mslist.get(groupPosition).getId();//获取父列表项对应位置的ID,即拿出键值对的键(key)
return mglist.get(groupId).size();//返回对应父列表项ID下存了多少个键值对,就是子列表的长度
}
//获得父列表项
@Override
public Object getGroup(int groupPosition) {
return mslist.get(groupPosition);
}
//获取子列表项
@Override
public Object getChild(int groupPosition, int childPosition) {
List<ChildInfo>childlist=mglist.get(mslist.get(groupPosition).getId());
return childlist.get(childPosition);
}
//获取父列表项id
@Override
public long getGroupId(int groupPosition) {
return groupPosition;//返回对应父组件位置,就有对应的父组件ID了
}
//获取子列表项id
@Override
public long getChildId(int groupPosition, int childPosition) {
return childPosition;//同理
}
@Override
public boolean hasStableIds() {
return false;//默认返回flase。不用管
}
//绑定父列表项的布局
@Override
public View getGroupView(final int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
final GroupViewHolder groupViewHolder;
if (convertView == null) {
convertView = View.inflate(mcontext, R.layout.item_shop, null);
groupViewHolder = new GroupViewHolder(convertView);
convertView.setTag(groupViewHolder);
}
else {
groupViewHolder = (GroupViewHolder) convertView.getTag();
}
final FatherInfo fatherInfo = (FatherInfo) getGroup(groupPosition);
groupViewHolder.tv_shop_name.setText(fatherInfo.getName());//给父组件的标题设置值
//这里不能用setOnChecklistener,因为需要每次点击的时候,去判断父组件和子组件的状态。
// 如果是用checkbox本身的状态判断,就会出现未点击的时候,其他控件无法改变此处的状态
//父组件的复选框点击
groupViewHolder.ck_shop_all.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
fatherInfo.setStoreChoosed(((CheckBox)v).isChecked());
checkInterface.checkGroup(groupPosition, ((CheckBox)v).isChecked());//父组件的复选框回调监听
}
});
//设置父组件的全选状态
groupViewHolder.ck_shop_all.setChecked(fatherInfo.isStoreChoosed());
return convertView;
}
//绑定子列表项的布局
@Override
public View getChildView(final int groupPosition, final int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
final ChildViewHolder childViewHolder;
if (convertView == null) {
convertView = View.inflate(mcontext, R.layout.item_shop_product, null);
childViewHolder = new ChildViewHolder(convertView);
convertView.setTag(childViewHolder);
} else {
childViewHolder = (ChildViewHolder) convertView.getTag();
}
final ChildInfo child = (ChildInfo) getChild(groupPosition, childPosition);
childViewHolder.tv_shop_product_name.setText(child.getName());
//子列表项的复选框
childViewHolder.ck_shop_product.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
child.setGoodsChoosed(((CheckBox) v).isChecked());
childViewHolder.ck_shop_product.setChecked(((CheckBox) v).isChecked());
checkInterface.checkChild(groupPosition, childPosition, ((CheckBox) v).isChecked());//子列表复选框的回调监听
}
});
childViewHolder.ck_shop_product.setChecked(child.isGoodsChoosed());//设置子元素的复选框状态
return convertView;
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return false;
}
//父组件Holder
static class GroupViewHolder {
private CheckBox ck_shop_all;
private TextView tv_shop_name;
public GroupViewHolder(View view) {
ck_shop_all= (CheckBox) view.findViewById(R.id.ck_shop_all);
tv_shop_name= (TextView) view.findViewById(R.id.tv_shop_name);
}
}
//子组件的Holder
static class ChildViewHolder{
private CheckBox ck_shop_product;
private TextView tv_shop_product_name;
public ChildViewHolder(View v) {
ck_shop_product= (CheckBox) v.findViewById(R.id.ck_shop_product);
tv_shop_product_name= (TextView) v.findViewById(R.id.tv_shop_product_name);
}
}
public interface CheckInterface {
/**
* 组选框状态改变触发的事件
* @param groupPosition 组元素的位置
* @param isChecked 组元素的选中与否
*/
void checkGroup(int groupPosition, boolean isChecked);
/*
* 子选框状态改变触发的事件
* @param groupPosition 组元素的位置
* @param childPosition 子元素的位置
* @param isChecked 子元素的选中与否
*/
void checkChild(int groupPosition, int childPosition, boolean isChecked);
}
public CheckInterface getCheckInterface() {
return checkInterface;
}
public void setCheckInterface(CheckInterface checkInterface) {
this.checkInterface = checkInterface;
}
}
//最后看一下activity中的代码
public class MainActivity extends AppCompatActivity implements ShopAdapter.CheckInterface {
private CheckBox ck_all;//全选
private ExpandableListView elv_card_plane_num;//可扩展的列表控件
private ShopAdapter shopAdapter;
private List<FatherInfo> groups; //组元素的列表
private Map<String, List<ChildInfo>> childs; //子元素的列表
private Context mcontext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ck_all= (CheckBox) findViewById(R.id.ck_all);
elv_card_plane_num= (ExpandableListView) findViewById(R.id.elv_card_plane_num);
initData();//初始化加载数据
mcontext=this;
shopAdapter = new ShopAdapter(groups, childs, mcontext);//实例化ExpandableListView的适配器
shopAdapter.setCheckInterface(this);//设置复选框的接口
// elv_card_plane_num.setGroupIndicator(null); //设置属性 GroupIndicator 去掉向下箭头
elv_card_plane_num.setAdapter(shopAdapter);//绑定数据
for (int i = 0; i < shopAdapter.getGroupCount(); i++) {
elv_card_plane_num.expandGroup(i); //将ExpandableListView以展开的方式显示,(屏蔽可收缩起来。)
}
//全选监听,这里不能用setOnCheckedChangeListener这个监听函数,adapter里面解释了
ck_all.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doCheckAll();
}
});
}
/**组元素监听的回调函数实现
* @param groupPosition 组元素的位置
* @param isChecked 组元素的选中与否
* 思路:组元素被选中了,那么下辖全部的子元素也被选中
*/
@Override
public void checkGroup(int groupPosition, boolean isChecked) {
FatherInfo group = groups.get(groupPosition);//获取组元素的位置
List<ChildInfo> child = childs.get(group.getId());//拿到对应组元素的ID
for (int i = 0; i < child.size(); i++) {
child.get(i).setGoodsChoosed(isChecked);
}
if (isCheckAll()) {
ck_all.setChecked(true);//全选
} else {
ck_all.setChecked(false);//反选
}
shopAdapter.notifyDataSetChanged();//刷新
}
//判断组元素是否全部选中
private boolean isCheckAll() {
for (FatherInfo group : groups) {
if (!group.isStoreChoosed()) {
return false;
}
}
return true;
}
/**子列表想复选框的回调函数实现
* @param groupPosition 组元素的位置
* @param childPosition 子元素的位置
* @param isChecked 子元素的选中与否
*/
@Override
public void checkChild(int groupPosition, int childPosition, boolean isChecked) {
boolean allChildSameState = true; //判断该组下面的所有子元素是否处于同一状态
FatherInfo group = groups.get(groupPosition);
List<ChildInfo> child = childs.get(group.getId());
for (int i = 0; i < child.size(); i++) {
//不选全中
if (child.get(i).isGoodsChoosed() != isChecked) {
allChildSameState = false;
break;
}
}
if (allChildSameState) {
group.setStoreChoosed(isChecked);//如果子元素状态相同,那么对应的组元素也设置成这一种的同一状态
} else {
group.setStoreChoosed(false);//否则一律视为未选中
}
if (isCheckAll()) {
ck_all.setChecked(true);//全选
} else {
ck_all.setChecked(false);//反选
}
shopAdapter.notifyDataSetChanged();
}
//初始化数据
private void initData() {
groups = new ArrayList<FatherInfo>();
childs = new HashMap<String, List<ChildInfo>>();
for (int i = 0; i < 6; i++) {
groups.add(new FatherInfo("组元素ID:"+ i, (i+1)+"号商店" ));//ID和NAME(实际运用时以具体参数为准)
List<ChildInfo> goods = new ArrayList<>();
for (int j = 0; j < 3; j++) {
goods.add(new ChildInfo("商品ID:"+i,"商品"+(j+1)));//ID和NAME(实际运用时以具体参数为准)
}
childs.put(groups.get(i).getId(), goods);
}
}
//全选或者取消全选,根据状态来改变
private void doCheckAll() {
for (int i = 0; i < groups.size(); i++) {
FatherInfo group = groups.get(i);
group.setStoreChoosed(ck_all.isChecked());//根据全选按钮的状态设置父组件的选中状态
List<ChildInfo> child = childs.get(group.getId());
for (int j = 0; j < child.size(); j++) {
child.get(j).setGoodsChoosed(ck_all.isChecked());//根据全选按钮的状态设置子元素的选中状态
}
}
shopAdapter.notifyDataSetChanged();
}
}
最后说一下可能遇到的坑,以及解决办法。
1.子元素布局中有button和checkbox这种可以获取焦点的控件时,需要在子元素布局的根布局加上这个属性去使子元素能够获取焦点。否则会导致无法收缩和展开
android:descendantFocusability="blocksDescendants"
2.setGroupIndicator()里面设置自定义drawable文件时。drawable文件里面的图片必须是点九图,否则会导致拉伸变形。