NestedScrollingParent
因为内控件是发起者, 所以外控件的大部分方法都是被内控件的对应方法回调的.
onStartNestedScroll : 对应startNestedScroll, 内控件通过调用外控件的这个方法来确定外控件是否接收滑动信息.
onNestedScrollAccepted : 当外控件确定接收滑动信息后该方法被回调, 可以让外控件针对嵌套滑动做一些前期工作.
onNestedPreScroll : 关键方法, 接收内控件处理滑动前的滑动距离信息, 在这里外控件可以优先响应滑动操作, 消耗部分或者全部滑动距离.
onNestedScroll : 关键方法, 接收内控件处理完滑动后的滑动距离信息, 在这里外控件可以选择是否处理剩余的滑动距离.
onStopNestedScroll : 对应stopNestedScroll, 用来做一些收尾工作.
getNestedScrollAxes : 返回嵌套滑动的方向, 区分横向滑动和竖向滑动, 作用不大 onNestedPreFling和onNestedFling : 同上略
外控件通过onNestedPreScroll和onNestedScroll来接收内控件响应滑动前后的滑动距离信息.
再次指出, 这两个方法是实现嵌套滑动效果的关键方法.
3
从NestedScrollView看嵌套机制
说完上面一大通, 终于可以开始分析源码来了解嵌套滑动机制起作用的具体逻辑了.
NestedScrollView简单地说就是支持嵌套滑动的ScrollView, 内部逻辑简单, 而且它既可以是内控件, 也可以是外控件, 所以选择分析它来了解嵌套滑动机制.
注意 : 因为NestedScrollingChildHelper和NestedScrollingParent这两个辅助类的实现跟View和ViewGroup中的对应方法是一样的, 而且View和ViewGroup的源码没有使用兼容类, 所以下面分析相关方法的时候源码都使用View和ViewGroup中的代码.
上面已经说了嵌套滑动是从startNestedScroll开始, 所以先看看哪里调用了这个方法, 在源码里一搜就能知道有两个地方调用了这个方法.
onInterceptTouchEvent中ACTION_DOWN的情况
onTouchEvent中ACTION_DOWN的情况
因为ACTION_DOWN是滑动操作的开始事件, 所以当接收到这个事件的时候尝试找对应的外控件. 只有找到了外控件才有后续的嵌套滑动的逻辑发生.
关于NestedScrollView在这里的实现其实有个奇怪的地方, 提出一个问题, 不感兴趣的可以直接跳过这段.
既然内控件是发起者, 为什么要在onInterceptTouchEvent也调用startNestedScroll呢?
因为事件传递的时候会先执行外控件的onInterceptTouchEvent, 也就是说第一个执行startNestedScroll的是最外层的NestedScrollView, 即使它找到了对应的外控件后续如果有子控件消费了这个事件, 也就是说不执行onTouchEvent方法, 那么找到外控件也没用的, 不清楚设计者的意图.
接着我们看startNestedScroll是如何找对应的外控件的, 因为NestedScrollView#startNestedScroll调用了辅助方法的startNestedScroll, 所以下面直接贴View#startNestedScroll.
// View.javapublic
boolean startNestedScroll(int axes) {
// …
if(isNestedScrollingEnabled()) {
ViewParent p = getParent();
View child = this;
while(p != null) {
try{
// 关键代码
if(p.onStartNestedScroll(child, this, axes)) {
mNestedScrollingParent = p;
p.onNestedScrollAccepted(child, this, axes);
returntrue;
}
} catch(AbstractMethodError e) {
// …
}
if(p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
returnfalse;
}
非常简单的逻辑遍历父控件, 调用父控件的onStartNestedScroll, 返回true表示找到了对应的外控件, 找到外控件后马上调用onNestedScrollAccepted
从这里可以知道
外控件不一定是内控件的直接父控件, 但一定是最近的符合条件的外控件.
还可以确定了上面关于onStartNestedScroll的方法说明, 返回true表示接收内控件的滑动信息.对于NestedScrollView#onStartNestedScroll内部逻辑很简单, 只要是竖直滑动方向就返回true, 所以可以知道
NestedScrollView不支持横向嵌套滑动.
接着被调用的是onNestedScrollAccepted, 看NestedScrollView#onNestedScrollAccepted
// NestedScrollView.java
@OverridepublicvoidonNestedScrollAccepted(View child, View target, intnestedScrollAxes){
mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
}
辅助类的方法很简单, 就是记录当前的滑动方向, 在这里NestedScrollView又调用startNestedScroll来找它自己的外控件, 这是为了连续嵌套NestedScrollView, 不过这是NestedScrollView自己的实现, 不管它.
找到了外控件后ACTION_DOWN事件就没嵌套滑动的事了, 要滑动肯定会在onTouchEvent中处理ACTION_MOVE事件, 接着我们看ACTION_MOVE事件是怎样处理的.
// NestedScrollView#onTouchEvent
caseMotionEvent.ACTION_MOVE:
// …
finalinty = ( int) MotionEventCompat.getY(ev, activePointerIndex);
intdeltaY = mLastMotionY – y;
// 让外控件先处理滑动距离
if(dispatchNestedPreScroll( 0, deltaY, mScrollConsumed, mScrollOffset)) {
deltaY -= mScrollConsumed[ 1]; // 消耗滑动距离
// …
}
// …
if(mIsBeingDragged) {
// …
// 内控件处理滑动距离
if(overScrollByCompat( 0, deltaY, 0, getScrollY(), 0, range, 0,
0, true) && !hasNestedScrollingParent()) {
// …
}
finalintscrolledDeltaY = getScrollY() – oldY;
finalintunconsumedY = deltaY – scrolledDeltaY;
if(dispatchNestedScroll( 0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
// …
}
// …
}
break;
这部分是NestedScrollView能够处理嵌套滑动的关键代码了, 其他能够嵌套滑动的控件也应该在ACTION_MOVE中类似地处理滑动距离.
先计算出本次滑动距离deltaY, 这里有个小细节
deltaY等于上一次的Y坐标减去这次的Y坐标, 这意味着在相关方法中接收到的滑动距离参数中, 滑动距离 > 0表示手指向下滑动, 反之表示手指向上滑动. 这是因为在屏幕中Y轴正方向是向下的.
得到滑动距离deltaY后, 先把它传给dispatchNestedPreScroll, 然后在结果返回true的时候, delta会减去mScrollConsumed[1].
接着看dispatchNestedPreScroll干了什么
// View.java
publicboolean dispatchNestedPreScroll(int dx, int dy,
@Nullable@Size(2)int[] consumed, @Nullable@Size(2)int[] offsetInWindow) {
// … 忽略状态判断
consumed[ 0] = 0;
consumed[ 1] = 0;
mNestedScrollingParent.onNestedPreScroll( this, dx, dy, consumed);
returnconsumed[ 0] != 0|| consumed[ 1] != 0;
// 其他情况返回false
}
忽略条件判断和offsetInWindow的相关处理, 先指出consumed就是上一步分析中的mScrollConsumed, dy就是deltaY.
因为dispatchNestedPreScroll的工作就是把滑动距离在内控件处理前分发给外控件, 所以这里的关键代码也很简单, 就是直接把相关的参数传给外控件的onNestedPreScroll, 然后只要外控件消耗了滑动距离(不论横向还是竖向), 就会返回true
所以
外控件如果想在内控件之前消耗滑动距离仅需要在onNestedPreScroll把消耗的值放到数组中返回给内控件.
onNestedPreScroll是决定外控件的嵌套滑动逻辑的关键方法,在不同的控件中应该是根据需要有不同的实现的, 而在NestedScrollView中就是直接询问它自己的外控件是否消耗滑动距离, 实现比较简单就不贴代码了.
在这里提醒下, 在我们自己修改嵌套滑动逻辑的时候需要注意滑动距离的正负号和内控件处理consumed数组的方式. 不过这些都是些数字游戏, 不细说了.
好了, 现在外控件已经比内控件先处理了滑动距离了, 如果外控件没有完全消耗掉所有滑动距离, 这时该内控件处理剩下的滑动距离了, 不同的控件有不同的滑动实现, 在NestedScrollView中通过NestedScrollView#overScrollByCompat来进行滑动, 并且滑动结束后通过比对滑动前后的scrollY值得到了内控件消耗的滑动距离, 然后得到剩下的滑动距离, 最后传给dispatchNestedScroll.
dispatchNestedScroll的逻辑跟dispatchNestedPreScroll几乎一样, 区别是它调用了外控件的onNestedScroll, 因为到这里已经是处理滑动距离最后的机会了, 所以onNestedScroll不会再影响内控件的处理逻辑了.
到这里ACTION_MOVE事件就分析完毕了.
最后就是stopNestedScroll了, 代码就不贴了, 调用这个方法基本是新的滑动操作开始前, 或者滑动操作结束/取消, 代码逻辑就是进行一些变量的重置工作和调用onStopNestedScroll, 而onStopNestedScroll也类似.
整个嵌套滑动的基本逻辑就是这样. 注意这里虽然分析的是NestedScrollView, 但这代表了嵌套滑动的”约定”处理方式, 虽然不同的控件实际的实现会有不同不过应该遵循基本方法的调用顺序, 确保参数的含义和参数的处理方式.
总结
如果要支持嵌套滑动, 内控件和外控件要支持对应的方法, 为了兼容低版本一般通过实现NestedScrollingChild和NestedScrollingParent接口以及使用NestedScrollingChildHelper和NestedScrollingParent辅助类.
具体嵌套滑动逻辑主要是在onNestedPreScroll和onNestedScroll方法中.
父控件通过给数组赋值来把消耗的滑动距离传递给内控件.
当你希望滑动内部列表的时候先把列表顶部的控件隐藏掉, 例如ActionBar, 这时候嵌套滑动就大有用处了, 具体的应用场景可以看看Android 嵌套滑动机制(NestedScrolling)
https://segmentfault.com/a/73657的实现效果.
感觉这篇说得有些零碎, 如果有改进的建议欢迎在讨论区指出. :D返回搜狐,查看更多
发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/228771.html原文链接:https://javaforall.net
