recyclervie刷新到底部_RecyclerView底部刷新实现详解
關于RecyclerView底部刷新實現(xiàn)的文章其實已經(jīng)很多了,但大都只介紹了其基本原理和框架,對其中的很多細節(jié)沒有交代,無法直接使用。本文會著重介紹RecyclerView底部刷新實現(xiàn)的一些細節(jié)處理。
頂部刷新和底部刷新
頂部刷新和底部刷新都是列表中兩種常見的交互方式。頂部刷新通常對應更新數(shù)據(jù),更新的數(shù)據(jù)會替換掉當前數(shù)據(jù)。而底部刷新則對應獲取更多的數(shù)據(jù),更多的數(shù)據(jù)會添加在當前數(shù)據(jù)的后面。
頂部刷新和底部刷新在其他文章中更多的被稱為下拉刷新和上拉加載更多,不過個人并不喜歡這樣的稱謂,每次別人提及上拉和下拉時都會感覺很困擾,要思考一下上拉和下拉究竟對應哪個操作。所以這里將這兩種操作稱為頂部刷新和底部刷新。當然如果讀者沒有這個困擾,覺得很容易區(qū)分上拉和下拉,不煩還是延續(xù)這種稱謂。
本文只會介紹底部刷新,對頂部刷新會在后面的文章中再介紹。
RecyclerView底部刷新的原理
RecyclerView底部刷新的原理很簡單,就是在RecyclerView最底部添加一個表示加載中的View,然后監(jiān)聽RecyclerView的滑動事件,當RecyclerView滑動時,判斷是否滑動到了RecyclerView的底部,也就是最后一個加載中的View是否可見,如果滑動到了RecyclerView底部,則執(zhí)行底部刷新操作,獲取更多數(shù)據(jù)。最后當獲取更多數(shù)據(jù)完成后,更新RecyclerView的Adapter。
RecyclerView底部刷新的一般實現(xiàn)
根據(jù)上述RecyclerView底部刷新的實現(xiàn)原理,可以知道RecyclerView底部刷新實際上包含如下步驟。注意這里的步驟并不代表代碼的書寫順序,它更多的表示的是代碼執(zhí)行的順序。
為RecyclerView底部添加一個表示加載中的View
設置RecyclerView的滑動事件監(jiān)聽,在滑動過程中,根據(jù)底部View是否可見,決定是否執(zhí)行底部刷新操作
執(zhí)行底部刷新時,獲取更多數(shù)據(jù)
獲取完數(shù)據(jù)后,通知Adapter更新RecyclerView
現(xiàn)分別介紹這4個步驟的實現(xiàn)。
在這之前,先限定一個約束條件。我們知道在使用RecyclerView時都需要調用其setLayoutManager()方法設置其LayoutManager,在V7包實現(xiàn)了三種類型的LayoutManager,即LinearLayoutManager,GridLayoutManager和StaggeredGridLayoutManager。這三種類型的LayoutManager在實現(xiàn)底部刷新時會有一些細節(jié)上的差異。為了簡化描述和方便理解,在這里介紹RecyclerView底部刷新的一般實現(xiàn)時,只考慮LinearLayoutManager,對其他兩種類型有差異的地方會在后文單獨說明。
為RecyclerView底部添加一個表示加載中的View
表示加載中的View
這個表示加載中的View通常會使用一個居中顯示的ProgressBar來表示。其布局如下。
android:layout_width="match_parent"
android:layout_height="40dp">
android:layout_gravity="center"
android:layout_width="30dp"
android:layout_height="30dp"/>
不過這并非是強制要求,其具體樣式可以根據(jù)需要自由定義。
后文會將這個表示加載中的View稱為底部刷新View(Bottom Refresh View)。
為RecyclerView添加底部刷新View
為RecyclerView添加底部刷新View一般是通過將底部刷新View作為RecyclerView的Item來實現(xiàn)的。
為此需要改寫RecyclerView Adapter的以下幾個方法。
getItemCount
RecyclerView Adapter的getItemCount方法返回的是item的數(shù)量,既然要將底部刷新View作為RecyclerView的Item添加到RecyclerView中,就需要在原有item數(shù)量基礎上加1。
例如:
@Override
public int getItemCount() {
return mList.size() + 1;
}
getItemViewType
RecyclerView Adapter的getItemViewType方法返回的是item的類型,為了將底部刷新View對應的item和其他item區(qū)分開,需要將底部刷新View作為一個單獨的類型返回。
例如:
@Override
public int getItemViewType(int position) {
if (position < mList.size()) {
return TYPE_NORMAL_ITEM;
} else {
return TYPE_BOTTOM_REFRESH_ITEM;
}
}
onCreateViewHolder
RecyclerView Adapter的onCreateViewHolder方法用來創(chuàng)建ViewHolder。這里首先需要為底部刷新View定義一個ViewHolder,然后根據(jù)item的類型來決定要創(chuàng)建哪個ViewHolder。
例如:
// 定義底部刷新View對應的ViewHolder
private class BottomRefreshViewHolder extends RecyclerView.ViewHolder {
BottomViewHolder(View itemView) {
super(itemView);
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 如果是底部刷新View,則加載底部刷新View布局,并創(chuàng)建底部刷新View對應的ViewHolder
if (viewType == TYPE_BOTTOM_REFRESH_ITEM) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_bottom_refresh_item, parent, false);
return new BottomRefreshViewHolder(view);
}
// 如果是其他類型的View,則按照正常流程創(chuàng)建普通的ViewHolder
else {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_normal_item, parent, false);
return new NormalViewHolder(view);
}
}
onBindViewHolder
RecyclerView Adapter的onBindViewHolder方法用來將ViewHolder和對應的數(shù)據(jù)綁定起來。由于底部刷新View并不需要綁定任何數(shù)據(jù),所以這里不需要對底部刷新ViewHolder做特別的處理,只需要判斷下是否是底部刷新ViewHolder就可以了。
例如:
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (!(holder instanceof BottomRefreshViewHolder)) {
...
}
}
完成了上述步驟之后即為RecyclerView底部添加一個底部刷新View
滑動事件處理
設置滑動事件監(jiān)聽
RecyclerView提供了addOnScrollListener()方法來設置滑動事件監(jiān)聽,只需要將監(jiān)聽滑動事件的RecyclerView.OnScrollListener對象作為參數(shù)傳遞進去即可。
例如:
addOnScrollListener(onScrollListener);
onScrollStateChanged()和onScrolled()
RecyclerView.OnScrollListener是一個抽象類,它包含兩個方法onScrollStateChanged()和onScrolled()。
onScrollStateChanged()方法會在每次滑動狀態(tài)發(fā)生改變時調用。例如,由靜止狀態(tài)變?yōu)榛瑒訝顟B(tài),或者由滑動狀態(tài)變?yōu)殪o止狀態(tài)時,onScrollStateChanged()方法都會被調用。
onScrolled()方法會在RecyclerView滑動時被調用,即使手指離開了屏幕,只要RecyclerView仍然在滑動onScrolled()就會被不斷調用。
理論上來說,我們既可以將判斷底部View是否可見和執(zhí)行底部刷新操作的過程放到onScrollStateChanged()方法中執(zhí)行,也可以將其放到onScrolled()方法中執(zhí)行。但放到不同方法中執(zhí)行在用戶體驗上會產(chǎn)生一些不同。
如果將判斷底部View是否可見和執(zhí)行底部刷新操作的過程放到onScrollStateChanged()方法中執(zhí)行,意味著是以一次滑動過程的最終狀態(tài)來決定是否要執(zhí)行底部刷新。如果在一次滑動過程中間,底部View已經(jīng)可見,但是最終停下來的時候底部View是不可見的,那么將不會執(zhí)行底部刷新操作。
如果將判斷底部View是否可見和執(zhí)行底部刷新操作的過程放到onScrolled()方法中執(zhí)行,意味著只要在一次滑動過程中間底部View可見,那么將會立刻觸發(fā)底部刷新操作。
觀察大部分的APP,都是只要出現(xiàn)底部加載中View,就會開始執(zhí)行底部刷新操作,這也和一般用戶的認知相一致。所以,一般我們都會將判斷底部View是否可見和執(zhí)行底部刷新操作的過程放到onScrolled()方法中執(zhí)行。但是onScrollStateChanged()方法仍然是有用的,有些輔助的邏輯會放到其中來執(zhí)行。具體哪些邏輯需要放到onScrollStateChanged()方法中會在文章后面提到。
判斷底部刷新View是否可見
判斷底部刷新View是否可見是實現(xiàn)RecyclerView底部刷新功能的關鍵。不過幸好它的實現(xiàn)并不復雜。
在LinearLayoutManager中提供了一個方法可以獲取到當前最后一個可見的item在RecyclerView Adapter中的位置,如果這個位置恰好等于RecyclerView Adapter中item的數(shù)量減1,那么就表示底部刷新View已經(jīng)可見了。這也很容易理解,例如RecyclerView Adapter中有55個item,由于Adapter中的位置都是從0開始的,所以這55個item的位置就是從0到54,最后一個item(也就是底部刷新View對應的item)的位置是54。如果當前最后一個可見的item位置為54,那么就表示底部刷新View是可見的。
對LinearLayoutManager,可以調用其findLastVisibleItemPosition()方法來獲取當前最后一個可見的item在RecyclerView Adapter中的位置。
示例代碼如下。
private int getLastVisibleItemPosition() {
RecyclerView.LayoutManager manager = getLayoutManager();
if (manager instanceof LinearLayoutManager) {
return ((LinearLayoutManager) manager).findLastVisibleItemPosition();
}
return NO_POSITION;
}
private boolean isBottomViewVisible() {
int lastVisibleItem = getLastVisibleItemPosition();
return lastVisibleItem != NO_POSITION && lastVisibleItem == getAdapter().getItemCount() - 1;
}
執(zhí)行底部刷新操作
將上述幾個步驟組合在一起就可以得到完整的滑動事件處理過程。示例代碼如下。
RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (isBottomViewVisible()) {
requestMoreData();
}
}
};
addOnScrollListener(onScrollListener);
獲取更多數(shù)據(jù)
獲取數(shù)據(jù)的流程一般都是通過調用約定的接口從服務端獲取數(shù)據(jù),這屬于業(yè)務邏輯,這里不做介紹了。
更新RecyclerView
獲取數(shù)據(jù)一般都是異步過程,在獲取數(shù)據(jù)完成后,調用RecyclerView Adapter的相關方法更新RecyclerView。由于是獲取更多數(shù)據(jù),所以一般可以調用notifyItemInserted()或者notifyItemRangeInserted()來更新RecyclerView。
至此,RecyclerView底部刷新的基本實現(xiàn)就已經(jīng)完成了。
底部刷新功能的封裝
上述底部刷新功能的實現(xiàn),包含了兩部分的修改,一部分是對RecyclerView自身的一些設置,例如設置滑動事件監(jiān)聽,判斷底部刷新View是否可見等。另外一部分是對RecyclerView Adapter的修改,也就是為RecyclerView添加底部刷新View。由于一個app中通常都會有多個界面需要實現(xiàn)底部刷新功能,如果每個要實現(xiàn)底部刷新功能的界面都這樣實現(xiàn)一遍,實在是太麻煩,也會使原本的代碼變得復雜和臃腫。因此,需要將上述底部刷新功能的實現(xiàn)封裝在一起。
對第一部分RecyclerView自身的一些設置,可以很容易的通過繼承RecyclerView來實現(xiàn)封裝,然后在代碼和xml中使用這個繼承之后的RecyclerView即可。對第二部分RecyclerView Adapter的修改要麻煩一些,由于不同的列表都需要定義單獨的Adapter,在這些Adapter中都需要重寫getItemCount(),getItemViewType()這些方法。所以不能簡單的通過繼承RecyclerView Adapter,然后各個列表的Adapter再繼承自這個修改后的Adapter來解決。為了實現(xiàn)Adapter的封裝,需要實現(xiàn)一個內部的Adapter,然后用這個內部的Adapter包裹外部列表的Adapter來實現(xiàn)。
現(xiàn)分別對這兩部分的封裝過程進行介紹。
RecyclerView的封裝
對RecyclerView的封裝只需要實現(xiàn)一個類繼承自RecyclerView,將底部刷新功能對RecyclerView的修改放到這個類中即可。
示例代碼如下。
public class XRecyclerView extends RecyclerView {
private OnBottomRefreshListener mBottomRefreshListener;
private RecyclerView.OnScrollListener mOnScrollListener;
private boolean mBottomRefreshable;
public XRecyclerView(Context context) {
super(context);
init();
}
public XRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mBottomRefreshListener = null;
mBottomRefreshable = false;
mOnScrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (isBottomViewVisible()) {
if (mBottomRefreshListener != null) {
mBottomRefreshListener.onBottomRefresh();
}
}
}
};
}
private int getLastVisibleItemPosition() {
RecyclerView.LayoutManager manager = getLayoutManager();
if (manager instanceof LinearLayoutManager) {
return ((LinearLayoutManager) manager).findLastVisibleItemPosition();
}
return NO_POSITION;
}
private boolean isBottomViewVisible() {
int lastVisibleItem = getLastVisibleItemPosition();
return lastVisibleItem != NO_POSITION && lastVisibleItem == getAdapter().getItemCount() - 1;
}
public boolean isBottomRefreshable() {
return mBottomRefreshable;
}
// 設置底部下拉刷新監(jiān)聽
public void setOnBottomRefreshListener(OnBottomRefreshListener listener) {
mBottomRefreshListener = listener;
if (mBottomRefreshListener != null) {
addOnScrollListener(mOnScrollListener);
mBottomRefreshable = true;
} else {
removeOnScrollListener(mOnScrollListener);
mBottomRefreshable = false;
}
}
public interface OnBottomRefreshListener {
void onBottomRefresh();
}
}
這里的代碼和上面介紹RecyclerView底部刷新的一般實現(xiàn)時的示例代碼基本一致。不同的是增加了一個OnBottomRefreshListener的接口類,setOnBottomRefreshListener()方法用來設置底部刷新事件的監(jiān)聽。當需要執(zhí)行底部刷新時,調用OnBottomRefreshListener的onBottomRefresh()方法,通知外部更新數(shù)據(jù)。此外,將設置滑動事件監(jiān)聽也放到setOnBottomRefreshListener()方法中,只有設置了底部下拉刷新監(jiān)聽,才需要添加滑動事件監(jiān)聽,不然監(jiān)聽滑動事件是沒有意義的。在setOnBottomRefreshListener()方法中還允許通過傳入一個null對象,表示取消對底部下拉刷新的監(jiān)聽。mBottomRefreshable表示底部是否可以刷新,它會后面Adapter中用到。
RecyclerView Adapter的封裝
對RecyclerView Adapter的封裝需要在繼承的RecyclerView類再實現(xiàn)一個包裹的Adapter(WrapperAdapter),并重新RecyclerView的setAdapter()方法,當在外部調用setAdapter()時會用WrapperAdapter包裹外部Adapter。
示例代碼如下。
@Override
public void setAdapter(RecyclerView.Adapter adapter) {
if (adapter != null) {
WrapperAdapter wrapperAdapter = new WrapperAdapter(adapter);
super.setAdapter(wrapperAdapter);
}
}
private class WrapperAdapter extends RecyclerView.Adapter {
private static final int TYPE_BOTTOM_REFRESH_ITEM = Integer.MIN_VALUE + 1;
/**
* 被包裹的外部Adapter
*/
private RecyclerView.Adapter mInnerAdapter;
private RecyclerView.AdapterDataObserver dataObserver = new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
super.onChanged();
notifyDataSetChanged();
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
super.onItemRangeChanged(positionStart, itemCount);
notifyItemRangeChanged(positionStart, itemCount);
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
notifyItemRangeInserted(positionStart, itemCount);
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
super.onItemRangeRemoved(positionStart, itemCount);
notifyItemRangeRemoved(positionStart, itemCount);
}
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
super.onItemRangeMoved(fromPosition, toPosition, itemCount);
notifyItemRangeChanged(fromPosition, toPosition + itemCount);
}
};
private WrapperAdapter(@NonNull RecyclerView.Adapter adapter) {
if (mInnerAdapter != null) {
notifyItemRangeRemoved(0, mInnerAdapter.getItemCount());
mInnerAdapter.unregisterAdapterDataObserver(dataObserver);
}
this.mInnerAdapter = adapter;
mInnerAdapter.registerAdapterDataObserver(dataObserver);
notifyItemRangeInserted(0, mInnerAdapter.getItemCount());
}
public boolean isLoadMoreView(int position) {
return isBottomRefreshable() && position == getItemCount() - 1;
}
@Override
public int getItemCount() {
if (mInnerAdapter != null) {
int itemCount = mInnerAdapter.getItemCount();
if (isBottomRefreshable()) {
return itemCount + 1;
} else {
return itemCount;
}
} else {
return 0;
}
}
@Override
public int getItemViewType(int position) {
if (isBottomRefreshable()) {
if (mInnerAdapter != null) {
int adapterCount = mInnerAdapter.getItemCount();
if (position < adapterCount) {
return mInnerAdapter.getItemViewType(position);
}
}
return TYPE_BOTTOM_REFRESH_ITEM;
} else {
if (mInnerAdapter != null) {
return mInnerAdapter.getItemViewType(position);
} else {
return super.getItemViewType(position);
}
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_BOTTOM_REFRESH_ITEM) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_bottom_refresh_item, parent, false);
return new BottomRefreshViewHolder(view);
} else {
return mInnerAdapter.onCreateViewHolder(parent, viewType);
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (!(holder instanceof BottomRefreshViewHolder)) {
if (mInnerAdapter != null) {
int adapterCount = mInnerAdapter.getItemCount();
if (position
mInnerAdapter.onBindViewHolder(holder, position);
}
}
}
}
}
/**
* bottom refresh View對應的ViewHolder
*/
private class BottomRefreshViewHolder extends RecyclerView.ViewHolder {
BottomRefreshViewHolder(View itemView) {
super(itemView);
}
}
對這里的代碼再稍作說明。
底部刷新View類型對應的整數(shù)值不能和外部Adapter已有的類型值重復,由于無法事先知道外部Adapter會定義哪些類型,所以這里為底部刷新View定義了一個相對來說比較特殊的數(shù)值Integer.MIN_VALUE + 1,但如果在實際使用時,發(fā)現(xiàn)外部Adapter也定義了這種類型,則需要修改這個數(shù)值,或者修改外部Adapter的定義。
由于外部數(shù)據(jù)更新時都是調用外部Adapter的notify…方法來通知RecyclerView更新,所以這里定義了dataObserver對象,當外部Adapter的notify…方法被調用時,調用包裹的Adapter的notify…方法。
這里使用了RecyclerView封裝時定義的isBottomRefreshable()方法,來判斷是否需要添加底部刷新View,也就是說,如果外部沒有調用setOnBottomRefreshListener()設置底部刷新監(jiān)聽,則isBottomRefreshable()將返回false,表示不需要執(zhí)行底部刷新操作。因此也就不需要添加底部加載View。
支持其他LayoutManager
如前所述,GridLayoutManager和StaggeredGridLayoutManager在實現(xiàn)底部刷新時會和LinearLayoutManager存在一些差異。這里介紹如何支持GridLayoutManager和StaggeredGridLayoutManager的底部刷新功能。
支持GridLayoutManager
GridLayoutManager是網(wǎng)格布局,允許在一行中有多列,每一列都是一個item。底部刷新View也是一個作為一個item添加到Adapter中,按照之前LinearLayoutManager的實現(xiàn),它會被安排在最后一行的某一列上,不能占據(jù)整行。如圖所示。
為了讓底部刷新View能夠占據(jù)整行,需要對GridLayoutManager對象進行設置。GridLayoutManager提供了setSpanSizeLookup()方法,在這里可以設置某個位置的item跨越多列。我們只需要將底部刷新View跨越列數(shù)設置為GridLayoutManager的列數(shù)即可。
為此,需要重寫Adapter的onAttachedToRecyclerView()方法。增加如下代碼。
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
// 對Grid布局進行支持
RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
if (manager instanceof GridLayoutManager) {
final GridLayoutManager gridLayoutManager = (GridLayoutManager) manager;
final GridLayoutManager.SpanSizeLookup lookup = gridLayoutManager.getSpanSizeLookup();
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
return isLoadMoreView(position) ? gridLayoutManager.getSpanCount() : lookup.getSpanSize(position) ;
}
});
}
}
支持StaggeredGridLayoutManager
StaggeredGridLayoutManager是瀑布流布局,它同樣允許在一行中有多列,因此和GridLayoutManager一樣,需要將底部刷新View設置為跨越多列。不過StaggeredGridLayoutManager并沒有setSpanSizeLookup方法,如果要設置某個item跨越整行,需要調用item的LayoutParams的setFullSpan()方法。
為此,需要重寫Adapter的onViewAttachedToWindow()方法,增加如下代碼。
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
super.onViewAttachedToWindow(holder);
if (holder instanceof BottomRefreshViewHolder) {
// 支持瀑布流布局
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if (lp instanceof StaggeredGridLayoutManager.LayoutParams) {
((StaggeredGridLayoutManager.LayoutParams) lp).setFullSpan(true);
}
}
}
對StaggeredGridLayoutManager,完成上述代碼后,還需要做另外一處修改。
在先前介紹判斷底部刷新View是否可見時,是通過LinearLayoutManager的findLastVisibleItemPosition()方法獲取當前最后一個可見的item位置來判斷的。對StaggeredGridLayoutManager,并沒有findLastVisibleItemPosition()方法,但StaggeredGridLayoutManager有一個findLastVisibleItemPositions()方法,它可以用來獲取每一列的最后一個可見item的位置,通過將所有列最后一個可見item位置取最大值就可以得取到當前最后一個可見的item位置。
代碼如下。
private int getLastVisibleItemPosition() {
RecyclerView.LayoutManager manager = getLayoutManager();
if (manager instanceof LinearLayoutManager) {
return ((LinearLayoutManager) manager).findLastVisibleItemPosition();
} else if (manager instanceof StaggeredGridLayoutManager) {
int positions[] = ((StaggeredGridLayoutManager) manager).findLastVisibleItemPositions(null);
int max = NO_POSITION;
for (int pos : positions) {
if (pos > max) {
max = pos;
}
}
return max;
}
return NO_POSITION;
}
注意到,這里面只有LinearLayoutManager和StaggeredGridLayoutManager兩個分支,并沒有GridLayoutManager。這是因為GridLayoutManager是繼承自LinearLayoutManager的,所以它包含在了LinearLayoutManager這個分支中,對GridLayoutManager不需要在這里特別處理了。有意思的是StaggeredGridLayoutManager雖然名字中包含了GridLayoutManager,但卻并非繼承自GridLayoutManager,也沒有繼承自LinearLayoutManager,所以對StaggeredGridLayoutManager是需要單獨處理的。
支持自定義的LayoutManager
對自定義的LayoutManager,如果是繼承自LinearLayoutManager,GridLayoutManager或StaggeredGridLayoutManager,則只需要按照上述說明,實現(xiàn)對GridLayoutManager和StaggeredGridLayoutManager的支持就可以了。如果自定義的LayoutManager是直接繼承自RecyclerView.LayoutManager,則需要在自定義的LayoutManager中實現(xiàn)獲取當前可見的最后一個item位置的方法,此外,如果自定義的LayoutManager支持一行放置多個item,還需要實現(xiàn)能夠將某個item設置為跨越整行的方法。然后參照上述對GridLayoutManager和StaggeredGridLayoutManager的處理,在包裹的Adapter中添加相應的分支即可。
一次滑動過程中避免重復刷新
在設置滑動事件監(jiān)聽時,我們重寫了RecyclerView.OnScrollListener對象的onScrolled()方法,在這里判斷底部刷新View是否可見,如果底部刷新View可見,則執(zhí)行底部刷新操作。
這里存在一個問題,設想手指正在屏幕上向上滑動,此時底部刷新View變得可見,于是觸發(fā)底部刷新操作。但是手指并未立刻離開屏幕,而是繼續(xù)向下滑動,這時onScrolled()方法仍然會被連續(xù)調用,由于底部刷新View仍然可見,所以底部刷新操作也會被不斷的觸發(fā)。再設想另外一種情況,用戶觸發(fā)底部刷新操作后手指立刻離開屏幕,但由于獲取數(shù)據(jù)通常都是異步的過程,從觸發(fā)底部刷新到獲取到數(shù)據(jù)是需要一定時間的,如果在這段時間內,用戶又重新滑動了列表,并使得底部刷新View可見,這時底部刷新操作又會被再一次調用。需要注意的是,這兩種情況雖然都是重復觸發(fā)了底部刷新操作,但是存在一定差異。第一種情況,觸發(fā)底部刷新操作后手指未離開屏幕,假設這時手指能夠完全靜止的停留在屏幕上,不會觸發(fā)新的底部刷新操作。等待一會之后外部完成了數(shù)據(jù)獲取,并更新了RecyclerView,這時手指繼續(xù)滑動,使得底部刷新View又變得可見,這時仍然會觸發(fā)底部刷新操作,所以第二種情況并不能夠涵蓋第一種情況。
一般來說,我們并不希望在一次滑動過程中,觸發(fā)多次底部刷新操作,也不希望在獲取到數(shù)據(jù)前,再次執(zhí)行底部刷新操作。為了避免重復刷新,需要在onScrolled()中增加一些額外的處理,同時還需要利用到RecyclerView.OnScrollListener的另外一個方法onScrollStateChanged()。
示例代碼如下。注意這里只是列出了相關修改的地方,之前已經(jīng)提到的部分就省略了,并非完整的代碼。
public class XRecyclerView extends RecyclerView {
private boolean mBottomRefreshing;
private void init() {
mBottomRefreshing = false;
mOnScrollListener = new RecyclerView.OnScrollListener() {
private boolean mAlreadyRefreshed = false;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
if (mAlreadyRefreshed) {
mAlreadyRefreshed = false;
}
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (isBottomRefreshing() || mAlreadyRefreshed) {
return;
}
if (isBottomViewVisible()) {
if (mBottomRefreshListener != null) {
mBottomRefreshListener.onBottomRefresh();
mBottomRefreshing = true;
mAlreadyRefreshed = true;
}
}
}
};
}
public boolean isBottomRefreshing() {
return mBottomRefreshing;
}
public void onBottomRefreshComplete() {
mBottomRefreshing = false;
}
}
在這段代碼中主要是增加了兩個變量,mBottomRefreshing和mAlreadyRefreshed。
mBottomRefreshing變量增加在繼承的RecyclerView中,表示是否正在刷新。它初始值為false,在onScroll中,如果觸發(fā)了一次刷新操作,則將其置為true。之后只有在外部調用了onBottomRefreshComplete()才會再次置為false。在onScroll中,觸發(fā)刷新操作之前增加了判斷,如果它為true,則不會再執(zhí)行刷新操作。這就杜絕了上述第二種情況下的重復刷新,在獲取數(shù)據(jù)完成之前,是不能夠再次觸發(fā)刷新操作的。但是同時要求外部在獲取數(shù)據(jù)完成后,調用這里的onBottomRefreshComplete(),通知RecyclerView數(shù)據(jù)已經(jīng)獲取好了。
mAlreadyRefreshed變量增加在實現(xiàn)RecyclerView.OnScrollListener接口的匿名內部類中,它表示本次滑動過程中是否已經(jīng)執(zhí)行過一次底部刷新操作了。如果觸發(fā)了一次刷新操作,則將其置為true。在onScrollStateChanged()中,如果滑動狀態(tài)變?yōu)橥V够瑒?#xff0c;且mAlreadyRefreshed為true,則將其重置為false。在onScroll中,觸發(fā)刷新操作之前判斷mAlreadyRefreshed是否為true,如果為true,則不執(zhí)行刷新操作。這樣就可以避免在一次滑動過程中重復執(zhí)行底部刷新操作。
底部刷新觸發(fā)過于敏感問題
在onScroll中,只要底部刷新View一旦可見就會觸發(fā)底部刷新操作,哪怕只是可見一個像素也會立刻觸發(fā)。個人覺得這種觸發(fā)機制過于敏感,用戶體驗不是很好,尤其是現(xiàn)在用戶很多都使用wifi環(huán)境來上網(wǎng),網(wǎng)速都很快,一旦觸發(fā)底部刷新,很快就能獲取到新的數(shù)據(jù),然后RecyclerView被更新,導致底部刷新View很難被用戶觀察到。如果在onScroll中執(zhí)行底部刷新操作之前增加判斷,只有底部刷新View可見到一定程度后,例如有一半可見時才會觸發(fā)底部刷新操作,這樣就可以避免這種問題。不過這個問題并不會影響到整個功能,如果覺得對用戶體驗影響不大,完全可以忽略問題。
示例代碼如下。同樣的,這里只是列出了相關修改的地方。
private void init() {
mOnScrollListener = new RecyclerView.OnScrollListener() {
private int mBottomViewVisibleDy = 0;
private int mBottomViewHeight = 0;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
boolean shouldHide = false; // 是否需要隱藏bottom view
// 如果之前還沒有滑到指定的位置就停止了滑動,則將shouldHide置為true
if (mBottomViewVisibleDy != 0) {
if (mBottomViewVisibleDy > 0) {
shouldHide = true;
}
mBottomViewVisibleDy = 0;
}
// 隱藏bottom view
if (shouldHide) {
hideBottomView();
}
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (isBottomViewVisible()) {
// dy是本次調用onScrolled和上次調用onScrolled在y軸方向的偏移量,這里將bottom view可見之后的偏移量累加起來
mBottomViewVisibleDy += dy;
if (mBottomViewHeight == 0) {
View itemView = getLastVisibleItem();
if (itemView != null) {
mBottomViewHeight = itemView.getHeight();
}
}
// 如果bottom view可見之后的y軸偏移量大于bottom view高度的一半,則執(zhí)行bottom refresh
if (mBottomViewHeight != 0 && mBottomViewVisibleDy > mBottomViewHeight / 2) {
if (mBottomRefreshListener != null) {
mBottomRefreshListener.onBottomRefresh();
mBottomRefreshing = true;
mAlreadyRefreshed = true;
}
mBottomViewVisibleDy = 0;
}
} else {
mBottomViewVisibleDy = 0;
}
}
};
}
private View getLastVisibleItem() {
int firstItemPosition = getFirstVisibleItemPosition();
int lastItemPosition = getLastVisibleItemPosition();
if (firstItemPosition != NO_POSITION && lastItemPosition != NO_POSITION) {
return getLayoutManager().getChildAt(lastItemPosition - firstItemPosition);
} else {
return null;
}
}
private int getFirstVisibleItemPosition() {
RecyclerView.LayoutManager manager = getLayoutManager();
if (manager instanceof LinearLayoutManager) {
return ((LinearLayoutManager) manager).findFirstVisibleItemPosition();
} else if (manager instanceof StaggeredGridLayoutManager) {
int positions[] = ((StaggeredGridLayoutManager) manager).findFirstVisibleItemPositions(null);
int min = Integer.MAX_VALUE;
for (int pos : positions) {
if (pos < min) {
min = pos;
}
}
return min;
}
return NO_POSITION;
}
// 如果bottom view是可見的,則根據(jù)bottom view 當前的位置和RecyclerView當前位置來決定要向上滑動的距離
private void hideBottomView() {
if (isBottomViewVisible()) {
View bottomView = getLastVisibleItem();
if (bottomView != null) {
int[] bottomViewLocation = new int[2];
bottomView.getLocationInWindow(bottomViewLocation);
int[] recyclerViewLocation = new int[2];
getLocationInWindow(recyclerViewLocation);
int recyclerViewHeight = getHeight();
int offset = recyclerViewLocation[1] + recyclerViewHeight - bottomViewLocation[1];
if (offset > 0) {
scrollBy(0, -offset);
}
}
}
}
在這段代碼中,增加了mBottomViewVisibleDy變量,表示底部刷新View可見的高度大小,此外,增加了getLastVisibleItem()方法,用來獲取最后一個可見的item,它主要是用來得到底部刷新View的高度。還增加了hideBottomView()方法,它用來隱藏底部刷新View,因為在onScroll中增加了這層判斷后,只有底部刷新View可見到一定程度(這里是底部刷新View高度的一半)時才會執(zhí)行底部刷新操作,如果沒有滑到一半就停下來了,那么就需要手動將這顯示出來一小半的底部刷新View隱藏起來。
獲取到數(shù)據(jù)后的處理
如前所述,在獲取到數(shù)據(jù)后,需要外部調用onBottomRefreshComplete(),通知RecyclerView數(shù)據(jù)已經(jīng)獲取好了。在onBottomRefreshComplete()會將mBottomRefreshing置為false。但是僅僅這樣處理是不夠的。這里還需要做一個額外的操作,就是將底部刷新View隱藏起來。
當外部調用onBottomRefreshComplete()時,即表明本次刷新操作已經(jīng)完成,這時就不應當再讓用戶看到這個底部刷新View。然而可能存在一些原因導致這時用戶仍然能夠看到底部刷新View,這可能是因為獲取到的數(shù)據(jù)量太少,RecyclerView填充新的數(shù)據(jù)后,也無法將底部刷新View擠到看不見的地方,也可能是因為外部在獲取數(shù)據(jù)后增加了去重的操作,新獲取的數(shù)據(jù)全部都在已經(jīng)獲取的數(shù)據(jù)里面,導致數(shù)據(jù)項沒有發(fā)生變化。無論是哪種情況都不應當再讓用戶看到底部刷新View,因此需要在onBottomRefreshComplete()中將底部刷新View隱藏起來。
示例代碼如下。
private boolean mShouldHideAfterScrollIdle;
private void init() {
mOnScrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
boolean shouldHide = false; // 是否需要隱藏bottom view
// 如果需要隱藏bottom view,則將shouldHide置為true
if (mShouldHideAfterScrollIdle) {
shouldHide = true;
mShouldHideAfterScrollIdle = false;
}
// 隱藏bottom view
if (shouldHide) {
hideBottomView();
}
}
}
};
}
public void onBottomRefreshComplete() {
mBottomRefreshing = false;
// 如果當前沒有在滑動狀態(tài),則直接隱藏
// 如果當前在滑動狀態(tài),則等待滑動停止后再隱藏
if (getScrollState() == SCROLL_STATE_IDLE) {
hideBottomView();
mShouldHideAfterScrollIdle = false;
} else {
mShouldHideAfterScrollIdle = true;
}
}
這里借用了之前的hideBottomView()方法來隱藏底部刷新View,同時還需要和滑動狀態(tài)和onScrollStateChanged()方法相結合。
獲取數(shù)據(jù)失敗后的處理
由于獲取數(shù)據(jù)通常都是聯(lián)網(wǎng)獲取,聯(lián)網(wǎng)獲取總是會有獲取失敗的可能。如果數(shù)據(jù)獲取失敗,那么需要做相應的處理。
一般來說有兩種處理方式。
1. 外部得知數(shù)據(jù)獲取失敗后,顯示出錯信息。調用onBottomRefreshComplete(),通知RecyclerView刷新完成,RecyclerView內部仍然是之前的處理流程,將mBottomRefreshing置為false,然后隱藏底部刷新View。
2. RecyclerView增加一個方法onBottomRefreshFailed()。外部得知數(shù)據(jù)獲取失敗后,調用onBottomRefreshFailed(),在onBottomRefreshFailed()方法中,將mBottomRefreshing置為false,同時在底部刷新View中顯示加載出錯的信息。
采用方法2需要對底部刷新View進行改造,將其設置為一個TextView和ProgressBar的組合。平時顯示ProgressBar,隱藏TextView,在獲取數(shù)據(jù)失敗后,改變其狀態(tài),隱藏ProgressBar,顯示TextView。將TextView文本設置為“加載數(shù)據(jù)失敗”之類的信息。同時,原先的滑動到底部刷新View執(zhí)行刷新的機制也需要修改,改為點擊后刷新。
對方法2雖然需要的改動較多,但總體思路是很明確的。這里就不給出實現(xiàn)代碼了。
全部數(shù)據(jù)獲取完成的處理
獲取數(shù)據(jù)的結果在獲取成功時還存在另一種情況特殊情況,就是已經(jīng)獲取到了全部的數(shù)據(jù),后面已經(jīng)沒有更多數(shù)據(jù)了。
為了處理這種情況,可以在RecyclerView中增加setBottomRefreshable()方法,當數(shù)據(jù)全部獲取完成后,調用此方法,通知RecyclerView已經(jīng)不能夠再刷新了。對RecyclerView來說,有兩種處理方案。
隱藏底部刷新View。這只需要將mBottomRefreshable置為false即可,由于在Adapter有判斷此變量的狀態(tài)(isBottomRefreshable()),如果mBottomRefreshable為false,則不會添加底部刷新View。
底部刷新View中顯示沒有更多的信息。
對方法2,同樣需要對底部刷新View進行改造,改造方式和之前介紹獲取數(shù)據(jù)失敗后的處理方式2一樣,將其設置為一個TextView和ProgressBar的組合。當RecyclerView得知數(shù)據(jù)已經(jīng)全部獲取完畢后,隱藏ProgressBar,顯示TextView。將TextView文本設置為“沒有更多了”之類的信息。同時,去掉原先的滑動到底部刷新View執(zhí)行刷新的機制。很多時候數(shù)據(jù)是在不斷變化的,數(shù)據(jù)已經(jīng)全部獲取完畢只是表示當前的狀態(tài),可能過一會之后就會有新的數(shù)據(jù)產(chǎn)生,這時可以將底部刷新View置為點擊后刷新。如果確實不會產(chǎn)生新的數(shù)據(jù)了,也可以不設置點擊刷新操作。
當前數(shù)據(jù)不足一屏的處理
考慮這樣一種情況,當填充完數(shù)據(jù)后,當前RecyclerView顯示不足一屏,由于底部刷新View是作為最后一個item添加的,所以這時底部刷新View將會直接可見。同時由于RecyclerView不足一屏,所以它不能滑動,onScroll也就不會被執(zhí)行,數(shù)據(jù)更新操作也就無法觸發(fā)。
要解決這個問題,通常也有兩種方案。
1. 外部第一次獲取數(shù)據(jù)時,盡量多獲取一些數(shù)據(jù)。確保RecyclerView可以填滿一屏。雖然這個方案看起來是什么也沒錯,將問題解決責任推給外部實現(xiàn)。但實際上大多數(shù)情況,都可以采用這種方式。一般來說,即使要顯示的item是非常簡單的,例如只有一個icon和一行文本,也只需要十多個就可以填滿一屏。外部獲取數(shù)據(jù)時完全可以將一次獲取數(shù)量設置為20或者更多,這樣就可以保證RecyclerView填滿一屏。而一次獲取20個數(shù)據(jù)大多數(shù)情況完全不會造成負擔。所以將數(shù)據(jù)不足一屏的問題推給外部,是完全合理的。
2. 同樣是改造底部刷新View,將其設置為一個TextView和ProgressBar的組合。根據(jù)RecyclerView是否可以滑動,決定是顯示TextView還是ProgressBar。如果RecyclerView超過一屏,則顯示ProgressBar,設置滑動觸發(fā)刷新,如果RecyclerView不足一屏,則顯示TextView,設置點擊觸發(fā)刷新。
要判斷RecyclerView是否超過一屏,只需要判斷其是否可以滑動即可,RecyclerView提供了一個computeVerticalScrollOffset()方法,如果它返回0,則表示RecyclerView不能滑動,也就是不足一屏。如果返回值大于0,則表示超過一屏。
支持水平方向的RecyclerView
對水平方向滑動的RecyclerView,上述底部刷新實現(xiàn)的原理和細節(jié)處理都是完全一樣的,只需要將底部刷新View的布局調整為豎直的,然后將一些和方向有關的接口更改一下即可。例如mBottomViewVisibleDy需要改為mBottomViewVisibleDx,getHeight()改成getWidth()等。這里就不多介紹了。
完整代碼
最后將整個實現(xiàn)的完整代碼貼在這里。不過這里底部刷新View仍然是ProgressBar,沒有改造成ProgressBar和TextView的組合。也就是說,對“獲取數(shù)據(jù)失敗后的處理”,“全部數(shù)據(jù)獲取完成的處理”和“當前數(shù)據(jù)不足一屏的處理”都是采用的方法1。如果需要采用方法2的,可以自行實現(xiàn)。建議將底部刷新View采用自定義View實現(xiàn),將相關狀態(tài)封裝在自定義View。此外,這里只支持垂直方向滑動的RecyclerView,對水平方向滑動的RecyclerView,由于沒有用到,所以沒有實現(xiàn)。
public class XRecyclerView extends RecyclerView {
private OnBottomRefreshListener mBottomRefreshListener;
private RecyclerView.OnScrollListener mOnScrollListener;
private boolean mBottomRefreshing;
private boolean mBottomRefreshable;
private boolean mShouldHideAfterScrollIdle;
public XRecyclerView(Context context) {
super(context);
init();
}
public XRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mBottomRefreshing = false;
mBottomRefreshable = false;
mShouldHideAfterScrollIdle = false;
mBottomRefreshListener = null;
mOnScrollListener = new RecyclerView.OnScrollListener() {
// 此變量為true表示的意思是此輪滑動過程已經(jīng)執(zhí)行過一次bottom refresh了
// 設想在一次滑動過程中,如果已經(jīng)執(zhí)行過一次bottom refresh,這時手指不離開屏幕,
// 接著收到bottom refresh結果,將bottom refresh狀態(tài)置為false,這時如果仍然在滑動,即使bottom view又變得可見了,也不應當再次執(zhí)行bottom refresh
private boolean mAlreadyRefreshed = false;
private int mBottomViewVisibleDy = 0;
private int mBottomViewHeight = 0;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
boolean shouldHide = false; // 是否需要隱藏bottom view
// 如果需要隱藏bottom view,則將shouldHide置為true
if (mShouldHideAfterScrollIdle) {
shouldHide = true;
mShouldHideAfterScrollIdle = false;
}
// 如果之前還沒有滑到指定的位置就停止了滑動,則同樣將shouldHide置為true
if (mBottomViewVisibleDy != 0) {
if (mBottomViewVisibleDy > 0) {
shouldHide = true;
}
mBottomViewVisibleDy = 0;
}
// 隱藏bottom view
if (shouldHide) {
hideBottomView();
}
if (mAlreadyRefreshed) {
mAlreadyRefreshed = false;
}
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
// 如果當前不可刷新,或者正在刷新,則不執(zhí)行bottom refresh操作
if (!isBottomRefreshable() || isBottomRefreshing() || mAlreadyRefreshed || dy == 0) {
return;
}
if (isBottomViewVisible()) {
// dy是本次調用onScrolled和上次調用onScrolled在y軸方向的偏移量,這里將bottom view可見之后的偏移量累加起來
mBottomViewVisibleDy += dy;
if (mBottomViewHeight == 0) {
View itemView = getLastVisibleItem();
if (itemView != null) {
mBottomViewHeight = itemView.getHeight();
}
}
// 如果bottom view可見之后的y軸偏移量大于bottom view高度的一半,則執(zhí)行bottom refresh
if (mBottomViewHeight != 0 && mBottomViewVisibleDy > mBottomViewHeight / 2) {
if (mBottomRefreshListener != null) {
mBottomRefreshListener.onBottomRefresh();
mBottomRefreshing = true;
mAlreadyRefreshed = true;
}
mBottomViewVisibleDy = 0;
}
} else {
mBottomViewVisibleDy = 0;
}
}
};
}
public View getFirstVisibleItem() {
return getLayoutManager().getChildAt(0);
}
public View getSecondVisibleItem() {
return getLayoutManager().getChildAt(1);
}
private View getLastVisibleItem() {
int firstItemPosition = getFirstVisibleItemPosition();
int lastItemPosition = getLastVisibleItemPosition();
if (firstItemPosition != NO_POSITION && lastItemPosition != NO_POSITION) {
return getLayoutManager().getChildAt(lastItemPosition - firstItemPosition);
} else {
return null;
}
}
private int getFirstVisibleItemPosition() {
RecyclerView.LayoutManager manager = getLayoutManager();
if (manager instanceof LinearLayoutManager) {
return ((LinearLayoutManager) manager).findFirstVisibleItemPosition();
} else if (manager instanceof StaggeredGridLayoutManager) {
int positions[] = ((StaggeredGridLayoutManager) manager).findFirstVisibleItemPositions(null);
int min = Integer.MAX_VALUE;
for (int pos : positions) {
if (pos < min) {
min = pos;
}
}
return min;
}
return NO_POSITION;
}
private int getLastVisibleItemPosition() {
RecyclerView.LayoutManager manager = getLayoutManager();
if (manager instanceof LinearLayoutManager) {
return ((LinearLayoutManager) manager).findLastVisibleItemPosition();
} else if (manager instanceof StaggeredGridLayoutManager) {
int positions[] = ((StaggeredGridLayoutManager) manager).findLastVisibleItemPositions(null);
int max = NO_POSITION;
for (int pos : positions) {
if (pos > max) {
max = pos;
}
}
return max;
}
return NO_POSITION;
}
private boolean isBottomViewVisible() {
int lastVisibleItem = getLastVisibleItemPosition();
return lastVisibleItem != NO_POSITION && lastVisibleItem == getAdapter().getItemCount() - 1;
}
// 滑到頂部
public void gotoTop() {
smoothScrollToPosition(0);
}
// 設置為沒有數(shù)據(jù)了
public void setBottomRefreshable(boolean refreshable) {
mBottomRefreshable = refreshable;
getAdapter().notifyDataSetChanged();
}
public boolean isBottomRefreshable() {
return mBottomRefreshable;
}
@Override
public void setAdapter(RecyclerView.Adapter adapter) {
if (adapter != null) {
WrapperAdapter wrapperAdapter = new WrapperAdapter(adapter);
super.setAdapter(wrapperAdapter);
}
}
// 設置底部下拉刷新監(jiān)聽
public void setOnBottomRefreshListener(OnBottomRefreshListener listener) {
mBottomRefreshListener = listener;
if (mBottomRefreshListener != null) {
addOnScrollListener(mOnScrollListener);
mBottomRefreshable = true;
} else {
removeOnScrollListener(mOnScrollListener);
mBottomRefreshable = false;
}
}
// 當前是否正在bottom refreshing
public boolean isBottomRefreshing() {
return mBottomRefreshing;
}
// 下拉刷新完成之后需要隱藏bottom view
// 一般來說,如果下拉刷新完成后獲取到了更多的數(shù)據(jù),這時填充的數(shù)據(jù)會自動將bottom view擠到后面看不見的位置
// 但是,如果下拉刷新沒有獲取到更多的數(shù)據(jù),這可能是已經(jīng)恰好沒有任何數(shù)據(jù)了,但也可能是接口返回錯誤(例如服務器連接不上,網(wǎng)絡問題等)
// 如果是第一種情況,則會通過setBottomRefreshable(false)取消bottom view
// 但如果是第二種情況,由于沒有更多數(shù)據(jù)的填充,也不能取消bottom view,這時bottom view就會一直顯示在這里,所以就需要手動將其隱藏起來
public void onBottomRefreshComplete() {
mBottomRefreshing = false;
// 如果當前沒有在滑動狀態(tài),則直接隱藏
// 如果當前在滑動狀態(tài),則等待滑動停止后再隱藏
if (getScrollState() == SCROLL_STATE_IDLE) {
hideBottomView();
mShouldHideAfterScrollIdle = false;
} else {
mShouldHideAfterScrollIdle = true;
}
}
// 隱藏bottom view
// 如果bottom view是可見的,則根據(jù)bottom view 當前的位置和RecyclerView當前位置來決定要向上滑動的距離
private void hideBottomView() {
if (isBottomViewVisible()) {
View bottomView = getLastVisibleItem();
if (bottomView != null) {
int[] bottomViewLocation = new int[2];
bottomView.getLocationInWindow(bottomViewLocation);
int[] recyclerViewLocation = new int[2];
getLocationInWindow(recyclerViewLocation);
int recyclerViewHeight = getHeight();
int offset = recyclerViewLocation[1] + recyclerViewHeight - bottomViewLocation[1];
if (offset > 0) {
scrollBy(0, -offset);
}
}
}
}
public interface OnBottomRefreshListener {
void onBottomRefresh();
}
/**
* 自定義包裹的Adapter,主要用來處理加載更多視圖
*/
private class WrapperAdapter extends RecyclerView.Adapter {
private static final int TYPE_BOTTOM_REFRESH_ITEM = Integer.MIN_VALUE + 1;
/**
* 被包裹的外部Adapter
*/
private RecyclerView.Adapter mInnerAdapter;
private RecyclerView.AdapterDataObserver dataObserver = new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
super.onChanged();
notifyDataSetChanged();
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
super.onItemRangeChanged(positionStart, itemCount);
notifyItemRangeChanged(positionStart, itemCount);
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
notifyItemRangeInserted(positionStart, itemCount);
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
super.onItemRangeRemoved(positionStart, itemCount);
notifyItemRangeRemoved(positionStart, itemCount);
}
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
super.onItemRangeMoved(fromPosition, toPosition, itemCount);
notifyItemRangeChanged(fromPosition, toPosition + itemCount);
}
};
private WrapperAdapter(@NonNull RecyclerView.Adapter adapter) {
if (mInnerAdapter != null) {
notifyItemRangeRemoved(0, mInnerAdapter.getItemCount());
mInnerAdapter.unregisterAdapterDataObserver(dataObserver);
}
this.mInnerAdapter = adapter;
mInnerAdapter.registerAdapterDataObserver(dataObserver);
notifyItemRangeInserted(0, mInnerAdapter.getItemCount());
}
public boolean isLoadMoreView(int position) {
return isBottomRefreshable() && position == getItemCount() - 1;
}
@Override
public int getItemCount() {
if (mInnerAdapter != null) {
int itemCount = mInnerAdapter.getItemCount();
if (isBottomRefreshable()) {
return itemCount + 1;
} else {
return itemCount;
}
} else {
return 0;
}
}
@Override
public int getItemViewType(int position) {
if (isBottomRefreshable()) {
if (mInnerAdapter != null) {
int adapterCount = mInnerAdapter.getItemCount();
if (position < adapterCount) {
return mInnerAdapter.getItemViewType(position);
}
}
return TYPE_BOTTOM_REFRESH_ITEM;
} else {
if (mInnerAdapter != null) {
return mInnerAdapter.getItemViewType(position);
} else {
return super.getItemViewType(position);
}
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_BOTTOM_REFRESH_ITEM) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_bottom_refresh_item, parent, false);
return new BottomRefreshViewHolder(view);
} else {
return mInnerAdapter.onCreateViewHolder(parent, viewType);
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (!(holder instanceof BottomRefreshViewHolder)) {
if (mInnerAdapter != null) {
int adapterCount = mInnerAdapter.getItemCount();
if (position
mInnerAdapter.onBindViewHolder(holder, position);
}
}
}
}
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
super.onViewAttachedToWindow(holder);
if (holder instanceof BottomRefreshViewHolder) {
// 支持瀑布流布局
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if (lp instanceof StaggeredGridLayoutManager.LayoutParams) {
((StaggeredGridLayoutManager.LayoutParams) lp).setFullSpan(true);
}
}
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
// 對Grid布局進行支持
RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
if (manager instanceof GridLayoutManager) {
final GridLayoutManager gridLayoutManager = (GridLayoutManager) manager;
final GridLayoutManager.SpanSizeLookup lookup = gridLayoutManager.getSpanSizeLookup();
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
return isLoadMoreView(position) ? gridLayoutManager.getSpanCount() : lookup.getSpanSize(position) ;
}
});
}
}
}
/**
* bottom refresh View對應的ViewHolder
*/
private class BottomRefreshViewHolder extends RecyclerView.ViewHolder {
BottomRefreshViewHolder(View itemView) {
super(itemView);
}
}
}
總結
以上是生活随笔為你收集整理的recyclervie刷新到底部_RecyclerView底部刷新实现详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java小程序连接数据库_Java程序连
- 下一篇: 为什么python用不了中文_【TK例子