项目需求讨论-RecycleView分页加载实现分析

我已经写了个Demo上传到GitHub上了。大家可以看看。BaseLoadAdapter

大家好,又是新的一期项目需求讨论,这期的需求是关于分页加载。我本来先是网上看RecycleView的分页加载的方式,但是看到很多文章都是帮你封装好,然后让你拿来直接用,一是直接拿别人封装的东西自己还是不理解,二是如果要加定制化的东西,改别人的代码毕竟不方便,或者你就用了一个功能,别人封装好的可能包含很多功能,就多余了。所以我主要还是来分析,分页加载到底是怎么样一步步来实现,而不是说封装好来让大家使用。

什么是分页加载,通俗的说就是,比如你在微信朋友圈,可能今天一共有100个别人发在朋友圈的状态:
有二种方式加载方式:

  1. 后台是直接把100个别人发的状态一次性给你了,然后你在列表上层显示100个朋友圈状态,然后上下滑动查看。
  2. 可能后台先给你10个朋友圈状态,然后当你拉到底的时候,显示<加载中>,然后再去像后台请求后面10条朋友圈状态,然后再滑到底部,再去加载10个新的数据。一直到最后100个数据都加载完了。就在底部显示<没有更多数据>了。用户也就知道今天朋友圈状态已经看完了。

优缺点
第一种加载开发起来方便,简单。可以直接下滑看全部状态,不需要看几条,等它加载更多后,再看几条,再等着加载再去看。但是如果你只看了前面5个朋友圈状态,却把100条的数据都发给你,一个是流量问题,一个是加载的速度问题。毕竟数据变多了,而且万一有好几千条数据怎么办。

第二种开发起来麻烦,要设置多种状态。比如滑到底了要去再去获取信息,然后显示<加载中>,如果还有数据就加入,没有数据再去显示<没用更多数据>。然后假如获取失败,还要显示<加载失败>。但是弥补了上述的第一种方法的缺点

所以第一种更适合用于条数固定,或者条数不多的情况下。开发方便。比如微信的联系人列表。一般都是直接全部层显,不会说我先显示几个联系人,然后下拉再加载再去加载剩下的联系人。第二种更适合数据会不停的变多的情况,比如你的某个软件有个交易查询功能,查询你的交易记录,虽然刚开始你的列表上的数据比较少,但是随着时间的推移,你的数据也会越来越多。所以就更适合第二种方式。


好了我们开始我们的正题,也正是项目中遇到的具体需求。

后台接口:

现在是一个交易记录列表,后台给我的接口是这样的:第一次给我10个数据,我这边就先显示10个,然后上拉到底的时候,把最后一个数据的orderid(也就是订单id)给他,他再根据这个id,加载接下来这个订单后面的10个数据给我。
(以前还有一种接口是这样的。比如第一次要数据的时候给我10条,然后同时给我一个页数的字段,告诉我如果是一页10条的话,一共有几页,然后我后面再去加载数据的时候就传页数即可。)

(以下为了方便。我都假设每次后台最多传递给我4个数据。)

第一步:

第一次调用接口拿数据,分二种情况:

  1. 第一次给我就没有4条数据,比如就给我3条,那就说明肯定没有其他数据了。这时候你就算拉到最下面,也不需要显示什么加载更多的显示。(别问我为啥。因为如果还有更多,最少也要给你4条)
  1. 如果给了你4条,这时候你滑到底部就要显示<加载中>。因为有可能说明后面还有数据。
    那我们怎么样才能滑到下面的时候能看到<加载中>这个呢,其实很简单,把这个<加载中>也作为RecycleView的列表中的一项即可。
    如下图所示:

这样是不是当你滑到最下面的时候一定能看到<加载中>这一项了。

所以在第一次访问的时候,我们的RecycleView的adapter中返回列表的个数要进行判断。如果是小于4条(就是跟后台约定好的条数),那adapter中item的个数直接返回就是实际的条数,比如返回三条,那我们列表就只要显示3条即可。如果是返回了4条,那么我们这时候adapter中item的个数就返回4+1 条了。(4条数据外加一个<加载中>这一项)。

第二步:

我们既然我们知道我们需要有<加载中>这一项,那我们就肯定知道这个<加载中>跟我们上面的具体的一项项数据的布局肯定不一样。比如我上面实际开发中,上面的数据布局是交易记录。那我们就来看怎么实现这个RecycleView的列表中如何层显不同布局。
我们自定义一个BaseLoadAdapter继承RecycleView.Adapter。然后覆写public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)方法。
这里面有个viewType,我们可以根据不同的viewType来返回不同的ViewHolder即可。那这个viewType又是怎么来的。就是复写
public int getItemViewType(int position)方法,不同的position
的item,返回特定的viewType即可。

所以我们这里就是:

public class BaseLoadAdapter<T> extends RecyclerView.Adapter {

    public List<T> list;
    public static final int TYPE_OTHER = 1;
    public static final int TYPE_BOTTOM = 2;

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        if (TYPE_BOTTOM == viewType) {
            //返回我们的那个加载中的布局Viewholder
            return new NewBottomViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_footer_new, parent, false));
        } else {
            //返回我们的交易记录的布局Viewholder
            return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_transferexam_info, parent, false));
        }
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (getItemViewType(position) == TYPE_BOTTOM) {
            //对相应的onBindViewHolder进行处理
            LinearLayout container = ((BaseLoadMoreAdapter.NewBottomViewHolder) holder).container;
            final ProgressBar pb = ((BaseLoadMoreAdapter.NewBottomViewHolder) holder).pb;
            final TextView content = ((BaseLoadMoreAdapter.NewBottomViewHolder) holder).content;

            .................
            .................
            .................
        } else {
            //对具体的交易记录的itemView进行相应的控件进行处理。
            TransferExamItemBean bean = ((TransferExamItemBean) list.get(position));
            holder.itemView.setTag(bean);
            ((ExamRefreshAdapter.MyViewHolder) holder).name.setText(bean.getToCompanyName());
            ((ExamRefreshAdapter.MyViewHolder) holder).date.setText(bean.getCreateDate());
            ((ExamRefreshAdapter.MyViewHolder) holder).money.setText(bean.getAmount()+"");
        }
    }

    @Override
    public int getItemCount() {
        return list.size() < 4 ? list.size() : list.size() + 1;
    }

    @Override
    public int getItemViewType(int position) {
        if (!list.isEmpty() && list.size() < position ) {
            return TYPE_OTHER;
        } else {
            return TYPE_BOTTOM;
        }
    }
}

第三步:

好了,现在我们已经可以滑到下面的时候能看到<加载中>这一项了。因为我们看到<加载中>的时候要继续去向后台访问获取数据,说明当滑到底部看到这个<加载中>的时候我们就要去调用相应的后台接口去获取接下来的交易记录数据。那问题就变成了:我们怎么知道我们已经滑到了底部并且已经出现了<加载中>这一项,然后进行网络接口调用。

自定义继承RecyclerView.OnScrollListener,复写public void onScrolled(RecyclerView recyclerView, int dx, int dy)方法,我们就可以监听RecycleView的滑动了。

public class LoadMoreScrollListener etends RecyclerView.OnScrollListener {

    private RecyclerView mRecyclerView;

    public LoadMoreScrollListener(RecyclerView recyclerView) {
        this.mRecyclerView = recyclerView;
    }

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);

        RecyclerView.LayoutManager manager = mRecyclerView.getLayoutManager();
        BaseLoadMoreAdapter adapter = (BaseLoadMoreAdapter) mRecyclerView.getAdapter();

        if (null == manager) {
            throw new RuntimeException("you should call setLayoutManager() first!!");
        }
        if (manager instanceof LinearLayoutManager) {
            int lastCompletelyVisibleItemPosition = ((LinearLayoutManager) manager).findLastCompletelyVisibleItemPosition();
           
            if (adapter.getItemCount() > 4 &&
                    lastCompletelyVisibleItemPosition == adapter.getItemCount() - 1 &&
                    adapter.isHasMore()) {
                    
                adapter.isLoadingMore();

            }
        }
    }
}

说明:

  • adapter.getItemCount():获取adapter一共有多少项。
  • findLastCompletelyVisibleItemPosition():由字面意思就可以看懂,返回最后一个完全可见的item项的position值。因为position是从0开始的,所以当findLastCompletelyVisibleItemPosition()返回的是adapter.getItemCount() - 1的时候,就说明已经可以看到最后一项了。
  • adapter.isHasMore():这个方法是我们自己在adapter中自定义的方法,返回一个boolean值,比如我们再次调用后台接口获取数据的时候,后台给我们返回的数据已经为空了。那我们就知道我们后面已经无法加载更多数据了。这时候把这个boolean值设为false,这样在监听滑动的时候就算滑到最底下也不需要去再次调用接口。
  • adapter.isLoadingMore():这个方法也是我们自己在adapter中自定义的方法,去调用后台接口。获取数据等后续操作。

然后进行监听即可recyclerView.addOnScrollListener(new LoadMoreScrollListener(recyclerView));

第四步:

底部这个<加载中>item在以后会有二种状态,一种是<加载失败>选项,一种是后台给的数据为空后的<没有更多>选项。
而我们第一次滑到底部的时候,总是先显示<加载中>。
因为这个最后一个选项会有三种状态显示情况。(即:<加载中>,<加载失败>,<加载更多>)所以定义一个变量。用来记录最后一项当前的状态。

public int loadState;
int STATE_LOADING = 1;
int STATE_LASTED = 2;
int STATE_ERROR = 3;

因为我们在滑到底部的时候去调用我们自己定义在adapter中的自定义方法isLoadingMore(),这个方法里面是什么内容呢:

public final void isLoadingMore() {
        if (loadState == STATE_LOADING) {
            return;
        }
        loadState = STATE_LOADING;
        notifyItemRangeChanged(getItemRealCount(), 1);
}

没错,我们就是默认先让当前最后一项的状态先变为STATE_LOADING,然后去刷新最后一项的内容,notifyItemRangeChanged(int positionStart, int itemCount)方法,从字面意思就能看出通知某个范围内的数据发生改变了。从posistionStart开始的itemCount个数据发生变化。我们因为是最后一项,它的position是list.size(),然后个数是一个,所以是notifyItemRangeChanged(getItemRealCount(), 1);

然后在通知最后一项发生变化后我们的onBindViewHolder就会再次被调用,这时候我们就要根据相应的不同STATE状况下对这个最后一项的布局进行相应的处理:

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (getItemViewType(position) == TYPE_BOTTOM) {
            //对相应的onBindViewHolder进行处理
            LinearLayout container = ((BaseLoadMoreAdapter.NewBottomViewHolder) holder).container;
            final ProgressBar pb = ((BaseLoadMoreAdapter.NewBottomViewHolder) holder).pb;
            final TextView content = ((BaseLoadMoreAdapter.NewBottomViewHolder) holder).content;
          

           //根据不同state来进行相应处理
            switch (loadState) {
                //1.当state是STATE_LOADING,
                //那我们就知道要把最后一项的字变为“加载中”
                //并且要让我写在布局中的滚动条进行显示(一般在加载中才会有滚动条的显示)
                //这时候调用我们的自定义方法loadMoreListener.onLoadMore();方法,这个方法是用来访问后台接口,然后去获取数据的。
                case AdapterLoader.STATE_LOADING:
                    content.setText("加载中");
                    container.setOnClickListener(null);
                    pb.setVisibility(View.VISIBLE);
                    if (loadMoreListener != null) {
                        loadMoreListener.onLoadMore();
                    }
                    
                    break;
                  
                //2.当state是STATE_LASTED的时候
                //最后一项的字变为“没有更多了”
                //我们的加载进度条也可以隐藏了
                case AdapterLoader.STATE_LASTED:
                    pb.setVisibility(View.GONE);       
                    container.setOnClickListener(null);
                    content.setText("---  没有更多了  ---");

                    //大家还记不记得我们在监听滑动的时候,我们有个adapter.isHasMore()变量作为控制,
                    //当我们的状态已经变为了STATE_LASTED了。那我们也不需要再监听是否滑到了最底部了。因为已经加载全部了。
                    adapter.setHasMore(false);

                    break;
                

                //3.当state是STATE_ERROR的时候
                //最后一项的字变为“加载更多失败点击重试”
                //我们的加载进度条也可以隐藏了
                //这里会跟其他二个状态不同的地方,那就是当加载失败的时候,我们可以通过点击这项,再去重新加载。
                //所以就要在最后一项中添加一个点击事件。所以在其他二个状态下,要重新设置setOnClickListener(null),来取消这个重新加载的点击事件。
                case AdapterLoader.STATE_ERROR:
                    pb.setVisibility(View.GONE);
                    content.setText("--- 加载更多失败点击重试 ---");
                    container.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            if (loadMoreListener != null) {
                                loadMoreListener.onLoadMore();
                            }
                            content.setText("加载中");
                            pb.setVisibility(View.VISIBLE);
                        }
                    });

                    break;
                default:
                    break;
            }

        } else {
            //对具体的交易记录的itemView进行相应的控件进行处理。
            TransferExamItemBean bean = ((TransferExamItemBean) list.get(position));
            holder.itemView.setTag(bean);
            ((ExamRefreshAdapter.MyViewHolder) holder).name.setText(bean.getToCompanyName());
            ((ExamRefreshAdapter.MyViewHolder) holder).date.setText(bean.getCreateDate());
            ((ExamRefreshAdapter.MyViewHolder) holder).money.setText(bean.getAmount()+"");
        }
    }

好了,所以现在的情况是,滑到底部,然后通知去刷新底部的item,因为刚开始默认是STATE_LOADING,所以在刷新创建这底部这项的时候,就会按照我们写的判断。出现加载框,文件显示“加载中”,然后会运行我们写的向后台获取数据的接口。然后我们只要在访问后台接口,根据返回的情况,适当的更改底部item的状态,然后再去刷新底部item,就可以了。

第五步:

我们滑到了底部,调用了我们的获取数据的接口代码,这时候我们要分三种情况来处理:

  1. 如果后台给我们的是四个数据,那说明有可能后面还会有数据,那我们这时候拿到四条数据后,只需要在最后一项前面插入,这样的话,最后一项的状态也不需要改变。

这时候我们把新加载的四条数据插在<加载中>的前面,然后我们对于最后一项不需要做处理,这样当我们往下滑的时候。又会重新跑一遍上面的逻辑。(也就是再次看到最后一项,调用notifyItemRangeChanged方法,然后根据状态去刷新最后一项,然后因为我们没改变过状态,还是STATE_LOADING,所以又再去向后台拿数据。)

我们在adapter中定义方法:

public final void appendList(List<T> data) {
        int positionStart = list.size();
        list.addAll(data);
        int itemCount = list.size() - positionStart;

        if (positionStart == 0) {
            notifyDataSetChanged();
        } else {
            notifyItemRangeInserted(positionStart + 1, itemCount);
        }
    }

假设我们已经拿到了后台给我们的list数据,这时候我们判断下这个list数据个数是不是等于4,如果等于4,我们就调用adapter.appendList(list)即可

2.如果后台给你的数据是小于四个的,这时我们要设置我们的adapter中最后一项的状态为STATE_LASTED,然后也要调用adapter.appendList(list);

3.查看后台返回的json中的code值是不是200(比如code== 200说明获取数据成功),我们获取到的数据时候,就对code做判断。如果不是200,那我们就把adapter中的状态变为STATE_ERROR。然后再调用notifyItemRangeChanged去刷新一下最后一项即可。这样最后一项就变成了<加载失败>,并且具有了点击重新加载的功能。

注意,比如我们已经滑到最下面了。这时候去调用我们后台的接口了。这时候,最好前面用一个boolean值去做判断。比如下面这个方法是我的访问后台接口方法:

public void onLoadMore() {
        if (isRun) {
            return;
        }
        isRun = true;
        presenter.getTransferExamList("zjzt", lastOrderID);
    }

防止重复滑到下面去调用多次后台接口,当后台接口返回数据后,再设置isRun = false即可。

我先大致写到这里。后面再贴上完整的代码,我主要先写的还是对分页加载来进行分析。thanks。哪里不对,请指教。

推荐阅读更多精彩内容