BaseRecyclerViewAdapterHelper 开源项目之 BaseSectionQuickAdapter 实现 Expandable And collapse 效果的源码学习

本贴最后更新于 2290 天前,其中的信息可能已经天翻地覆

version:2.8.5

更多分享请看:http://cherylgood.cn

今天我们来学习 BaseRecyclerViewAdapterHelper 中有关实现可展开和折叠二级 Item 或多级 Item 的源码。在开始学习之前,我想先分析下实现的思路,这样对于进行源码的理解效果比较好。

实现伸展 and 折叠,很多控件都有,网上也有用 linearlayout 实现的功能很强大、很炫酷的开源项目,平时要实现一些伸缩性的自定义控件,我们也可以是用属性动画,或者动态控制控件的 Layout 属性等都可以实现。那么现在我们来想象一下,如果在 recyclerview 中实现该功能,相对来说能想到的比较合适的方式是什么呢?

其实我们可以很好的利用 RecyclerView.Adapter 给我们提供的如下一些通知数据源更新的方法来实现我们的动态伸展 and 折叠功能。当要伸展时,我们动态将下一级 item 的数据添加在与 adapter 绑定的数据集合中,然后通知 layoutManger 更新数据源。当要收缩时,同理,将下一级的 item 的数据源从与 adapter 绑定的数据集合中移除,然后通知更新。

* @see #notifyItemChanged(int) * @see #notifyItemInserted(int) * @see #notifyItemRemoved(int) * @see #notifyItemRangeChanged(int, int) * @see #notifyItemRangeInserted(int, int) * @see #notifyItemRangeRemoved(int, int)

思路:

  1. 数据 bean 应该有存储自己数据的字段
  2. 数据 bean 应该有存储下一级 item 列表的集合类型的字段
  3. 数据 bean 应该有一个字段标识当前 item 的状态(伸展 or 收缩)
  4. 初始化 adapter 时只渲染顶级的 item
  5. 点击 item 是检测该 item 是否支持伸缩
  6. 支持伸缩:当前状态展开-> 折叠(将次级 list 插入 adapter 绑定的 data 集合中,刷新数据);当前状态折叠-> 展开(将次级的 list 从与 adapter 绑定的 data 集合中移除,刷新数据)
  7. 插入或移除的位置根据点击的 item 确定,插入量与移除量根据下一级 item 数量确定
  8. 插入移除过程中可以使用动画效果

思路理清之后我们接下来开始学习源代码:

实现 Expandable And collapse 效果我们仍然是使用 BaseMultiItemQuickAdapter 实现即可

然后我们需要先看两个相关的类:IExpandable 接口;AbstractExpandableItem: 对数据 bean 的再次封装,某个 bean 如果有次级的 list 可以实现该抽象类。

package com.chad.library.adapter.base.entity; import java.util.List; /** * implement the interface if the item is expandable * Created by luoxw on 2016/8/8. */ public interface IExpandable { boolean isExpanded(); void setExpanded(boolean expanded); List getSubItems(); /** * Get the level of this item. The level start from 0. * If you don't care about the level, just return a negative. */ int getLevel(); }

可以看到,IExpandable 里面定义了四个接口方法:

  1. isExpanded 判断当前的 bean 是否已展开
  2. setExoanded 更新 bean 的当前状态
  3. getSubItems 返回下一级的数据集合
  4. getLevel 返回当前 item 属于第几个层级, 第一级 from 0
package com.chad.library.adapter.base.entity; import java.util.ArrayList; import java.util.List; public abstract class AbstractExpandableItem implements IExpandable { protected boolean mExpandable = false; protected List mSubItems; @Override public boolean isExpanded() { return mExpandable; } @Override public void setExpanded(boolean expanded) { mExpandable = expanded; } @Override public List getSubItems() { return mSubItems; } public boolean hasSubItem() { return mSubItems != null && mSubItems.size() > 0; } public void setSubItems(List list) { mSubItems = list; } public T getSubItem(int position) { if (hasSubItem() && position < mSubItems.size()) { return mSubItems.get(position); } else { return null; } } public int getSubItemPosition(T subItem) { return mSubItems != null ? mSubItems.indexOf(subItem) : -1; } public void addSubItem(T subItem) { if (mSubItems == null) { mSubItems = new ArrayList<>(); } mSubItems.add(subItem); } public void addSubItem(int position, T subItem) { if (mSubItems != null && position >= 0 && position < mSubItems.size()) { mSubItems.add(position, subItem); } else { addSubItem(subItem); } } public boolean contains(T subItem) { return mSubItems != null && mSubItems.contains(subItem); } public boolean removeSubItem(T subItem) { return mSubItems != null && mSubItems.remove(subItem); } public boolean removeSubItem(int position) { if (mSubItems != null && position >= 0 && position < mSubItems.size()) { mSubItems.remove(position); return true; } return false; } }

字段方法解析:

  1. mExpandable 保存当前的状态值,默认为 false
  2. mSubItems 存储数据 bean 集合

里面还包装了一些常用的方法,这里就不一一解析了。

接下来我们以一个使用 demo 的实现来进行分析:

我们可以看群主 demo 中的 ExpandableUseActivity :

private ArrayList generateData() { int lv0Count = 9; int lv1Count = 3; int personCount = 5; String[] nameList = {"Bob", "Andy", "Lily", "Brown", "Bruce"}; Random random = new Random(); ArrayList res = new ArrayList<>(); for (int i = 0; i < lv0Count; i++) { Level0Item lv0 = new Level0Item("This is " + i + "th item in Level 0", "subtitle of " + i); for (int j = 0; j < lv1Count; j++) { Level1Item lv1 = new Level1Item("Level 1 item: " + j, "(no animation)"); for (int k = 0; k < personCount; k++) { lv1.addSubItem(new Person(nameList[k], random.nextInt(40))); } lv0.addSubItem(lv1); } res.add(lv0); } return res; }

这段代码的作用是生成一个支持 Expandable and collapse 的数据集合,创建一个 0 级的 LevelOItem 然后将下一级的 Level1Item 添加到 Level0Item 中。

public class Level0Item extends AbstractExpandableItem implements MultiItemEntity { public String title; public String subTitle; public Level0Item( String title, String subTitle) { this.subTitle = subTitle; this.title = title; } @Override public int getItemType() { return ExpandableItemAdapter.TYPE_LEVEL_0; } @Override public int getLevel() { return 0; } }

可以看到 Level0Item 继承了 AbstractExpandableItem 并实现 MultiItemEntity 接口。里面根据实际需求定义相应的字段即可。

Level1Item 与 Level0Item 一样,只是返回的 Level =1:

public class Level1Item extends AbstractExpandableItem implements MultiItemEntity{ public String title; public String subTitle; public Level1Item(String title, String subTitle) { this.subTitle = subTitle; this.title = title; } @Override public int getItemType() { return ExpandableItemAdapter.TYPE_LEVEL_1; } @Override public int getLevel() { return 1; } }

当如过某一级的 item 没有下一级的 list 时,就不需要在实现 AbstractExpandableItem 了

然后我们的切入点时 adapter,因为默认是折叠状态,当我们点击具备展开折叠能力的 item 时才会触发该功能,所以逻辑的控制是在 adapter 中的。

package com.chad.baserecyclerviewadapterhelper.adapter; import android.util.Log; import android.view.View; import android.view.ViewGroup; import com.chad.baserecyclerviewadapterhelper.R; import com.chad.baserecyclerviewadapterhelper.entity.Level0Item; import com.chad.baserecyclerviewadapterhelper.entity.Level1Item; import com.chad.baserecyclerviewadapterhelper.entity.Person; import com.chad.library.adapter.base.BaseMultiItemQuickAdapter; import com.chad.library.adapter.base.BaseViewHolder; import com.chad.library.adapter.base.entity.MultiItemEntity; import java.util.List; /** * Created by luoxw on 2016/8/9. */ public class ExpandableItemAdapter extends BaseMultiItemQuickAdapter<MultiItemEntity, BaseViewHolder> { private static final String TAG = ExpandableItemAdapter.class.getSimpleName(); public static final int TYPE_LEVEL_0 = 0; public static final int TYPE_LEVEL_1 = 1; public static final int TYPE_PERSON = 2; /** * Same as QuickAdapter#QuickAdapter(Context,int) but with * some initialization data. * * @param data A new list is created out of this one to avoid mutable list */ public ExpandableItemAdapter(List data) { super(data); addItemType(TYPE_LEVEL_0, R.layout.item_expandable_lv0); addItemType(TYPE_LEVEL_1, R.layout.item_expandable_lv1); addItemType(TYPE_PERSON, R.layout.item_expandable_lv2); } @Override protected void convert(final BaseViewHolder holder, final MultiItemEntity item) { switch (holder.getItemViewType()) { case TYPE_LEVEL_0: switch (holder.getLayoutPosition() % 3) { case 0: holder.setImageResource(R.id.iv_head, R.mipmap.head_img0); break; case 1: holder.setImageResource(R.id.iv_head, R.mipmap.head_img1); break; case 2: holder.setImageResource(R.id.iv_head, R.mipmap.head_img2); break; } final Level0Item lv0 = (Level0Item)item; holder.setText(R.id.title, lv0.title) .setText(R.id.sub_title, lv0.subTitle) .setImageResource(R.id.iv, lv0.isExpanded() ? R.mipmap.arrow_b : R.mipmap.arrow_r); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int pos = holder.getAdapterPosition(); Log.d(TAG, "Level 0 item pos: " + pos); if (lv0.isExpanded()) { collapse(pos); } else { // if (pos % 3 == 0) { // expandAll(pos, false); // } else { expand(pos); // } } } }); break; case TYPE_LEVEL_1: final Level1Item lv1 = (Level1Item)item; holder.setText(R.id.title, lv1.title) .setText(R.id.sub_title, lv1.subTitle) .setImageResource(R.id.iv, lv1.isExpanded() ? R.mipmap.arrow_b : R.mipmap.arrow_r); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int pos = holder.getAdapterPosition(); Log.d(TAG, "Level 1 item pos: " + pos); if (lv1.isExpanded()) { collapse(pos, false); } else { expand(pos, false); } } }); break; case TYPE_PERSON: final Person person = (Person)item; holder.setText(R.id.tv, person.name + " parent pos: " + getParentPosition(person)); break; } } }

可以看到里面我们先添加 3 个 level 的布局资源文件。重点在 convert 回调方法;

  1. 最外层进行 viewholder 的类型判断进行数据绑定
  2. 添加点击事件的监听
  3. 当被点击时,判断当前的 levelitem 是不是展开的或折叠的,然后根据你的需要调用 collapse 或者 expand 进行折叠或展开操作。

重点来的,最终实现展开、折叠功能其实是依赖 collapse 和 expand 这些 api;那我们来看下这些 api 到底内部是怎么实现的,我们从 expand 开始。代码中 expand(pos);传了一个 pos 进来,而这个 pos 就是被点击的 item 在 adapter 数据集合中的 index。

/** * Expand an expandable item * * @param position position of the item * @param animate expand items with animation * @param shouldNotify notify the RecyclerView to rebind items, false if you want to do it * yourself. * @return the number of items that have been added. */ public int expand(@IntRange(from = 0) int position, boolean animate, boolean shouldNotify) { position -= getHeaderLayoutCount(); IExpandable expandable = getExpandableItem(position); if (expandable == null) { return 0; } if (!hasSubItems(expandable)) { expandable.setExpanded(false); return 0; } int subItemCount = 0; if (!expandable.isExpanded()) { List list = expandable.getSubItems(); mData.addAll(position + 1, list); subItemCount += recursiveExpand(position + 1, list); expandable.setExpanded(true); subItemCount += list.size(); } int parentPos = position + getHeaderLayoutCount(); if (shouldNotify) { if (animate) { notifyItemChanged(parentPos); notifyItemRangeInserted(parentPos + 1, subItemCount); } else { notifyDataSetChanged(); } } return subItemCount; } /** * Expand an expandable item * * @param position position of the item, which includes the header layout count. * @param animate expand items with animation * @return the number of items that have been added. */ public int expand(@IntRange(from = 0) int position, boolean animate) { return expand(position, animate, true); } /** * Expand an expandable item with animation. * * @param position position of the item, which includes the header layout count. * @return the number of items that have been added. */ public int expand(@IntRange(from = 0) int position) { return expand(position, true, true); }

可以看到 expand 是一个方法多态,提供了三种参数类型的调用。支持是否需要动画,是否更新数据源。

排除 headerview 的干扰,获得实际的位置 position

position -= getHeaderLayoutCount();

判断其是否支持展开折叠,是否有下一级 items 需要展开,没有就直接返回 0

IExpandable expandable = getExpandableItem(position); if (expandable == null) { return 0; } if (!hasSubItems(expandable)) { expandable.setExpanded(false); return 0; }

下面代码作用:如果处于折叠状态且需要展开,则执行到下面代码,通过 getSubItems 获得要展开的 list,将其添加到 mdata 中,通过 recursiveExpand 获得要展开的 items 的数量

int subItemCount = 0; if (!expandable.isExpanded()) { List list = expandable.getSubItems(); mData.addAll(position + 1, list); subItemCount += recursiveExpand(position + 1, list); expandable.setExpanded(true); subItemCount += list.size(); }

我们可以看到 recursiveExpand 的源码如下:下面是一个递归调用,一直遍历到最后一层不支持展开折叠的 item 才会回溯回来,遍历过程中可以看到一个判断,if(item.isExpanded) 就是如果下一级的 items 原来已经是处于展开状态的,此时我们也需要展开他。最终返回的是所需展开的 items 的数量。

private int recursiveExpand(int position, @NonNull List list) { int count = 0; int pos = position + list.size() - 1; for (int i = list.size() - 1; i >= 0; i--, pos--) { if (list.get(i) instanceof IExpandable) { IExpandable item = (IExpandable) list.get(i); if (item.isExpanded() && hasSubItems(item)) { List subList = item.getSubItems(); mData.addAll(pos + 1, subList); int subItemCount = recursiveExpand(pos + 1, subList); count += subItemCount; } } } return count; }

获得需要展开的 items 的数量值,也将数据集合添加到了 mData 中,此时我们通知 layoutManager 刷新数据即可

int parentPos = position + getHeaderLayoutCount(); if (shouldNotify) { if (animate) { notifyItemChanged(parentPos); notifyItemRangeInserted(parentPos + 1, subItemCount); } else { notifyDataSetChanged(); } }

刷新的时候我们要先确定开始刷新位置,所以需要加上 headerview 的数量

然后调用如上代码即可。折叠是反向进行的,根据这个思路看就可以了。

总结:折叠-> 展开:mData 添加需展开的数据集,更新数据源;展开-> 折叠:mData 移除需折叠的数据集,更新数据源。

后面会继续分析其他功能的实现源码,欢迎一起学习!

  • Android

    Android 是一种以 Linux 为基础的开放源码操作系统,主要使用于便携设备。2005 年由 Google 收购注资,并拉拢多家制造商组成开放手机联盟开发改良,逐渐扩展到到平板电脑及其他领域上。

    335 引用 • 324 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • NetBeans

    NetBeans 是一个始于 1997 年的 Xelfi 计划,本身是捷克布拉格查理大学的数学及物理学院的学生计划。此计划延伸而成立了一家公司进而发展这个商用版本的 NetBeans IDE,直到 1999 年 Sun 买下此公司。Sun 于次年(2000 年)六月将 NetBeans IDE 开源,直到现在 NetBeans 的社群依然持续增长。

    78 引用 • 102 回帖 • 701 关注
  • 导航

    各种网址链接、内容导航。

    43 引用 • 177 回帖
  • 深度学习

    深度学习(Deep Learning)是机器学习的分支,是一种试图使用包含复杂结构或由多重非线性变换构成的多个处理层对数据进行高层抽象的算法。

    53 引用 • 40 回帖
  • Webswing

    Webswing 是一个能将任何 Swing 应用通过纯 HTML5 运行在浏览器中的 Web 服务器,详细介绍请看 将 Java Swing 应用变成 Web 应用

    1 引用 • 15 回帖 • 638 关注
  • 数据库

    据说 99% 的性能瓶颈都在数据库。

    345 引用 • 739 回帖 • 1 关注
  • 小说

    小说是以刻画人物形象为中心,通过完整的故事情节和环境描写来反映社会生活的文学体裁。

    31 引用 • 108 回帖
  • Flutter

    Flutter 是谷歌的移动 UI 框架,可以快速在 iOS 和 Android 上构建高质量的原生用户界面。 Flutter 可以与现有的代码一起工作,它正在被越来越多的开发者和组织使用,并且 Flutter 是完全免费、开源的。

    39 引用 • 92 回帖
  • Spark

    Spark 是 UC Berkeley AMP lab 所开源的类 Hadoop MapReduce 的通用并行框架。Spark 拥有 Hadoop MapReduce 所具有的优点;但不同于 MapReduce 的是 Job 中间输出结果可以保存在内存中,从而不再需要读写 HDFS,因此 Spark 能更好地适用于数据挖掘与机器学习等需要迭代的 MapReduce 的算法。

    74 引用 • 46 回帖 • 567 关注
  • Visio
    1 引用 • 2 回帖 • 1 关注
  • Ruby

    Ruby 是一种开源的面向对象程序设计的服务器端脚本语言,在 20 世纪 90 年代中期由日本的松本行弘(まつもとゆきひろ/Yukihiro Matsumoto)设计并开发。在 Ruby 社区,松本也被称为马茨(Matz)。

    7 引用 • 31 回帖 • 254 关注
  • Typecho

    Typecho 是一款博客程序,它在 GPLv2 许可证下发行,基于 PHP 构建,可以运行在各种平台上,支持多种数据库(MySQL、PostgreSQL、SQLite)。

    12 引用 • 67 回帖 • 445 关注
  • Laravel

    Laravel 是一套简洁、优雅的 PHP Web 开发框架。它采用 MVC 设计,是一款崇尚开发效率的全栈框架。

    20 引用 • 23 回帖 • 737 关注
  • Logseq

    Logseq 是一个隐私优先、开源的知识库工具。

    Logseq is a joyful, open-source outliner that works on top of local plain-text Markdown and Org-mode files. Use it to write, organize and share your thoughts, keep your to-do list, and build your own digital garden.

    7 引用 • 69 回帖
  • 周末

    星期六到星期天晚,实行五天工作制后,指每周的最后两天。再过几年可能就是三天了。

    14 引用 • 297 回帖
  • Sym

    Sym 是一款用 Java 实现的现代化社区(论坛/BBS/社交网络/博客)系统平台。

    下一代的社区系统,为未来而构建

    524 引用 • 4601 回帖 • 700 关注
  • React

    React 是 Facebook 开源的一个用于构建 UI 的 JavaScript 库。

    192 引用 • 291 回帖 • 382 关注
  • FlowUs

    FlowUs.息流 个人及团队的新一代生产力工具。

    让复杂的信息管理更轻松、自由、充满创意。

    1 引用
  • 音乐

    你听到信仰的声音了么?

    61 引用 • 512 回帖
  • 国际化

    i18n(其来源是英文单词 internationalization 的首末字符 i 和 n,18 为中间的字符数)是“国际化”的简称。对程序来说,国际化是指在不修改代码的情况下,能根据不同语言及地区显示相应的界面。

    8 引用 • 26 回帖 • 1 关注
  • 脑图

    脑图又叫思维导图,是表达发散性思维的有效图形思维工具 ,它简单却又很有效,是一种实用性的思维工具。

    31 引用 • 96 回帖
  • 博客

    记录并分享人生的经历。

    273 引用 • 2388 回帖
  • Node.js

    Node.js 是一个基于 Chrome JavaScript 运行时建立的平台, 用于方便地搭建响应速度快、易于扩展的网络应用。Node.js 使用事件驱动, 非阻塞 I/O 模型而得以轻量和高效。

    139 引用 • 269 回帖
  • 友情链接

    确认过眼神后的灵魂连接,站在链在!

    24 引用 • 373 回帖
  • Access
    1 引用 • 3 回帖 • 6 关注
  • Sphinx

    Sphinx 是一个基于 SQL 的全文检索引擎,可以结合 MySQL、PostgreSQL 做全文搜索,它可以提供比数据库本身更专业的搜索功能,使得应用程序更容易实现专业化的全文检索。

    1 引用 • 214 关注
  • Firefox

    Mozilla Firefox 中文俗称“火狐”(正式缩写为 Fx 或 fx,非正式缩写为 FF),是一个开源的网页浏览器,使用 Gecko 排版引擎,支持多种操作系统,如 Windows、OSX 及 Linux 等。

    7 引用 • 30 回帖 • 394 关注
  • RESTful

    一种软件架构设计风格而不是标准,提供了一组设计原则和约束条件,主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。

    30 引用 • 114 回帖 • 6 关注