Android RecyclerView 二级列表实现

[TOC]

Android RecyclerView 二级列表实现

2017.5.16 添加demo

git

Demo

简述

在开发 Android APP 的时候,难免会需要实现二级列表的情况,而在自己的项目中使用的列表是 android.support.v7.widget 包里面的 RecyclerView,好处是可以根据情况实现不同样式的列表,可扩展程度高。而坏处是什么都要自己实现。所以在想要用 RecyclerView 实现的二级列表的时候,却发现没有类似 ListViewExpandableListView,只能自己去实现。

实现基础

在使用 RecyclerView 的时候,与 ListView 类似,需要创建一个 Adapter 去告诉 RecyclerView 如何工作[1],而在创建 RecyclerViewAdapter 的时候,一般需要重载以下几个方法:
onCreateViewHolder() 为每个项目创建 ViewHolder
onBindViewHolder() 处理每个 item
getItemViewType()onCreateViewHolder 前调用,返回 item 类型
getItemCount() 获取 item 总数
加载 RecyclerView 的过程如下图:

flow.PNG

ps简书的MD不支持流程图?

除此之外,还需要创建一个 ViewHolder 用于寻找自定义 item的各个控件。

实现思路

根据上述,在实现二级列表的时候,我们在 onCreateViewHolder()onBindViewHolder() 中,判断是加载一级项目(GroupItem)还是二级子项目(SubItem)。

实现过程

二级列表数据格式

一般来说,一个 GroupItem 下面有一个,或多个 SubItem,一对多。
在这里,用一个 DataTree 来封装这种数据格式,代码如下:

public class DataTree<K, V> {

    private K groupItem;
    private List<V> subItems;

    public DataTree(K groupItem, List<V> subItems) {
        this.groupItem = groupItem;
        this.subItems = subItems;
    }

    public K getGroupItem() {
        return groupItem;
    }

    public List<V> getSubItems() {
        return subItems;
    }
}

RecyclerView Item状态

ItemStatus 用封装列表每一项的状态,包括:
viewType item的类型,group item 还是 subitem
groupItemIndex 一级索引位置
subItemIndex 如果该 item 是一个二级子项目,则保存子项目索引

    private static class ItemStatus {

        public static final int VIEW_TYPE_GROUPITEM = 0;
        public static final int VIEW_TYPE_SUBITEM = 1;

        private int viewType;
        private int groupItemIndex = 0;
        private int subItemIndex = -1;

        public ItemStatus() {
        }

        public int getViewType() {
            return viewType;
        }

        public void setViewType(int viewType) {
            this.viewType = viewType;
        }

        public int getGroupItemIndex() {
            return groupItemIndex;
        }

        public void setGroupItemIndex(int groupItemIndex) {
            this.groupItemIndex = groupItemIndex;
        }

        public int getSubItemIndex() {
            return subItemIndex;
        }

        public void setSubItemIndex(int subItemIndex) {
            this.subItemIndex = subItemIndex;
        }
    }

ViewHolder

这里的 groupItemsubItem 分别用不同的布局文件,所以我把 ViewHolder 分开写,如下:

    public static class GroupItemViewHolder extends RecyclerView.ViewHolder {
        ...
        public GroupItemViewHolder(View itemView) {
            super(itemView);
            ...
        }
    }

    public static class SubItemViewHolder extends RecyclerView.ViewHolder {
        ...
        public SubItemViewHolder(View itemView) {
            super(itemView);
            ...
        }
    }

其它属性和方法

context
list dataTrees 用于显示的数据
list<Boolean> groupItemStatus 保存 groupItem 状态,开还是关

    //向外暴露设置显示数据的方法
    public void setDataTrees(List<DataTree<Album, Track>> dt) {
        this.dataTrees = dt;
        initGroupItemStatus(groupItemStatus);
        notifyDataSetChanged();
    }

    //设置初始值,所有 groupItem 默认为关闭状态
    private void initGroupItemStatus(List l) {
        for (int i = 0; i < dataTrees.size(); i++) {
            l.add(false);
        }
    }

getItemStatusByPosition() 方法实现

顾名思义,用于根据 position 来计算判断该 item 的状态,返回一个 ItemStatus

position

代码如下:

private ItemStatus getItemStatusByPosition(int position) {

        ItemStatus itemStatus = new ItemStatus();

        int count = 0;    //计算groupItemIndex = i 时,position最大值
        int i = 0;

        //轮询 groupItem 的开关状态
        for (i = 0; i < groupItemStatus.size(); i++ ) {
            
            //pos刚好等于计数时,item为groupItem
            if (count == position) {
                itemStatus.setViewType(ItemStatus.VIEW_TYPE_GROUPITEM);
                itemStatus.setGroupItemIndex(i);
                break;
                
            //pos大于计数时,item为groupItem(i - 1)中的某个subItem
            } else if (count > position) {

                itemStatus.setViewType(ItemStatus.VIEW_TYPE_SUBITEM);
                itemStatus.setGroupItemIndex(i - 1);
                itemStatus.setSubItemIndex(position - ( count - dataTrees.get(i - 1).getSubItems().size() ) );
                break;

            }
            
            //无论groupItem状态是开或者关,它在列表中都会存在,所有count++
            count++;

            //当轮询到的groupItem的状态为“开”的话,count需要加上该groupItem下面的子项目数目
            if (groupItemStatus.get(i)) {

                count += dataTrees.get(i).getSubItems().size();

            }


        }
        
        //简单地处理当轮询到最后一项groupItem的时候
        if (i >= groupItemStatus.size()) {
            itemStatus.setGroupItemIndex(i - 1);
            itemStatus.setViewType(ItemStatus.VIEW_TYPE_SUBITEM);
            itemStatus.setSubItemIndex(position - ( count - dataTrees.get(i - 1).getSubItems().size() ) );
        }

        return itemStatus;
    }

getItemCount()方法实现

该方法在显示列表的时候会执行多次,如果返回的项目计数不正确的话程序会出现错误奔溃,代码如下:

    @Override
    public int getItemCount() {

        Logger.i("1");

        int itemCount = 0;

        if (groupItemStatus.size() == 0) {
            return 0;
        }

        for (int i = 0; i < dataTrees.size(); i++) {

            if (groupItemStatus.get(i)) {
                itemCount += dataTrees.get(i).getSubItems().size() + 1;
            } else {
                itemCount++;
            }

        }

        return itemCount;
    }

其它方法实现

  • getItemViewType()

该方法会在 onCreateViewHolder() 前执行,并返回 int viewType,代码如下:

    @Override
    public int getItemViewType(int position) {
        return getItemStatusByPosition(position).getViewType();
    }
  • onCreateViewHolder()
    根据不同的由 getItemViewType() 返回的 viewType 选择不同的项目布局,代码如下:
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v;
        RecyclerView.ViewHolder viewHolder = null;

        if (viewType == ItemStatus.VIEW_TYPE_GROUPITEM) {

            v = LayoutInflater.from(parent.getContext()).inflate(R.layout
                    .item_artist_detail_album, parent, false);
            viewHolder = new GroupItemViewHolder(v);

        } else if (viewType == ItemStatus.VIEW_TYPE_SUBITEM) {

            v = LayoutInflater.from(parent.getContext()).inflate(R.layout
                    .item_artist_detail_track, parent, false);
            viewHolder = new SubItemViewHolder(v);
        }

        return viewHolder;
    }
  • onBindViewHolder()
    根据不同的 viewType 绑定不同的 ViewHolder ,代码如下:
   @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

        final ItemStatus itemStatus = getItemStatusByPosition(position);

        final DataTree<Album, Track> dt = dataTrees.get(itemStatus.getGroupItemIndex());

        if ( itemStatus.getViewType() == ItemStatus.VIEW_TYPE_GROUPITEM ) {

            final GroupItemViewHolder groupItemVh = (GroupItemViewHolder) holder;

            . . .    //加载groupItem,处理groupItem控件

            groupItemVh.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {

                    int groupItemIndex = itemStatus.getGroupItemIndex();

                    if ( !groupItemStatus.get(groupItemIndex) ) {

                        . . .  //groupItem由“关闭”状态到“打开”状态
                        
                        groupItemStatus.set(groupItemIndex, true);
                        notifyItemRangeInserted(groupItemVh.getAdapterPosition() + 1, dt.getSubItems().size());

                    } else {

                     . . .    //groupItem由“打开”状态到“关闭”状态
           
                    groupItemStatus.set(groupItemIndex, false);
                        notifyItemRangeRemoved(groupItemVh.getAdapterPosition() + 1, dt.getSubItems().size());
                        
                    }

                }
            });

        } else if (itemStatus.getViewType() == ItemStatus.VIEW_TYPE_SUBITEM) {

            SubItemViewHolder subItemVh = (SubItemViewHolder) holder;

             . . .    //加载subItem,处理subItem控件

            subItemVh.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {

                 . . .    //点击subItem处理

                }
            });
        }
    }

总结

二级列表对 RecyclerView 小小地扩展,在实现的过程中,有点麻烦的地方是对特定的 item 进行识别,即 getItemViewType() 的实现,在这里,我简单地用遍历轮询的方法去判断,应该会有更简单更节省资源的方法。实现二级列表之后,理论上可以实现多级列表,可以试试。

在用这个方法实现之前,有尝试过有 View 提供的方法—— View.setTag() ,但是由于 RecyclerView 的加载机制,当列表被划出界面时,会被销毁,而重新划进来显示时,RecyclerView 会重新创建新的 item,而不是之前的那个,所以,之前的 Tag 会不见,最后没能实现出来,感兴趣的同学可以试试。


  1. [Android RecyclerView 使用完全解析 体验艺术般的控件 - hongyang - CSDN博客]http://blog.csdn.net/lmj623565791/article/details/45059587

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351

推荐阅读更多精彩内容