1. 程式人生 > >SwipeRefreshLayout巢狀RecyclerView實現上下拉重新整理

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裡面能找到。