4.Android RecyclerView完美支持下拉刷新和上拉加载(美团奔跑的小人)

请问:简书怎么可以把代码格式调整?我贴出来换格式了。你们直接去Github下载工程!

今天开始讲RecycleView的系列教程。分割线,分组,局部刷新,动态添加,缓存原理,抖音效果,瀑布流。嵌套,动画等等





框架重写需要考虑的问题

1.是否支持下拉

2.是否支持上拉

3.自定义Header

4.自定义Footer

5.自定义动画

原理介绍:

使用RecyclerView.OnScrollListener滚动的监听器来监听RecyclerView是否滑动到了底部,这样我们就可以执行添加底部动画的操作。

边界条件

根据原理我们能够知道,做一个下拉刷新的动画加载,我们需要做的有两个动作:

1.如何判断RecyclerView已经滑动到底部。

2.如何添加RecyclerView的底部动画。

重要的就是RecyclerView滚动监听

 RecyclerView.OnScrollListener 

mRecyclerView.canScrollVertically(1) 是否拉到底部

4.一些属性的介绍

setWaveHeight、setHeaderHeight、setBottomHeight、setOverScrollHeight

setWaveHeight 设置头部可拉伸的最大高度。

setHeaderHeight 头部固定高度(在此高度上显示刷新状态)

setBottomHeight 底部高度

setOverScrollHeight 设置最大的越界高度

setEnableRefresh、setEnableLoadmore

灵活的设置是否禁用上下拉。

setHeaderView(IHeaderView headerView)、setBottomView(IBottomView bottomView)

设置头部/底部个性化刷新效果,头部需要实现IHeaderView,底部需要实现IBottomView。

setEnableOverScroll

是否允许越界回弹。

总结步骤:

1.完成头部添加和脚部添加

2.下拉刷新实现

3.同理,完成上拉加载

下拉刷新具体实现:

需要用的接口:

1.自动下拉

2.可以停止下拉

3.可以动态设置下拉的ui

4.下拉完成回调

下拉核心东西

1.处理拦截事件:因为拖动和点击事件有冲突


/****

    * 条目的点击事件和滑动事件

    * @param ev

    * @return

    */

@Override

publicbooleandispatchTouchEvent(MotionEventev) {

switch(ev.getAction()) {

caseMotionEvent.ACTION_DOWN:

// 记录手指按下的位置 ,之所以写在dispatchTouchEvent那是因为如果我们处理了条目点击事件,

// 那么就不会进入onTouchEvent里面,所以只能在这里获取

mFingerDownY=(int)ev.getRawY();

break;

caseMotionEvent.ACTION_UP:

if(mCurrentDrag) {//拖动没有超过一定距离就要还原

restoreRefreshView();

                }

break;

        }

returnsuper.dispatchTouchEvent(ev);

    }

2.处理滑动事件:


@Override

publicbooleanonTouchEvent(MotionEvente) {

switch(e.getAction()) {

caseMotionEvent.ACTION_MOVE:

// 如果是在最顶部才处理,否则不需要处理

if(canScrollUp()||mCurrentRefreshStatus==REFRESH_STATUS_REFRESHING) {

// 如果没有到达最顶端,也就是说还可以向上滚动就什么都不处理

returnsuper.onTouchEvent(e);

                }

// 解决下拉刷新自动滚动问题

if(mCurrentDrag) {

scrollToPosition(0);

                }

// 获取手指触摸拖拽的距离

intdistanceY=(int) ((e.getRawY()-mFingerDownY)*mDragIndex);

// 如果是已经到达头部,并且不断的向下拉,那么不断的改变refreshView的marginTop的值

if(distanceY>0) {

intmarginTop=distanceY-mRefreshViewHeight;

setRefreshViewMarginTop(marginTop);

updateRefreshStatus(marginTop);

mCurrentDrag=true;

returnfalse;

                }

break;

        }

returnsuper.onTouchEvent(e);

    }

需要判断是否到了顶部


/**

    * @return Whether it is possible for the child view of this layout to

    * scroll up. Override this if the child view is a custom view.

    * 判断是不是滚动到了最顶部,这个是从SwipeRefreshLayout里面copy过来的源代码

    */

publicbooleancanScrollUp() {

if(android.os.Build.VERSION.SDK_INT<14) {

returnViewCompat.canScrollVertically(this,-1)||this.getScrollY()>0;

}else{

returnViewCompat.canScrollVertically(this,-1);

        }

    }

滑动过程中要不断修改recycleView的参数值


/**

    * 设置刷新View的marginTop

    */

publicvoidsetRefreshViewMarginTop(intmarginTop) {

MarginLayoutParamsparams=(MarginLayoutParams)mRefreshView.getLayoutParams();

if(marginTop<-mRefreshViewHeight+1) {

marginTop=-mRefreshViewHeight+1;

        }

params.topMargin=marginTop;

mRefreshView.setLayoutParams(params);

    }

重写位置方法


@Override

protectedvoidonLayout(booleanchanged,intl,intt,intr,intb) {

super.onLayout(changed,l,t,r,b);

if(changed) {

if(mRefreshView!=null&&mRefreshViewHeight<=0) {

// 获取头部刷新View的高度

mRefreshViewHeight=mRefreshView.getMeasuredHeight();

if(mRefreshViewHeight>0) {

// 隐藏头部刷新的View  marginTop  多留出1px防止无法判断是不是滚动到头部问题

setRefreshViewMarginTop(-mRefreshViewHeight+1);

                }

            }

        }

    }

1.定义接口:下拉,停止下拉,下拉回调,下拉的view

* Description: 下拉刷新的辅助类为了匹配所有效果

*/

public abstract class RefreshViewCreator {

/**

    * 获取下拉刷新的View

*

    * @param context 上下文

    * @param parent  RecyclerView

*/

    public abstract ViewgetRefreshView(Context context, ViewGroup parent);

    /**

    * 正在下拉

    * @param currentDragHeight  当前拖动的高度

    * @param refreshViewHeight  总的刷新高度

    * @param currentRefreshStatus 当前状态

    */

    public abstract void onPull(int currentDragHeight, int refreshViewHeight, int currentRefreshStatus);

    /**

    * 正在刷新中

    */

    public abstract void onRefreshing();

    /**

    * 停止刷新

    */

    public abstract void onStopRefresh();

}

2.核心东西。事件处理

/**

* Description: 下拉刷新的RecyclerView

*/

public class RefreshRecyclerViewextends WrapRecyclerView {

// 下拉刷新的辅助类

    private RefreshViewCreatormRefreshCreator;

    // 下拉刷新头部的高度

    private int mRefreshViewHeight =0;

    // 下拉刷新的头部View

    private ViewmRefreshView;

    // 手指按下的Y位置

    private int mFingerDownY;

    // 手指拖拽的阻力指数

    private float mDragIndex =0.35f;

    // 当前是否正在拖动

    private boolean mCurrentDrag =false;

    // 当前的状态

    private int mCurrentRefreshStatus;

    // 默认状态

    public int REFRESH_STATUS_NORMAL =0x0011;

    // 下拉刷新状态

    public int REFRESH_STATUS_PULL_DOWN_REFRESH =0x0022;

    // 松开刷新状态

    public int REFRESH_STATUS_LOOSEN_REFRESHING =0x0033;

    // 正在刷新状态

    public int REFRESH_STATUS_REFRESHING =0x0033;

    public RefreshRecyclerView(Context context) {

super(context);

    }

public RefreshRecyclerView(Context context, @Nullable AttributeSet attrs) {

super(context, attrs);

    }

public RefreshRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {

super(context, attrs, defStyle);

    }

// 先处理下拉刷新,同时考虑刷新列表的不同风格样式,确保这个项目还是下一个项目都能用

    // 所以我们不能直接添加View,需要利用辅助类

    public void addRefreshViewCreator(RefreshViewCreator refreshCreator) {

this.mRefreshCreator = refreshCreator;

        addRefreshView();

    }

@Override

    public void setAdapter(Adapter adapter) {

super.setAdapter(adapter);

        addRefreshView();

    }

/****

    * 条目的点击事件和滑动事件

    * @param ev

    * @return

    */

    @Override

    public boolean dispatchTouchEvent(MotionEvent ev) {

switch (ev.getAction()) {

case MotionEvent.ACTION_DOWN:

// 记录手指按下的位置 ,之所以写在dispatchTouchEvent那是因为如果我们处理了条目点击事件,

                // 那么就不会进入onTouchEvent里面,所以只能在这里获取

                mFingerDownY = (int) ev.getRawY();

break;

            case MotionEvent.ACTION_UP:

if (mCurrentDrag) {//拖动没有超过一定距离就要还原

                    restoreRefreshView();

                }

break;

        }

return super.dispatchTouchEvent(ev);

    }

/**

    * 重置当前刷新状态状态

    */

    private void restoreRefreshView() {

int currentTopMargin = ((MarginLayoutParams)mRefreshView.getLayoutParams()).topMargin;

        int finalTopMargin = -mRefreshViewHeight +1;

        if (mCurrentRefreshStatus ==REFRESH_STATUS_LOOSEN_REFRESHING) {

finalTopMargin =0;

            mCurrentRefreshStatus =REFRESH_STATUS_REFRESHING;

            if (mRefreshCreator !=null) {

mRefreshCreator.onRefreshing();

            }

if (mListener !=null) {

mListener.onRefresh();

            }

}

int distance = currentTopMargin - finalTopMargin;

        // 回弹到指定位置

        ValueAnimator animator = ObjectAnimator.ofFloat(currentTopMargin, finalTopMargin).setDuration(distance);

        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

@Override

            public void onAnimationUpdate(ValueAnimator animation) {

float currentTopMargin = (float) animation.getAnimatedValue();

                setRefreshViewMarginTop((int) currentTopMargin);

            }

});

        animator.start();

        mCurrentDrag =false;

    }

@Override

    public boolean onTouchEvent(MotionEvent e) {

switch (e.getAction()) {

case MotionEvent.ACTION_MOVE:

// 如果是在最顶部才处理,否则不需要处理

                if (canScrollUp() ||mCurrentRefreshStatus ==REFRESH_STATUS_REFRESHING) {

// 如果没有到达最顶端,也就是说还可以向上滚动就什么都不处理

                    return super.onTouchEvent(e);

                }

// 解决下拉刷新自动滚动问题

                if (mCurrentDrag) {

scrollToPosition(0);

                }

// 获取手指触摸拖拽的距离

                int distanceY = (int) ((e.getRawY() -mFingerDownY) *mDragIndex);

                // 如果是已经到达头部,并且不断的向下拉,那么不断的改变refreshView的marginTop的值

                if (distanceY >0) {

int marginTop = distanceY -mRefreshViewHeight;

                    setRefreshViewMarginTop(marginTop);

                    updateRefreshStatus(marginTop);

                    mCurrentDrag =true;

return false;

                }

break;

        }

return super.onTouchEvent(e);

    }

/**

    * 更新刷新的状态

    */

    private void updateRefreshStatus(int marginTop) {

if (marginTop <= -mRefreshViewHeight) {

mCurrentRefreshStatus =REFRESH_STATUS_NORMAL;

        }else if (marginTop <0) {

mCurrentRefreshStatus =REFRESH_STATUS_PULL_DOWN_REFRESH;

        }else {

mCurrentRefreshStatus =REFRESH_STATUS_LOOSEN_REFRESHING;

        }

if (mRefreshCreator !=null) {

mRefreshCreator.onPull(marginTop, mRefreshViewHeight, mCurrentRefreshStatus);

        }

}

/**

    * 添加头部的刷新View

*/

    private void addRefreshView() {

RecyclerView.Adapter adapter = getAdapter();

        if (adapter !=null &&mRefreshCreator !=null) {

// 添加头部的刷新View

            View refreshView =mRefreshCreator.getRefreshView(getContext(), this);

            if (refreshView !=null) {

addHeaderView(refreshView);

                this.mRefreshView = refreshView;

            }

}

}

@Override

    protected void onLayout(boolean changed, int l, int t, int r, int b) {

super.onLayout(changed, l, t, r, b);

        if (changed) {

if (mRefreshView !=null &&mRefreshViewHeight <=0) {

// 获取头部刷新View的高度

                mRefreshViewHeight =mRefreshView.getMeasuredHeight();

                if (mRefreshViewHeight >0) {

// 隐藏头部刷新的View  marginTop  多留出1px防止无法判断是不是滚动到头部问题

                    setRefreshViewMarginTop(-mRefreshViewHeight +1);

                }

}

}

}

/**

    * 设置刷新View的marginTop

*/

    public void setRefreshViewMarginTop(int marginTop) {

MarginLayoutParams params = (MarginLayoutParams)mRefreshView.getLayoutParams();

        if (marginTop < -mRefreshViewHeight +1) {

marginTop = -mRefreshViewHeight +1;

        }

params.topMargin = marginTop;

        mRefreshView.setLayoutParams(params);

    }

/**

    * @return Whether it is possible for the child view of this layout to

* scroll up. Override this if the child view is a custom view.

    * 判断是不是滚动到了最顶部,这个是从SwipeRefreshLayout里面copy过来的源代码

    */

    public boolean canScrollUp() {

if (android.os.Build.VERSION.SDK_INT <14) {

return ViewCompat.canScrollVertically(this, -1) ||this.getScrollY() >0;

        }else {

return ViewCompat.canScrollVertically(this, -1);

        }

}

/**

    * 停止刷新

    */

    public void onStopRefresh() {

mCurrentRefreshStatus =REFRESH_STATUS_NORMAL;

        restoreRefreshView();

        if (mRefreshCreator !=null) {

mRefreshCreator.onStopRefresh();

        }

}

// 处理刷新回调监听

    private OnRefreshListenermListener;

    public void setOnRefreshListener(OnRefreshListener listener) {

this.mListener = listener;

    }

public interface OnRefreshListener {

void onRefresh();

    }

}

3.调用


publicclassPullActivityextendsAppCompatActivity{

privateRefreshRecyclerViewmRecycleView;

privateLinearLayoutManagermLinearLayoutManager;//布局管理器

privateListmList;

@Override

protectedvoidonCreate(BundlesavedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_pull);

mList=newArrayList();

mRecycleView=findViewById(R.id.rv_list);

//初始化数据

initData(mList);

//创建布局管理器,垂直设置LinearLayoutManager.VERTICAL,水平设置LinearLayoutManager.HORIZONTAL

mLinearLayoutManager=newLinearLayoutManager(this,LinearLayoutManager.VERTICAL,false);

//创建适配器,将数据传递给适配器

//设置布局管理器

mRecycleView.setLayoutManager(mLinearLayoutManager);

MyRecycleViewAdapteradapter=newMyRecycleViewAdapter(mList);

mRecycleView.setAdapter(newWrapRecyclerAdapter(adapter));

defaultRefreshCreator=newDefaultRefreshCreator();

mRecycleView.addRefreshViewCreator(defaultRefreshCreator);

mRecycleView.setOnRefreshListener(newRefreshRecyclerView.OnRefreshListener() {

@Override

publicvoidonRefresh() {

Log.d("PullActivity","onRefresh");

            }

        });

handler.sendEmptyMessageDelayed(0,5000);

    }

Handlerhandler=newHandler() {

@Override

publicvoidhandleMessage(@NonNullMessagemsg) {

super.handleMessage(msg);

mRecycleView.onStopRefresh();//停止刷新

        }

    };

DefaultRefreshCreatordefaultRefreshCreator;

publicvoidinitData(Listlist) {

for(inti=1;i<=80;i++) {

list.add("第"+i+"条数据");

        }

    }

}

问题:

给你一个普通的页面,你自己弄一个下拉刷新?怎么处理?

RecycleView是不是要搭配adapter一起?

不需要,recycleView和第三方的adataper是分开的,2个独立的控件

不能滑动都问题

支持下拉刷新和上拉加载的RefreshLayout,自带越界回弹效果,支持RecyclerView,AbsListView,ScrollView,WebView

Google官方推出了SwipeRefreshLayout和RecyclerView的共同使用,,为我们提供了更加便捷的列表下拉刷新功能,但是,并没有给我们提供上拉加载功能

基于第三方:    SmartRefreshLayout

基于第三方控件TwinklingRefreshLayout

第一种方式:嵌套

使用finishRefreshing()方法结束刷新,finishLoadmore()方法结束加载更多。此处OnRefreshListener还有其它方法,可以选择需要的来重写。

如果你想进入到界面的时候主动调用下刷新,可以调用startRefresh()/startLoadmore()方法。(会自动执行RefreshListenerAdapter监听事件中的onRefresh()/onLoadMore())

第二种方式:

方便实现个性化的 Header 和 Footer


demo地址:https://github.com/pengcaihua123456/shennandadao

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

推荐阅读更多精彩内容