android 二级 滚动,说一说android的嵌套滚动机制

欣赏一下

afdaf21ecb7f

年轻时候的贝鲁奇

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的关系图

afdaf21ecb7f

图片来自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这里好坑啊!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值