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 事件的時候調用的 |