欣赏一下
1. 系统接口
NestedScrollingParent, NestedScrollingChild,android5.0之后新增的特性
在传统的事件分发机制中,如果一次手势想让多个view来联动,只能让里面的view先滚动起来然后等到适当的条件拦截事件让外面的view滚动,若想交换滚动顺序即先让外面的view动再让里面的view动,这是做不到的,因为事件机制是由里向外抛出,没法再回到里面了!但是在5.0左右的时候,提供了NestedScrollingParent,NestedScrollingChild接口,支持了嵌套手势操作,可以弥补这个缺陷哦。
-
什么是嵌套滚动呢?
- 当页面里面的控件在接受到手势行为去滚动的时候,能够让外面的view去滚动,然后外面滚到到符合你的要求了,你再让里面的控件滚动,也可以让外面的view和里面的控件一起滚动, 这个过程都是在一次手势中哦,所以正好弥补了传统事件机制中的不足。
-
NestedScrollingParent: 作为嵌套滑动的parent
public interface NestedScrollingParent { /** *当嵌套的child调用startNestedScroll,会触发这个方法,检测我们的parent是否支持嵌套的去滚动操作; return true即支持parent来滚动,return false即不支持嵌套滚动。 * target是我们的发起嵌套滚动操作的view哦。 */ public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes); /** *当上面的onStartNestedScroll返回true的时候,会触发这个方法来做你想要的初始化操作; */ 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); /** * 在嵌套的层级中,当嵌套的子view滑动时候,我们想在他之前先让parent来滑动,就执行这个操作。 */ public void onNestedPreScroll(View target, int dx, int dy, int[] consumed); /** *parent实现一定的滑翔处理 */ public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed); /** * 一般没什么用,在子child滑翔之前开始滑翔,一般不会有这个操作。retur false即可。 */ public boolean onNestedPreFling(View target, float velocityX, float velocityY); /** * 返回当前滚动的坐标轴线,横轴线/纵轴 */ public int getNestedScrollAxes();
NestedScrollingChild: 作为嵌套滑动的child
public interface NestedScrollingChild {
/**
* 设置child支持嵌套滑动,表示是否支持滚动的时候是否将发给parent.
*/
public void setNestedScrollingEnabled(boolean enabled);
/**
* 判断是否支持嵌套滑动
*/
public boolean isNestedScrollingEnabled();
/**
*child 开始着手触发嵌套活动了
*/
public boolean startNestedScroll(int axes);
/**
* child开始想要停止嵌套滑动了,与startNestedScroll对应,由他发起自然要由他结束了。
*/
public void stopNestedScroll();
/**
* 在child自身滚动之后分发剩余的未消费滑动距离
*/
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
/**
* 在子child决定滑动前先让他的parent来尝试下要不要先滑动下.
*/
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
/**
*当child滑翔的过程中时候,问问parent要不要也滑一下。
*/
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
/**
* 略
*/
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
-
NestedScrollingParent和NestedScrollingChild的关系图
NestedScrollingParentHelper:嵌套滚动的parent辅助类, 只是设计的方便,里面并没有做什么实际的动作。
-
NestedScrollingChildHelper:嵌套滚动的发起方child, 下面列出几个关键的方法
//当child滚动的时候,会调用该方法,找到可以接受嵌套去滚动的父容器, true表示找到了,false表示没有找到 public boolean startNestedScroll(int axes) { //如果已经有了,直接返回 if (hasNestedScrollingParent()) { return true; } //需要当前的child能支持嵌套滚动哦 if (isNestedScrollingEnabled()) { ViewParent p = mView.getParent(); View child = mView; //递归地上巡找到能够接收嵌套滚动的parent while (p != null) { //这个if检测当前的container是否支持嵌套滚动哦, if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) { //如果支持赋值给mNestedScrollingParent,后面就直接用它就好了 mNestedScrollingParent = p; ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes); return true; } //没找到继续向上遍历。 if (p instanceof View) { child = (View) p; } p = p.getParent(); } } return false; } //在嵌套滚动的时候,child在自己滚动前会先问问他的parent要不要先滚动下,是通过该方法来实现的。 // public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { //如果child支持嵌套滚动,并且存在嵌套的父容器, if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { if (dx != 0 || dy != 0) { int startX = 0; int startY = 0; if (offsetInWindow != null) { mView.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; //滚动父容器 ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed); if (offsetInWindow != null) { mView.getLocationInWindow(offsetInWindow); offsetInWindow[0] -= startX; offsetInWindow[1] -= startY; } //consumed记录了父容器消耗的距离,有就会返回true. return consumed[0] != 0 || consumed[1] != 0; } else if (offsetInWindow != null) { offsetInWindow[0] = 0; offsetInWindow[1] = 0; } } return false; } //在嵌套滚动的时候,如果child滚动了一段距离,还剩下一段手势距离,就交给他的父容器问问他要不要划一划,基本逻辑和前面的方法是一样的呢,return true表明有这样的parent并且划了。 public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { //如果child支持嵌套滚动,并且有嵌套的parent. if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) { int startX = 0; int startY = 0; if (offsetInWindow != null) { mView.getLocationInWindow(offsetInWindow); startX = offsetInWindow[0]; startY = offsetInWindow[1]; } //那么就让嵌套的parent来滑动一下 ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); if (offsetInWindow != null) { mView.getLocationInWindow(offsetInWindow); offsetInWindow[0] -= startX; offsetInWindow[1] -= startY; } //表明parent滚动了一段距离 return true; } else if (offsetInWindow != null) { // No motion, no dispatch. Keep offsetInWindow up to date. offsetInWindow[0] = 0; offsetInWindow[1] = 0; } } //表明没有滚动距离 return false; }
2. 嵌套在系统中的应用:NestedScrollView作为嵌套的parent, RecyclerView作为嵌套滚动的child的场景
- NestedScrollView:他既充当着嵌套滚动的父view,(其实也可同时充当着嵌套滚动的子child) 这里就看看作为parent实现了的NestedScrollingParent的相关接口吧, 接受嵌套child发起的滚动的操作都会在下面的接口中进行动作啦:
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
//如果是纵向的滚动,NestedScrollView支持嵌套地滚动;
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
//如果onStartNestedScroll返回true,走到这里。
mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
//NestedScrollView同时也作为child, 将嵌套事件发给他的parent中去;是一种递归嵌套
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
}
@Override
public void onStopNestedScroll(View target) {
mParentHelper.onStopNestedScroll(target);
//NestedScrollView同时也作为child,将嵌套滚动发给他的parent中去;
stopNestedScroll();
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed) {
final int oldScrollY = getScrollY();
//消耗child没有滚动完的距离,
scrollBy(0, dyUnconsumed);
final int myConsumed = getScrollY() - oldScrollY;
final int myUnconsumed = dyUnconsumed - myConsumed;
//将自己未消耗完的距离继续递归地给到他的parent去消耗。NestedScrollView这时候又冲到嵌套的child
dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
// Do nothing
//不会在child滑行前做什么
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
//如果child没有消耗,NestedScrollView将消耗掉这些。
if (!consumed) {
flingWithNestedDispatch((int) velocityY);
return true;
}
return false;
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
// Do nothing
return false;
}
@Override
public int getNestedScrollAxes() {
//获取滚动的轴,横向的或是纵向的。
return mParentHelper.getNestedScrollAxes();
}
-
NestedScrollView中的拦截和消耗事件对嵌套滚动原则的相关处理,看看onInterceptTouchEvent和onTouchEvent.
- onInterceptTouchEvent, 如何拦截的呢,看源码注释解读
//返回true就是拦截下来, false就是不拦截 public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction(); //如果当前是move,并且当前NestedScrollView处于了滚动状态,就返回true.滚动事件不会下去,所以他的子view没法发起嵌套滚动的操作。 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { return true; } switch (action & MotionEventCompat.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 = MotionEventCompat.findPointerIndex(ev, activePointerId); //无效的判断...... if (pointerIndex == -1) { Log.e(TAG, "Invalid pointerId=" + activePointerId + " in onInterceptTouchEvent"); break; } final int y = (int) MotionEventCompat.getY(ev, pointerIndex); final int yDiff = Math.abs(y - mLastMotionY); //如果当前move是滚动操作,并且当前View压根就不支持嵌套滚动,那么就表示自己要来实现滚动啦。这时候后面的move都会被该NestedScrollView拦截下来的。 if (yDiff > mTouchSlop && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) { mIsBeingDragged = true; mLastMotionY = y; initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement(ev); mNestedYOffset = 0; final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } break; } case MotionEvent.ACTION_DOWN: { final int y = (int) ev.getY(); //如果down位置落点不在他的child内部,啥都不做,没法滚动 if (!inChild((int) ev.getX(), (int) y)) { mIsBeingDragged = false; recycleVelocityTracker(); break; } mLastMotionY = y; mActivePointerId = MotionEventCompat.getPointerId(ev, 0); //建立速度跟踪,然后跟踪手势 initOrResetVelocityTracker(); mVelocityTracker.addMovement(ev); //计算滚动 mScroller.computeScrollOffset(); //滚动没结束,mIsBeingDragged为true. mIsBeingDragged = !mScroller.isFinished(); //作为嵌套的child, 发起滚动请求 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: //mIsBeingDragged清除掉状态, mIsBeingDragged = false; mActivePointerId = INVALID_POINTER; //清除掉速度跟踪 recycleVelocityTracker(); //检查是否要滚动回弹一下 if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { ViewCompat.postInvalidateOnAnimation(this); } //停下嵌套滚动,如果有嵌套滚动的操作。 stopNestedScroll(); break; } //mIsBeingDragged其实表示的就是档次拖动是不是给这个ScrollView用; return mIsBeingDragged; }
-
onTouchEvent:如何响应的呢,看源码注释解读
public boolean onTouchEvent(MotionEvent ev) { initVelocityTrackerIfNotExists(); MotionEvent vtev = MotionEvent.obtain(ev); final int actionMasked = MotionEventCompat.getActionMasked(ev); if (actionMasked == MotionEvent.ACTION_DOWN) { mNestedYOffset = 0; } vtev.offsetLocation(0, mNestedYOffset); switch (actionMasked) { case MotionEvent.ACTION_DOWN: { if (getChildCount() == 0) { return false; } //down的时候,请求父容器不要拦截; if ((mIsBeingDragged = !mScroller.isFinished())) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } //当我们滚动scrollView的时候,如果还在滑行,我们突然按下手指,滚动就会停下来,就是因为这里的处理哦! if (!mScroller.isFinished()) { mScroller.abortAnimation(); } // Remember where the motion event started mLastMotionY = (int) ev.getY(); mActivePointerId = MotionEventCompat.getPointerId(ev, 0); //这里是和嵌套滚动相关的地方,作为嵌套的child, 发起纵向的滚动请求 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); break; } case MotionEvent.ACTION_MOVE: final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); if (activePointerIndex == -1) { Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); break; } final int y = (int) MotionEventCompat.getY(ev, activePointerIndex); //计算滚动的距离 int deltaY = mLastMotionY - y; //这时候其实作为一个child,滚动前先问问parent要不要滚动一下 if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { //除去parent滚动过的距离 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) {//表示NestedScrollView自己要滚动了 // Scroll to follow the motion event mLastMotionY = y - mScrollOffset[1]; final int oldY = getScrollY(); final int range = getScrollRange(); final int overscrollMode = ViewCompat.getOverScrollMode(this); boolean canOverscroll = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS || (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); //overScrollByCompat表示要自己来滚动对应的距离啦,并不一定会滚动完所有的剩余距离 if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0, 0, true) && !hasNestedScrollingParent()) { // Break our velocity if we hit a scroll barrier. mVelocityTracker.clear(); } final int scrolledDeltaY = getScrollY() - oldY; final int unconsumedY = deltaY - scrolledDeltaY; //这里还是作为child, 把还没滚完的手势给到父parent.让他去滚动 if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) { mLastMotionY -= mScrollOffset[1]; vtev.offsetLocation(0, mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; } else if (canOverscroll) {//如果支持, 当滑动了上下边界的,要绘制边界阴影了 ensureGlows(); final int pulledToY = oldY + deltaY; if (pulledToY < 0) {//绘制上面的边界阴影 mEdgeGlowTop.onPull((float) deltaY / getHeight(), MotionEventCompat.getX(ev, activePointerIndex) / getWidth()); if (!mEdgeGlowBottom.isFinished()) { mEdgeGlowBottom.onRelease(); } } else if (pulledToY > range) {//绘制下面的边界阴影 mEdgeGlowBottom.onPull((float) deltaY / getHeight(), 1.f - MotionEventCompat.getX(ev, activePointerIndex) / getWidth()); if (!mEdgeGlowTop.isFinished()) { mEdgeGlowTop.onRelease(); } } if (mEdgeGlowTop != null && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {//刷新绘制,从而让边界阴影显示出来; ViewCompat.postInvalidateOnAnimation(this); } } } break; case MotionEvent.ACTION_UP: if (mIsBeingDragged) { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(velocityTracker, mActivePointerId); //如果大于最小速度限制,会滑行 if ((Math.abs(initialVelocity) > mMinimumVelocity)) { //该方法会做嵌套滑行分发,也就是当钱view支持滑行的时候也会给parent-view去滑行一下,不过他们没有做距离和速度分减少,也不好做因为他们都是根据最后的初始速度去减速滑行的。只是对应的parent可以根据child是否到边界了选择滑还是不滑。 flingWithNestedDispatch(-initialVelocity); } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { ViewCompat.postInvalidateOnAnimation(this); } } mActivePointerId = INVALID_POINTER; endDrag(); break; ....... vtev.recycle(); return true; }
本来是想分析NestedScrollView作为嵌套的parent行为,但从前面的onTouchEvent中源码可以看到,NestedScrollView这里其实基本充当着嵌套的child角色的,想想也是对的,嵌套滚动操作是由child来发起的然后parent响应,onTouchEvent自然是动作发起的地方,所以这里基本就是child的动作行为。我们在认识传统事件分发的时候,知道滚动这些move操作当前只能给某个view去消耗,没法给多个人使用的,而嵌套滚动却可以,在这里总结下他的实现,他在move的时候先将滚动距离通过
dispatchNestedPreScroll
传递给实现了NestedScrollingParent的接口的parent, 让他先滚动滚动,然后扣除parent滚动过的距离,接着自己再调用overScrollByCompat
,NestedScrollView自己来滚动,如果还有剩余又调用dispatchNestedScroll
, 继续让parent去滚动。在手指抬起的时候如果有滑行操作,也会把滑行速度传递父parent,父parent可以自行决定要不要进行滑行。大概就是这么个逻辑,实现了多个view来消耗一次手势操作呢。
-
RecyclerView, 他只能作为嵌套的子child, 即实现NestedScrollingChild,而没能做parent. 就来看看他的onInterceptTouchEvent和onTouchEvent,是如何处理嵌套相关的行为吧。感觉应该和NestedScrollView应该是很相似的逻辑的哦
- RecyclerView.onInterceptTouchEvent,看源码注释解读
public boolean onInterceptTouchEvent(MotionEvent e) { if (mLayoutFrozen) { return false; } if (dispatchOnItemTouchIntercept(e)) { cancelTouch(); return true; } if (mLayout == null) { return false; } final boolean canScrollHorizontally = mLayout.canScrollHorizontally(); final boolean canScrollVertically = mLayout.canScrollVertically(); if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(e); final int action = MotionEventCompat.getActionMasked(e); final int actionIndex = MotionEventCompat.getActionIndex(e); switch (action) { case MotionEvent.ACTION_DOWN: if (mIgnoreMotionEventTillDown) { mIgnoreMotionEventTillDown = false; } mScrollPointerId = MotionEventCompat.getPointerId(e, 0); mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f); mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f); if (mScrollState == SCROLL_STATE_SETTLING) { getParent().requestDisallowInterceptTouchEvent(true); setScrollState(SCROLL_STATE_DRAGGING); } // Clear the nested offsets mNestedOffsets[0] = mNestedOffsets[1] = 0; int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE; if (canScrollHorizontally) { nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL; } if (canScrollVertically) { nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL; } //down的时候发起嵌套滚动请求 startNestedScroll(nestedScrollAxis); break; case MotionEventCompat.ACTION_POINTER_DOWN: mScrollPointerId = MotionEventCompat.getPointerId(e, actionIndex); mInitialTouchX = mLastTouchX = (int) (MotionEventCompat.getX(e, actionIndex) + 0.5f); mInitialTouchY = mLastTouchY = (int) (MotionEventCompat.getY(e, actionIndex) + 0.5f); break; case MotionEvent.ACTION_MOVE: { final int index = MotionEventCompat.findPointerIndex(e, mScrollPointerId); if (index < 0) { Log.e(TAG, "Error processing scroll; pointer index for id " + mScrollPointerId + " not found. Did any MotionEvents get skipped?"); return false; } final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f); final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f); if (mScrollState != SCROLL_STATE_DRAGGING) { final int dx = x - mInitialTouchX; final int dy = y - mInitialTouchY; boolean startScroll = false; if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) { mLastTouchX = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1); startScroll = true; } if (canScrollVertically && Math.abs(dy) > mTouchSlop) { mLastTouchY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1); startScroll = true; } if (startScroll) { setScrollState(SCROLL_STATE_DRAGGING); } } } break; ....... case MotionEvent.ACTION_UP: { mVelocityTracker.clear(); //停止嵌套滚动 stopNestedScroll(); } break; case MotionEvent.ACTION_CANCEL: { cancelTouch(); } } return mScrollState == SCROLL_STATE_DRAGGING; }
- 总结一下,从recyclerView的拦截方法中可以看出,其实和嵌套滚动操作的内容是很少的,只有在down的时候发起一下嵌套操作startNestedScroll,在up的时候停止嵌套滚动,告知到他的父容器,比如NestedScrollView。那么就看看他的其他关于拦截的逻辑吧,只要在拖拽的过程中,就会拦截下来,那么他的子view一般在这里就没法响应触摸事件啦。
-
RecyclerView.onTouchEvent,看源码注释解读
public boolean onTouchEvent(MotionEvent e) { ...... if (action == MotionEvent.ACTION_DOWN) { mNestedOffsets[0] = mNestedOffsets[1] = 0; } vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]); switch (action) { case MotionEvent.ACTION_DOWN: { mScrollPointerId = MotionEventCompat.getPointerId(e, 0); mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f); mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f); if (mScrollState == SCROLL_STATE_SETTLING) { //请求recyclerView的父容器不要拦截啊,看样子android系统也是这么做的哦,也担心上面被拦了 getParent().requestDisallowInterceptTouchEvent(true); setScrollState(SCROLL_STATE_DRAGGING); } int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE; if (canScrollHorizontally) { nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL; } if (canScrollVertically) { nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL; } //根据是横向的还是竖向的启动嵌套滚动 startNestedScroll(nestedScrollAxis); } break; ...... case MotionEvent.ACTION_MOVE: { final int index = MotionEventCompat.findPointerIndex(e, mScrollPointerId); //检查操作 if (index < 0) { Log.e(TAG, "Error processing scroll; pointer index for id " + mScrollPointerId + " not found. Did any MotionEvents get skipped?"); return false; } final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f); final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f); int dx = mLastTouchX - x; int dy = mLastTouchY - y; // 传递给parent去预先滚动一段距离 if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) { dx -= mScrollConsumed[0]; dy -= mScrollConsumed[1]; vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); // Updated the nested offsets mNestedOffsets[0] += mScrollOffset[0]; mNestedOffsets[1] += mScrollOffset[1]; } //这里应该是一个设定,只要我们的move达到了一段的距离,我们就要让recyclerView滚动起来! if (mScrollState != SCROLL_STATE_DRAGGING) { boolean startScroll = false; if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) { if (dx > 0) { dx -= mTouchSlop; } else { dx += mTouchSlop; } startScroll = true; } if (canScrollVertically && Math.abs(dy) > mTouchSlop) { if (dy > 0) { dy -= mTouchSlop; } else { dy += mTouchSlop; } startScroll = true; } //设置滚动态 if (startScroll) { setScrollState(SCROLL_STATE_DRAGGING); } } if (mScrollState == SCROLL_STATE_DRAGGING) { mLastTouchX = x - mScrollOffset[0]; mLastTouchY = y - mScrollOffset[1]; //scrollByInternal自己滚动一段距离,并且内部还会将剩下的距离又传递给parent. //以后可以去查看该方法的实现。 if (scrollByInternal( canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, vtev)) { //请求父容器不要拦截 getParent().requestDisallowInterceptTouchEvent(true); } } } break; ...... case MotionEvent.ACTION_UP: { mVelocityTracker.addMovement(vtev); eventAddedToVelocityTracker = true; mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity); final float xvel = canScrollHorizontally ? -VelocityTrackerCompat.getXVelocity(mVelocityTracker, mScrollPointerId) : 0; final float yvel = canScrollVertically ? -VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0; //fling操作,在这里处理嵌套滑行的行为,可以查看里面的方法细节 if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) { setScrollState(SCROLL_STATE_IDLE); } resetTouch(); } break; case MotionEvent.ACTION_CANCEL: { cancelTouch(); } break; } if (!eventAddedToVelocityTracker) { mVelocityTracker.addMovement(vtev); } vtev.recycle(); return true; }
总结一下,RecyclerView的onTouchEvent和NestedScrollView的逻辑很相似,二者在这个区间里表现的都是一个嵌套child的行为,在down的时候发起,在move先传递给parent, 然后自己消耗。大概就这样子吧。
3. 嵌套存在着的问题,以及造成的原因
-
NestedScrollView/ScrollView嵌套ListView显示不全,经常显示一行问题!
- 原因在哪里呢,如下:
---NestedScrollView方法: protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); //在测量子View的高度的时候传递进去的是UNSPECIFIED,也就是不限制子view的高度。 final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
---ListView方法: protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Sets up mListPadding super.onMeasure(widthMeasureSpec, heightMeasureSpec); ....... if (widthMode == MeasureSpec.UNSPECIFIED) { widthSize = mListPadding.left + mListPadding.right + childWidth + getVerticalScrollbarWidth(); } else { widthSize |= (childState & MEASURED_STATE_MASK); } ....... //重点在这里呢,如果是MeasureSpec.UNSPECIFIED模式,他设置的高度就是单个条目加上padding距离啊!所以就显示了一行......但是如果我们用其他的布局嵌套listView的时候,一般是不会传递UNSPECIFIED的规格的,所以没问题。 if (heightMode == MeasureSpec.UNSPECIFIED) { heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2; } setMeasuredDimension(widthSize, heightSize); mWidthMeasureSpec = widthMeasureSpec; }
- 解决, 重写LinearLayout的onMeasure方法,改写ScrollView传进来的测量规格哦,虽然解决了显示不全的问题,但是复用规则被打破!这不是好的办法。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //改写规格,将高度设置成无限。因此也就造成了一开始就全部展开,无法复用listView的单元控件。重要弊端! int heightSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); super.onMeasure(widthMeasureSpec, heightSpec); }
- NestedScrollView与RecyclerView嵌套,RecyclerView不能被重复利用
- 原因,还是看代码吧:
--- LineaLayoutManager //当layoutState.mInfinite为true的时候,会一直调用layoutChunk,从而让所有的itemView一次性全部创建了。ayoutState.mInfinite的计算就是mLayoutState.mInfinite = mOrientationHelper.getMode() == View.MeasureSpec.UNSPECIFIED;而这个mode也是ScrollView传递进来的! while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { layoutChunkResult.resetInternal(); layoutChunk(recycler, state, layoutState, layoutChunkResult); if (layoutChunkResult.mFinished) { break; } layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection; /** * Consume the available space if: * * layoutChunk did not request to be ignored * * OR we are laying out scrap children * * OR we are not doing pre-layout */ if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null || !state.isPreLayout()) { layoutState.mAvailable -= layoutChunkResult.mConsumed; // we keep a separate remaining space because mAvailable is important for recycling remainingSpace -= layoutChunkResult.mConsumed; } if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) { layoutState.mScrollingOffset += layoutChunkResult.mConsumed; if (layoutState.mAvailable < 0) { layoutState.mScrollingOffset += layoutState.mAvailable; } recycleByLayoutState(recycler, layoutState); } if (stopOnFocusable && layoutChunkResult.mFocusable) { break; } } void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) { View view = layoutState.next(recycler); ........ if (layoutState.mScrapList == null) { if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) { addView(view); } else { addView(view, 0); }
- 总结,上面两个都有复用规则打破的问题,这是个大问题,在少量数据还好,数据多了就会出现crash的,所以利用NestedScrollView+RecyclerView的去实现复杂界面并没有好的实现策略。虽然系统对二者都实现了嵌套滚动的策略,看上去处理的很好,然而却是存在着巨大的bug, google也推荐我们不要这么搞,但是实际有这样的需求啊, 感觉google这里好坑啊!