为RecyclerView打造通用Adapter 让RecyclerView更加好用

一、概述

记得好久以前针对ListView类控件写过一篇打造万能的ListView GridView 适配器,如今RecyclerView异军突起,其Adapter的用法也与ListView类似,那么我们也可以一步一步的为其打造通用的Adapter,使下列用法书写更加简单:

  • 简单的数据绑定(单种Item)
  • 多种Item Type 数据绑定
  • 增加onItemClickListener , onItenLongClickListener
  • 优雅的添加分类header

二、使用方式和效果图

在一步一步完成前,我们先看下使用方式和效果图:

(1)简单的数据绑定

首先看我们最常用的单种Item的书写方式:

mRecyclerView.setAdapter(new CommonAdapter<String>(this, R.layout.item_list, mDatas)
{
    @Override
    public void convert(ViewHolder holder, String s)
    {
        holder.setText(R.id.id_item_list_title, s);
    }
});
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

是不是相当方便,在convert方法中完成数据、事件绑定即可。

(2)多种ItemViewType

多种ItemViewType,正常考虑下,我们需要根据Item指定ItemType,并且根据ItemType指定相应的布局文件。我们通过MultiItemTypeSupport完成指定:

MultiItemTypeSupport  multiItemSupport = new MultiItemTypeSupport<ChatMessage>()
{
    @Override
    public int getLayoutId(int itemType)
    {
       //根据itemType返回item布局文件id
    }

    @Override
    public int getItemViewType(int postion, ChatMessage msg)
    {
       //根据当前的bean返回item type
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

剩下就简单了,将其作为参数传入到MultiItemCommonAdapter即可。

mRecyclerView.setAdapter(new SectionAdapter<String>(this, mDatas, multiItemSupport)
{
    @Override
    public void convert(ViewHolder holder, String s)
    {
        holder.setText(R.id.id_item_list_title, s);
    }
});
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

贴个效果图:

(3)添加分类header

其实属于多种ItemViewType的一种了,只是比较常用,我们就简单封装下。

依赖正常考虑下,这种方式需要额外指定header的布局,以及布局中显示标题的TextView了,以及根据Item显示什么样的标题。我们通过SectionSupport对象指定:

SectionSupport<String> sectionSupport = new SectionSupport<String>()
{
    @Override
    public int sectionHeaderLayoutId()
    {
        return R.layout.header;
    }

    @Override
    public int sectionTitleTextViewId()
    {
        return R.id.id_header_title;
    }

    @Override
    public String getTitle(String s)
    {
        return s.substring(0, 1);
    }
};
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

3个方法,一个指定header的布局文件,一个指定布局文件中显示title的TextView,最后一个用于指定显示什么样的标题(根据Adapter的Bean)。

接下来就很简单了:

mRecyclerView.setAdapter(new SectionAdapter<String>(this, R.layout.item_list, mDatas, sectionSupport)
{
    @Override
    public void convert(ViewHolder holder, String s)
    {
        holder.setText(R.id.id_item_list_title, s);
    }
});
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这样就完了,效果图如下:

ok,看完上面简单的介绍,相信你已经基本了解了,没错,和我上篇ListView万能Adapter的使用方式基本一样,并且已经封装到同一个库了,链接为:https://github.com/hongyangAndroid/base-adapter,此外还提供了ItemClick,ItemLongClick,添加EmptyView等支持。

说了这么多,下面进入正题,看我们如何一步步完成整个封装的过程。

三、通用的ViewHolder

RecyclerView要求必须使用ViewHolder模式,一般我们在使用过程中,都需要去建立一个新的ViewHolder然后作为泛型传入Adapter。那么想要建立通用的Adapter,必须有个通用的ViewHolder。

首先我们确定下ViewHolder的主要的作用,实际上是通过成员变量存储对应的convertView中需要操作的字View,避免每次findViewById,从而提升运行的效率。

那么既然是通用的View,那么对于不同的ItemType肯定没有办法确定创建哪些成员变量View,取而代之的只能是个集合来存储了。

那么代码如下:

public class ViewHolder extends RecyclerView.ViewHolder
{
    private SparseArray<View> mViews;
    private View mConvertView;
    private Context mContext;

    public ViewHolder(Context context, View itemView, ViewGroup parent)
    {
        super(itemView);
        mContext = context;
        mConvertView = itemView;
        mViews = new SparseArray<View>();
    }


    public static ViewHolder get(Context context, ViewGroup parent, int layoutId)
    {

        View itemView = LayoutInflater.from(context).inflate(layoutId, parent,
                false);
        ViewHolder holder = new ViewHolder(context, itemView, parent, position);
        return holder;
    }


    /**
     * 通过viewId获取控件
     *
     * @param viewId
     * @return
     */
    public <T extends View> T getView(int viewId)
    {
        View view = mViews.get(viewId);
        if (view == null)
        {
            view = mConvertView.findViewById(viewId);
            mViews.put(viewId, view);
        }
        return (T) view;
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

代码很简单,我们的ViewHolder继承自RecyclerView.ViewHolder,内部通过SparseArray来缓存我们itemView内部的子View,从而得到一个通用的ViewHolder。每次需要创建ViewHolder只需要传入我们的layoutId即可。

ok,有了通用的ViewHolder之后,我们的通用的Adapter分分钟就出来了。

四、通用的Adapter

我们的每次使用过程中,针对的数据类型Bean肯定是不同的,那么这里肯定要引入泛型代表我们的Bean,内部通过一个List代表我们的数据,ok,剩下的看代码:

package com.zhy.base.adapter.recyclerview;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.zhy.base.adapter.ViewHolder;

import java.util.List;

/**
 * Created by zhy on 16/4/9.
 */
public abstract class CommonAdapter<T> extends RecyclerView.Adapter<ViewHolder>
{
    protected Context mContext;
    protected int mLayoutId;
    protected List<T> mDatas;
    protected LayoutInflater mInflater;


    public CommonAdapter(Context context, int layoutId, List<T> datas)
    {
        mContext = context;
        mInflater = LayoutInflater.from(context);
        mLayoutId = layoutId;
        mDatas = datas;
    }

    @Override
    public ViewHolder onCreateViewHolder(final ViewGroup parent, int viewType)
    {
        ViewHolder viewHolder = ViewHolder.get(mContext, parent, mLayoutId);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position)
    {
        holder.updatePosition(position);
        convert(holder, mDatas.get(position));
    }

    public abstract void convert(ViewHolder holder, T t);

    @Override
    public int getItemCount()
    {
        return mDatas.size();
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

继承自RecyclerView.Adapter,需要复写的方法还是比较少的。首先我们使用过程中传输我们的数据集mDatas,和我们item的布局文件layoutId。

onCreateViewHolder时,通过layoutId即可利用我们的通用的ViewHolder生成实例。

onBindViewHolder这里主要用于数据、事件绑定,我们这里直接抽象出去,让用户去操作。可以看到我们修改了下参数,用户可以拿到当前Item所需要的对象和viewHolder去操作。

那么现在用户的使用是这样的:

mRecyclerView.setAdapter(new CommonAdapter<String>(this, R.layout.item_list, mDatas)
{
    @Override
    public void convert(ViewHolder holder, String s)
    {
        TextView tv = holder.getView(R.id.id_item_list_title);
        tv.setText(s);
    }
});
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

看到这里,爽了很多,目前我们仅仅写了很少的代码,但是我们的通用的Adapter感觉已经初步完成了。

可以看到我们这里通过viewholder根据控件的id拿到控件,然后再进行数据绑定和事件操作,我们还能做些什么简化呢?

恩,我们可以通过一些辅助方法简化我们的代码,所以继续往下看。

五、进一步封装ViewHolder

我们的Item实际上使用的控件较多时候可能都是TextView,ImageView等,我们一般在convert方法都是去设置文本,图片什么的,那么我们可以在ViewHolder里面,写上如下的一些辅助方法:

class ViewHolder extends RecyclerView.Adapter<ViewHolder>
{
    //...
    public ViewHolder setText(int viewId, String text)
    {
        TextView tv = getView(viewId);
        tv.setText(text);
        return this;
    }

    public ViewHolder setImageResource(int viewId, int resId)
    {
        ImageView view = getView(viewId);
        view.setImageResource(resId);
        return this;
    }

    public ViewHolder setOnClickListener(int viewId,
                                         View.OnClickListener listener)
    {
        View view = getView(viewId);
        view.setOnClickListener(listener);
        return this;
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

当然上面只给出了几个方法,你可以把常用控件的方法都写进去,并且在使用过程中不断完善即可。

有了一堆辅助方法后,我们的操作更加简化了一步。

mRecyclerView.setAdapter(new CommonAdapter<String>(this, R.layout.item_list, mDatas)
{
    @Override
    public void convert(ViewHolder holder, String s)
    {
        //TextView tv = holder.getView(R.id.id_item_list_title);
        //tv.setText(s);
        holder.setText(R.id.id_item_list_title,s);
    }
});
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

ok,到这里,我们的针对单种ViewItemType的通用Adapter就完成了,代码很简单也很少,但是简化效果非常明显。

ok,接下来我们考虑多种ItemViewType的情况。

六、多种ItemViewType

多种ItemViewType,一般我们的写法是:

  • 复写getItemViewType,根据我们的bean去返回不同的类型
  • onCreateViewHolder中根据itemView去生成不同的ViewHolder

如果大家还记得,我们的ViewHolder是通用的,唯一依赖的就是个layoutId。那么上述第二条就变成,根据不同的itemView告诉我用哪个layoutId即可,生成viewholder这种事我们通用adapter来做。

于是,引入一个接口:

public interface MultiItemTypeSupport<T>
{
    int getLayoutId(int itemType);

    int getItemViewType(int position, T t);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

可以很清楚的看到,这个接口实际就是完成我们上述的两条工作。用户在使用过程中,通过实现上面两个方法,指明不同的Bean返回什么itemViewType,不同的itemView所对应的layoutId.

ok,有了上面这个接口,我们的参数就够了,下面开始我们的MultiItemCommonAdapter的编写。

public abstract class MultiItemCommonAdapter<T> extends CommonAdapter<T>
{
    protected MultiItemTypeSupport<T> mMultiItemTypeSupport;

    public MultiItemCommonAdapter(Context context, List<T> datas,
                                  MultiItemTypeSupport<T> multiItemTypeSupport)
    {
        super(context, -1, datas);
        mMultiItemTypeSupport = multiItemTypeSupport;
    }

    @Override
    public int getItemViewType(int position)
    {
        return mMultiItemTypeSupport.getItemViewType(position, mDatas.get(position));
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
    {
        int layoutId = mMultiItemTypeSupport.getLayoutId(viewType);
        ViewHolder holder = ViewHolder.get(mContext, parent, layoutId;
        return holder;
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

几乎没有几行代码,感觉简直不需要消耗脑细胞。getItemViewType用户的传入的MultiItemTypeSupport.getItemViewType完成,onCreateViewHolder中根据MultiItemTypeSupport.getLayoutId返回的layoutId,去生成ViewHolder即可。

ok,这样的话,我们的多种ItemViewType的支持也就完成了,一路下来感觉还是蛮轻松的~~~

最后,我们还有个添加分类的header,为什么想起来封装这个呢,这个是因为我看到了这么个项目:https://github.com/ragunathjawahar/simple-section-adapter,这个项目给了种类似装饰者模式的方法,为ListView添加了header,有兴趣可以看下。我想我们的RecylerView也来个吧,不过我们这里直接通过继承Adapter完成。

七、添加分类Header

话说添加分类header,其实就是我们多种ItemViewType的一种,那么我们需要知道哪些参数呢?

简单思考下,我们需要:

  1. header所对应的布局文件
  2. 显示header的title对应的TextView
  3. 显示的title是什么(一般肯定根据Bean生成)

ok,这样的话,我们依然引入一个接口,用于提供上述3各参数

public interface SectionSupport<T>
{
    public int sectionHeaderLayoutId();

    public int sectionTitleTextViewId();

    public String getTitle(T t);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

方法名应该很明确了,这里引入泛型,对应我们使用时的数据类型Bean。

刚才也说了我们的分类header是多种ItemViewType的一种,那么直接继承MultiItemCommonAdapter实现。

public abstract class SectionAdapter<T> extends MultiItemCommonAdapter<T>
{
    private SectionSupport mSectionSupport;
    private static final int TYPE_SECTION = 0;
    private LinkedHashMap<String, Integer> mSections;

    private MultiItemTypeSupport<T> headerItemTypeSupport = new MultiItemTypeSupport<T>()
    {
        @Override
        public int getLayoutId(int itemType)
        {
            if (itemType == TYPE_SECTION)
                return mSectionSupport.sectionHeaderLayoutId();
            else
                return mLayoutId;
        }
        @Override
        public int getItemViewType(int position, T o)
        {
             return mSections.values().contains(position) ?
                    TYPE_SECTION :
                    1;
        }
    };

    @Override
    public int getItemViewType(int position)
    {
        return mMultiItemTypeSupport.getItemViewType(position, null);
    }

    final RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver()
    {
        @Override
        public void onChanged()
        {
            super.onChanged();
            findSections();
        }
    };


    public SectionAdapter(Context context, int layoutId, List<T> datas, SectionSupport sectionSupport)
    {
        super(context, datas, null);
        mLayoutId = layoutId;
        mMultiItemTypeSupport = headerItemTypeSupport;
        mSectionSupport = sectionSupport;
        mSections = new LinkedHashMap<>();
        findSections();
        registerAdapterDataObserver(observer);
    }

    @Override
    protected boolean isEnabled(int viewType)
    {
        if (viewType == TYPE_SECTION)
            return false;
        return super.isEnabled(viewType);
    }

    @Override
    public void onDetachedFromRecyclerView(RecyclerView recyclerView)
    {
        super.onDetachedFromRecyclerView(recyclerView);
        unregisterAdapterDataObserver(observer);
    }

    public void findSections()
    {
        int n = mDatas.size();
        int nSections = 0;
        mSections.clear();

        for (int i = 0; i < n; i++)
        {
            String sectionName = mSectionSupport.getTitle(mDatas.get(i));

            if (!mSections.containsKey(sectionName))
            {
                mSections.put(sectionName, i + nSections);
                nSections++;
            }
        }

    }


    @Override
    public int getItemCount()
    {
        return super.getItemCount() + mSections.size();
    }

    public int getIndexForPosition(int position)
    {
        int nSections = 0;

        Set<Map.Entry<String, Integer>> entrySet = mSections.entrySet();
        for (Map.Entry<String, Integer> entry : entrySet)
        {
            if (entry.getValue() < position)
            {
                nSections++;
            }
        }
        return position - nSections;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position)
    {
        position = getIndexForPosition(position);
        if (holder.getItemViewType() == TYPE_SECTION)
        {
            holder.setText(mSectionSupport.sectionTitleTextViewId(), mSectionSupport.getTitle(mDatas.get(position)));
            return;
        }
        super.onBindViewHolder(holder, position);
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121

根据我们之前的代码,使用MultiItemCommonAdapter,需要提供一个MultiItemTypeSupport,我们这里当然也不例外。可以看到上述代码,我们初始化了成员变量headerItemTypeSupport,分别对getLayoutIdgetItemViewType进行了实现。

  • getLayoutId如果type是header类型,则返回mSectionSupport.sectionHeaderLayoutId();否则则返回mLayout.
  • getItemViewType根据位置判断,如果当前是header所在位置,返回header类型常量;否则返回1.

ok,可以看到我们构造方法中调用了findSections(),主要为了存储我们的title和对应的position,通过一个MapmSections来存储。

那么对应的getItemCount()方法,我们多了几个title肯定总数会增加,所以需要复写。

onBindViewHolder中我们有一行重置position的代码,因为我们的position变大了,所以在实际上绑定我们数据时,这个position需要还原,代码逻辑见getIndexForPosition(position)

最后一点就是,每当我们的数据发生变化,我们的title集合,即mSections就可能会发生变化,所以需要重新生成,本来准备复写notifyDataSetChanged方法,在里面重新生成,没想到这个方法是final的,于是利用了registerAdapterDataObserver(observer);,在数据发生变化回调中重新生成,记得在onDetachedFromRecyclerView里面对注册的observer进行解注册。

ok,到此我们的增加Header就结束了~~

恩,上面是针对普通的Item增加header的代码,如果是针对多种ItemViewType呢?其实也很简单,这种方式需要传入MultiItemTypeSupport。那么对于headerItemTypeSupport中的getItemViewType等方法,不是header类型时,交给传入的MultiItemTypeSupport即可,大致的代码如下:

headerItemTypeSupport = new MultiItemTypeSupport<T>()
{
    @Override
    public int getLayoutId(int itemType)
    {
        if (itemType == TYPE_SECTION)
            return mSectionSupport.sectionHeaderLayoutId();
        else
            return multiItemTypeSupport.getLayoutId(itemType);
    }

    @Override
    public int getItemViewType(int position, T o)
    {
        int positionVal = getIndexForPosition(position);
        return mSections.values().contains(position) ?
                TYPE_SECTION :
                multiItemTypeSupport.getItemViewType(positionVal, o);
    }
};
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

那么这样的话,今天的博客就结束了,有几点需要说明下:

本来是想接着以前的万能Adapter后面写,但是为了本文的独立和完整性,还是尽可能没有去依赖上篇博客的内容了。

此外,文章最后给出的开源代码与上述代码存在些许的差异,因为开源部分源码整合了ListView,RecyclerView等,而本文上述代码完全针对RecyclerView进行编写。

对于ItemClick,ItemLongClick的代码就不赘述了,其实都是通过itemView.setXXXListener完成,详细的参考代码即可。

一、概述

记得好久以前针对ListView类控件写过一篇打造万能的ListView GridView 适配器,如今RecyclerView异军突起,其Adapter的用法也与ListView类似,那么我们也可以一步一步的为其打造通用的Adapter,使下列用法书写更加简单:

  • 简单的数据绑定(单种Item)
  • 多种Item Type 数据绑定
  • 增加onItemClickListener , onItenLongClickListener
  • 优雅的添加分类header

二、使用方式和效果图

在一步一步完成前,我们先看下使用方式和效果图:

(1)简单的数据绑定

首先看我们最常用的单种Item的书写方式:

mRecyclerView.setAdapter(new CommonAdapter<String>(this, R.layout.item_list, mDatas)
{
    @Override
    public void convert(ViewHolder holder, String s)
    {
        holder.setText(R.id.id_item_list_title, s);
    }
});
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

是不是相当方便,在convert方法中完成数据、事件绑定即可。

(2)多种ItemViewType

多种ItemViewType,正常考虑下,我们需要根据Item指定ItemType,并且根据ItemType指定相应的布局文件。我们通过MultiItemTypeSupport完成指定:

MultiItemTypeSupport  multiItemSupport = new MultiItemTypeSupport<ChatMessage>()
{
    @Override
    public int getLayoutId(int itemType)
    {
       //根据itemType返回item布局文件id
    }

    @Override
    public int getItemViewType(int postion, ChatMessage msg)
    {
       //根据当前的bean返回item type
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

剩下就简单了,将其作为参数传入到MultiItemCommonAdapter即可。

mRecyclerView.setAdapter(new SectionAdapter<String>(this, mDatas, multiItemSupport)
{
    @Override
    public void convert(ViewHolder holder, String s)
    {
        holder.setText(R.id.id_item_list_title, s);
    }
});
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

贴个效果图:

(3)添加分类header

其实属于多种ItemViewType的一种了,只是比较常用,我们就简单封装下。

依赖正常考虑下,这种方式需要额外指定header的布局,以及布局中显示标题的TextView了,以及根据Item显示什么样的标题。我们通过SectionSupport对象指定:

SectionSupport<String> sectionSupport = new SectionSupport<String>()
{
    @Override
    public int sectionHeaderLayoutId()
    {
        return R.layout.header;
    }

    @Override
    public int sectionTitleTextViewId()
    {
        return R.id.id_header_title;
    }

    @Override
    public String getTitle(String s)
    {
        return s.substring(0, 1);
    }
};
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

3个方法,一个指定header的布局文件,一个指定布局文件中显示title的TextView,最后一个用于指定显示什么样的标题(根据Adapter的Bean)。

接下来就很简单了:

mRecyclerView.setAdapter(new SectionAdapter<String>(this, R.layout.item_list, mDatas, sectionSupport)
{
    @Override
    public void convert(ViewHolder holder, String s)
    {
        holder.setText(R.id.id_item_list_title, s);
    }
});
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这样就完了,效果图如下:

ok,看完上面简单的介绍,相信你已经基本了解了,没错,和我上篇ListView万能Adapter的使用方式基本一样,并且已经封装到同一个库了,链接为:https://github.com/hongyangAndroid/base-adapter,此外还提供了ItemClick,ItemLongClick,添加EmptyView等支持。

说了这么多,下面进入正题,看我们如何一步步完成整个封装的过程。

三、通用的ViewHolder

RecyclerView要求必须使用ViewHolder模式,一般我们在使用过程中,都需要去建立一个新的ViewHolder然后作为泛型传入Adapter。那么想要建立通用的Adapter,必须有个通用的ViewHolder。

首先我们确定下ViewHolder的主要的作用,实际上是通过成员变量存储对应的convertView中需要操作的字View,避免每次findViewById,从而提升运行的效率。

那么既然是通用的View,那么对于不同的ItemType肯定没有办法确定创建哪些成员变量View,取而代之的只能是个集合来存储了。

那么代码如下:

public class ViewHolder extends RecyclerView.ViewHolder
{
    private SparseArray<View> mViews;
    private View mConvertView;
    private Context mContext;

    public ViewHolder(Context context, View itemView, ViewGroup parent)
    {
        super(itemView);
        mContext = context;
        mConvertView = itemView;
        mViews = new SparseArray<View>();
    }


    public static ViewHolder get(Context context, ViewGroup parent, int layoutId)
    {

        View itemView = LayoutInflater.from(context).inflate(layoutId, parent,
                false);
        ViewHolder holder = new ViewHolder(context, itemView, parent, position);
        return holder;
    }


    /**
     * 通过viewId获取控件
     *
     * @param viewId
     * @return
     */
    public <T extends View> T getView(int viewId)
    {
        View view = mViews.get(viewId);
        if (view == null)
        {
            view = mConvertView.findViewById(viewId);
            mViews.put(viewId, view);
        }
        return (T) view;
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

代码很简单,我们的ViewHolder继承自RecyclerView.ViewHolder,内部通过SparseArray来缓存我们itemView内部的子View,从而得到一个通用的ViewHolder。每次需要创建ViewHolder只需要传入我们的layoutId即可。

ok,有了通用的ViewHolder之后,我们的通用的Adapter分分钟就出来了。

四、通用的Adapter

我们的每次使用过程中,针对的数据类型Bean肯定是不同的,那么这里肯定要引入泛型代表我们的Bean,内部通过一个List代表我们的数据,ok,剩下的看代码:

package com.zhy.base.adapter.recyclerview;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.zhy.base.adapter.ViewHolder;

import java.util.List;

/**
 * Created by zhy on 16/4/9.
 */
public abstract class CommonAdapter<T> extends RecyclerView.Adapter<ViewHolder>
{
    protected Context mContext;
    protected int mLayoutId;
    protected List<T> mDatas;
    protected LayoutInflater mInflater;


    public CommonAdapter(Context context, int layoutId, List<T> datas)
    {
        mContext = context;
        mInflater = LayoutInflater.from(context);
        mLayoutId = layoutId;
        mDatas = datas;
    }

    @Override
    public ViewHolder onCreateViewHolder(final ViewGroup parent, int viewType)
    {
        ViewHolder viewHolder = ViewHolder.get(mContext, parent, mLayoutId);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position)
    {
        holder.updatePosition(position);
        convert(holder, mDatas.get(position));
    }

    public abstract void convert(ViewHolder holder, T t);

    @Override
    public int getItemCount()
    {
        return mDatas.size();
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

继承自RecyclerView.Adapter,需要复写的方法还是比较少的。首先我们使用过程中传输我们的数据集mDatas,和我们item的布局文件layoutId。

onCreateViewHolder时,通过layoutId即可利用我们的通用的ViewHolder生成实例。

onBindViewHolder这里主要用于数据、事件绑定,我们这里直接抽象出去,让用户去操作。可以看到我们修改了下参数,用户可以拿到当前Item所需要的对象和viewHolder去操作。

那么现在用户的使用是这样的:

mRecyclerView.setAdapter(new CommonAdapter<String>(this, R.layout.item_list, mDatas)
{
    @Override
    public void convert(ViewHolder holder, String s)
    {
        TextView tv = holder.getView(R.id.id_item_list_title);
        tv.setText(s);
    }
});
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

看到这里,爽了很多,目前我们仅仅写了很少的代码,但是我们的通用的Adapter感觉已经初步完成了。

可以看到我们这里通过viewholder根据控件的id拿到控件,然后再进行数据绑定和事件操作,我们还能做些什么简化呢?

恩,我们可以通过一些辅助方法简化我们的代码,所以继续往下看。

五、进一步封装ViewHolder

我们的Item实际上使用的控件较多时候可能都是TextView,ImageView等,我们一般在convert方法都是去设置文本,图片什么的,那么我们可以在ViewHolder里面,写上如下的一些辅助方法:

class ViewHolder extends RecyclerView.Adapter<ViewHolder>
{
    //...
    public ViewHolder setText(int viewId, String text)
    {
        TextView tv = getView(viewId);
        tv.setText(text);
        return this;
    }

    public ViewHolder setImageResource(int viewId, int resId)
    {
        ImageView view = getView(viewId);
        view.setImageResource(resId);
        return this;
    }

    public ViewHolder setOnClickListener(int viewId,
                                         View.OnClickListener listener)
    {
        View view = getView(viewId);
        view.setOnClickListener(listener);
        return this;
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

当然上面只给出了几个方法,你可以把常用控件的方法都写进去,并且在使用过程中不断完善即可。

有了一堆辅助方法后,我们的操作更加简化了一步。

mRecyclerView.setAdapter(new CommonAdapter<String>(this, R.layout.item_list, mDatas)
{
    @Override
    public void convert(ViewHolder holder, String s)
    {
        //TextView tv = holder.getView(R.id.id_item_list_title);
        //tv.setText(s);
        holder.setText(R.id.id_item_list_title,s);
    }
});
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

ok,到这里,我们的针对单种ViewItemType的通用Adapter就完成了,代码很简单也很少,但是简化效果非常明显。

ok,接下来我们考虑多种ItemViewType的情况。

六、多种ItemViewType

多种ItemViewType,一般我们的写法是:

  • 复写getItemViewType,根据我们的bean去返回不同的类型
  • onCreateViewHolder中根据itemView去生成不同的ViewHolder

如果大家还记得,我们的ViewHolder是通用的,唯一依赖的就是个layoutId。那么上述第二条就变成,根据不同的itemView告诉我用哪个layoutId即可,生成viewholder这种事我们通用adapter来做。

于是,引入一个接口:

public interface MultiItemTypeSupport<T>
{
    int getLayoutId(int itemType);

    int getItemViewType(int position, T t);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

可以很清楚的看到,这个接口实际就是完成我们上述的两条工作。用户在使用过程中,通过实现上面两个方法,指明不同的Bean返回什么itemViewType,不同的itemView所对应的layoutId.

ok,有了上面这个接口,我们的参数就够了,下面开始我们的MultiItemCommonAdapter的编写。

public abstract class MultiItemCommonAdapter<T> extends CommonAdapter<T>
{
    protected MultiItemTypeSupport<T> mMultiItemTypeSupport;

    public MultiItemCommonAdapter(Context context, List<T> datas,
                                  MultiItemTypeSupport<T> multiItemTypeSupport)
    {
        super(context, -1, datas);
        mMultiItemTypeSupport = multiItemTypeSupport;
    }

    @Override
    public int getItemViewType(int position)
    {
        return mMultiItemTypeSupport.getItemViewType(position, mDatas.get(position));
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
    {
        int layoutId = mMultiItemTypeSupport.getLayoutId(viewType);
        ViewHolder holder = ViewHolder.get(mContext, parent, layoutId;
        return holder;
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

几乎没有几行代码,感觉简直不需要消耗脑细胞。getItemViewType用户的传入的MultiItemTypeSupport.getItemViewType完成,onCreateViewHolder中根据MultiItemTypeSupport.getLayoutId返回的layoutId,去生成ViewHolder即可。

ok,这样的话,我们的多种ItemViewType的支持也就完成了,一路下来感觉还是蛮轻松的~~~

最后,我们还有个添加分类的header,为什么想起来封装这个呢,这个是因为我看到了这么个项目:https://github.com/ragunathjawahar/simple-section-adapter,这个项目给了种类似装饰者模式的方法,为ListView添加了header,有兴趣可以看下。我想我们的RecylerView也来个吧,不过我们这里直接通过继承Adapter完成。

七、添加分类Header

话说添加分类header,其实就是我们多种ItemViewType的一种,那么我们需要知道哪些参数呢?

简单思考下,我们需要:

  1. header所对应的布局文件
  2. 显示header的title对应的TextView
  3. 显示的title是什么(一般肯定根据Bean生成)

ok,这样的话,我们依然引入一个接口,用于提供上述3各参数

public interface SectionSupport<T>
{
    public int sectionHeaderLayoutId();

    public int sectionTitleTextViewId();

    public String getTitle(T t);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

方法名应该很明确了,这里引入泛型,对应我们使用时的数据类型Bean。

刚才也说了我们的分类header是多种ItemViewType的一种,那么直接继承MultiItemCommonAdapter实现。

public abstract class SectionAdapter<T> extends MultiItemCommonAdapter<T>
{
    private SectionSupport mSectionSupport;
    private static final int TYPE_SECTION = 0;
    private LinkedHashMap<String, Integer> mSections;

    private MultiItemTypeSupport<T> headerItemTypeSupport = new MultiItemTypeSupport<T>()
    {
        @Override
        public int getLayoutId(int itemType)
        {
            if (itemType == TYPE_SECTION)
                return mSectionSupport.sectionHeaderLayoutId();
            else
                return mLayoutId;
        }
        @Override
        public int getItemViewType(int position, T o)
        {
             return mSections.values().contains(position) ?
                    TYPE_SECTION :
                    1;
        }
    };

    @Override
    public int getItemViewType(int position)
    {
        return mMultiItemTypeSupport.getItemViewType(position, null);
    }

    final RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver()
    {
        @Override
        public void onChanged()
        {
            super.onChanged();
            findSections();
        }
    };


    public SectionAdapter(Context context, int layoutId, List<T> datas, SectionSupport sectionSupport)
    {
        super(context, datas, null);
        mLayoutId = layoutId;
        mMultiItemTypeSupport = headerItemTypeSupport;
        mSectionSupport = sectionSupport;
        mSections = new LinkedHashMap<>();
        findSections();
        registerAdapterDataObserver(observer);
    }

    @Override
    protected boolean isEnabled(int viewType)
    {
        if (viewType == TYPE_SECTION)
            return false;
        return super.isEnabled(viewType);
    }

    @Override
    public void onDetachedFromRecyclerView(RecyclerView recyclerView)
    {
        super.onDetachedFromRecyclerView(recyclerView);
        unregisterAdapterDataObserver(observer);
    }

    public void findSections()
    {
        int n = mDatas.size();
        int nSections = 0;
        mSections.clear();

        for (int i = 0; i < n; i++)
        {
            String sectionName = mSectionSupport.getTitle(mDatas.get(i));

            if (!mSections.containsKey(sectionName))
            {
                mSections.put(sectionName, i + nSections);
                nSections++;
            }
        }

    }


    @Override
    public int getItemCount()
    {
        return super.getItemCount() + mSections.size();
    }

    public int getIndexForPosition(int position)
    {
        int nSections = 0;

        Set<Map.Entry<String, Integer>> entrySet = mSections.entrySet();
        for (Map.Entry<String, Integer> entry : entrySet)
        {
            if (entry.getValue() < position)
            {
                nSections++;
            }
        }
        return position - nSections;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position)
    {
        position = getIndexForPosition(position);
        if (holder.getItemViewType() == TYPE_SECTION)
        {
            holder.setText(mSectionSupport.sectionTitleTextViewId(), mSectionSupport.getTitle(mDatas.get(position)));
            return;
        }
        super.onBindViewHolder(holder, position);
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121

根据我们之前的代码,使用MultiItemCommonAdapter,需要提供一个MultiItemTypeSupport,我们这里当然也不例外。可以看到上述代码,我们初始化了成员变量headerItemTypeSupport,分别对getLayoutIdgetItemViewType进行了实现。

  • getLayoutId如果type是header类型,则返回mSectionSupport.sectionHeaderLayoutId();否则则返回mLayout.
  • getItemViewType根据位置判断,如果当前是header所在位置,返回header类型常量;否则返回1.

ok,可以看到我们构造方法中调用了findSections(),主要为了存储我们的title和对应的position,通过一个MapmSections来存储。

那么对应的getItemCount()方法,我们多了几个title肯定总数会增加,所以需要复写。

onBindViewHolder中我们有一行重置position的代码,因为我们的position变大了,所以在实际上绑定我们数据时,这个position需要还原,代码逻辑见getIndexForPosition(position)

最后一点就是,每当我们的数据发生变化,我们的title集合,即mSections就可能会发生变化,所以需要重新生成,本来准备复写notifyDataSetChanged方法,在里面重新生成,没想到这个方法是final的,于是利用了registerAdapterDataObserver(observer);,在数据发生变化回调中重新生成,记得在onDetachedFromRecyclerView里面对注册的observer进行解注册。

ok,到此我们的增加Header就结束了~~

恩,上面是针对普通的Item增加header的代码,如果是针对多种ItemViewType呢?其实也很简单,这种方式需要传入MultiItemTypeSupport。那么对于headerItemTypeSupport中的getItemViewType等方法,不是header类型时,交给传入的MultiItemTypeSupport即可,大致的代码如下:

headerItemTypeSupport = new MultiItemTypeSupport<T>()
{
    @Override
    public int getLayoutId(int itemType)
    {
        if (itemType == TYPE_SECTION)
            return mSectionSupport.sectionHeaderLayoutId();
        else
            return multiItemTypeSupport.getLayoutId(itemType);
    }

    @Override
    public int getItemViewType(int position, T o)
    {
        int positionVal = getIndexForPosition(position);
        return mSections.values().contains(position) ?
                TYPE_SECTION :
                multiItemTypeSupport.getItemViewType(positionVal, o);
    }
};
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

那么这样的话,今天的博客就结束了,有几点需要说明下:

本来是想接着以前的万能Adapter后面写,但是为了本文的独立和完整性,还是尽可能没有去依赖上篇博客的内容了。

此外,文章最后给出的开源代码与上述代码存在些许的差异,因为开源部分源码整合了ListView,RecyclerView等,而本文上述代码完全针对RecyclerView进行编写。

对于ItemClick,ItemLongClick的代码就不赘述了,其实都是通过itemView.setXXXListener完成,详细的参考代码即可。

一、概述

记得好久以前针对ListView类控件写过一篇打造万能的ListView GridView 适配器,如今RecyclerView异军突起,其Adapter的用法也与ListView类似,那么我们也可以一步一步的为其打造通用的Adapter,使下列用法书写更加简单:

  • 简单的数据绑定(单种Item)
  • 多种Item Type 数据绑定
  • 增加onItemClickListener , onItenLongClickListener
  • 优雅的添加分类header

二、使用方式和效果图

在一步一步完成前,我们先看下使用方式和效果图:

(1)简单的数据绑定

首先看我们最常用的单种Item的书写方式:

mRecyclerView.setAdapter(new CommonAdapter<String>(this, R.layout.item_list, mDatas)
{
    @Override
    public void convert(ViewHolder holder, String s)
    {
        holder.setText(R.id.id_item_list_title, s);
    }
});
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

是不是相当方便,在convert方法中完成数据、事件绑定即可。

(2)多种ItemViewType

多种ItemViewType,正常考虑下,我们需要根据Item指定ItemType,并且根据ItemType指定相应的布局文件。我们通过MultiItemTypeSupport完成指定:

MultiItemTypeSupport  multiItemSupport = new MultiItemTypeSupport<ChatMessage>()
{
    @Override
    public int getLayoutId(int itemType)
    {
       //根据itemType返回item布局文件id
    }

    @Override
    public int getItemViewType(int postion, ChatMessage msg)
    {
       //根据当前的bean返回item type
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

剩下就简单了,将其作为参数传入到MultiItemCommonAdapter即可。

mRecyclerView.setAdapter(new SectionAdapter<String>(this, mDatas, multiItemSupport)
{
    @Override
    public void convert(ViewHolder holder, String s)
    {
        holder.setText(R.id.id_item_list_title, s);
    }
});
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

贴个效果图:

(3)添加分类header

其实属于多种ItemViewType的一种了,只是比较常用,我们就简单封装下。

依赖正常考虑下,这种方式需要额外指定header的布局,以及布局中显示标题的TextView了,以及根据Item显示什么样的标题。我们通过SectionSupport对象指定:

SectionSupport<String> sectionSupport = new SectionSupport<String>()
{
    @Override
    public int sectionHeaderLayoutId()
    {
        return R.layout.header;
    }

    @Override
    public int sectionTitleTextViewId()
    {
        return R.id.id_header_title;
    }

    @Override
    public String getTitle(String s)
    {
        return s.substring(0, 1);
    }
};
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

3个方法,一个指定header的布局文件,一个指定布局文件中显示title的TextView,最后一个用于指定显示什么样的标题(根据Adapter的Bean)。

接下来就很简单了:

mRecyclerView.setAdapter(new SectionAdapter<String>(this, R.layout.item_list, mDatas, sectionSupport)
{
    @Override
    public void convert(ViewHolder holder, String s)
    {
        holder.setText(R.id.id_item_list_title, s);
    }
});
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这样就完了,效果图如下:

ok,看完上面简单的介绍,相信你已经基本了解了,没错,和我上篇ListView万能Adapter的使用方式基本一样,并且已经封装到同一个库了,链接为:https://github.com/hongyangAndroid/base-adapter,此外还提供了ItemClick,ItemLongClick,添加EmptyView等支持。

说了这么多,下面进入正题,看我们如何一步步完成整个封装的过程。

三、通用的ViewHolder

RecyclerView要求必须使用ViewHolder模式,一般我们在使用过程中,都需要去建立一个新的ViewHolder然后作为泛型传入Adapter。那么想要建立通用的Adapter,必须有个通用的ViewHolder。

首先我们确定下ViewHolder的主要的作用,实际上是通过成员变量存储对应的convertView中需要操作的字View,避免每次findViewById,从而提升运行的效率。

那么既然是通用的View,那么对于不同的ItemType肯定没有办法确定创建哪些成员变量View,取而代之的只能是个集合来存储了。

那么代码如下:

public class ViewHolder extends RecyclerView.ViewHolder
{
    private SparseArray<View> mViews;
    private View mConvertView;
    private Context mContext;

    public ViewHolder(Context context, View itemView, ViewGroup parent)
    {
        super(itemView);
        mContext = context;
        mConvertView = itemView;
        mViews = new SparseArray<View>();
    }


    public static ViewHolder get(Context context, ViewGroup parent, int layoutId)
    {

        View itemView = LayoutInflater.from(context).inflate(layoutId, parent,
                false);
        ViewHolder holder = new ViewHolder(context, itemView, parent, position);
        return holder;
    }


    /**
     * 通过viewId获取控件
     *
     * @param viewId
     * @return
     */
    public <T extends View> T getView(int viewId)
    {
        View view = mViews.get(viewId);
        if (view == null)
        {
            view = mConvertView.findViewById(viewId);
            mViews.put(viewId, view);
        }
        return (T) view;
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

代码很简单,我们的ViewHolder继承自RecyclerView.ViewHolder,内部通过SparseArray来缓存我们itemView内部的子View,从而得到一个通用的ViewHolder。每次需要创建ViewHolder只需要传入我们的layoutId即可。

ok,有了通用的ViewHolder之后,我们的通用的Adapter分分钟就出来了。

四、通用的Adapter

我们的每次使用过程中,针对的数据类型Bean肯定是不同的,那么这里肯定要引入泛型代表我们的Bean,内部通过一个List代表我们的数据,ok,剩下的看代码:

package com.zhy.base.adapter.recyclerview;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.zhy.base.adapter.ViewHolder;

import java.util.List;

/**
 * Created by zhy on 16/4/9.
 */
public abstract class CommonAdapter<T> extends RecyclerView.Adapter<ViewHolder>
{
    protected Context mContext;
    protected int mLayoutId;
    protected List<T> mDatas;
    protected LayoutInflater mInflater;


    public CommonAdapter(Context context, int layoutId, List<T> datas)
    {
        mContext = context;
        mInflater = LayoutInflater.from(context);
        mLayoutId = layoutId;
        mDatas = datas;
    }

    @Override
    public ViewHolder onCreateViewHolder(final ViewGroup parent, int viewType)
    {
        ViewHolder viewHolder = ViewHolder.get(mContext, parent, mLayoutId);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position)
    {
        holder.updatePosition(position);
        convert(holder, mDatas.get(position));
    }

    public abstract void convert(ViewHolder holder, T t);

    @Override
    public int getItemCount()
    {
        return mDatas.size();
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

继承自RecyclerView.Adapter,需要复写的方法还是比较少的。首先我们使用过程中传输我们的数据集mDatas,和我们item的布局文件layoutId。

onCreateViewHolder时,通过layoutId即可利用我们的通用的ViewHolder生成实例。

onBindViewHolder这里主要用于数据、事件绑定,我们这里直接抽象出去,让用户去操作。可以看到我们修改了下参数,用户可以拿到当前Item所需要的对象和viewHolder去操作。

那么现在用户的使用是这样的:

mRecyclerView.setAdapter(new CommonAdapter<String>(this, R.layout.item_list, mDatas)
{
    @Override
    public void convert(ViewHolder holder, String s)
    {
        TextView tv = holder.getView(R.id.id_item_list_title);
        tv.setText(s);
    }
});
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

看到这里,爽了很多,目前我们仅仅写了很少的代码,但是我们的通用的Adapter感觉已经初步完成了。

可以看到我们这里通过viewholder根据控件的id拿到控件,然后再进行数据绑定和事件操作,我们还能做些什么简化呢?

恩,我们可以通过一些辅助方法简化我们的代码,所以继续往下看。

五、进一步封装ViewHolder

我们的Item实际上使用的控件较多时候可能都是TextView,ImageView等,我们一般在convert方法都是去设置文本,图片什么的,那么我们可以在ViewHolder里面,写上如下的一些辅助方法:

class ViewHolder extends RecyclerView.Adapter<ViewHolder>
{
    //...
    public ViewHolder setText(int viewId, String text)
    {
        TextView tv = getView(viewId);
        tv.setText(text);
        return this;
    }

    public ViewHolder setImageResource(int viewId, int resId)
    {
        ImageView view = getView(viewId);
        view.setImageResource(resId);
        return this;
    }

    public ViewHolder setOnClickListener(int viewId,
                                         View.OnClickListener listener)
    {
        View view = getView(viewId);
        view.setOnClickListener(listener);
        return this;
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

当然上面只给出了几个方法,你可以把常用控件的方法都写进去,并且在使用过程中不断完善即可。

有了一堆辅助方法后,我们的操作更加简化了一步。

mRecyclerView.setAdapter(new CommonAdapter<String>(this, R.layout.item_list, mDatas)
{
    @Override
    public void convert(ViewHolder holder, String s)
    {
        //TextView tv = holder.getView(R.id.id_item_list_title);
        //tv.setText(s);
        holder.setText(R.id.id_item_list_title,s);
    }
});
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

ok,到这里,我们的针对单种ViewItemType的通用Adapter就完成了,代码很简单也很少,但是简化效果非常明显。

ok,接下来我们考虑多种ItemViewType的情况。

六、多种ItemViewType

多种ItemViewType,一般我们的写法是:

  • 复写getItemViewType,根据我们的bean去返回不同的类型
  • onCreateViewHolder中根据itemView去生成不同的ViewHolder

如果大家还记得,我们的ViewHolder是通用的,唯一依赖的就是个layoutId。那么上述第二条就变成,根据不同的itemView告诉我用哪个layoutId即可,生成viewholder这种事我们通用adapter来做。

于是,引入一个接口:

public interface MultiItemTypeSupport<T>
{
    int getLayoutId(int itemType);

    int getItemViewType(int position, T t);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

可以很清楚的看到,这个接口实际就是完成我们上述的两条工作。用户在使用过程中,通过实现上面两个方法,指明不同的Bean返回什么itemViewType,不同的itemView所对应的layoutId.

ok,有了上面这个接口,我们的参数就够了,下面开始我们的MultiItemCommonAdapter的编写。

public abstract class MultiItemCommonAdapter<T> extends CommonAdapter<T>
{
    protected MultiItemTypeSupport<T> mMultiItemTypeSupport;

    public MultiItemCommonAdapter(Context context, List<T> datas,
                                  MultiItemTypeSupport<T> multiItemTypeSupport)
    {
        super(context, -1, datas);
        mMultiItemTypeSupport = multiItemTypeSupport;
    }

    @Override
    public int getItemViewType(int position)
    {
        return mMultiItemTypeSupport.getItemViewType(position, mDatas.get(position));
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
    {
        int layoutId = mMultiItemTypeSupport.getLayoutId(viewType);
        ViewHolder holder = ViewHolder.get(mContext, parent, layoutId;
        return holder;
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

几乎没有几行代码,感觉简直不需要消耗脑细胞。getItemViewType用户的传入的MultiItemTypeSupport.getItemViewType完成,onCreateViewHolder中根据MultiItemTypeSupport.getLayoutId返回的layoutId,去生成ViewHolder即可。

ok,这样的话,我们的多种ItemViewType的支持也就完成了,一路下来感觉还是蛮轻松的~~~

最后,我们还有个添加分类的header,为什么想起来封装这个呢,这个是因为我看到了这么个项目:https://github.com/ragunathjawahar/simple-section-adapter,这个项目给了种类似装饰者模式的方法,为ListView添加了header,有兴趣可以看下。我想我们的RecylerView也来个吧,不过我们这里直接通过继承Adapter完成。

七、添加分类Header

话说添加分类header,其实就是我们多种ItemViewType的一种,那么我们需要知道哪些参数呢?

简单思考下,我们需要:

  1. header所对应的布局文件
  2. 显示header的title对应的TextView
  3. 显示的title是什么(一般肯定根据Bean生成)

ok,这样的话,我们依然引入一个接口,用于提供上述3各参数

public interface SectionSupport<T>
{
    public int sectionHeaderLayoutId();

    public int sectionTitleTextViewId();

    public String getTitle(T t);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

方法名应该很明确了,这里引入泛型,对应我们使用时的数据类型Bean。

刚才也说了我们的分类header是多种ItemViewType的一种,那么直接继承MultiItemCommonAdapter实现。

public abstract class SectionAdapter<T> extends MultiItemCommonAdapter<T>
{
    private SectionSupport mSectionSupport;
    private static final int TYPE_SECTION = 0;
    private LinkedHashMap<String, Integer> mSections;

    private MultiItemTypeSupport<T> headerItemTypeSupport = new MultiItemTypeSupport<T>()
    {
        @Override
        public int getLayoutId(int itemType)
        {
            if (itemType == TYPE_SECTION)
                return mSectionSupport.sectionHeaderLayoutId();
            else
                return mLayoutId;
        }
        @Override
        public int getItemViewType(int position, T o)
        {
             return mSections.values().contains(position) ?
                    TYPE_SECTION :
                    1;
        }
    };

    @Override
    public int getItemViewType(int position)
    {
        return mMultiItemTypeSupport.getItemViewType(position, null);
    }

    final RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver()
    {
        @Override
        public void onChanged()
        {
            super.onChanged();
            findSections();
        }
    };


    public SectionAdapter(Context context, int layoutId, List<T> datas, SectionSupport sectionSupport)
    {
        super(context, datas, null);
        mLayoutId = layoutId;
        mMultiItemTypeSupport = headerItemTypeSupport;
        mSectionSupport = sectionSupport;
        mSections = new LinkedHashMap<>();
        findSections();
        registerAdapterDataObserver(observer);
    }

    @Override
    protected boolean isEnabled(int viewType)
    {
        if (viewType == TYPE_SECTION)
            return false;
        return super.isEnabled(viewType);
    }

    @Override
    public void onDetachedFromRecyclerView(RecyclerView recyclerView)
    {
        super.onDetachedFromRecyclerView(recyclerView);
        unregisterAdapterDataObserver(observer);
    }

    public void findSections()
    {
        int n = mDatas.size();
        int nSections = 0;
        mSections.clear();

        for (int i = 0; i < n; i++)
        {
            String sectionName = mSectionSupport.getTitle(mDatas.get(i));

            if (!mSections.containsKey(sectionName))
            {
                mSections.put(sectionName, i + nSections);
                nSections++;
            }
        }

    }


    @Override
    public int getItemCount()
    {
        return super.getItemCount() + mSections.size();
    }

    public int getIndexForPosition(int position)
    {
        int nSections = 0;

        Set<Map.Entry<String, Integer>> entrySet = mSections.entrySet();
        for (Map.Entry<String, Integer> entry : entrySet)
        {
            if (entry.getValue() < position)
            {
                nSections++;
            }
        }
        return position - nSections;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position)
    {
        position = getIndexForPosition(position);
        if (holder.getItemViewType() == TYPE_SECTION)
        {
            holder.setText(mSectionSupport.sectionTitleTextViewId(), mSectionSupport.getTitle(mDatas.get(position)));
            return;
        }
        super.onBindViewHolder(holder, position);
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121

根据我们之前的代码,使用MultiItemCommonAdapter,需要提供一个MultiItemTypeSupport,我们这里当然也不例外。可以看到上述代码,我们初始化了成员变量headerItemTypeSupport,分别对getLayoutIdgetItemViewType进行了实现。

  • getLayoutId如果type是header类型,则返回mSectionSupport.sectionHeaderLayoutId();否则则返回mLayout.
  • getItemViewType根据位置判断,如果当前是header所在位置,返回header类型常量;否则返回1.

ok,可以看到我们构造方法中调用了findSections(),主要为了存储我们的title和对应的position,通过一个MapmSections来存储。

那么对应的getItemCount()方法,我们多了几个title肯定总数会增加,所以需要复写。

onBindViewHolder中我们有一行重置position的代码,因为我们的position变大了,所以在实际上绑定我们数据时,这个position需要还原,代码逻辑见getIndexForPosition(position)

最后一点就是,每当我们的数据发生变化,我们的title集合,即mSections就可能会发生变化,所以需要重新生成,本来准备复写notifyDataSetChanged方法,在里面重新生成,没想到这个方法是final的,于是利用了registerAdapterDataObserver(observer);,在数据发生变化回调中重新生成,记得在onDetachedFromRecyclerView里面对注册的observer进行解注册。

ok,到此我们的增加Header就结束了~~

恩,上面是针对普通的Item增加header的代码,如果是针对多种ItemViewType呢?其实也很简单,这种方式需要传入MultiItemTypeSupport。那么对于headerItemTypeSupport中的getItemViewType等方法,不是header类型时,交给传入的MultiItemTypeSupport即可,大致的代码如下:

headerItemTypeSupport = new MultiItemTypeSupport<T>()
{
    @Override
    public int getLayoutId(int itemType)
    {
        if (itemType == TYPE_SECTION)
            return mSectionSupport.sectionHeaderLayoutId();
        else
            return multiItemTypeSupport.getLayoutId(itemType);
    }

    @Override
    public int getItemViewType(int position, T o)
    {
        int positionVal = getIndexForPosition(position);
        return mSections.values().contains(position) ?
                TYPE_SECTION :
                multiItemTypeSupport.getItemViewType(positionVal, o);
    }
};
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

那么这样的话,今天的博客就结束了,有几点需要说明下:

本来是想接着以前的万能Adapter后面写,但是为了本文的独立和完整性,还是尽可能没有去依赖上篇博客的内容了。

此外,文章最后给出的开源代码与上述代码存在些许的差异,因为开源部分源码整合了ListView,RecyclerView等,而本文上述代码完全针对RecyclerView进行编写。

对于ItemClick,ItemLongClick的代码就不赘述了,其实都是通过itemView.setXXXListener完成,详细的参考代码即可。

一、概述

记得好久以前针对ListView类控件写过一篇打造万能的ListView GridView 适配器,如今RecyclerView异军突起,其Adapter的用法也与ListView类似,那么我们也可以一步一步的为其打造通用的Adapter,使下列用法书写更加简单:

  • 简单的数据绑定(单种Item)
  • 多种Item Type 数据绑定
  • 增加onItemClickListener , onItenLongClickListener
  • 优雅的添加分类header

二、使用方式和效果图

在一步一步完成前,我们先看下使用方式和效果图:

(1)简单的数据绑定

首先看我们最常用的单种Item的书写方式:

mRecyclerView.setAdapter(new CommonAdapter<String>(this, R.layout.item_list, mDatas)
{
    @Override
    public void convert(ViewHolder holder, String s)
    {
        holder.setText(R.id.id_item_list_title, s);
    }
});
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

是不是相当方便,在convert方法中完成数据、事件绑定即可。

(2)多种ItemViewType

多种ItemViewType,正常考虑下,我们需要根据Item指定ItemType,并且根据ItemType指定相应的布局文件。我们通过MultiItemTypeSupport完成指定:

MultiItemTypeSupport  multiItemSupport = new MultiItemTypeSupport<ChatMessage>()
{
    @Override
    public int getLayoutId(int itemType)
    {
       //根据itemType返回item布局文件id
    }

    @Override
    public int getItemViewType(int postion, ChatMessage msg)
    {
       //根据当前的bean返回item type
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

剩下就简单了,将其作为参数传入到MultiItemCommonAdapter即可。

mRecyclerView.setAdapter(new SectionAdapter<String>(this, mDatas, multiItemSupport)
{
    @Override
    public void convert(ViewHolder holder, String s)
    {
        holder.setText(R.id.id_item_list_title, s);
    }
});
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

贴个效果图:

(3)添加分类header

其实属于多种ItemViewType的一种了,只是比较常用,我们就简单封装下。

依赖正常考虑下,这种方式需要额外指定header的布局,以及布局中显示标题的TextView了,以及根据Item显示什么样的标题。我们通过SectionSupport对象指定:

SectionSupport<String> sectionSupport = new SectionSupport<String>()
{
    @Override
    public int sectionHeaderLayoutId()
    {
        return R.layout.header;
    }

    @Override
    public int sectionTitleTextViewId()
    {
        return R.id.id_header_title;
    }

    @Override
    public String getTitle(String s)
    {
        return s.substring(0, 1);
    }
};
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

3个方法,一个指定header的布局文件,一个指定布局文件中显示title的TextView,最后一个用于指定显示什么样的标题(根据Adapter的Bean)。

接下来就很简单了:

mRecyclerView.setAdapter(new SectionAdapter<String>(this, R.layout.item_list, mDatas, sectionSupport)
{
    @Override
    public void convert(ViewHolder holder, String s)
    {
        holder.setText(R.id.id_item_list_title, s);
    }
});
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这样就完了,效果图如下:

ok,看完上面简单的介绍,相信你已经基本了解了,没错,和我上篇ListView万能Adapter的使用方式基本一样,并且已经封装到同一个库了,链接为:https://github.com/hongyangAndroid/base-adapter,此外还提供了ItemClick,ItemLongClick,添加EmptyView等支持。

说了这么多,下面进入正题,看我们如何一步步完成整个封装的过程。

三、通用的ViewHolder

RecyclerView要求必须使用ViewHolder模式,一般我们在使用过程中,都需要去建立一个新的ViewHolder然后作为泛型传入Adapter。那么想要建立通用的Adapter,必须有个通用的ViewHolder。

首先我们确定下ViewHolder的主要的作用,实际上是通过成员变量存储对应的convertView中需要操作的字View,避免每次findViewById,从而提升运行的效率。

那么既然是通用的View,那么对于不同的ItemType肯定没有办法确定创建哪些成员变量View,取而代之的只能是个集合来存储了。

那么代码如下:

public class ViewHolder extends RecyclerView.ViewHolder
{
    private SparseArray<View> mViews;
    private View mConvertView;
    private Context mContext;

    public ViewHolder(Context context, View itemView, ViewGroup parent)
    {
        super(itemView);
        mContext = context;
        mConvertView = itemView;
        mViews = new SparseArray<View>();
    }


    public static ViewHolder get(Context context, ViewGroup parent, int layoutId)
    {

        View itemView = LayoutInflater.from(context).inflate(layoutId, parent,
                false);
        ViewHolder holder = new ViewHolder(context, itemView, parent, position);
        return holder;
    }


    /**
     * 通过viewId获取控件
     *
     * @param viewId
     * @return
     */
    public <T extends View> T getView(int viewId)
    {
        View view = mViews.get(viewId);
        if (view == null)
        {
            view = mConvertView.findViewById(viewId);
            mViews.put(viewId, view);
        }
        return (T) view;
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

代码很简单,我们的ViewHolder继承自RecyclerView.ViewHolder,内部通过SparseArray来缓存我们itemView内部的子View,从而得到一个通用的ViewHolder。每次需要创建ViewHolder只需要传入我们的layoutId即可。

ok,有了通用的ViewHolder之后,我们的通用的Adapter分分钟就出来了。

四、通用的Adapter

我们的每次使用过程中,针对的数据类型Bean肯定是不同的,那么这里肯定要引入泛型代表我们的Bean,内部通过一个List代表我们的数据,ok,剩下的看代码:

package com.zhy.base.adapter.recyclerview;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.zhy.base.adapter.ViewHolder;

import java.util.List;

/**
 * Created by zhy on 16/4/9.
 */
public abstract class CommonAdapter<T> extends RecyclerView.Adapter<ViewHolder>
{
    protected Context mContext;
    protected int mLayoutId;
    protected List<T> mDatas;
    protected LayoutInflater mInflater;


    public CommonAdapter(Context context, int layoutId, List<T> datas)
    {
        mContext = context;
        mInflater = LayoutInflater.from(context);
        mLayoutId = layoutId;
        mDatas = datas;
    }

    @Override
    public ViewHolder onCreateViewHolder(final ViewGroup parent, int viewType)
    {
        ViewHolder viewHolder = ViewHolder.get(mContext, parent, mLayoutId);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position)
    {
        holder.updatePosition(position);
        convert(holder, mDatas.get(position));
    }

    public abstract void convert(ViewHolder holder, T t);

    @Override
    public int getItemCount()
    {
        return mDatas.size();
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

继承自RecyclerView.Adapter,需要复写的方法还是比较少的。首先我们使用过程中传输我们的数据集mDatas,和我们item的布局文件layoutId。

onCreateViewHolder时,通过layoutId即可利用我们的通用的ViewHolder生成实例。

onBindViewHolder这里主要用于数据、事件绑定,我们这里直接抽象出去,让用户去操作。可以看到我们修改了下参数,用户可以拿到当前Item所需要的对象和viewHolder去操作。

那么现在用户的使用是这样的:

mRecyclerView.setAdapter(new CommonAdapter<String>(this, R.layout.item_list, mDatas)
{
    @Override
    public void convert(ViewHolder holder, String s)
    {
        TextView tv = holder.getView(R.id.id_item_list_title);
        tv.setText(s);
    }
});
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

看到这里,爽了很多,目前我们仅仅写了很少的代码,但是我们的通用的Adapter感觉已经初步完成了。

可以看到我们这里通过viewholder根据控件的id拿到控件,然后再进行数据绑定和事件操作,我们还能做些什么简化呢?

恩,我们可以通过一些辅助方法简化我们的代码,所以继续往下看。

五、进一步封装ViewHolder

我们的Item实际上使用的控件较多时候可能都是TextView,ImageView等,我们一般在convert方法都是去设置文本,图片什么的,那么我们可以在ViewHolder里面,写上如下的一些辅助方法:

class ViewHolder extends RecyclerView.Adapter<ViewHolder>
{
    //...
    public ViewHolder setText(int viewId, String text)
    {
        TextView tv = getView(viewId);
        tv.setText(text);
        return this;
    }

    public ViewHolder setImageResource(int viewId, int resId)
    {
        ImageView view = getView(viewId);
        view.setImageResource(resId);
        return this;
    }

    public ViewHolder setOnClickListener(int viewId,
                                         View.OnClickListener listener)
    {
        View view = getView(viewId);
        view.setOnClickListener(listener);
        return this;
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

当然上面只给出了几个方法,你可以把常用控件的方法都写进去,并且在使用过程中不断完善即可。

有了一堆辅助方法后,我们的操作更加简化了一步。

mRecyclerView.setAdapter(new CommonAdapter<String>(this, R.layout.item_list, mDatas)
{
    @Override
    public void convert(ViewHolder holder, String s)
    {
        //TextView tv = holder.getView(R.id.id_item_list_title);
        //tv.setText(s);
        holder.setText(R.id.id_item_list_title,s);
    }
});
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

ok,到这里,我们的针对单种ViewItemType的通用Adapter就完成了,代码很简单也很少,但是简化效果非常明显。

ok,接下来我们考虑多种ItemViewType的情况。

六、多种ItemViewType

多种ItemViewType,一般我们的写法是:

  • 复写getItemViewType,根据我们的bean去返回不同的类型
  • onCreateViewHolder中根据itemView去生成不同的ViewHolder

如果大家还记得,我们的ViewHolder是通用的,唯一依赖的就是个layoutId。那么上述第二条就变成,根据不同的itemView告诉我用哪个layoutId即可,生成viewholder这种事我们通用adapter来做。

于是,引入一个接口:

public interface MultiItemTypeSupport<T>
{
    int getLayoutId(int itemType);

    int getItemViewType(int position, T t);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

可以很清楚的看到,这个接口实际就是完成我们上述的两条工作。用户在使用过程中,通过实现上面两个方法,指明不同的Bean返回什么itemViewType,不同的itemView所对应的layoutId.

ok,有了上面这个接口,我们的参数就够了,下面开始我们的MultiItemCommonAdapter的编写。

public abstract class MultiItemCommonAdapter<T> extends CommonAdapter<T>
{
    protected MultiItemTypeSupport<T> mMultiItemTypeSupport;

    public MultiItemCommonAdapter(Context context, List<T> datas,
                                  MultiItemTypeSupport<T> multiItemTypeSupport)
    {
        super(context, -1, datas);
        mMultiItemTypeSupport = multiItemTypeSupport;
    }

    @Override
    public int getItemViewType(int position)
    {
        return mMultiItemTypeSupport.getItemViewType(position, mDatas.get(position));
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
    {
        int layoutId = mMultiItemTypeSupport.getLayoutId(viewType);
        ViewHolder holder = ViewHolder.get(mContext, parent, layoutId;
        return holder;
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

几乎没有几行代码,感觉简直不需要消耗脑细胞。getItemViewType用户的传入的MultiItemTypeSupport.getItemViewType完成,onCreateViewHolder中根据MultiItemTypeSupport.getLayoutId返回的layoutId,去生成ViewHolder即可。

ok,这样的话,我们的多种ItemViewType的支持也就完成了,一路下来感觉还是蛮轻松的~~~

最后,我们还有个添加分类的header,为什么想起来封装这个呢,这个是因为我看到了这么个项目:https://github.com/ragunathjawahar/simple-section-adapter,这个项目给了种类似装饰者模式的方法,为ListView添加了header,有兴趣可以看下。我想我们的RecylerView也来个吧,不过我们这里直接通过继承Adapter完成。

七、添加分类Header

话说添加分类header,其实就是我们多种ItemViewType的一种,那么我们需要知道哪些参数呢?

简单思考下,我们需要:

  1. header所对应的布局文件
  2. 显示header的title对应的TextView
  3. 显示的title是什么(一般肯定根据Bean生成)

ok,这样的话,我们依然引入一个接口,用于提供上述3各参数

public interface SectionSupport<T>
{
    public int sectionHeaderLayoutId();

    public int sectionTitleTextViewId();

    public String getTitle(T t);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

方法名应该很明确了,这里引入泛型,对应我们使用时的数据类型Bean。

刚才也说了我们的分类header是多种ItemViewType的一种,那么直接继承MultiItemCommonAdapter实现。

public abstract class SectionAdapter<T> extends MultiItemCommonAdapter<T>
{
    private SectionSupport mSectionSupport;
    private static final int TYPE_SECTION = 0;
    private LinkedHashMap<String, Integer> mSections;

    private MultiItemTypeSupport<T> headerItemTypeSupport = new MultiItemTypeSupport<T>()
    {
        @Override
        public int getLayoutId(int itemType)
        {
            if (itemType == TYPE_SECTION)
                return mSectionSupport.sectionHeaderLayoutId();
            else
                return mLayoutId;
        }
        @Override
        public int getItemViewType(int position, T o)
        {
             return mSections.values().contains(position) ?
                    TYPE_SECTION :
                    1;
        }
    };

    @Override
    public int getItemViewType(int position)
    {
        return mMultiItemTypeSupport.getItemViewType(position, null);
    }

    final RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver()
    {
        @Override
        public void onChanged()
        {
            super.onChanged();
            findSections();
        }
    };


    public SectionAdapter(Context context, int layoutId, List<T> datas, SectionSupport sectionSupport)
    {
        super(context, datas, null);
        mLayoutId = layoutId;
        mMultiItemTypeSupport = headerItemTypeSupport;
        mSectionSupport = sectionSupport;
        mSections = new LinkedHashMap<>();
        findSections();
        registerAdapterDataObserver(observer);
    }

    @Override
    protected boolean isEnabled(int viewType)
    {
        if (viewType == TYPE_SECTION)
            return false;
        return super.isEnabled(viewType);
    }

    @Override
    public void onDetachedFromRecyclerView(RecyclerView recyclerView)
    {
        super.onDetachedFromRecyclerView(recyclerView);
        unregisterAdapterDataObserver(observer);
    }

    public void findSections()
    {
        int n = mDatas.size();
        int nSections = 0;
        mSections.clear();

        for (int i = 0; i < n; i++)
        {
            String sectionName = mSectionSupport.getTitle(mDatas.get(i));

            if (!mSections.containsKey(sectionName))
            {
                mSections.put(sectionName, i + nSections);
                nSections++;
            }
        }

    }


    @Override
    public int getItemCount()
    {
        return super.getItemCount() + mSections.size();
    }

    public int getIndexForPosition(int position)
    {
        int nSections = 0;

        Set<Map.Entry<String, Integer>> entrySet = mSections.entrySet();
        for (Map.Entry<String, Integer> entry : entrySet)
        {
            if (entry.getValue() < position)
            {
                nSections++;
            }
        }
        return position - nSections;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position)
    {
        position = getIndexForPosition(position);
        if (holder.getItemViewType() == TYPE_SECTION)
        {
            holder.setText(mSectionSupport.sectionTitleTextViewId(), mSectionSupport.getTitle(mDatas.get(position)));
            return;
        }
        super.onBindViewHolder(holder, position);
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121

根据我们之前的代码,使用MultiItemCommonAdapter,需要提供一个MultiItemTypeSupport,我们这里当然也不例外。可以看到上述代码,我们初始化了成员变量headerItemTypeSupport,分别对getLayoutIdgetItemViewType进行了实现。

  • getLayoutId如果type是header类型,则返回mSectionSupport.sectionHeaderLayoutId();否则则返回mLayout.
  • getItemViewType根据位置判断,如果当前是header所在位置,返回header类型常量;否则返回1.

ok,可以看到我们构造方法中调用了findSections(),主要为了存储我们的title和对应的position,通过一个MapmSections来存储。

那么对应的getItemCount()方法,我们多了几个title肯定总数会增加,所以需要复写。

onBindViewHolder中我们有一行重置position的代码,因为我们的position变大了,所以在实际上绑定我们数据时,这个position需要还原,代码逻辑见getIndexForPosition(position)

最后一点就是,每当我们的数据发生变化,我们的title集合,即mSections就可能会发生变化,所以需要重新生成,本来准备复写notifyDataSetChanged方法,在里面重新生成,没想到这个方法是final的,于是利用了registerAdapterDataObserver(observer);,在数据发生变化回调中重新生成,记得在onDetachedFromRecyclerView里面对注册的observer进行解注册。

ok,到此我们的增加Header就结束了~~

恩,上面是针对普通的Item增加header的代码,如果是针对多种ItemViewType呢?其实也很简单,这种方式需要传入MultiItemTypeSupport。那么对于headerItemTypeSupport中的getItemViewType等方法,不是header类型时,交给传入的MultiItemTypeSupport即可,大致的代码如下:

headerItemTypeSupport = new MultiItemTypeSupport<T>()
{
    @Override
    public int getLayoutId(int itemType)
    {
        if (itemType == TYPE_SECTION)
            return mSectionSupport.sectionHeaderLayoutId();
        else
            return multiItemTypeSupport.getLayoutId(itemType);
    }

    @Override
    public int getItemViewType(int position, T o)
    {
        int positionVal = getIndexForPosition(position);
        return mSections.values().contains(position) ?
                TYPE_SECTION :
                multiItemTypeSupport.getItemViewType(positionVal, o);
    }
};
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

那么这样的话,今天的博客就结束了,有几点需要说明下:

本来是想接着以前的万能Adapter后面写,但是为了本文的独立和完整性,还是尽可能没有去依赖上篇博客的内容了。

此外,文章最后给出的开源代码与上述代码存在些许的差异,因为开源部分源码整合了ListView,RecyclerView等,而本文上述代码完全针对RecyclerView进行编写。

对于ItemClick,ItemLongClick的代码就不赘述了,其实都是通过itemView.setXXXListener完成,详细的参考代码即可。

通过封装BaseAdapterRecyclerView.Adapter得到的通用的,简易的Adapter。项目地址:https://github.com/tianzhijiexian/CommonAdapter 效果图:已解决的问题 提升item的独立性,完美支持item被多处复用 item会根据type来做自动复用 支持多种类型的item 一个item仅会调用一次setViews(),避免重复建立监听器 一个item仅会触发一次绑定视图的操作,提示效率 支持dataBinding和其他第三方注入框架 提供了getView()方法来简化findViewById 支持通过item的构造方法来传入Activity对象 支持通过item的构造方法来传入item中事件的回调 提供了getConvertedData(data, type)方法来对item传入的数据做转换,方便拆包和提升item的复用性 支持viewpager的正常加载模式和懒加载 支持快速将listview的适配器切换为recyclerView的适配器 viewpager的notifyDataSetChanged可以正常更新界面 支持recyclerView的添加头部和底部 支持适配器的数据自动绑定,只用操作数据便可,adapter会自动notify界面零、重要接口adapter的item必须实现此接口,接口源码如下:public interface AdapterItem<T> {     /**      * @return item布局文件的layoutId      */     @LayoutRes     int getLayoutResId();     /**      * 初始化views      */     void bindViews(final View root);     /**      * 设置view的参数      */     void setViews();     /**      * 根据数据来设置item的内部views      *      * @param model    数据list内部的model      * @param position 当前adapter调用item的位置      */     void handleData(T model, int position); }例子:public class TextItem implements AdapterItem<DemoModel> {     @Override     public int getLayoutResId() {         return R.layout.demo_item_text;     }     TextView textView;     @Override     public void bindViews(View root) {         textView = (TextView) root.findViewById(R.id.textView);     }     @Override     public void setViews() { }     @Override     public void handleData(DemoModel model, int position) {         textView.setText(model.content);     } }一、ListView GridView通用适配器——CommonAdapter只需继承CommonAdapter便可实现适配器:listView.setAdapter(new CommonAdapter<DemoModel>(data, 1) {     public AdapterItem<DemoModel> createItem(Object type) {         return new TextItem();     } });二、RecyclerView通用适配器——CommonRcvAdapter通过继承CommonRcvAdapter来实现适配器:mAdapter = new CommonRcvAdapter<DemoModel>(data) {  public AdapterItem createItem(Object type) {         return new TextItem();   } };三、ViewPager的通用适配器——CommonPagerAdapter通过继承CommonPagerAdapter来实现适配器:viewPager.setAdapter(new CommonPagerAdapter<DemoModel>() {     public AdapterItem createItem(Object type) {         return new TextItem();     } });设计思路1. Adapter如果用adapter常规写法,你会发现代码量很大,可读性低。如果adapter中有多个类型的Item,我们还得在getView()中写很多if-else语句,很乱。 而现在我让adapter的代码量减少到一个8行的内部类,如果你需要更换item只需要动一行代码,真正实现了可插拔化。最关键的是item现在作为了一个独立的对象,可以方便的进行复用。2. AdapterItem和原来方式最为不同的一点就是我把adapter的item作为了一个实体,这种方式借鉴了RecyclerView中ViewHolder的设计。把item作为实体的好处有很多,比如复用啊,封装啊,其余的就不细说了。3. 分层在使用过程中,我发现如果adapter放在view层,那就会影响到view层的独立性。此外adapter中经常有很多数据处理的操作,比如通过type选择item,数据的拆包、转换等操作。于是我还是推荐把adapter放在mvp的p层,或者是mvvm的m层。通过在实际的项目中使用来看,放在m或p层的效果较好,view的复用也比较好做。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值