关于NestedScroll
NestedScroll(嵌套滑动)其实在Android5.0就已经出了,大名鼎鼎的CoordinatorLayout就是嵌套滑动的产物。
传统的滑动,一旦事件被子view获取,那么也将由子view进行消费,父控件无法参与事件的消费过程。而嵌套滑动则是在子view无法继续消费滑动距离时,将产生的距离传递给父控件(滑动事件依旧由子view获取,只是将滑动的距离传递给了父控件),形成子view和父控件嵌套滑动的效果,从而产生了下拉刷新、上拉加载。
NestedScroll
与嵌套滑动相关的有一下四个类:
- NestedScrollChild
- NestedScorllChildHelper
- NestedScrollParent
- NestedScrollParentHelper
关于这四个类的介绍和分析,可以参考以下文章:
http://www.jianshu.com/p/6547ec3202bd
看完是不是依旧有点懵逼呢?没事!我们上代码走流程!
1.先创建一个SimpleRefreshLayout, 实现NestedScrollParent接口。实现一下接口里面的方法.
public interface NestedScrollingParent {
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
public void onStopNestedScroll(View target);
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public int getNestedScrollAxes();
}
2.用SimpleRefreshLayout作为父控件,初始化RecyclerView假数据,开始走流程!
相关方法打上断点,滑动RecyclerView, 观察!
-
事件先从recyclerView开始(因为获取到事件的是子view),走recyclerView的
StartNestedScroll
-
(Rv) startNestedScroll --> (SimpleRefreshLayout) onStartNestedScroll
recyclerView告诉父控件:我要开始滑动了!你要不要动呢?
很显然如果onStartNestedScroll返回true,则说明:儿子阿,我也要一起动!
-
(SimpleRefreshLayout) onStartNestedScroll --> onNestedScrollAccepted
通过parentHelper表明该次滑动是否被接受了
-
(SimpleRefreshLayout) onNestedScrollAccepted --> (Rv) stopNestedScroll
rv结束嵌套滑动过程
-
(Rv) stopNestedScroll --> (SimpleRefreshLayout) onStopNestedScroll
rv告诉父控件:我停止事件,你也停了吧!
父控件会在该方法做一些停止的操作,比如回弹、状态回调等。
以上,Down事件结束!开始 Move !
-
rv开始了又一轮的事件
-
继续来到(SimpleRefreshLayout) onStartNestedScroll
-
继续(SimpleRefreshLayout) 接受事件
9)终于到重点了!
RecyclerView开始dispatch嵌套滑动的距离,通过recyclerView源码可以知道,该距离会通过childHelper最终回调到NestedScrollParent的相关方法。
- 来了来了! (Rv) dispatchNestedPreScroll --> (simpleLayout) OnNestedPreScroll
父控件:儿阿,你终于把你要滑动的距离dy给我了!我计算下自己要滑动多少再告诉你!
最后父控件会把自己消耗的距离通过cousumed[ ]
回传给子view
-
Rv 自己消耗之后,把剩余的距离传给父控件
-
父控件接收到Rv消耗之后剩余的距离,并消耗掉剩余的距离。
13)Rv结束move
-
SimpleRefreshLayout 结束move
最后,RecyclerView在up事件 结束整个过程
-
Rv结束up
整个过程一目了然!有木有!
总结:
- Down事件为何走onStopNestedScroll?
这是因为 view需要停止上一次可能存在的滑动。最开始可能很难理解为什么会先走onStop,然后再走嵌套滑动流程,直到最终在view的触摸事件Down找到stopNestedScroll. - NestedScroll的流程
- 事件都是从recyclerView的 startNestedScroll开始(除了up事件)。嵌套滑动始于recyclerView(
startNestedScroll
),也终于recyclerView(stopNestedScroll
)。
通过上面的流程可以看到,down和move事件都是从rv的startNestedScroll
开始的,因为rv是事件源,并且在整个过程中rv始终获取着事件的处理权,只是将消耗的距离传递给了父控件,一起处理这部分距离而已。
2)核心流程
1>(recyclerView)dispatchNestedPreScroll --> (父) onNestedPreScroll
在这个过程中,父类拿到了子view传递过来的距离,到底自己要消耗多少呢可以自己考虑,并且移动自己想要的距离,最后将这部分距离通过consumed[1] = yConsumed
回传给子view,子view获取的consumed[1]
就是父控件消耗的距离。
2> (父) onNestedPreScroll --> (recyclerView) dispatchNestedScroll --> (父) onNestedScroll
前面父控件已经消耗了一部分距离(称为预消耗), 回传给子view后子view也消耗了一部分,最后剩余的子view不想再消耗的距离将通过dispatchNestedScroll
传到父控件onNestedScroll
,这是告诉父控件: 我滑不动了,还是你来吧!
即先父控件预消耗,然后子view消耗,,最后父控件对剩余进行消耗。
3> 刷新、加载的思路
通过上面的分析,我们有了清晰的思路:
当recyclerView滑动到顶部时,rv将距离传递给父控件,此时我们让父控件去消耗这部分距离,使其显示 刷新的布局就好了! 加载也一样!
SimpleRefreshLayout
github上一直都有很多很优秀的刷新框架,像TwinklingRefreshLayout
、CanRefreshLayout
等等。而将刷新和加载封装到adapter里面,个人感觉实用性扩展性都没那么强。况且google早就将SwipeRefreshLayout
作为例子,我们应该也可以模仿着打造一个自己的刷新加载框架。
说说主要流程。
- header、footer、bottom以及滑动view的获取以及布局处理
swipeRefreshLayout
给出了很好的示范,我们只关心滑动view;因此对滑动view获取:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mTarget == null) {
ensureTarget();
}
//省略部分代码...
}
private void ensureTarget() {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child != mHeaderView && child != mFooterView && child != mBottomView) {
//获取到我们的滑动view mTarget
mTarget = child;
break;
}
}
}
很显然,获取到四个布局之后,header放在viewgroup顶部,footer和bottomView放在底部,mTarget在中间。布局的代码在onLayout()中。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child == mHeaderView) {
child.layout(0, -child.getMeasuredHeight(), child.getMeasuredWidth(), 0);
} else if (child == mFooterView) {
child.layout(0, getMeasuredHeight(), child.getMeasuredWidth(), getMeasuredHeight() + child.getMeasuredHeight());
} else if (child == mBottomView) {
child.layout(0, getMeasuredHeight(), child.getMeasuredWidth(), getMeasuredHeight() + child.getMeasuredHeight());
} else {
child.layout(getPaddingLeft(), getPaddingTop(), getPaddingLeft() + child.getMeasuredWidth(), getMeasuredHeight() - getPaddingBottom());
}
}
}
- 嵌套滑动过程
通过上面的分析,嵌套滑动的消耗开始于simpleRefreshLayout的onNestedPreScroll
, 然后到recyclerView消耗,最后到simpleRefreshLayout的onNestedScroll
。
这里的做法是先在onNestedScroll
让simpleRefreshLayout滑动, 下一次距离到来时就会在onNestedPreScroll
进行预消耗。
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
if (enable) {
if (!isLastScrollComplete) return;
if (direction == SCROLL_DOWN && !pullUpEnable) return; //用户不开启加载
if (direction == SCROLL_UP && !pullDownEnable) return; //用户不开启下拉
doScroll(dyUnconsumed);
}
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
if (getScrollY() != 0) { //只有在自己滑动的情形下才进行预消耗
//省略部分代码...
int yConsumed = Math.abs(dy) >= Math.abs(getScrollY()) ? getScrollY() : dy;
doScroll(yConsumed);
consumed[1] = yConsumed;
}
}
为什么可以这么做呢?
因为recyclerView滑动到顶部,继续scroll up的距离它肯定是无法滑动的(也就是它无法消耗这部分距离),因此会先走到onNestedPreScroll
,而此时simpleRefreshLayout的getScrollY() = 0
,无法进行预消耗;距离再次传递给recyclerView,它也无法消耗;最终就会来到onNestedScroll
,doScroll()
方法会使simpleRefreshLayout滑动。
之后getScrollY()
不再为0了, 也就进入了recyclerView不滑动,父控件滑动的环节。之后距离就会走完整的 onNestedPreScroll
--> recyclerView --> onNestedScroll
过程。 从而显示出隐藏在顶部的刷新布局。
当然,我们也可以在onNestedPreScroll
直接去判断recyclerView是否到顶部,是则开始滑动我们的父控件。
- 其他的一些细节
包括刷新、加载状态的回调,以及控制滑动回弹结束再开始下一次滑动等。
比较有意思的是,在上拉加载的过程中,我们希望下一页如果有数据,那么recyclerView能够向上滚动一小段距离,以便让用户们可以感知得到下一页是还有数据的。
这里我加了一个Handler,当adapter刷新完数据后,让recyclerView向上滚动一点位移。
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case MSG_PULL_UP:
pullCount++;
if (canChildScrollDown()) {
//如果上拉滚动结束,此时去判断recyclerView是否可滚动(是则说明有下一页), 位移一段距离
pullCount = 0;
mHandler.removeMessages(MSG_PULL_UP);
mTarget.scrollBy(0, (int) (getResources().getDisplayMetrics().density * 6));
} else {
//省略部分代码...
}
break;
}
Handler也是无奈之举。主要是不清楚怎么去获取adapter已经刷新完毕。
如果大家知道,请评论留言,非常感谢!
结尾!我已经尽力说清楚了,感谢你的耐心!
综上~
SimpleRefreshLayout封装了常用的刷新和加载,并增加了没有更多
的功能,想以后我们也能做个美美的列表效果,带有刷新和加载,到达底部还有底部布局,美美哒!
实现效果:
github地址:
https://github.com/dengzq/SimpleRefreshLayout
转载请注明出处哦~