07.19 控件-ExpandableListView
ExpandableListView 是 AdapterView 类型的控件,简单理解是一个可折叠的列表,它是ListView的子类, 在ListView的基础上将列表项分为几组,每组里又可包含多个列表项。实现了一种嵌套列表的效果,但是需要使用ExpandableAdapter适配器。
# 基本属性和方法
常用属性:
android:childDivider:指定各组内子类表项之间的分隔条,图片不会完全显示, 分离子列表项的是一条直线android:childIndicator:显示在子列表旁边的Drawable对象,可以是一个图像android:childIndicatorEnd:子列表项指示符的结束约束位置android:childIndicatorLeft:子列表项指示符的左边约束位置android:childIndicatorRight:子列表项指示符的右边约束位置android:childIndicatorStart:子列表项指示符的开始约束位置android:groupIndicator:显示在组列表旁边的Drawable对象,可以是一个图像android:indicatorEnd:组列表项指示器的结束约束位置android:indicatorLeft:组列表项指示器的左边约束位置android:indicatorRight:组列表项指示器的右边约束位置android:indicatorStart:组列表项指示器的开始约束位置
# 实现ExpandableAdapter的几种方式
- 扩展
BaseExpandableListAdpter实现 ExpandableAdapter - 使用
SimpleExpandableListAdpater将两个List集合包装成ExpandableAdapter - 使用
simpleCursorTreeAdapter将Cursor中的数据包装成SimpleCuroTreeAdapter
# 基本使用示例
ExpandableListView 最常见的使用方式就是扩展 BaseExpandableListAdapter 实现适配器,绑定自定义的组布局和子项布局。下面就来看看这种常见的实现方式。
- 定义列表的数据项对象:组数据对象 GroupBean 和子项数据对象 ChildBean
/**
* 定义组数据对象.
*/
public class GroupBean {
// 组图标
private int icon;
// 组标题
private String title;
// 组包含的子项数据
private List<ChildBean> children;
public GroupBean(int icon, String title, List<ChildBean> children) {
this.icon = icon;
this.title = title;
this.children = children;
}
public int getIcon() {
return icon;
}
public void setIcon(int icon) {
this.icon = icon;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public List<ChildBean> getChildren() {
return children;
}
public void setChildren(List<ChildBean> children) {
this.children = children;
}
}
/**
* 定义子项数据对象.
*/
public class ChildBean {
// 子项内容
private String content;
public ChildBean(String content) {
this.content = content;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
- 扩展 BaseExpandableListAdapter 自定义实现适配器:ExpandableListViewAdapter
/**
* ExpandableListView 列表适配器.
*/
public class ExpandableListViewAdapter extends BaseExpandableListAdapter {
private Context mContext;
private List<GroupBean> mGroupBeans;
public ExpandableListViewAdapter(Context context, List<GroupBean> groupBeans) {
mContext = context;
mGroupBeans = groupBeans;
}
// 返回组的数量
@Override
public int getGroupCount() {
return mGroupBeans == null ? 0 : mGroupBeans.size();
}
// 返回指定组中子项的数量
@Override
public int getChildrenCount(int groupPosition) {
if (mGroupBeans == null || mGroupBeans.get(groupPosition) == null) {
return 0;
}
List<ChildBean> children = mGroupBeans.get(groupPosition).getChildren();
return children == null ? 0 : children.size();
}
// 返回指定组的组数据对象
@Override
public Object getGroup(int groupPosition) {
return mGroupBeans == null ? null : mGroupBeans.get(groupPosition);
}
// 返回指定组中指定位置的子项数据对象
@Override
public Object getChild(int groupPosition, int childPosition) {
if (getGroup(groupPosition) == null) {
return null;
}
List<ChildBean> children = mGroupBeans.get(groupPosition).getChildren();
return children == null ? null : children.get(childPosition);
}
// 返回组的ID
@Override
public long getGroupId(int groupPosition) {
return groupPosition;
}
// 返回子项的ID
@Override
public long getChildId(int groupPosition, int childPosition) {
return childPosition;
}
// 组和子项的ID是否持有稳定的ID(跨底层数据的更改),一般返回false即可
@Override
public boolean hasStableIds() {
return false;
}
// 返回指定组的View视图
@Override
public View getGroupView(int groupPosition, boolean isExpanded,
View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(mContext)
.inflate(R.layout.layout_expandable_listview_group, parent, false);
}
ImageView ivIcon = convertView.findViewById(R.id.iv_icon);
TextView tvTitle = convertView.findViewById(R.id.tv_title);
TextView tvCount = convertView.findViewById(R.id.tv_count);
GroupBean groupBean = mGroupBeans.get(groupPosition);
ivIcon.setBackgroundResource(groupBean.getIcon());
tvTitle.setText(groupBean.getTitle());
tvCount.setText(String.format("(%d)", getChildrenCount(groupPosition)));
return convertView;
}
// 返回指定组中指定位置的子项View视图
@Override
public View getChildView(int groupPosition, int childPosition,
boolean isLastChild, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(mContext)
.inflate(R.layout.layout_expandable_listview_child, parent, false);
}
TextView tvContent = convertView.findViewById(R.id.tv_content);
ChildBean childBean = (ChildBean) getChild(groupPosition, childPosition);
tvContent.setText(childBean.getContent());
return convertView;
}
// 指定位置的子项是否可选中,一般返回true
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true;
}
}
- 自定义组布局:layout_expandable_listview_group.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/iv_icon"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_margin="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:textColor="#333333"
android:textSize="16dp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/iv_icon"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:textColor="#888888"
android:textSize="14dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
- 自定义子项布局:layout_expandable_listview_child.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:textColor="#666666"
android:textSize="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
- xml布局使用
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<ExpandableListView
android:id="@+id/elv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:childDivider="@color/colorDivider"
android:groupIndicator="@drawable/expandable_listview_indicator" />
</LinearLayout>
其中 android:groupIndicator 自定义组指示器(展开/收起状态对应不同的图片),使用 <selector> 的方式实现:expandable_listview_indicator.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 展开状态 -->
<item android:drawable="@mipmap/ic_indicator_expand" android:state_expanded="true" />
<!-- 默认(收起状态) -->
<item android:drawable="@mipmap/ic_indicator_collapse" />
</selector>
- 示例代码
// 初始化组的模拟数据
private List<GroupBean> initGroupData() {
List<GroupBean> groups = new ArrayList<>();
for (int i = 0; i < 4; i++) {
// 初始化组数据
int iconRes;
String title;
switch (i) {
case 0:
iconRes = R.drawable.icon_1;
title = "春天";
break;
case 1:
iconRes = R.drawable.icon_2;
title = "夏天";
break;
case 2:
iconRes = R.drawable.icon_3;
title = "秋天";
break;
case 3:
iconRes = R.drawable.icon_4;
title = "冬天";
break;
default:
iconRes = 0;
title = "";
}
// 初始化子项数据
List<ChildBean> children = new ArrayList<>();
for (int j = 0; j < 3; j++) {
ChildBean childBean = new ChildBean(String.format("%s的第%d项内容", title, j + 1));
children.add(childBean);
}
GroupBean groupBean = new GroupBean(iconRes, title, children);
groups.add(groupBean);
}
return groups;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo);
List<GroupBean> groups = initGroupData();
ExpandableListView expandableListView = findViewById(R.id.elv);
// 设置适配器
ExpandableListViewAdapter adapter = new ExpandableListViewAdapter(this, groups);
expandableListView.setAdapter(adapter);
// 设置组点击监听(返回false则执行默认的展开/收起处理)
expandableListView.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
@Override
public boolean onGroupClick(ExpandableListView parent, View v,
int groupPosition, long id) {
Toast.makeText(getApplicationContext(),
String.format("点击了第%d组:%s", groupPosition, groups.get(groupPosition).getTitle()),
Toast.LENGTH_SHORT).show();
return false;
}
});
// 设置组展开/收起状态变化监听
expandableListView.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener() {
@Override
public void onGroupExpand(int groupPosition) {
Toast.makeText(getApplicationContext(),
String.format("第%d组展开", groupPosition),
Toast.LENGTH_SHORT).show();
}
});
expandableListView.setOnGroupCollapseListener(new ExpandableListView.OnGroupCollapseListener() {
@Override
public void onGroupCollapse(int groupPosition) {
Toast.makeText(getApplicationContext(),
String.format("第%d组收起", groupPosition),
Toast.LENGTH_SHORT).show();
}
});
// 设置子项点击监听
expandableListView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
@Override
public boolean onChildClick(ExpandableListView parent, View v,
int groupPosition, int childPosition, long id) {
ChildBean childBean = groups.get(groupPosition).getChildren().get(childPosition);
Toast.makeText(getApplicationContext(),
String.format("点击了第%d组第%d子项:%s", groupPosition, childPosition, childBean.getContent()),
Toast.LENGTH_SHORT).show();
// 返回true表示消费本次点击事件
return true;
}
});
}
- 展开/收起控制
ExpandableListView 默认是全部收起的状态,可以通过如下方法控制组的展开与收起:
ExpandableListView expandableListView = findViewById(R.id.elv);
// 展开指定组(第二个参数true表示带展开动画)
expandableListView.expandGroup(0, true);
// 收起指定组
expandableListView.collapseGroup(0);
// 指定组是否处于展开状态
boolean isExpanded = expandableListView.isGroupExpanded(0);
// 设置默认全部展开
for (int i = 0; i < adapter.getGroupCount(); i++) {
expandableListView.expandGroup(i);
}
// setOnGroupExpandListener配合可实现"手风琴"效果:同一时间只展开一个组
expandableListView.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener() {
// 记录上一次展开的组
private int lastExpandGroup = -1;
@Override
public void onGroupExpand(int groupPosition) {
// 当有组展开时,收起上一次展开的组,实现互斥展开
if (lastExpandGroup != -1 && lastExpandGroup != groupPosition) {
expandableListView.collapseGroup(lastExpandGroup);
}
lastExpandGroup = groupPosition;
}
});
# 隐藏组指示器
ExpandableListView 默认在组的旁边有一个展开/收起的箭头指示器(groupIndicator),如果不需要显示指示器,可以设置 android:groupIndicator="@null" 或调用 setGroupIndicator(null) 隐藏。
需要注意的是,隐藏指示器后,如果组布局没有其他展开/收起状态的提示,用户可能无法直观地看出组可以点击展开,建议在自定义组布局中自行实现一个随展开状态变化的图标(通过 getGroupView 中的 isExpanded 参数判断):
@Override
public View getGroupView(int groupPosition, boolean isExpanded,
View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(mContext)
.inflate(R.layout.layout_expandable_listview_group, parent, false);
}
// 通过isExpanded判断当前组的展开状态,切换对应的图标
ImageView ivIndicator = convertView.findViewById(R.id.iv_indicator);
ivIndicator.setBackgroundResource(isExpanded
? R.mipmap.ic_indicator_expand : R.mipmap.ic_indicator_collapse);
return convertView;
}
# 数据更新
ExpandableListView 与 ListView 一样,数据的更新也是通过 Adapter 的 notifyDataSetChanged() 来通知刷新的,列表会重新刷新组与子项的视图。这里就不再重复展开了。
# 结束
本节介绍了 Android 中的可折叠列表控件(ExpandableListView)常用属性以及相关使用方法,自定义实现的组布局、子项布局和适配器的方法,以及组的展开/收起控制等,更多属性和用法请参考:ExpandableListView (opens new window)。