本文共 33763 字,大约阅读时间需要 112 分钟。
很久没有发表这个系列的文章了 。由于最近工作确实有点忙碌,也在脚踏实地的花时间研究android方面自己很多不懂的东西。但是写博客确实是一个坚持不懈和自我提高的过程,也希望在保持文章更新的同时能够保持文章的质量 。之前翻译了一些文章,有兴趣的小伙伴可以去看一下。今天这篇文章来谈一谈RecyclerView的封装,对RecyclerView的一些使用点进行总结,以及如何将RecyclerView的adapter进一步简化。平时开发使用的RecyclerView Adapter是来自鸿洋大神的以及对应的github项目.但是有个问题是他这篇文章写的时间比较早,项目一直在维护,所以本篇文章也算是对整个项目的思路的再梳理。
刚好解决了昨天在鸿洋博客下看到的这个小问题。哈哈。希望对大家有帮助。首先看看我的项目结构,项目分为common 和module模块,这里对之前整个项目的框架进行了改造,没有了之前的library,取而代之的是将所有公用组件放在了common包中,这是每个项目都可以copy使用的部分。在module包中就是具体每个项目的每个模块。比如这个示例项目中,包含
recyclerView组件作为每个项目中都可以使用的组件,这里放在common-widgets-recyclerview包下。
这里可以看到的recyclerView组件这里添加了adapter,base,divider,section,utils,wrapper包。下面来进行深入的讲解以及怎样在项目开发中进行使用吧。
RecyclerView is a more advanced and flexible version of ListView. This widget is a container for large sets of views that can be recycled and scrolled very efficiently. Use the RecyclerView widget when you have lists with elements that change dynamically.
RecyclerView 是Android L版本中新添加的一个用来取代ListView的SDK,它的灵活性与可替代性比listview更好。RecyclerView与ListView原理是类似的:都是仅仅维护少量的View并且可以展示大量的数据集。在RecyclerView标准化了ViewHolder类似于ListView中convertView用来做视图缓存。
请直接参考
ViewHolder是google在优化ListView性能的技巧上就提到的,虽然google并没有强制使用,但事实上它已经成为ListView的编写规范。在RecyclerView上,ViewHolder的使用成为了一种强制手段了。接下来对封装的ViewHolder进行分析:
首先来看看ViewHolder的用法,这是一个简单的获取String数组并展现到TextView上。通过数组的大小来创建item的数量。public class MyAdapter extends RecyclerView.Adapter{ public String[] datas = null; public MyAdapter(String[] datas) { this.datas = datas; } //创建新View,被LayoutManager所调用 @Override public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item,viewGroup,false); ViewHolder vh = new ViewHolder(view); return vh; } //将数据与界面进行绑定的操作 @Override public void onBindViewHolder(ViewHolder viewHolder, int position) { viewHolder.mTextView.setText(datas[position]); } //获取数据的数量 @Override public int getItemCount() { return datas.length; } //自定义的ViewHolder,持有每个Item的的所有界面元素 public static class ViewHolder extends RecyclerView.ViewHolder { public TextView mTextView; public ViewHolder(View view){ super(view); mTextView = (TextView) view.findViewById(R.id.text); } }}
当然这里只是简单的一个TextView,但是当数据多起来之后,很多TextView,ImageView,以及代码段
View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item,viewGroup,false); ViewHolder vh = new ViewHolder(view);
都可以进行稍微修改。现在ViewHolder修改如下:
public class ViewHolder extends RecyclerView.ViewHolder { private SparseArraymViews; private View mConvertView; private Context mContext; ImageLoaderUtil imageLoaderUtil; public ViewHolder(Context context, View itemView) { super(itemView); mContext = context; mConvertView = itemView; mViews = new SparseArray<>(); imageLoaderUtil = new ImageLoaderUtil(); } public static ViewHolder createViewHolder(Context context, View itemView) { return new ViewHolder(context, itemView); } public static ViewHolder createViewHolder(Context context, ViewGroup parent, int layoutId) { View itemView = LayoutInflater.from(context).inflate(layoutId, parent, false); return new ViewHolder(context, itemView); } /** * 通过viewId获取控件 * * @param viewId * @return */ public T getView(int viewId) { View view = mViews.get(viewId); if (view == null) { view = mConvertView.findViewById(viewId); mViews.put(viewId, view); } return (T) view; } public View getConvertView() { return mConvertView; } /****以下为辅助方法*****/ /** * 设置TextView的值 * * @param viewId * @param text * @return */ 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 setImageUrl(int viewId, String url) { ImageView view = getView(viewId); ImageLoader.Builder builder = new ImageLoader.Builder(); ImageLoader img = builder.url(url) .imgView(view).strategy(ImageLoaderUtil.LOAD_STRATEGY_ONLY_WIFI).build(); imageLoaderUtil.loadImage(mContext, img); return this; } public ViewHolder setImageBitmap(int viewId, Bitmap bitmap) { ImageView view = getView(viewId); view.setImageBitmap(bitmap); return this; } public ViewHolder setImageDrawable(int viewId, Drawable drawable) { ImageView view = getView(viewId); view.setImageDrawable(drawable); return this; }............ /** * 关于事件的 */ public ViewHolder setOnClickListener(int viewId, View.OnClickListener listener) { View view = getView(viewId); view.setOnClickListener(listener); return this; }......}
这里需要关注的是getView方法,直接返回当前view的类型。
所以我们可以在使用viewholder的时候holder.setText(R.id.text_view, "text");
就完成了textView的setText操作。而没有进行类型转换。当然这里省去了findViewById的步骤的同时,使用private SparseArray<View> mViews
进行所有view的保存,也就是在牺牲一定内存性能的情况下,确保了代码的整洁性。还需要关注上面的
public ViewHolder setImageUrl(int viewId, String url) { ImageView view = getView(viewId); ImageLoader.Builder builder = new ImageLoader.Builder(); ImageLoader img = builder.url(url) .imgView(view).strategy(ImageLoaderUtil.LOAD_STRATEGY_ONLY_WIFI).build(); imageLoaderUtil.loadImage(mContext, img); return this; }
这里只需要传入ImageView的id,和url就可以解析网络图片并完成加载。使用的是Glide进行图片加载。具体可以参考之前的文章.这样封装还有一个好处是当你遇到奇葩的服务器返回字段,比如说我们前段时间遇到的每次返回的textView的text都是有问题的,需要我们自己处理,比如说时间需要截取并返回刚刚,几小时前,我们都可以在ViewHolder进行统一的处理,而不需要在每个数据获取的时候进行处理。
这也是我们使用RecyclerView和ListView中过程中经常遇到的问题。看看网易新闻的列表样式,顶部大图,标题+三张图片,标题+左侧图片,视频样式,广告样式....... 这种情况我们怎么便捷快速处理呢?
看看通常处理itemView的type类型不同的方法:
public class MyAdapter extends RecyclerView.Adapter{ class ViewHolder0 extends RecyclerView.ViewHolder { ... } class ViewHolder2 extends RecyclerView.ViewHolder { ... } @Override public int getItemViewType(int position) { // Just as an example, return 0 or 2 depending on position // Note that unlike in ListView adapters, types don't have to be contiguous return position % 2 * 2; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { switch (viewType) { case 0: return new ViewHolder0(...); case 2: return new ViewHolder2(...); ... } }}
这里对方法进行重写, 并且在 onCreateViewHolder()针对不同的viewType进行不同的ViewHolder创建。
同时,这样使用不同的type 来处理不同的位置的数据,也能解决ListView中经常遇到的一个问题,那就是header和footer 的view的添加。我们只需要针对首尾位置进行itemViewType 的处理并且返回header和footer的view就行了。这里也对这种情况进行了处理的封装:
看看实际项目中的效果,一个adapter就完成了所有的不同的item类型操作。 整个类继承自MultiItemTypeAdapter<T>
: /** * Created by Anthony * * */public class NewsMultiAdapter extends MultiItemTypeAdapter{ public NewsMultiAdapter(Context context) { super(context); addItemViewDelegate(new TodayTopicDelegate());// docType = 5, 今日头条样式 addItemViewDelegate(new JustTitleDelegate());// docType = 4, 纯文字样式 addItemViewDelegate(new OtherTypeDelegate());// docType = 0/1, 默认左侧图片 + 右侧标题,描述字段样式// addItemViewDelegate(new BigPicTypeDelegate()); //docType = 2, 顶部标题 + 一张大横图样式 } /* docType = 5, 今日头条样式 docType = 0, 默认左侧图片 + 右侧标题,描述字段样式 docType = 1, 顶部标题 + 三张图片样式 docType = 2, 顶部标题 + 一张大横图样式 docType = 3, 默认样式 + 图集图标 -->点击进入图集细览详情 docType = 4, 纯文字样式 docType = 5, 今日头条样式 docType = 6, 专题样式 */ public class TodayTopicDelegate implements ItemViewDelegate { @Override public int getItemViewLayoutId() { return R.layout.gz_tab1_item_today_topic; } @Override public boolean isForViewType(NewsItem item, int position) { return item.getType() == 5; } @Override public void convert(ViewHolder holder, NewsItem item, int position) { holder.setText(R.id.tv_title_center, item.getTitle()); holder.setText(R.id.tv_news_date, item.getTime()); } } public class OtherTypeDelegate implements ItemViewDelegate { @Override public int getItemViewLayoutId() { return R.layout.gz_tab1_item_normal_news; } @Override public boolean isForViewType(NewsItem item, int position) { return item.getType() == 0; } @Override public void convert(ViewHolder holder, NewsItem item, int position) { holder.setText(R.id.tv_title_center, item.getTitle()); holder.setText(R.id.tv_news_source, item.getSummary()); holder.setText(R.id.tv_news_date, item.getTime()); if (item.getImgs() != null) { String url = item.getImgs().get(0); holder.setImageUrlInGZ(R.id.img_news_image, url); } } } public class JustTitleDelegate implements ItemViewDelegate { @Override public int getItemViewLayoutId() { return R.layout.gz_tab1_item_just_title; } @Override public boolean isForViewType(NewsItem item, int position) { return item.getType() == 4; } @Override public void convert(ViewHolder holder, NewsItem item, int position) { holder.setText(R.id.tv_title_center, item.getTitle()); holder.setText(R.id.tv_news_date, item.getDate()); } }
来看看MultiItemTypeAdapter<T>
/** * Created by zhy on 16/4/9. */public class MultiItemTypeAdapterextends RecyclerView.Adapter { protected Context mContext; protected List mDatas; protected ItemViewDelegateManager mItemViewDelegateManager; protected OnItemClickListener mOnItemClickListener; public int offset = 0; public MultiItemTypeAdapter(Context context, List datas) { mContext = context; mDatas = datas; mItemViewDelegateManager = new ItemViewDelegateManager(); } public MultiItemTypeAdapter(Context context) { this(context, new ArrayList ()); } @Override public int getItemViewType(int position) { if (!useItemViewDelegateManager()) return super.getItemViewType(position); return mItemViewDelegateManager.getItemViewType(mDatas.get(position), position); } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { int layoutId = mItemViewDelegateManager.getItemViewLayoutId(viewType); ViewHolder holder = ViewHolder.createViewHolder(mContext, parent, layoutId); setListener(parent, holder, viewType); return holder; } public void convert(ViewHolder holder, T t) { mItemViewDelegateManager.convert(holder, t, holder.getAdapterPosition()); } protected boolean isEnabled(int viewType) { return true; } protected void setListener(final ViewGroup parent, final ViewHolder viewHolder, int viewType) { if (!isEnabled(viewType)) return; viewHolder.getConvertView().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mOnItemClickListener != null) { int position = viewHolder.getAdapterPosition(); mOnItemClickListener.onItemClick(v, viewHolder, mDatas.get(position - offset), position); } } }); viewHolder.getConvertView().setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { if (mOnItemClickListener != null) { int position = viewHolder.getAdapterPosition(); return mOnItemClickListener.onItemLongClick(v, viewHolder, mDatas.get(position - offset), position); } return false; } }); } @Override public void onBindViewHolder(ViewHolder holder, int position) { convert(holder, mDatas.get(position)); } @Override public int getItemCount() { int itemCount = mDatas.size(); return itemCount; } public List getDatas() { return mDatas; } public MultiItemTypeAdapter addItemViewDelegate(ItemViewDelegate itemViewDelegate) { mItemViewDelegateManager.addDelegate(itemViewDelegate); return this; } public MultiItemTypeAdapter addItemViewDelegate(int viewType, ItemViewDelegate itemViewDelegate) { mItemViewDelegateManager.addDelegate(viewType, itemViewDelegate); return this; } protected boolean useItemViewDelegateManager() { return mItemViewDelegateManager.getItemViewDelegateCount() > 0; } public interface OnItemClickListener { void onItemClick(View view, RecyclerView.ViewHolder holder, T o, int position); boolean onItemLongClick(View view, RecyclerView.ViewHolder holder, T o, int position); } public void setOnItemClickListener(OnItemClickListener onItemClickListener) { this.mOnItemClickListener = onItemClickListener; } public void addDataAll(List data) { mDatas.addAll(data); } public void clearData() { mDatas.clear(); }}
MultiItemTypeAdapter<T>
,这里主要完成了List形式添加数据,数据类型使用泛型操作,只需要在构造函数中,或者public方法 addDataAll
就可以添加列表类型数据。利用ItemViewDelegateManager
完成不同类型type的管理.而添加不同的type是对接口ItemViewDelegate
的实现。ItemViewDelegateManager
起到了一个中间管理者和代理者的作用。具体看下面的代码:
/** * Created by zhy on 16/6/22. */public interface ItemViewDelegate{ int getItemViewLayoutId(); boolean isForViewType(T item, int position); void convert(ViewHolder holder, T t, int position);}
package com.app.gzgov.common.widgets.recyclerview.base;import android.support.v4.util.SparseArrayCompat;/** * Created by zhy on 16/6/22. */public class ItemViewDelegateManager{ SparseArrayCompat > delegates = new SparseArrayCompat(); public int getItemViewDelegateCount() { return delegates.size(); } public ItemViewDelegateManager addDelegate(ItemViewDelegate delegate) { int viewType = delegates.size(); if (delegate != null) { delegates.put(viewType, delegate); viewType++; } return this; } public ItemViewDelegateManager addDelegate(int viewType, ItemViewDelegate delegate) { if (delegates.get(viewType) != null) { throw new IllegalArgumentException( "An ItemViewDelegate is already registered for the viewType = " + viewType + ". Already registered ItemViewDelegate is " + delegates.get(viewType)); } delegates.put(viewType, delegate); return this; } public ItemViewDelegateManager removeDelegate(ItemViewDelegate delegate) { if (delegate == null) { throw new NullPointerException("ItemViewDelegate is null"); } int indexToRemove = delegates.indexOfValue(delegate); if (indexToRemove >= 0) { delegates.removeAt(indexToRemove); } return this; } public ItemViewDelegateManager removeDelegate(int itemType) { int indexToRemove = delegates.indexOfKey(itemType); if (indexToRemove >= 0) { delegates.removeAt(indexToRemove); } return this; } public int getItemViewType(T item, int position) { int delegatesCount = delegates.size(); for (int i = delegatesCount - 1; i >= 0; i--) { ItemViewDelegate delegate = delegates.valueAt(i); if (delegate.isForViewType( item, position)) { return delegates.keyAt(i); } } throw new IllegalArgumentException( "No ItemViewDelegate added that matches position=" + position + " in data source"); } public void convert(ViewHolder holder, T item, int position) { int delegatesCount = delegates.size(); for (int i = 0; i < delegatesCount; i++) { ItemViewDelegate delegate = delegates.valueAt(i); if (delegate.isForViewType( item, position)) { delegate.convert(holder, item, position); return; } } throw new IllegalArgumentException( "No ItemViewDelegateManager added that matches position=" + position + " in data source"); } public int getItemViewLayoutId(int viewType) { return delegates.get(viewType).getItemViewLayoutId(); } public int getItemViewType(ItemViewDelegate itemViewDelegate) { return delegates.indexOfValue(itemViewDelegate); }}
这里也就解决了多种itemViewType的问题。实现了方便的添加不同的类型的item数据。泛型数据降低了代码的耦合度。
这里提供一种item布局,就只需要对MultiItemTypeAdapter<T>进行限定一种layout类型。并且isForViewType方法返回true,代表着始终返回当前的layout。
那么对于只有一种类型的列表数据
/** * Created by zhy on 16/4/9. */public abstract class CommonAdapterextends MultiItemTypeAdapter { protected Context mContext; protected int mLayoutId; protected List mDatas; protected LayoutInflater mInflater; public CommonAdapter(final Context context, final int layoutId) { this(context, layoutId, new ArrayList ()); } public CommonAdapter(final Context context, final int layoutId, List datas) { super(context, datas); mContext = context; mInflater = LayoutInflater.from(context); mLayoutId = layoutId; mDatas = datas; addItemViewDelegate(new ItemViewDelegate () { @Override public int getItemViewLayoutId() { return layoutId; } @Override public boolean isForViewType(T item, int position) { return true; } @Override public void convert(ViewHolder holder, T t, int position) { CommonAdapter.this.convert(holder, t, position); } }); } protected abstract void convert(ViewHolder holder, T t, int position);}
具体的新闻类型:
public class NewsSingleAdapter extends CommonAdapter{ public NewsSingleAdapter(Context context) { super(context, R.layout.prj_list_item_news); } @Override protected void convert(ViewHolder holder, NewsItem item, int position) { holder.setText(R.id.tv_news_title, item.getTitle()); holder.setText(R.id.tv_news_summary, item.getSummary()); holder.setText(R.id.tv_news_date, item.getTime()); holder.setImageUrl(R.id.img_news_image,item.getImgs().get(0)); }}
这里也就实现了单一的列表形式,比如网易新闻的专题样式:
这里直接参加wrapper包中几个类,
这里是对不同的item的type 类型进行控制,从而得到了不同的RecyclerView的样式。具体可以参考我的中的代码。
/** * Created by zhy on 16/6/23. */public class HeaderAndFooterWrapperextends RecyclerView.Adapter { private static final int BASE_ITEM_TYPE_HEADER = 100000; private static final int BASE_ITEM_TYPE_FOOTER = 200000; private SparseArrayCompat mHeaderViews = new SparseArrayCompat<>(); private SparseArrayCompat mFootViews = new SparseArrayCompat<>(); private RecyclerView.Adapter mInnerAdapter; private RecyclerView.Adapter mNotifyAdapter; public HeaderAndFooterWrapper(RecyclerView.Adapter adapter) { mInnerAdapter = adapter; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (mHeaderViews.get(viewType) != null) { ViewHolder holder = ViewHolder.createViewHolder(parent.getContext(), mHeaderViews.get(viewType)); return holder; } else if (mFootViews.get(viewType) != null) { ViewHolder holder = ViewHolder.createViewHolder(parent.getContext(), mFootViews.get(viewType)); return holder; } return mInnerAdapter.onCreateViewHolder(parent, viewType); } @Override public int getItemViewType(int position) { if (isHeaderViewPos(position)) { return mHeaderViews.keyAt(position); } else if (isFooterViewPos(position)) { return mFootViews.keyAt(position - getHeadersCount() - getRealItemCount()); } return mInnerAdapter.getItemViewType(position - getHeadersCount()); } private int getRealItemCount() { return mInnerAdapter.getItemCount(); } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (isHeaderViewPos(position)) { return; } if (isFooterViewPos(position)) { return; } mInnerAdapter.onBindViewHolder(holder, position - getHeadersCount()); } @Override public int getItemCount() { return getHeadersCount() + getFootersCount() + getRealItemCount(); } @Override public void onAttachedToRecyclerView(RecyclerView recyclerView) { mNotifyAdapter = recyclerView.getAdapter(); WrapperUtils.onAttachedToRecyclerView(mInnerAdapter, recyclerView, new WrapperUtils.SpanSizeCallback() { @Override public int getSpanSize(GridLayoutManager layoutManager, GridLayoutManager.SpanSizeLookup oldLookup, int position) { int viewType = getItemViewType(position); if (mHeaderViews.get(viewType) != null) { return layoutManager.getSpanCount(); } else if (mFootViews.get(viewType) != null) { return layoutManager.getSpanCount(); } if (oldLookup != null) return oldLookup.getSpanSize(position); return 1; } }); } @Override public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) { mInnerAdapter.onViewAttachedToWindow(holder); int position = holder.getLayoutPosition(); if (isHeaderViewPos(position) || isFooterViewPos(position)) { WrapperUtils.setFullSpan(holder); } } private boolean isHeaderViewPos(int position) { return position < getHeadersCount(); } private boolean isFooterViewPos(int position) { return position >= getHeadersCount() + getRealItemCount(); } public void addHeaderView(View view) { int key = findHeaderKeyByView(view); if (key == -1) { mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view); if (mNotifyAdapter != null) mNotifyAdapter.notifyDataSetChanged(); if (mInnerAdapter instanceof MultiItemTypeAdapter) { ((MultiItemTypeAdapter) mInnerAdapter).offset += 1; } } } public void addFootView(View view) { mFootViews.put(mFootViews.size() + BASE_ITEM_TYPE_FOOTER, view); } public int getHeadersCount() { return mHeaderViews.size(); } public int getFootersCount() { return mFootViews.size(); } public void deleteHeaderView(View view) {// if (mHeaderViews.size() > position && position >=0 ) { // View v = mHeaderViews.get(position + BASE_ITEM_TYPE_HEADER, null);// if (v != null) { // mHeaderViews.remove(position + BASE_ITEM_TYPE_HEADER);// if (mInnerAdapter instanceof MultiItemTypeAdapter) { // ((MultiItemTypeAdapter) mInnerAdapter).offset -= 1;// }// if (mNotifyAdapter != null)// mNotifyAdapter.notifyDataSetChanged();// }// }// for(int i=0; i
这里的header和footer没有个数的限制。
现在需求又来了 。需要对RecyclerView中的item进行分区操作,就比如说微信以B开头的姓名都放在B这个分区下,以C开头的名字,都在C这个分区下。比如说京东的这个界面
列表数据里面添加了分区。那么怎么操作呢?这里对开源库做了集成。并且添加的上面的ViewHolder,简化onCreateViewHolder中的数据绑定操作。 也就是代码中的recyclerview-section包中的部分。
1) 创建自定义 Section 类集成自 StatelessSection
public class WeiboGridSection extends StatelessSection { private final Listlist; public WeiboGridSection(List list) { super(R.layout.grid_item); this.list = list; } @Override public int getContentItemsTotal() { return list.getItems().size(); } @Override public ViewHolder getItemViewHolder(View view, int viewType) { return new ViewHolder(mContext, view); } @Override public void onBindItemViewHolder(ViewHolder holder, final int position) { final NewsItem newsItem = list.get(position); String itemImgUrl = newsItem.getImages().get(0); final String name = newsItem.getTitle(); holder.setImageUrl(R.id.grid_item_iv, itemImgUrl); holder.setText(R.id.grid_item_tv, name); holder.getConvertView().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(getActivity(), WebViewCommentActivity.class); intent.putExtra(WebViewCommentActivity.WEB_VIEW_ITEM, newsItem); startActivity(intent); } }); } } @Override public ViewHolder getHeaderViewHolder(Context context, View view) { return new ViewHolder(mContext, view); } @Override public void onBindHeaderViewHolder(ViewHolder holder) { holder.setText(R.id.section_header_tv, "微博关注"); holder.setImageResource(R.id.section_header_iv, R.mipmap.wb_focus); } }
2) 添加section到adapter,注意这里是SectionRVAdapter
// Create an instance of SectionedRecyclerViewAdapter SectionRVAdapter sectionAdapter = new SectionRVAdapter();// Add your SectionssectionAdapter.addSection(new MySection());// Set up your RecyclerView with the SectionRVAdapterRecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerview);recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));recyclerView.setAdapter(sectionAdapter);
看看界面效果
整个代码由于是对RecyclerView.Adapter
封装。所以需要关注的方法自然是getItemViewType
,onBindViewHolder
,createViewHolder
和getItemCount
四个方法,下面以这四个方法为切入点进行分析:
onCreateViewHolder完成的是ViewHolder的创建,每一个section分为头部header,底部footer。以及中间部分,中间布局可以有Loading/Loaded/Failed三种状态分别对应加载,加载成功,失败界面。注意这里的状态都分别对应于每个section里面,而不是整个RecyclerView.
也就是说一个RecyclerView可以由多个Section组成,一个Section最多只能有一个Header和Footer,Section由多个RecyclerView的item条目组成。每个Section中间可以有三种状态。Loading/Loaded/Failed三种状态分别对应加载,加载成功,失败界面。
下面是来自的界面效果
@Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { ViewHolder viewHolder = null; View view = null; for (Map.Entryentry : sectionViewTypeNumbers.entrySet()) { if (viewType >= entry.getValue() && viewType < entry.getValue() + VIEW_TYPE_QTY) { Section section = sections.get(entry.getKey()); int sectionViewType = viewType - entry.getValue(); switch (sectionViewType) { case VIEW_TYPE_HEADER: { Integer resId = section.getHeaderResourceId(); if (resId == null) throw new NullPointerException("Missing 'header' resource id"); view = LayoutInflater.from(parent.getContext()).inflate(resId, parent, false); // get the header viewholder from the section viewHolder = section.getHeaderViewHolder(mContext,view); break; } case VIEW_TYPE_FOOTER: { Integer resId = section.getFooterResourceId(); if (resId == null) throw new NullPointerException("Missing 'footer' resource id"); view = LayoutInflater.from(parent.getContext()).inflate(resId, parent, false); // get the footer viewholder from the section viewHolder = section.getFooterViewHolder(mContext,view); break; } case VIEW_TYPE_ITEM_LOADED: { view = LayoutInflater.from(parent.getContext()).inflate(section.getItemResourceId(), parent, false); // get the item viewholder from the section viewHolder = section.getItemViewHolder(view,viewType); break; } case VIEW_TYPE_LOADING: { Integer resId = section.getLoadingResourceId(); if (resId == null) throw new NullPointerException("Missing 'loading state' resource id"); view = LayoutInflater.from(parent.getContext()).inflate(resId, parent, false); // get the loading viewholder from the section viewHolder = section.getLoadingViewHolder(mContext,view); break; } case VIEW_TYPE_FAILED: { Integer resId = section.getFailedResourceId(); if (resId == null) throw new NullPointerException("Missing 'failed state' resource id"); view = LayoutInflater.from(parent.getContext()).inflate(resId, parent, false); // get the failed load viewholder from the section viewHolder = section.getFailedViewHolder(mContext,view); break; } default: throw new IllegalArgumentException("Invalid viewType"); } } } return viewHolder; }
和前面MultiItemTypeAdapter<T>
的实现一样,我们需要根据不同viewType创建不同的viewHolder.但是需要注意的是一个Section是由一组item组成的,所以一个section需要多次的调用onCreateViewHolder进行创建不同的类型的样式。
接下来关注onBindViewHolder
方法,通过
int sectionTotal = section.getSectionItemsTotal();
获取到了section的item的数量,并在下方针对每个section的头部header,底部footer,以及中间部分进行操作。并且调用onBindHeaderViewHolder(holder)
,onBindFooterViewHolder(holder)
以及onBindContentViewHolder(holder, getSectionPosition(position))
方法,这就是当我们实现Section代码的时候需要实现的方法。section.getSectionItemsTotal()
也是我们实现Section的时候提供的section的item的个数。 @Override public void onBindViewHolder(ViewHolder holder, int position) { int currentPos = 0; for (Map.Entryentry : sections.entrySet()) { Section section = entry.getValue(); // ignore invisible sections if (!section.isVisible()) continue; int sectionTotal = section.getSectionItemsTotal(); // check if position is in this section if (position >= currentPos && position <= (currentPos + sectionTotal - 1)) { if (section.hasHeader()) { if (position == currentPos) { // delegate the binding to the section header getSectionForPosition(position).onBindHeaderViewHolder(holder); return; } } if (section.hasFooter()) { if (position == (currentPos + sectionTotal - 1)) { // delegate the binding to the section header getSectionForPosition(position).onBindFooterViewHolder(holder); return; } } // delegate the binding to the section content getSectionForPosition(position).onBindContentViewHolder(holder, getSectionPosition(position)); return; } currentPos += sectionTotal; } throw new IndexOutOfBoundsException("Invalid position"); }
接下来关注getItemCount,代表整个RecyclerView的item的个数。当然是所有Section的item的总和,所以代码如下。
最后需要关注的是方法
getItemViewType
,这里也就完成了每个Section的五种类型的int返回操作。 @Override public int getItemViewType(int position) { /* Each Section has 5 "viewtypes": 1) header 2) footer 3) items 4) loading 5) load failed */ int currentPos = 0; for (Map.Entryentry : sections.entrySet()) { Section section = entry.getValue(); // ignore invisible sections if (!section.isVisible()) continue; int sectionTotal = section.getSectionItemsTotal(); // check if position is in this section if (position >= currentPos && position <= (currentPos + sectionTotal - 1)) { int viewType = sectionViewTypeNumbers.get(entry.getKey()); if (section.hasHeader()) { if (position == currentPos) { return viewType; } } if (section.hasFooter()) { if (position == (currentPos + sectionTotal - 1)) { return viewType + 1; } } switch (section.getState()) { case LOADED: return viewType + 2; case LOADING: return viewType + 3; case FAILED: return viewType + 4; default: throw new IllegalStateException("Invalid state"); } } currentPos += sectionTotal; } throw new IndexOutOfBoundsException("Invalid position"); }
首先关注的是SectionRVAdapter,这里我并没有集成自上面的MultiItemTypeAdapter<T>
,应为这里涉及到对RecyclerView.Adapter的封装操作。这里的弊端也就是每一个section甚至整个RecyclerView的itemView都是一个形式。但是多种形式的section我们可以转化为将section也视作一种MultiItemTypeAdapter<T>
的item类型就能解决,所以这里也不算问题。这里是的完整代码。至于类,主要是对几种方法和view状态,以及所有item的封装,这里不再赘述。直接看代码。
最后针对最近项目中遇到的这个问题,针对不同的布局,比如说下面的这个既有Grid,又有linear的形式。由于之前的问题全部是针对一个RecyclerView的,而一个RecyclerView在调用recyclerView.setLayoutManager()
方法的时候,就只能有一个布局方式。好吧,当初我就是为了解决这个问题,后来才发现需要用三个RecyclerView来解决。
这篇博客就到这里,回过头去,去看看鸿洋写的,以及开源库相信你一目了然。
这里就是对引用到实际项目中以及引入开源库作为实际开发的例子。注:这里不能提供实际项目代码,只能提供代码片段作为参考。目前暂未提供示例代码到github的项目中,只提供了recyclerView公用组件。
项目地址: