NestedScrolling,包含在android.support.v4包中,由 22.10 版本开始引入,支持 5.0 及 5.0 以上的系统。java
NestedScrolling,简称嵌套滑动,可主要分为NestedScrollingParen和NestedScrollingChild两部分,使用它能够实现一些很是绚丽的效果。android
Google 帮咱们封装好了一些相应的空间,好比 RecyclerView 实现了 NestedScrollingChild 接口,CoordinatorLayout 实现了 NestedScrollingParent 接口,NestedScrollingView,SwipeRefreshLayout 实现了 NestedScrollingChild,NestedScrollingParent 接口等。ide
那么相比较于传统的事件分发机制,NetstedScroll 机制有什么特色呢?oop
在传统的事件分发机制 中,一旦某个 View 或者 ViewGroup 消费了事件,就很难将事件交给父 View 进行共同处理。而 NestedScrolling 机制很好地帮助咱们解决了这一问题。咱们只须要按照规范实现相应的接口便可,子 View 实现 NestedScrollingChild,父 View 实现 NestedScrollingParent ,经过 NestedScrollingChildHelper 或者 NestedScrollingParentHelper 完成交互。post
NestedScrolling 总体主要包含四个类:this
在嵌套滑动中,若是父View 想实现 嵌套滑动,要实现这个 NestedScrollingParent 借口,与 NestedScrollingChild 大概有一一对应的关系。spa
在嵌套滑动中,若是scrolling child 想实现嵌套滑动,必须实现这个借口代理
实现 Child 和 Parent 交互的逻辑code
实现 Child 和 Parent 交互的逻辑blog
它的处理流程大体以下:
目前已实现改接口的类包括: HorizontalGridView, NestedScrollView, RecyclerView, SwipeRefreshLayout, VerticalGridView
在开始滑动的时候会调用这个方法,axes 表明滑动的方向:ViewCompat.SCROLL_AXIS_HORIZONTAL 表明水平滑动,ViewCompat.SCROLL_AXIS_VERTICAL 表明垂直滑动。返回值是布尔类型的,根据返回值,咱们能够判断是否找到支持嵌套滑动的父View ,返回 true,表示在scrolling parent (须要注意的是这里不必定是直接scrolling parent ,间接scrolling parent 也可会返回 true) 中找到支持嵌套滑动的。反之,则找不到。
在scrolling child 滑动以前,提供机会让scrolling parent 先于scrolling child滑动。
dx,dy 是输入参数,表示scrolling child 传递给 scrolling parent 水平方向,垂直方向上的偏移量,consumed 是输出参数,consumed[0] 表示父 View 在水平方向上消费的值,,consumed[1 表示父 View 在垂直方向上消费的值。
返回值也是布尔类型的,根据这个值 ,咱们能够判断scrolling parent 是都消费了相应距离 。
在scrolling child 滑动以后,调用这个方法,提供机会给scrolling parent 滑动,dxConsumed,dyConsumed 是输入参数,表示scrolling child 在水平方向,垂直方向消耗的值,dxUnconsumed,dyUnconsumed 也是输入参数,表示scrolling child 在水平方向,垂直方向未消耗的值。
调用这个方法,在scrolling child 处理 fling 动做以前,提供机会scrolling parent 先于scrolling child 处理 fling 动做。
三个参数都是输入参数,velocityX 表示水平方向的速度,velocityY 表示垂直方向感的速度,consumed 表示scrolling child 是否消费 fling 动做 。返回值也是布尔类型的,表示scrolling parent 是否有消费了fling 动做或者对 fling 动做作出相应的 处理。true 表示有,false 表示没有。
在 Scrolling child 处理 fling 动做以后,提供机会给 Scrolling Parent 处理 fling 动做。各个参数的意义这里就再也不意义阐述了,跟 dispatchNestedFling 参数的意义是同样的。
当滑动取消或中止的时候,会调用这个方法。例如在 RecyclerView 中,当 ACTION_UP 或者 ACTION_CANCEL 或者 item 消费了 Touch 事件的时候,会调用这个方法。
目前已实现改接口的类包括: CoordinatorLayout, NestedScrollView, SwipeRefreshLayout。它一般是配合 NestedScrollingChild 进行嵌套滑动的。
在 Scrolling Child 开始滑动的时候会调用这个方法
当 Scrolling Child 调用 onStartNestedScroll 方法的时候,经过 NestedScrollingChildHelper 会回调 Scrolling parent 的 onStartNestedScroll 方法,若是返回 true, Scrolling parent 的 onNestedScrollAccepted(View child, View target, int nestedScrollAxes) 方法会被回调。
target 表示发起滑动事件的 View,Child 是 ViewParent 的直接子View,包含 target,nestedScrollAxes 表示滑动方向。
若是 Scrolling Parent 的onStartNestedScroll 返回 true, Scrolling parent 的 onNestedScrollAccepted(View child, View target, int nestedScrollAxes) 方法会被回调。
在 Scrolling Child 进行滑动以前,Scrolling Parent 能够先于Scrolling Child 进行相应的处理
若是 Scrolling Child 调用 dispatchNestedPreFling(float velocityX, float velocityY) ,经过 NestedScrollingChildHelper 会回调 Scrolling parent 的 onNestedPreScroll 方法
接下来的几个方法,咱们不一一介绍了。与 Scrolling Child 方法几乎是一一对应的。
RecyclerView实现了NestedScrollingChild接口,所以咱们以RecyclerView为例,详细探究NetsedScrollingChildHelper的具体应用
public RecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { ... mScrollingChildHelper = new NestedScrollingChildHelper(this); setNestedScrollingEnabled(true); } ... @Override public void setNestedScrollingEnabled(boolean enabled) { mScrollingChildHelper.setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return mScrollingChildHelper.isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return mScrollingChildHelper.startNestedScroll(axes); } @Override public void stopNestedScroll() { mScrollingChildHelper.stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return mScrollingChildHelper.hasNestedScrollingParent(); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return mScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return mScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return mScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY); } ...
从代码中能够看到,RecyclerView充当了一个代理的角色,它的不少逻辑实际上是交给 NestedScrollingChildHelper 去帮助其完成的,下面咱们一块儿来看一下 NestedScrollingChildHelper 里的方法
/** * Start a new nested scroll for this view. * * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass * method/{@link NestedScrollingChild} interface method with the same signature to implement * the standard policy.</p> * * @param axes Supported nested scroll axes. * See {@link NestedScrollingChild#startNestedScroll(int)}. * @return true if a cooperating parent view was found and nested scrolling started successfully */ public boolean startNestedScroll(int axes) { if (hasNestedScrollingParent()) { // Already in progress return true; } if (isNestedScrollingEnabled()) { ViewParent p = mView.getParent(); View child = mView; while (p != null) { if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) { mNestedScrollingParent = p; ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes); return true; } if (p instanceof View) { child = (View) p; } p = p.getParent(); } } return false; }
/** * Dispatch one step of a nested 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 NestedScrollingChild} 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 dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { 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]; } ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); 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; }
当childView已发生滑动时,首先获取childView在屏幕上的位置并记录X, Y坐标,由于上一步在startNestedScroll 方法中已完成对 mNestedScrollingParent的初始化,在这里调用 ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
dyConsumed, dxUnconsumed,dyUnconsumed),最后从新获取滑动后的childView在屏幕上的位置,并将childView左上角的X,Y轴坐标从新赋值为当前位置与初始位置之差;当childView未发生滑动时,直接将childView左上角的X,Y轴坐标赋值为0。
看完了上面的两个主要方法,咱们能够得出这样的一个结论:当咱们调用 Scrolling Child 的 onStartNested 方法的时候,会经过 ChildHelper 去寻找是否有相应的 Scrolling Parent,若是有的话,会 回调相应的方法。同理 dispatchNestedPreScroll,dispatchNestedScroll,dispatchNestedPreFling 一样如此。
public boolean onTouchEvent(MotionEvent e) { ... // 若是 Item 处理了 Touch 事件,直接返回 true ,在在处理 if (dispatchOnItemTouch(e)) { cancelTouch(); return true; } if (mLayout == null) { return false; } ... switch (action) { case MotionEvent.ACTION_DOWN: { mScrollPointerId = e.getPointerId(0); mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f); mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f); int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE; if (canScrollHorizontally) { nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL; } if (canScrollVertically) { nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL; } // 在 Action_Down 的时候 调用 startNestedScroll startNestedScroll(nestedScrollAxis); } break; case MotionEvent.ACTION_MOVE: { ... // 在 Action_move 的时候,回调 dispatchNestedPreScroll 方法 if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) { // 减去 Scrolling Parent 的消费的值 dx -= mScrollConsumed[0]; dy -= mScrollConsumed[1]; vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); // Updated the nested offsets mNestedOffsets[0] += mScrollOffset[0]; mNestedOffsets[1] += mScrollOffset[1]; } ... if (mScrollState == SCROLL_STATE_DRAGGING) { mLastTouchX = x - mScrollOffset[0]; mLastTouchY = y - mScrollOffset[1]; // 在 scrollByInternal 方法里面会回调 onNestedScroll 方法 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; case MotionEvent.ACTION_UP: { ... // 在 fling 方法里面会回调 onNestedPreFling dispatchNestedFling 等方法 if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) { setScrollState(SCROLL_STATE_IDLE); } // 在resetTouch方法中调用 onStopScroll 方法 resetTouch(); } break; case MotionEvent.ACTION_CANCEL: { // 在 cancelTouch中经过调用 resetTouch 调用 onStopScroll 方法 cancelTouch(); } break; } if (!eventAddedToVelocityTracker) { mVelocityTracker.addMovement(vtev); } vtev.recycle(); return true; }
private void resetTouch() { if (mVelocityTracker != null) { mVelocityTracker.clear(); } stopNestedScroll(); releaseGlows(); } private void cancelTouch() { resetTouch(); setScrollState(SCROLL_STATE_IDLE); }
在 ACTION_DOWN 时,Scrolling Child 会调用 startNestedScroll 方法,经过 childHelper 回调 Scrolling Parent 的 startNestedScroll 方法;
子View | 父View | 方法描述 |
---|---|---|
startNestedScroll | onStartNestedScroll、onNestedScrollAccepted | Scrolling Child 开始滑动的时候,通知 Scrolling Parent 要开始滑动了,一般是在 Action_down 动做 的时候调用这个方法 |
dispatchNestedPreScroll | onNestedPreScroll | 在 Scrolling Child 要开始滑动的时候,询问 Scrolling Parent 是否先于 Scrolling Child 进行相应的处理,同时是在 Action_move 的时候调用 |
dispatchNestedScroll | onNestedScroll | 在 Scrolling Child 滑动后会询问 Scrolling Parent 是否须要继续滑动 |
dispatchNestedPreFling | onNestedPreFling | 在 Scrolling Child 开始处理 Fling 动做的时候,询问 Scrolling Parent 是否须要先处理 Fling 动做 |
dispatchNestedFling | onNestedFling | 在 Scrolling Child 处理 Fling 动做完毕的时候,询问 Scrolling Parent 是都还须要进行相应的处理 |
stopNestedScroll | onStopNestedScroll | 在 Scrolling Child 中止滑动的时候,会调用 Scrolling Parent 的这个方法。一般是在 Action_up 或者 Action_cancel 或者被别的 View 消费 Touch 事件的时候调用的 |