MultiRecyclerAdapter

Introduction: 模仿于 b 站番剧列表显示方式,而不止于其显示方式
More: Author   ReportBugs   
Tags:
RecyclerView-RecyclerViewAdapter-

MultiRecyclerAdapter for RecyclerView

Add Footer and Header for your adapter!

下面先介绍第一个MultiRecyclerAdapter

模仿 b 站列表的 Adapter,适用于 RecyclerView.

先看一下原样式效果:

像这样:

还有这样:

用法

继承项目 library 中的抽象类MultiBaseRecyclerAdapter<K, V>.就像下面这样:

/**
 * Created by : youngkaaa on 2016/10/9. * Contact me : 645326280@qq.com
 */

public class MyMultiAdapter extends MultiBaseRecyclerAdapter<TitleBean,ContentBean> {
    private OnItemClickedListener mItemClickedListener;
    private Context mContext;

    public MyMultiAdapter(Context context, Map<TitleBean, List<ContentBean>> data) {
        super(context, data);
        mContext=context;
    }

    public MyMultiAdapter(Context context, List<TitleBean> keys, Map<TitleBean, List<ContentBean>> data) {
        super(context, keys, data);
        mContext=context;
    }

    public void setItemClickedListener(OnItemClickedListener itemClickedListener) {
        mItemClickedListener = itemClickedListener;
    }

    @Override
    public int getTitleLayoutId() {
        return R.layout.title_layout;
    }

    @Override
    public int getContentLayoutId() {
        return R.layout.recycler_item;
    }

    @Override
    public void bindData(int type, BaseViewHolder holder, Object data) {
        TextView textViewLeft=holder.getViewById(R.id.textViewTitleLeft);
        TextView textViewRight=holder.getViewById(R.id.textViewTitleRight);
        TextView textViewContent=holder.getViewById(R.id.textViewRecyclerItemContent);
        ImageView imageView=holder.getViewById(R.id.imageViewContentIco);
        if(type==TYPE_TITLE){
            final TitleBean bean= (TitleBean) data;
            textViewLeft.setText(bean.getLeftTitle());
            textViewRight.setText(bean.getRightTitle());
            textViewLeft.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    if(mItemClickedListener!=null){
                        mItemClickedListener.onClick(TYPE_TITLE,bean,view);
                    }
                }
            });
            textViewRight.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    if(mItemClickedListener!=null){
                        mItemClickedListener.onClick(TYPE_TITLE,bean,view);
                    }
                }
            });

        }else{
            final ContentBean bean= (ContentBean) data;
            textViewContent.setText(bean.getContent());
            Glide.with(mContext).load(bean.getPicUrl())
                    .centerCrop().into(imageView);
            imageView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    if(mItemClickedListener!=null){
                        mItemClickedListener.onClick(TYPE_TITLE,bean,view);
                    }
                }
            });
        }
    }
}

这里我传入的KV分别为:TitleBeanContentBean。这两个分别是自定义的类。 注意:前者K是用于 Adapter 中的每个Title布局中的,即将用于Title布局的赋值,下面是我的TitleBean的代码:

/**
 * Created by : youngkaaa on 2016/10/9. * Contact me : 645326280@qq.com
 */

public class TitleBean {
    private String leftTitle;
    private String rightTitle;

    public TitleBean() {
    }

    public TitleBean(String left, String right) {
        this.leftTitle = left;
        this.rightTitle = right;
    }

    public void setLeftTitle(String leftTitle) {
        this.leftTitle = leftTitle;
    }

    public String getLeftTitle() {
        return leftTitle;
    }

    public void setRightTitle(String rightTitle) {
        this.rightTitle = rightTitle;
    }

    public String getRightTitle() {
        return rightTitle;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;

        TitleBean person = (TitleBean) obj;

        if (leftTitle != null && person.leftTitle != null && rightTitle != null && person.rightTitle != null
                && leftTitle.equals(person.leftTitle) && person.rightTitle.equals(rightTitle)) {
            return true;
        }

        return true;
    }

    @Override
    public int hashCode() {
        return 7 * leftTitle.hashCode() + 13 * rightTitle.hashCode();
    }

注意,要重写equals()hashCode()方法哦,因为我们这里用到了Map<>,而我们知道:当Map<>的键是一个自定义类(在我的这个 demo 中是 TitleBean)时,是需要重写上面这两个方法的,因为Map<>中是通过上面那两个方法来确定键是不是一样的。所以你需要重写他们俩。而至于如何重写就不是这里的重点了,去找 java 有关书籍看看吧。

然后就是ContentBean类,这个类是用来给非title布局的布局来赋值的。这个类不需要重写上面那两个方法,因为一个是值,一个是键,只有键是自定义类时才需要重写那两个方法!

然后继承实现MultiBaseRecyclerAdapter中的方法,下面逐个解释:

  • MultiBaseRecyclerAdapter(Context context, Map> data)

构造方法之一,第一个参数不用讲了吧,第二个参数就是一个Map<>参数,其中该Map<>的键用来给Title布局赋值,然后每个键对应的值用来给指定的非Title布局赋值。这样的话可以简便的实现最终的效果,但是由于Map<>的一个特点:遍历取出来的值每次顺序都是不一样的,即不能指定顺序,所以我还提供了另外一个重载构造方法,你可以按需继承,即当你不关心最终的列表顺序,只需要显示出来内容就行,那么你就继承实现该构造方法,如果你需要指定顺序,那么你就继承下一个构造方法吧!

  • MultiBaseRecyclerAdapter(Context context, List keys, Map> data)

构造方法之二,第一个参数不讲了。第二个参数就是你单独传入的键,用来给Title布局赋值,然后后面的Map<>中的对应值来给指定的非Title布局赋值。与上面的构造方法唯一不同的就是本构造方法可以指定列表顺序,即按你传入的第二个参数的顺序来放置布局。两种常用的供不同需求使用!

  • getTitleLayoutId()

重写本方法来返回Title布局的布局资源 id。所以说你可以给Title部分添加任何你想要的样式,而不只是上面 b 站的样式!

  • getContentLayoutId()

重写本方法来返回非Title布局的布局资源 id。所以说你可以给非Title部分添加任何你想要的样式,而不只是上面 b 站的样式!

  • bindData(int type, BaseViewHolder holder, Object data)

该方法就可以让你来绑定布局数据。第一个参数type有两个可选的:TYPE_TITLETYPE_OTHER。你可以通过判断来决定具体现在是要给那个布局绑定数据,第二个参数holder就是指定的BaseViewHolder,你可以用它来获得指定view,通过其getViewById()方法。第三个参数是该布局应该绑定的数据,当typeTYPE_TITLE时,该data其实就是一个TitleBean类型的数据,即你上面重写时设置的K的类型,你可以放心的使用类型转换,当typeTYPE_OTHER时,该data其实就是一个ContentBean类型的数据,即你上面重写时设置的V的类型,你可以放心的使用类型转换来绑定。这里没有牵扯到position,这些我在MultiBaseRecyclerAdapter中都做好了,哪个位置应该绑定Map<>中的哪个位置的数据我已经处理好直接返回给你了,你就光绑定就行了!示例代码如下:

  @Override
    public void bindData(int type, BaseViewHolder holder, Object data) {
        TextView textViewLeft=holder.getViewById(R.id.textViewTitleLeft);
        TextView textViewRight=holder.getViewById(R.id.textViewTitleRight);
        TextView textViewContent=holder.getViewById(R.id.textViewRecyclerItemContent);
        ImageView imageView=holder.getViewById(R.id.imageViewContentIco);
        if(type==TYPE_TITLE){
            final TitleBean bean= (TitleBean) data;
            textViewLeft.setText(bean.getLeftTitle());
            textViewRight.setText(bean.getRightTitle());
            textViewLeft.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    if(mItemClickedListener!=null){
                        mItemClickedListener.onClick(TYPE_TITLE,bean,view);
                    }
                }
            });
            textViewRight.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    if(mItemClickedListener!=null){
                        mItemClickedListener.onClick(TYPE_TITLE,bean,view);
                    }
                }
            });

        }else{
            final ContentBean bean= (ContentBean) data;
            textViewContent.setText(bean.getContent());
            Glide.with(mContext).load(bean.getPicUrl())
                    .centerCrop().into(imageView);
            imageView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    if(mItemClickedListener!=null){
                        mItemClickedListener.onClick(TYPE_TITLE,bean,view);
                    }
                }
            });
        }
    }

注意:

  1. 如果你使用第二个构造方法,那么要注意传入的键的数量和Map<>中对应的值的数量要相同,否则会抛出一个RuntimeException,内容为:Key's size should be equal to the Map's key size!

  2. 你如果要设置点击事件的话,你可以仿照我在项目中的实现,我内部现成的提供了一个点击事件的接口,你可以按照我的样子来设置点击事件,这样很轻松。当然你也可以自己定义使用自己定义的接口。

  3. 上面使用的是GridLayoutManager,当然你也可以使用LinearLayout,其中LinearLayout布局比较简单,这里就不再解释了。需要注意的是GridLayoutManager,在使用GridLayoutManager时,你需要做下面这一步:

gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
    @Override
    public int getSpanSize(int position) {
        return mMutiAdapter.isTitleView(position)?gridLayoutManager.getSpanCount():1;
    }
});

即给RecyclerView中每个Item设置 span,显然Title布局要占据一整行的,而非Title就按应有的一个占用一个格子,所以上面的mMutiAdapter.isTitleView(position)?gridLayoutManager.getSpanCount():1就不难懂了吧。而至于上面用到的isTitleView()方法你光使用就行了,我帮你做好了判断的,不要乱继承实现该方法哦!

写的差不多了,够详细了,没写到位的请查看我提供的 demo 吧,里面的代码都比较简单,所以注释就没写多少,个别重要的地方有注释。

下面贴一下运行图:

更多样式请自行实现!

更新 2016-11-1 10:14:48:增加对于Border的判断

比如我要实现时间线的效果,此时就需要获得每个Content的开始和结束,用以控制时间线的显示或者隐藏,下面先贴一下一个实际的 Demo 截图:

上面这个实例是实现时间线的效果,左边会有时间线用以连接每个图标,以达到时间线的效果。此时我们就需要把每个Content的第一个的上面的时间线和最后一个的下面的时间线隐藏掉。所以我对原先的MultiRecyclerAdapter进行了新的封装,新提供了一个内部类ItemResult,它内部的实现是这样的:

public class ItemResult {
        private int type;
        private Object data;
        private int border;

        public ItemResult(int type, Object data, int border) {
            this.type = type;
            this.data = data;
            this.border = border;
        }

        public int getBorder() {
            return border;
        }

        public int getType() {
            return type;
        }

        public Object getData() {
            return data;
        }
    }

第一个type和第二个data原先的版本中存在了的,代表的意思和上面的一样。新增了一个border属性。它有三种取值:

public static final int TOP_BORDER = 3;
public static final int BOTTOM_BORDER = 4;
public static final int OTHER_BORDER = 5;

看这个简单的图解:

上面 1 处就是TOP_BORDER,2 处就是BOTTOM_BORDER,3 处就是OTHER_BORDER

即当当前要绑定的 item 是每个Content中的第一个时,你通过 itemResult.getBorder()获得的值就是:TOP_BORDER 当时每个Content中的最后一个时,你通过 itemResult.getBorder()获得的值就是:BOTTOM_BORDER 除此之外其它的就都是OTHER_BORDER

所以在 onBind()方法实现中可以是这样的:

@Override
    public void bindData(BaseViewHolder holder, ItemResult result) {
        if (result.getType() == TYPE_TITLE) {

             //bind your data here!

        } else {
            final BookItem item = (BookItem) result.getData();

            //bind your data here!

            if(result.getBorder()==TOP_BORDER){
               //TOP_BORDER
            }else if(result.getBorder()==BOTTOM_BORDER){
                //BOTTOM_BORDER
            }else {
                //OTHER_BORDER
            }
        }
    }

这里需要注意的一点就是:当你在控制每个 Item 中的某些控件可见度时,需要考虑到 RecyclerView 的重用性哦,不然会出现 item 中控件可见性混乱的情况。千万注意!

下面介绍第二个HeaderFooterAdapter

下面先贴出图:

然后,用法很简单,就像下面这样(只贴出部分有关代码):

mMutiAdapter = new MyMultiAdapter(this, initListKeys(), initMapData());
headerFooterAdapter=new HeaderFooterAdapter(this,mMutiAdapter);
headerFooterAdapter.setFooterResId(R.layout.footer_layout);
headerFooterAdapter.setHeaderResId(R.layout.header_layout);
mRecyclerView.setAdapter(headerFooterAdapter);

就是先调用构造方法(上面第二句),将你已经实现了的 Adapter传入,这样做的优点就是几乎不用修改你原来的逻辑,只需要给你原先的RecyclerAdapter外面再包裹一个Adapter就行了,然后设置该Adapter给你的RecyclerView,这样原来的逻辑都在,就多了上面两三行代码就让你的RecyclerView有了HeaderFooter。更多用法请查看项目内的 Demo.

你需要注意的是在绑定AdapterRecyclerView时,一定要记得在之前调用setFooterResId()setHeaderResId()方法将你的布局传入哦!!

欢迎大神提意见,你有任何好的 pr 都欢迎!

如果该项目对你有用的话,给个star以示鼓励吧!你也可以顺便围观一下我的其他项目谢谢!

Apps
About Me
GitHub: Trinea
Facebook: Dev Tools