之前一直写的是ListVIew下拉刷新,可是好多朋友都说要RecycleView的下拉刷新和滑动载入。事实上,这个原理都是几乎相同。抽出时间,我就写了下RecycleView的下拉刷新和滑动载入很多其它。因此,这才写到博客里,记录一下。
在大家阅读这篇博客前。大家须要了解的知识
事件是以ACTION_DOWN開始到ACTION_UP货ACTION_CANCEL结束的一个序列,期间事件分发,能够通过onInterceptTouchEvent方法和dispatchTouchEvent进行事件的阻止和消费。
比方怎样加入一个Decoration.
当RecyclerView滑动到了最顶部,则能够触发下拉事件;当RecyclerView滑动到了底部,则能够触发滑动载入很多其它的事件。然后在通过事件分发。进行滑动事件的处理。
package 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; //刷新的View private 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); //加入HeaderView mHeaderView = new CustomHeaderView(context); LayoutParams params = new LayoutParams(-1, -2); mHeaderView.setLayoutParams(params); addView(mHeaderView, 0); //加入FooterView mFooterView = new CustomFooterView(context); LayoutParams fParams = new LayoutParams(-1, 200); mFooterView.setLayoutParams(fParams); addView(mFooterView, -1); //弹性滑动实现 mScroller = new Scroller(context); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); //第一次获取相关參数,并隐藏HeaderView,FooterView if (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; @Override public 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; @Override public 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_UP if (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(); } @Override public 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; } }
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); //加入HeaderView mHeaderView = new CustomHeaderView(context); LayoutParams params = new LayoutParams(-1, -2); mHeaderView.setLayoutParams(params); addView(mHeaderView, 0); //加入FooterView mFooterView = new CustomFooterView(context); LayoutParams fParams = new LayoutParams(-1, 200); mFooterView.setLayoutParams(fParams); addView(mFooterView, -1); //弹性滑动实现 mScroller = new Scroller(context); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); }
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); //第一次获取相关參数,并隐藏HeaderView。FooterView if (isFirst) { mHeaderHeight = mHeaderView.getMeasuredHeight(); mMaxScrollHeight = mHeaderHeight * 3; resetHeaderLayout(-mHeaderHeight); mFooterHeight = mFooterView.getMeasuredHeight(); resetFooterLayout(-mFooterHeight); Log.v("@mHeaderHeight", mHeaderHeight + ""); Log.v("@mFooterHeight", mFooterHeight + ""); isFirst = false; } }
在这种方法里面,我们获取了HeaderView,FooterView的測量高。而且,我们设置了HeaderView。FooterView的margin值,隐藏了头部和尾部。
//按下时的位置,当事件被阻止时。第一次ActionDown事件,onTouchEvent无法获取这个位置 //须要在onInterceptTouchEvent获取 private float downY; @Override public 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; }
//假设当前是正在刷新而且是下拉状态,则当前视图处理事件 if (isRefreshing && mScrollY < 0) { return true; } //假设当前是刷新状态,而且处于上拉状态。则视图不可进入下拉状态 if (mScrollY >= 0 && isRefreshing) return false;
假设当前为下拉而且在刷新状态,则返回true,表示拦截事件,RecyclerView不可滑动。假设当前是滑动载入很多其它。而且刷新状态。则不拦截,由于后面我想在滑动载入很多其它时,RecyclerView能够滑动。 截止后面。在ACTION_DOWN事件中,我们记录下按下的y轴位置。然后是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; }
假设当前滑动,大于这个值,继续走里面的if推断,假设当前是下拉状态,而且是能够下拉。那么拦截事件,否则进行滑动载入很多其它。假设满足滑动载入很多其它的条件,那么能够向上滑动。而且整个过程,用isPullDownMotion记录下了是向上还是向下的动作。后面在onTouchEvent中须要使用。最后,ACTION_UP和ACTION_CANCEL不拦截。假设拦截,会影响到子View的点击事件。
//记录当前滑动的位置 private int mScrollY; @Override public 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_UP if (isPullDownMotion) { doPullDownResult(); } else { doLoadMoreResult(); } break; } return true; }
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)); } }
这样就完毕了触摸滑动。 后面。我们在通过滑动的位置,设置对应的状态。并回调HeaderView的各个状态的方法。
/** * 处理释放后的操作 */ 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; } }
/** * 从当前位置滑动到指定位置 * @param y 滑动到的位置 */ private void smoothScrollTo(int y) { int dY = y - mScrollY; mScroller.startScroll(0, mScrollY, 0, dY, 500); invalidate(); }
这种方法,必须要配合computeScroll使用。不然是没有效果的。详细的原因,须要查看View的绘制流程,这里我就不详细分析。
@Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(0, mScroller.getCurrY()); mScrollY = mScroller.getCurrY(); invalidate(); } }
这样,每次都移动一小段位置,就实现了平滑滑动的效果。
<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.PullToRefreshRecycleView android:id="@+id/prrv" android:layout_width="match_parent" android:layout_height="match_parent"/> </LinearLayout>
mPRRV = (PullToRefreshRecycleView) findViewById(R.id.prrv); mPRRV.setOnRefreshListener(new PullToRefreshRecycleView.OnRefreshListener() { @Override public void onPullDownRefresh() { mHandler.postDelayed(new Runnable() { @Override public void run() { datas.add(0, "add"); mAdapter.notifyDataSetChanged(); mPRRV.completeRefresh(); } }, 2000); } @Override public void onLoadMore() { mHandler.postDelayed(new Runnable() { @Override public void run() { datas.add("李四"); datas.add("王五"); datas.add("张三"); datas.add("李四"); datas.add("王五"); datas.add("张三"); mAdapter.notifyDataSetChanged(); mPRRV.completeLoadMore(); } }, 1000); } });