Android 5.X新特性之为RecyclerView添加下拉刷新和上拉加载及SwipeRefreshLayout实现原理
RecyclerView已经写过两篇文章了,分别是Android 5.X新特性之RecyclerView基本解析及无限复用 和 Android 5.X新特性之为RecyclerView添加HeaderView和FooterView,既然来到这里还没学习的,先去学习下吧。
今天我们的主题是学习为RecyclerView添加下拉刷新和上拉加载功能。
首先,我们先来学习下拉刷新,google公司已经为我们提供的一个很好的包装类,那就是SwipeRefreshLayout,这个类可以支持我们向下滑动并进行监听。那么我们先了解一些基本知识,然后再从源码的角度来解析它。
A. SwipeRefreshLayout 是一个容器,直接继承于ViewGroup。
从其源码中我们可以直接看出,它是直接继承于ViewGroup的,所以它是一个容器,既然是一个容器,那么我们就可以向其中添加View。
B. SwipeRefreshLayout 封装了一些列的方法供我们使用,其中较常用的包括以下几个。
1. setColorSchemeResources: 刷新时动画的颜色,可以设置4个
2. setProgressBackgroundColorSchemeResource: 设置刷新时进度圆环的背景颜色
3. setOnRefreshListener(SwipeRefreshLayout.OnRefreshListener listener): 设置手势滑动监听器。
4. setRefreshing(Boolean refreshing): 设置组件的刷洗状态。
5. setSize(int size):设置进度圈的大小,只有两个值:DEFAULT、LARGE
其中最主要的是setOnRefreshListener,它是用来监听我们下拉手势的回调方法。
C. 接下来我们再从源码的角度来了解这个类:
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 就是捕捉到当前的事件。
那么我们再来看看它是怎么处理当前捕捉到的事件的。请看源码:
@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,今天就讲到这里吧。祝大家学习愉快。
更多资讯请关注微信平台,有博客更新会及时通知。爱学习爱技术。