RecyclerView新增Header的正確方式
看了一下部落格目錄,已經有好幾篇部落格是關於RecyclerView
的,不過對於這麼一款強大的控制元件,我還是要再寫一篇部落格來學習一下,這篇部落格的主題是《為RecyclerView新增header》,當然在看完這篇部落格後,相信新增Footer你也應該能夠學會。話說在這麼多新控制元件中為何RecyclerView
備受開發者的喜愛?這還是因為在Android發展到今天基本上還沒有像RecyclerView
這麼靈活的一個玩意,鑑於他的靈活以及強大,很多人(包括我)已經開始拋棄ListView
和GridView
轉為RecyclerView
了,再使用過RecyclerView
和被善變的需求折磨後,我相信會有越來越多的人轉到RecyclerView
問題
好了,廢話不多說了,這篇部落格我們要解決的問題有:
- 如何為RecyclerView新增Header
- 如何讓Header適配各種LayoutManager
- 在有Header的情況下,我們的分割線該怎麼畫
- 作為一個懶惰的程式設計師,如何將這些做到最簡便
如果為RecyclerView新增Header
大家在使用ListView
的時候可以很輕鬆的新增headers, 但是不知道大家發現沒有,RecyclerView
和各種LayoutManager
都沒有哪個方法是為新增header而設立的,這個時候我們就開始思考如何為RecyclerView
Adapter
的itemType
來設定的,思路就是根據不同的itemType去載入不同的佈局。
/**
* 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
看看咋樣。
為GridLayoutManager新增header
// mLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
mLayoutManager = new GridLayoutManager(this, 2);
哎喲,我的小心臟啊,快受不了了,這是什麼玩意,我們的header竟然作為一個cell出現在了介面上,這完全不是我們想要的效果啊! 冷靜下來想想,肯定會有解決方法的吧。這時候我們就該引入一個不太常用的方法了:
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
,
@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;
}
});
}
}
這個時候我們再來看一下效果,
恩,這次達到我們的要求了,不過對於StaggeredGridLayoutManager
我們還沒做處理,而且我們還發現StaggeredGridLayoutManager
中並沒有像GridLayoutManager
中這樣的方法,我們還需要單獨為StaggeredGridLayoutManager
單獨處理一下。
為StaggeredGridLayoutManager新增header
我們繼續重寫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啦。
好了,這篇部落格就到這裡吧,最後是本文程式碼的下載。