Android 实现自己的RecyclerView加载更多

很多时候,项目中都会有列表加载更多的场景,这次我们让RecyclerView轻松拥有加载更多的功能。虽然已有许多类似的轮子,但有的功能过于复杂,其实很多都用不到,所以不妨打造更适合自己的轮子。

我们的RecyclerView加载更多是通过其Adapter子类实现的,接下来我们一步步的构建Adapter吧!

1、编写通用的Adapter、ViewHolder

一般情况下使用Adapter都要为其创建一个ViewHolder,既然要编写通用的Adapter,首先要有一个通用的ViewHolder:

public class ViewHolder extends RecyclerView.ViewHolder {
    private SparseArray<View> mViews;
    private View mConvertView;

    private ViewHolder(View itemView) {
        super(itemView);
        mConvertView = itemView;
        mViews = new SparseArray<>();
    }

    public static ViewHolder create(Context context, int layoutId, ViewGroup parent) {
        View itemView = LayoutInflater.from(context).inflate(layoutId, parent, false);
        return new ViewHolder(itemView);
    }

    public static ViewHolder create(View itemView) {
        return new ViewHolder(itemView);
    }

    public <T extends View> T getView(int viewId) {
        View view = mViews.get(viewId);
        if (view == null) {
            view = mConvertView.findViewById(viewId);
            mViews.put(viewId, view);
        }
        return (T) view;
    }

    public View getConvertView() {
        return mConvertView;
    }

    public void setText(int viewId, String text) {
        TextView textView = getView(viewId);
        textView.setText(text);
    }
    .......省略其它辅助方法.........
}

我们自定义的ViewHolder类可以根据布局文件的id或具体的itemView返回一个ViewHolder对象,并用SparseArray来缓存我们itemView中的子View,避免每次都要去解析子View,同时提供相关辅助方法设置itemView的内容。有了ViewHolder,接下来编写Adapter就简单了:

public abstract class BaseAdapter<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    public static final int TYPE_COMMON_VIEW = 100001;
  
    private OnItemClickListeners<T> mItemClickListener;

    protected Context mContext;
    protected List<T> mDatas;

    protected abstract void convert(ViewHolder holder, T data);

    protected abstract int getItemLayoutId();

    public BaseAdapter(Context context, List<T> datas) {
        mContext = context;
        mDatas = datas == null ? new ArrayList<T>() : datas;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ViewHolder viewHolder = null;
        switch (viewType) {
            case TYPE_COMMON_VIEW:
                viewHolder = ViewHolder.create(mContext, getItemLayoutId(), parent);
                break;
        }
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        switch (holder.getItemViewType()) {
            case TYPE_COMMON_VIEW:
                bindCommonItem(holder, position);
                break;
        }
    }

    private void bindCommonItem(RecyclerView.ViewHolder holder, final int position) {
        final ViewHolder viewHolder = (ViewHolder) holder;
        convert(viewHolder, mDatas.get(position));
        viewHolder.getConvertView().setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mItemClickListener.onItemClick(viewHolder, mDatas.get(position), position);
            }
        });
    }

    @Override
    public int getItemCount() {
        return mDatas.size();
    }

    @Override
    public int getItemViewType(int position) {
        return TYPE_COMMON_VIEW;
    }

    public T getItem(int position) {
        if (mDatas.isEmpty()) {
            return null;
        }
        return mDatas.get(position);
    }

    public void setOnItemClickListener(OnItemClickListeners<T> itemClickListener) {
        mItemClickListener = itemClickListener;
    }
}

很简单,继承RecyclerView.Adapter,重写相关方法,提供了getItemLayoutId()convert()两个抽象方法供BaseAdapter的子类实现,来初始化item的布局id,以及item内容,同时通过OnItemClickListeners接口为item绑定点击事件。

编写好了Adapter,我们在其构造方法中添加一个参数isOpenLoadMore,来表示是否开启加载更多:

public BaseAdapter(Context context, List<T> datas, boolean isOpenLoadMore) {
        mContext = context;
        mDatas = datas == null ? new ArrayList<T>() : datas;
        mOpenLoadMore = isOpenLoadMore;
    }

这样初级版本的Adapter就完成了。

2、添加Footer View

接下来就要添加Footer View,这样才能有加载更多的视觉效果么。其实很简单,如果当前item的position满足如下条件:

private boolean isFooterView(int position) {
        return mOpenLoadMore && position >= getItemCount() - 1;
    }

即已经开启加载更多、当前position在列表的尾部,则在getItemViewType()返回

@Override
    public int getItemViewType(int position) {
        if (isFooterView(position)) {
            return TYPE_FOOTER_VIEW;
        }
    }

之后会创建Footer View对应的ViewHolder:

@Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ViewHolder viewHolder = null;
        switch (viewType) {
            case TYPE_FOOTER_VIEW:
                if (mFooterLayout == null) {
                    mFooterLayout = new RelativeLayout(mContext);
                }
                viewHolder = ViewHolder.create(mFooterLayout);
                break;
        }
        return viewHolder;
    }

可以看到mFooterLayout是一个空的Container,因为要根据加载更多对应的状态来更新mFooterLayout,这个稍后再说。

这样Footer View就添加完了吗?当然没有,我们需要针对StaggeredGridLayoutManager、GridLayoutManager模式分别重写onViewAttachedToWindow()onAttachedToRecyclerView()方法,否则会出现Footer View不能在列表底部占据一行的问题:

@Override
    public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
        super.onViewAttachedToWindow(holder);
        if (isFooterView(holder.getLayoutPosition())) {
            ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();

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

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        super.onAttachedToRecyclerView(recyclerView);
        final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        if (layoutManager instanceof GridLayoutManager) {
            final GridLayoutManager gridManager = ((GridLayoutManager) layoutManager);
            gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    if (isFooterView(position)) {
                        return gridManager.getSpanCount();
                    }
                    return 1;
                }
            });
        }
    }

到此无论是那种形式的列表都能正常添加Footer View了。

3、判断列表是否滚动到了底部

按照常理,只有滑动到列表的底部才会触发加载更多的操作,之前提到了onAttachedToRecyclerView()方法,通过该方法可以得到Adapter所绑定的RecyclerView,这样就能监听RecyclerView的滚动事件,进而判断列表是否滚动了底部:

private void startLoadMore(RecyclerView recyclerView, final RecyclerView.LayoutManager layoutManager) {
        if (!mOpenLoadMore || mLoadMoreListener == null) {
            return;
        }

        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                    if (!isAutoLoadMore && findLastVisibleItemPosition(layoutManager) + 1 == getItemCount()) {
                        scrollLoadMore();
                    }
                }
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                if (isAutoLoadMore && findLastVisibleItemPosition(layoutManager) + 1 == getItemCount()) {
                    scrollLoadMore();
                } else if (isAutoLoadMore) {
                    isAutoLoadMore = false;
                }
            }
        });
    }

我们单独封装了startLoadMore()方法,当列表滚动状态改变会回调onScrollStateChanged()方法,如果状态为SCROLL_STATE_IDLE,并且当前可见的item位置为列表最后一项,则开始加载更多数据。这里还重写了onScrolled()方法,当列表滚动结束后会回调,重写该方法有什么用呢?如果初始item不满一屏幕,则可在该方法中加载更多数据,直到item占满一屏幕,也就自动加载更多。我们用isAutoLoadMore来区分这种情况,如果isAutoLoadMore为true,则Footer View可见则自动加载更多。

再看一下scrollLoadMore()方法:

private void scrollLoadMore() {
        if (mFooterLayout.getChildAt(0) == mLoadingView) {
            mLoadMoreListener.onLoadMore(false);
        }
    }

如果当前的Footer View 是正在加载的状态,则调用OnLoadMoreListener接口的onLoadMore()方法进行具体的加载操作,该方法有一个boolean类型的参数,表示是否重新加载,因为存在加载失败的情况,这样可方便使用。

4、更新Footer View布局样式

到这里,我们已经明确了加载更多操作的触发时机,接下来就是在加载更多的时候来更新Footer View,我们定义了三种状态:加载中、加载失败、加载结束,通过如下方法将对应状态的View或布局id添加到Footer View中:

public void setLoadingView(int loadingId) {
        setLoadingView(Util.inflate(mContext, loadingId));
    }

public void setLoadFailedView(int loadFailedId) {
        setLoadFailedView(Util.inflate(mContext, loadFailedId));
    }

public void setLoadEndView(int loadEndId) {
        setLoadEndView(Util.inflate(mContext, loadEndId));
    }

这三个方法时是通过布局id来给Footer View设置新样式,当然还有通过View来设置的重载方法。在初始化Adapter时可以调用setLoadingView()来设置加载中的Footer View样式,如果加载失败了可调用setLoadFailedView()、如果加载结束没有更多数据则可以调用setLoadEndView()设对应的布局样式。其实就是先移除mFooterLayout的子View,然后将新的布局添加进去。

5、添加EmptyView

考虑一种情况,如果初始化时,需要先从网络请求数据,然后再更新列表,则一般需要有一个加载提示,所以我们有必要将这个小功能也封装到Adapter中,这样就省去了修改界面布局或者手动显示、隐藏加载提示的步骤。
实现也很简单,先看如下代码:

@Override
    public int getItemCount() {
        if (mDatas.isEmpty() && mEmptyView != null) {
            return 1;
        }
    }

如果mData为空,且设置了EmptyView则getItemCount()直接返回1。同理返回的item类型为TYPE_EMPTY_VIEW,代表EmptyView:

@Override
    public int getItemViewType(int position) {
        if (mDatas.isEmpty() && mEmptyView != null) {
            return TYPE_EMPTY_VIEW;
        }
    }

onCreateViewHolder()方法中会创建对应的ViewHolder。

@Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ViewHolder viewHolder = null;
        switch (viewType) {
            case TYPE_EMPTY_VIEW:
                viewHolder = ViewHolder.create(mEmptyView);
                break;
        }
        return viewHolder;
    }

同时提供方法在初始化Adapter时设置EmptyView:

public void setEmptyView(View emptyView) {
        mEmptyView = emptyView;
    }

6、具体使用

完成了封装,来看看具体的使用,首先创建一个RefreshAdapter继承我们的BaseAdapter:

public class RefreshAdapter extends BaseAdapter<String> {

    public RefreshAdapter(Context context, List<String> datas, boolean isLoadMore) {
        super(context, datas, isLoadMore);
    }

    @Override
    protected void convert(ViewHolder holder, final String data) {
        holder.setText(R.id.item_title, data);
        holder.setOnClickListener(R.id.item_btn, new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(mContext, "我是" + data + "的button", Toast.LENGTH_SHORT).show();
            }
        });
    }

    @Override
    protected int getItemLayoutId() {
        return R.layout.item_layout;
    }
}

getItemLayoutId()中返回item布局id,在convert()中初始化item的内容。有了RefreshAdapter,接下来看Activity的操作:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mRecyclerView = (RecyclerView) findViewById(R.id.recyclerview);

        //初始化adapter
        mAdapter = new RefreshAdapter(this, null, true);

        //初始化EmptyView
        View emptyView = LayoutInflater.from(this).inflate(R.layout.empty_layout, (ViewGroup) mRecyclerView.getParent(), false);
        mAdapter.setEmptyView(emptyView);

        //初始化 开始加载更多的loading View
        mAdapter.setLoadingView(R.layout.load_loading_layout);

        //设置加载更多触发的事件监听
        mAdapter.setOnLoadMoreListener(new OnLoadMoreListener() {
            @Override
            public void onLoadMore(boolean isReload) {
                loadMore();
            }
        });

        //设置item点击事件监听
        mAdapter.setOnItemClickListener(new OnItemClickListeners<String>() {

            @Override
            public void onItemClick(ViewHolder viewHolder, String data, int position) {
                Toast.makeText(MainActivity.this, data, Toast.LENGTH_SHORT).show();
            }
        });

        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        mRecyclerView.setLayoutManager(layoutManager);

        mRecyclerView.setAdapter(mAdapter);


        //延时3s刷新列表
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                List<String> data = new ArrayList<>();
                for (int i = 0; i < 12; i++) {
                    data.add("item--" + i);
                }
                //刷新数据
                mAdapter.setNewData(data);
            }
        }, 3000);
    }

注释已经很详细了,就不多说了。其中loadMore()方法如下:

private void loadMore() {

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {

                if (mAdapter.getItemCount() > 15 && isFailed) {
                    isFailed = false;
                    //加载失败,更新footer view提示
                    mAdapter.setLoadFailedView(R.layout.load_failed_layout);
                } else if (mAdapter.getItemCount() > 17) {
                    //加载完成,更新footer view提示
                    mAdapter.setLoadEndView(R.layout.load_end_layout);
                } else {
                    final List<String> data = new ArrayList<>();
                    for (int i = 0; i < 2; i++) {
                        data.add("item--" + (mAdapter.getItemCount() + i - 1));
                    }
                    //刷新数据
                    mAdapter.setLoadMoreData(data);
                }
            }
        }, 2000);
    }

就是延时2s更新列表数据,同时人为模拟加载失败和结束的情况。

7、效果

运行后,看具体的效果:

EmptyView
loading
load_failed
load_end
auto_load

PS:更新

(1)重构基类继承关系
(2)支持多种类型的Item View


创建只有一种类型的Item View的Adapter时,直接继承CommonBaseAdapter类即可,其它操作不变。

创建有多种类型的Item View的Adapter时时,继承MultiBaseAdapter即可,实例如下:

public class MultiRefreshAdapter extends MultiBaseAdapter<String> {

    public MultiRefreshAdapter(Context context, List<String> datas, boolean isOpenLoadMore) {
        super(context, datas, isOpenLoadMore);
    }

    @Override
    protected void convert(ViewHolder holder, final String data, int viewType) {
        if (viewType == 0) {
            holder.setText(R.id.item_title, data);
            holder.setOnClickListener(R.id.item_btn, new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Toast.makeText(mContext, "我是" + data + "的button", Toast.LENGTH_SHORT).show();
                }
            });
        } else {
            holder.setText(R.id.item_title1, data);
        }
    }

    @Override
    protected int getItemLayoutId(int viewType) {
        if (viewType == 0) {
            return R.layout.item_layout;
        }
        return R.layout.item_layout1;
    }

    @Override
    protected int getViewType(int position, String data) {
        if (position % 2 == 0) {
            return 0;
        }
        return 1;
    }
}

设置Item点击事件时,通过如下方法:

mAdapter.setOnMultiItemClickListener(new OnMultiItemClickListeners<String>() {
            @Override
            public void onItemClick(ViewHolder viewHolder, String data, int position, int viewType) {
                
            }
        });

其它的操作不变。效果就不贴了,可通过源码查看。

2016.12.6更新

使用EmptyView时,初始加载无数据可移除EmptyView,或添加新ReloadView以便进行重新加载、提示等操作。

2017.7.4更新

支持Adapter重置、完善使用方式

2017.12.22更新

  1. 支持给RecyclerView添加HeaderView
  2. 自动判断是否正在加载更多,避免重复加载

更多详情可参考源码,不合理的地方还求反馈!
☞源码戳这里

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

推荐阅读更多精彩内容