HeaderRecycleAdapter--通用的带头部RecycleView.Adapter

来源:互联网 发布:魔方虚拟光驱软件 编辑:程序博客网 时间:2024/06/15 12:34

目录

概述

HeaderRecycleViewAdapter这是一个通用的RecycleView.Adapter,可以不需要继承即可简单的使用.此Adapter实现了带头部的处理显示,不需要使用头部显示时可以使用其简单版本SimpleRecycleViewAdapter.

  • 适用范围
    任何类型的数据
    支持LinearLayoutManagerGridLayoutManager
    支持竖直方向及横向方向
    暂时不支持reverseLayout,即反方向布局

  • 其它
    对于文中提到一些不太能马上理解的点,后面基本都会有更详细地说明.


关于头部的实现

recycleView的视图view是自己维护的,adapter仅仅只是提供了数据和做数据绑定的一个操作.因此header的实现也必须通过这种形式进行.
使用过RecycleView.Adapter的应该都知道继承Adapter需要实现两个抽象方法,实际可以实现的方法还有很多,有一些也是必须的.

//获取adapter中的item数量public int getItemCount();//根据位置获取item的视图类型public int getItemViewType(int position);

通过方法getItemViewType(int position)我们就可以实现在正常的数据中添加item的头部了.根据位置确定需要显示为头部的位置,然后返回头部的viewType类型,再通过“加载出头部的视图即可.


关于具体的实现方式

固定头部的实现参考了一部分为RecyclerView打造通用Adapter,链接中也提到如何去为RecycleView添加头部,但是个人觉得那咱添加方式有点过于难理解,如果说到通用肯定是使用方式更简单,操作更容易会更加方便合理.
所以针对这个对实现header的方式进行了修改,不再需要自己实现某些处理的接口,完全可以小白方式的操作使用.需要做的仅仅是要自己绑定所有的itemheader显示的数据.

header的实现方式

  • 数据源形式为List<List>的形式,将需要显示不同的header的分组以不同的列表分隔开,每一个列表即对应了一个header
  • header数据通过Map<>传入Adapter,通过分组列表的索引自动匹配对应的header数据,再回调绑定header.
//数据源如下,若数据源中存在两个列表List<List<String>> groupList;//header数据创建//key必须是0和1,因为列表只有两个,索引必定是0和1//可以不放入对应key的数据或者放入其它key的数据,但是相应的该部分的header绑定数据将返回nullMap<Integer,String> headerMap;headerMap.put(0,"header1");headerMap.put(1,"header2");

由于header是动态添加上去的,所以并不需要提前将header的数据添加到数据源的列表中,这样可以很好地分离数据源和header之间的关联,当需要从adapter中获取数据源进行处理数据时,也不会受到添加的header的影响.


动态添加header的问题

headerAdapter通过计算添加入的List<List>分组数据源列表自动添加到RecycleView中显示出来的,所以存在的问题是,原数据源的位置position将会被改变,由于header的存在.这是个不可避免的一个问题.
由于position往往是被用在处理该项或都获取数据源中某项数据来使用,所以解决方案从这两个使用方面入手.

  • 在某项itemView的单击返回接口中,将完整地提供position/groudId/childId(分别对应位置,分组索引与分组的列表内的索引)
 /** * 带header的item单击事件 * * @param groupId  当前item所在分组,分组ID从0开始 * @param childId  当前item在所有分组中的ID,从0开始,当此值为-1时,当前为该分组的头部 * @param position 当前item所有分组的位置(header也会占用一个位置,请注意) * @param viewId   当前响应的单击事件view的ID,若为rootView,则该值为{@link #ROOT_VIEW_ID} * @param isHeader 当前item是否为header * @param rootView 当前item的rootView * @param holder */public void onItemClick(int groupId, int childId, int position, int viewId, boolean isHeader, View rootView, HeaderRecycleViewHolder holder);
  • Adapter提供了相应的与位置及实际数据源相关的方法
//根据位置获取非header位置的item数据public T getItem(int position);//根据数据源的分组索引及分组内列表的索引,获取非header位置的item数据,若该位置为header,返回nullpublic T getItem(int groupId, int childId)//根据position计算分组的索引及分组内列表的数据项索引public Point getGroupIdAndChildIdFromPosition(List<Integer> eachGroupCountList, int position, boolean isShowGroup);//根据分组数据列表获取分组数据量的列表public List<Integer> getEachGroupCountList(List<List<T>> groupList);

header的添加与计算方式

header的添加中需要处理的东西并不多,但可能会相对复杂一些,有几个很重要的点是必须解决的.

  • Adapter的数据项总数
  • 计算位置对应的数据源的数据项索引
  • 判断某个位置是否为header

对于以上两个需求其实都依赖于数据源的数据量进行处理.首先需要处理数据源,得到数据源的每个分组列表中的数据量.

//若数据源为 List<List<String>> groupList;//计算数据源的分组列表中数据量并缓存起来List<Integer> eachGroupCountList = new ArrayList<Integer>(groupList.size());for (List group : groupList) {    eachGroupCountList.add(group == null ? 0 : group.size());}

得到数据源列表数据量之后,Adapter的数据项总数就很容易计算出来了.并将计算的数据项总数记录下来,作为getItemCount()的返回数据,这样就不需要每一次getItemCount()的时候都要重新计算一次了.

if (eachGroupCountList != null) {    for (int groupCount : eachGroupCountList) {        //若显示Header将Header添加到总数据量中,每一个Header占用一行        mCount += 1 + groupEachLine;    }}

最后是最重要的一部分了,通过某个位置计算数据源中数据项的索引.由于数据源是使用List<List>二维的数据形式,所以一个数据项的索引必须有两个值(使用Point类来表示),其中point.x表示哪一个分组,point.y表示分组内列表的索引.

if (eachGroupCountList != null) {    for (int groupCount : eachGroupCountList) {        //获取分组的数据量        childId = groupCount;        //将当前position计算与分组数据量进行计算        //1表示header        position = position - 1 - childId;        //当position小于0时,说明当前的位置在该分组列表中        if (position < 0) {            //回滚到该列表中的数据项索引            //此时groupId即为分组的索引            //childId即为分组内列表的索引            childId += position;            break;        }        //每计算完一组分组ID添加1        groupId++;    }    //new Point(groupId,childId)即为当前位置的数据项在数据源中的索引}

以上为如何通过位置计算出数据源的方式;得到的结果已经包含了两种情况:

  • 数据源数据项:groupList.get(point.x).get(point.y)
  • header:若为header,则该索引值的groupId是指当前header所在的分组,而childId则为-1.

上面的计算中需要注意的是:position本身是正数,通过不断地按顺序排除分组而到所在的分组索引时,计算后得到的childId就是分组内列表的索引.(建议举个实际的例子和数据代入计算一下就非常清晰了)

总之,计算header的结果所得的Point,groupId为分组的索引,而childId<0时表示当前位置为header,否则childId为分组内列表的索引


关于数据相关的绑定接口

HeaderRecycleAdapter本质还是扩展自RecycleView.Adapter,所以必然是需要实现关于item界面的加载及数据绑定的功能.正常的情况下是通过继承实现此功能,但是在这里通过接口的方式.使用这种处理方式有两个好处.

  • 将界面与数据绑定的功能独立出来
    HeaderRecycleAdapter仅仅是处理显示界面,并不处理数据的绑定功能,会更加清晰地分离逻辑与界面.

  • HeaderRecycleAdapter更加专注并且独立
    不需要任何继承或者修改就可以直接进行使用,对于不同的item数据,仅需要实现IHeaderAdapterOption接口即可处理不同的界面.
    切换显示不同的内容时甚至不需要重新创建一个Adapter,只需要把HeaderRecycleAdapter中的IHeaderAdapterOption替换即可.

/** * 带头部的adapter配置接口 * 其中参数T为每个item对应的设置的数据类型 * 参数H为每个header对应设置的数据类型 */public interface IHeaderAdapterOption<T, H> {    /**     * 不存在headerView类型,{@value Integer#MIN_VALUE}     */    public static final int NO_HEADER_TYPE = Integer.MIN_VALUE;    /**     * 获取headerView的类型,headerView类型专用     *     * @param groupId 分组类型     * @return     */    public int getHeaderViewType(int groupId, int position);    /**     * 获取View的类型     *     * @param position     位置     * @param groupId     * @param childId     * @param isHeaderItem     * @param isShowHeader 是否显示header       * @return     */    public int getItemViewType(int position, int groupId, int childId, boolean isHeaderItem, boolean isShowHeader);    /**     * 根据ViewType获取加载的当前项layoutID     *     * @param viewType     * @return     */    public int getLayoutId(int viewType);    /**     * 设置Header显示绑定数据     *     * @param groupId 当前组ID     * @param header  当前Header数据,来自于Map     * @param holder     */    public void setHeaderHolder(int groupId, H header, HeaderRecycleViewHolder holder);    /**     * 设置子项ViewHolder     *     * @param groupId  分组ID     * @param childId  当前组子项ID     * @param position 当前项位置(此位置为recycleView中的位置)     * @param itemData     * @param holder     */    public void setViewHolder(int groupId, int childId, int position, T itemData, HeaderRecycleViewHolder holder);}

查看以上绑定数据的接口IHeaderAdapterOption可见,这里绑定数据还分为两个不同的内容,一个是header的数据绑定,一个是普通的子view数据绑定,在绑定数据时也会更加清晰,降低出错的可能.


关于内置的OnItemClickListener接口

关于item的单击监听事件,此处为内置的一个接口,返回的item信息更加具体;但是对一般的RecycleView添加item监听事件时,可以参考下面的博客:RecyclerView无法添加onItemClickListener最佳的高效解决方案,需要注意的是该方案暂时只能提供对整个item的单击监听事件

由于这是一个列表展示,往往是需要处理某一项单击处理的事件,所以HeaderReycleViewHolder提供了内置的item处理接口.
接口中提供了position/groupId/childId/viewId/isHeader/viewHolder(分别对应被单击item的位置/分组索引/分组内列表的索引/被单击view的ID/是否为header/viewHolder).

/** * 带header的item单击事件 * * @param groupId  当前item所在分组,分组ID从0开始 * @param childId  当前item在所有分组中的ID,从0开始,当此值为-1时,当前为该分组的头部 * @param position 当前item所有分组的位置(header也会占用一个位置,请注意) * @param viewId   当前响应的单击事件view的ID,若为rootView,则该值为{@link #ROOT_VIEW_ID} * @param isHeader 当前item是否为header * @param rootView 当前item的rootView * @param holder */public void onItemClick(int groupId, int childId, int position, int viewId, boolean isHeader, View rootView, HeaderRecycleViewHolder holder);
  • 处理header和非header数据项分别可以通以下方式:
if(isHeader){    //当前单击位置为header    //获取当前位置的header: holder.getAdapter().getHeader(groupId);}else{    //当前单击位置为某个数据项    //获取当前位置的数据项: holder.getAdapter().getItem(groupId,childId);}
  • 通过HeaderRecycleViewHolder注册监听方法
    由于item中可能存在很多不同的控件,所以HeaderRecycleViewHolder中也提供了根据view的ID对指定view单击事件进行注册监听的功能.
//给指定ID的某个view注册单击监听事件public boolean registerViewOnClickListener(int viewId, OnItemClickListener listener) {    if (mItemClickMap == null) {        mItemClickMap = new ArrayMap<Integer, OnItemClickListener>(10);    }    View view = this.getView(viewId);    if (view != null) {        //若该view有效,则缓存起其注册的监听事件        mItemClickMap.put(viewId, listener);        view.setOnClickListener(this);        return true;    } else {        return false;    }}

从以上代码可以看出,注册的监听事件是被缓存起来的,当view被单击触发事件时若该view的ID与缓存的监听事件匹配则会回调事件.
这里也提供了对整个item(rootView)的注册监听.当rootView被注册了监听事件时,该item的任何控件被触发单击事件时,都只会触发rootView的监听事件.
rootView的监听事件不会清除其它控件的监听事件,但会优先于所有控件的监听事件,并且当rootView监听事件存在时,其它监听事件将被忽略.(当移除rootView的监听事件时,其它监听事件可以正常响应)

//注册rootView的单击监听事件public void registerRootViewItemClickListener(OnItemClickListener listener) {    mRootViewClickListener = listener;    mRootView.setOnClickListener(this);}

GridLayoutManager添加header

以上对头部的添加处理都是针对LinearLayoutManager类型的RecycleView,除了线性布局之外,其实网格布局使用率也是不低的,所以要实现GridLayoutManager添加头部.
由于GridLayoutManager是将多个item置于同一行显示的,所以当需要添加头部时,就必须确定如何将多行显示为1行.
通过查阅资料可以知道,GridLayoutManager可以设置一个SpanSizeLookup的类,该类只有一个抽象方法getSpanSize(int position),该方法决定了当前GridLayoutManager需要显示的某个位置的item占用了多少个网格的空间.
当我们确定GridLayoutManager每行的显示的网格数时,即可实现该类的抽象方法,返回header需要占用的网格数.


HeaderSpanSizeLookup

由以上分析我们需要自己实现该抽象方法以正确返回header占用的网格数.由于SpanSizeLookup是通过item的位置确定该位置需要返回显示的网格数,所以我们需要知道当前position位置的item是否为一个header.
考虑到自定义SpanSizeLookup的扩展性和尽可能与其它类解耦,原本是将HeaderRecycleAdapter作为内部成员参数保留在lookup中从而进行检测当前位置的item是否为header.但后来修改为使用一个接口来实现这些操作.这样在既使不是使用HeaderRecycleAdapter的情况下也可以实现对应的接口从而活用此类.

  • 以下为接口ISpanSizeHandler的方法
//是否特殊的item,如header或者某些特别的itempublic boolean isSpecialItem(int position);//获取特殊item占用的网格数,此方法仅会在是特殊item时才调用public int getSpecialItemSpanSize(int spanCount, int position);//获取正常item占用的网格数,默认值理论上应该为1,但可以由实现类决定,此方法仅会在非特殊item时调用public int getNormalItemSpanSize(int spanCount,int position);

从以上方法可以得出其实这个接口和HeaderSpanSizeLookup并不局限于对headeritem的检测,只要是特殊的item需要改变在GridLayoutManager中占用的网格数都可以实现此接口和通过HeaderSpanSizeLookup对界面显示进行布局的.

HeaderRecycleAdapter默认实现了此接口,所以可以直接使用到HeaderSpanSizeLookup


HeaderGridLayoutManager

当需要使用GridLayoutManager显示header时,只需要为GridLayoutManager通过setSpanSizeLookup(SpanSizeLookup)设置HeaderSpanSizeLookup即可.

//假设已经存在headerAdapter(HeaderRecycleAdapter)//rv(RecyclerView)//创建一个普通的GridLayoutManagerGridLayoutManager layoutManager=new GridLayoutManager(this,3);//创建一个基于GridLayoutManager的HeaderSpanSizeLookup//其中HeaderRecycleAdapter已经实现了ISpanSizeHandlerHeaderSpanSizeLookup lookup=new HeaderSpanSizeLookup(layoutManager.getSpanCount(),headerAdapter);//将lookup设置为GridLayoutManager使用的SpanSizeLookuplayoutManager.setSpanSizeLookup(lookup);rv.setLayoutManager(layoutManager);

以上为直接使用HeaderSpanSizeLookup结合GridLayoutManager实现带header的GridView.
但是有时可能会在运行中改动显示的GridLayoutManager展示的每行网格数(spanCount),在这种情况下,如果不去修改HeaderSpanSizeLookup中对应的setSpanCount(int),则会导致最终显示出来的header是不正常的.
所以增加了一个自定义的GridLayoutManager,用于处理HeaderSpanSizeLookup与GridLayoutManager关联的数据,在需要对GridLayoutManager进行任何调整时,直接对其进行修改就可以将修正的数据反应到HeaderSpanSizeLookup中.

//假设已经存在headerAdapter(HeaderRecycleAdapter)//rv(RecyclerView)//直接创建一个专用于header的GridLayoutManagerHeaderGridLayoutManager layoutManager=new HeaderGridLayoutManager(this,3,headerAdapter);//对于需要修改GridLayoutManager的配置时可以直接设置//如:layoutManager.setSpanCount(5)相关的对象都会自动更新//需要注意的是setSpanSizeLookup()时只能设置为HeaderSpanSizeLookup的类型rv.setLayoutManager(layoutManager);

对于HeaderGridLayoutManager来说修改的内容并不多,主要是重写了setSpanSizeLookup()setSpanCount()两个方法.

  • 覆盖setSpanSizeLookup(SpanSizeLookup)
//参数必须是 HeaderSpanSizeLookup 的类型,因为这是专用于处理不同item显示不同的网格空间的GridLayoutManagerif (spanSizeLookup instanceof HeaderSpanSizeLookup) {    mLookup = (HeaderSpanSizeLookup) spanSizeLookup;    super.setSpanSizeLookup(spanSizeLookup);} else {    throw new IllegalArgumentException("spanSizeLookup must be HeaderSpanSizeLookup");}
  • 覆盖setSpanCount(int)
//在通过GridLayoutManager更新spanCount时,也一并更新HeaderSpanSizeLookup中的spanCount,确保设置同步以正确显示界面布局super.setSpanCount(spanCount);if (mLookup != null) {    mLookup.setSpanCount(spanCount);}

附加功能–是否显示header

对于HeaderRecycleAdapter主要是为了解决显示header的item,附加的功能也是跟header相关的.有时候在某些情况下可能不需要暂时性不需要显示header(虽然暂时还想不出有什么情况…),所以预留了这个功能.

//设置当前是否显示headerpublic void setIsShowHeader(boolean isShowHeader) {    //当isShowHeader的设置不一样时,进行显示的调整    if (mIsShowHeader != isShowHeader) {        //更新当前的item数量(少了header位置信息会变动)        updateCount(mEachGroupCountList, isShowHeader);        mIsShowHeader = isShowHeader;    }}

以上的是对是否显示header进行设置,设置中并没有设置完毕后直接notifyDataChanged,所以当设置后数据没有改变时,需要手动调用adapter进行通知RecycleView更新数据.


使用示例

HeaderRecycleAdapter使用起来是比较简单的,只需要提供数据源,头部数据,还有自己实现数据绑定IHeaderAdapterOption接口即可.

  • 创建数据源及头部数据
mGroupList = new LinkedList<List<String>>();mHeaderMap = new ArrayMap<Integer, String>();int groupId = 0;int count = 0;count = groupId + 10;//数据源使用 List<List> 的数据结构for (; groupId < count; groupId++) {    int childCount = 8;    List<String> childList = new ArrayList<String>(childCount);    for (int j = 0; j < childCount; j++) {        childList.add("child - " + j);    }    mGroupList.add(childList);    //头部数据使用与分组索引对应的 Map<Integer,xxx>    mHeaderMap.put(groupId, "title - " + groupId);}
  • 实现IHeaderAdapterOption接口,自定义数据的绑定显示
//创建数据绑定option时可以设置泛型数据的类型,第一个为子item的数据类型,第二个为header的数据类型private class HeaderAdapterOption implements HeaderRecycleAdapter.IHeaderAdapterOption<String, String> {    //获取头部layout的类型    @Override    public int getHeaderViewType(int groupId, int position) {       return -1;    }    //获取子item layout的类型    @Override    public int getItemViewType(int position, int groupId, int childId, boolean isHeaderItem, boolean isShowHeader) {        if (isHeaderItem) {            return getHeaderViewType(groupId, position);        } else {            return 0;        }    }    //根据view的类型返回对应的layoutId,用于adapter加载界面    @Override    public int getLayoutId(int viewType) {        switch (viewType) {            case 0:            case NO_HEADER_TYPE:                return R.layout.item_content_2;            case -1:                return R.layout.item_header;            default:                return R.layout.item_content;        }    }    //设置头部数据绑定    @Override    public void setHeaderHolder(int groupId, String header, HeaderRecycleViewHolder holder) {        //注册rootView的监听事件        holder.registerRootViewItemClickListener(MainActivity.this);        //获取holder的缓存的子view进行数据绑定        TextView tv_header = holder.getView(R.id.tv_header);        if (tv_header != null) {            tv_header.setText(header.toString());        }    }    //子item数据绑定,类似头部数据绑定    //参数提供了完整地子item的分组索引,分组内列表的索引,当前item所在的位置    @Override    public void setViewHolder(int groupId, int childId, int position, String itemData, HeaderRecycleViewHolder holder) {        holder.registerRootViewItemClickListener(MainActivity.this);        TextView tv_content = holder.getView(R.id.tv_content);        tv_content.setText(itemData.toString());    }}
  • 创建HeaderRecycleAdapter并绑定到RecycleView
//创建 adapter//参数要求包括 context,option,数据源及头部数据HeaderRecycleAdapter adapter=new HeaderRecycleAdapter<String, String>(this, new HeaderAdapterOption(), mGroupList, mHeaderMap);//绑定到recycleViewrv.setAdapter(adapter);

简单版的SimpleRecycleAdapter

SimpleRecycleAdapter是不带headerAdapter,其实跟普通的创建一个adapter的结果是没有什么区别的.这里只是为了某些时间方便使用所以添加的一个简单版不带header的adapter.
同时相对于一般的adapter,SimpleRecycleAdapter继承自HeaderRecycleAdapter,所以保存了其大部分的特点.包括方便地注册监听事件,所以某些情况下使用简单版也可以节省很多不必要的时间和代码编写.
SimpleRecycleAdapter的使用方式与HeaderRecycleAdapter是一致的,创建数据源及数据绑定option即可.通过以上的示例我们知道只有分组的情况下会显示Header,所以SimpleRecycleAdapter内部其实只是覆盖和修改了部分设置.

  • 修改数据源为一维数据List<>
//本质还是创建了一个List<List>的数据源,只是这里作了另一层转换,调用者可以不需要自己创建二维数据源,直接使用一维数据源即可//一维数据源永远只放在第一项.并且不存在其它的一维数据源(这样就不会有不同的分组了)public void setItemList(List<T> itemList) {    List<List<T>> groupList = this.getGroupList();    if (groupList == null) {        groupList = new LinkedList<List<T>>();    }    groupList.clear();    groupList.add(itemList);    this.setGroupList(groupList);    mItemList = itemList;}
  • 修改头部显示功能
//覆盖头部显示的功能,永远不启用显示头部的功能public void setIsShowHeader(boolean isShowHeader) {       super.setIsShowHeader(false);   }
  • 简化了IHeaderAdapterOption
    IHeaderAdatperOption接口是针对带header的数据绑定,当然不显示header时,仅会调用到子item的数据绑定部分的代码.
    对此继承自该接口实现了一个简化版的SimpleAdapterOption
    由于简单版adapter不需要显示header,因此位置position也是正常的数据位置,不再受header影响.所以直接与普通的adapter一样根据位置返回item类型并绑定数据即可.
    不需要显示header的时候,使用此SimpleAdapterOption更加简单方便,不建议直接实现IHeaderAdaperOption接口,可能有部分方法会造成混乱
//继承自 IHeaderAdapterOption 接口的简单Adapter配置抽象类public static abstract class SimpleAdapterOption<T> implements IHeaderAdapterOption<T, Object> {    @Override    public int getItemViewType(int position, int groupId, int childId, boolean isHeaderItem, boolean isShowHeader) {        return getViewType(position);    }    @Override    public int getHeaderViewType(int groupId, int position) {        return NO_HEADER_TYPE;    }    @Override    public void setHeaderHolder(int groupId, Object header, HeaderRecycleViewHolder holder) {        //简单Adapter不处理Header,所以此方法不需要使用到,空实现    }    @Override    public void setViewHolder(int groupId, int childId, int position, T itemData, HeaderRecycleViewHolder holder) {        setViewHolder(itemData, position, holder);    }    //获取ViewType    public abstract int getViewType(int position);    //设置view数据绑定    public abstract void setViewHolder(T itemData, int position, HeaderRecycleViewHolder holder);}

事实上,完全可以不使用SimpleRecycleAdapter创建不带头部的adapter,直接用HeaderRecycleAdapter并设置其setIsShowHeader(boolean)为false即可,简单版的实现原理其实也是基于这个.


小结

通过以上说明的原理,可以在不需要自己计算header的位置,只需要将需要分组的数据源正确地分为不同的列表并创建对应的List<List>的分组数据源HedaerRecycleAdapter即可自动计算并显示出header.
相比需要自己通过重写Adapter的方式去处理需要返回的header的位置,会更加容易和方便.当然关于数据的绑定方面还是需要自己完成了.

示例图片

示例图片

GitHub地址

https://github.com/CrazyTaro/RecycleViewAdatper

资源下载

不想下载github项目的,或者不使用AS只需要类文件的,可以到以下下载地址直接下载类文件:
http://download.csdn.net/detail/u011374875/9556686

回到目录

0 0
原创粉丝点击