以
Recyclerview(v) -> (ViewPager2(h) -> LinearLayout(v)) -> NestScrollView(v)为例
为什么从
Recyclerview出发呢?因为在嵌套滚动机制中,是由支持嵌套滚动的子View控制的,先看一段代码,Recyclerview默认是支持嵌套滚动
public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
boolean nestedScrollingEnabled = true;
// ……是否更改nestedScrollingEnabled的值,默认不修改 -> true
setNestedScrollingEnabled(nestedScrollingEnabled);
}
Recyclerview#onTouchEvent()
Down
从
Down事件开始就在向上查找和当前子View支持滚动方向一致的父View了 ->vertical
public boolean onTouchEvent(MotionEvent e) {
switch (action) {
case MotionEvent.ACTION_DOWN: {
mScrollPointerId = e.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
// 可滚动方向:RecyclerView -> LayoutMager -> 此处以竖向为例 -> 0010
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE; // 0000
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL; // 0001
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL; // 0010
}
startNestedScroll(nestedScrollAxis, TYPE_TOUCH); //看这里!看这里!看这里!
} break;
}
开始阶段
Recyclerview#startNestedScroll()return getScrollingChildHelper().startNestedScroll(axes, type);
查找
NestedScrollingChildHelper#startNestedScroll()在这个方法中向上查找,注意:1.支持嵌套滚动 2.方向一致
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) { // 判断是否已经找到了处理滚动事件的父View:return mNestedScrollingParentTouch != null if (hasNestedScrollingParent(type)) { // Already in progress return true; } if (isNestedScrollingEnabled()) { ViewParent p = mView.getParent(); View child = mView; // 向上查找到支持嵌套滚动的父View:RecyclerView -> ViewPager2 -> LinearLayout -> NestedScrollView while (p != null) { if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) { // 设置处理滚动事件的父View:mNestedScrollingParentTouch = p; setNestedScrollingParentForType(type, p); ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type); return true; } if (p instanceof View) { child = (View) p; } p = p.getParent(); } } return false; }
标记
-
ViewParentCompat#onStartNestedScroll()不同接口实现的兼容处理(Compat),后面会逐渐省略这部分
NestedScrollView接口NestedScrollingParent3,NestedScrollingParent3继承自NestedScrollingParent2,由NestedScrollView实现。其实是public static boolean onStartNestedScroll(@NonNull ViewParent parent, @NonNull View child, @NonNull View target, int nestedScrollAxes, int type) { if (parent instanceof NestedScrollingParent2) { // First try the NestedScrollingParent2 API return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target, nestedScrollAxes, type); } else if (type == ViewCompat.TYPE_TOUCH) { // Else if the type is the default (touch), try the NestedScrollingParent API if (Build.VERSION.SDK_INT >= 21) { try { return Api21Impl.onStartNestedScroll(parent, child, target, nestedScrollAxes); } catch (AbstractMethodError e) { Log.e(TAG, "ViewParent " + parent + " does not implement interface " + "method onStartNestedScroll", e); } } else if (parent instanceof NestedScrollingParent) { return ((NestedScrollingParent) parent).onStartNestedScroll(child, target, nestedScrollAxes); } } return false; }-
(NestedScrollingParent2)NestedScrollView#onStartNestedScroll()看咱滚的方向一致不一致,一致就你了
@Override public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) { // 纵向:SCROLL_AXIS_VERTICAL = 1 << 1 = 2 横向:1。本例中 0010 & 0010 = 0010 = 2 != 0 return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; }
-
-
ViewParentCompat#onNestedScrollAccepted()后面好多这样的
ViewCompat兼容处理,之后就省略掉了public static void onNestedScrollAccepted(@NonNull ViewParent parent, @NonNull View child, @NonNull View target, int nestedScrollAxes, int type) { if (parent instanceof NestedScrollingParent2) { // First try the NestedScrollingParent2 API ((NestedScrollingParent2) parent).onNestedScrollAccepted(child, target, nestedScrollAxes, type); } //……省略其它兼容处理(parent instanceof NestedScrollingParent) }(NestedScrollingParent2)NestedScrollView#onNestedScrollAccepted()NestedScrollView可以接收竖向滚动事件,实现类NestedScrollingParentHelper记录滚动方向@Override public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) { mParentHelper.onNestedScrollAccepted(child, target, axes, type); // 标记可滚动方向 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type); // return true }
Move
在
Move事件中处理嵌套滚动的2种情况(if中的方法):1.子View消费前 2.子View消费后
public boolean onTouchEvent(MotionEvent e) {
switch (action) {
case MotionEvent.ACTION_MOVE: {
if (mScrollState == SCROLL_STATE_DRAGGING) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
// 这里!这里!这里!1.子View消费前 -> 父View消费滚动事件
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
// 根据父View消费的多少consumed[]计算还剩多少dx、dy
dx -= mReusableIntPair[0];
dy -= mReusableIntPair[1];
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
// Scroll has initiated, prevent parents from intercepting
getParent().requestDisallowInterceptTouchEvent(true);
}
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
// 还有这里!这里!2.先子View消费 -> 询问父View是否消费剩余部分
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
e)) { // -> dispatchNestedScroll()
getParent().requestDisallowInterceptTouchEvent(true);
}
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
} break;
}
滚动阶段
阶段一:子View消费前
NestedScrollingChildHelper#dispatchNestedPreScroll()子View将
Move事件分发给Down事件中找到的parentpublic boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, @NestedScrollType int type) { if (isNestedScrollingEnabled()) { // 判断是否可以滚动 final ViewParent parent = getNestedScrollingParentForType(type); // parent = NestedScrollView if (parent == null) { return false; } // 有滚动 if (dx != 0 || dy != 0) { int startX = 0; int startY = 0; if (offsetInWindow != null) { mView.getLocationInWindow(offsetInWindow); // 获取当前View在父View中的坐标 startX = offsetInWindow[0]; startY = offsetInWindow[1]; } if (consumed == null) { consumed = getTempNestedScrollConsumed(); } consumed[0] = 0; consumed[1] = 0; // 重写下面的方法处理滚动事件 ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type); if (offsetInWindow != null) { mView.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; }NestScrollView#onNestedPreScroll()重写这个方法处理滚动逻辑,不重写的话,为啥我感觉会有问题呢😂
@Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { dispatchNestedPreScroll(dx, dy, consumed, null, type); } @Override public boolean dispatchNestedPreScroll( int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type ) { return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type); }
阶段二:子View消费后
Recyclerview#scrollByInternal()
这一段是
Recyclerview先进行消费,剩余的部分再交给NestedScrollView消费
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0;
int unconsumedY = 0;
int consumedX = 0;
int consumedY = 0;
consumePendingUpdateOperations();
// 子View(RecyclerView)消费滚动事件
if (mAdapter != null) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
scrollStep(x, y, mReusableIntPair);
consumedX = mReusableIntPair[0];
consumedY = mReusableIntPair[1];
unconsumedX = x - consumedX;
unconsumedY = y - consumedY;
}
if (!mItemDecorations.isEmpty()) {
invalidate();
}
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
// 子View再次询问父View是否要消费滚动事件
dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
TYPE_TOUCH, mReusableIntPair);
unconsumedX -= mReusableIntPair[0];
unconsumedY -= mReusableIntPair[1];
boolean consumedNestedScroll = mReusableIntPair[0] != 0 || mReusableIntPair[1] != 0;
// Update the last touch co-ords, taking any scroll offset into account
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
// 处理过度滚动效果
if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) {
pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
}
considerReleasingGlowsOnScroll(x, y);
}
// 分发滚动事件给相应的监听器 -> 同步滑动效果实现
if (consumedX != 0 || consumedY != 0) {
dispatchOnScrolled(consumedX, consumedY);
}
if (!awakenScrollBars()) {
invalidate();
}
return consumedNestedScroll || consumedX != 0 || consumedY != 0;
}
NestedScrollingChildHelper#dispatchNestedScroll()这块除了部分判断条件不同,和阶段一
dispatchNestedPreScroll()的实现大体一致public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type, @Nullable int[] consumed) { dispatchNestedScrollInternal(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed); } // 实现和预滚动分发区别不大 private boolean dispatchNestedScrollInternal(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type, @Nullable int[] consumed) { if (isNestedScrollingEnabled()) { final ViewParent parent = getNestedScrollingParentForType(type); if (parent == null) { return false; } 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]; } if (consumed == null) { consumed = getTempNestedScrollConsumed(); consumed[0] = 0; consumed[1] = 0; } // 还是重写这个方法,看!是不是和阶段一差不多 ViewParentCompat.onNestedScroll(parent, mView, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed); if (offsetInWindow != null) { mView.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; }NestScrollView#onNestedScroll()这里
NestScrollView提供了部分实现public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type, @NonNull int[] consumed) { onNestedScrollInternal(dyUnconsumed, type, consumed); } private void onNestedScrollInternal(int dyUnconsumed, int type, @Nullable int[] consumed) { final int oldScrollY = getScrollY(); scrollBy(0, dyUnconsumed); // 消费没有被消费掉的滚动距离 final int myConsumed = getScrollY() - oldScrollY; if (consumed != null) { consumed[1] += myConsumed; } final int myUnconsumed = dyUnconsumed - myConsumed; mChildHelper.dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null, type, consumed); }
总结
滚动阶段需要重写的方法:
onNestedPreScroll()和onNestedScroll()
Up
public boolean onTouchEvent(MotionEvent e) {
switch (action) {
case MotionEvent.ACTION_UP: {
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
final float xvel = canScrollHorizontally
? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
final float yvel = canScrollVertically
? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
// 看这里!看这里!看这里! -> fling()
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) { // 惯性滑动`fling`
setScrollState(SCROLL_STATE_IDLE);
}
resetScroll(); // 还有这里!这里!结束阶段 -> stopNestedScroll(TYPE_TOUCH)
} break;
}
}
惯性阶段
这一块建议多想一想,真的很绕😟,其实有点没搞明白,希望能有位大佬指点迷津(☆▽☆)
Recyclerview#fling()
public boolean fling(int velocityX, int velocityY) {
// ……
if (!dispatchNestedPreFling(velocityX, velocityY)) {
// 父View没有消费(完)惯性滑动距离velocityY -> false
final boolean canScroll = canScrollHorizontal || canScrollVertical; // Recyclerview的可滚动方向
dispatchNestedFling(velocityX, velocityY, canScroll); // 通知NestedScrollView 惯性滑动距离和方向
// 如果有监听器 -> 监听器处理
if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
return true;
}
if (canScroll) {
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontal) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertical) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH); // 再次启用嵌套滚动
velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
mViewFlinger.fling(velocityX, velocityY); // 启动惯性滚动,当作给了Recyclerview一组`Move`
return true;
}
}
// 父View消费完惯性滑动距离velocityY -> true
return false;
}
情况一:在子View前响应
NestedScrollingChildHelper#dispatchNestedPreFling()public boolean dispatchNestedPreFling(float velocityX, float velocityY) { if (isNestedScrollingEnabled()) { ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH); if (parent != null) { return ViewParentCompat.onNestedPreFling(parent, mView, velocityX, velocityY); } } return false; }NestedScrollView#onNestedPreFling()@Override // 重写此方法 public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) { return dispatchNestedPreFling(velocityX, velocityY); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); }
情况二:在子View后响应
NestedScrollingChildHelper#dispatchNestedFling()public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { if (isNestedScrollingEnabled()) { ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH); if (parent != null) { return ViewParentCompat.onNestedFling(parent, mView, velocityX, velocityY, consumed); } } return false; }NestedScrollView#onNestedFling()public boolean onNestedFling( @NonNull View target, float velocityX, float velocityY, boolean consumed) { if (!consumed) { dispatchNestedFling(0, velocityY, true); fling((int) velocityY); return true; } return false; }
结束阶段
Recyclerview#resetScroll()private void resetScroll() { if (mVelocityTracker != null) { mVelocityTracker.clear(); } stopNestedScroll(TYPE_TOUCH); // getScrollingChildHelper().stopNestedScroll(type); releaseGlows(); }NestedScrollingChildHelper#stopNestedScroll()还记得在开始阶段做了什么嘛?1.parent = p 2.实现类
NestedScrollingParentHelper记录了滚动方向public void stopNestedScroll(@NestedScrollType int type) { ViewParent parent = getNestedScrollingParentForType(type); if (parent != null) { ViewParentCompat.onStopNestedScroll(parent, mView, type); setNestedScrollingParentForType(type, null); // parent = null 看这里! } }NestedScrollView#onStopNestedScroll()@Override public void onStopNestedScroll(@NonNull View target, int type) { mParentHelper.onStopNestedScroll(target, type); stopNestedScroll(type); // mChildHelper.stopNestedScroll(type); 就上面那个 }NestedScrollingParentHelper#onStopNestedScroll()这里相当于清掉了开始阶段记录的滚动方向
public void onStopNestedScroll(@NonNull View target, @NestedScrollType int type) { if (type == ViewCompat.TYPE_NON_TOUCH) { mNestedScrollAxesNonTouch = ViewGroup.SCROLL_AXIS_NONE; } else { mNestedScrollAxesTouch = ViewGroup.SCROLL_AXIS_NONE; } }
嵌套滑动机制流程图(自绘)
关于滚动和惯性
可以在手机APP:哔哩哔哩视频暂停或者放完、小红书个人主页、米游社首页来回滑动感受一下
简单的嵌套实现
github.com/userbiliy/N… 自己写了一点,并不完善,快速滚动惯性效果会出问题