RecyclerView 上拉載入更多及滾動到底部的判斷(上)
關於下拉重新整理上拉載入更多,網上有很多例子;下拉重新整理比較簡單直接使用系統提供 SwipeRefreshLayout 即可,比較麻煩的是上拉載入更多,實現上拉的方法多種多樣,這裡對各個方法總結一下。
需求分析
RecyclerView 滾動到底部後,使用者再往上拖拽(這裡使用場景是拖拽,而不是手指離屏後的自動滾動到底部)時,RecyclerView 展示出 載入更多 的字樣並請求更多的資料,請求成功後更新 RecyclerView ;在預設情況下 RecyclerView 的高度大於可展示高度,即 RecyclerView 沒有展示出全部 item ,可滑動,反之則說明不需要載入更多。
技術點
- 判斷 RecyclerView 滾動到了底部
- 判斷 RecyclerView 拖拽
判斷 RecyclerView 滾動到了底部
判斷 RecyclerView 是否到達底部網上流傳著下面幾種方法,這些方法都存在些許問題,最後我們再給出推薦的方式。
1. 根據 item 判斷是否到達底部
這種方法最常見,一般都是像下面這樣實現:
public static boolean isVisBottom(RecyclerView recyclerView){ LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); //螢幕中最後一個可見子項的position int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition(); //當前螢幕所看到的子項個數 int visibleItemCount = layoutManager.getChildCount(); //當前RecyclerView的所有子項個數 int totalItemCount = layoutManager.getItemCount(); //RecyclerView的滑動狀態 int state = recyclerView.getScrollState(); if(visibleItemCount > 0 && lastVisibleItemPosition == totalItemCount - 1 && state == recyclerView.SCROLL_STATE_IDLE){ return true; }else { return false; } }
用這種方式判斷是否滾動到底部時,只要最後一個 item 顯示出一點,就會觸發載入更多,使用者此時看不到在 FooterView 處的 載入更多 字樣(與拖拽展示出載入更多的需求不符);另外,當 RecyclerView 的 item 過少不足填滿整個 RecyclerView 時,也會觸發 載入更多 ;因此,這種方式不符合我們的要求。
2. 使用 canScrollVertically(int direction) 判斷是否到達底部
RecyclerView.canScrollVertically(1)的值表示是否能向上滾動,false表示已經滾動到底部 RecyclerView.canScrollVertically(-1)的值表示是否能向下滾動,false表示已經滾動到頂部
這種方法看似簡單,其實同樣存在一些陷阱。當 RecyclerView 的 item 過少不足填滿整個 RecyclerView 時,無論上拉還是下拉都會觸發載入更多;另外,direction 不只可取1和-1,只需保證正負就能達到一樣的效果。
// View#canScrollVertically(int direction) 原始碼
public boolean canScrollVertically(int direction) {
final int offset = computeVerticalScrollOffset();
final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
if (range == 0) return false;
if (direction < 0) {
return offset > 0;
} else {
return offset < range - 1;
}
}
3. 通過 LinearLayoutManager 進行一系列的計算
這種方法極不推薦使用,過程很複雜,不過對於理解 View 的佈局有很大的幫助。這種方法共分為四步,下面將網上的方法抄錄如下(請讀者自行驗證是否也存在方法1,2同樣的問題):
算出一個子項的高度
public static int getItemHeight(RecyclerView recyclerView) {
int itemHeight = 0;
View child = null;
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
int firstPos = layoutManager.findFirstCompletelyVisibleItemPosition();
int lastPos = layoutManager.findLastCompletelyVisibleItemPosition();
child = layoutManager.findViewByPosition(lastPos);
if (child != null) {
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
itemHeight = child.getHeight() + params.topMargin + params.bottomMargin;
}
return itemHeight;
}算出滑過的子項的總距離
public static int getLinearScrollY(RecyclerView recyclerView) {
int scrollY = 0;
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
int headerCildHeight = getHeaderHeight(recyclerView);
int firstPos = layoutManager.findFirstVisibleItemPosition();
View child = layoutManager.findViewByPosition(firstPos);
int itemHeight = getItemHeight(recyclerView);
if (child != null) {
int firstItemBottom = layoutManager.getDecoratedBottom(child);
scrollY = headerCildHeight + itemHeight * firstPos - firstItemBottom;
if(scrollY < 0){
scrollY = 0;
}
}
return scrollY;
}算出所有子項的總高度
public static int getLinearTotalHeight(RecyclerView recyclerView) { int totalHeight = 0;
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
View child = layoutManager.findViewByPosition(layoutManager.findFirstVisibleItemPosition());
int headerCildHeight = getHeaderHeight(recyclerView);
if (child != null) {
int itemHeight = getItemHeight(recyclerView);
int childCount = layoutManager.getItemCount();
totalHeight = headerCildHeight + (childCount - 1) * itemHeight;
}
return totalHeight;
}高度作比較
public static boolean isLinearBottom(RecyclerView recyclerView) {
boolean isBottom = true;
int scrollY = getLinearScrollY(recyclerView);
int totalHeight = getLinearTotalHeight(recyclerView);
int height = recyclerView.getHeight();
// Log.e(“height”,”scrollY ” + scrollY + ” totalHeight ” + totalHeight + ” recyclerHeight ” + height);
if (scrollY + height < totalHeight) {
isBottom = false;
}
return isBottom;
}
判斷 RecyclerView 拖拽
這一步比較簡單,直接監聽滾動即可。
RecyclerView.addOnScrollListener(new OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == SCROLL_STATE_DRAGGING) {
// 拖拽狀態,實際使用中還需要判斷 載入更多 是否已顯示
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
}
});
推薦方法
該方法在 View#canScrollVertically(int direction) 的基礎上,針對上拉拖拽且有可能 items 沒有填充滿整個 RecyclerView 這個場景做了優化,程式碼如下:
RecyclerView.addOnScrollListener(new OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == SCROLL_STATE_DRAGGING && 沒有觸發載入更多) {
if (RecyclerView.computeVerticalScrollOffset() > 0) {// 有滾動距離,說明可以載入更多,解決了 items 不能充滿 RecyclerView
的問題及滑動方向問題
boolean isBottom = false ;
isBottom = RecyclerView.computeVerticalScrollExtent()
+ RecyclerView.computeVerticalScrollOffset()
== RecyclerView.computeVerticalScrollRange() ;
// 也可以使用 方法2
// isBottom = !RecyclerView.canScrollVertically(1) ;
if (isBottom) {
// 說明滾動到底部,觸發載入更多
...
}
}
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
}
});
到這裡,上拉的判斷就處理完成了,下一篇將處理 載入更多 檢視的顯示。
附一張從網上找的原理圖: