UI之RecyclerView加载更多

作者: zly394 | 来源:发表于2016-09-21 18:37 被阅读807次

    效果图

    anglerNRD90Tzhuleiyue09212016183407.gif

    RecyclerView实现加载更多可分为两个步骤

    1. RecyclerView滑动到底部的监听
    2. 给RecyclerView添加footer,展示加载状态

    一、给RecyclerView添加ScrollListener监听滑动到底部

    1. 继承RecyclerView添加滑动监听

    public class LoadMoreRecyclerView extends RecyclerView {
    
        public LoadMoreRecyclerView(Context context) {
            this(context, null);
        }
    
        public LoadMoreRecyclerView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public LoadMoreRecyclerView(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
            init();
        }
    
        private void init() {
            addOnScrollListener(new OnScrollListener() {
                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                    super.onScrollStateChanged(recyclerView, newState);
                }
    
                @Override
                public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                    super.onScrolled(recyclerView, dx, dy);
                }
            });
        }
    }
    

    2. 判断滑动到底部

    2.1 判断滑动方向

    给LoadMoreRecyclerView添加属性

    /**
     * 是否是向下滑动
     */
    private boolean isScrollDown;
    

    在OnScrollListener中的onScrolled方法中判断RecyclerView的滑动方向

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        isScrollDown = dy > 0;
    }
    

    RecyclerView在滑动完成的时候会调用onScrolled方法,其中dx和dy分别表示水平滑动和垂直滑动的距离
    如果dy>0表示向下滑动

    2.2 判断滑动到底部

    在OnScrollListener中的onScrollStateChanged方法中判断RecyclerView是否滑动到底部

    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        if (newState == RecyclerView.SCROLL_STATE_IDLE) {// RecyclerView已经停止滑动
            int lastVisibleItem;
            // 获取RecyclerView的LayoutManager
            LayoutManager layoutManager = recyclerView.getLayoutManager();
            // 获取到最后一个可见的item
            if (layoutManager instanceof LinearLayoutManager) {// 如果是LinearLayoutManager
                lastVisibleItem = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
            } else if (layoutManager instanceof StaggeredGridLayoutManager) {// 如果是StaggeredGridLayoutManager
                int[] into = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()];
                ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(into);
                lastVisibleItem = findMax(into);
            } else {// 否则抛出异常
                throw new RuntimeException("Unsupported LayoutManager used");
            }
            // 获取item的总数
            int totalItemCount = layoutManager.getItemCount();
                /*
                    并且最后一个可见的item为最后一个item
                    并且是向下滑动
                 */
            if (lastVisibleItem >= totalItemCount - 1 && isScrollDown) {
                // 此处调用加载更多回调接口的回调方法
            }
        }
    }
    
    /**
     * 获取数组中的最大值
     *
     * @param lastPositions 需要找到最大值的数组
     * @return 数组中的最大值
     */
    private int findMax(int[] lastPositions) {
        int max = lastPositions[0];
        for (int value : lastPositions) {
            if (value > max) {
                max = value;
            }
        }
        return max;
    }
    

    RecyclerView的滑动状态改变时会调用onScrollStateChanged方法,其中newState表示RecyclerView的滑动状态

    • SCROLL_STATE_IDLE 表示RecyclerView没有在滑动
    • SCROLL_STATE_DRAGGING 表示RecyclerView正在被拖着滑动
    • SCROLL_STATE_SETTLING 表示RecyclerView正在滑动但是没有外部控制

    3. 添加加载更多的回调接口

    3.1 创建加载更多回调接口
    /**
     * 加载更多的回调接口
     */
    public interface OnLoadMore {
        void onLoad();
    }
    
    3.2 给RecyclerView添加设置回调的方法
    /**
     * 加载更多的回调接口
     */
    private OnLoadMore mOnLoadMore;
    
    /**
     * 是否加载更多
     */
    private boolean mIsLoadMore;
    
    /**
     * 设置加载更多的回调接口
     * @param onLoadMore 加载更多的回调接口
     */
    public void setOnLoadMore(OnLoadMore onLoadMore) {
        // 是否加载更多置为true
        this.mIsLoadMore = true;
        this.mOnLoadMore = onLoadMore;
    }
    

    在判断滑动到底部的地方调用回调接口的回调方法

    二、给RecyclerView添加footer展示加载状态

    1. 继承Adapter添加footer

    private static class LoadMoreAdapter extends Adapter {
        /**
         * 添加footer的类型
         */
        private static final int TYPE_FOOTER = -1;
        /**
         * footer的状态
         */
        protected int mLoadMoreStatus = STATUS_PREPARE;
        /**
         * footer的点击事件
         */
        protected View.OnClickListener mListener;
        /**
         * 正常item的adapter
         */
        private Adapter mAdapter;
        /**
         * 是否加载更多
         */
        private boolean mIsLoadMore;
        /**
         * GridLayoutManager
         */
        private GridLayoutManager mGridLayoutManager;
    
        public LoadMoreAdapter(Adapter adapter, boolean isLoadMore) {
            this.mAdapter = adapter;
            this.mIsLoadMore = isLoadMore;
        }
    
        @Override
        public void onAttachedToRecyclerView(RecyclerView recyclerView) {
            super.onAttachedToRecyclerView(recyclerView);
            if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
                this.mGridLayoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
            }
        }
    
        @Override
        public void onViewAttachedToWindow(ViewHolder holder) {
            super.onViewAttachedToWindow(holder);
            if (mIsLoadMore) {// 如果加载更多
                if (mGridLayoutManager != null) {
                    mGridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                        @Override
                        public int getSpanSize(int position) {
                            // 当position为最后一项时返回spanCount
                            return position == getItemCount() - 1 ? mGridLayoutManager.getSpanCount() : 1;
                        }
                    });
                }
                ViewGroup.LayoutParams params = holder.itemView.getLayoutParams();
                if (params instanceof StaggeredGridLayoutManager.LayoutParams) {
                    if (holder.getLayoutPosition() == getItemCount() - 1) { // 当position为最后一项时这是FullSpan为true
                        ((StaggeredGridLayoutManager.LayoutParams) params).setFullSpan(true);
                    }
                }
            }
        }
    
        /**
         * 如果是footer类型,创建FooterView
         * 否则创建正常的ItemView
         */
        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            if (mIsLoadMore && viewType == TYPE_FOOTER) {
                return onCreateFooterViewHolder(parent);
            } else {
                return mAdapter.onCreateViewHolder(parent, viewType);
            }
        }
    
        /**
         * 如果加载更多且是footer类型,则展示footer
         * 否则展示正常的item
         */
        @Override
        public void onBindViewHolder(ViewHolder holder, int position) {
            if (mIsLoadMore && getItemViewType(position) == TYPE_FOOTER) {
                bindFooterItem(holder);
            } else {
                mAdapter.onBindViewHolder(holder, position);
            }
        }
    
        /**
         * 如果加载更多
         * 如果正常的item为0  则不显示footer,返回0
         * 如果正常的item不为0  则返回mAdapter.getItemCount() + 1
         * 如果不加载更多
         * 返回mAdapter.getItemCount()
         */
        @Override
        public int getItemCount() {
            return mIsLoadMore ? mAdapter.getItemCount() == 0 ? 0 : mAdapter.getItemCount() + 1 : mAdapter.getItemCount();
        }
    
        /**
         * 如果加载更多且position为最有一个,则返回类型为footer
         * 否则返回mAdapter.getItemViewType(position)
         */
        @Override
        public int getItemViewType(int position) {
            if (mIsLoadMore && position == getItemCount() - 1) {
                return TYPE_FOOTER;
            } else {
                return mAdapter.getItemViewType(position);
            }
        }
    
        /**
         * 设置footer的状态,并通知更改
         */
        void setLoadMoreStatus(int status) {
            this.mLoadMoreStatus = status;
            notifyItemChanged(getItemCount() - 1);
        }
    
        /**
         * 设置footer的点击重试事件
         * @param listener
         */
        public void setRetryListener(View.OnClickListener listener) {
            this.mListener = listener;
        }
    
        public int getLoadMoreStatus() {
            return this.mLoadMoreStatus;
        }
    
        /**
         * 创建FooterView
         */
        public ViewHolder onCreateFooterViewHolder(ViewGroup parent) {
            return new FooterViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.footer_view_sample, parent, false));
        }
    
        /**
         * 设置是否加载更多
         */
        public void setIsLoadMore(boolean isLoadMore) {
            this.mIsLoadMore = isLoadMore;
        }
    
        /**
         * 展示FooterView
         * @param holder
         */
        protected void bindFooterItem(ViewHolder holder) {
            FooterViewHolder footerViewHolder = (FooterViewHolder) holder;
            switch (mLoadMoreStatus) {
                case STATUS_LOADING:
                    holder.itemView.setVisibility(View.VISIBLE);
                    footerViewHolder.pb.setVisibility(View.VISIBLE);
                    footerViewHolder.tv.setText("正在加载更多...");
                    break;
                case STATUS_EMPTY:
                    holder.itemView.setVisibility(View.VISIBLE);
                    footerViewHolder.pb.setVisibility(View.GONE);
                    footerViewHolder.tv.setText("没有更多了");
                    holder.itemView.setOnClickListener(null);
                    break;
                case STATUS_ERROR:
                    holder.itemView.setVisibility(View.VISIBLE);
                    footerViewHolder.pb.setVisibility(View.GONE);
                    footerViewHolder.tv.setText("加载出错,点击重试");
                    holder.itemView.setOnClickListener(mListener);
                    break;
                case STATUS_PREPARE:
                    holder.itemView.setVisibility(View.INVISIBLE);
                    break;
                case STATUS_DISMISS:
                    holder.itemView.setVisibility(GONE);
            }
        }
    }
    
    static class FooterViewHolder extends RecyclerView.ViewHolder {
        ProgressBar pb;
        TextView tv;
    
        public FooterViewHolder(View itemView) {
            super(itemView);
            pb = (ProgressBar) itemView.findViewById(R.id.pb_footer_view);
            tv = (TextView) itemView.findViewById(R.id.tv_footer_view);
        }
    }
    

    footer_view_sample.xml

    <?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="wrap_content"
        android:gravity="center_horizontal"
        android:orientation="horizontal"
        android:padding="16dp">
    
        <ProgressBar
            android:id="@+id/pb_footer_view"
            style="@android:style/Widget.ProgressBar.Small"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    
        <TextView
            android:id="@+id/tv_footer_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="8dp"
            android:gravity="center"
            android:text="正在加载更多..." />
    
    </LinearLayout>
    

    2. 设置在GridLayoutManager和StaggeredGridLayoutManager下footer撑满一行

    2.1 GridLayoutManager

    重写Adapter的onAttachedToRecyclerView,获取GridLayoutManager

    /**
     * GridLayoutManager
     */
    private GridLayoutManager mGridLayoutManager;
    
    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        super.onAttachedToRecyclerView(recyclerView);
        if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
            this.mGridLayoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
        }
    }
    

    重写onViewAttachedToWindow,给GridLayoutManager设置SpanSizeLookup

    @Override
    public void onViewAttachedToWindow(ViewHolder holder) {
        super.onViewAttachedToWindow(holder);
        if (mIsLoadMore) {// 如果加载更多
            if (mGridLayoutManager != null) {
                mGridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                    @Override
                    public int getSpanSize(int position) {
                        // 当position为最后一项时返回spanCount
                        return position == getItemCount() - 1 ? mGridLayoutManager.getSpanCount() : 1;
                    }
                });
            }
        }
    }
    
    2.2 StaggeredGridLayoutManager

    重写onViewAttachedToWindow

    @Override
    public void onViewAttachedToWindow(ViewHolder holder) {
        super.onViewAttachedToWindow(holder);
        if (mIsLoadMore) {// 如果加载更多
            ViewGroup.LayoutParams params = holder.itemView.getLayoutParams();
            if (params instanceof StaggeredGridLayoutManager.LayoutParams) {
                if (holder.getLayoutPosition() == getItemCount() - 1) { // 当position为最后一项时这是FullSpan为true
                    ((StaggeredGridLayoutManager.LayoutParams) params).setFullSpan(true);
                }
            }
        }
    }
    

    2. 在LoadMoreRecyclerView的滑动监听中添加判断并设置footer状态

    private void init() {
        addOnScrollListener(new OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if (mOnLoadMore != null) {// 如果加载更多的回调接口不为空
                    if (newState == RecyclerView.SCROLL_STATE_IDLE) {// RecyclerView已经停止滑动
                        ...
                        /*
                            如果RecyclerView的footer的状态为准备中
                            并且最后一个可见的item为最后一个item
                            并且是向下滑动
                         */
                        if (mLoadMoreAdapter.getLoadMoreStatus() == STATUS_PREPARE
                                && lastVisibleItem >= totalItemCount - 1 && isScrollDown) {
                            // 设置RecyclerView的footer的状态为加载中
                            mLoadMoreAdapter.setLoadMoreStatus(STATUS_LOADING);
                            // 触发加载更多的回调方法
                            mOnLoadMore.onLoad();
                        }
                    }
                }
            }
    
            ...
        });
    }
    

    3. 在LoadMoreRecyclerView中重写setAdapter方法,设置footer状态和点击事件

    private LoadMoreAdapter mLoadMoreAdapter;
    
    public void setAdapter(Adapter adapter) {
        this.mLoadMoreAdapter = new LoadMoreAdapter(adapter, mIsLoadMore);
        this.mLoadMoreAdapter.setRetryListener(retryListener);
        super.setAdapter(mLoadMoreAdapter);
    }
    
    /**
     * 设置footer的状态
     */
    public void setLoadMoreStatus(int status) {
        if (mLoadMoreAdapter != null) {
            mLoadMoreAdapter.setLoadMoreStatus(status);
        }
    }
    
    /**
     * footer的重试点击事件
     */
    View.OnClickListener retryListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            mLoadMoreAdapter.setLoadMoreStatus(STATUS_LOADING);
            mOnLoadMore.onLoad();
        }
    };
    

    ** ps: 关于onViewAttachedToWindow和onViewAttachedToWindow的说明 **

    • onViewAttachedToWindow方法在RecyclerView调用setAdapter方法是被调用

    • onViewAttachedToWindow方法在RecyclerView展示在界面上是被调用

      ** 为了保证LoadMoreRecyclerView中setOnLoadMore和setAdapter调用的无序性,不能在onViewAttachedToWindow方法中设置GridLayoutManager的SpanSizeLookup **

    三、使用注意

    ** 因为重写了RecyclerView的setAdapter方法,把传如的adapter包装之后重新设置,所以在调用notifyDataSetChanged()等方法时,不能直接用自己创建adapter调用,而要使用RecyclerView.getAdapter调用。**

    demo地址:https://github.com/zly394/LoadMoreRecyclerView

    相关文章

      网友评论

      • b0199aa97173:楼主,问下如果刷新后,怎么让其停留再底部呢?
        呦西小强:我大概是笨了点。。。会用 但是不太能看懂
        zly394: @dondurma 是要在加载完更多之后滑动到底部么?
      • 二月_:楼主,我想问一下,判断是否拉到最底部的方法可不可以放到adapter里去做?recycleview最好啥都不用去处理,用原生的控件就好,这样改动就变得最少了。
        zly394:@二月_ 很好的想法,可以在Adapter的onAttachedToRecyclerView(RecyclerView)回调方法里给RecyclerView添加监听。

      本文标题:UI之RecyclerView加载更多

      本文链接:https://www.haomeiwen.com/subject/ptbaettx.html