上一篇文章我们介绍了NestedScrollingParent和NestedScrollingChild接口,了解了两个接口里的方法和相互之间的调用关系。这篇我们以NestedScrollView
类为例,看先嵌套滚动Parent和Child之前具体是怎么实现的。为啥用NestedScrollView
呢,因为这既是一个NestedScrollingParent又是一个NestedScrollingChild,了解了整个类后就了解了整个机制了。
这里就不全部贴出了源码,后面我们一边讲解一边贴出源码。
一、类简介
还是老规矩,我们先看下Google对这个类的介绍
NestedScrollView is just like
ScrollView
, but it supports acting as both a nested scrolling parent and child on both new and old versions of Android. Nested scrolling is enabled by default.
就是说NestedScrollView
和ScrollView
类似,是一个支持滚动的控件。此外,它还同时支持作为NestedScrollingParent
或者NestedScrollingChild
进行嵌套滚动操作。默认是启用嵌套滚动的。
再看下继承关系
public class NestedScrollView extends FrameLayout implements NestedScrollingParent,
NestedScrollingChild2, ScrollingView {}
可以看到该类继承自FrameLayout
,实现了NestedScrollingParent
、NestedScrollingChild
和
ScrollingView
接口。所以才具有上诉的特性咯。
另外这里有个一个NestedScrollingChild2
,在上篇文章已经提到了,这个其实核心和NestedScrollingChild
是一样的,只是在部分方法上面多了一个type字段用于判断而已。基本上就可以直接看成NestedScrollingChild
接口。
另外这里说明一下,因为这里重点是研究嵌套机制的,所以并不是所有的源码都有涉及,只介绍与嵌套相关的
二、嵌套滚动流程分析
1、总流程介绍
总的来说Parent和Child之间的相互调用遵循下面的调用关系:
2、具体分析
NestedScrollView是一个FrameLayout也就是一个ViewGroup,根据Android的触摸事件分发机制,一般会进入到onInterceptTouchEvent(MotionEvent ev)
进行拦截判断。所以我们也就从这里作为分析的入口。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
/*
* This method JUST determines whether we want to intercept the motion.
* If we return true, onMotionEvent will be called and we do the actual
* scrolling there.
*/
/*
* Shortcut the most recurring case: the user is in the dragging
* state and he is moving his finger. We want to intercept this
* motion.
*/
final int action = ev.getAction();
//mIsBeingDragged标识当前View是否在移动 这里的意思在移动或者移动事件都进行拦截
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
/*
* mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
* whether the user has moved far enough from his original down touch.
*/
/*
* Locally do absolute value. mLastMotionY is set to the y value
* of the down event.
*/
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + activePointerId
+ " in onInterceptTouchEvent");
break;
}
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
// 是滑动事件 并且是垂直方向
if (yDiff > mTouchSlop
&& (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {
// 进入这里说明是自己想处理的情况了 所以设置mIsBeingDragged 用于拦截事件
mIsBeingDragged = true;
mLastMotionY = y;
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
final ViewParent parent = getParent();
// 因为自己要处理 所以叫Parent不要拦截
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
final int y = (int) ev.getY();
// 判断是否是在子控件区域
if (!inChild((int) ev.getX(), y)) {
mIsBeingDragged = false;
recycleVelocityTracker();
break;
}
/*
* Remember location of down touch.
* ACTION_DOWN always refers to pointer index 0.
*/
// 记录按下的位置
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
/*
* If being flinged and user touches the screen, initiate drag;
* otherwise don't. mScroller.isFinished should be false when
* being flinged. We need to call computeScrollOffset() first so that
* isFinished() is correct.
*/
// 如果是在惯性滑动中的点击 交给自己处理
mScroller.computeScrollOffset();
mIsBeingDragged = !mScroller.isFinished();
// 这里就是开始嵌套滑动的地方了
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
/* Release the drag */
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) {
ViewCompat.postInvalidateOnAnimation(this);
}
// 停止嵌套滑动((前提是要作为NestedScrollingChild))
stopNestedScroll(ViewCompat.TYPE_TOUCH);
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
/*
* The only time we want to intercept motion events is if we are in the
* drag mode.
*/
return mIsBeingDragged;
}
这个方法的代码量不大,部分说明已经写在注释里了,这里总结起来主要就是做了一下几件事情:
在ACTION_DOWN中:一个是判断是否需要拦截事件,二是在合适的时候调用startNestedScroll();
方法
在ACTION_MOVE中:在需要处理的情况下,将mIsBeingDragged
置为true,将事件传递给自己的onTouchEvent()
方法进行处理。
在ACTION_UP或者ACTION_CANCEL中:将mIsBeingDragged
重置为false,然后调用stopNestedScroll()
停止嵌套滑动。
这里我们看下startNestedScroll();
和stopNestedScroll()
的实现。
@Override
public boolean startNestedScroll(int axes, int type) {
return mChildHelper.startNestedScroll(axes, type);
}
@Override
public void stopNestedScroll() {
mChildHelper.stopNestedScroll();
}
可以看到,就是简单代理给了ChildHelper进行处理,根据上篇文章的解析,我们知道这两个方法的调用,最终会进入到Parent对应的onStartNestedScroll(View child, View target, int nestedScrollAxes)
和onStopNestedScroll(View target)
方法。那我们就继续看下这两个方法的处理吧
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
// 只处理垂直滑动的情况
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onStopNestedScroll(View target) {
mParentHelper.onStopNestedScroll(target);
stopNestedScroll();
}
onStartNestedScroll()
方法中,就是判断是否是垂直滑动,是的话,返回true,表示进行处理。
onStopNestedScroll(View target)
方法,代理给ParentHelper,然后继续调用stopNestedScroll();
继续通知它的NestedScrollingParent(如果有的情况)停止嵌套滑动。
PS:这里有个主意的地方哦,有些人看到这里可能会觉得这不是形成死循环了么
stopNestedScroll()->mChildHelper.stopNestedScroll()->onStopNestedScroll()->stopNestedScroll()
,形成一个闭环了。有这个误解的朋友是因为没有区分NestedScrollingParent和NestedSrollingChild身份。当我们调用
stopNestedScroll()
方法的时候,当前的NestedScrollView是必须是具有Child的有效的身份,如果是Parent这个方法没有意义的,相当于是一个空方法。对应的
onStopNestedScroll(View target)
方法,是作为Parent有效身份的时候,才会被回调。对于Child身份也是没有意义的。所以其实从stopNestedScroll()
到stopNestedScroll()
已经从一个对象(Child)到另一个对象了(Parent)的传递,不是在同一个对象里的调用,所以是不会死循环的。后面不会再做说明,记得所有重写的NestedScrollingChild接口的方法,只有在Child身份的对象上有效,所有重写的NestedScrollingParent接口的方法,只有在Parent身份的对象上有效。
到这里,startNestedScroll和stopNestedScroll这两组流程分析就完了。那中间真正的分发流程在哪儿呢?那就是onTouchEvent()
方法啊,在上面onInterceptTouchEvent(MotionEvent ev)
方法里面,适当的时候,不是做了拦截操作么,那就会进入onTouchEvent()
方法咯,而且如果该类的子View没有消费掉触摸事件,正常情况也会再分发到该类的onTouchEvent()
方法。
这里我们就继续这个方法的分析吧
@Override
public boolean onTouchEvent(MotionEvent ev) {
initVelocityTrackerIfNotExists();
MotionEvent vtev = MotionEvent.obtain(ev);
final int actionMasked = ev.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
mNestedYOffset = 0;
}
vtev.offsetLocation(0, mNestedYOffset);
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
if (getChildCount() == 0) {
// 没有Child,那还滑动个啥啊,都撑不开布局
return false;
}
if ((mIsBeingDragged = !mScroller.isFinished())) {
final ViewParent parent = getParent();
// 通知父View不要拦截触摸事件
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
/*
* If being flinged and user touches, stop the fling. isFinished
* will be false if being flinged.
*/
// 如果是在惯性滑动中 停止滑动
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
// Remember where the motion event started
// 记录按下的位置
mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
// 也是开启嵌套滑动
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
break;
}
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
break;
}
final int y = (int) ev.getY(activePointerIndex);
// 手指滑动距离
int deltaY = mLastMotionY - y;
// 先分发给Parent进行预处理
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
ViewCompat.TYPE_TOUCH)) {
// 如果Parent消费了滑动距离 需要减去
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
final ViewParent parent = getParent();
// 也是告知父View不要拦截事件
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
if (mIsBeingDragged) {
// Scroll to follow the motion event
mLastMotionY = y - mScrollOffset[1];
final int oldY = getScrollY();
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS
|| (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
// Calling overScrollByCompat will call onOverScrolled, which
// calls onScrollChanged if applicable.
// 滚动自己
if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
// 重新计算未消费的距离
final int scrolledDeltaY = getScrollY() - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
// 分发给Parent进行嵌套滚动
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
ViewCompat.TYPE_TOUCH)) {
mLastMotionY -= mScrollOffset[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
} else if (canOverscroll) {
// 如果Parent没有消费 并且可以滚动 继续处理
ensureGlows();
final int pulledToY = oldY + deltaY;
if (pulledToY < 0) {
EdgeEffectCompat.onPull(mEdgeGlowTop, (float) deltaY / getHeight(),
ev.getX(activePointerIndex) / getWidth());
if (!mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onRelease();
}
} else if (pulledToY > range) {
EdgeEffectCompat.onPull(mEdgeGlowBottom, (float) deltaY / getHeight(),
1.f - ev.getX(activePointerIndex)
/ getWidth());
if (!mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onRelease();
}
}
if (mEdgeGlowTop != null
&& (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
}
break;
case MotionEvent.ACTION_UP:
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
// 如果达到了惯性的速度 分发惯性滑动事件
flingWithNestedDispatch(-initialVelocity);
} else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
getScrollRange())) {
ViewCompat.postInvalidateOnAnimation(this);
}
mActivePointerId = INVALID_POINTER;
// 这个方法里面会调用stopNestedScroll(ViewCompat.TYPE_TOUCH);
endDrag();
break;
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged && getChildCount() > 0) {
if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
getScrollRange())) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
mActivePointerId = INVALID_POINTER;
// 这个方法里面会调用stopNestedScroll(ViewCompat.TYPE_TOUCH);
endDrag();
break;
case MotionEvent.ACTION_POINTER_DOWN: {
final int index = ev.getActionIndex();
mLastMotionY = (int) ev.getY(index);
mActivePointerId = ev.getPointerId(index);
break;
}
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
break;
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
// 这里始终返回true 所以Parent作为父View其实是进不了onTouchEvent()方法的。
return true;
}
这个方法比较长,里面涉及到一些具体滚动操作,这不是我们本篇的重点,所以我们看关键地方就可以了。重要地方我都写了注释说明。
先看返回值,这里直接返回了true,从这里知道,Parent和Child嵌套的时候,Parent是肯定进不了该方法的。所以这里面的情况,我们只需要考虑Child身份即可。
ACTION_DOWN中:
如果是惯性滑动的情况,停止滑动。同样是调用startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
开启嵌套滑动,所以Parent中的onStartNestedScroll()
可能不止一次调用。但是多次调用有影响吗?没影响,在里面没有做具体滚动操作,只是做是否需要处理的判断而已。
ACTION_MOVE中:
先通过调用dispatchNestedPreScroll()
分发给Parent进行滚动处理。然后再通过overScrollByCompat()
自己处理滚动事件,最后再计算一下未消费的距离,再通过dispatchNestedScroll()
继续给Parent进行处理。同时根据返回值,判断Parent是否处理了,进行下一步操作。
这里的dispatchNestedPreScroll()
,就会进入Parent的onNestedPreScroll()
的方法,我们看下处理:
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
dispatchNestedPreScroll(dx, dy, consumed, null);
}
可以看到其实啥也没做,就是继续给它自己的Parent(如果有的情况)分发事件
接下来再看下dispatchNestedScroll()
对应的Parent的onNestedScroll()
方法
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed) {
final int oldScrollY = getScrollY();
// 滚动自己 消费掉距离
scrollBy(0, dyUnconsumed);
final int myConsumed = getScrollY() - oldScrollY;
final int myUnconsumed = dyUnconsumed - myConsumed;
// 继续分发给上一级
dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
}
这里面也比较简单,就是使用scrollBy()
方法,滚动自己。消费掉滚动距离。同样在自己还有Parent的情况下,继续向上分发。
到这里,ACTION_DOWN的情况,我们就介绍完了,继续看UP和CANCEL
ACTION_UP和ACTION_CANCEL中:
在这两个case中,最后都调用了endDrag()。我们看下这个方法
private void endDrag() {
mIsBeingDragged = false;
recycleVelocityTracker();
// 停止嵌套滚动
stopNestedScroll(ViewCompat.TYPE_TOUCH);
if (mEdgeGlowTop != null) {
mEdgeGlowTop.onRelease();
mEdgeGlowBottom.onRelease();
}
}
这里面就是调用stopNestedScroll(ViewCompat.TYPE_TOUCH);
,这个方法前面已经分析了,这里不做多的说明。
回到ACTION_UP中。这里面在调用endDrag()
之前,还调用了flingWithNestedDispatch()
方法,看下具体实现:
private void flingWithNestedDispatch(int velocityY) {
final int scrollY = getScrollY();
final boolean canFling = (scrollY > 0 || velocityY > 0)
&& (scrollY < getScrollRange() || velocityY < 0);
// 先给Parent看是否需要处理
if (!dispatchNestedPreFling(0, velocityY)) {
// 再次回调Parent,其实主要目的通过canFling参数,是告诉Parent我自己处理了
dispatchNestedFling(0, velocityY, canFling);
// 没有处理 自己处理
fling(velocityY);
}
}
这里面就是继续惯性滑动事件的分发而已,注释说的很清楚了,就不解释了。那我们就继续看Parent中对应的两个方法:onNestedPreScroll()和onNestedFling()
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
if (!consumed) {
flingWithNestedDispatch((int) velocityY);
return true;
}
return false;
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return dispatchNestedPreFling(velocityX, velocityY);
}
处理很简单,onNestedPreFling()
直接往上一级Parent分发,onNestedFling()
直接,看Child是否消费了,没有消费往上一级Parent分发。并返回true,如果已经消费了,直接返回fasle即可。
到这里,整个流程就分析完了,还是做一个简单的总结吧。
3、总结
大家再通过这张流程图(画的不好,将就看了)自己回忆和梳理一下吧。