BaseRecyclerViewAdapterHelper 源码分析:分组、多布局、折叠

本贴最后更新于 2569 天前,其中的信息可能已经事过景迁
  • BaseMultiItemQuickAdapter 多布局
    首先我们要实现多类型布局,我们的 adapter 不再是继承自 BaseQuickAdapter 类,而是继承自其的子类

BaseMultiItemQuickAdapter。而且数据源类型需要继承自 MultiItemEntity。

MultiItemEntity 是一个接口,代码很少:

public interface MultiItemEntity {

    int getItemType();

}

其主要用意是我们的数据源继承 MultiItemEntity,这样子,我们可以在数据源中动态的返回一个 int 类型的数值(代表某一类型的 item),大家可以想一下,我们在渲染 viewholder 的时候,如果想实现多类型的 viewholder,而 viewholder 的类型展示又跟所需绑定的数据息息相关,那么如果我们在数据源中提供一种确定 viewholder 类型的能力,理论上是不是就可以达到我们要的效果了?我们带着这样的一个假设继续往下看。

而这个 BaseMultiItemQuickAdapter 是何许人也,是如何实现多类型布局的呢?我们来看下源码:

package com.chad.library.adapter.base;

import android.support.annotation.LayoutRes;
import android.util.SparseArray;
import android.view.ViewGroup;

import com.chad.library.adapter.base.entity.MultiItemEntity;

import java.util.List;

/**
 * https://github.com/CymChad/BaseRecyclerViewAdapterHelper */public abstract class BaseMultiItemQuickAdapter<T extends MultiItemEntity, K extends BaseViewHolder> extends BaseQuickAdapter<T, K> {

    /**
 * layouts indexed with their types */  private SparseArray layouts;

 private static final int DEFAULT_VIEW_TYPE = -0xff;

  /**
 * 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 BaseMultiItemQuickAdapter( List<T> data) {
        super( data);
  }

    @Override
  protected int getDefItemViewType(int position) {
        Object item = mData.get(position);
 if (item instanceof MultiItemEntity) {
            return ((MultiItemEntity)item).getItemType();
  }
        return DEFAULT_VIEW_TYPE;
  }

    protected void setDefaultViewTypeLayout(@LayoutRes int layoutResId) {
        addItemType(DEFAULT_VIEW_TYPE, layoutResId);
  }

    @Override
  protected K onCreateDefViewHolder(ViewGroup parent, int viewType) {
        return createBaseViewHolder(parent, getLayoutId(viewType));
  }

    private int getLayoutId(int viewType) {
        return layouts.get(viewType);
  }

    protected void addItemType(int type, @LayoutRes int layoutResId) {
        if (layouts == null) {
            layouts = new SparseArray<>();
  }
        layouts.put(type, layoutResId);
  }

}

源码不多:

字段解析:

1、存储我们的布局资源的 ids

private SparseArray layouts;

2、缺省的布局类型值,当使用多布局时,在渲染 viewholder 的时候类型址不是我们配置的类型值中,就会使用这个。

private static final int DEFAULT_VIEW_TYPE = -0xff;

接下来,我们以一个 BaseMultiItemQuickAdapter 的创建过程来分析代码:

之前我们分析了 BaseQuickAdapter 的代码,其执行过程是一样的,我们实现多布局功能的切入口无非是

1、在执行 getItemViewType 时的能够根据我们的数据源返回对应的布局类型值。

2、在 onCreateDefViewHolder 能够正确拿到类型值进行 viewholder 的渲染。

3、我们在 onBindViewHolder 中根据传递给我们的数据源中接口定义的 getItemViewType 方法返回的类型值来确定当前的 viewholder 是什么类型的,需要绑定什么数据。

(注:之前分析了 adapter 的加载数据时的生命周期方法:getItemViewType->onCreateDefViewHolder->onBindViewHolder,如果不大清楚可以看下前面的文章)

所以,我们在 BaseMultiItemQuickAdapter 里面重写了 getDefItemViewType 方法,为什么时重写

getDefItemViewType 方法而不是 getItemViewType 方法呢?这可不是我糊弄你,因为我们在 BaseQuickAdapter 里面重写了 getItemViewType 方法,而在 getItemViewType 方法里调用了 getDefItemViewType 方法来回去类型值,该方法也在之前的分析 BaseQuickAdapter 源码的文章中分析了的。

重写之后做了什么呢?看代码:

@Override
protected int getDefItemViewType(int position) {
    Object item = mData.get(position);
 if (item instanceof MultiItemEntity) {
        return ((MultiItemEntity)item).getItemType();
  }
    return DEFAULT_VIEW_TYPE;
}

很简单,因为我们的数据源实现了 MultiItemEntity 接口。直接判断该 position 的数据是不是实现了
MultiItemEntity 接口,是调用接口的 getItemType 方法返回类型值,不是返回默认类型值。
第一步返回类型值的代码改造完成了,接下来第二部就是根据类型值渲染 viewholder。BaseMultiItemQuickAdapter 直接重写了 onCreateDefViewHolder 方法来实现该扩展:

@Override
protected K onCreateDefViewHolder(ViewGroup parent, int viewType) {
    return createBaseViewHolder(parent, getLayoutId(viewType));
}

代码很简单,从我们存储布局缓存的字段中根据 viewType 返回对象的布局资源的 ids。
所以 BaseMultiItemQuickAdapter 还给我们包装了一个 addItemType 方法:

protected void addItemType(int type, @LayoutRes int layoutResId) {
    if (layouts == null) {
        layouts = new SparseArray<>();
  }
    layouts.put(type, layoutResId);
}

该方法很简单,就是将不同的布局资源的 ids 和对应的类型值存储起来。
所以我们的创建多布局的时候,需要的构造函数中调用 addItemType 来添加不同的布局资源
最后一步,绑定数据;一般绑定数据实在 onBindViewHolder 中实现的,而我们的 BaseRecyclerViewAdapterHelper 对其进行了包装,提供了一个 convert 方法,所以我们只需要的 convert 方法中根据数据源数据节点的类型值判断绑定的是那个布局的数据即可。

总结:理解了 adapter 加载数据的生命周期方法的执行顺序很重要(getItemViewType->onCreateDefViewHolder->onBindViewHolder)。
只要控制 viewType 的返回、viewholder 的渲染。viewholder 数据的绑定即可。

BaseSectionQuickAdapter 分组

首先今天的学习我们还是按照前面的学习思路,根据 getItemViewType->onCreateDefViewHolder->onBindViewHolder,即从确认 viewholder 类型-> 根据类型值创建 viewholder-> 根据数据源类型绑定数据到 viewholder 上。

第一步:我们看一下 BaseSectionQuickAdapter 这个类的定义

public abstract class BaseSectionQuickAdapter extends BaseQuickAdapter {

跟前面分析的多类型 BaseMultiItemQuickAdapter 差不多,只是我们的数据源需要继承自 SetionEntity。那么这个 SetionEntity 做了什么事呢,我们来看下源码:

package com.chad.library.adapter.base.entity;

/**
 * https://github.com/CymChad/BaseRecyclerViewAdapterHelper */public abstract class SectionEntity<T> {
    public boolean isHeader;
 public T t;
 public String header;

 public SectionEntity(boolean isHeader, String header) {
        this.isHeader = isHeader;
 this.header = header;
 this.t = null;
  }

    public SectionEntity(T t) {
        this.isHeader = false;
 this.header = null;
 this.t = t;
  }
}

从源码可以看出,他是一个抽象类,可能你会问,为什么要定义成抽象类呢,为什么不定义成接口或者普通类呢。

以下理由仅由我意想得出,大家也可以发表下自己的看法:

1、我们定义 SectionEntity 这个类,目的自然是希望用户的 bean 都具有某些规范,而我们的 BaseSectionQuickAdapter 将根据该规范进行数据的处理。虽然使用普通类一样能达到相同的效果,但是不推荐,我觉得这让有可能会让用户忽略我们所需要让用户知道的规范。

2、接口类,接口类其实是特殊的抽象类,上次分析的 MultiItemEntity 为什么又定义成接口类型呢。

public interface MultiItemEntity {

    int getItemType();

}

根据实际需求而定,因为我们在实现多类型时,只需要用户的数据源提供一个类型值给我们即可,所以此时定义成接口类是最为合适的,因为用户数据源只要实现了该接口,他必须实现接口的方法,而我们需要的恰恰是在使用时调用该接口即可。

但是在 SetionEntity 中,我们帮用户多做点事,为其提供两个构造方法,一个时分组头,一个是分组体。而此时如果是定义成接口类,是不符合需求的,因为接口类的方法不能有方法体等。

SectionEntity 代码分析:从源码可以看出,假如我们当前数据是分组头,那么我们在创建 bean 时使用

public SectionEntity(boolean isHeader, String header) {
    this.isHeader = isHeader;
 this.header = header;
 this.t = null;
}

即可,当前定义分组头只有个 string 类型的分组头名字,你在继承时可以根据实际需求进行扩展,内部调用父类的该构造方法即可。

如果时普通的数据 bean:调用以下构造方法即可,当然你也可以进行扩展,根据个人需求而定。

public SectionEntity(T t) {
    this.isHeader = false;
 this.header = null;
 this.t = t;
}

我们来看 BaseSectionQuickAdapter 的源码:

package com.chad.library.adapter.base;

import android.view.ViewGroup;

import com.chad.library.adapter.base.entity.SectionEntity;

import java.util.List;

/**
 * https://github.com/CymChad/BaseRecyclerViewAdapterHelper */public abstract class BaseSectionQuickAdapter<T extends SectionEntity, K extends BaseViewHolder> extends BaseQuickAdapter<T, K> {

    protected int mSectionHeadResId;
 protected static final int SECTION_HEADER_VIEW = 0x00000444;

  /**
 * Same as QuickAdapter#QuickAdapter(Context,int) but with * some initialization data. * * @param sectionHeadResId The section head layout id for each item
 * @param layoutResId The layout resource id of each item.
 * @param data A new list is created out of this one to avoid mutable list
 */  public BaseSectionQuickAdapter( int layoutResId, int sectionHeadResId, List<T> data) {
        super(layoutResId, data);
 this.mSectionHeadResId = sectionHeadResId;
  }

    @Override
  protected int getDefItemViewType(int position) {
        return mData.get(position).isHeader ? SECTION_HEADER_VIEW : 0;
  }

    @Override
  protected K onCreateDefViewHolder(ViewGroup parent, int viewType) {
        if (viewType == SECTION_HEADER_VIEW)
            return createBaseViewHolder(getItemView(mSectionHeadResId, parent));

 return super.onCreateDefViewHolder(parent, viewType);
  }

    @Override
  public void onBindViewHolder(K holder, int positions) {
        switch (holder.getItemViewType()) {
            case SECTION_HEADER_VIEW:
                setFullSpan(holder);
  convertHead(holder, mData.get(holder.getLayoutPosition() - getHeaderLayoutCount()));
 break; default:
                super.onBindViewHolder(holder, positions);
 break;  }
    }

    protected abstract void convertHead(K helper, T item);

}

大家可以看到,源码比较少,跟 BaseMultiItemQuickAdapter 是一样的思路。

字段解析:

protected int mSectionHeadResId;

mSectionHeadResId 用来保存我们分组头的布局资源 ids;

protected static final int SECTION_HEADER_VIEW = 0x00000444;

定义了一个默认的分组头类型。思想与实现多类型一致;

我们在实例化 BaseSectionQuickAdapter 时需要多传递一个分组头的资源 ids 过来,所以构造方法是这样的:

/**
 * Same as QuickAdapter#QuickAdapter(Context,int) but with * some initialization data. * * @param sectionHeadResId The section head layout id for each item
 * @param layoutResId The layout resource id of each item.
 * @param data A new list is created out of this one to avoid mutable list
 */public BaseSectionQuickAdapter( int layoutResId, int sectionHeadResId, List<T> data) {
    super(layoutResId, data);
 this.mSectionHeadResId = sectionHeadResId;
}

构造好之后,我们也是利用来 adapter 的生命周期方法:

1、重写 getDefItemViewType 方法,前面也解释过为什么不是重写 Recycler.adapter 的 getItemViewType 方法,以为我们的 BaseQuickAdapter 对其进行来包装。最终在 getItemViewType 方法中会调用我们的 getDefItemViewType 方法。

重写该方法所做的事不多:

@Override
protected int getDefItemViewType(int position) {
    return mData.get(position).isHeader ? SECTION_HEADER_VIEW : 0;
}

根据我们当前位置的数据 bean,判断当前节点的数据 bean 是不是分组头 bean,如果是,返回 SECTION_HEADER_VIEW 告诉 BaseQuickAdapter,你要创建的 viewholder 是分组头类型的 viewholder。否则返回 0(0 时 RecyclerView.Adapter 的缺省值,前面有分析)
接下来,我们也同样是重写了 onCreateDefViewHolder 方法。

@Override
protected K onCreateDefViewHolder(ViewGroup parent, int viewType) {
    if (viewType == SECTION_HEADER_VIEW)
        return createBaseViewHolder(getItemView(mSectionHeadResId, parent));

 return super.onCreateDefViewHolder(parent, viewType);
}

根据返回的类型值,如果是 SECTION_HEADER_VIEW 那么我们就创建一个分组头 viewholder 返回。否则调用父类的方法按原流程走。

在这里我们还需要重写 onBindViewHolder 方法,因为我们要多做两件事情:

1、对我们的分组头进行特殊处理;

2、增加一个分组头数据绑定的抽象方法的调用;

@Override
public void onBindViewHolder(K holder, int positions) {
    switch (holder.getItemViewType()) {
        case SECTION_HEADER_VIEW:
            setFullSpan(holder);
  convertHead(holder, mData.get(holder.getLayoutPosition() - getHeaderLayoutCount()));
 break; default:
            super.onBindViewHolder(holder, positions);
 break;  }
}

里面有个很有趣的方法。setFullSpan,从字面上理解是设置充满空间,我们来看下代码:

/**
 * When set to true, the item will layout using all span area. That means, if orientation * is vertical, the view will have full width; if orientation is horizontal, the view will * have full height. * if the hold view use StaggeredGridLayoutManager they should using all span area * * @param holder True if this item should traverse all spans.
 */protected void setFullSpan(RecyclerView.ViewHolder holder) {
    if (holder.itemView.getLayoutParams() instanceof StaggeredGridLayoutManager.LayoutParams) {
        StaggeredGridLayoutManager.LayoutParams params = (StaggeredGridLayoutManager.LayoutParams) holder
                .itemView.getLayoutParams();
  params.setFullSpan(true);
  }
}

里面原来是对 Layoutmanager 为 StaggeredGridLayoutManager 类型时做特殊处理,大家可以去了解下 StaggeredGridLayoutManager 这种类型的 LayoutManager。

最后会调用一个方法

params.setFullSpan(true);

继续看该方法源码:

/**
 * When set to true, the item will layout using all span area. That means, if orientation * is vertical, the view will have full width; if orientation is horizontal, the view will * have full height. * * @param fullSpan True if this item should traverse all spans.
 * @see #isFullSpan()
 */public void setFullSpan(boolean fullSpan) {
    mFullSpan = fullSpan;
}

该方法是 StaggeredGridLayoutManager 提供的,英文说明的大意是:

如果你设置 true,当前 item 将使用布局的所有空间。如果是垂直的,会沾满水平方向的宽度空间,如果是水平,会占满垂直方向的高度空间。

然后将 holder 和当前节点的数据 bean 作为参数调用 convertHead 函数。

所以当你实现的是帶分组头的 adapter 时,会多出一个数据绑定的回调接口:

protected abstract void convertHead(BaseViewHolder helper, T item);

可能你还会看到以下代码:

convertHead(holder, mData.get(holder.getLayoutPosition() - getHeaderLayoutCount()));

里面有一句 holder.getLayoutPosition()。

getLayoutPosition 是干什么用的呢,因为 RecyclerView 的 item 的布局和渲染其实是交给 layoutManager 来完成的,所以 adapter 中的 item 的位置可能跟 data 的 index 匹配不上,而 getLayoutPosition 将返回给我们当前 viewholder 在 recyclerView 中的最新位置信息。

总结:分析思路还是老套路,根据一个组件的生命周期及业务流程进行分析,掌握一个控件的执行流程是理解一个控件的实现的一个较好的方法,本人是这么认为的,也是这么做的。

Expandable 折叠
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<T> {
    boolean isExpanded();
 void setExpanded(boolean expanded);
  List<T> 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;

/**
 * A helper to implement expandable item.
  * if you don't want to extent a class, you can also implement the interface IExpandable
  * Created by luoxw on 2016/8/9.
 */public abstract class AbstractExpandableItem<T> implements IExpandable<T> {
    protected boolean mExpandable = false;
 protected List<T> mSubItems;

  @Override
  public boolean isExpanded() {
        return mExpandable;
  }

    @Override
  public void setExpanded(boolean expanded) {
        mExpandable = expanded;
  }

    @Override
  public List<T> getSubItems() {
        return mSubItems;
  }

    public boolean hasSubItem() {
        return mSubItems != null && mSubItems.size() > 0;
  }

    public void setSubItems(List<T> 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 的实现来进行分析:

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 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, 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 是一个方法多态,提供了三种参数类型的调用。支持是否需要动画,是否更新数据源。

排除 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 的数量。

/**
 * Get the row id associated with the specified position in the list. * * @param position The position of the item within the adapter's data set whose row id we want.
 * @return The id of the item at the specified position.
 */@Override
public long getItemId(int position) {
    return position;
}

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 收购注资,并拉拢多家制造商组成开放手机联盟开发改良,逐渐扩展到到平板电脑及其他领域上。

    333 引用 • 323 回帖 • 70 关注

相关帖子

欢迎来到这里!

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

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

推荐标签 标签

  • Vim

    Vim 是类 UNIX 系统文本编辑器 Vi 的加强版本,加入了更多特性来帮助编辑源代码。Vim 的部分增强功能包括文件比较(vimdiff)、语法高亮、全面的帮助系统、本地脚本(Vimscript)和便于选择的可视化模式。

    27 引用 • 66 回帖
  • Lute

    Lute 是一款结构化的 Markdown 引擎,支持 Go 和 JavaScript。

    25 引用 • 191 回帖 • 19 关注
  • 支付宝

    支付宝是全球领先的独立第三方支付平台,致力于为广大用户提供安全快速的电子支付/网上支付/安全支付/手机支付体验,及转账收款/水电煤缴费/信用卡还款/AA 收款等生活服务应用。

    29 引用 • 347 回帖 • 1 关注
  • 大数据

    大数据(big data)是指无法在一定时间范围内用常规软件工具进行捕捉、管理和处理的数据集合,是需要新处理模式才能具有更强的决策力、洞察发现力和流程优化能力的海量、高增长率和多样化的信息资产。

    89 引用 • 113 回帖
  • iOS

    iOS 是由苹果公司开发的移动操作系统,最早于 2007 年 1 月 9 日的 Macworld 大会上公布这个系统,最初是设计给 iPhone 使用的,后来陆续套用到 iPod touch、iPad 以及 Apple TV 等产品上。iOS 与苹果的 Mac OS X 操作系统一样,属于类 Unix 的商业操作系统。

    84 引用 • 139 回帖 • 1 关注
  • 架构

    我们平时所说的“架构”主要是指软件架构,这是有关软件整体结构与组件的抽象描述,用于指导软件系统各个方面的设计。另外还有“业务架构”、“网络架构”、“硬件架构”等细分领域。

    139 引用 • 441 回帖
  • IDEA

    IDEA 全称 IntelliJ IDEA,是一款 Java 语言开发的集成环境,在业界被公认为最好的 Java 开发工具之一。IDEA 是 JetBrains 公司的产品,这家公司总部位于捷克共和国的首都布拉格,开发人员以严谨著称的东欧程序员为主。

    180 引用 • 400 回帖
  • ActiveMQ

    ActiveMQ 是 Apache 旗下的一款开源消息总线系统,它完整实现了 JMS 规范,是一个企业级的消息中间件。

    19 引用 • 13 回帖 • 628 关注
  • 一些有用的避坑指南。

    69 引用 • 93 回帖
  • Dubbo

    Dubbo 是一个分布式服务框架,致力于提供高性能和透明化的 RPC 远程服务调用方案,是 [阿里巴巴] SOA 服务化治理方案的核心框架,每天为 2,000+ 个服务提供 3,000,000,000+ 次访问量支持,并被广泛应用于阿里巴巴集团的各成员站点。

    60 引用 • 82 回帖 • 601 关注
  • 单点登录

    单点登录(Single Sign On)是目前比较流行的企业业务整合的解决方案之一。SSO 的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

    9 引用 • 25 回帖 • 3 关注
  • Webswing

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

    1 引用 • 15 回帖 • 634 关注
  • Google

    Google(Google Inc.,NASDAQ:GOOG)是一家美国上市公司(公有股份公司),于 1998 年 9 月 7 日以私有股份公司的形式创立,设计并管理一个互联网搜索引擎。Google 公司的总部称作“Googleplex”,它位于加利福尼亚山景城。Google 目前被公认为是全球规模最大的搜索引擎,它提供了简单易用的免费服务。不作恶(Don't be evil)是谷歌公司的一项非正式的公司口号。

    49 引用 • 192 回帖
  • JSON

    JSON (JavaScript Object Notation)是一种轻量级的数据交换格式。易于人类阅读和编写。同时也易于机器解析和生成。

    51 引用 • 190 回帖 • 2 关注
  • 负能量

    上帝为你关上了一扇门,然后就去睡觉了....努力不一定能成功,但不努力一定很轻松 (° ー °〃)

    85 引用 • 1201 回帖 • 454 关注
  • HTML

    HTML5 是 HTML 下一个的主要修订版本,现在仍处于发展阶段。广义论及 HTML5 时,实际指的是包括 HTML、CSS 和 JavaScript 在内的一套技术组合。

    103 引用 • 294 回帖 • 3 关注
  • RabbitMQ

    RabbitMQ 是一个开源的 AMQP 实现,服务器端用 Erlang 语言编写,支持多种语言客户端,如:Python、Ruby、.NET、Java、C、PHP、ActionScript 等。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。

    49 引用 • 60 回帖 • 392 关注
  • MySQL

    MySQL 是一个关系型数据库管理系统,由瑞典 MySQL AB 公司开发,目前属于 Oracle 公司。MySQL 是最流行的关系型数据库管理系统之一。

    675 引用 • 535 回帖
  • Unity

    Unity 是由 Unity Technologies 开发的一个让开发者可以轻松创建诸如 2D、3D 多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

    25 引用 • 7 回帖 • 250 关注
  • CodeMirror
    1 引用 • 2 回帖 • 114 关注
  • VirtualBox

    VirtualBox 是一款开源虚拟机软件,最早由德国 Innotek 公司开发,由 Sun Microsystems 公司出品的软件,使用 Qt 编写,在 Sun 被 Oracle 收购后正式更名成 Oracle VM VirtualBox。

    10 引用 • 2 回帖 • 6 关注
  • 域名

    域名(Domain Name),简称域名、网域,是由一串用点分隔的名字组成的 Internet 上某一台计算机或计算机组的名称,用于在数据传输时标识计算机的电子方位(有时也指地理位置)。

    43 引用 • 208 回帖
  • golang

    Go 语言是 Google 推出的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。谷歌首席软件工程师罗布派克(Rob Pike)说:我们之所以开发 Go,是因为过去 10 多年间软件开发的难度令人沮丧。Go 是谷歌 2009 发布的第二款编程语言。

    491 引用 • 1383 回帖 • 373 关注
  • 阿里巴巴

    阿里巴巴网络技术有限公司(简称:阿里巴巴集团)是以曾担任英语教师的马云为首的 18 人,于 1999 年在中国杭州创立,他们相信互联网能够创造公平的竞争环境,让小企业通过创新与科技扩展业务,并在参与国内或全球市场竞争时处于更有利的位置。

    43 引用 • 221 回帖 • 243 关注
  • flomo

    flomo 是新一代 「卡片笔记」 ,专注在碎片化时代,促进你的记录,帮你积累更多知识资产。

    3 引用 • 80 回帖 • 1 关注
  • webpack

    webpack 是一个用于前端开发的模块加载器和打包工具,它能把各种资源,例如 JS、CSS(less/sass)、图片等都作为模块来使用和处理。

    41 引用 • 130 回帖 • 297 关注
  • wolai

    我来 wolai:不仅仅是未来的云端笔记!

    1 引用 • 11 回帖 • 1 关注