原文连接:http://blog.csdn.net/qibin0506/article/details/49716795java
看了一下博客目录,已经有好几篇博客是关于RecyclerView
的,不过对于这么一款强大的控件,我仍是要再写一篇博客来学习一下,这篇博客的主题是《为RecyclerView添加header》,固然在看完这篇博客后,相信添加Footer你也应该可以学会。话说在这么多新控件中为什么RecyclerView
备受开发者的喜好?这仍是由于在Android发展到今天基本上尚未像RecyclerView
这么灵活的一个玩意,鉴于他的灵活以及强大,不少人(包括我)已经开始抛弃ListView
和GridView
转为RecyclerView
了,再使用过RecyclerView
和被善变的需求折磨后,我相信会有愈来愈多的人转到RecyclerView
的使用上。android
好了,废话很少说了,这篇博客咱们要解决的问题有:程序员
- 如何为RecyclerView添加Header
- 如何让Header适配各类LayoutManager
- 在有Header的状况下,咱们的分割线该怎么画
- 做为一个懒惰的程序员,如何将这些作到最简便
你们在使用ListView
的时候能够很轻松的添加headers, 可是不知道你们发现没有,RecyclerView
和各类LayoutManager
都没有哪一个方法是为添加header而设立的,这个时候咱们就开始思考如何为RecyclerView
添加header了。 这里咱们的解决方案和网上你能搜到的大多数方案同样,是经过控制Adapter
的itemType
来设置的,思路就是根据不一样的itemType去加载不一样的布局。ide
/** * Created by qibin on 2015/11/5. */ public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { public static final int TYPE_HEADER = 0; public static final int TYPE_NORMAL = 1; private ArrayList<String> mDatas = new ArrayList<>(); private View mHeaderView; private OnItemClickListener mListener; public void setOnItemClickListener(OnItemClickListener li) { mListener = li; } public void setHeaderView(View headerView) { mHeaderView = headerView; notifyItemInserted(0); } public View getHeaderView() { return mHeaderView; } public void addDatas(ArrayList<String> datas) { mDatas.addAll(datas); notifyDataSetChanged(); } @Override public int getItemViewType(int position) { if(mHeaderView == null) return TYPE_NORMAL; if(position == 0) return TYPE_HEADER; return TYPE_NORMAL; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if(mHeaderView != null && viewType == TYPE_HEADER) return new Holder(mHeaderView); View layout = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false); return new Holder(layout); } @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { if(getItemViewType(position) == TYPE_HEADER) return; final int pos = getRealPosition(viewHolder); final String data = mDatas.get(pos); if(viewHolder instanceof Holder) { ((Holder) viewHolder).text.setText(data); if(mListener == null) return; viewHolder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mListener.onItemClick(pos, data); } }); } } public int getRealPosition(RecyclerView.ViewHolder holder) { int position = holder.getLayoutPosition(); return mHeaderView == null ? position : position - 1; } @Override public int getItemCount() { return mHeaderView == null ? mDatas.size() : mDatas.size() + 1; } class Holder extends RecyclerView.ViewHolder { TextView text; public Holder(View itemView) { super(itemView); if(itemView == mHeaderView) return; text = (TextView) itemView.findViewById(R.id.text); } } interface OnItemClickListener { void onItemClick(int position, String data); } }
这里咱们重写了getItemViewType
方法,并根据位置来返回不一样的type,这个type是咱们预先商定好的常量,接在onCreateViewHolder
方法中来判断itemType,若是是header,则返回咱们设置的headerView,不然正常加载item布局,相信你们对于上面的代码不会有任何疑问,接下来咱们就在Activity中用一下试试看,布局
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mRecyclerView = (RecyclerView) findViewById(R.id.list); mLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false); mRecyclerView.setLayoutManager(mLayoutManager); mRecyclerView.setItemAnimator(new DefaultItemAnimator()); mAdapter = new MyAdapter(); mRecyclerView.setAdapter(mAdapter); mAdapter.addDatas(generateData()); setHeader(mRecyclerView); mAdapter.setOnItemClickListener(new MyAdapter.OnItemClickListener() { @Override public void onItemClick(int position, String data) { Toast.makeText(MainActivity.this, data, Toast.LENGTH_SHORT).show(); } }); } private void setHeader(RecyclerView view) { View header = LayoutInflater.from(this).inflate(R.layout.header, view, false); mAdapter.setHeaderView(header); }
这里LayoutManager
咱们使用了LinearLayoutManager
,而且给Adapter
设置了一个header,运行一下
看看效果:学习
恩,还不错,item的点击事件也很完美,那接下来,咱们将LayoutManager
换成GridLayoutManager
看看咋样。this
// mLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false); mLayoutManager = new GridLayoutManager(this, 2);
哎哟,个人当心脏啊,快受不了了,这是什么玩意,咱们的header居然做为一个cell出如今了界面上,这彻底不是咱们想要的效果啊! 冷静下来想一想,确定会有解决方法的吧。这时候咱们就该引入一个不太经常使用的方法了:spa
gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { return getItemViewType(position) == TYPE_HEADER ? gridManager.getSpanCount() : 1; } });
咱们解释一下这段代码,首先咱们设置了一个SpanSizeLookup
,这个类是一个抽象类,并且仅有一个抽象方法getSpanSize
,这个方法的返回值决定了咱们每一个position上的item占据的单元格个数,而咱们这段代码综合上面为GridLayoutManager
设置的每行的个数来解释的话,
就是当前位置是header的位置,那么该item占据2个单元格,正常状况下占据1个单元格。那这段代码放哪呢? 为了之后的封装,咱们仍是在Adapter
中找方法放吧。
咱们在Adapter
中再重写一个方法onAttachedToRecyclerView
,.net
@Override public void onAttachedToRecyclerView(RecyclerView recyclerView) { super.onAttachedToRecyclerView(recyclerView); RecyclerView.LayoutManager manager = recyclerView.getLayoutManager(); if(manager instanceof GridLayoutManager) { final GridLayoutManager gridManager = ((GridLayoutManager) manager); gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { return getItemViewType(position) == TYPE_HEADER ? gridManager.getSpanCount() : 1; } }); } }
这个时候咱们再来看一下效果,code
恩,此次达到咱们的要求了,不过对于StaggeredGridLayoutManager
咱们还没作处理,并且咱们还发现StaggeredGridLayoutManager
中并无像GridLayoutManager
中这样的方法,咱们还须要单独为StaggeredGridLayoutManager
单独处理一下。
咱们继续重写Adapter
中另一个方法。
@Override public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) { super.onViewAttachedToWindow(holder); ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); if(lp != null && lp instanceof StaggeredGridLayoutManager.LayoutParams) { StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp; p.setFullSpan(holder.getLayoutPosition() == 0); } }
这里的处理方式是用经过LayoutParams
,并且这里更简单,StaggeredGridLayoutManager.LayoutParams
为咱们提供了一个setFullSpan
方法来设置占领所有空间,好开心,看一下StaggeredGridLayoutManager
的效果,
啊, 怎么和上面的效果同样? 很简单嘛,咱们的item都是等高的。
这是咱们开开心心的继续写代码,而且为咱们的item添加了分隔符,分隔符我仍是用的翔哥写的那个,毕竟翔哥写的太好了,并且咱们没有必要重复造轮子,不过这时候问题出现了,相信你也确定能猜到应该会出现问题了,由于无论咱们怎么处理,header对于RecyclerView
来讲仍是一个普普统统的item,这时候咱们添加分割线,确定也会对header产生影响,那下面,咱们再来对翔哥的分割线改造一下吧。
public class GridItemDecoration extends RecyclerView.ItemDecoration { private static final int[] ATTRS = new int[]{android.R.attr.listDivider}; private Drawable mDivider; private boolean hasHeader; public GridItemDecoration(Context context) { final TypedArray a = context.obtainStyledAttributes(ATTRS); mDivider = a.getDrawable(0); a.recycle(); } public GridItemDecoration(Context context, boolean header) { this(context); hasHeader = header; } ... @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { int position = parent.getChildAdapterPosition(view); int spanCount = getSpanCount(parent); int childCount = parent.getAdapter().getItemCount(); int pos = position; if(hasHeader) { if(position == 0) { outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); return; } else { pos = position - 1; } } if (isLastColum(parent, pos, spanCount, childCount)) { outRect.set(0, 0, mDivider.getIntrinsicWidth(), mDivider.getIntrinsicHeight()); } else { outRect.set(0, 0, mDivider.getIntrinsicWidth(), mDivider.getIntrinsicHeight()); } } }
改造的地方是获取偏移量的方法咱们换了一个,由于原来的那个已通过时了,并且,这里咱们还加了一个boolean类型的hasHeader
变量来表示是否是有header,若是hasHeader而且position为0,那么咱们仅仅绘制底部的分割线,其余的地方不绘制,在有header的状况下,咱们还须要将position减1,由于咱们认为的第1个item实际上是第2个。这个时候咱们再来看看有分割线的效果。
看来咱们的想法是对的,header部分除了底部有一个分割线外,并无其余的分割线,这也彻底符合咱们的需求。
这下好了,基本上完美的处理好了,但是难道咱们对于不一样的Adapter
都须要写那么多代码吗? 对于一个懒程序员来讲,这确定是一个可怕的事情,因此,咱们还须要对咱们的Adapter
进行封装,目的就是能够轻轻松松的写代码,
/** * Created by qibin on 2015/11/5. */ public abstract class BaseRecyclerAdapter<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder> { public static final int TYPE_HEADER = 0; public static final int TYPE_NORMAL = 1; private ArrayList<T> mDatas = new ArrayList<>(); private View mHeaderView; private OnItemClickListener mListener; public void setOnItemClickListener(OnItemClickListener li) { mListener = li; } public void setHeaderView(View headerView) { mHeaderView = headerView; notifyItemInserted(0); } public View getHeaderView() { return mHeaderView; } public void addDatas(ArrayList<T> datas) { mDatas.addAll(datas); notifyDataSetChanged(); } @Override public int getItemViewType(int position) { if(mHeaderView == null) return TYPE_NORMAL; if(position == 0) return TYPE_HEADER; return TYPE_NORMAL; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, final int viewType) { if(mHeaderView != null && viewType == TYPE_HEADER) return new Holder(mHeaderView); return onCreate(parent, viewType); } @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { if(getItemViewType(position) == TYPE_HEADER) return; final int pos = getRealPosition(viewHolder); final T data = mDatas.get(pos); onBind(viewHolder, pos, data); if(mListener != null) { viewHolder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mListener.onItemClick(pos, data); } }); } } @Override public void onAttachedToRecyclerView(RecyclerView recyclerView) { super.onAttachedToRecyclerView(recyclerView); RecyclerView.LayoutManager manager = recyclerView.getLayoutManager(); if(manager instanceof GridLayoutManager) { final GridLayoutManager gridManager = ((GridLayoutManager) manager); gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { return getItemViewType(position) == TYPE_HEADER ? gridManager.getSpanCount() : 1; } }); } } @Override public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) { super.onViewAttachedToWindow(holder); ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); if(lp != null && lp instanceof StaggeredGridLayoutManager.LayoutParams) { StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp; p.setFullSpan(holder.getLayoutPosition() == 0); } } public int getRealPosition(RecyclerView.ViewHolder holder) { int position = holder.getLayoutPosition(); return mHeaderView == null ? position : position - 1; } @Override public int getItemCount() { return mHeaderView == null ? mDatas.size() : mDatas.size() + 1; } public abstract RecyclerView.ViewHolder onCreate(ViewGroup parent, final int viewType); public abstract void onBind(RecyclerView.ViewHolder viewHolder, int RealPosition, T data); public class Holder extends RecyclerView.ViewHolder { public Holder(View itemView) { super(itemView); } } public interface OnItemClickListener<T> { void onItemClick(int position, T data); } }
咱们将BaseRecyclerAdapter
抽象起来,而且提供两个抽象方法onCreate
和onBind
用来建立holder和绑定数据,而对于header作的一系列工做,咱们都放到了BaseRecyclerAdapter
中,而继承BaseRecyclerAdapter
后,咱们仅仅关心咱们的holder怎么建立和数据怎么绑定就ok。例以下面代码:
/** * Created by qibin on 2015/11/7. */ public class MyAdapter extends BaseRecyclerAdapter<String> { @Override public RecyclerView.ViewHolder onCreate(ViewGroup parent, int viewType) { View layout = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false); return new MyHolder(layout); } @Override public void onBind(RecyclerView.ViewHolder viewHolder, int RealPosition, String data) { if(viewHolder instanceof MyHolder) { ((MyHolder) viewHolder).text.setText(data); } } class MyHolder extends BaseRecyclerAdapter.Holder { TextView text; public MyHolder(View itemView) { super(itemView); text = (TextView) itemView.findViewById(R.id.text); } } }
这样咱们再用起来就简单多了,对于这样的封装,咱们还算满意,再作完添加header后,相信你们对于footer也有想法了,有想法就实现它吧,扩展一下BaseRecyclerAdapter
就ok啦。