美文网首页小技巧APP开发经验总结Android进阶之路
Android 实现自己的RecyclerView加载更多

Android 实现自己的RecyclerView加载更多

作者: SheHuan | 来源:发表于2016-09-02 11:01 被阅读12425次

    很多时候,项目中都会有列表加载更多的场景,这次我们让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. 自动判断是否正在加载更多,避免重复加载

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

    相关文章

      网友评论

      • Obsession丶执:当数据只有一页且不满一屏时,如何让他不去自动加载,即不要在下面显示loadEndView“没有更多了”
      • 可以再长高10cm:有一个问题不知道楼主注意了没有。如果设置isAutoLoadMore为false,然后请求数据的话,如果第一页的数据不够一屏,就会一直处于 “努力加载中” ,不会去走 loadMore的那一步,这个是什么原因呢?
      • 啟风了:写的很好,谢谢楼主
      • 可以再长高10cm:不满一屏会自动加载更多,如果不让它自动加载呢?
      • 码农的world:有一个bug,就是设置一个setloadingview 当数据占不满屏幕的时候会一直显示在页面上
        码农的world:当把刷新加上得时候会和加载更多冲突 刷新和加载更多会一直显示在页面上 楼主可以把刷新加上试试
        码农的world:@Othershe 楼主可以把刷新加上呀
        SheHuan:所有数据加载完了,调用下adapter的loadEnd()方法
      • RoyAlex:在加载下一页的时候,多划两次会导致onScrollStateIdle回调两次,因此会导致一页加载两次
        SheHuan:@RoyAlex 在onLoadMore(boolean isReload)回调中,做一下判断,如果当前分页请求没结束,又触发了下一次,就return掉onLoadMore()方法,至于是否结束可用页码判断。
        RoyAlex:@Othershe 滑动速度快的话一个方法差不多是同时进行了2次,所以我觉得仍然有问题
        SheHuan: @RoyAlex 这个问题文中有解决方案哦
      • 3537bbdd1e1f:模拟初次加载失败时不能进入加载失败页面,而是会启动setOnLoadMoreListener监听加载数据然后直接跳转到item页面
      • 空老表:上拉加载不回调是怎么回事呢
      • BenjaminCool:给了我思路
      • 天神Deity:你好,从最开始的没有任何数据到加载失败,无法从Empty View 跳转到 LoadFail View中
        天神Deity:@冯文华 该问题已处理
      • b6c7a3ded5ff: FATAL EXCEPTION: main
        Process: com.othershe.recyclerviewadapter, PID: 27046
        java.lang.ClassCastException: android.view.View cannot be cast to android.view.ViewGroup
        at com.othershe.baseadapter.base.CommonBaseAdapter.bindCommonItem(CommonBaseAdapter.java:64)

        你的dome,CommonBaseAdapter有个BUG
        b6c7a3ded5ff:@VipOthershe :+1:
        SheHuan:@dyhuang bug已经修复
      • SScience:请问一下,如果recyclerview之前添加CollapsingToolbarLayout(或其他的控件)时,数据为空的时会有加载动画(正常加载动画控件会在正中央),但是由于CollapsingToolbarLayout存在,加载动画控件会在屏幕底部,甚至会离开屏幕,也就是这个空数据时的加载动画控件的高度始终是屏幕高度。但是有CollapsingToolbarLayout时,正常情况加载动画控件应该位于CollapsingToolbarLayout底部到屏幕底部中间的位置。想问下博主如何解决啊?这是不正常的加载控件位置http://7xthed.com1.z0.glb.clouddn.com/device-2016-11-13-235530.mp4
      • Q_JR:@VipOthershe adapter.setOnLoadMoreListener必须写在recyclerView.setAdapter(adapter) 之前,写在之后加载更多就无效了 而adapter.setOnItemClickListener却不影响
        有兴不虚昧:@Q_JR 前面项目赶时间,朋友推荐用这个,我试了加载更多不行,我就换了。不知道是不是你说的这样
      • tczyb:不满一屏也会自动加载?
        SheHuan: @tczyb 目前是这样设定的
      • 囧_囧:你好 我发现代码里CommonRefreshAdapter 继承 CommonBaseAdapter 继承 BaseAdapter,请问下 有必要套这么深吗?多余了吧
        SheHuan:@囧_囧 BaseAdapter是处理加载更多需求的,它的两个子类是处理单Item和多Item情况的,写到一个基类不好扩展。
      • 不可不知不用:如果itemd的布局不同,是怎么玩。
        不可不知不用:@VipOthershe :+1: 恩恩、
        SheHuan:@dang当年情 多item布局已经支持了哦
        SheHuan:@dang当年情 这个正能在做呢
      • 宇宙只有巴掌大:设置了setLoadEndView 后家在完成的view会一直显示在底部 不会消失是咋回事?
        宇宙只有巴掌大: @VipOthersh1.0.4上啦加载不回调
        宇宙只有巴掌大: @VipOthershe 谢谢,你真是太热心了
        SheHuan:@宇宙只有巴掌大 设置一个空的完成view,底部就不显示了
      • 宇宙只有巴掌大:大神 你这上拉加载结束 调用什么方法呢?
        SheHuan:@宇宙只有巴掌大 setLoadEndView(),设置结束对应的Footer View
      • 宇宙只有巴掌大:有没有下拉刷新方案?
        宇宙只有巴掌大: @VipOthershe 谢谢
        SheHuan: @宇宙只有巴掌大 liaohuqiu的还有Android自带的都是不错的选择
      • 间歇性丶神经病患者:改了第一次加载的,改为加载1条,他好像会一直自己去加载知道铺满屏幕
        SheHuan: @间歇性丶神经病患者 是这样设定的,明白了原理可自行修改哦
      • 捡淑:马克
      • Arthurcsh:值得拥有
      • Exception_Cui:写的很好
        SheHuan:@Exception_Cui 仍需努力 :sunglasses:

      本文标题:Android 实现自己的RecyclerView加载更多

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