Recyclerview 下拉刷新和加載更多的簡單封裝

android 開發中下拉刷新和加載更多有很多優秀的三方庫,大多數三方庫都是基于listview,gridview,scrollview做的擴展,而基于recyclerview實現的不多,如果你的項目中使用了pullTorefresh這種三方庫,又想在使用recyclerview頁面實現和它統一的刷新效果,這篇文章或許能幫到你。
??先來看下效果圖:

1.gif

??這個效果圖展示的是staggerlayoutmanager下的效果,其他兩個layoutManager下類似,這里就不貼圖了。
??整個實現是基于RecyclerView分欄顯示和touch事件的處理。

RecyclerView分欄顯示處理

使用過RecyclerView的小伙伴們對它都應該很熟悉了,這個案例中,分欄處理分為3個部分,分為頭部下拉刷新,普通item,加載更多部分,具體代碼如下:
BaseRefreshRecyclerViewAdapter.java

    @Override
    public int getItemViewType(int position) {
        if (position == 0) {
            return VIEW_TYPE_REFRESH_HEADER;
        } else if (position == 1 + data.size()) {
            return VIEW_TYPE_REFRESH_FOOTER;
        }
        return VIEW_TYPE_ITEM;
    }
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        RecyclerView.ViewHolder viewHolder = null;
        switch (viewType) {
            case VIEW_TYPE_REFRESH_HEADER:
                View headerView = View
                        .inflate(parent.getContext(), R.layout.view_refresh_header, null);
                this.headerView = headerView;
                viewHolder = new RefreshHeaderViewHolder(headerView);
                break;
            case VIEW_TYPE_ITEM:
                viewHolder = onCreateItemViewHolder(parent);
                break;
            case VIEW_TYPE_REFRESH_FOOTER:
                View footerView = LayoutInflater.from(parent.getContext())
                        .inflate(R.layout.view_refresh_footer, parent, false);
                viewHolder = new RefreshFooterViewHolder(footerView);
                break;
        }
        return viewHolder;
    }
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        int itemViewType = getItemViewType(position);
        switch (itemViewType) {
            case VIEW_TYPE_ITEM:
                onBindItemViewHolder(holder, position - 1);
                break;
            case VIEW_TYPE_REFRESH_HEADER:
                prepareHeaderView(holder);
                break;
            case VIEW_TYPE_REFRESH_FOOTER:
                prepareFooterView(holder);
                break;
        }
    }

這段代碼功能相信大家都很熟悉,在這里,我將VIEW_TYPE_ITEM類型view具體的createViewholder和onBindViewHolder交給子類去實現,在子類中你依然可以根據需求進行分欄處理。
??這里有2點需要注意,分欄后普通item可能需要占據多個span,沒有占據整個屏幕,而header和footer部分需要占據整個屏幕寬度。具體處理如下:

    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        super.onAttachedToRecyclerView(recyclerView);
        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        if (layoutManager != null && layoutManager instanceof GridLayoutManager) {
            final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
            gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    return getItemViewType(position) == VIEW_TYPE_REFRESH_HEADER || getItemViewType(position) == VIEW_TYPE_REFRESH_FOOTER
                            ? gridLayoutManager.getSpanCount() : 1;
                }
            });
        }
    }

    @Override
    public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
        super.onViewAttachedToWindow(holder);
        View itemView = holder.itemView;
        ViewGroup.LayoutParams lp = itemView.getLayoutParams();
        if (lp == null) {
            return;
        }
        if (holder instanceof RefreshHeaderViewHolder || holder instanceof RefreshFooterViewHolder) {

            if (lp instanceof StaggeredGridLayoutManager.LayoutParams) {
                StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
                p.setFullSpan(true);
            }
        }
    }

linelayoutmanager情況下不需要處理。這里順便提下,有的小伙伴遇到recyclerView需要添加頭部,普通item部分是分為多個item一排時,會使用srollview嵌套recyclerView的方式處理,其實不需要,上面的代碼完全可以解決你的需求。本人不喜歡嵌套這種方式,因為我不會 O(∩_∩)O~,并且計算高度會讓性能下降。
??第二個需要注意的地方是我們要在正確的位置將header部分的高度計算出來為后面的處理做準備,這里我選擇在createheaderviewHolder的時候處理:


            headerView.post(new Runnable() {
                @Override
                public void run() {
                    headerViewMeasuredHeight = headerView.getMeasuredHeight();
                    setHeaderPadding();
                }
            });
 
Touch事件的處理

這部分處理是為了完成類似pulltorefresh的頭部刷新效果,所有的處理在BaseRefreshRecyclerView中完成,BaseRefreshRecyclerView繼承于RecyclerView。重寫dispatchTouchEvent方法。

    public boolean dispatchTouchEvent(MotionEvent e) {
        if (!refreshAble) {
            return super.dispatchTouchEvent(e);
        }
        switch (e.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startY = e.getY();
                headerRefreshHeight = mAdapter.getHeaderRefreshHeight();
                break;
            case MotionEvent.ACTION_MOVE:
                if (currentState == STATE_LOADING) {
                    break;
                }
                float tmpY = e.getY();
                if (currentState == STATE_PULL_TO_REFRESH) {
                    if ((tmpY - startY) / ranY <= this.headerRefreshHeight) {
                        currentDist = (int) ((tmpY - startY) / ranY);
                        mAdapter.setHeaderPadding((int) ((tmpY - startY) / ranY - this.headerRefreshHeight));
                        initAnimationHideHeader();
                    } else if (firstCompletelyVisibleItemPosition >= 0 && firstCompletelyVisibleItemPosition <= 1) {
                        currentState = STATE_RELASE_TO_REFRESH;
                        changeWightState();
                    }
                }
                if (currentState == STATE_RELASE_TO_REFRESH) {
                    changeWightState();
                    currentDist = (int) ((tmpY - startY) / ranY - this.headerRefreshHeight);
                    mAdapter.setHeaderPadding(currentDist);
                }
                break;
            case MotionEvent.ACTION_UP:
                if (currentState == STATE_LOADING) {
                    break;
                }
                if (currentState == STATE_PULL_TO_REFRESH) {
                    if (animator_hide_header == null) {
                        initAnimationHideHeader();
                    }
                    animator_hide_header.start();
                }
                if (currentState == STATE_RELASE_TO_REFRESH) {
                    currentState = STATE_LOADING;
                    changeWightState();
                    View view = getLayoutManager().getChildAt(0);
                    if (view.getTop() <= 5) {
                        onRefresh();
                        initAnimaionRelasetoRefresh();
                    } else {
                        currentDist = -view.getTop();
                        animator_hide_header.start();
                        currentState = STATE_PULL_TO_REFRESH;
                    }

                }
                break;
        }
        return super.onTouchEvent(e);

    }

具體流程分為兩種情況:
?1.下拉距離小于header高度,手指放開,不刷新。
?2.下拉距離從小于header高度到大于,放手,完成一次完整刷新過程。
??這里應該需要添加一種情況,下拉距離從大于變為小于,不刷新,沒寫明白,放棄了。
??在不同的頭部狀態調用changeWightState()方法,改變頭部狀態,這個方法中也是調用adapter提供的方法去改變狀態。如果你實現了自己的頭部效果,你可以在adapter中將setHeaderState()方法改寫。
??為了添加一個阻尼效果,我將手指距離除以了一個系數ranY,你可以自己調整。header部分實現擴大是通過設置padding實現的,通過調用如下方法:mAdapter.setHeaderPadding(currentDist)動態實現頭部高度變化。手指放開,頭部回縮時,為了讓頭部彈性的回歸,我給他添加了一個屬性動畫效果:

private void initAnimaionRelasetoRefresh() {
        ValueAnimator animator_relase_torefresh = ValueAnimator.ofInt(currentDist, 0);
        animator_relase_torefresh.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mAdapter.setHeaderPadding((Integer) valueAnimator.getAnimatedValue());
            }
        });
        animator_relase_torefresh.setDuration(400);
        animator_relase_torefresh.start();
    }

    private void initAnimationRefreshOver() {
        ValueAnimator animator_refresh_over = ValueAnimator.ofInt(0, -headerRefreshHeight);
        animator_refresh_over.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mAdapter.setHeaderPadding((Integer) valueAnimator.getAnimatedValue());
            }
        });
        animator_refresh_over.setDuration(200);
        animator_refresh_over.start();
    }

    private void initAnimationHideHeader() {
        animator_hide_header = ValueAnimator.ofInt(-currentDist, -headerRefreshHeight);
        animator_hide_header.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mAdapter.setHeaderPadding((Integer) valueAnimator.getAnimatedValue());
            }
        });
        animator_hide_header.setDuration(100);
    }

這樣頭部回縮動作會顯得平滑。

加載更多

加載更多這部分基于對recyclerView滑動監聽的處理,沒有什么難點,代碼如下:

 @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        LayoutManager layoutManager = getLayoutManager();
        int lastVisibleItemPosition = 0;
        if (layoutManager instanceof LinearLayoutManager) {
            lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
            firstCompletelyVisibleItemPosition = ((LinearLayoutManager) layoutManager).findFirstCompletelyVisibleItemPosition();
        }
        if (layoutManager instanceof GridLayoutManager) {
            lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
            firstCompletelyVisibleItemPosition = ((GridLayoutManager) layoutManager).findFirstCompletelyVisibleItemPosition();
        } else if (layoutManager instanceof StaggeredGridLayoutManager) {
            int[] last = null;
            int[] first = null;
            if (!hasInit) {
                last = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()];
                first = new int[last.length];
                hasInit = true;
            }
            int[] lastVisibleItemPositions = ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(last);
            int[] firstCompletelyVisibleItemPositions = ((StaggeredGridLayoutManager) layoutManager).findFirstCompletelyVisibleItemPositions(first);
            firstCompletelyVisibleItemPosition = firstCompletelyVisibleItemPositions[0];
            for (int i : lastVisibleItemPositions) {
                lastVisibleItemPosition = i > lastVisibleItemPosition ? i : lastVisibleItemPosition;
            }
        }
        if (lastVisibleItemPosition == mAdapter.getItemCount() - 1) {
            mAdapter.setFooterVisible(true);
            layoutManager.scrollToPosition(mAdapter.getItemCount());
            onLoadMore();
        }
    }

在這里,需要注意的是在獲取StaggeredGridLayoutManager下最后一個可見的位置時,需要將保存在lastVisibleItemPositions[]中的所有數據進行比較,找出最大的那個位置。

ItemDecoration的處理

你可能需要普通item四周間隙一致,而頭部和尾部沒有空隙,就像文章開頭的一樣,你可能會在item布局中添加padding,并且在recyclerView中添加padding,這樣是行不通的,你會發現item間隙一致了,但是header部分也會有padding。解決方法就是自己繼承RecyclerView.ItemDecoration類,這里我的寫法如下:

@Override
    public void getItemOffsets(Rect outRect, View view,
                               RecyclerView parent, RecyclerView.State state) {
        // set header and footer space zero
        outRect.bottom = space;
        if (parent.getChildLayoutPosition(view) == 0
                || parent.getChildLayoutPosition(view) == parent.getAdapter().getItemCount()) {
            outRect.top = 0;
            outRect.left = 0;
            outRect.right = 0;

        } else if (parent.getLayoutManager() instanceof StaggeredGridLayoutManager) {
            StaggeredGridLayoutManager.LayoutParams lp = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
            int spanIndex = lp.getSpanIndex();
            if (spanIndex == span-1) {
                outRect.left = space;
                outRect.right = space;

            } else {
                outRect.left = space;
                outRect.right = 0;
            }
        } else if (parent.getLayoutManager() instanceof GridLayoutManager){
            if (parent.getChildLayoutPosition(view) % span == 0) {
                outRect.left = space;
                outRect.right = space;
            } else {
                outRect.left = space;
                outRect.right = 0;
            }
        }

    }

這里需要注意的是在StaggeredGridLayoutManager中不能和GridLayoutManager中那樣通過parent.getChildLayoutPosition(view) % span來判斷item位置,而需要根據

StaggeredGridLayoutManager.LayoutParams lp = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
            int spanIndex = lp.getSpanIndex();

來獲取位置,spanIndex從左到右的值依次為0,1.... ,span-1;所以我們在設置outRect的左右間隙時,只需要關心最右邊位置的view,將它左右設為space,而其他view只設置left為space,上下間隙只需要將bottom設置為span就可以了,這樣上下左右間隙就一致了,當然這排除了頭部和尾部。

使用方法

這些示例代碼你只需要修改少許部分就能實現一個屬于自己的封裝抽取,當然自己實現也是很容易的。
??- 修改header和footer布局文件,初始化布局文件中的控件,注意一點,header布局中最外層使用RelativeLayout,因為在inflate的時候沒有將布局加入parent,會讓頭部布局在linelayoutmanager中變成wrapcontent。
??- 在BaseRefreshRecyclerViewAdapter中修改setHeaderState()方法設置不同狀態下header的state。修改setFooterRefreshFailState()方法,實現在加載更多失敗后的狀態。
??- 最后在使用的時候,需要adapter繼承BaseRefreshRecyclerViewAdapter。
使用示例:

 final BaseRefreshRecyclerView rcv_test = (BaseRefreshRecyclerView) findViewById(R.id.rcv_test);
        final TestRecyclerViewAdapter madapter = new TestRecyclerViewAdapter();
        StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);
        rcv_test.setLayoutManager(staggeredGridLayoutManager);
        rcv_test.addItemDecoration(new SimpleItemDecoration(20,3));
        staggeredGridLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
        ArrayList list = new ArrayList();
        for (int i = 0; i < 15; i++) {
            list.add(i);
        }
        madapter.setData(list);
        rcv_test.setAdapter(madapter);
        rcv_test.setOnRefreshAndLoadMoreListener(new BaseRefreshRecyclerView.OnRefreshAndLoadMoreListener() {
            @Override
            public void onRefresh() {
                Toast.makeText(MainActivity.this, "Refreshing", Toast.LENGTH_SHORT).show();
                rcv_test.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        rcv_test.completeRefresh();
                    }
                }, 3000);
            }

            @Override
            public void onLoadMore() {
                rcv_test.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        List data = madapter.getData();
                        for (int i = 0; i < 10; i++) {
                            data.add(i * 1000);
                        }
                        madapter.setData(data);
                        madapter.notifyDataSetChanged();
                        rcv_test.completeLoadMore();
                    }
                }, 3000);
            }
        });

完整代碼在這里。作者android菜鳥10個月,難免代碼寫的很渣,多擔待,輕噴。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容