美文网首页Android UIAndroid
自定义 RecyclerView 实现刷新与加载更多

自定义 RecyclerView 实现刷新与加载更多

作者: 程序杂念 | 来源:发表于2018-01-07 15:27 被阅读2383次

    “哎呀,最近新接手一个项目里要用 RecyclerView 的地方好多,而且基本上都需要上拉刷新和下拉加载,我才写了两个页面,手都快断了,有没有什么比较好的方法?”

    “什么!你还在自己一点点地写?那你得写到啥时候去?”

    “那怎么办?”

    “你是不是傻,Github 上那么多优秀的开源库,用一个啊。或者是自己写一个简单的,别写那么复杂的,能满足自己需求的就行。”

    “有道理,我去看看”

    ......

    “Github 上的优秀的好多啊,我都看花了眼,不知道该选哪一个?”

    “实在不行,你就自己写一个呗,如果只是实现你的需求应该不难,来说说看,你的需求是什么?”

    “也不是很复杂,就简单的一个,能上拉加载更多,下拉刷新的就行,简单不?”

    “你出去别说你是程序员,会被怼死的……”

    “咋了,这不行吗?”

    “不是不行,你这需求提了跟没提一个样,有没有点实质性的东西?”

    “嗯……,那我重说

    1. 在布局文件中只使用一个控件,不做任何的嵌套;
    2. 在 RecyclerView 没有数据的时候,页面上显示提示信息,‘暂无数据’ 就行;
    3. 支持下拉刷新,刷新显示的 Header 就用官方的那个小圆圈就行了;
    4. 支持上拉加载,如果有下一页数据, Footer 显示一个 Loading 配上‘加载中……’,如果没有数据,就显示‘你已经扯到底了’。

    嗯,就这么多。”

    “还行,也不算难,有什么想法吗?”

    “暂时没了,后面想到再加吧。”

    “…………”

    “那行,那我们先来看看具体怎么实现吧。”

    “1. 同一个控件内既能刷新又要可以上拉加载,官方的控件应该是没有的,那就自定义一个吧,自定义一个 ViweGroup ,然后把 SwipeRefreshLayout 和 RecyclerView 包裹起来,然后再给 RecyclerView 添加一个滑动监听事件;

    1. 在没有数据的情况下显示提示信息,这个可以放一个 TextView 或者是 ImageView ,然后在获取数据之后,判断是否为空,如果数据不为空就显示 RecyclerView,如果数据为空,就显示 TextView;
    2. 需要动态的修改 Footer 显示的内容,那就在 RecyclerView 的 Adapter 中动态更改呗。

    “等等,那个 Footer 的动态更改能不能不让我做啊,每次都要写动态改变代码,实在不想写。”

    “没让你写啊,我是说放在我们内部的 Adapter 里面。是这样的,我打算自己实现一个 Adapter ,在这个 Adapter 里面去动态的更新 Footer。”

    “你实现了 Adapter,那我外面还能用吗?

    “可以啊,咋不行,我自定义的 Adapter 把你外层需要用到的 Adapter 包裹起来,你还是你,不过,你外面还有一层。玩过俄罗斯套娃吗?”

    “没有……”

    “想想手机和手机壳的关系,这个跟那个类似。”

    “好像懂了……”

    “那我们来实现吧。”

    “从哪下手啊?我怎么感觉一脸懵逼。”

    “先从布局文件开始,先来写显示出来的列表布局,在里面写上你要的那几个控件。然后再给 Footer 来一个布局。”

    me_recyclerview.xml

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <android.support.v7.widget.AppCompatTextView
            android:id="@+id/list_tip_message"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="没有数据"
            android:textSize="16sp" />
    
        <android.support.v4.widget.SwipeRefreshLayout
            android:id="@+id/list_swipe_refresh"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <android.support.v7.widget.RecyclerView
                android:id="@+id/list_list"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:visibility="gone" />
    
        </android.support.v4.widget.SwipeRefreshLayout>
    
    </FrameLayout>
    

    “嗯,这就是列表布局了,内容很简单就是你现在写的样子……,好了,再来写个 Footer 布局。”

    “等等,这个简单,我来。”

    “行行行,你来。”

    me_foot.xml

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="48dp">
    
        <ProgressBar
            android:id="@+id/item_footer_progress"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.4"
            app:layout_constraintStart_toStartOf="parent" />
    
        <TextView
            android:id="@+id/item_footer_message"
            android:layout_width="wrap_content"
            android:layout_height="48dp"
            android:layout_marginStart="8dp"
            android:gravity="center"
            android:text="加载中"
            app:layout_constraintLeft_toRightOf="@+id/item_footer_progress" />
    
    </android.support.constraint.ConstraintLayout>
    

    “还行,那我们继续?”

    “好嘞。”

    “刚才说了,需要使用一个 Adapter 来包裹外层调用方的 Adapter ,那么还需要自定义一个 Adapter ,就叫 MeRefreshListAdapter 吧。来继续”

    MeRefreshListAdapter.java

        private static final int TYPE_FOOTER = -1;
        private RecyclerView.Adapter adapter;
        private LayoutInflater inflater;
        private boolean isShowFooter;
        private boolean isNoData;
    
        public MeRefreshListAdapter(RecyclerView.Adapter adapter, Context context, boolean isShowFooter, boolean isNoData) {
            this.adapter = adapter;
            this.isShowFooter = isShowFooter;
            this.isNoData = isNoData;
            inflater = LayoutInflater.from(context);
        }
    

    “你这里那个 RecyclerView.Adapter 干啥使得?”

    “那个就是外面你传进来的 Adapter 啊,你外面的 Adapter 肯定得继承自 RecyclerView.Adapter 吧,那我得保证,你加的什么泛型我这边都能用啊。比如你有一个是继承自 RecyclerView.Adapter<Person.ViewHolder>,还有一个是继承自 RecyclerView.Adapter<City.ViewHolder>”。

    “哦~”

    “还有,这个 MeRefreshListAdapter 需要控制 Footer 和外层 Adapter 的 Item 的创建与绑定,总的来说,他们是两类,我不管你外层的 Item 的类型有几个,我都要加上 1 ,这个 1 就是 Footer。并且,我内部只对 Footer 这一个类型的 Item 进行控制,其余的还是交给你外层去做。不过这时候就需要进行判断了,到底哪一个是 Footer 类型的,哪一个是普通类型(外层的 Item 类型)。”

    “这个我知道,简单,Footer 肯定是在最后一个的,那就直接把最后一个归为 Footer 就行了,不对,当不显示 Footer 的时候,最后一个 Item 也是普通类型,那就是在显示 Footer 的情况下的最后一个 Item 是 Footer。”

    “嗯,是的,这样一来,我们自定义的 Adapter 也就出来了。”

    MeRefreshListAdapter.java

        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            RecyclerView.ViewHolder viewHolder;
            switch (viewType) {
                case TYPE_FOOTER:
                    View footer = inflater.inflate(R.layout.me_list_footer, parent, false);
                    viewHolder = new FooterHolder(footer);
                    break;
                default:
                    viewHolder = adapter.onCreateViewHolder(parent, viewType);
                    break;
            }
            return viewHolder;
        }
    
        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            if (holder instanceof FooterHolder) {
                FooterHolder footerHolder = (FooterHolder) holder;
                footerHolder.progressBar.setVisibility(isNoData ? View.GONE : View.VISIBLE);
                footerHolder.message.setText(isNoData ? "--- 扯到底了 ---" : "加载中……");
            }else{
                adapter.onBindViewHolder(holder,position);
            }
        }
    
        @Override
        public int getItemCount() {
            return isShowFooter ? adapter.getItemCount() + 1 : adapter.getItemCount();
        }
    
        @Override
        public int getItemViewType(int position) {
            if (isShowFooter && position + 1 == getItemCount()) {
                return TYPE_FOOTER;
            } else {
                return adapter.getItemViewType(position);
            }
        }
        
        static class FooterHolder extends RecyclerView.ViewHolder {
            ProgressBar progressBar;
            TextView message;
    
            FooterHolder(View itemView) {
                super(itemView);
                progressBar = itemView.findViewById(R.id.item_footer_progress);
                message = itemView.findViewById(R.id.item_footer_message);
            }
        }
    
        public void setShowFooter(boolean flag) {
            this.isShowFooter = flag;
            this.notifyDataSetChanged();
        }
    
        public void setNoData(boolean flag) {
            this.isNoData = flag;
            this.notifyDataSetChanged();
        }
    

    “当然了,还需要对外开放更新 Adapter 的方法,这样才能实时控制嘛。”

    “我们接下来干啥?”

    “写接口,我们需要自定义两个接口方法,用来在外层调用。很简单,就下面这样的。”

    RefreshLoadListener.java

    public interface RefreshLoadListener {
        /**
         * 上拉加载更多
         */
        void upLoad();
    
        /**
         * 下拉刷新
         */
        void downRefresh();
    }
    

    “接口也写完了,现在开始进入正题了。自定义 ViewGroup 继承自 LinearLayout,然后重写其中的构造方法。四个构造方法都要重写啊,不写的话可能会运行报错。”

    MeRecyclerView.java

    public class MeRecyclerView extends LinearLayout implements SwipeRefreshLayout.OnRefreshListener {
    
        private SwipeRefreshLayout refreshLayout;
        private RecyclerView refreshList;
        private AppCompatTextView listTip;
        private RefreshLoadListener loadListener;
        private MeRefreshListAdapter meRefreshListAdapter;
        private Context mContext;
        private RecyclerView.LayoutManager layoutManager;
        private boolean isShowFooter;
        private boolean isNoData;
        private int lastVisibleItem;
    
        public MeRecyclerView(Context context) {
            super(context);
            init(context);
        }
    
        public MeRecyclerView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            init(context);
        }
        public MeRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init(context);
        }
    
        public MeRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
            init(context);
        }
        
        private void init(Context context) {
            this.mContext = context;
            LayoutInflater.from(context).inflate(R.layout.me_recyclerview, this, true);
            refreshLayout = findViewById(R.id.list_swipe_refresh);
            listTip = findViewById(R.id.list_tip_message);
            refreshList = findViewById(R.id.list_list);
    
            refreshLayout.setOnRefreshListener(this);
        }
    }
    

    “你看,刚才在初始化操作的时候,我们就设置了 SwipeRefreshLayout 的刷新接口,现在就到了我们需要实现他的时候了,可能跟你平时写的不太一样,因为虽然我们实现了这个接口,但我们还是得把这个具体实现的机会交给外层,让外层进行实际的数据获取。当然了,我们如果当前页面处于刷新状态,那 Footer 肯定是不能显示出来的,这个时候就需要操作一下刚才我们写的 MeRefreshListAdapter ”

    MeRecyclerView.java

        @Override
        public void onRefresh() {
            if (null != loadListener) {
                isNoData = false;
                isShowFooter = false;
                meRefreshListAdapter.setNoData(isNoData);
                meRefreshListAdapter.setShowFooter(isShowFooter);
                loadListener.downRefresh();
            }
        }
    

    “好了,下拉刷新结束了,是不是很简单?”

    “这就结束了,这也太快了。”

    “下面来看上拉加载更多。”

    “刷新的容易,加载更多的,要控制下面 Footer 显示还要改变显示的文字,想想头就大。”

    “你想啥呢,那些都做完了啊,刚在在 MeRefreshListAdapter 不是就做过了……”

    “完全没意识到……”

    “上拉加载其实跟刷新一样的,都挺好实现的,无非就是给 RecyclerView 添加一个滑动监听,然后再根据当前位置去判断是否加载新数据。你看”

    MeRecyclerView.java

            refreshList.addOnScrollListener(new RecyclerView.OnScrollListener() {
                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                    int totalCount = layoutManager.getItemCount() - 1;
                    if (totalCount > 18 && newState == RecyclerView.SCROLL_STATE_IDLE && lastVisibleItem == totalCount && !isNoData && !isShowFooter) {
                        if (null != loadListener) {
                            setFooter();
                            loadListener.upLoad();
                        }
                    }
                }
    
                @Override
                public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                    if (layoutManager instanceof LinearLayoutManager) {
                        lastVisibleItem = ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition();
                    }
                }
            });
            
        private void setFooter() {
            isShowFooter = true;
            meRefreshListAdapter.setShowFooter(true);
        }
    

    "好了,上拉加载更多也解决了,不过这里我偷了个懒。在获取当前列表的最后一项的 position 时,我判断了 LinearLayoutManager ,其他的没进行判断,不过也简单,GridLayoutManager 是继承自 LinearLayoutManager ,而 StaggeredGridLayoutManager 获取最后一个 item 的 position 返回的是一个数组,取出其中最大的即可。"

    “好吧,那这样是不是就算完成了啊?”

    “想多了,看起来,我们是做完这么多的,但是,并没有做完,比如,我们还需要进行停止刷新,要不然那个无论是上拉加载还是刷新都会有那个 loading 在那不停地动。”

    MeRecyclerView.java

        public void stopRefresh(int pageCount, boolean isNoData) {
            this.isNoData = isNoData;
            meRefreshListAdapter.setNoData(isNoData);
            showData(meRefreshListAdapter.getItemCount() > 0);
            if (pageCount == 1) {
                refreshLayout.setRefreshing(false);
            } else {
                if (!isNoData) {
                    isShowFooter = false;
                    meRefreshListAdapter.setShowFooter(isShowFooter);
                }
            }
        }
        
        private void showData(boolean b) {
            refreshList.setVisibility(b ? VISIBLE : VISIBLE);
            listTip.setVisibility(b ? GONE : VISIBLE);
        }
    

    "再比如,我们还需要与外部的 LayoutManager 以及 Adapter 建立联系以及在外部数据变动的时候,通知我们进行刷新"

    MeRecyclerView.java

        public void setLayoutManager(RecyclerView.LayoutManager layoutManager) {
            this.layoutManager = layoutManager;
            refreshList.setLayoutManager(layoutManager);
        }
    
        public void setAdapter(RecyclerView.Adapter adapter) {
            meRefreshListAdapter = new MeRefreshListAdapter(adapter, mContext, isShowFooter, isNoData);
            refreshList.setAdapter(meRefreshListAdapter);
        }
        
        public void notifyDataSetChanged() {
            meRefreshListAdapter.notifyDataSetChanged();
        }
    

    "这还不算完……"

    “还没完?”

    “肯定的啊,你想啊,页面刚打开的时候,你是不是得立即去刷新一下,做事总得主动点嘛,所以我们还需要一个可以开始刷新的。”

    “我知道,是让页面进来的时候去刷新一下,开始获取数据对吧,我们可以直接调用下拉刷新的方法啊,那样最省事。就像这样。”

        public void startRefresh() {
            refreshLayout.setRefreshing(true);
            onRefresh();
        }
    

    “是的,这么样就行了。不过还有一个最最最重要的就是得让外部把接口实现了,所以还要这么一个方法。”

        public void setLoadListener(RefreshLoadListener loadListener) {
            this.loadListener = loadListener;
        }
    

    "这个,我肯定不会晚啊,不过如果忘记了,肯定都不能用啊。我先拿去试试看好不好用啊。"

    “……”

    “真爽,用起来特简单。在布局文件里面只用一个控件,就是刚才我们自定义的那个,然后,像往常一样设置 LayoutManager 和 Adapter 不过需要在服务器返回数据之后自己手动停止刷新。还是挺简单的。哦,对,还有一个要自己启动刷新。”

    RefreshActivity.java

        @BindView(R.id.refresh_demo_list)
        MeRecyclerView refreshDemoList;
        int page = 1;
        int size = 20;
        List<Map<String, Object>> dataList;
        ListAdapter listAdapter;
        LinearLayoutManager layoutManager;
        
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_refresh);
            ButterKnife.bind(this);
    
            dataList = new ArrayList<>();
            listAdapter = new ListAdapter(this, R.layout.item_simgle_check, dataList);
            layoutManager = new LinearLayoutManager(this);
            
            refreshDemoList.setLoadListener(this);
            refreshDemoList.setLayoutManager(layoutManager);
            refreshDemoList.setAdapter(listAdapter);
        }
        
        @Override
        protected void onResume() {
            super.onResume();
            refreshDemoList.startRefresh();
        }
        
        @Override
        public void downRefresh() {
            page = 1;
            getData();
        }
    
        @Override
        public void upLoad() {
            page += 1;
            getData();
        }
        
        private void update(List<Map<String, Object>> maps) {
            if (page > 5) {
                maps.clear();
            }
            if (maps.size() > 0) {
                if (page == 1) {
                    dataList.clear();
                    dataList.addAll(maps);
                } else {
                    dataList.addAll(maps);
                }
                refreshDemoList.notifyDataSetChanged();
                refreshDemoList.stopRefresh(page, false);
            } else {
                refreshDemoList.stopRefresh(page, true);
            }
        }
    
    
    没有数据 加载更多 没有更多

    "好了,跟你学完了,我还去继续写代码了,还有什么问题,下次再来问你。希望有一天,我能不问你也能把问题解决了。👍"

    相关文章

      网友评论

      • 比克大魔王_:一篇博文也可以这么多戏分
      • 德鲁大叔凯里欧文:可以可以。简单实用。
      • yaoTongxue:看到最后没有地址。。。。散了
        yaoTongxue:@Mr_Monster 哈哈,我自己整出来了,就是看看别人的效果是怎么样的:smile:
        程序杂念:我以为,看完一篇文章,找个时间对照着来一遍,会加深对其的理解能力。
      • 帅大叔的简书:大佬顺便加上github Demo地址啊:smiley:
        嗨匠:@Mr_Monster 哈哈,这解释我也是服气的。冲着这精分对话风格的文章,关注了,希望多写点哈
        程序杂念:不好意思,不提供 demo 地址。我以为,看完一篇文章,找个时间对照着来一遍,会加深对其的理解能力。

      本文标题:自定义 RecyclerView 实现刷新与加载更多

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