SwipeRefreshLayout巢狀RecyclerView實現上下拉重新整理
在這裡特別感謝大神,這裡附上大神帖子:https://github.com/1030310877/LoadMoreRecyclerView
SwipeRefreshLayout巢狀RecyclerView實現上下拉重新整理。SwipeRedreshLayout是Android自帶的一個下拉重新整理控制元件。
它有自帶的下拉重新整理方法setOnRefreshListener();
//下拉重新整理 swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { //最後清空資料,否則可能造成下標越界,但是業務要求先清空資料,所以,我在重新整理資料的同時,將RecyclerView的滑動事件給攔截掉 msgList.clear(); //設定RecyclerView的滑動狀態,在下拉重新整理時,將RecyclerView的滑動事件給消費,連攔截 recyclerView.setRecycleScrollBug(true); //載入資料 getData("0", "20"); //判斷是否下拉重新整理 refreshFlag = 1; //得到重新整理資料的狀態 firstFlag = 0; } });
比較簡單不在贅述。只是有一點需要注意:在下拉重新整理的同時,如果同時做上拉載入的動作會導致下標越界。原因是,先將資料清空後,導致請求下拉無資料。但是,這是重新整理,又不得不先將資料清空,所以,我在做下拉重新整理的動作時,將頁面滑動給禁止。我只需要禁止掉RecyclerView的滑動事件就可以了。使用如下方法。
//暫時關閉RecyclerView滑動 public void setRecycleScrollBug(final boolean mIsRefreshing) { recyclerView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (mIsRefreshing) { return true; } else { return false; } } }); }
很簡單,只是攔截的RecyclerView的onTouch事件,return true攔截,return false不攔截。
一般來說,上拉載入更多,只需要在RecyclerView新增尾部局就可以了。但是親測不行。
//返回item數量 @Override public int getItemCount() { //如果返回mMsgList.size()+1會造成下標越界的異常。但是如果反回mMsgList.size()時。會造成資料少一條,載入更多試圖將最後一條item給替換了 return mMsgList.size() == 0 ? 0 : mMsgList.size() + 1; } @Override public int getItemViewType(int position) { if (position + 1 == getItemCount()) { //最後一個item設定為footerView return TYPE_FOOTER; } else { return TYPE_ITEM; } }
如果返回mMsgList.size()+1會造成下標越界的異常。
但是如果反回mMsgList.size()時。會造成資料少一條,載入更多試圖將最後一條item給替換了。
糾結了好長一段時間。因為我的思路一直停留在RecyclerView的多條目載入上面了。
這個有個弊端就是在GridLayoutManager時候就沒有用了,footView或者headVeiw就跑到第一個和最後一個Grid中去了。
或者在Adapter中進行處理
這個需要通過LinearLayoutManager中findLastVisibleView的方法來判斷是否到達最後。
通過GridLayoutManager中的方法設定頭尾view的Grid佔比來使頭尾變成一整行。 這種在adapter中處理的方法,不覺得還算是自定義view的範疇,頂多是recyclerView的活用。
直到今天看到另外一種寫法,直接在RecyclerView佈局下面增加布局,使用使用組合View的方法,結合v4包中的NestedScrollingParent來處理和RecyclerView的滑動,然後使用NestedScrollingChild來處理巢狀在SwipeRefreshLayout中的滑動事件,達成使用SwipeRefreshLayout來進行下拉重新整理。這樣只要處理好滑動事件,那就不管是什麼LayoutManager,footView永遠在最下面。
上程式碼
public class MyRecyclerView extends LinearLayout implements NestedScrollingParent, NestedScrollingChild {
private View rootLayout;
private RecyclerView recyclerView;
private FrameLayout footView;
private NestedScrollingParentHelper helper;
private NestedScrollingChildHelper childHelper;
private boolean isBottom = false;
private boolean changeBottom = false;
private boolean enableLoad = true;
private boolean isLoading = false;
//TODO footView的內容View
private View footContentView;
private final int[] mScrollOffset = new int[2];
private ProgressBar progressBar;
public MyRecyclerView(Context context) {
this(context, null);
}
public MyRecyclerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
helper = new NestedScrollingParentHelper(this);
childHelper = new NestedScrollingChildHelper(this);
childHelper.setNestedScrollingEnabled(true);
initView();
}
private void initView() {
View.inflate(getContext(), R.layout.layout_myrecycler, this);
recyclerView = (RecyclerView) findViewById(R.id.recycler_content);
footView = (FrameLayout) findViewById(R.id.layout_footView);
//TODO
footContentView = LayoutInflater.from(getContext()).inflate(R.layout.footview_up, null);
footView.addView(footContentView);
rootLayout = getChildAt(0);
}
private void smoothScrollBy(int dx, int dy) {
//將ProgressBar隱藏
findViewById(R.id.item_load_pb).setVisibility(GONE);
//設定mScroller的滾動偏移量
if (isBottom) {
//已經到了底部,並且還在往上拉,直接返回,不處理事件
if (rootLayout.getScrollY() + dy >= footView.getMeasuredHeight()) {
return;
}
//回彈
if (rootLayout.getScrollY() + dy <= 0) {
dy = -rootLayout.getScrollY();
}
changeBottom = false;
rootLayout.scrollBy(0, dy);
invalidate();//這裡必須呼叫invalidate()才能保證computeScroll()會被呼叫,否則不一定會重新整理介面,看不到滾動效果
} else {
if (!isLoading) {
//TODO 重置檢視
((TextView) footContentView.findViewById(R.id.status_tv)).setText("上拉載入");
}
//往上拉時,距離大於了footview的高度,只讓它拉到那麼大
if (rootLayout.getScrollY() + dy >= footView.getMeasuredHeight()) {
dy = footView.getMeasuredHeight() - rootLayout.getScrollY();
changeBottom = true;
//TODO 拉到footView最大高度時候可以做的事情,檢視變化
if (!isLoading) {
((TextView) footContentView.findViewById(R.id.status_tv)).setText("鬆開載入更多");
}
} else {
//往下拉時,滑動距離大於了當前的偏移值
if (rootLayout.getScrollY() + dy < 0) {
dy = -rootLayout.getScrollY();
}
}
rootLayout.scrollBy(0, dy);
invalidate();//這裡必須呼叫invalidate()才能保證computeScroll()會被呼叫,否則不一定會重新整理介面,看不到滾動效果
}
}
/*==========以下為Child的方法==========*/
@Override
public void setNestedScrollingEnabled(boolean enabled) {
childHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return childHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return childHelper.startNestedScroll(axes);
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
return childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public void stopNestedScroll() {
childHelper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return childHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return childHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return childHelper.dispatchNestedPreFling(velocityX, velocityY);
}
/*==========以下為Parent的方法,用來接收RecyclerView的滑動事件==========*/
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
startNestedScroll(nestedScrollAxes);
return enableLoad;
}
@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
helper.onNestedScrollAccepted(child, target, axes);
}
@Override
public int getNestedScrollAxes() {
return helper.getNestedScrollAxes();
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
if (dy < 0 && rootLayout.getScrollY() > 0) {
//如果是手指下滑
smoothScrollBy(dx, dy);
consumed[1] = dy;
} else {
dispatchNestedPreScroll(dx, dy, consumed, mScrollOffset);
}
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
if (dyConsumed == 0 && dyUnconsumed > 0) {
smoothScrollBy(dxUnconsumed, dyUnconsumed);
} else {
//其餘的未消費的事件,傳給父View消費
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, mScrollOffset);
}
}
@Override
public void onStopNestedScroll(View child) {
//將ProgressBar展示
findViewById(R.id.item_load_pb).setVisibility(VISIBLE);
isBottom = changeBottom;
if (isBottom && !isLoading && rootLayout.getScrollY() == footView.getMeasuredHeight()) {
isLoading = true;
//TODO LoadMore檢視變化
((TextView) footContentView.findViewById(R.id.status_tv)).setText("正在載入");
if (listener != null) {
listener.onLoading();
}
} else {
isBottom = false;
if (rootLayout.getScrollY() > 0 && !isBottom) {
smoothScrollBy(0, -rootLayout.getScrollY());
}
}
helper.onStopNestedScroll(child);
//最後一定要呼叫這個,告訴父view滑動結束,不然父view的滑動會卡住。
stopNestedScroll();
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
return false;
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return false;
}
@Override
public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) {
return false;
}
/*==============以下為開放部分的recyclerView方法 ================*/
//TODO 如果還需要其他recyclerview的方法,可在下方進行開放。
public void setAdapter(RecyclerView.Adapter adapter) {
recyclerView.setAdapter(adapter);
}
public void setLayoutManager(RecyclerView.LayoutManager layoutManager) {
recyclerView.setLayoutManager(layoutManager);
}
public void addItemDecoration(RecyclerView.ItemDecoration decoration) {
recyclerView.addItemDecoration(decoration);
}
public void addItemDecoration(RecyclerView.ItemDecoration decoration, int index) {
recyclerView.addItemDecoration(decoration, index);
}
public RecyclerView.Adapter getAdapter() {
return recyclerView.getAdapter();
}
public RecyclerView.LayoutManager getLayoutManager() {
return recyclerView.getLayoutManager();
}
public void addOnScrollListener(RecyclerView.OnScrollListener onScrollListener) {
recyclerView.addOnScrollListener(onScrollListener);
}
/**
* 載入結束後呼叫該方法進行footview縮回
*
* @param total
* @param size
*/
public void loadFinished(int total, int size) {
isLoading = false;
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (isBottom) {
smoothScrollBy(0, -footView.getMeasuredHeight());
}
}
}, 200);
if (total == size){
//TODO 載入完成後的處理,縮回以及檢視變化
//將ProgressBar隱藏
findViewById(R.id.item_load_pb).setVisibility(GONE);
((TextView) footContentView.findViewById(R.id.status_tv)).setText("無更多資料");
}else {
//TODO 載入完成後的處理,縮回以及檢視變化
((TextView) footContentView.findViewById(R.id.status_tv)).setText("載入成功");
}
}
public void setEnableLoad(boolean tf) {
this.enableLoad = tf;
}
private Handler handler = new Handler();
/*=============== 監聽 ===================*/
public onLoadingMoreListener listener;
public void setOnLoadingListener(onLoadingMoreListener listener) {
this.listener = listener;
}
public interface onLoadingMoreListener {
void onLoading();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
/**
* 關閉RecyclerView滑動
* @param mIsRefreshing true表示攔截滑動事件,頁面不可滑動,false表示不攔截事件,頁面可以滑動
*/
public void setRecycleScrollBug(final boolean mIsRefreshing) {
recyclerView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (mIsRefreshing) {
return true;
} else {
return false;
}
}
});
}
}
貼上佈局程式碼:layout_myrecycler
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
<FrameLayout
android:id="@+id/layout_footView"
android:layout_width="match_parent"
android:layout_height="50dp" />
</LinearLayout>
佈局程式碼:footview_up
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="80dp"
android:gravity="center"
android:orientation="horizontal">
<ProgressBar
android:id="@+id/item_load_pb"
style="@android:style/Widget.Holo.ProgressBar"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_centerHorizontal="true"
/>
<TextView
android:id="@+id/status_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/ten_dp"
android:text="上拉重新整理" />
</LinearLayout>
不過這樣還是會出現下拉重新整理時偶爾會造成下標越界的Bug,原因還是一樣,重新整理的同時,將資料清空導致的。我在這裡使用了一個方法,再重新整理時,寫一個標識,在請求資料時,得到資料之前,將之前存在的資料清空,這樣就不會導致下標越界了
/**
* 如果繫結的 List 物件在更新資料之前進行了 clear,而這時使用者緊接著迅速上滑RecycleView,就會造成崩潰,而且異常不會報到你的程式碼上,屬於RecycleView的內部錯誤。
* 原因是當你 clear 了 list 之後,這時迅速上滑,而新資料還沒到來,導致 RecycleView 要更新載入下面的 Item 時候,找不到資料來源了,造成程式直接崩潰了
* 思路:當你在下拉重新整理時,將RecyclerView設為不可滑動狀態,重新整理完成在設定為可滑動狀態。暫時解決
*/
//下拉重新整理
swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
//設定RecyclerView的滑動狀態,在下拉重新整理時,將RecyclerView的滑動事件給消費,連攔截
recyclerView.setRecycleScrollBug(true);
try {
//最後清空資料,否則可能造成下標越界,但是業務要求先清空資料,所以,我在重新整理資料的同時,將RecyclerView的滑動事件給攔截掉
//msgList.clear();
//設定標識位,在重新整理時,得到資料前,將資料清空,而不是在重新整理方法裡清空
pageindex = 1;
getData("0", "" + NEXNUM);
//判斷是否下拉重新整理
refreshFlag = 1;
//得到重新整理資料的狀態
totalFlag = 0;
} catch (Exception e) {
e.printStackTrace();
ToastUtils.showShort("操作次數過於頻繁,請稍後再試");
}
}
});
原始碼在文章開頭的github裡面能找到。