RecyclerView已经写过两篇文章了,分别是Android 5.X新特性之RecyclerView基本解析及无限复用 和 Android 5.X新特性之为RecyclerView添加HeaderView和FooterView,既然来到这里还没学习的,先去学习下吧。html
今天咱们的主题是学习为RecyclerView添加下拉刷新和上拉加载功能。python
首先,咱们先来学习下拉刷新,google公司已经为咱们提供的一个很好的包装类,那就是SwipeRefreshLayout,这个类能够支持咱们向下滑动并进行监听。那么咱们先了解一些基本知识,而后再从源码的角度来解析它。android
A. SwipeRefreshLayout 是一个容器,直接继承于ViewGroup。微信
从其源码中咱们能够直接看出,它是直接继承于ViewGroup的,因此它是一个容器,既然是一个容器,那么咱们就能够向其中添加View。
B. SwipeRefreshLayout 封装了一些列的方法供咱们使用,其中较经常使用的包括如下几个。ide
1. setColorSchemeResources: 刷新时动画的颜色,能够设置4个 2. setProgressBackgroundColorSchemeResource: 设置刷新时进度圆环的背景颜色 3. setOnRefreshListener(SwipeRefreshLayout.OnRefreshListener listener): 设置手势滑动监听器。 4. setRefreshing(Boolean refreshing): 设置组件的刷洗状态。 5. setSize(int size):设置进度圈的大小,只有两个值:DEFAULT、LARGE
其中最主要的是setOnRefreshListener,它是用来监听咱们下拉手势的回调方法。布局
C. 接下来咱们再从源码的角度来了解这个类:post
SwipeRefreshLayout 是一个ViewGroup容器,那在向它添加子View的时候,那首先会去测量各个子View的大小来肯定自己的大小,而且还会制定子View的坐标位置,最后绘制View并显示出来。针对ViewGroup的绘制我以前有写过一篇博文,你们能够去参考下Android自定义控件之继承ViewGroup建立新容器(四) ,里面有详细的讲解。而咱们今天所要讲解的是从SwipeRefreshLayout 的事件机制来讲起,也更符合咱们下拉刷新的主题。学习
在SwipeRefreshLayout 的事件拦截分发器onInterceptTouchEvent中,它是这么定制的,源码以下:动画
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { ensureTarget(); final int action = MotionEventCompat.getActionMasked(ev); if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing || mNestedScrollInProgress) { // Fail fast if we're not in a state where a swipe is possible return false; } switch (action) { case MotionEvent.ACTION_DOWN: setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true); mActivePointerId = MotionEventCompat.getPointerId(ev, 0); mIsBeingDragged = false; final float initialDownY = getMotionEventY(ev, mActivePointerId); if (initialDownY == -1) { return false; } mInitialDownY = initialDownY; break; case MotionEvent.ACTION_MOVE: if (mActivePointerId == INVALID_POINTER) { Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id."); return false; } final float y = getMotionEventY(ev, mActivePointerId); if (y == -1) { return false; } final float yDiff = y - mInitialDownY; if (yDiff > mTouchSlop && !mIsBeingDragged) { mInitialMotionY = mInitialDownY + mTouchSlop; mIsBeingDragged = true; mProgress.setAlpha(STARTING_PROGRESS_ALPHA); } break; case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIsBeingDragged = false; mActivePointerId = INVALID_POINTER; break; } return mIsBeingDragged; }
它最终返回的是表明是否滑动的mIsBeingDragged布尔值。在咱们按下,抬起,或取消时mIsBeingDragged的值是false,意思是在这几个动做中,SwipeRefreshLayout 自己是不拦截事件的,而是传递给父类,让父类进行处理。而咱们主要来看MotionEvent.ACTION_MOVE:这个动做,它首先判断是不是可用的活动id: mActivePointerId,而后根据获得mActivePointerId来获取滑动的中坐标距离值:Y,而后作出判断:若是Y==-1就表明没滑动,因此直接返回false表示不拦截;若是Y值大于规定的最小滑动距离mTouchSlop值,而且!mIsBeingDragged为真,那么就让mIsBeingDragged == true;并返回,也就是在这种状况下,SwipeRefreshLayout 它本身消化了事件,而不是传递给父类。所以,当咱们在向下滑动了必定的距离时,SwipeRefreshLayout 就是捕捉到当前的事件。ui
那么咱们再来看看它是怎么处理当前捕捉到的事件的。请看源码:
@Override public boolean onTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); int pointerIndex = -1; if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } if (!isEnabled() || mReturningToStart || canChildScrollUp() || mNestedScrollInProgress) { // Fail fast if we're not in a state where a swipe is possible return false; } switch (action) { case MotionEvent.ACTION_DOWN: mActivePointerId = MotionEventCompat.getPointerId(ev, 0); mIsBeingDragged = false; break; case MotionEvent.ACTION_MOVE: { pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); if (pointerIndex < 0) { Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id."); return false; } final float y = MotionEventCompat.getY(ev, pointerIndex); final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; if (mIsBeingDragged) { if (overscrollTop > 0) { moveSpinner(overscrollTop); } else { return false; } } break; } case MotionEventCompat.ACTION_POINTER_DOWN: { pointerIndex = MotionEventCompat.getActionIndex(ev); if (pointerIndex < 0) { Log.e(LOG_TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index."); return false; } mActivePointerId = MotionEventCompat.getPointerId(ev, pointerIndex); break; } case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; case MotionEvent.ACTION_UP: { pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); if (pointerIndex < 0) { Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id."); return false; } final float y = MotionEventCompat.getY(ev, pointerIndex); final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; mIsBeingDragged = false; finishSpinner(overscrollTop); mActivePointerId = INVALID_POINTER; return false; } case MotionEvent.ACTION_CANCEL: return false; } return true; }
一样的道理在MotionEvent.ACTION_DOWN和case MotionEvent.ACTION_CANCEL时不处理事件,交给父类处理。而在MotionEvent.ACTION_MOVE:中获取到与顶端窗口的overscrollTop,若是overscrollTop值大于0就调用moveSpinner(overscrollTop);方法来初始化mCircleView旋转的。最后在MotionEvent.ACTION_UP:抬起事件中,一样获取overscrollTop,且调用finishSpinner(overscrollTop);方法来完成mCircleView的旋转事件并回复一些属性配置值。
而后咱们再来看看finishSpinner(overscrollTop);方法中是怎么处理的。
private void finishSpinner(float overscrollTop) { if (overscrollTop > mTotalDragDistance) { setRefreshing(true, true /* notify */); } else { // cancel refresh mRefreshing = false; mProgress.setStartEndTrim(0f, 0f); Animation.AnimationListener listener = null; if (!mScale) { listener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { if (!mScale) { startScaleDownAnimation(null); } } @Override public void onAnimationRepeat(Animation animation) { } }; } animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener); mProgress.showArrow(false); } }
方法里面很简单,if (overscrollTop > mTotalDragDistance) 就调用setRefreshing(true, true /* notify */);用来设置刷新事件的,不然就回复初始前的属性配置值。
再来看看setRefreshing(true, true)方法:
private void setRefreshing(boolean refreshing, final boolean notify) { if (mRefreshing != refreshing) { mNotify = notify; ensureTarget(); mRefreshing = refreshing; if (mRefreshing) { animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener); } else { startScaleDownAnimation(mRefreshListener); } } }
也很好理解,由于传进来的refreshing值为true,因此它会调用animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);来开启mCircleView的动画展现,并传进了mRefreshListener监听器,这个监听器是什么呢?来看看
private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { if (mRefreshing) { // Make sure the progress view is fully visible mProgress.setAlpha(MAX_ALPHA); mProgress.start(); if (mNotify) { if (mListener != null) { mListener.onRefresh(); } } mCurrentTargetOffsetTop = mCircleView.getTop(); } else { reset(); } } };
它是一个动画监听器,在动画结束时调用mListener.onRefresh();而mListener是一个接口,里面封装了一个onRefresh()的方法,而且它暴露了对外调用的方法setOnRefreshListener(),因此咱们能够在Activity中调用该方法能够实现咱们本身的逻辑业务。
ok,到这里,相信你们都知道了wipeRefreshLayout.setOnRefreshListener();的工做原理,那么咱们如今来实现咱们的刷新功能吧;
首先,咱们的布局文件先把RecyclerView放到SwipeRefreshLayout容器中:
recycer_view.xml文件:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:custom="http://schemas.android.com/apk/res-auto" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v4.widget.SwipeRefreshLayout android:id="@+id/srl_refresh" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.RecyclerView android:id="@+id/recycler_view" custom:listDividerSize="2dp" custom:listDividerBackgroundColor="#FF0000" android:layout_width="match_parent" android:layout_height="match_parent"> </android.support.v7.widget.RecyclerView> </android.support.v4.widget.SwipeRefreshLayout> </LinearLayout>
而后RecycerActivity中配置一些SwipeRefreshLayout属性值,并调用setOnRefreshListener方法并在onRefresh()实现本身的逻辑业务:
srl_refresh.setColorSchemeResources(android.R.color.holo_blue_light, android.R.color.holo_red_light,android.R.color.holo_orange_light, android.R.color.holo_green_light); srl_refresh.setProgressBackgroundColorSchemeResource(android.R.color.white); srl_refresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { new Handler().postDelayed(new Runnable() { @Override public void run() { List<String> newDatas = new ArrayList<String>(); for (int i = 0; i <5; i++) { int index = i + 1; newDatas.add("new item" + index); } mBaseRecyclerAdapter.addDatas(newDatas); srl_refresh.setRefreshing(false); Toast.makeText(RecycerActivity.this, "更新了五条数据...", Toast.LENGTH_SHORT).show(); } }, 5000); } });
来看看结果吧
好了,RecyclerView利用SwipeRefreshLayout实现上拉刷新咱们已经实现了,而且也带你们看过它的实现原理了,相信你们必定能更好的掌握它了,那么接下来咱们就来实现上拉加载了。
在上一讲中,咱们已经实现了在底部添加上了一个FooterView,那么咱们如今能够利用它来实现咱们的上拉加载。
其思想咱们能够这样设计,当咱们滑动到最后一个ItemView时,让它去加载数据,那怎么获取到列表的最后一个ItemView呢?所幸的是,在RecyclerView中封装的LayoutManger子类中有这样的方法能够供咱们获取到最后一个ItemView,该方法是findLastVisibleItemPosition();那咱们又该怎么监听RecyclerView滑动呢?能够调用它的addOnScrollListener()方法,由此咱们找到了解决方案
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if(newState == RecyclerView.SCROLL_STATE_IDLE && lastVisibleItem + 1 == mBaseRecyclerAdapter.getItemCount()){ mBaseRecyclerAdapter.changeStatus(BaseRecyclerAdapter.LOADING_MORE); new Handler().postDelayed(new Runnable() { @Override public void run() { List<String> newDatas = new ArrayList<String>(); for (int i = 0; i< 5; i++) { int index = i +1; newDatas.add("more item" + index); } if(newDatas == null){ mBaseRecyclerAdapter.changeStatus(BaseRecyclerAdapter.LOADED_MORE); return; } mBaseRecyclerAdapter.addMoreDatas(newDatas); mBaseRecyclerAdapter.changeStatus(BaseRecyclerAdapter.LOAD_MORE); Toast.makeText(RecycerActivity.this,"已加载了数据", Toast.LENGTH_SHORT).show(); } },1000); } } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); lastVisibleItem = linearLayoutManger.findLastVisibleItemPosition(); } });
代码解释:首先咱们会在onScrolled方法中回去到最后一行的ItenView,而后再onScrollStateChanged方法中进行必要的判断,若是lastVisibleItem + 1 == mBaseRecyclerAdapter.getItemCount(),那么就能够肯定给ItemView是最后一个ItemView,而后就能够用来实现咱们的业务逻辑了,在这里我让它新加了5条数据,而后更新Adapter。
最后在onBindViewHolder稍做修改,以下
@Override public void onBindViewHolder(BaseViewHolderHelper holder, int position) { //把每个itemView设置一个标签,方便之后根据标签获取到该itemView以便作其余事项,比较点击事件 if(getItemViewType(position) == TYPE_HEADER){ return; }else if(getItemViewType(position) == TYPE_FOOTER){ FooterViewHolder footViewHolder=(FooterViewHolder)holder; footViewHolder.footView.setText("上拉加载更多..."); switch (status){ case LOAD_MORE: footViewHolder.footView.setText("上拉加载更多..."); break; case LOADING_MORE: footViewHolder.footView.setText("正在加载中..."); break; case LOADED_MORE: footViewHolder.footView.setText("已加载完毕"); break; } } else{ ... } }
ok,来看看结果吧:
好了,已经实现了上拉加载的功能了,相信你们也均可以作不少事情了。
总结:本节主题是为RecyclerView添加下拉刷新和上拉加载的功能,基本的思路也都已讲清楚了,并且着重的讲解了一下利用SwipeRefreshLayout实现下拉刷新的实现原理,相信你们经过这节更能学到一些原理性的东西,ok,今天就讲到这里吧。祝你们学习愉快。
更多资讯请关注微信平台,有博客更新会及时通知。爱学习爱技术。