Android Design包通过提供NestedScrollingChild和NestedScrollingParent来帮助开发实现嵌套滑动效果
NestedScrollingChild |
NestedScrollingParent |
备注 |
dispatchNestedScroll |
onNestedScroll |
分发嵌套滑动事件,在子View滑动处理完之后调用, unconsumed表示未被子View滚动消费的距离, consumed表示被子View消费的滚动距离 |
startNestedScroll |
onStartNestedScroll |
前者的调用会触发后者的调用,然后后者的返回值将决定后续的嵌套滑动事件是否能传递给父View,如果返回false,父View将不处理嵌套滑动事件,一般前者的返回值即后者的返回值 |
|
onNestedScrollAccepted |
如果onStartNestedScroll返回true,则回调此方法 |
stopNestedScroll |
onStopNestedScroll |
dispatchNestedPreScroll |
onNestedPreScroll |
分发预嵌套滑动事件,在子View滑动处理之前调用, 通过consumed数组得到NestedScrollingParent消耗掉的滚动距离 |
dispatchNestedFling |
onNestedFling |
|
dispatchNestedPreFling |
onNestedPreFling |
|
|
getNestedScrollAxes |
获得滑动方向,没有回调,为主动调用的方法 |
举例说明: SwipeRefreshLayout && RecyclerView嵌套
本例子中,SwipeRefreshLayout为NestedScrollingParent,而RecyclerView为NestedScrollingChild
根据触摸事件分发机制,ACTION_DOWN首先会来到SwipeRefreshLayout#onInterceptTouchEvent
, 而SwipeRefreshLayout未做拦截,因此来到RecyclerView#onTouchEvent, 而RecyclerView#onTouchEvent针对ACTION_DOWN返回true;之后的所有事件都会传递到RecyclerView中,之后我们看下RecyclerView中对ACTION_MOVE事件的处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| case MotionEvent.ACTION_MOVE: {
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) { dx -= mScrollConsumed[0]; dy -= mScrollConsumed[1]; vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); mNestedOffsets[0] += mScrollOffset[0]; mNestedOffsets[1] += mScrollOffset[1]; }
if (mScrollState == SCROLL_STATE_DRAGGING) { mLastTouchX = x - mScrollOffset[0]; mLastTouchY = y - mScrollOffset[1];
if (scrollByInternal( canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, vtev)) { getParent().requestDisallowInterceptTouchEvent(true); } if (mGapWorker != null && (dx != 0 || dy != 0)) { mGapWorker.postFromTraversal(this, dx, dy); } } } break;
|
关注这里的dispatchNestedPreScroll
和scrollByInternal
方法, dispatchNestedPreScroll
最终会触发SwipeRefreshLayout#onNestedPreScroll
方法,scrollByInternal
会调用dispatchNestedScroll
方法,最终也来到SwipeRefreshLayout#onNestedScroll
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| @Override public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed, final int dxUnconsumed, final int dyUnconsumed) { dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, mParentOffsetInWindow);
final int dy = dyUnconsumed + mParentOffsetInWindow[1];
if (dy < 0 && !canChildScrollUp()) { mTotalUnconsumed += Math.abs(dy);
moveSpinner(mTotalUnconsumed); } }
@Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { if (dy > 0 && mTotalUnconsumed > 0) { if (dy > mTotalUnconsumed) { consumed[1] = dy - (int) mTotalUnconsumed; mTotalUnconsumed = 0; } else { mTotalUnconsumed -= dy; consumed[1] = dy; } moveSpinner(mTotalUnconsumed); }
if (mUsingCustomStart && dy > 0 && mTotalUnconsumed == 0 && Math.abs(dy - consumed[1]) > 0) { mCircleView.setVisibility(View.GONE); }
final int[] parentConsumed = mParentScrollConsumed; if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) { consumed[0] += parentConsumed[0]; consumed[1] += parentConsumed[1]; } }
|
嵌套滚动时一个核心问题是当手指滑动时,这个滑动的距离由谁消费?为了解决这个问题,NestedScrollingParent引入了consumed这个概念,通过一个consumed数组的引用,可以告知上层View消费掉了多少距离,而子View则可以根据这个消费掉的距离,以及滑动的总距离,来处理自己的滑动距离
在本例子中,手指移动的距离 = refreshView滑动的距离 + RecyclerView滑动的距离
如何在代码中实现嵌套滚动效果
对一个NestedScrollingChild首先调用startNestedScroll,之后调用dispatchNestedScroll即可
1 2 3 4 5 6 7 8
| recyclerView.startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) val arr = IntArray(2) recyclerView.dispatchNestedPreScroll(0, offsetY, arr, null) val dyUnconsumed = offsetY - arr[1] if (dyUnconsumed != 0) { recyclerView.scrollBy(0, dyUnconsumed) }
|