实现带header和footer功能的RecyclerView
這個項目很簡單,其實一年前就開發完成了,但是一直沒閑下來去整理。
RecyclerView是Android 5.0版本引入的一個新的組件,目的是在一些場景中取代之前ListView和GridView,實現性能更優的解決方案。同時RecyclerView的靈活性讓它可勝任更多的場景。關于RecyclerView的使用有太多的文章了,大家可以自行搜索。
我們知道RecyclerView很靈活,靈活到很多功能需要我們自己實現,比如ListView和GridView中最常用的Item點擊事件。所以在使用了幾次后,我準備自己封裝一個WrapRecyclerView,實現一些非常常用的功能。
在ListView中我們經常使用header和footer功能,確實也給我們帶來了不少方便,而且使用場景很多。但是在RecyclerView并沒有默認實現這個功能,所以WrapRecyclerView首要任務就是添加這個功能。實現后的效果圖如下:
? ? ?? ? ?
首先,我們為了WrapRecyclerView創建一個內部類 WrapAdapterextendsAdapter<ViewHolder>?,同時重寫WrapRecyclerView部分方法,如
@Overridepublic void setAdapter(Adapter adapter) {mWrapAdapter.setAdapter(adapter);super.setAdapter(mWrapAdapter);}@Overridepublic Adapter getAdapter() {return mWrapAdapter.getAdapter();}將傳入的adapter(在下面內容中我們稱這個adapter為外部adapter)交給WrapAdapter來處理,WrapAdapter在WrapRecyclerView構造函數中已經初始化。
在WrapAdapter中我們增加一些針對header和footer的方法,如
public void addHeaderView(View header){if(mHeaderViews == null){mHeaderViews = new ArrayList<View>();}if(!mHeaderViews.contains(header)) {mHeaderViews.add(header);}notifyDataSetChanged();}public void removeHeaderView(View header){if(mHeaderViews != null){mHeaderViews.remove(header);notifyDataSetChanged();}}public int getHeaderCount(){if(mHeaderViews == null){return 0;}return mHeaderViews.size();}public void addFooterView(View footer){if(mFooterViews == null){mFooterViews = new ArrayList<View>();}if(!mFooterViews.contains(footer)) {mFooterViews.add(footer);}notifyDataSetChanged();}public void removeFooterView(View footer){if(mFooterViews != null){mFooterViews.remove(footer);notifyDataSetChanged();}}public int getFooterCount(){if(mFooterViews == null){return 0;}return mFooterViews.size();}
這部分代碼比較簡單,不詳細解釋了,主要是在WrapAdapter中分別用兩個list(mHeaderViews和mFooterViews)來管理header和footer。
接下來要區分item做不同的處理,使用getItemViewType來區分不同的item,如
@Overridepublic int getItemViewType(int position) {if(position < getHeaderCount()){return TYPE_HEADER - position;}else if(position < getItemCount() - getFooterCount()){return mAdapter.getItemViewType(position - getHeaderCount());}else {return TYPE_FOOTER - position + getItemCount() - getFooterCount();}}TYPE_HEADER和TYPE_FOOTER分別為-1000和-2000(一般header和footer不會有太多)。
如果是正常的item,直接調用外部adapter的對應方法;如果是header和footer,在對應標識上要減去該header或footer在對應的list中的位置,下面就會解釋這樣做的原因。
得到了不同的type之后在create和bind就可以根據type做不同的處理,如
@Overridepublic ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {ViewHolder holder = null;int type = 0;int position = 0;if(viewType <= TYPE_FOOTER){type = TYPE_FOOTER;position = TYPE_FOOTER - viewType;}else if(viewType <= TYPE_HEADER){type = TYPE_HEADER;position = TYPE_HEADER - viewType;}else{type = viewType;}switch (type){case TYPE_HEADER:View header = mHeaderViews.get(position);setWrapParems(header);holder = new WrapViewHolder(header);break;case TYPE_FOOTER:View footer = mFooterViews.get(position);setWrapParems(footer);holder = new WrapViewHolder(footer);break;default:holder = mAdapter.onCreateViewHolder(parent, viewType);break;}return holder;}@Overridepublic void onBindViewHolder(ViewHolder holder, int position) {if (holder instanceof WrapViewHolder){}else {int realPosition = position - getHeaderCount();mAdapter.onBindViewHolder(holder, realPosition);holder.itemView.setOnClickListener(new OnPositionClick(realPosition));holder.itemView.setOnLongClickListener(new OnPositionLongClick(realPosition));}}在onCreateViewHolder中不僅要區分type,同時如果是header或footer還需要知道是哪一個,這就是前面代碼中在type中添入位置的原因。
如果是item,直接調用外部adapter的create方法來生成view;如果是header或footer,則根據計算出來的position從list中獲取并封裝進一個WrapViewHolder。
在onBindViewHolder中判斷如果是WrapViewHolder則表示是header或footer,一般header 和footer在添加進來之前數據都加載到view中了,這里不再處理;否則調用外部adapter的bind方法加載數據。
經過上面幾步,我們已經構建了一個帶有header和footer的adapter。但是還有一個問題,因為RecyclerView有三種LayoutManager:LinearLayoutManager、GridLayoutManager、StaggeredLayoutManager。對于LinearLayoutManager來說,上面封裝的功能已經可以實現header和footer了。但是對于其他兩個來說,還遠遠不夠。
由于GridLayoutManager和StaggeredLayoutManager是多列的,每個header和footer都需要獨占一行,所以我們需要對這兩種LayoutManager分別作一些處理。
首先是GridLayoutManager,需要在WrapRecyclerView中重寫setLayoutManager,代碼如下
@Overridepublic void setLayoutManager(final LayoutManager layout) {if(layout instanceof GridLayoutManager){final GridLayoutManager.SpanSizeLookup old = ((GridLayoutManager) layout).getSpanSizeLookup();((GridLayoutManager) layout).setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {@Overridepublic int getSpanSize(int position) {if(mWrapAdapter.isHeader(position) || mWrapAdapter.isFooter(position)){return ((GridLayoutManager) layout).getSpanCount();}if(old != null){return old.getSpanSize(position - mWrapAdapter.getHeaderCount());}return 1;}});}super.setLayoutManager(layout);}
當為WrapRecyclerView設置的LayoutManager是GridLayoutManager時,為其設置SpanSizeLookup,并通過position判斷如果是header或footer返回SpanCount(這個count是初始化GridLayoutMananger設置的每行的列數),這樣就保證了header和footer可以獨占一行。
注意:這里考慮到用戶也需要自定義SpanSizeLookup,所以在設置前先獲取一下,如果存在則在getSpanSize中返回正確的值保證顯示效果。
StaggeredLayoutManager則需要另外一種處理方法。在之前的onCreateViewHolder代碼中可以看到存在setWrapParams方法,這個方法代碼如下
private void setWrapParems(View view){int width = getLayoutManager().canScrollVertically() ? ViewGroup.LayoutParams.MATCH_PARENT : ViewGroup.LayoutParams.WRAP_CONTENT;int height = getLayoutManager().canScrollVertically() ? ViewGroup.LayoutParams.WRAP_CONTENT : ViewGroup.LayoutParams.MATCH_PARENT;if(getLayoutManager() instanceof StaggeredGridLayoutManager) {StaggeredGridLayoutManager.LayoutParams headerParams = new StaggeredGridLayoutManager.LayoutParams(width, height);headerParams.setFullSpan(true);view.setLayoutParams(headerParams);}}
在如果LayoutManager是StaggeredLayoutManager,需要創建一個LayoutParams并設置FullSpan,賦予header或footer的view即可獨占一行。
為了讓header和footer功能適應橫向和豎向,還需要判斷設定的方向后為LayoutParams設置不同的寬和高。
本章的內容基本結束了,這個功能的關鍵點有兩個:一個是對header和footer的管理,包括判斷、創建、加載等環節;另外一個就是實現在GridLayoutManager和StaggeredLayoutManager中獨占一行。
由于我們大量的重寫了很多父類方法,所以一些關聯的其他方法也需要整理邏輯重寫,這里就不細說了,具體可以參考源碼。
這里還有一些問題,包括divider處理,我們在下一篇文章中講解。
源碼
完整源碼請關注公眾號:BennuCTech,發送“WrapRecyclerView”獲取。
?
總結
以上是生活随笔為你收集整理的实现带header和footer功能的RecyclerView的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android App 瘦身总结 第三章
- 下一篇: 实现带header和footer功能的R