二级菜单——ExpandableListView以及用RecyclerView实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/shangming150/article/details/80006016

前言

二级菜单这个功能,相信很多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 individually 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

猜你喜欢

转载自blog.csdn.net/shangming150/article/details/80006016