您的位置:首页 > 移动开发 > Android开发

RecyclerView下拉刷新和加载更多

2017-06-01 09:37 295 查看
之前一直写的是ListVIew下拉刷新,但是好多朋友都说要RecycleView的下拉刷新和滑动加载,其实,这个原理都是差不多。抽空,我就写了下RecycleView的下拉刷新和滑动加载更多。因此,这才写到博客里,记录一下。在大家阅读这篇博客前,大家需要了解的知识1.Scroller。实现弹性滑动的类,这个是经常用到的,不懂的请自觉先学习Scroller的知识。2.事件分发机制。事件是以ACTION_DOWN开始到ACTION_UP货ACTION_CANCEL结束的一个序列,期间事件分发,能够通过onInterceptTouchEvent方法和dispatchTouchEvent进行事件的阻止和消费。3.RecyclerView的基本使用。比如如何添加一个Decoration.4.onSizeChange的触发时机。onSizeChange()在View的layout中触发,它执行在所有控件的onMeasure()之后,因此可以直接获取到控件的测量长和宽。整体的思路:采用的是LinearLayout+RecyclerView的组合,在LinearLayout中添加HeaderView和FooterView。当RecyclerView滑动到了最顶部,则可以触发下拉事件;当RecyclerView滑动到了底部,则可以触发滑动加载更多的事件。然后在通过事件分发,进行滑动事件的处理。先看一下效果:下面是自定义View的代码,后面会逐一分析代码块:[java] viewplain copypackage com.mjc.recyclerviewdemo.refresh;import android.content.Context;import android.graphics.Color;import android.support.v7.widget.LinearLayoutManager;import android.support.v7.widget.RecyclerView;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.view.ViewConfiguration;import android.widget.LinearLayout;import android.widget.Scroller;import com.mjc.recyclerviewdemo.CustomItemDecoration;/*** Created by mjc on 2016/3/11.*/public class PullToRefreshRecycleView extends LinearLayout {//头部private IHeaderView mHeaderView;private int mHeaderHeight;//尾部private CustomFooterView mFooterView;private int mFooterHeight;//阻尼系数,越大,阻力越大public static final float RATIO = 0.5f;//当前是否阻止事件private boolean isIntercept;//是否正在刷新private boolean isRefreshing;//滑动类private Scroller mScroller;//刷新的Viewprivate RecyclerView mRefreshView;private Context mContext;private int mMaxScrollHeight;private boolean isFirst = true;public static final int NORMAL = 0;public static final int PULL_TO_REFRESH = 1;public static final int RELEASE_TO_REFRESH = 2;public static final int REFRESING = 3;private int mCurrentState;private int mTouchSlop;private OnRefreshListener listener;private boolean isPullDownMotion;private int lastVisible;public PullToRefreshRecycleView(Context context) {super(context);init(context);}public PullToRefreshRecycleView(Context context, AttributeSet attrs) {super(context, attrs);init(context);}private void init(Context context) {mContext = context;this.setOrientation(VERTICAL);mRefreshView = getRefreshView();mRefreshView.setBackgroundColor(Color.WHITE);LayoutParams listParams = new LayoutParams(-1, -1);mRefreshView.setLayoutParams(listParams);addView(mRefreshView);//添加HeaderViewmHeaderView = new CustomHeaderView(context);LayoutParams params = new LayoutParams(-1, -2);mHeaderView.setLayoutParams(params);addView(mHeaderView, 0);//添加FooterViewmFooterView = new CustomFooterView(context);LayoutParams fParams = new LayoutParams(-1, 200);mFooterView.setLayoutParams(fParams);addView(mFooterView, -1);//弹性滑动实现mScroller = new Scroller(context);mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);//第一次获取相关参数,并隐藏HeaderView,FooterViewif (isFirst) {mHeaderHeight = mHeaderView.getMeasuredHeight();mMaxScrollHeight = mHeaderHeight * 3;resetHeaderLayout(-mHeaderHeight);mFooterHeight = mFooterView.getMeasuredHeight();resetFooterLayout(-mFooterHeight);Log.v("@mHeaderHeight", mHeaderHeight + "");Log.v("@mFooterHeight", mFooterHeight + "");isFirst = false;}}private void resetHeaderLayout(int offset) {LayoutParams params = (LayoutParams) mHeaderView.getLayoutParams();params.topMargin = offset;mHeaderView.requestLayout();}private void resetFooterLayout(int offset) {LayoutParams params = (LayoutParams) mFooterView.getLayoutParams();params.bottomMargin = offset;mFooterView.requestLayout();}//按下时的位置,当事件被阻止时,第一次ActionDown事件,onTouchEvent无法获取这个位置//需要在onInterceptTouchEvent获取private float downY;@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {//如果当前是正在刷新并且是下拉状态,则当前视图处理事件if (isRefreshing && mScrollY < 0) {return true;}//如果当前是刷新状态,并且处于上拉状态,则视图不可进入下拉状态if (mScrollY >= 0 && isRefreshing)return false;boolean isIntercept = false;int action = ev.getAction();switch (action) {case MotionEvent.ACTION_DOWN:downY = ev.getY();break;case MotionEvent.ACTION_MOVE://如果达到了滑动条件if (Math.abs(ev.getY() - downY) >= mTouchSlop) {if (ev.getY() - downY > 0) {//下拉isIntercept = isEnablePullDown();if (isIntercept)//设置下拉还是上滑的状态,true表示下拉动作isPullDownMotion = true;} else {//上滑isIntercept = isEnableLoadMore();if (isIntercept)//false表示上滑状态isPullDownMotion = false;}} else {isIntercept = false;}break;case MotionEvent.ACTION_CANCEL://如果返回true,子视图如果包含点击事件,则无法进行处理isIntercept = false;break;case MotionEvent.ACTION_UP:isIntercept = false;break;}return isIntercept;}//记录当前滑动的位置private int mScrollY;@Overridepublic boolean onTouchEvent(MotionEvent event) {int action = event.getAction();switch (action) {case MotionEvent.ACTION_DOWN://第一次判断时,downY只能从intercept中获取,之后从这里获取downY = event.getY();break;case MotionEvent.ACTION_MOVE:float dY = event.getY() - downY;if (isPullDownMotion)//下拉doPullDownMoveEvent(dY);else {//自动加载更多doLoadMoreEvent(dY);}break;case MotionEvent.ACTION_UP:if (isPullDownMotion) {//处理下拉结果doPullDownResult();} else {//处理滑动加载更多结果doLoadMoreResult();}break;case MotionEvent.ACTION_CANCEL://同ACTION_UPif (isPullDownMotion) {doPullDownResult();} else {doLoadMoreResult();}break;}return true;}/*** 处理加载更多*/private void doLoadMoreResult() {//手指松开时,如果FooterView,没有完全滑动出来,自动滑动出来scrollTo(0, mFooterHeight);mScrollY = getScrollY();if (!isRefreshing) {isRefreshing = true;if (listener != null)listener.onLoadMore();}}/*** 加载更多完成后调用*/public void completeLoadMore() {scrollTo(0, 0);mScrollY = 0;isRefreshing = false;LinearLayoutManager manager = (LinearLayoutManager) mRefreshView.getLayoutManager();int count = manager.getItemCount();if (count > lastVisible + 1)//加载了更多数据mRefreshView.scrollToPosition(lastVisible + 1);}//处理加载更多private void doLoadMoreEvent(float y) {int scrollY = (int) (mScrollY - y);if (scrollY < 0) {scrollY = 0;}if (scrollY > mFooterHeight) {scrollY = mFooterHeight;}Log.v("@scrollY", scrollY + "");scrollTo(0, scrollY);}/*** 处理释放后的操作*/private void doPullDownResult() {//先获取现在滑动到的位置mScrollY = getScrollY();switch (mCurrentState) {case PULL_TO_REFRESH:mCurrentState = NORMAL;mHeaderView.onNormal();smoothScrollTo(0);break;case RELEASE_TO_REFRESH://松开时,如果是释放刷新,则开始进行刷新动作if (!isRefreshing) {//滑动到指定位置smoothScrollTo(-mHeaderHeight);mHeaderView.onRefreshing();isRefreshing = true;if (listener != null) {//执行刷新回调listener.onPullDownRefresh();}//如果当前滑动位置太靠下,则滑动到指定刷新位置} else if (mScrollY < -mHeaderHeight) {smoothScrollTo(-mHeaderHeight);}break;}}/*** 获取到数据后,调用*/public void completeRefresh() {isRefreshing = false;mCurrentState = NORMAL;smoothScrollTo(0);}private void doPullDownMoveEvent(float y) {int scrollY = (int) (mScrollY - y * RATIO);if (scrollY > 0) {scrollY = 0;}if (scrollY < -mMaxScrollHeight) {scrollY = -mMaxScrollHeight;}scrollTo(0, scrollY);if (isRefreshing)return;//设置相应的状态if (scrollY == 0) {mCurrentState = NORMAL;mHeaderView.onNormal();} else if (scrollY <= 0 && scrollY > -mHeaderHeight) {mCurrentState = PULL_TO_REFRESH;mHeaderView.onPullToRefresh(Math.abs(scrollY));} else if (scrollY <= -mHeaderHeight && scrollY >= -mMaxScrollHeight) {mCurrentState = RELEASE_TO_REFRESH;mHeaderView.onReleaseToRefresh(Math.abs(scrollY));}}/*** 从当前位置滑动到指定位置* @param y 滑动到的位置*/private void smoothScrollTo(int y) {int dY = y - mScrollY;mScroller.startScroll(0, mScrollY, 0, dY, 500);invalidate();}private RecyclerView getRefreshView() {mRefreshView = new RecyclerView(mContext);mRefreshView.setLayoutManager(new LinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, false));mRefreshView.addItemDecoration(new CustomItemDecoration(mContext, CustomItemDecoration.VERTICAL_LIST));return mRefreshView;}public void setAdapter(RecyclerView.Adapter adapter) {mRefreshView.setAdapter(adapter);}/*** 判断列表是否在最顶端* @return*/private boolean isEnablePullDown() {LinearLayoutManager manager = (LinearLayoutManager) mRefreshView.getLayoutManager();int firstVisible = manager.findFirstVisibleItemPosition();//当前还没有数据,可以进行下拉if(manager.getItemCount()==0)return true;return firstVisible == 0 && manager.getChildAt(0).getTop() == 0;}/*** 判断列表是否滑动到了最底部* @return*/private boolean isEnableLoadMore() {LinearLayoutManager manager = (LinearLayoutManager) mRefreshView.getLayoutManager();lastVisible = manager.findLastVisibleItemPosition();int totalCount = manager.getItemCount();//如果没有数据,只能下拉刷新if (totalCount == 0)return false;int bottom = manager.findViewByPosition(lastVisible).getBottom();int decorHeight = manager.getBottomDecorationHeight(mRefreshView.getChildAt(0));//最后一个child的底部位置在当前视图的上面return totalCount == lastVisible + 1 && bottom + decorHeight <= getMeasuredHeight();}@Overridepublic void computeScroll() {if (mScroller.computeScrollOffset()) {scrollTo(0, mScroller.getCurrY());mScrollY = mScroller.getCurrY();invalidate();}}/*** 设置Footer的内容*/public void setFooterViewState(boolean hasMoreData){if(hasMoreData){mFooterView.onRefreshing();}else{mFooterView.onNoData();}}public interface OnRefreshListener {void onPullDownRefresh();void onLoadMore();}public void setOnRefreshListener(OnRefreshListener listener) {this.listener = listener;}}接下来一步一步的进行分析。首先,我们在构造方法中,调用了init(Context)方法,如下:[java] viewplain copyprivate void init(Context context) {mContext = context;this.setOrientation(VERTICAL);mRefreshView = getRefreshView();mRefreshView.setBackgroundColor(Color.WHITE);LayoutParams listParams = new LayoutParams(-1, -1);mRefreshView.setLayoutParams(listParams);addView(mRefreshView);//添加HeaderViewmHeaderView = new CustomHeaderView(context);LayoutParams params = new LayoutParams(-1, -2);mHeaderView.setLayoutParams(params);addView(mHeaderView, 0);//添加FooterViewmFooterView = new CustomFooterView(context);LayoutParams fParams = new LayoutParams(-1, 200);mFooterView.setLayoutParams(fParams);addView(mFooterView, -1);//弹性滑动实现mScroller = new Scroller(context);mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();}方法中,我们构造了HeaderView,RecyclerView以及FooterView。HeaderView和FooterView是简单的自定义View,RecyclerView是直接构造的。并且在init()方法中,构造了Scroller,用于后面的弹性滑动需要。接着,后面会执行onSizeChange方法:[java] viewplain copy@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);//第一次获取相关参数,并隐藏HeaderView,FooterViewif (isFirst) {mHeaderHeight = mHeaderView.getMeasuredHeight();mMaxScrollHeight = mHeaderHeight * 3;resetHeaderLayout(-mHeaderHeight);mFooterHeight = mFooterView.getMeasuredHeight();resetFooterLayout(-mFooterHeight);Log.v("@mHeaderHeight", mHeaderHeight + "");Log.v("@mFooterHeight", mFooterHeight + "");isFirst = false;}}
设置了一个isFirst变量,防止重复设置里面的代码。在这个方法里面,我们获取了HeaderView,FooterView的测量高。并且,我们设置了HeaderView,FooterView的margin值,隐藏了头部和尾部。再接着,就是与用户的交互过程,即用户的触摸事件。这个实现过程,分成两块,一块是下拉刷新,一块是滑动到底部自动加载。这里我们一起分析。[java] viewplain copy//按下时的位置,当事件被阻止时,第一次ActionDown事件,onTouchEvent无法获取这个位置//需要在onInterceptTouchEvent获取private float downY;@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {//如果当前是正在刷新并且是下拉状态,则当前视图处理事件if (isRefreshing && mScrollY < 0) {return true;}//如果当前是刷新状态,并且处于上拉状态,则视图不可进入下拉状态if (mScrollY >= 0 && isRefreshing)return false;boolean isIntercept = false;int action = ev.getAction();switch (action) {case MotionEvent.ACTION_DOWN:downY = ev.getY();break;case MotionEvent.ACTION_MOVE://如果达到了滑动条件if (Math.abs(ev.getY() - downY) >= mTouchSlop) {if (ev.getY() - downY > 0) {//下拉isIntercept = isEnablePullDown();if (isIntercept)//设置下拉还是上滑的状态,true表示下拉动作isPullDownMotion = true;} else {//上滑isIntercept = isEnableLoadMore();if (isIntercept)//false表示上滑状态isPullDownMotion = false;}} else {isIntercept = false;}break;case MotionEvent.ACTION_CANCEL://如果返回true,子视图如果包含点击事件,则无法进行处理isIntercept = false;break;case MotionEvent.ACTION_UP:isIntercept = false;break;}return isIntercept;}onInterceptTouchEvent的作用,如果返回值为true,表示拦截事件,则事件交个当前控件进行处理,子View无法接收到事件;否则事件交给子View处理。  我们要知道,一般,一个事件序列,只能由一个控件处理,也就是说,如果这个控件消费了ACTION_DOWN事件,那么,后面的ACTION_MOVE等都会交给他处理。但是,如果他的parentView在ACTION_MOVE中,拦截了事件,事件将会转交给ParentView的onTouchEvent处理。然后,开始分析代码,[java] viewplain copy//如果当前是正在刷新并且是下拉状态,则当前视图处理事件if (isRefreshing && mScrollY < 0) {return true;}//如果当前是刷新状态,并且处于上拉状态,则视图不可进入下拉状态if (mScrollY >= 0 && isRefreshing)return false;如果当前为下拉并且在刷新状态,则返回true,表示拦截事件,RecyclerView不可滑动。如果当前是滑动加载更多,并且刷新状态,则不拦截,因为后面我想在滑动加载更多时,RecyclerView可以滑动。  截止后面,在ACTION_DOWN事件中,我们记录下按下的y轴位置。然后是ACTION_MOVE;[java] viewplain copy//如果达到了滑动条件if (Math.abs(ev.getY() - downY) >= mTouchSlop) {if (ev.getY() - downY > 0) {//下拉isIntercept = isEnablePullDown();if (isIntercept)//设置下拉还是上滑的状态,true表示下拉动作isPullDownMotion = true;} else {//上滑isIntercept = isEnableLoadMore();if (isIntercept)//false表示上滑状态isPullDownMotion = false;}} else {isIntercept = false;}mTouchSlop是滑动的最小值,如果小于这个值,我们认为没有滑动,大于这个值才算滑动。如果当前滑动,大于这个值,继续走里面的if判断,如果当前是下拉状态,并且是可以下拉,那么拦截事件,否则进行滑动加载更多,如果满足滑动加载更多的条件,那么可以向上滑动。并且整个过程,用isPullDownMotion记录下了是向上还是向下的动作。后面在onTouchEvent中需要使用。最后,ACTION_UP和ACTION_CANCEL不拦截,如果拦截,会影响到子View的点击事件。最后是onTouchEvent[java] viewplain copy//记录当前滑动的位置private int mScrollY;@Overridepublic boolean onTouchEvent(MotionEvent event) {int action = event.getAction();switch (action) {case MotionEvent.ACTION_DOWN://第一次判断时,downY只能从intercept中获取,之后从这里获取downY = event.getY();break;case MotionEvent.ACTION_MOVE:float dY = event.getY() - downY;if (isPullDownMotion)//下拉doPullDownMoveEvent(dY);else {//自动加载更多doLoadMoreEvent(dY);}break;case MotionEvent.ACTION_UP:if (isPullDownMotion) {//处理下拉结果doPullDownResult();} else {//处理滑动加载更多结果doLoadMoreResult();}break;case MotionEvent.ACTION_CANCEL://同ACTION_UPif (isPullDownMotion) {doPullDownResult();} else {doLoadMoreResult();}break;}return true;}看下拉环节(滑动加载更多类似,不再介绍),下拉过程ACTION_MOVE中先调用doPullDownMoveEvent,然后在ACTION_UP中调用了doPullDownResult。先看duPullDownMoveEvent[java] viewplain copyprivate void doPullDownMoveEvent(float y) {int scrollY = (int) (mScrollY - y * RATIO);if (scrollY > 0) {scrollY = 0;}if (scrollY < -mMaxScrollHeight) {scrollY = -mMaxScrollHeight;}scrollTo(0, scrollY);if (isRefreshing)return;//设置相应的状态if (scrollY == 0) {mCurrentState = NORMAL;mHeaderView.onNormal();} else if (scrollY <= 0 && scrollY > -mHeaderHeight) {mCurrentState = PULL_TO_REFRESH;mHeaderView.onPullToRefresh(Math.abs(scrollY));} else if (scrollY <= -mHeaderHeight && scrollY >= -mMaxScrollHeight) {mCurrentState = RELEASE_TO_REFRESH;mHeaderView.onReleaseToRefresh(Math.abs(scrollY));}}先计算滑动的位置,把滑动的位置限制在-mMaxScrollHeight和0之间,这样就不会滑动到其他地方,然后调用View的scrollTo方法,滑动到对应位置。这样就完成了触摸滑动。    后面,我们在通过滑动的位置,设置相应的状态。并回调HeaderView的各个状态的方法。然后再看doPullDownResult[java] viewplain copy/*** 处理释放后的操作*/private void doPullDownResult() {//先获取现在滑动到的位置mScrollY = getScrollY();switch (mCurrentState) {case PULL_TO_REFRESH:mCurrentState = NORMAL;mHeaderView.onNormal();smoothScrollTo(0);break;case RELEASE_TO_REFRESH://松开时,如果是释放刷新,则开始进行刷新动作if (!isRefreshing) {//滑动到指定位置smoothScrollTo(-mHeaderHeight);mHeaderView.onRefreshing();isRefreshing = true;if (listener != null) {//执行刷新回调listener.onPullDownRefresh();}//如果当前滑动位置太靠下,则滑动到指定刷新位置} else if (mScrollY < -mHeaderHeight) {smoothScrollTo(-mHeaderHeight);}break;}}这个方法,就是手指松开屏幕时触发。然后判断移动过程中的状态,如果是下拉刷新状态,则重新恢复到下拉之前,调用smoothScrollTo(后面分析具体实现),弹性滑动到初始位置,并设置状态为NORMAL状态。    如果松开时,是释放刷新状态,那么,先弹性滑动到刷新位置,并执行回调方法。现在分析,弹性滑动 smoothScrollTo[java] viewplain copy/*** 从当前位置滑动到指定位置* @param y 滑动到的位置*/private void smoothScrollTo(int y) {int dY = y - mScrollY;mScroller.startScroll(0, mScrollY, 0, dY, 500);invalidate();}
这个方法,必须要配合computeScroll使用,不然是没有效果的。具体的原因,需要查看View的绘制流程,这里我就不具体分析。[java] viewplain copy@Overridepublic void computeScroll() {if (mScroller.computeScrollOffset()) {scrollTo(0, mScroller.getCurrY());mScrollY = mScroller.getCurrY();invalidate();}}这个过程,是从Scroller的startScroll方法开始的,这个方法,调用后,Scroller的computeScrollOffset只要动作没有执行完,就会一直返回true。调用了startScroll方法,需要调用invalide()来引起computeScroll方法的调用,而里面scrollTo方法,才是真正实现位移的原因。里面再调用invalidate又重新引起了computeScroll方法,直到Scroller的computeOffset方法返回false。    这样,每次都移动一小段位置,就实现了平滑滑动的效果。使用方法,布局文件[html] viewplain copy<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><com.mjc.recyclerviewdemo.refresh.PullToRefreshRecycleViewandroid:id="@+id/prrv"android:layout_width="match_parent"android:layout_height="match_parent"/></LinearLayout>Activity中[java] viewplain copymPRRV = (PullToRefreshRecycleView) findViewById(R.id.prrv);mPRRV.setOnRefreshListener(new PullToRefreshRecycleView.OnRefreshListener() {@Overridepublic void onPullDownRefresh() {mHandler.postDelayed(new Runnable() {@Overridepublic void run() {datas.add(0, "add");mAdapter.notifyDataSetChanged();mPRRV.completeRefresh();}}, 2000);}@Overridepublic void onLoadMore() {mHandler.postDelayed(new Runnable() {@Overridepublic void run() {datas.add("李四");datas.add("王五");datas.add("张三");datas.add("李四");datas.add("王五");datas.add("张三");mAdapter.notifyDataSetChanged();mPRRV.completeLoadMore();}}, 1000);}});
                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android
相关文章推荐