转载请注明出处:http://blog.youkuaiyun.com/anyfive/article/details/53036125
前言
之前,我们介绍了下拉刷新上拉加载RecyclerView的使用,那么现在,我们就来说一下这个下拉刷新是怎么实现的。
在开发过程中,我想了两种方案。一是使用LinearLayout嵌套头部、recyclerview、尾部的方式,如下图:

- 当recyclerview滑动到顶部时,移动LinearLayout露出头部;
- 当recyclerview滑动到底部时,移动LinearLayout露出尾部;
著名的PullToRefreshListView采用的就是这种方式。
但后来,我放弃了这个方案,为什么呢?
因为多次尝试对recyclerview内部的fling事件进行处理,总是达不到自己想要的效果,我想要的是:
比如当前正在刷新,我向下fling RecyclerView,这时候RecyclerView向上滚动到顶部后,剩余速度继续露出RefreshHeader,而且我不喜欢每次都全露出来,而是要该露多少就露多少。简单地说,就是我想要给人一种刷新头部就是隶属于RecyclerView的、不存在断层的感觉。
恩,懂我意思吗?(刚刚怕表达不清楚,特地把同事叫来看他懂不懂)
总之,这种方案处理的效果我不满意!那怎么办呢?重来吧,删代码(心在滴血)。
于是有了第二种方案:给RecyclerView添加两个头部,分别是:用于造成下拉效果的辅助头部、刷新头部;添加两个尾部,分别是:加载尾部,用于造成上拉效果的辅助尾部。当滑动到顶部时,改变辅助头部的高度,把其他item往下推,造成下拉的感觉;上拉同理。
我还是再画个图吧:

- 在onLayout中,通过设置RecyclerView的margin,将头部和尾部偏移出屏幕;
- 辅助头部:初始高度为1px;当RecyclerView滑动到顶部时,通过改变高度,造成下拉效果;
- 辅助尾部:初始高度为1px;当RecyclerView滑动到底部时,通过改变高度,造成上拉的效果
思路就是这样,但在实际的开发过程中,下拉还好,而上拉会遇到各种各样的问题,不过好在解决了这些问题后,实际的效果完美符合我的要求,所以PTLRecyclerView采用了这个方案进行实现。
接下来我们来依次介绍下拉和上拉,以及开发过程中遇到的问题。
下拉刷新
其实下拉刷新是比较简单的,PullToRefreshRecyclerView继承于HeaderAndFooterRecyclerView,我们按顺序来一一介绍PullToRefreshRecyclerView中的几个主要方法:
- 首先介绍下全局变量,免得看代码的时候吃力:
private int mState = STATE_DEFAULT;
public final static int STATE_DEFAULT = 0;
public final static int STATE_PULLING = 1;
public final static int STATE_RELEASE_TO_REFRESH = 2;
public final static int STATE_REFRESHING = 3;
private float mPullRatio = 0.5f;
private View topView;
private View mRefreshView;
private int mRefreshViewHeight = 0;
private float mFirstY = 0;
private boolean mPulling = false;
private boolean mRefreshEnable = true;
private ValueAnimator valueAnimator;
private OnRefreshListener mOnRefreshListener;
private RefreshHeaderCreator mRefreshHeaderCreator;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 在构造函数中初始化,获得默认的刷新头部:
private void init(Context context) {
if (topView == null) {
topView = new View(context);
topView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 1));
setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false));
mRefreshHeaderCreator = new DefaultRefreshHeaderCreator();
mRefreshView = mRefreshHeaderCreator.getRefreshView(context,this);
}
}
- 在onLayout方法中,获得刷新头部的高度,并偏移RecyclerView:
/**
* 在measure的时候,隐藏刷新头部
*/
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mRefreshView != null && mRefreshViewHeight == 0) {
mRefreshView.measure(0,0);
mRefreshViewHeight = mRefreshView.getLayoutParams().height;
ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
marginLayoutParams.setMargins(marginLayoutParams.leftMargin, marginLayoutParams.topMargin-mRefreshViewHeight-1, marginLayoutParams.rightMargin, marginLayoutParams.bottomMargin);
setLayoutParams(marginLayoutParams);
}
super.onMeasure(widthSpec, heightSpec);
}
- 触摸事件:
@Override
public boolean onTouchEvent(MotionEvent e) {
if (!mRefreshEnable) return super.onTouchEvent(e);
if (mRefreshView == null)
return super.onTouchEvent(e);
if (valueAnimator != null && valueAnimator.isRunning())
return super.onTouchEvent(e);
switch (e.getAction()) {
case MotionEvent.ACTION_MOVE:
if (!mPulling) {
if (isTop()) {
mFirstY = e.getRawY();
}
else
break;
}
float distance = (int) ((e.getRawY() - mFirstY)*mPullRatio);
if (distance < 0) break;
mPulling = true;
if (mState == STATE_REFRESHING) {
distance += mRefreshViewHeight;
}
setState(distance);
return true;
case MotionEvent.ACTION_UP:
replyPull();
break;
}
return super.onTouchEvent(e);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 判断是否滑动到了顶部:
private boolean isTop() {
return !ViewCompat.canScrollVertically(this, -1);
}
- 设置当前下拉状态:
private void setState(float distance) {
if (mState == STATE_REFRESHING) {
}
else if (distance == 0) {
mState = STATE_DEFAULT;
}
else if (distance >= mRefreshViewHeight) {
int lastState = mState;
mState = STATE_RELEASE_TO_REFRESH;
if (mRefreshHeaderCreator != null)
if (!mRefreshHeaderCreator.onReleaseToRefresh(distance,lastState))
return;
}
else if (distance < mRefreshViewHeight) {
int lastState = mState;
mState = STATE_PULLING;
if (mRefreshHeaderCreator != null)
if (!mRefreshHeaderCreator.onStartPull(distance,lastState))
return;
}
startPull(distance);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
这里可以看到,当头部构造器的onStartPull和onReleaseToRefresh返回false时,便不再下拉,其实这里也是为了应对类似“超过多少就不再下拉了”这种需求。
- 改变辅助头部的高度,造成下拉的效果:
private void startPull(float distance) {
if (distance < 1)
distance = 1;
if (topView != null) {
LayoutParams layoutParams = (LayoutParams) topView.getLayoutParams();
layoutParams.height = (int) distance;
topView.setLayoutParams(layoutParams);
}
}
- 松手回弹,在这个方法中,我们需要判断是直接刷新,还是直接回弹到原来位置:
private void replyPull() {
mPulling = false;
float destinationY = 0;
if (mState == STATE_REFRESHING) {
destinationY = mRefreshViewHeight;
}
else if (mState == STATE_RELEASE_TO_REFRESH) {
mState = STATE_REFRESHING;
if (mRefreshHeaderCreator != null)
mRefreshHeaderCreator.onStartRefreshing();
if (mOnRefreshListener != null)
mOnRefreshListener.onStartRefreshing();
if (mState != STATE_REFRESHING) return;
destinationY = mRefreshViewHeight;
} else if (mState == STATE_DEFAULT || mState == STATE_PULLING) {
mState = STATE_DEFAULT;
}
LayoutParams layoutParams = (RecyclerView.LayoutParams) topView.getLayoutParams();
float distance = layoutParams.height;
if (distance <= 0) return;
valueAnimator = ObjectAnimator.ofFloat(distance, destinationY).setDuration((long) (distance * 0.5));
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float nowDistance = (float) animation.getAnimatedValue();
startPull(nowDistance);
}
});
valueAnimator.start();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 完成刷新:
public void completeRefresh() {
if (mRefreshHeaderCreator != null)
mRefreshHeaderCreator.onStopRefresh();
mState = STATE_DEFAULT;
replyPull();
mRealAdapter.notifyDataSetChanged();
}
- 在设置适配器的时候,添加辅助头部和刷新头部:
@Override
public void setAdapter(Adapter adapter) {
super.setAdapter(adapter);
if (mRefreshView != null) {
addHeaderView(topView);
addHeaderView(mRefreshView);
}
}
- 设置自定义的头部:
public void setRefreshViewCreator(RefreshHeaderCreator refreshHeaderCreator) {
this.mRefreshHeaderCreator = refreshHeaderCreator;
mRefreshView = refreshHeaderCreator.getRefreshView(getContext(),this);
if (mAdapter != null) {
addHeaderView(topView);
addHeaderView(mRefreshView);
}
mRealAdapter.notifyDataSetChanged();
}
以上就是PullToRefreshRecyclerView主要的几个方法了,介绍得算比较清楚吧,再加上代码中已经有注释了,就不再累赘了。核心就一句话:拦截触摸事件,改变辅助头部的高度。 就是这么easy~~~~
上拉加载
本来上拉加载我想单独用一篇文章来介绍的,但其实上拉加载的处理和下拉刷新的处理逻辑是一致的,因此在这里便一起介绍了吧,双飞更开心呦客官~~
咳咳,说正经的,上面我们说过上拉加载会遇到各种问题,具体有哪些呢?
- 滑动到底部时,继续上拉,改变辅助底部的高度造成上拉的效果,然后现实很骨感,你会发现(通过调试或打印)辅助底部的高度是在改变,但RecyclerView中的item并没有挤上去啊,根本就没有上拉的效果出现。
- 当你添加FooterView的时候,发现你添加的FooterView居然跑到刷新底部的下面去了,坑了个爹…..
- 哎,怎么好像没了,我记得碰到了很多问题呀…….
以下是我的解决方法:
1. 这个问题我实在没想到什么好办法,因此用了最粗暴的方式:在改变高度后直接调用scrollToPosition滚动到最底部。这样做有什么后果呢?效率肯定是不高的,但为了效果,我可以忍….经过测试,StaggredLayoutManager不会有任何影响,效果溜溜哒。但是但是,LinearLayoutManager上拉时会出现卡顿的现象,这个怎么忍!当然GridLayoutManager也会卡顿,毕竟他是LinearLayoutManager的儿子啊,遗传病。为什么呢?因为LinearLayoutManager对item的layout和StaggredLayoutManager的是不一样的,既然StaggredLayoutManager没问题,那么我们用只有一列的StaggredLayoutManager替代LinearLayoutManager就是最粗暴的方法。当然,更好的方式是直接继承LayoutManager写一个自己的LinearLayoutManager,但由于时间和水平的限制,就……采用StaggredLayoutManager吧。这就是为什么我之前说使用PullToLoadRecyclerView的时候,要用PTLLinearLayout和PTLGridLayoutManager。
2. 这个问题其实最好解决,继承HeaderAndFooterAdapter写一个PullToLoadAdapter就可以啦。
虽然解决方法比较坑爹,但不管黑猫还是白猫,能抓老鼠的就是好猫。当然,这么说有点过分了,所以在这里,希望有大牛有更好的方法,欢迎到github上提交您的代码,共同构建这个项目。
PullToLoadRecyclerView和PullToRefreshRecyclerView的代码逻辑其实基本一致,而PullToLoadAdapter的代码和HeaderAndFooterAdapter也比较像,因此这里就不再展开了,有兴趣的同学可以去github上把项目clone下来看看。
自定义的刷新头部和加载尾部
有没有遇到过这种情况,当你辛辛苦苦找到一个需要的库时,却发现他的UI居然不支持自定义!摔!在实际开发中,产品和设计怎么会允许你使用那个库默认的UI设计,这是基本不可能的事。因此,支持自定义的刷新头部和加载尾部是非常非常重要的事!!
之前在介绍使用方法时,我们就已经介绍了如何使用自定义的刷新头部和加载尾部,而通过上面的代码,你应该也已经理解了RefreshHeaderCreator和LoadFooterCreator的工作方式。
其实就是使用这两个抽象类,把刷新头部和加载尾部的UI与RecyclerView进行解耦,交给用户自己去实现,项目中的默认刷新头部和加载尾部就是很好的例子,相信你看完应该就知道怎么去构造自己的刷新头部和加载尾部了。
直接上DefaultRefreshHeaderCreator的代码:
public class DefaultRefreshHeaderCreator extends RefreshHeaderCreator {
private View mRefreshView;
private ImageView iv;
private TextView tv;
private int rotationDuration = 200;
private int loadingDuration = 1000;
private ValueAnimator ivAnim;
@Override
public boolean onStartPull(float distance,int lastState) {
if (lastState == PullToRefreshRecyclerView.STATE_DEFAULT ) {
iv.setImageResource(R.drawable.arrow_down);
iv.setRotation(0f);
tv.setText("下拉刷新");
} else if (lastState == PullToRefreshRecyclerView.STATE_RELEASE_TO_REFRESH) {
startArrowAnim(0);
tv.setText("下拉刷新");
}
return true;
}
@Override
public void onStopRefresh() {
if (ivAnim != null) {
ivAnim.cancel();
}
}
@Override
public boolean onReleaseToRefresh(float distance,int lastState) {
if (lastState == PullToRefreshRecyclerView.STATE_DEFAULT ) {
iv.setImageResource(R.drawable.arrow_down);
iv.setRotation(-180f);
tv.setText("松手立即刷新");
} else if (lastState == PullToRefreshRecyclerView.STATE_PULLING) {
startArrowAnim(-180f);
tv.setText("松手立即刷新");
}
return true;
}
@Override
public void onStartRefreshing() {
iv.setImageResource(R.drawable.loading);
startLoadingAnim();
tv.setText("正在刷新...");
}
@Override
public View getRefreshView(Context context, RecyclerView recyclerView) {
mRefreshView = LayoutInflater.from(context).inflate(R.layout.layout_ptr_ptl,recyclerView,false);
iv = (ImageView) mRefreshView.findViewById(R.id.iv);
tv = (TextView) mRefreshView.findViewById(R.id.tv);
return mRefreshView;
}
private void startArrowAnim(float roration) {
if (ivAnim != null) {
ivAnim.cancel();
}
float startRotation = iv.getRotation();
ivAnim = ObjectAnimator.ofFloat(startRotation,roration).setDuration(rotationDuration);
ivAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
iv.setRotation((Float) animation.getAnimatedValue());
}
});
ivAnim.start();
}
private void startLoadingAnim() {
if (ivAnim != null) {
ivAnim.cancel();
}
ivAnim = ObjectAnimator.ofFloat(0,360).setDuration(loadingDuration);
ivAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
iv.setRotation((Float) animation.getAnimatedValue());
}
});
ivAnim.setRepeatMode(ObjectAnimator.RESTART);
ivAnim.setRepeatCount(ObjectAnimator.INFINITE);
ivAnim.setInterpolator(new LinearInterpolator());
ivAnim.start();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
系不系很简单?
照例上两张用烂了的效果图:



源码地址:https://github.com/whichname/PTLRecyclerView
有意见或建议或疑问等等,欢迎提出~~
传送门:
android 打造真正的下拉刷新上拉加载recyclerview(一):使用
android 打造真正的下拉刷新上拉加载recyclerview(二):添加删除头尾部
android 打造真正的下拉刷新上拉加载recyclerview(四):自动加载和其他封装