欣赏一下
年轻时候的贝鲁奇
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的关系图
图片来自https://www.jianshu.com/p/490659fae773
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这里好坑啊!