Android 下拉刷新上拉加载(解决item未满一页时的显示问题、添加网络错误等提示)

最近项目中用到下拉刷新和上拉加载,于是在网上找了许多轮子,发现很多都有瑕疵,大多都没有解决当前item不满一页时,上拉加载的View一直显示的问题。之后自己尝试解决该问题,并完善了其他功能,就写了如下demo,看下面:

代码地址:github

一、下拉刷新:

使用的是SwipeRefrshLayout,在support-v4兼容包下,作为官方的下拉刷新控件,其功能还很强大的,也不详细介绍了,网上的使用详解一大堆,这里讲一下基本使用方法:

  • setOnRefreshListener(OnRefreshListener):添加下拉刷新监听器
  • setRefreshing(boolean):显示或者隐藏刷新进度条
  • isRefreshing():检查是否处于刷新状态
  • setColorSchemeResources():设置进度条的颜色主题,最多设置四种。

然后在xml中:

<android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/sr_load_more"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <android.support.v7.widget.RecyclerView
            android:id="@+id/rv_load_more"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </android.support.v4.widget.SwipeRefreshLayout>

Java代码中添加刷新事件:

 mSrl=(SwipeRefreshLayout)findViewById(R.id.sr_load_more);
mSrl.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                // 调用刷新方法
            }
        });

效果:

image

是不是很简单呢?具体使用可以去看下官方文档,下面我们来说上拉加载。

二、上拉加载:

首先,确定需求:

  1. 当RecyclerView加载时,只显示RecyclerView的item;
  2. 用户上拉,拉到最后一项item时,出现下拉刷新的footerView,并进行数据加载;
  3. 数据加载完成后,footerView消失,并将数据添加进RecyclerView;
  4. 用户再次下拉,重复上面的步骤。

这里有几点需要注意:

  • RecyclerView初次加载时,当item不满一页时,footerView应隐藏;
  • 数据加载失败(网络错误等)时,footView应提示,并取消上拉加载事件,添加点击重新加载事件;
  • 服务器数据全部加载完成后,footView也应提示,并取消上拉加载事件。

代码实现:

首先,我们定义RecyclerView的item和footerView:

item的xml布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="48dp">
    <TextView
        android:id="@+id/tv_item_test"
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:text="测试数据1"
        android:paddingLeft="16dp"
        android:gravity="center_vertical|left"/>
</LinearLayout>
footerView的xml布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/load_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingBottom="12dip"
    android:paddingTop="12dip"
    android:orientation="vertical">
    <!--加载时显示的布局-->
    <LinearLayout
        android:id="@+id/ll_footer_loading"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:gravity="center"
        android:orientation="horizontal">
        <ProgressBar
            style="?android:attr/progressBarStyleSmall"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <TextView
            android:id="@+id/more_data_msg"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="16dp"
            android:text="正在加载..." />
    </LinearLayout>
    <!--错误时显示的布局-->
    <LinearLayout
        android:id="@+id/ll_footer_error"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:gravity="center"
        android:orientation="horizontal"
        android:clickable="true">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="网络错误,点击重新加载" />
    </LinearLayout>
    <!--全部加载完毕显示的布局-->
    <LinearLayout
        android:id="@+id/ll_footer_all_loaded"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:gravity="center"
        android:orientation="horizontal">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="已经全部加载完啦!" />
    </LinearLayout>

</LinearLayout>

嗯,看起来比较复杂,其实就是三个状态的布局,我们可以在代码中控制其显示、隐藏。
然后,我们来写RecyclerView的适配器。

首先,我们知道RecyclerView适配器中,有这个方法:

public int getItemViewType(int position) {
            return 0;
        }

利用这个方法,我们可以判断item是普通item还是底部的FooterView,我们定义两个类型:

/**
     * 普通项和底部
     */
    private final int TYPE_ITEM=1;
    private final int TYPE_FOOTER=2;

然后,重写getItemViewType方法和getItemCount方法:

@Override
    public int getItemViewType(int position) {
        if(position+1==getItemCount()){ // 到了底部
            return TYPE_FOOTER;
        }else {
            return TYPE_ITEM;
        }
    }
    
@Override
    public int getItemCount() {
        return mData.size()!=0?mData.size()+1:0;
    }

然后,创建我们的ViewHolder,并且重写onCreateViewHolder方法,在这里,我们根据itemType,来决定要加载的布局:

private View mFootView;
class MyViewHolder extends RecyclerView.ViewHolder{

        public TextView tvTest;

        public LinearLayout llLoading;  // 正在加载
        public LinearLayout llLoadError; // 错误
        public LinearLayout llLoadedAll; // 全部加载完

        public MyViewHolder(View itemView) {
            super(itemView);
            // 判断是否是底部加载的view
            if(mFootView==null || itemView!=mFootView){
                tvTest=(TextView)itemView.findViewById(R.id.tv_item_test);
            }
            if(mFootView!=null && mFootView==itemView){
                llLoading=(LinearLayout)itemView.findViewById(R.id.ll_footer_loading);
                llLoadError=(LinearLayout)itemView.findViewById(R.id.ll_footer_error);
                llLoadedAll=(LinearLayout)itemView.findViewById(R.id.ll_footer_all_loaded);
            }

        }
    }
    
    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if(viewType==TYPE_ITEM){
            // 普通item
            View view= LayoutInflater.from(mContext).inflate(R.layout.item_rv_test,parent,false);
            return new MyViewHolder(view);
        } else{
            // 底部
            mFootView=LayoutInflater.from(mContext).inflate(R.layout.item_first_footer,parent,false);
            return new MyViewHolder(mFootView);
        }
    }

接下来是最重要的onBindViewHolder方法,在这里,我们首先根据itemType,判断ViewHolder数据,然后,判断footerView的状态:

/**
     * 底部上拉加载的view有四种状态:
     * 初始时:隐藏(避免recyclerView的item不满一页时,上拉加载的view一直显示)
     * 第一次滑动后:显示(显示信息为正在加载)
     * 网络错误时:显示(显示信息为网络错误,点击重新加载)
     * 全部都已加载完时:显示(显示信息为已全部加载完毕)
     */
    public static final int STATE_FIRST=1;
    public static final int STATE_SHOW=2;
    public static final int STATE_ERROR_NET=3;
    public static final int STATE_ALL_LOADED=4;

    /**
     * 当前加载状态
     */
    private int mLoadState=STATE_FIRST;
    
    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        if(getItemViewType(position)==TYPE_ITEM){  // 普通项
            holder.tvTest.setText(mData.get(position));
        }else {      // 底部
            switch (mLoadState){  // 判断底部上拉加载的view的状态
                case STATE_FIRST:  // 初始状态 view为隐藏
                    mFootView.setVisibility(View.GONE);
                    holder.llLoading.setVisibility(View.VISIBLE);
                    holder.llLoadError.setVisibility(View.GONE);
                    holder.llLoadedAll.setVisibility(View.GONE);
                    break;
                case STATE_SHOW:
                    mFootView.setVisibility(View.VISIBLE);
                    holder.llLoading.setVisibility(View.VISIBLE);
                    holder.llLoadError.setVisibility(View.GONE);
                    holder.llLoadedAll.setVisibility(View.GONE);
                    break;
                case STATE_ERROR_NET:
                    mFootView.setVisibility(View.VISIBLE);
                    holder.llLoading.setVisibility(View.GONE);
                    holder.llLoadError.setVisibility(View.VISIBLE);
                    holder.llLoadedAll.setVisibility(View.GONE);
                    // 重新加载
                    if(mOnListener!=null){
                        holder.llLoadError.setOnClickListener(new View.OnClickListener() {
                            @Override
                            public void onClick(View v) {
                                mOnListener.onReLoad();
                            }
                        });
                    }
                    break;
                case STATE_ALL_LOADED:
                    mFootView.setVisibility(View.VISIBLE);
                    holder.llLoading.setVisibility(View.GONE);
                    holder.llLoadError.setVisibility(View.GONE);
                    holder.llLoadedAll.setVisibility(View.VISIBLE);
                    break;
            }
        }
    }
    
    private OnListener mOnListener;
    
    // item点击事件和底部重新加载点击事件
    public interface OnListener{
        void onItemClick();
        void onReLoad();
    }

恩,主要代码就是这样,然后我们就可以在Activity中设置RecyclerView的点击事件、RecyclerView的滑动事件:

// recyclerView的item点击事件,和网络错误时重新加载的点击事件
        mAdapter.setListener(new RvLoadMoreAdapter.OnListener() {
            @Override
            public void onItemClick() { // item点击事件

            }

            @Override
            public void onReLoad() {  // 重新加载
                
            }
        });

        // 监听RecyclerView的滑动事件,判断是否是手指向上滑动,避免item不满一页时,下拉刷新触发上拉加载事件。
        
        private boolean isUp=false;
        mRv.setOnTouchListener(new View.OnTouchListener() {
            float oldy;
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()){
                    case MotionEvent.ACTION_DOWN:
                        oldy=event.getY();
                        break;
                    case MotionEvent.ACTION_UP:
                        if(oldy-event.getY()>ViewConfiguration.get(mContext).getScaledTouchSlop()){
                            isUp=true;
                        } else {
                            isUp=false;
                        }
                }
                return false; // 不拦截事件
            }
        });


        mRv.addOnScrollListener(new RecyclerView.OnScrollListener() {
            private int lastVisibleItemPosition;
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);

                //正在滚动
                if(isUp && !mAdapter.ismIsLoading() && newState==RecyclerView.SCROLL_STATE_IDLE  &&
                        lastVisibleItemPosition+1==mAdapter.getItemCount()){  // 判断条件:手指上滑、当前不是正在加载的状态、停止滑动、末尾的item为最后一项
                    // 初始状态时,footer为隐藏,这里将其设为可见
                    mAdapter.showFooter();
                    if(mAdapter.getLoadState()==RvLoadMoreAdapter.STATE_FIRST){ //初始状态
                        mAdapter.setmLoadState(false,false,null);
                    }
                    // 只有显示状态时,才能响应滑动事件(不包括错误状态和加载完成状态)
                    if(mAdapter.getLoadState()==RvLoadMoreAdapter.STATE_SHOW){ // 显示状态
                        mAdapter.setmIsLoading(true);
                        // 这里写加载事件
                    }

                }

            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                lastVisibleItemPosition=mLayoutManager.findLastVisibleItemPosition();
            }
        });

基本逻辑就是这样了,具体代码可以看demo,演示如下:

image

代码地址:github

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

推荐阅读更多精彩内容