UI之RecyclerView加载更多

效果图

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,561评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,218评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,162评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,470评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,550评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,806评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,951评论 3 407
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,712评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,166评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,510评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,643评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,306评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,930评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,745评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,983评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,351评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,509评论 2 348

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,770评论 25 707
  • 简介: 提供一个让有限的窗口变成一个大数据集的灵活视图。 术语表: Adapter:RecyclerView的子类...
    酷泡泡阅读 5,148评论 0 16
  • 很多时候,项目中都会有列表加载更多的场景,这次我们让RecyclerView轻松拥有加载更多的功能。虽然已有许多类...
    SheHuan阅读 28,741评论 45 184
  • 这个世界没有任何个体不具有存在的意义。如果当真是这样,那么那个问题(究竟是我们改变了世界还是世界改变了我们)就有了...
    大婷和小贝阅读 664评论 0 1
  • 看了伯乐在线里的一篇文章,自己面试的时候也被问到了.挑了重点来记录记录.点击查看 为什么HTML5里面我们不需要D...
    神奇的少年阅读 467评论 0 0