NestedScrollView 是用于替代 ScrollView 来解决嵌套滑动过程当中的滑动事件的冲突。做为开发者,你会发现不少地方会用到嵌套滑动的逻辑,好比下拉刷新页面,京东或者淘宝的各类商品页面。html
那为何要去了解 NestedScrollView 的源码呢?那是由于 NestedScrollView 是嵌套滑动实现的模板范例,经过研读它的源码,可以让你知道如何实现嵌套滑动,而后若是需求上 NestedScrollView 没法知足的时候,你能够自定义。android
说到嵌套滑动,就得说说这两个类了:NestedScrollingParent3 和 NestedScrollingChild3 ,固然同时也存在后面不带数字的类。之因此后面带数字了,是为了解决以前的版本遗留的问题:fling 的时候涉及嵌套滑动,没法透传到另外一个View 上继续 fling,致使滑动效果大打折扣 。spring
其实 NestedScrollingParent2 相比 NestedScrollingParent 在方法调用上多了一个参数 type,用于标记这个滑动是如何产生的。type 的取值以下:canvas
/** * Indicates that the input type for the gesture is from a user touching the screen. 触摸产生的滑动 */ public static final int TYPE_TOUCH = 0; /** * Indicates that the input type for the gesture is caused by something which is not a user * touching a screen. This is usually from a fling which is settling. 简单理解就是fling */ public static final int TYPE_NON_TOUCH = 1;
嵌套滑动,说得通俗点就是子 view 和 父 view 在滑动过程当中,互相通讯决定某个滑动是子view 处理合适,仍是 父view 来处理。因此, Parent 和 Child 之间存在相互调用,遵循下面的调用关系:app
上图能够这么理解:ide
在滑动事件产生可是子 view 还没处理前能够调用 dispatchNestedPreScroll(0,dy,consumed,offsetInWindow) 这个方法把事件传给父 view,这样父 view 就能在onNestedPreScroll 方法里面收到子 view 的滑动信息,而后作出相应的处理把处理完后的结果经过 consumed 传给子 view。函数
dispatchNestedPreScroll()以后,child能够进行本身的滚动操做。布局
若是父 view 须要在子 view 滑动后处理相关事件的话能够在子 view 的事件处理完成以后调用 dispatchNestedScroll 而后父 view 会在 onNestedScroll 收到回调。post
最后,滑动结束,调用 onStopNestedScroll() 表示本次处理结束。ui
可是,若是滑动速度比较大,会触发 fling, fling 也分为 preFling 和 fling 两个阶段,处理过程和 scroll 基本差很少。
首先是看类的名字
class NestedScrollView extends FrameLayout implements NestedScrollingParent3, NestedScrollingChild3, ScrollingView {
能够发现它继承了 FrameLayout,至关于它就是一个 ViewGroup,能够添加子 view , 可是须要注意的事,它只接受一个子 view,不然会报错。
@Override public void addView(View child) { if (getChildCount() > 0) { throw new IllegalStateException("ScrollView can host only one direct child"); } super.addView(child); } @Override public void addView(View child, int index) { if (getChildCount() > 0) { throw new IllegalStateException("ScrollView can host only one direct child"); } super.addView(child, index); } @Override public void addView(View child, ViewGroup.LayoutParams params) { if (getChildCount() > 0) { throw new IllegalStateException("ScrollView can host only one direct child"); } super.addView(child, params); } @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { if (getChildCount() > 0) { throw new IllegalStateException("ScrollView can host only one direct child"); } super.addView(child, index, params); }
对于 NestedScrollingParent3,NestedScrollingChild3 的做用,前文已经说了,若是仍是不理解,后面再对源码的分析过程当中也会分析到。
其实这里还能够提一下 RecyclerView:
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2, NestedScrollingChild3 {
这里没有继承 NestedScrollingParent3 是由于开发者以为 RecyclerView 适合作一个子类。而且它的功能做为一个列表去展现,也就是不适合再 RecyclerView 内部去作一些复杂的嵌套滑动之类的。这样 RecycylerView 外层就能够再嵌套一个 NestedScrollView 进行嵌套滑动了。后面再分析嵌套滑动的时候,也会把 RecycylerView 看成子类来进行分析,这样能更好的理解源码。
内部有个接口,使用者须要对滑动变化进行监听的,能够添加这个回调:
public interface OnScrollChangeListener { /** * Called when the scroll position of a view changes. * * @param v The view whose scroll position has changed. * @param scrollX Current horizontal scroll origin. * @param scrollY Current vertical scroll origin. * @param oldScrollX Previous horizontal scroll origin. * @param oldScrollY Previous vertical scroll origin. */ void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY); }
下面来看下构造函数:
public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initScrollView(); final TypedArray a = context.obtainStyledAttributes( attrs, SCROLLVIEW_STYLEABLE, defStyleAttr, 0); // 是否要铺满全屏 setFillViewport(a.getBoolean(0, false)); a.recycle(); // 便是子类,又是父类 mParentHelper = new NestedScrollingParentHelper(this); mChildHelper = new NestedScrollingChildHelper(this); // ...because why else would you be using this widget? 默认是滚动,否则你使用它就没有意义了 setNestedScrollingEnabled(true); ViewCompat.setAccessibilityDelegate(this, ACCESSIBILITY_DELEGATE); }
这里咱们用了两个辅助类来帮忙处理嵌套滚动时候的一些逻辑处理,NestedScrollingParentHelper,NestedScrollingChildHelper。这个是和前面的你实现的接口 NestedScrollingParent3,NestedScrollingChild3 相对应的。
下面看下 initScrollView 方法里的具体逻辑:
private void initScrollView() { mScroller = new OverScroller(getContext()); setFocusable(true); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
// 会调用 ViewGroup 的 onDraw setWillNotDraw(false); // 获取 ViewConfiguration 中一些配置,包括滑动距离,最大最小速率等等 final ViewConfiguration configuration = ViewConfiguration.get(getContext()); mTouchSlop = configuration.getScaledTouchSlop(); mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); }
在构造函数中,有这么一个设定:
setFillViewport(a.getBoolean(0, false));
与 setFillViewport 对应的属性是 android:fillViewport="true"。若是不设置这个属性为 true,可能会出现以下图同样的问题:
xml 布局:
<?xml version="1.0" encoding="utf-8"?> <NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" > <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:background="#fff000"> <Button android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout> </NestedScrollView>
效果:
能够发现这个没有铺满全屏,但是 xml 明明已经设置了 match_parent 了。这是什么缘由呢?
那为啥设置 true 就能够了呢?下面来看下它的 onMeasure 方法:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // false 直接返回 if (!mFillViewport) { return; } final int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (heightMode == MeasureSpec.UNSPECIFIED) { return; } if (getChildCount() > 0) { View child = getChildAt(0); final NestedScrollView.LayoutParams lp = (LayoutParams) child.getLayoutParams(); int childSize = child.getMeasuredHeight(); int parentSpace = getMeasuredHeight() - getPaddingTop() - getPaddingBottom() - lp.topMargin - lp.bottomMargin; // 若是子 view 高度小于 父 view 高度,那么须要从新设定高度 if (childSize < parentSpace) { int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin, lp.width); // 这里生成 MeasureSpec 传入的是 parentSpace,而且用的是 MeasureSpec.EXACTLY int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(parentSpace, MeasureSpec.EXACTLY); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } }
当你将 mFillViewport 设置为 true 后,就会把父 View 高度给予子 view 。但是这个解释了设置 mFillViewport 能够解决不能铺满屏幕的问题,但是没有解决为啥 match_parent 无效的问题。
在回到类的继承关系上,NestedScrollView 继承的是 FrameLayout,也就是说,FrameLayout 应该和 NestedScrollView 拥有同样的问题。但是当你把 xml 中的布局换成 FrameLayout 后,你发现居然没有问题。那么这是为啥呢?
缘由是 NestedScrollView 又重写了 measureChildWithMargins 。子view 的 childHeightMeasureSpec 中的 mode 是 MeasureSpec.UNSPECIFIED 。当被设置为这个之后,子 view 的高度就彻底是由自身的高度决定了。
@Override 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 的 MeasureSpec 时候,传入的是 MeasureSpec.UNSPECIFIED final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
好比子 view 是 LinearLayout ,这时候,它的高度就是子 view 的高度之和。并且,这个 MeasureSpec.UNSPECIFIED 会一直影响着后面的子子孙孙 view 。
我猜这么设计的目的是由于你既然使用了 NestedScrollView,就不必在把子 View 搞得跟屏幕同样大了,它该多大就多大,否则你滑动的时候,看见一大片空白体验也很差啊。
而 ViewGroup 中,measureChildWithMargins 的方法是这样的:
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
因为通常使用 NestedScrollView 的时候,都是会超过屏幕高度的,因此不设置这个属性为 true 也没有关系。
既然前面已经把 onMeasure 讲完了,那索引把绘制这块都讲了把。下面是 draw 方法,这里主要是绘制边界的阴影:
@Override public void draw(Canvas canvas) { super.draw(canvas); if (mEdgeGlowTop != null) { final int scrollY = getScrollY();
// 上边界阴影绘制 if (!mEdgeGlowTop.isFinished()) { final int restoreCount = canvas.save(); int width = getWidth(); int height = getHeight(); int xTranslation = 0; int yTranslation = Math.min(0, scrollY); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) { width -= getPaddingLeft() + getPaddingRight(); xTranslation += getPaddingLeft(); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) { height -= getPaddingTop() + getPaddingBottom(); yTranslation += getPaddingTop(); } canvas.translate(xTranslation, yTranslation); mEdgeGlowTop.setSize(width, height); if (mEdgeGlowTop.draw(canvas)) { ViewCompat.postInvalidateOnAnimation(this); } canvas.restoreToCount(restoreCount); }
// 底部边界阴影绘制 if (!mEdgeGlowBottom.isFinished()) { final int restoreCount = canvas.save(); int width = getWidth(); int height = getHeight(); int xTranslation = 0; int yTranslation = Math.max(getScrollRange(), scrollY) + height; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) { width -= getPaddingLeft() + getPaddingRight(); xTranslation += getPaddingLeft(); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) { height -= getPaddingTop() + getPaddingBottom(); yTranslation -= getPaddingBottom(); } canvas.translate(xTranslation - width, yTranslation); canvas.rotate(180, width, 0); mEdgeGlowBottom.setSize(width, height); if (mEdgeGlowBottom.draw(canvas)) { ViewCompat.postInvalidateOnAnimation(this); } canvas.restoreToCount(restoreCount); } } }
onDraw 是直接用了父类的,这个没啥好讲的,下面看看 onLayout:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); mIsLayoutDirty = false; // Give a child focus if it needs it if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) { scrollToChild(mChildToScrollTo); } mChildToScrollTo = null; if (!mIsLaidOut) { // 是不是第一次调用onLayout // If there is a saved state, scroll to the position saved in that state. if (mSavedState != null) { scrollTo(getScrollX(), mSavedState.scrollPosition); mSavedState = null; } // mScrollY default value is "0" // Make sure current scrollY position falls into the scroll range. If it doesn't, // scroll such that it does. int childSize = 0; if (getChildCount() > 0) { View child = getChildAt(0); NestedScrollView.LayoutParams lp = (LayoutParams) child.getLayoutParams(); childSize = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; } int parentSpace = b - t - getPaddingTop() - getPaddingBottom(); int currentScrollY = getScrollY(); int newScrollY = clamp(currentScrollY, parentSpace, childSize); if (newScrollY != currentScrollY) { scrollTo(getScrollX(), newScrollY); } } // Calling this with the present values causes it to re-claim them scrollTo(getScrollX(), getScrollY()); mIsLaidOut = true; }
onLayout 方法也没什么说的,基本上是用了父类 FrameLayout 的布局方法,加入了一些 scrollTo 操做滑动到指定位置。
若是对滑动事件不是很清楚的小伙伴能够先看看这篇文章:Android View 的事件分发原理解析。
在分析以前,先作一个假设,好比 RecyclerView 就是 NestedScrollView 的子类,这样去分析嵌套滑动更容易理解。这时候,用户点击 RecyclerView 触发滑动。须要分析整个滑动过程的事件传递。
这里,NestedScrollView 用的是父类的处理,并无添加本身的逻辑。
当事件进行分发前,ViewGroup 首先会调用 onInterceptTouchEvent 询问本身要不要进行拦截,不拦截,就会分发传递给子 view。通常来讲,对于 ACTION_DOWN 都不会拦截,这样子类有机会获取事件,只有子类不处理,才会再次传给父 View 来处理。下面来看看其具体代码逻辑:
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { /* * This method JUST determines whether we want to intercept the motion. * If we return true, onMotionEvent will be called and we do the actual * scrolling there. */ /* * Shortcut the most recurring case: the user is in the dragging * state and he is moving his finger. We want to intercept this * motion. */ final int action = ev.getAction();
// 若是已经在拖动了,说明已经在滑动了,直接返回 true if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { return true; } switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_MOVE: { /* * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check * whether the user has moved far enough from his original down touch. */ /* * Locally do absolute value. mLastMotionY is set to the y value * of the down event. */ final int activePointerId = mActivePointerId; if (activePointerId == INVALID_POINTER) { // If we don't have a valid id, the touch down wasn't on content. 不是一个有效的id break; } final int pointerIndex = ev.findPointerIndex(activePointerId); if (pointerIndex == -1) { Log.e(TAG, "Invalid pointerId=" + activePointerId + " in onInterceptTouchEvent"); break; } final int y = (int) ev.getY(pointerIndex);
// 计算垂直方向上滑动的距离 final int yDiff = Math.abs(y - mLastMotionY);
// 肯定能够产生滚动了 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) {
// 让父 view 不要拦截,这里应该是为了保险起见,由于既然已经走进来了,只要你返回 true,父 view 就不会拦截了。 parent.requestDisallowInterceptTouchEvent(true); } } break; } case MotionEvent.ACTION_DOWN: { final int y = (int) ev.getY();
// 若是点击的范围不在子 view 上,直接break,好比本身设置了很大的 margin,此时用户点击这里,这个范围理论上是不参与滑动的 if (!inChild((int) ev.getX(), y)) { mIsBeingDragged = false; recycleVelocityTracker(); break; } /* * Remember location of down touch. * ACTION_DOWN always refers to pointer index 0. */ mLastMotionY = y; mActivePointerId = ev.getPointerId(0); // 在收到 DOWN 事件的时候,作一些初始化的工做 initOrResetVelocityTracker(); mVelocityTracker.addMovement(ev); /* * If being flinged and user touches the screen, initiate drag; * otherwise don't. mScroller.isFinished should be false when * being flinged. We need to call computeScrollOffset() first so that * isFinished() is correct. */ mScroller.computeScrollOffset();
// 若是此时正在fling, isFinished 会返回 flase mIsBeingDragged = !mScroller.isFinished();
// 开始滑动 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH); break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: /* Release the drag */ mIsBeingDragged = false; mActivePointerId = INVALID_POINTER; recycleVelocityTracker(); if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { ViewCompat.postInvalidateOnAnimation(this); }
// 手抬起后,中止滑动 stopNestedScroll(ViewCompat.TYPE_TOUCH); break; case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; } /* * The only time we want to intercept motion events is if we are in the * drag mode. */ return mIsBeingDragged; }
onInterceptTouchEvent 事件就是作一件事,决定事件是否是要继续交给本身的 onTouchEvent 处理。这里须要注意的一点是,若是子 view 在 dispatchTouchEvent 中调用了:
parent.requestDisallowInterceptTouchEvent(true)
那么,其实就不会再调用 onInterceptTouchEvent 方法。也就是说上面的逻辑就不会走了。可是能够发现,down 事件,通常是不会拦截的。可是若是正在 fling,此时就会返回 true,直接把事件所有拦截。
那看下 RecyclerView 的 dispatchTouchEvent 是父类的,没啥好分析的。并且它的 onInterceptTouchEvent 也是作了一些初始化的一些工做,和 NestedScrollView 同样没啥可说的。
再说 NestedScrollView 的 onTouchEvent。
对于 onTouchEvent 得分两类进行讨论,若是其子 view 不是 ViewGroup ,且是不可点击的,就会把事件直接交给 NestedScrollView 来处理。
可是若是点击的子 view 是 RecyclerView 的 ViewGroup 。当 down 事件来的时候,ViewGroup 的子 view 没有处理,那么就会交给 ViewGroup 来处理,你会发现ViewGroup 的 onTouchEvent 是默认返回 true 的。也就是说事件都是由 RecyclerView 来处理的。
这时候来看下 NestedScrollView 的 onTouchEvent 代码:
public boolean onTouchEvent(MotionEvent ev) { initVelocityTrackerIfNotExists(); MotionEvent vtev = MotionEvent.obtain(ev); final int actionMasked = ev.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { mNestedYOffset = 0; } vtev.offsetLocation(0, mNestedYOffset); switch (actionMasked) { case MotionEvent.ACTION_DOWN: {
// 须要有一个子类才能够进行滑动 if (getChildCount() == 0) { return false; }
// 前面提到若是用户在 fling 的时候,触碰,此时是直接拦截返回 true,本身来处理事件。 if ((mIsBeingDragged = !mScroller.isFinished())) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } /* * If being flinged and user touches, stop the fling. isFinished * will be false if being flinged.处理结果就是中止 fling */ if (!mScroller.isFinished()) { mScroller.abortAnimation(); } // Remember where the motion event started mLastMotionY = (int) ev.getY(); mActivePointerId = ev.getPointerId(0);
// 寻找嵌套父View,告诉它准备在垂直方向上进行 TOUCH 类型的滑动 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH); break; } case MotionEvent.ACTION_MOVE: final int activePointerIndex = ev.findPointerIndex(mActivePointerId); if (activePointerIndex == -1) { Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); break; } final int y = (int) ev.getY(activePointerIndex); int deltaY = mLastMotionY - y;
// 滑动前先把移动距离告诉嵌套父View,看看它要不要消耗,返回 true 表明消耗了部分距离 if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset, ViewCompat.TYPE_TOUCH)) { 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) { // Scroll to follow the motion event mLastMotionY = y - mScrollOffset[1]; final int oldY = getScrollY(); final int range = getScrollRange(); final int overscrollMode = getOverScrollMode(); boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); // Calling overScrollByCompat will call onOverScrolled, which // calls onScrollChanged if applicable.
// 该方法会触发自身内容的滚动 if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0, 0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) { // Break our velocity if we hit a scroll barrier. mVelocityTracker.clear(); } final int scrolledDeltaY = getScrollY() - oldY; final int unconsumedY = deltaY - scrolledDeltaY;
// 通知嵌套的父 View 我已经处理完滚动了,该你来处理了 if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset, ViewCompat.TYPE_TOUCH)) {
// 若是嵌套父View 消耗了滑动,那么须要更新 mLastMotionY -= mScrollOffset[1]; vtev.offsetLocation(0, mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; } else if (canOverscroll) { ensureGlows(); final int pulledToY = oldY + deltaY;
// 触发边缘的阴影效果 if (pulledToY < 0) { EdgeEffectCompat.onPull(mEdgeGlowTop, (float) deltaY / getHeight(), ev.getX(activePointerIndex) / getWidth()); if (!mEdgeGlowBottom.isFinished()) { mEdgeGlowBottom.onRelease(); } } else if (pulledToY > range) { EdgeEffectCompat.onPull(mEdgeGlowBottom, (float) deltaY / getHeight(), 1.f - ev.getX(activePointerIndex) / getWidth()); if (!mEdgeGlowTop.isFinished()) { mEdgeGlowTop.onRelease(); } } if (mEdgeGlowTop != null && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) { ViewCompat.postInvalidateOnAnimation(this); } } } break; case MotionEvent.ACTION_UP: final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
// 计算滑动速率 int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
// 大于最小的设定的速率,触发fling if ((Math.abs(initialVelocity) > mMinimumVelocity)) { flingWithNestedDispatch(-initialVelocity); } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { ViewCompat.postInvalidateOnAnimation(this); } mActivePointerId = INVALID_POINTER; endDrag(); break; case MotionEvent.ACTION_CANCEL: if (mIsBeingDragged && getChildCount() > 0) { if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { ViewCompat.postInvalidateOnAnimation(this); } } mActivePointerId = INVALID_POINTER; endDrag(); break; case MotionEvent.ACTION_POINTER_DOWN: { final int index = ev.getActionIndex(); mLastMotionY = (int) ev.getY(index); mActivePointerId = ev.getPointerId(index); break; } case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); break; } if (mVelocityTracker != null) { mVelocityTracker.addMovement(vtev); } vtev.recycle(); return true; }
先看 down 事件,若是处于 fling 期间,那么直接中止 fling, 接着会调用 startNestedScroll,会让 NestedScrollView 做为子 view 去 通知嵌套父 view,那么就须要找到有没有能够嵌套滑动的父 view 。
public boolean startNestedScroll(int axes, int type) { // 交给 mChildHelper 代理来处理相关逻辑 return mChildHelper.startNestedScroll(axes, type); } public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) { // 找到嵌套父 view 了,就直接返回 if (hasNestedScrollingParent(type)) { // Already in progress return true; } // 是否支持嵌套滚动 if (isNestedScrollingEnabled()) { ViewParent p = mView.getParent(); View child = mView; while (p != null) { // while 循环,将支持嵌套滑动的父 View 找出来。 if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) { // 把父 view 设置进去 setNestedScrollingParentForType(type, p); // 找到后,经过该方法能够作一些初始化操做 ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type); return true; } if (p instanceof View) { child = (View) p; } p = p.getParent(); } } return false; }
能够看到,这时候主要就是为了找到嵌套父 view。当 ViewParentCompat.onStartNestedScroll 返回 true,就表示已经找到嵌套滚动的父 View 了 。下面来看下这个方法的具体逻辑:
// ViewParentCompat public static boolean onStartNestedScroll(ViewParent parent, View child, 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 parent.onStartNestedScroll(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; }
这里其实没啥好分析,就是告诉父类当前是什么类型的滚动,以及滚动方向。其实这里能够直接看下 NestedScrollView 的 onStartNestedScroll 的逻辑。
// NestedScrollView public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
// 确保触发的是垂直方向的滚动 return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; }
当肯定了嵌套父 View 之后,又会调用父 view 的 onNestedScrollAccepted 方法,在这里能够作一些准备工做和配置。下面咱们看到的 是 Ns 里面的方法,注意不是父 view 的,只是看成参考。
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) { mParentHelper.onNestedScrollAccepted(child, target, axes, type);
// 这里 Ns 做为子 view 调用 该方法去寻找嵌套父 view。注意这个方法会被调用是 NS 做为父 view 收到的。这样就 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type); }
到这里,down 的做用就讲完了。
首先是会调用 dispatchNestedPreScroll,讲当前的滑动距离告诉嵌套父 View。
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type) {
// Ns 做为子 view 去通知父View return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type); }
下面看下 mChildHelper 的代码逻辑:
/** * Dispatch one step of a nested pre-scrolling operation to the current nested scrolling parent. * * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass * method/{@link androidx.core.view.NestedScrollingChild2} interface method with the same * signature to implement the standard policy.</p> * * @return true if the parent consumed any of the nested scroll */ public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, @NestedScrollType int type) { if (isNestedScrollingEnabled()) {
// 获取以前找到的嵌套滚动的父 View final ViewParent parent = getNestedScrollingParentForType(type); if (parent == null) { return false; } // 滑动距离确定不为0 才有意义 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;
// 调用嵌套父 View 的对应的回调 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; }
这里主要是将滑动距离告诉 父 view,有消耗就会返回 true 。
// ViewParentCompat public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy, int[] consumed) { onNestedPreScroll(parent, target, dx, dy, consumed, ViewCompat.TYPE_TOUCH); }
其实下面的 onNestedPreScroll 跟前面的 onStartNestedScroll 逻辑很像,就是层层传递。
public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy, int[] consumed, int type) { if (parent instanceof NestedScrollingParent2) { // First try the NestedScrollingParent2 API ((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, 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 { parent.onNestedPreScroll(target, dx, dy, consumed); } catch (AbstractMethodError e) { Log.e(TAG, "ViewParent " + parent + " does not implement interface " + "method onNestedPreScroll", e); } } else if (parent instanceof NestedScrollingParent) { ((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed); } } }
下面为了方便,无法查看 NS 的嵌套父 View 的逻辑。直接看 Ns 中对应的方法。
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
// 最终也是 Ns 再传给其嵌套父 View dispatchNestedPreScroll(dx, dy, consumed, null, type); }
传递完了以后,就会调用 overScrollByCompat 来实现滚动。
boolean overScrollByCompat(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { final int overScrollMode = getOverScrollMode(); final boolean canScrollHorizontal = computeHorizontalScrollRange() > computeHorizontalScrollExtent(); final boolean canScrollVertical = computeVerticalScrollRange() > computeVerticalScrollExtent(); final boolean overScrollHorizontal = overScrollMode == View.OVER_SCROLL_ALWAYS || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal); final boolean overScrollVertical = overScrollMode == View.OVER_SCROLL_ALWAYS || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical); int newScrollX = scrollX + deltaX; if (!overScrollHorizontal) { maxOverScrollX = 0; } int newScrollY = scrollY + deltaY; if (!overScrollVertical) { maxOverScrollY = 0; } // Clamp values if at the limits and record final int left = -maxOverScrollX; final int right = maxOverScrollX + scrollRangeX; final int top = -maxOverScrollY; final int bottom = maxOverScrollY + scrollRangeY; boolean clampedX = false; if (newScrollX > right) { newScrollX = right; clampedX = true; } else if (newScrollX < left) { newScrollX = left; clampedX = true; } boolean clampedY = false; if (newScrollY > bottom) { newScrollY = bottom; clampedY = true; } else if (newScrollY < top) { newScrollY = top; clampedY = true; } if (clampedY && !hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) { mScroller.springBack(newScrollX, newScrollY, 0, 0, 0, getScrollRange()); } onOverScrolled(newScrollX, newScrollY, clampedX, clampedY); return clampedX || clampedY; }
整块逻辑其实没啥好说的,而后主要是看 onOverScrolled 这个方法:
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { super.scrollTo(scrollX, scrollY); }
最终是调用 scrollTo 方法来实现了滚动。
当滚动完了后,会调用 dispatchNestedScroll 告诉父 view 当前还剩多少没消耗,若是是 0,那么就不会上传,若是没消耗完,就会传给父 View 。
若是是子 View 传给 NS 的,是会经过 scrollBy 来进行消耗的,而后继续向上层传递。
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) { final int oldScrollY = getScrollY(); scrollBy(0, dyUnconsumed); final int myConsumed = getScrollY() - oldScrollY; final int myUnconsumed = dyUnconsumed - myConsumed; dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null, type); }
假设当前已经滑动到顶部了,此时继续滑动的话,就会触发边缘的阴影效果。
当用户手指离开后,若是滑动速率超过最小的滑动速率,就会调用 flingWithNestedDispatch(-initialVelocity) ,下面来看看这个方法的具体逻辑:
private void flingWithNestedDispatch(int velocityY) { final int scrollY = getScrollY(); final boolean canFling = (scrollY > 0 || velocityY > 0) && (scrollY < getScrollRange() || velocityY < 0);
// fling 前问问父View 要不要 fling, 通常是返回 false if (!dispatchNestedPreFling(0, velocityY)) {
// 这里主要是告诉父类打算本身消耗了 dispatchNestedFling(0, velocityY, canFling);
// 本身处理 fling(velocityY); } }
下面继续看 fling 的实现。
public void fling(int velocityY) { if (getChildCount() > 0) { mScroller.fling(getScrollX(), getScrollY(), // start 0, velocityY, // velocities 0, 0, // x Integer.MIN_VALUE, Integer.MAX_VALUE, // y 0, 0); // overscroll runAnimatedScroll(true); } } private void runAnimatedScroll(boolean participateInNestedScrolling) { if (participateInNestedScrolling) { // fling 其实也是一种滚动,只不过是非接触的 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH); } else { stopNestedScroll(ViewCompat.TYPE_NON_TOUCH); } mLastScrollerY = getScrollY(); ViewCompat.postInvalidateOnAnimation(this); }
最终会触发重绘操做,重绘过程当中会调用 computeScroll,下面看下其内部的代码逻辑。
@Override public void computeScroll() { if (mScroller.isFinished()) { return; } mScroller.computeScrollOffset(); final int y = mScroller.getCurrY(); int unconsumed = y - mLastScrollerY; mLastScrollerY = y; // Nested Scrolling Pre Pass mScrollConsumed[1] = 0;
// 滚动的时候,依然会把当前的未消耗的滚动距离传给嵌套父View dispatchNestedPreScroll(0, unconsumed, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH); unconsumed -= mScrollConsumed[1]; final int range = getScrollRange(); if (unconsumed != 0) { // Internal Scroll final int oldScrollY = getScrollY();
// 本身消耗 overScrollByCompat(0, unconsumed, getScrollX(), oldScrollY, 0, range, 0, 0, false); final int scrolledByMe = getScrollY() - oldScrollY; unconsumed -= scrolledByMe; // Nested Scrolling Post Pass mScrollConsumed[1] = 0;
// 继续上传给父View dispatchNestedScroll(0, scrolledByMe, 0, unconsumed, mScrollOffset, ViewCompat.TYPE_NON_TOUCH, mScrollConsumed); unconsumed -= mScrollConsumed[1]; } // 若是到这里有未消耗的,说明已经滚动到边缘了 if (unconsumed != 0) { final int mode = getOverScrollMode(); final boolean canOverscroll = mode == OVER_SCROLL_ALWAYS || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); if (canOverscroll) { ensureGlows(); if (unconsumed < 0) { if (mEdgeGlowTop.isFinished()) { mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); } } else { if (mEdgeGlowBottom.isFinished()) { mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); } } }
// 中止滚动 abortAnimatedScroll(); } // 若是此时滚动还未结束,而且当前的滑动距离都被消耗了,那么继续刷新滚动,直到中止为止 if (!mScroller.isFinished()) { ViewCompat.postInvalidateOnAnimation(this); } }
到这里,关于 Ns 的嵌套滑动就讲完了。但愿你们可以对嵌套滑动有个理解。
阅读 Ns 的源码,可让你更好的理解嵌套滑动,以及事件分发的逻辑。