expandablelistview
前言
二级菜单这个功能,相信很多APP都需要这个功能,而我最近的项目中也有这样的需求。正常情况下,快捷的实现方式是使用Android提供的二级菜单控件——expandablelistview,并编写相应的adapter,继承自BaseExpandableListAdapter即可。而非正常情况下,就是自己去实现这个二级菜单控件功能,而实现的基础就是RecyclerView。
ExpandableListView:优点是便捷,逻辑处理Android系统已经都处理好了,很容易实现这个功能;缺点是都封装好了,可扩展性不高。
RecyclerView:优点就是扩展性高;缺点当然就是这些扩展性都需要自己实现,实现起来稍微费点时间。
但是呢,我们不怕麻烦,为了以后的代码有好的可扩展性,还是选择走这个非正常的情况。。。。
ExpandableListView,了解一下
在开始之前,我们也稍稍了解一下Android系统提供的这个二级菜单控件,这样可以更好的让我们了解实现二级菜单列表的原理,更好的开展我们的自定义的二级菜单列表。
ExpandableListView 是什么?
官方给出的解释是:
A view that shows items in a vertically scrolling two-level list. This differs from the ListView by allowing two levels: groups which can inpidually be expanded to show its children. The items come from the ExpandableListAdapter associated with this view.
简单翻译一下就是:
一种用于垂直滚动展示两级列表的视图,和 ListView 的不同之处就是它可以展示两级列表,分组可以单独展开显示子选项。这些选项的数据是通过 ExpandableListAdapter 关联的。
从上面的翻译,我们就可以了解到,其实二级菜单列表与一级菜单列表一样,都是通过Adapter来提供数据源的,因而实际使用的时候主要也是实现这个Adapter中的数据关联逻辑即可。
所以,简单的实现ExpandableListView功能来了解这个控件。
1 . 定义布局文件,这里就不多说了,类似于ListView,就是一个ExpandableListView控件
<ExpandableListView
android:id="@+id/expand_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
2 . 定义数据,分组的数据是个一维数组,子列表的数据是个二维数组——子列表依附于某个分组,本身还有索引,所以要定义成二维的。
public String[] groups = {"A", "B", "C", "D"};
public String[][] children = {
{"A1", "A2"},
{"B1"},
{"C1", "C2", "C3"},
{"D1", "D2", "D3", "D4", "D5", "D6"}
};
3 . 定义分组的视图和子选项的视图,就像定义ListView的item布局一样,不过这里是要定义两个布局分别显示组和子项,用最简单的 TextView显示文字即可,不再列代码。
4 . 最后就是关键的一步,继承BaseExpandableListAdapter,实现自定义的Adapter。直接上代码吧,在代码中有解释每一个方法的作用。
// 获得组数
@Override
public int getGroupCount() {
return groups.length;
}
// 获得组的子项个数
@Override
public int getChildrenCount(int groupPosition) {
return children[groupPosition].length;
}
// 获得某组数据
@Override
public Object getGroup(int groupPosition) {
return groups[groupPosition];
}
// 获得指定子项
@Override
public Object getChild(int groupPosition, int childPosition) {
return children[groupPosition][childPosition];
}
// 获取组ID, 这个ID必须是唯一的
@Override
public long getGroupId(int groupPosition) {
return groupPosition;
}
// 获取子项ID, 这个ID必须是唯一的
@Override
public long getChildId(int groupPosition, int childPosition) {
return childPosition;
}
// 分组和子选项是否持有稳定的ID, 就是说底层数据的改变会不会影响到它们。
@Override
public boolean hasStableIds() {
return true;
}
// 获取组视图
@Override
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
GroupViewHolder groupViewHolder;
if (convertView == null) {
convertView = LayoutInflater.from(MainActivity.this).inflate(R.layout.item_expand_group, parent, false);
groupViewHolder = new GroupViewHolder();
groupViewHolder.tvtitle = (TextView) convertView.findViewById(R.id.label_expand_group);
convertView.setTag(groupViewHolder);
} else {
groupViewHolder = (GroupViewHolder) convertView.getTag();
}
groupViewHolder.tvTitle.setText(groups[groupPosition]);
return convertView;
}
// 获取指定组的指定子项视图
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
ChildViewHolder childViewHolder;
if (convertView == null) {
convertView = LayoutInflater.from(MainActivity.this).inflate(R.layout.item_expand_child, parent, false);
childViewHolder = new ChildViewHolder();
childViewHolder.tvTitle = (TextView) convertView.findViewById(R.id.label_expand_child);
convertView.setTag(childViewHolder);
} else {
childViewHolder = (ChildViewHolder) convertView.getTag();
}
childViewHolder.tvTitle.setText(children[groupPosition][childPosition]);
return convertView;
}
// 子项是否可选
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true;
}
static class GroupViewHolder {
TextView tvTitle;
}
static class ChildViewHolder {
TextView tvTitle;
}
5 . 为ExpandableListView添加Adapter
expandableListView.setAdapter(new MyExpandableListAdapter());
6 . 为组列表项以及子列表项分别设置点击事件,Android系统的api中提供了设置对应列表项点击事件的api,调用即可。
setOnChildClickListener
setOnGroupClickListener
setOnGroupCollapseListener
setOnGroupExpandListener
通过方法名我们就能知道各自的用途,它们分别设置单击子选项、单击分组项、分组合并、分组展开的监听器。
// 设置分组点击事件
expandableListView.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
@Override
public boolean onGroupClick(ExpandableListView expandableListView, View view, int i, long l) {
toast.maketext(getApplicationcontext(), groups[i], Toast.LENGTH_SHORT).show();
// 请务必返回 false,否则分组不会展开
return false;
}
// 设置子项点击事件
expandableListView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
@Override
public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) {
Toast.makeText(getApplicationContext(), children[groupPosition][childPosition], Toast.LENGTHshow();
return true;
}
});
到此,使用ExpandableListView实现二级菜单列表的功能完成,其中主要的代码量就是实现数据源适配器,在实现的过程也没有什么逻辑处理,分别实现对应的方法功能即可。
通过上面的过程,我们了解到Android系统为我们提供的二级菜单列表是怎么样工作的,其中包含了哪些方法,分别需要实现怎么样的逻辑。那么接下来使用RecyclerView控件,来实现类似的二级菜单列表功能。
RecyclerView实现二级列表
基础
在使用 RecyclerView 的时候,主要的工作也是定义一个数据源适配器Adapter,而在创建 RecyclerView 的 Adapter 的时候,一般需要重载以下几个方法:
onCreateViewHolder() 为每个项目创建 ViewHolder
onBindViewHolder() 处理每个 item
getItemViewType() 在 onCreateViewHolder 前调用,返回 item 类型
getItemCount() 获取 item 总数
加载 RecyclerView 的过程如下图:
从上图可知,需要实现以下方法
1 . getItemCount:在实现中,我们将组项和已展开的子项都计算到Count中,所以此方法返回的个数包含所有组项以及已展开的子项个数和;
2 .getItemViewType:因为我们将组项和子项都看做是列表项,所以在列表中,我们需要区分当前列表项是组项还是子项;
3 .onCreateViewHolder:我们需要在这个方法中,创建列表项对应的RecyclerView.ViewHolder,因为在上一步中,我们已经得到了当前列表项的类型,所以在这里可以根据上一步的结果,来创建对应类型的列表项Holder;
4 .onBindViewHolder:上面创建好Holder后,在这个方法中就需要绑定列表项中的控件,并显示控件内容。
搞懂了我们要实现的内容,接下来就开始我们的实现过程。
实现过程
1 . Adapter数据源类型
组项与子项之间是1对多的关系,所以定义如下的数据源类型:
public class DataListTree<K,V> {
private K mGroupItem;
private List<V> mSubItem;
public DataListTree(K groupItem, List<V> subItem) {
mGroupItem = groupItem;
mSubItem = subItem;
}
public K getGroupItem() {
return mGroupItem;
}
public List<V> getSubItem() {
return mSubItem;
}
}
2 . 列表项类型
因为在上面的认知中,我们将组项和子项都看做是列表项,所以就需要对列表项进行类别划分,找出组项以及子项类型,并且标识组项索引以及对应的子项索引。所以定义如下列表项类封装我们需要的内容:
public class ItemStatus {
public static final int VIEW_TYPE_GROUP_ITEM = 0;
public static final int VIEW_TYPE_SUB_ITEM = 1;
private int mViewType; // item类型:group or sub
private int mGroupItemIndex; // 一级列表索引
private int mSubItemIndex = -1; // 二级列表索引
public int getViewType() {
return mViewType;
}
public void setViewType(int viewType) {
mViewType = viewType;
}
public int getGroupItemIndex() {
return mGroupItemIndex;
}
public void setGroupItemIndex(int groupItemIndex) {
mGroupItemIndex = groupItemIndex;
}
public int getSubItemIndex() {
return mSubItemIndex;
}
public void setSubItemIndex(int subItemIndex) {
mSubItemIndex = subItemIndex;
}
}
3 . 定义组和子项布局文件
在这里只简单显示字符串内容,所以两个布局文件都只有一个TextView控件
expandalbe_group_item.xml
<?xml version="1.0" encoding="utf-8"?>
<relativelayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/item_bg">
<TextView
android:id="@+id/group_item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="15dp"
android:textSize="18sp"
android:textStyle="bold"/>
</RelativeLayout>
expandalbe_sub_item.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/item_bg">
<TextView
android:id="@+id/sub_item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:padding="12dp"
/>
</RelativeLayout>
4 . Adapter适配器实现(重点)
自定义一个ExpandableRecyclerAdapter,继承自RecyclerView.Adapter,并重写相应的方法。
public class ExpandableRecyclerAdapter extends RecyclerView.Adapter
定义三个全局变量:
private Context mContext;
private List<DataListTree<String, String>> mDataListTrees; // 数据源(暂时只显示字符串类型)
private List<Boolean> mGroupItemStatus; // 保存一级标题的开关状态
4.1 ViewHolder的实现
两种列表项的ViewHolder定义:
/**
* 组项ViewHolder
*/
static class GroupItemViewHolder extends RecyclerView.ViewHolder {
TextView mGroupItemTitle;
GroupItemViewHolder(View itemView) {
super(itemView);
mGroupItemTitle = (TextView) itemView.findViewById(R.id.group_item_title);
}
}
/**
* 子项ViewHolder
*/
static class SubItemViewHolder extends RecyclerView.ViewHolder {
TextView mSubItemTitle;
SubItemViewHolder(View itemView) {
super(itemView);
mSubItemTitle = (TextView) itemView.findViewById(R.id.sub_item_title);
}
}
4.2 Adapter的数据源初始化
/**
* 设置显示的数据
*
* @param dataListTrees
*/
public void setData(List<DataListTree<String, String>> dataListTrees) {
this.mDataListTrees = dataListTrees;
initGroupItemStatus();
notifydatasetChanged();
}
/**
* 初始化一级列表开关状态
*/
private void initGroupItemStatus() {
mGroupItemStatus = new ArrayList<>();
for (int i = 0; i < mDataListTrees.size(); i++) {
mGroupItemStatus.add(false);
}
}
4.3 获取指定位置(Position)的列表项状态(重点)
按照上面的图示,定义这样的方法:根据方法参数position指定的列表项,分别获取列表项所对应的组索引、子项索引(如果是二级标题的话)以及列表项类型,即返回一个之前定义的ItemStatus类型实例。代码如下:
/**
* 根据item的位置,获取当前Item的状态
*
* @param position 当前item的位置(此position的计数包含groupItem和subItem合计)
* @return 当前Item的状态(此Item可能是groupItem,也可能是SubItem)
*/
private ItemStatus getItemStatUSByPosition(int position) {
ItemStatus itemStatus = new ItemStatus();
int itemCount = 0;
int i;
//轮询 groupItem 的开关状态
for (i = 0; i < mGroupItemStatus.size(); i++) {
if (itemCount == position) { //position刚好等于计数时,item为groupItem
itemStatus.setViewType(ItemStatus.VIEW_TYPE_GROUP_ITEM);
itemStatus.setGroupItemIndex(i);
break;
} else if (itemCount > position) { //position大于计数时,item为groupItem(i - 1)中的某个subItem
itemStatus.setViewType(ItemStatus.VIEW_TYPE_SUB_ITEM);
itemStatus.setGroupItemIndex(i - 1); // 指定的position组索引
// 计算指定的position前,统计的列表项和
int temp = (itemCount - mDataListTrees.get(i - 1).getSubItem().size());
// 指定的position的子项索引:即为position-之前统计的列表项和
itemStatus.setSubItemIndex(position - temp);
break;
}
itemCount++;
if (mGroupItemStatus.get(i)) {
itemCount += mDataListTrees.get(i).getSubItem().size();
}
}
// 轮询到最后一组时,未找到对应位置
if (i >= mGroupItemStatus.size()) {
itemStatus.setViewType(ItemStatus.VIEW_TYPE_SUB_ITEM); // 设置为二级标签类型
itemStatus.setGroupItemIndex(i - 1); // 设置一级标签为最后一组
itemStatus.setSubItemIndex(position - (itemCount - mDataListTrees.get(i - 1).getSubItem().size()));
}
return itemStatus;
}
接下来 ,按照RecyclerView.Adapter覆写方法的执行顺序来依次重写对应的方法。
4.4 getItemCount获取当前显示的列表项数目
返回的结果中,包含当前已经展开的的二级列表数目,所以方法功能如下:
@Override
public int getItemCount() {
int itemCount = 0;
if (0 == mGroupItemStatus.size()) {
return itemCount;
}
for (int i = 0; i < mDataListTrees.size(); i++) {
itemCount++; // 每个一级标题项+1
if (mGroupItemStatus.get(i)) { // 二级标题展开时,再加上二级标题的数量
itemCount += mDataListTrees.get(i).getSubItem().size();
}
}
return itemCount;
}
4.5 getItemViewType获取指定position的列表项类型
由于之前定义的方法getItemStatusByPosition与当前方法的参数相同,且返回了一个标识列表项状态的对象实例,所以getItemViewType这个方法就可以这样实现:
@Override
public int getItemViewType(int position) {
return getItemStatusByPosition(position).getViewType();
}
4.6 onCreateViewHolder创建列表项Holder
需要判断当前列表项的类型,来初始化不同的Holder
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view;
RecyclerView.ViewHolder viewHolder = null;
if (viewType == ItemStatus.VIEW_TYPE_GROUP_ITEM) {
// parent需要传入对应的位置,否则列表项触发不了点击事件
view = LayoutInflater.from(mContext).inflate(R.layout.expandalbe_group_item, parent, false);
viewHolder = new GroupItemViewHolder(view);
} else if (viewType == ItemStatus.VIEW_TYPE_SUB_ITEM) {
view = LayoutInflater.from(mContext).inflate(R.layout.expandalbe_sub_item, parent, false);
viewHolder = new SubItemViewHolder(view);
}
return viewHolder;
}
4.7 onBindViewHolder绑定ViewHolder,显示列表项内容
方法中需要分别处理组以及子项列表,对于组列表还需要判断是打开还是关闭组列表。
另外对于组的处理方式中,代码中做了两种处理:1. 可打开多组一级列表的功能; 2. 只保证有一组列表处于打开的状态。
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
ItemStatus itemStatus = getItemStatusByPosition(position); // 获取列表项状态
final DataListTree data = mDataListTrees.get(itemStatus.getGroupItemIndex());
if (itemStatus.getViewType() == ItemStatus.VIEW_TYPE_GROUP_ITEM) { // 组类型
GroupItemViewHolder groupItemViewHolder = (GroupItemViewHolder) holder;
groupItemViewHolder.mGroupItemTitle.setText((Charsequence) data.getGroupItem());
int groupIndex = itemStatus.getGroupItemIndex(); // 组索引
groupItemViewHolder.itemView.setOnClickListener(v -> {
if (mGroupItemStatus.get(groupIndex)) { // 一级标题打开状态
mGroupItemStatus.set(groupIndex, false);
notifyItemRangeRemoved(groupItemViewHolder.getAdapterPosition() + 1, data.getSubItem().size());
} else { // 一级标题关闭状态
initGroupItemStatus(); // 1. 实现只展开一个组的功能,缺点是没有动画效果
mGroupItemStatus.set(groupIndex, true);
notifyDataSetChanged(); // 1. 实现只展开一个组的功能,缺点是没有动画效果
// notifyItemRangeInserted(groupItemViewHolder.getAdapterPosition() + 1, data.getSubItem().size()); // 2. 实现展开可多个组的功能,带动画效果
}
});
} else if (itemStatus.getViewType() == ItemStatus.VIEW_TYPE_SUB_ITEM) { // 子项类型
SubItemViewHolder subItemViewHolder = (SubItemViewHolder) holder;
subItemViewHolder.mSubItemTitle.setText((CharSequence) data.getSubItem().get(itemStatus.getSubItemIndex()));
subItemViewHolder.itemView.setOnClickListener(v -> ToastUtil.makeText(mContext, mDataListTrees.get(itemStatus.getGroupItemIndex()).getSubItem().get(itemStatus.getSubItemIndex()), Toast.LENGTH_SHORT).show());
}
}
至此,使用RecyclerView实现二级菜单列表的功能完成,最终效果图如下:
DEMO传送门
在此,感谢此博主的分享:
https://blog.csdn.net/yuhys/article/details/70228591
相关阅读
ExpandableListView二级列表购物车,MVP获取数据
android中常常要用到ListView,有时也要用到ExpandableListView,如在手机设置中,对于分类有很好的效果,会用ListView的人一定会用Ex
吐槽 每次写个博客都要吐槽一下,这估计是我博客的习惯吧,,,,好了不多说了,上一篇讲了安卓的dialog的知识,但是产品给的需求还是弄不了,需