虽然很早之前使用CoordinatorLayout
时就认识过nestedScrollingChild
和nestedScrollingParent
, 也看多很多博客,但每次看着就不知所云了,所以这篇文章,我们就以问题为线索,带着问题找答案。
1. 谁实现 NestedScrollingChild,谁实现NestedScrollingParent ?
在实际项目中,我们往往会遇到这样一种需求,当ViewA
还显示的时候,往上滑动到viewA
不可见时,才开始滑动viewB
, 又或者向下滑动到viewB
不能滑动时,才开始向上滑动viewC
. 如果列表滑动、上拉加载和下拉刷新的view
都封装成一个组件的话,那滑动逻辑就是刚刚这样。而这其中列表就要实现nestedScrollingChild
, 最外层的Container
实现nestedScrollingParent
. 如果最外层的Container
希望在其它布局中仍然能够将滑动事件继续往上冒泡,那么container
在实现nestedScrollingParent
的同时也要实现nestedScrollingChild
。 如下示意图所示。
所以这个问题的答案:
触发滑动的组件或者接受到滑动事件且需要继续往上传递的是nestedScrollingChild
.
是nestedScrollingChild
的父布局,且需要消费传递的滑动事件就是nestedScrollingParent
.
我们今天的最后也会给出如何利用nestedScrollingChild
和nestedScrollingParent
来自定义一个集上拉加载和下拉刷新的组件。
2. 滑动事件如何在二者之间传递和消费的?
2.1 你能一眼认出这是child
还是parent
的api
吗?
首先呢,我们要看一下nestedScrollingChild
和nestedScrollingParent
有哪些api
.
public interface NestedScrollingChild {
public void setNestedScrollingEnabled(boolean enabled);
public boolean isNestedScrollingEnabled();
public boolean startNestedScroll(int axes);
public void stopNestedScroll();
public boolean hasNestedScrollingParent();
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
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 int getNestedScrollAxes();
}
这里呢,我删掉了注释,我不想翻译那些注释放在上面的代码中,这样你就会将注意力放在我的注释中,然后陷入了咬文嚼字,最后感叹为什么每个字我都认识,连在了一起我怎么就看不懂的自我否定中。 之所以把上面的代码放出来,是为了我后面简述时,你不用一边看文章,一边还要切过去看源码看这个api
是属于child
还是属于parent
的。
其实这里给你一个分辨是child
和parent
的api
的一个小诀窍,因为child
是产生滑动的造势者,所以它的api
都是以直接的动词开头,而parent
的滑动响应是child
通知parent
的,所以都是以监听on
开头,这样就记住了。
parent
----> onXXXX()
child
-----> verbXXXX()
嗯,废话了好像很多了,这里我们要回到问题上,滑动事件如何在child
和parent
之间传递和消费掉的呢?
2.2 滑动事件的是如何传递的
那既然能传递,说明这个滑动事件一定产生了,如何产生滑动事件?当然是用户手指在屏幕上滑动了呀。为了不说的这么枯燥,我们拿最熟悉熟悉的小伙伴RecyclerView
来作为nestedScrollingChild
讲解。这里我引入的版本是:25.3.1
implementation 'com.android.support:recyclerview-v7:25.3.1'
2.2.1 滑动事件传递从哪里产生?
switch (action) {
case MotionEvent.ACTION_DOWN:
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis);
}
break;
}
这里,我们可以发现,当我的小手按在RecyclerView
上时,调用了nestedScrollingChild
的startNestedScroll(nestedScrollAxis)
, 这里我们再多让我们的大脑接受一点信息,那就是这个方法的参数:nestedScrollAxis
, 滑动的坐标轴。 RecylerView
是不是既可以水平滑动,又可以纵向滑动,那这里就是传递的就是RecyclerView
可以滑动的坐标轴。
发现了startNestedScroll(axis)
,看看走到了哪里。
@Override
public boolean startNestedScroll(int axes) {
return getScrollingChildHelper().startNestedScroll(axes);
}
这里,我们发现又出来一个类:NestedScrollingChildHelper
. 里面好像又有一些滑动的api
。读到这里,不要怕,心态要稳住,不要崩塌了。给自己吃颗定心丸,我能行。
我们先看一眼,这个childHelper
的这个方法干了啥?
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
mNestedScrollingParent = p;
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
所有的绝妙之处就在这个方法中,这个方法我是原封不动拷贝下来,听我和你一句一句讲解。
第一句: 判断 mNestedScrollingParent
是不是 null
。 在NestedScrollingChildHelper
这个类,全类只有两处给它赋值了,一个赋有值,就是上面代码中的while
循环里面,一个是赋空值,在方法stopNestedScroll
,这个方法什么时候调用啊,在你美丽的小手离开屏幕的时候。所以只要你的小手在屏幕上,这个startedNestedScroll
这个方法只会调用一次。也就是通知parent
我美丽的小手指要滑动啦,通知过你,我就不通知了,哪个小仙女不是傲娇的。
第二句: 判断mIsNestedScrollingEnabled
是否要true
. 这个变量也是至关重要的,它的作用是 要不要向上冒泡滑动事件,所以说哪天小仙女不开心了,直接调用了:setNestedScrollingEnabled(false)
, 父布局是怎么都不知道小手指有没有滑动的。
第三句+第四句:这里的p
就是父布局了,这里的mView
是在初始化这个类的时候,传递过来的,所以在RecyclerView
中,可以找到这句话:mScrollingChildHelper = new NestedScrollingChildHelper(this);
. 这里的mView
就是RecyclerView
这位小仙女啦。
第五句:进入while
循环了,为什么这里要while
循环,因为它要确保使命必达,不管我的父布局有多深,我都要找到你,并通知到你。
第六句:if
里的逻辑说明,如果parent
监听到即将要在这个轴上有滑动事件,并且正是parent
需要的事件,那么就会调用onNestedScrollAccept
。 这里的ViewParentCompat.onStartNestedScroll(p, child, mView, axes)
会最终调用到实现nestedScrollingParent
组件中的onStartNestedScroll
方法,这个方法就是parent
判断收到该滑动通知时,是不是天时地利人和,如果是,我就返回true
,后面一系列的小手指滑动都要告知我。如果返回false
,说明parent
此时在处理别的事情,后面小手指滑动的弧线再怎么优美,都不要来烦我。
第七句:onNestedScrollAccepted
说明parent
正式接收了此child
也就是recyclerView
的滑动通知,最终会调用到parent
的onNestedScrollAccept
方法中,如果此parent
还实现了接口nestedScrollingChild
, 可以在这个方法继续向parent
的parent
上报了。
所以整个流程可以概括为:通知
ACTION_DOWN
--> child.startNestedScroll
--> childHelper.startNestedScroll
--> parent.onStartNestedScroll
--> parent.onNestedScrollAccept
2.2.2 小手指滑动的时候,child
和parent
之间是如何通信的?
case MotionEvent.ACTION_MOVE: {
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
if (scrollByInternal(canScrollHorizontally ? dx : 0,canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
}
这里呢,有两个重要的方法:dispatchNestedPreScroll
和 scrollByInternal
.
第一句: dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
,这里的参数中只有dx
,dy
两个参数在前面赋值了,而后面两个参数在哪里操作的呢?这里我们留个问号?首先这个方法会走到NestedScrollingChildHelper
类中的方法:dispatchNestedPreScroll
调用ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);
}
最终目的地来到了parent
的onNestedPreScroll()
。所以我们可以大胆猜测,consumed
, offsetInwindow
, 是在parent
这里赋值的,当然你可以不用赋值,不赋值的话,值也就是保留上一次的值。
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
dispatchNestedPreScroll()
这个方法返回true
后,发现重新计算了dx
,dy
, 在方法scrollByInternal()
方法中,用的是最新的dx,dy
值。说明当小手指产生滑动位移的时候,先分发给parent
,让parent
先消耗,并在方法中将parent
消耗的位移传递过来,那么剩下的位移,ok,那充当child
的RecyclerView
内部消费了。
if (scrollByInternal(canScrollHorizontally ? dx : 0,canScrollVertically ? dy : 0,vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0, unconsumedY = 0;
int consumedX = 0, consumedY = 0;
if (mAdapter != null) {
if (y != 0) {
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
}
}
if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
// Update the last touch co-ords, taking any scroll offset into account
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
if (ev != null) {
ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
return consumedX != 0 || consumedY != 0;
}
第二句:scrollByInternal()
就是内部滑动消耗了,在这个方法里面,我们发现继续往parent
分发了事件:dispatchNestedScroll(consumeX, consumeY, unconsumeX, unconsumeY)
, 把自己未消耗的滑动位移继续移交给parent
,这个时候最终会走到parent
的方法:onNestedScroll()
。 在这里,如果parent
还实现了nestedScrollingChild
,可以将未消耗的滑动位移继续移交给自己的parent
.
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
if(isNestedScrollingEnabled()) {
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, mParentOffsetInWindow);
}
}
所以我们可以总结如下:通信
ACTION_MOVE
: 小手指滑动位移为:dy
--> childHelper.dispatchNestedPreScroll
(dy)
--> parent.onNestedPreScroll(dy)
, consumedY = parent.onNestedPreScroll(dy)
--> dy' = dy - consumeY
recyclerView.scrollByInternal(dy')
unconsumeY = dy' - recyclerView.scrollByInternal(dy')
--> parent.startNestedScroll(unconsumeY)
2.2.3 小手指滑累了,离开屏幕时,又有哪些事件传递?
case MotionEvent.ACTION_UP: {
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetTouch();
}
break;
public boolean fling(int velocityX, int velocityY) {
if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
dispatchNestedFling(velocityX, velocityY, canScroll);
if (canScroll) {
mViewFlinger.fling(velocityX, velocityY);
return true;
}
}
return false;
}
private void resetTouch() {
stopNestedScroll();
}
这里我们发现先是child
执行fling
方法,也就是当手松开时仍然有速度,那么会执行一段惯性滑动,而在这惯性滑动中, 这里就很奇妙了,先是通过dispatchNestedPreFling()
将滑动速度传递给parent
, 如果parent
不消耗的话,再次通过dispatchNestedFling
向parent
传递,只是这次的传递会带上child
自己是否有能力消费惯性滑动,最后不管parent
有没有消费,child
也就是recyclerview
都会执行自己的fling
.也就是:
mViewFlinger.fling(velocityX, velocityY);
走完了惯性滑动,就会走到stopNestedScroll()
. 按照上面的逻辑处理,我们应该可以猜到接下来的逻辑就是走到NestedScrollingChildHelper
这个类。然后目的地会到达parent
的onStopNestedScroll
方法。这里,parent
就可以处理当小手指离开屏幕时的一些逻辑了。这条路很简单,没有返回值,也没有传递什么变量。还是很好理解的。
public void stopNestedScroll() {
if (mNestedScrollingParent != null) {
ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);
mNestedScrollingParent = null;
}
}
这里呢,我们可以总结如下:收尾
ACTION_UP
--> childHelper.dispatchNestedPreFling
--> parent.onNestedPreFling
--> childHelper.dispatchNestedFling
--> parent.onNestedFling
--> child.fling
--> childHelper.stopNestedScroll
--> parent.onStopNestedScroll
这样,我们整个nestedScrollingChild
和nestedScrollingParent
之间的丝丝缕缕都讲解完了。
3. 实践
这里我们利用nestedScrollingChild
和nestedScrollingParent
实现的自定义上拉加载,下拉刷新的控件。
作者:雨打空城
链接:https://www.jianshu.com/p/8f412c6cb0ef
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
网友评论