集下拉重新整理、自動載入和側滑選單的RecyclerView基本實現原理
目錄
前言
現在這個功能的框架也挺多的了。之所以要寫是因為這個框架是自己親手實現的。說起來有點小激動,這是我正經寫出來的第一個框架。對於”不要重複造輪子”這句話,我一直不是太認同,得從不同的維度看。如果從使用上來看,當然沒必要重複造輪子,白白費時費力不划算。但是如果從個人學習的角度來看的話,重複造輪子不但應該去做,而且很有必要。只會使用輪子對個人的成長幫助不大。你得知道它是怎麼工作的,它為什麼能夠這樣工作,然後更進一步的話,看看我還能不能改進它?而學習輪子效果最好的方法,我認為就是自己再造一個輪子。說白了你來山寨一個,如果可以,就改進它!
正文
專案地址
效果
和常見的側滑以及下拉重新整理效果一樣,見下圖:
側滑選單效果:
下拉重新整理效果:
上拉載入效果:
效果圖就是這樣,基本使用在上面原始碼地址中都有,步驟非常簡便。下面主要想說的,是它實現的基本原理。
1.側滑原理
側滑的主要實現,靠的是一個自定義的佈局容器。專案中類名為:SwipeMenuLayout,繼承FrameLayout.
public class SwipeMenuLayout extends FrameLayout
它有兩個成員:
private View contentView;
private LinearLayout menuView;
一目瞭然,一個是內容,一個是選單。內容自然就是RecyclerView條目中的佈局內容,選單則是自定義的選單佈局。它作為一個容器,包含了這兩個子佈局。
重點是,怎麼讓兩個子佈局歸位到自己的初始位置呢?內容佈局鋪滿整個寬度,選單佈局放在螢幕外邊。簡單看看下面的程式碼:
//init()方法中執行下面三句
LayoutParams contentParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
contentView.setLayoutParams(contentParams);
menuView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
//重寫onLayout()方法
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Log.d(TAG, "contentView.getWidth() = " + contentView.getWidth() + "contentView.getHeight() = " + contentView.getHeight());
super.onLayout(changed, left, top, right, bottom);
int contentViewWidth = contentView.getWidth();
int contentViewHeight = contentView.getHeight();
int menuViewWidth = menuView.getWidth();
if (contentView != null && contentViewWidth != 0 && contentViewHeight != 0) {
contentView.layout(0, 0, contentView.getWidth(), contentView.getHeight());
if (menuView != null) {
menuView.layout(contentViewWidth, 0, contentViewWidth + menuViewWidth, contentViewHeight);
}
}
}
首先對設定進來的內容佈局和選單佈局設定寬高,內容佈局寬度鋪滿整個螢幕。選單佈局的寬度為包裹內容。然後,決定子控制元件的位置就是在onLayout()方法中進行的,所以重寫onLayout()方法,根據內容佈局和選單佈局的寬和高來執行它們的layout()方法。可以看到,內容佈局的寬是鋪滿整個螢幕的,選單佈局的寬度範圍是contentViewWidth到contentViewWidth+menuViewWidth,也就是從內容佈局的寬度終點位置到這個位置加上自己寬度的位置,就剛好在螢幕外面了。
初始化位置搞定之後,就要開始處理它的滑動事件了,要把選單側滑出來,重寫onTouchEvent()方法。這裡需要自己來實現smoothScroll()等功能,細節上要處理控制具體可滑動方向,選單自動開啟、自動關閉等問題,具體實現請參考程式碼。整個SwipeMenuLayout也就兩三百行程式碼,並不複雜。
處理完側滑選單的具體實現之後,就要考慮把它放到RecyclerView裡面去,作為預設的ItemView。當使用者設定他自己的ItemView時,將其作為SwipeMenuLayout的內容佈局,然後加上構造的選單佈局(使用者自定義的),返回SwipeMenuLayout作為新的ItemView,這樣,每一個Item就都具備側滑選單的效果了。
要做以上的事情,不可避免的,需要重寫Adapter,這裡承擔這個角色的是SwipeMenuAdapter,繼承RecyclerView.Adapter。
public abstract class SwipeMenuAdapter<V extends PtrSwipeMenuRecyclerView.ViewHolder> extends RecyclerView.Adapter
細心的同學會發現這裡ViewHolder和預設的不一樣,確實,ViewHolder也重寫了,主要是為了設定選單的點選事件監聽,這裡先不討論它。
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
menuView = createMenuView(parent, viewType);
contentView = createContentView(parent, viewType);
SwipeMenuLayout swipeMenuLayout = new SwipeMenuLayout(parent.getContext(), contentView, menuView);
return onCreateThisViewHolder(swipeMenuLayout, viewType);
}
/**
* 建立item內容的view佈局
*
* @param parent
* @param viewType
* @return
*/
protected abstract View createContentView(ViewGroup parent, int viewType);
/**
* 建立選單view的佈局
*
* @return
*/
protected abstract LinearLayout createMenuView(ViewGroup parent, int viewType);
/**
* 建立ViewHolder
*
* @param contentView 已經在createContentView()中建立好,然後經過再次包裹了側滑選單佈局的itemview
* @param viewType
* @return
*/
public abstract RecyclerView.ViewHolder onCreateThisViewHolder(ViewGroup contentView, int viewType);
這裡有三個抽象方法,createMenuView()建立一個選單佈局,由使用者自己實現,createContentView()建立一個內容佈局,同樣由使用者來實現。onCreateThisViewHolder則是替代了原來的onCreateViewHolder()方法,用來返回一個ViewHolder,但是在這裡返回的ViewHolder,其實已經是item被包裹了SwipeMenuLayout的item了,實現了側滑選單的功能。
到這裡,側滑選單的主幹實現原理就大致說完了。下面看下拉重新整理和自動載入。
2.下拉重新整理及自動載入原理
下拉重新整理效果總體的流程就是:控制touch事件,根據手指滑動動態的改變頭部HeaderView的高度和其內部View的狀態,達到好像控制元件被拉下來觸發重新整理的效果。(當然也有根據手指滑動往下滾動View的實現方法不是這裡用的不去多講)
以前ListView做下拉重新整理的時候,在頂部會增加一個Header作為下拉重新整理頭,而ListView也已經封裝了setHeader()方法,十分方便。但是RecyclerView沒有,所以實現下拉重新整理的第一個任務就是給RecyclerView增加一個HeaderView作為下拉重新整理頭。
增加HeaderView,其實就是在RecyclerView的第0個位置放上自己特定的一個View,用來實現下拉重新整理的效果。首先我們封裝一個HeaderView,相當於一個自定義佈局,方便下拉重新整理效果變化的管理(程式碼略,請參考原始碼)。
要增加HeaderView,又得去重寫Adapter了,好在上面做側滑選單的時候已經重寫了,所以把SwipeMenuAdapter拿出來,繼續新增程式碼。主要是onCreateViewHolder()方法,然後牽涉到getItemCount()和getItemViewType()等方法。由於自動載入更多所新增的FooterView與HeaderView是同樣的原理,所以就一併說吧。先看程式碼:
public class HeaderFooterViewHolder extends RecyclerView.ViewHolder {
public HeaderFooterViewHolder(View itemView) {
super(itemView);
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == HeaderType) {
headerViewHolder = new HeaderFooterViewHolder(new HeaderView(parent.getContext()));
return headerViewHolder;
}
if (viewType == FooterType) {
footerViewHolder = new HeaderFooterViewHolder(new FooterView(parent.getContext()));
if(!footerViewEnable) { //不允許上拉載入更多,隱藏FooterView
FooterView footerView = (FooterView) footerViewHolder.itemView;
footerView.setNowState(FooterView.STATE.HIND);
}
return footerViewHolder;
}
...
...
}
@Override
public int getItemCount() {
//新增Header和Footer的數目
return getThisItemCount() + 2;
}
/**
* 此方法執行RecyclerView.Adapter中getItemCount()的邏輯
*
* @return
*/
public abstract int getThisItemCount();
/**
* 重寫此方法時請注意保留父類方法的邏輯,否則導致header計數混亂,下拉刷新出錯
* 使用position時注意減1(減去header的位置)
*
* @param position
* @return
*/
@Override
public int getItemViewType(int position) {
if (position == 0)
return HeaderType;
if (position == getThisItemCount() + 1)
return FooterType;
return super.getItemViewType(position - 1);//減1去掉herder的位置
}
首先是getItemCount()方法,加上HeaderView和FooterView的位置,也就是在原有的數目上加2,原有的數目由getThisItemCount()獲取,由使用者自己實現。然後在特定的位置返回特定的型別,position為0時,返回HeaderType,position在最後時,返回FooterType。然後,在onCreateViewHolder()中根據viewType返回特定的ViewHolder型別。這樣,就把HeaderView和FooterView都增加進去了。
接下來的步驟就是控制touch事件動態設定HeaderView的高度及控制元件來實現下拉重新整理的效果了。當RecyclerView滑動到頂部時,繼續往下拉觸發下拉重新整理。當滑到底部時,自動觸發載入更多。然後設定好相關的介面回撥,就基本完成。這裡面許多細節,一篇文章很難講完了,基本可以另開新篇。涉及很多基本知識和細節邏輯。大家真的願意瞭解的話。原始碼連結在下方,可以作為參考。