原文連接: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啦。