RecyclerView 自帶的上拉加載更多

RecyclerView 大家肯定很熟悉,平常使用的時候也難免會遇到下拉刷新和上拉加載更多,網上相關的控件也是多如牛毛。我特別期待谷歌什麼時候能夠自己開發一個,可惜一直到現在,也只有一個SwipeRefreshLayout下拉刷新控件,上拉加載連個影子都沒。。。

如果不想使用第三方的控件,又想有上拉加載更多功能怎麼辦呢?於是上網查了相關資料,發現RecyclerView有一個監聽方法addOnScrollListener,可以監聽到RecyclerView的滑動狀態,欣喜若狂,好好研究了一番,並且自己封裝了下

Demo 地址
https://github.com/linqinen708/MyDatabindingRecyclerView

在這裏插入圖片描述

使用方式:

首先在xml佈局中使用

 <MyRefreshLayout
        android:id="@+id/refresh_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

然後在Activity中初始化,大功告成
其中adapter 就是RecyclerView的adapter

private void initRefreshLayout() {
        mRefreshLayout.setAdapter(mAdapter);

        mRefreshLayout.setRefreshListener(new MyRefreshLayout.RefreshListener() {
            @Override
            public void loadMore() {
                
            }

            @Override
            public void refresh() {
                
            }
        });
    }

那麼其中的原理是什麼呢?
下面簡單說一下:

/**最後一個可見的item的索引*/
private int lastVisibleItemPosition;
/**
 * 是否正在上拉加載更多
 */
private boolean isLoadingMore;
***
***
***
private void initLoadMoreListener() {
        mLayoutManager = new LinearLayoutManager(this);
        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
//                    LogT.i("停止滑動:");
                    int visibleItemCount = mLayoutManager.getChildCount();
                    int totalItemCount = mLayoutManager.getItemCount();
                    LogT.i("visibleItemCount:" + visibleItemCount + ", totalItemCount:" + totalItemCount + ",lastVisibleItemPosition:" + lastVisibleItemPosition);
                    if (!isLoadingMore && lastVisibleItemPosition == totalItemCount - 1) {
                        isLoadingMore = true;
                    }
                }
            }

            /**僅對LinearLayoutManager 有效,其他LayoutManager未驗證*/
            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                lastVisibleItemPosition = mLayoutManager.findLastVisibleItemPosition();

            }
        });
    }

首先需要通過onScrolled方法,獲得屏幕中可見的最後一個item的position,然後在onScrollStateChanged方法中,判斷RecyclerView是否已經停止滾動,也就是newState == RecyclerView.SCROLL_STATE_IDLE,
然後判斷最後一個item的position是否是總數的最後一個lastVisibleItemPosition == totalItemCount - 1,如果是,則說明滑到底部了,整個邏輯就是這麼簡單粗暴
在這裏插入圖片描述
然後我就自己封裝了下RecyclerView,變成了LoadMoreRecyclerView

public class LoadMoreRecyclerView extends RecyclerView {

    private LinearLayoutManager mLayoutManager;

    private int lastVisibleItemPosition;


    /**
     * 是否可以上拉加載更多
     * 比如當暫無更多數據之後,就沒必要再支持上拉加載更多了
     * 可以關閉上拉功能
     */
    private boolean isLoadMoreEnable = true;

    public boolean isLoadMoreEnable() {
        return isLoadMoreEnable;
    }

    public void setLoadMoreEnable(boolean loadMoreEnable) {
        isLoadMoreEnable = loadMoreEnable;
    }

    /**
     * 是否正在上拉加載更多,
     * 如果不做額外判斷,而用戶連續快速上拉,則會出現多次請求
     */
    private boolean isLoadingMore;

    public boolean isLoadingMore() {
        return isLoadingMore;
    }

    public void completeLoadMore() {
        isLoadingMore = false;
    }

    private LoadMoreListener mLoadMoreListener;

    public void setLoadMoreListener(LoadMoreListener loadMoreListener) {
        init();
        mLoadMoreListener = loadMoreListener;
    }

    public interface LoadMoreListener {
        void loadMore();
    }

    public LoadMoreRecyclerView(@NonNull Context context) {
        super(context);
        initView();
    }

    public LoadMoreRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public LoadMoreRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    /**如果不取消動畫,會導致數據不一致的報錯
     * IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter
     * */
    private void initView(){
        setItemAnimator(null);
    }

    private void init() {
        LogT.i("初始化:" + getLayoutManager());
        if (getLayoutManager() != null && getLayoutManager() instanceof LinearLayoutManager) {
            mLayoutManager = (LinearLayoutManager) getLayoutManager();
        }
        if (mLayoutManager != null) {
            addOnScrollListener(new RecyclerView.OnScrollListener() {
                /**
                 * 當顯示的item數量不夠多,無法撐滿屏幕高度時
                 * 無論是上拉還是下拉,都會觸發該方法,
                 * 所以下拉需要滿足條件
                 * 否則下拉也會觸發加載更多
                 * 所以當 visibleItemCount < totalItemCount 才觸發加載更多
                 * 即 item的數量夠多,並超過屏幕高度時,才觸發加載更多
                 * */
                @Override
                public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
                    if (isLoadMoreEnable && newState == RecyclerView.SCROLL_STATE_IDLE) {
//                    LogT.i("停止滑動:");
                        int visibleItemCount = mLayoutManager.getChildCount();
                        int totalItemCount = mLayoutManager.getItemCount();
//                        LogT.i(":" + getAdapter().getItemViewType(0));
//                        LogT.i("isLoadingMore:"+isLoadingMore + ",visibleItemCount:" + visibleItemCount + ", totalItemCount:" + totalItemCount + ",lastVisibleItemPosition:" + lastVisibleItemPosition);
                        if (!isLoadingMore && visibleItemCount < totalItemCount && lastVisibleItemPosition == totalItemCount - 1) {
                            isLoadingMore = true;
                            if (getAdapter() != null && getAdapter() instanceof BaseBindingAdapter) {
                                LogT.i("加載更多:");
                                ((BaseBindingAdapter) getAdapter()).showLoadMore(true);
                            }
                            LogT.i("mLoadMoreListener:" + mLoadMoreListener);
                            if (mLoadMoreListener != null) {
                                mLoadMoreListener.loadMore();
                            }
                        }
                    }
                }

                /**僅對LinearLayoutManager 有效,其他LayoutManager未驗證*/
                @Override
                public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
//                    LogT.i("dx:" +dx + ", dy:"  + dy);
                    if (isLoadMoreEnable) {
                        lastVisibleItemPosition = mLayoutManager.findLastVisibleItemPosition();
                    }

                }
            });
        }
    }
}

使用方式:

mRecyclerView.setLoadMoreListener(new LoadMoreRecyclerView.LoadMoreListener() {
            @Override
            public void loadMore() {
                httpRequest();
            }
        });

不過這樣子只完成了一半,還不是一個完整的控件,於是我自己也封裝了一個控件MyRefreshLayout,整個控件非常簡單,就是把SwipeRefreshLayout和LoadMoreRecyclerView 整合在一起,這樣,就是一個簡單的下拉刷新和上拉加載更多控件
在這裏插入圖片描述

public class MyRefreshLayout extends FrameLayout implements SwipeRefreshLayout.OnRefreshListener {

    private Context mContext;

    private SwipeRefreshLayout mSwipeRefreshLayout;

    private LoadMoreRecyclerView mRecyclerView;

    private BaseBindingAdapter mAdapter;

    private RefreshListener mRefreshListener;
    /**
     * 頁數
     */
    private int page = 1;

    public int getPage() {
        return page;
    }

    public void setPage(int page) {
        this.page = page;
    }

    public void setRefreshListener(RefreshListener refreshListener) {
        mRefreshListener = refreshListener;
        mSwipeRefreshLayout.setEnabled(true);
        mSwipeRefreshLayout.setOnRefreshListener(this);
        mRecyclerView.setLoadMoreListener(new LoadMoreRecyclerView.LoadMoreListener() {
            @Override
            public void loadMore() {
                page++;
                if (mRefreshListener != null) {
                    mRefreshListener.loadMore();
                }
            }
        });
    }

    public interface RefreshListener {
        void loadMore();

        void refresh();
    }

    public MyRefreshLayout(@NonNull Context context) {
        super(context);
        mContext = context;
        initView();
    }

    public MyRefreshLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        initView();
    }

    public MyRefreshLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        initView();
    }

    /**
     * 初始化控件
     */
    private void initView() {
        mSwipeRefreshLayout = new SwipeRefreshLayout(mContext);
        mSwipeRefreshLayout.setEnabled(false);
        mRecyclerView = new LoadMoreRecyclerView(mContext);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(mContext));
        /*因爲自定義的LoadMoreListener 需要LinearLayoutManager支持
         * 所以setLoadMoreListener 需要在setLayoutManager 之後 設置
         * */


        mSwipeRefreshLayout.addView(mRecyclerView);

        addView(mSwipeRefreshLayout);
    }

    @Override
    public void onRefresh() {
        page = 1;
        if (mRefreshListener != null) {
            mRefreshListener.refresh();
        }
    }

    /**
     * 下拉刷新完成
     */
    public void completeRefresh() {
        /*如果下拉刷新後,mRecyclerView 自動加載到底部,則讓其返回到頂部*/
        mRecyclerView.scrollToPosition(0);
        mSwipeRefreshLayout.setRefreshing(false);
    }

    /**
     * 上拉加載更多完成
     */
    public void completeLoadMore() {
        mRecyclerView.completeLoadMore();
    }

    public void complete() {
        completeLoadMore();
        completeRefresh();
    }

    public void completeHttpRequest(Collection collection) {
        if (mAdapter == null) {
            return;
        }
        if (page == 1) {
            mAdapter.getItems().clear();
            completeRefresh();
            mAdapter.toggleFootView(collection);
            setLoadMoreEnable(true);
        } else {
            completeLoadMore();
            setLoadMoreEnable(mAdapter.toggleLoadMore(collection));
        }
    }

    public void setAdapter(BaseBindingAdapter adapter) {
        mAdapter = adapter;
        mRecyclerView.setAdapter(adapter);
    }

    public void addItemDecoration(@NonNull RecyclerView.ItemDecoration decor) {
        mRecyclerView.addItemDecoration(decor);
    }

    public void setLoadMoreEnable(boolean loadMoreEnable) {
        mRecyclerView.setLoadMoreEnable(loadMoreEnable);
    }
}

坑1:
在使用的過程當中,如果用的是notifyItemRangeInserted 或則 notifyItemRangeRemoved 刷新數據,而不是notifyDataSetChanged(),有可能會出錯IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter,上網查了很多方法,解決的方式是mRecyclerView.setItemAnimator(null);真是一臉懵逼,加個動畫居然還有問題。。。還是RecyclerView默認的自帶動畫。。。

坑2:
如果mAdapter.getItems().clear()之後,再加入數據,很有可能會發現RecyclerView仍然在底部,所以需要mRecyclerView.scrollToPosition(0); 返回頂部

坑3:
當顯示的item數量不夠多,無法撐滿屏幕高度時無論是上拉還是下拉,都會觸發onScrollStateChanged該方法,所以下拉需要滿足條件否則下拉也會觸發加載更多 所以當 visibleItemCount < totalItemCount 才觸發加載更多 即 item的數量夠多,並超過屏幕高度時,才觸發加載更多

坑4:
如果有人連續上拉加載更多,可能會導致數據錯亂,所以加一個boolean值isLoadMoreEnable,當上拉加載更多還沒有結束時,不讓其繼續上拉加載更多

溫馨提示:
自己封裝的RefreshLayout採用的adapter 是自己封裝的BaseBindingAdapter
我自己又額外封裝了一個方法
當請求後臺數據,獲得List後,直接調用completeHttpRequest方法,
就可以動態的實現FooterView 是 “正在加載…” 還是 “暫無更多數據”
非常方便

mRefreshLayout.completeHttpRequest(bean.getRooms());

MVVM模式下RecyclerView與databinding的結合
MVVM模式下RecyclerView與databinding的結合(2)

參考資料 https://blog.csdn.net/weixin_37577039/article/details/79214663

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章