温馨提示:Android事件分发机制是Android嵌套滑动机制的基石,阅读本文前请务必保证熟练掌握Android事件分发机制。传送门->事件分发四步曲之一《深度遍历讲解Android事件分发机制》
本文大纲
1. 嵌套ScrollView同向滑动
2. 传统事件分发和嵌套滑动事件分发
3. NestedScrollingChild&NestedScrollingParent
4. 结合案例讲解嵌套滑动事件分发顺序
1. 嵌套ScrollView同向滑动
所谓嵌套ScrollView同向滑动,是指两个可滑动的View内外嵌套,并且他们的方向是相同的。
当在内部ScrollView中上下滑动时,会有两种情况。
- 外部ScrollView优先获取上下滑动的权力,在蓝色区域上下滑动,内部ScrollView并不会上下滑动
内部的ScrollView优先获取上下滑动的权力,在蓝色区域内上下滑动,内部ScrollView上下滑动
在Android未提供嵌套滑动机制之前,嵌套ScrollView同向滑动的效果是情况一,内部ScrollView优先获得DOWN事件的处理机会,外部的ScrollView优先获得MOVE事件的处理机会(yDiff > mTouchSlop)。原因如下:
当外部ScrollView嵌套内部ScrollView时,DOWN事件在内部ScrollView的onTouchEvent中返回true,内部ScrollView处理了DOWN事件。MOVE事件首先到达外部ScrollView的onInterceptTouchEvent方法,当滑动距离大于mTouchSlop时,会拦截掉MOVE事件,给内部ScrollView发出CANCEL事件,从而外部ScrollView获得了事件的处理权,内部ScrollView失去了事件的处理权。
ScrollView#onTouchEvent方法返回true。根据传统事件分发机制,内部ScrollView优先获得DOWN事件的处理机会。
//ScrollView#onTouchEvent 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) { return false; } if ((mIsBeingDragged = !mScroller.isFinished())) { final ViewParent parent = getParent(); 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(); if (mFlingStrictSpan != null) { mFlingStrictSpan.finish(); mFlingStrictSpan = null; } } // Remember where the motion event started mLastMotionY = (int) ev.getY(); mActivePointerId = ev.getPointerId(0); startNestedScroll(SCROLL_AXIS_VERTICAL); break; } //此处省略其它事件的代码 } if (mVelocityTracker != null) { mVelocityTracker.addMovement(vtev); } vtev.recycle(); return true; }
关于MOVE事件,首先会调用外部ScrollView的onInterceptTouchEvent方法,如果返回false,MOVE事件会调用内部ScrollView的onTouchEvent方法,如果返回true,会调用外部ScrollView的onTouchEvent方法。当yDiff > mTouchSlop时,方法返回true。
public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction(); if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { return true; } if (super.onInterceptTouchEvent(ev)) { return true; } if (getScrollY() == 0 && !canScrollVertically(1)) { return false; } switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_MOVE: { 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() & SCROLL_AXIS_VERTICAL) == 0) { mIsBeingDragged = true; mLastMotionY = y; initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement(ev); mNestedYOffset = 0; if (mScrollStrictSpan == null) { mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll"); } final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } break; } //此处省略其它事件的代码 } return mIsBeingDragged; }
这种效果显然不能满足用户的需求。内部的ScrollView永远无法获得上下滑动的机会。用户希望在内部ScrollView的区域上下滑动时,内部ScrollView优先获得事件的分发权力。Android嵌套滑动机制,很好的解决了ScrollView嵌套时内部ScrollView无法优先处理滑动事件的问题。
2. 传统事件分发和嵌套滑动事件分发
传统事件分发在事件分发四步曲之一《深度遍历讲解Android事件分发机制》中已经详细讲解过,这里不做详细的讲解。事件会调用父控件的onInterceptTouchEvent方法,判断是否拦截事件。换言之传统的事件分发,生杀大权掌握在父控件上,只有父控件不拦截事件,子控件才有处理事件的机会。
传统事件分发树形图如下
而嵌套滑动事件分发则区别于传统事件分发,事件分发的生杀大权并不掌握在父控件上。当DOWN事件传递到NestedScrollingChild时,NestedScrollingChild控件会向上寻找愿意响应后代View发出嵌套滑动请求的NestedScrollingParent控件,告诉NestedScrollingParent控件,后续的MOVE事件,不要再拦截,事件优先交给NestedScrollingChild处理,如果MOVE滑动超过mTouchSlop阈值,NestedScrollingChild向上请求"不拦截事件"->"parent.requestDisallowInterceptTouchEvent(true)",从此NestedScrollingChild的所有父控件,对事件全部不拦截,直达NestedScrollingChild控件。
嵌套滑动事件分发树形图如下
总结:传统事件分发,父View会优先获取事件的分发权,并且如果事件被某个View处理了,那么它的父View无法获得事件分发的机会。嵌套滑动事件分发,NestedScrollingChild处理DOWN事件时,会告诉它的NestedScrollingParent,后续的MOVE、UP等事件,NestedScrollingParent不拦截,直接交由NestedScrollingChild处理,而且它的父View有机会再次获得事件分发的机会。
3. NestedScrollingChild&NestedScrollingParent
嵌套滑动机制提供了NestedScrollingChild和NestedScrollingParent接口。
定义控件节点如下
- NestedScrollingChild节点表示实现了NestedScrollingChild接口的View或者ViewGroup
- NestedScrollingParent节点表示实现了NestedScrollingParent接口的ViewGroup
- Both 表示两个接口都实现了的ViewGroup
NSC和NSP是成对出现的,一个NestedScrollingChild只能对应一个NestedScrollingParent,而一个NestedScrollingParent可以对应多个NestedScrollingChild。一个控件即可以同时实现NSC和NSP接口,它对于父的NSP控件而言是一个NSC可以主动发起嵌套滑动的请求,对于它的NSC控件而言是一个NSP,它可以响应后代控件发起的嵌套滑动请求。当NSC的滑动嵌套请求(startNestedScroll)被NSP响应了(onStartNestedScroll返回true)称NSC和NSP为pair(天生一对),NSC可能和最近的祖先NSP组成Pair,也可能与非最近的祖先NSP组成pair。下面我们分析几个Case。
- NestedScrollingParent和NestedScrollingChild是有效的pair
NestedScrollingParent和NestedScrollingChild是无效的pair
NestedScrollingParent2和NestedScrollingChild是有效的pair,NestedScrollingParent1和NestedScrollingChild也有可能是有效的pair(当NSP2不响应嵌套滑动,而NSP1响应 好好思考一下 下面的Case相同,不表)
NestedScrollingParent和Both是有效的pair,Both和NestedScrollingChild是有效的pair
NestedScrollingParent和NestedScrollingChild是有效的pair,尽管中间隔了一个普通的ViewGroup
NestedScrollingParent和NestedScrollingChild1是有效的pair,NestedScrollingParent和NestedScrollingChild2是有效的pair
NestedScrollingChild方法以及作用
- 控制嵌套滑动机制是否开启->NestedScrollingChild#setNestedScrollingEnable和NestedScrollingChild#isNestedScrollingEnable。
//View.java public void setNestedScrollingEnabled(boolean enabled) { if (enabled) { mPrivateFlags3 |= PFLAG3_NESTED_SCROLLING_ENABLED; } else { stopNestedScroll(); mPrivateFlags3 &= ~PFLAG3_NESTED_SCROLLING_ENABLED; } } public boolean isNestedScrollingEnabled() { return (mPrivateFlags3 & PFLAG3_NESTED_SCROLLING_ENABLED) == PFLAG3_NESTED_SCROLLING_ENABLED; }
通知NestedScrollingParent不拦截MOVE、UP等事件->NestedScrollingChild#startNestedScroll。
//View.java public boolean startNestedScroll(int axes) { //如果已经找到的NSP返回 if (hasNestedScrollingParent()) { // Already in progress return true; } //如果开启了嵌套滑动 if (isNestedScrollingEnabled()) { //往上查找NestedScrollingParent ViewParent p = getParent(); View child = this; while (p != null) { try { //如果NSP的onStartNestedScroll返回true,表示响应NSC的嵌套滑动 if (p.onStartNestedScroll(child, this, axes)) { //设置NSC的mNestedScrollingParent对象,下次不用再查找了 mNestedScrollingParent = p; p.onNestedScrollAccepted(child, this, axes); return true; } } catch (AbstractMethodError e) { Log.e(VIEW_LOG_TAG, "ViewParent " + p + " does not implement interface " + "method onStartNestedScroll", e); // Allow the search upward to continue } if (p instanceof View) { child = (View) p; } p = p.getParent(); } } //如果没找到返回false return false; }
如上图,假设NestedScrollingParent2的onStartNestedScroll返回false,NestedScrollingParent1的onStartNestedScroll返回true。当NestedScrollingChild调用startNestedScroll方法时。NestedScrollingParent1和NestedScrollingChild是有效的Pair
调用时机,以ScrollView为例子,ScrollView实现了NSC接口,一般在Down事件中调用,或者在NSP的onNestedScrollAccepted中
- 调用时机1 ScrollView#onInterceptTouchEvent
public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction(); if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { return true; } if (super.onInterceptTouchEvent(ev)) { return true; } if (getScrollY() == 0 && !canScrollVertically(1)) { return false; } switch (action & MotionEvent.ACTION_MASK) { //这里省略其它事件 case MotionEvent.ACTION_DOWN: { final int y = (int) ev.getY(); if (!inChild((int) ev.getX(), (int) y)) { mIsBeingDragged = false; recycleVelocityTracker(); break; } mLastMotionY = y; mActivePointerId = ev.getPointerId(0); initOrResetVelocityTracker(); mVelocityTracker.addMovement(ev); mScroller.computeScrollOffset(); mIsBeingDragged = !mScroller.isFinished(); if (mIsBeingDragged && mScrollStrictSpan == null) { mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll"); } //调用时机1 startNestedScroll(SCROLL_AXIS_VERTICAL); break; } } return mIsBeingDragged; }
调用时机2 ScrollView#onTouchEvent
public boolean onTouchEvent(MotionEvent ev) { initVelocityTrackerIfNotExists(); //省略代码 switch (actionMasked) { case MotionEvent.ACTION_DOWN: { if (getChildCount() == 0) { return false; } if ((mIsBeingDragged = !mScroller.isFinished())) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } if (!mScroller.isFinished()) { mScroller.abortAnimation(); if (mFlingStrictSpan != null) { mFlingStrictSpan.finish(); mFlingStrictSpan = null; } } // Remember where the motion event started mLastMotionY = (int) ev.getY(); mActivePointerId = ev.getPointerId(0); //调用时机2 startNestedScroll(SCROLL_AXIS_VERTICAL); break; } } //省略代码 return true; }
调用时机3 ScrollView#onNestedScrollAccepted
public void onNestedScrollAccepted(View child, View target, int axes) { super.onNestedScrollAccepted(child, target, axes); //调用时机3。NSP往上找它的NSP 对应Case4中的Both往上找NSP startNestedScroll(SCROLL_AXIS_VERTICAL); }
理滑动事件之前,将滑动事件的处理权交由NestedScrollingParent处理->(NestedScrollingChild#dispatchNestedPreScroll)。(这里叨唠一下,子View虽然优先获取了事件分发的权力,但是在滑动之前,还是会把事件交给父View去处理。传统事件分发,父View如果获取了分发权,子View就无法获取分发权。滑动嵌套即使子View获取了分发权,它还是会征询父View,是否要处理掉事件的一部分滑动,父View可以选择处理或者不处理,子View再处理事件剩余滑动距离)。
//View.java public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable @Size(2) int[] consumed, @Nullable @Size(2) int[] offsetInWindow) { if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { if (dx != 0 || dy != 0) { int startX = 0; int startY = 0; if (offsetInWindow != null) { getLocationInWindow(offsetInWindow); startX = offsetInWindow[0]; startY = offsetInWindow[1]; } if (consumed == null) { if (mTempNestedScrollConsumed == null) { mTempNestedScrollConsumed = new int[2]; } consumed = mTempNestedScrollConsumed; } consumed[0] = 0; consumed[1] = 0; mNestedScrollingParent.onNestedPreScroll(this, dx, dy, consumed); if (offsetInWindow != null) { getLocationInWindow(offsetInWindow); offsetInWindow[0] -= startX; offsetInWindow[1] -= startY; } return consumed[0] != 0 || consumed[1] != 0; } else if (offsetInWindow != null) { offsetInWindow[0] = 0; offsetInWindow[1] = 0; } } return false; }
调用时机,一般在NSC真正处理MOVE事件之前
ScrollView#onTouchEvent
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_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; //调用时机,在NSC真正处理MOVE事件之前 if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { deltaY -= mScrollConsumed[1]; vtev.offsetLocation(0, mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; } //伪代码 真正处理MOVE事件 //if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true) // && !hasNestedScrollingParent()) { // //} //当NSC滚动到底或者到顶了,unconsumedY不为0 //if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) { // mLastMotionY -= mScrollOffset[1]; // vtev.offsetLocation(0, mScrollOffset[1]); // mNestedYOffset += mScrollOffset[1]; // } break; } return true; }
NestedScrollingChild处理完滑动事件后,如果有剩余的滑动距离没有处理掉,会交给NestedScrollingParent去处理剩余的滑动距离->NestedScrollingChild#dispatchNestedScoll,如果NestedScrollingParent处理了剩余的距离,NestedScrollingChild会校正mLastMotionY。
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable @Size(2) int[] offsetInWindow) { if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { //NSC如果有滑动距离没消耗掉,会交给Pair对应的NSP去处理 if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) { int startX = 0; int startY = 0; if (offsetInWindow != null) { getLocationInWindow(offsetInWindow); startX = offsetInWindow[0]; startY = offsetInWindow[1]; } mNestedScrollingParent.onNestedScroll(this, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); if (offsetInWindow != null) { getLocationInWindow(offsetInWindow); offsetInWindow[0] -= startX; offsetInWindow[1] -= startY; } return true; } else if (offsetInWindow != null) { // No motion, no dispatch. Keep offsetInWindow up to date. offsetInWindow[0] = 0; offsetInWindow[1] = 0; } } return false; }
调用时机有两个
- 调用时机1 ScrollView#onTouchEvent中 NSC处理完事件后,调用
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_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; if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { deltaY -= mScrollConsumed[1]; vtev.offsetLocation(0, mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; } if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { final ViewParent parent = getParent(); 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 = mScrollY; final int range = getScrollRange(); final int overscrollMode = getOverScrollMode(); boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); // Calling overScrollBy will call onOverScrolled, which // calls onScrollChanged if applicable. if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true) && !hasNestedScrollingParent()) { // Break our velocity if we hit a scroll barrier. mVelocityTracker.clear(); } final int scrolledDeltaY = mScrollY - oldY; final int unconsumedY = deltaY - scrolledDeltaY; #调用时机1 if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) { mLastMotionY -= mScrollOffset[1]; vtev.offsetLocation(0, mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; } else if (canOverscroll) { final int pulledToY = oldY + deltaY; if (pulledToY < 0) { mEdgeGlowTop.onPull((float) deltaY / getHeight(), ev.getX(activePointerIndex) / getWidth()); if (!mEdgeGlowBottom.isFinished()) { mEdgeGlowBottom.onRelease(); } } else if (pulledToY > range) { mEdgeGlowBottom.onPull((float) deltaY / getHeight(), 1.f - ev.getX(activePointerIndex) / getWidth()); if (!mEdgeGlowTop.isFinished()) { mEdgeGlowTop.onRelease(); } } if (mEdgeGlowTop != null && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) { postInvalidateOnAnimation(); } } } break; } if (mVelocityTracker != null) { mVelocityTracker.addMovement(vtev); } vtev.recycle(); return true; }
调用时机2 ScrollView#onNestedScroll NSP处理完剩余的滚动之后,如果还有剩余滚动距离
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { final int oldScrollY = mScrollY; scrollBy(0, dyUnconsumed); final int myConsumed = mScrollY - oldScrollY; final int myUnconsumed = dyUnconsumed - myConsumed; dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null); }
- fling的逻辑比较特殊,这里以ScrollView为例,先把fling交给NSP的onNestedPreFling处理,如果返回true结束,如果返回false,先判断NSC能否滑动,如果能交给NSC处理,如果不能让NSP重复该步骤。这里有一个递归的逻辑
public boolean dispatchNestedPreFling(float velocityX, float velocityY) { if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { return mNestedScrollingParent.onNestedPreFling(this, velocityX, velocityY); } return false; }
private void flingWithNestedDispatch(int velocityY) { final boolean canFling = (mScrollY > 0 || velocityY > 0) && (mScrollY < getScrollRange() || velocityY < 0); if (!dispatchNestedPreFling(0, velocityY)) { dispatchNestedFling(0, velocityY, canFling); if (canFling) { fling(velocityY); } } }
NestedScrollingChild事件处理步骤
- 获取事件的优先处理权-> 调用startNestedScroll
- 处理滑动之前先问NesteScrollingParent要不要处理一部分距离
- NesteScrollingParent处理完之后,NestedScrollingChild再来处理剩余的距离
- NestedScrollingChild处理完之后还有剩余的距离,交给NesteScrollingParent去处理
NestedScrollingParent接口只能被ViewGroup实现。View实现该接口没有意义。NestedScrollingParent主要有两个作用。作用一、响应NesteScrollingChild的嵌套滑动请求,配合NesteScrollingChild完成嵌套滑动机制的交互。作用二、处理NestedScrollingChild滑动前和滑动后传递过来的事件剩余距离。
NestedScrollingParent方法以及作用
- getNestedScrollAxes返回嵌套滑动的方向,垂直方向或者水平方向。NestedScrollingParent正是通过该标志位,决定要不要拦截MOVE事件
- onNestedScrollAccepted在onStartNestedScroll返回true时,会被调用,该方法体会给mNestedScrollAxes赋值。
//ViewGroup.java public int getNestedScrollAxes() { return mNestedScrollAxes; }
//ScrollView.java public void onNestedScrollAccepted(View child, View target, int axes) { super.onNestedScrollAccepted(child, target, axes); startNestedScroll(SCROLL_AXIS_VERTICAL); }
onStartNestedScroll():Boolean方法与NestedScrollingChild#startNestedScroll方法相对应。如果返回true表示NestedScrollingParent响应NestedScrollingChild的嵌套滑动,反之表示不响应嵌套滑动。一般做法是外部ScrollView滑动方向和内部ScrollView滑动方向一致返回true。
//ScrollView.java public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { return (nestedScrollAxes & SCROLL_AXIS_VERTICAL) != 0; }
onNestedPreScroll方法会在NestedScrollingChild正式处理滑动事件之前调用。
//ScrollView#onNestedPreScroll 默认分发给父NSP public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { dispatchNestedPreScroll(dx, dy, consumed, null); }
onNestedScroll方法会在NestedScrollingChild正式处理滑动事件之后调用
//ScrollView.java public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { final int oldScrollY = mScrollY; //NSP先滑动NSC未消费完的 scrollBy(0, dyUnconsumed); final int myConsumed = mScrollY - oldScrollY; final int myUnconsumed = dyUnconsumed - myConsumed; //还有没消费完的继续向上分发 dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null); }
- onNestedPreFling与onNestedPreScroll类似
- onNesteFling如果NSC不可滑动了,则NSP分发。分发逻辑前面已经讲过
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { if (!consumed) { flingWithNestedDispatch((int) velocityY); return true; } return false; }
onStopNestedScroll会向上递归调用stopNestedScroll,mNestedScrollAxes置为0
public void onStopNestedScroll(View child) { // Stop any recursive nested scrolling. stopNestedScroll(); mNestedScrollAxes = 0; }
4.结合案例讲解嵌套滑动事件分发顺序
上述效果代码如下
自定义的ScrollView,主要是在相关事件方法中加入日志打印
Activity代码
布局文件
我们把布局文件转化成树形图
关于树中的节点,介绍如下
日志打印如下
OuterScrollView拦截MOVE事件条件:滑动距离>mTouchSlop&&OuterScrollView未响应嵌套滑动机制。
- DOWN事件到达OuterScrollView#onInterceptTouchEvent,不拦截。
- DOWN事件到达InnerScrollView#onInterceptTouchEvent,接着调用InnerScrollView#startNestedScroll,触发OuterScrollView#onStartNested,接着调用InnerScrollView#dispatchTouchEvent,由于是DOWN事件,会调用View#dispatchTouchEvent,调用stopNestedScroll,触发OuterScrollView#onStopNestedScroll。
- DOWN事件到达InnerScrollView#onTouchEvent方法,调用startNestedScroll,触发OuterScrollView#onStartNestedScroll,OuterScrollView响应嵌套滑动机制。
- MOVE事件到达OuterScrollView#onInterceptTouchEvent,拦截条件不成立,不拦截。
- MOVE事件到达InnerScrollView#onTouchEvent,调用InnerScrollView#dispatchNestedPreScroll,由于dy=0 dx=0,不会触发OuterScrollView的onNestedPreScroll。
- MOVE事件到达OuterScrollView#onInterceptTouchEvent,拦截条件不成立,不拦截。
- MOVE事件到达InnerScrollView#onTouchEvent,dy<mTouchSlop,只会触发InnerScrollView#dispatchNestedPreScroll,不会触发dispatchNestedScroll。
- MOVE事件到达OuterScrollView#onInterceptTouchEvent,拦截条件不成立,不拦截。
- MOVE事件到达InnerScrollView#onTouchEvent,dy<mTouchSlop,只会触发InnerScrollView#dispatchNestedPreScroll,不会触发dispatchNestedScroll。
- MOVE事件到达OuterScrollView#onInterceptTouchEvent,拦截条件不成立,不拦截。
- MOVE事件到达InnerScrollView#onTouchEvent,dy<mTouchSlop,只会触发InnerScrollView#dispatchNestedPreScroll,不会触发dispatchNestedScroll。
- MOVE事件到达OuterScrollView#onInterceptTouchEvent,拦截条件不成立,不拦截。
- MOVE事件到达InnerScrollView#onTouchEvent,(dy=31)>mTouchSlop,会同时触发InnerScrollView#dispatchNestedPreScroll和dispatchNestedScroll,同时会调用parent.requestDisallowInterceptTouchEvent(true)。从此MOVE事件直达InnerScrollView#onTouchEvent
- MOVE事件直达InnerScrollView#onTouchEvent
- MOVE事件直达InnerScrollView#onTouchEvent
- 处理UP事件
总结DOWN事件和MOVE事件在NestedScrollingChild中的分发逻辑
- DOWN事件到达NestedScrollingChild#onInterceptTouchEvent方法会调用startNestedScroll方法。
- DOWN事件到达NestedScrollingChild#dispathTouchEvent方法会调用stopNestedScroll方法。
- MOVE事件到达NestedScrollingChild#onTouchEvent方法,调用dispatchNestedPreScroll方法。如果mIsBeingDragged为true。会调用dispatchNestedScroll方法。从非dragged状态变成dragged状态时,会调用ViewParent#requestDisallowInterceptTouchEvent(true)。