NestedScrolling機制原理解析

簡介

NestedScrolling,包含在android.support.v4包中,由 22.10 版本開始引入,支持 5.0 及 5.0 以上的系統。java

NestedScrolling,簡稱嵌套滑動,可主要分爲NestedScrollingParen和NestedScrollingChild兩部分,使用它能夠實現一些很是絢麗的效果。android

Google 幫咱們封裝好了一些相應的空間,好比 RecyclerView 實現了 NestedScrollingChild 接口,CoordinatorLayout 實現了 NestedScrollingParent 接口,NestedScrollingViewSwipeRefreshLayout 實現了 NestedScrollingChild,NestedScrollingParent 接口等。ide

那麼相比較於傳統的事件分發機制,NetstedScroll 機制有什麼特色呢?oop

在傳統的事件分發機制 中,一旦某個 View 或者 ViewGroup 消費了事件,就很難將事件交給父 View 進行共同處理。而 NestedScrolling 機制很好地幫助咱們解決了這一問題。咱們只須要按照規範實現相應的接口便可,子 View 實現 NestedScrollingChild,父 View 實現 NestedScrollingParent ,經過 NestedScrollingChildHelper 或者 NestedScrollingParentHelper 完成交互。post

 

NestedScrolling機制的原理

NestedScrolling 總體主要包含四個類:this

  • NestedScrollingParent

在嵌套滑動中,若是父View 想實現 嵌套滑動,要實現這個 NestedScrollingParent 藉口,與 NestedScrollingChild 大概有一一對應的關係。spa

  • NestedScrollingChild

在嵌套滑動中,若是scrolling child 想實現嵌套滑動,必須實現這個藉口代理

  • NestedScrollingChildHelper

實現 Child 和 Parent 交互的邏輯code

  • NestedScrollingParentHelper

實現 Child 和 Parent 交互的邏輯blog

它的處理流程大體以下:

  1. scrolling child 在滑動以前,會經過 NestedScrollingChildHelper 查找是否有響應的 scrolling parent,若是有的話,會先詢問scrolling parent 是否須要先於scrolling child 滑動,若是須要的話,scrolling parent 進行相應的滑動,並消費必定的距離;
  2. 接着scrolling child 進行相應的滑動,並消耗必定的距離值 dx,dy;
  3. scrolling child 滑動完以後,詢問scrolling parent 是否還須要繼續進行滑動,須要的話,進行相應的處理;
  4. 滑動結束以後,Scrolling child 會中止滑動,並經過 NestedScrollingChildHelper 通知相應的 Scrolling Parent 中止滑動。

NestedScrollingChild 相關方法

目前已實現改接口的類包括: HorizontalGridView, NestedScrollView, RecyclerView, SwipeRefreshLayout, VerticalGridView

  • boolean startNestedScroll(int axes)

在開始滑動的時候會調用這個方法,axes 表明滑動的方向:ViewCompat.SCROLL_AXIS_HORIZONTAL 表明水平滑動,ViewCompat.SCROLL_AXIS_VERTICAL 表明垂直滑動。返回值是布爾類型的,根據返回值,咱們能夠判斷是否找到支持嵌套滑動的父View ,返回 true,表示在scrolling parent (須要注意的是這裏不必定是直接scrolling parent ,間接scrolling parent 也可會返回 true) 中找到支持嵌套滑動的。反之,則找不到。

  • boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow)

在scrolling child 滑動以前,提供機會讓scrolling parent 先於scrolling child滑動。

dx,dy 是輸入參數,表示scrolling child 傳遞給 scrolling parent 水平方向,垂直方向上的偏移量,consumed 是輸出參數,consumed[0] 表示父 View 在水平方向上消費的值,,consumed[1 表示父 View 在垂直方向上消費的值。

返回值也是布爾類型的,根據這個值 ,咱們能夠判斷scrolling parent 是都消費了相應距離 。

  • boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow)

在scrolling child 滑動以後,調用這個方法,提供機會給scrolling parent 滑動,dxConsumed,dyConsumed 是輸入參數,表示scrolling child 在水平方向,垂直方向消耗的值,dxUnconsumed,dyUnconsumed 也是輸入參數,表示scrolling child 在水平方向,垂直方向未消耗的值。

  • boolean dispatchNestedPreFling(float velocityX, float velocityY, boolean consumed)

調用這個方法,在scrolling child 處理 fling 動做以前,提供機會scrolling parent 先於scrolling child 處理 fling 動做。

三個參數都是輸入參數,velocityX 表示水平方向的速度,velocityY 表示垂直方向感的速度,consumed 表示scrolling child 是否消費 fling 動做 。返回值也是布爾類型的,表示scrolling parent 是否有消費了fling 動做或者對 fling 動做作出相應的 處理。true 表示有,false 表示沒有。

  • boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed)

在 Scrolling child 處理 fling 動做以後,提供機會給 Scrolling Parent 處理 fling 動做。各個參數的意義這裏就再也不意義闡述了,跟 dispatchNestedFling 參數的意義是同樣的。

  • void stopNestedScroll

當滑動取消或中止的時候,會調用這個方法。例如在 RecyclerView 中,當 ACTION_UP 或者 ACTION_CANCEL 或者 item 消費了 Touch 事件的時候,會調用這個方法。

NestedScrollingParent主要方法

目前已實現改接口的類包括: CoordinatorLayout, NestedScrollView, SwipeRefreshLayout。它一般是配合 NestedScrollingChild 進行嵌套滑動的。

  • boolean onStartNestedScroll(View child, View target, int nestedScrollAxes)

在 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 表示滑動方向。

  • void onNestedScrollAccepted(View child, View target, int nestedScrollAxes)

若是 Scrolling Parent 的onStartNestedScroll 返回 true, Scrolling parent 的 onNestedScrollAccepted(View child, View target, int nestedScrollAxes) 方法會被回調。

  • boolean onNestedPreScroll(View target, int dx, int dy, int[] consumed)

在 Scrolling Child 進行滑動以前,Scrolling Parent 能夠先於Scrolling Child 進行相應的處理

若是 Scrolling Child 調用 dispatchNestedPreFling(float velocityX, float velocityY) ,經過 NestedScrollingChildHelper 會回調 Scrolling parent 的 onNestedPreScroll 方法

接下來的幾個方法,咱們不一一介紹了。與 Scrolling Child 方法幾乎是一一對應的。

NetsedScrollingChildHelper相關方法

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;
    }
  1. 第一步,判斷 P 是否爲空,不爲空, 從 P (初始值是RecyclerView 的直接父 View) 開始找起,判斷其是否支持嵌套滑動,若支持,返回true;
  2. 第二步:若 P 不支持嵌套滑動,再將 p 指向 p.getParent(),循環第一步;
  3. 第三步:若循環了全部的 P ,都找不到支持嵌套滑動的 View,返回 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);
    }

 

執行流程

  1. 在 ACTION_DOWN 時,Scrolling Child 會調用 startNestedScroll 方法,經過 childHelper 回調 Scrolling Parent 的 startNestedScroll 方法;

  2. 在 AACTION_MOVE 時,Scrolling Child 要開始滑動的時候,會調用dispatchNestedPreScroll 方法,經過 NestedScrollingChildHelper 詢問 Scrolling Parent 是否要先於 Child 進行 滑動,若須要的話,會調用 Parent 的 onNestedPreScroll 方法,協同 ChildView 一塊兒進行滑動;
  3. 當 Scrolling Child 滑動完成的時候,會調用 dispatchNestedScroll 方法,經過 ChildHelper 詢問 Scrolling Parent 是否須要進行滑動,須要的話,會 調用 Scrolling Parent 的 onNestedScroll 方法
  4. 在 ACTION_DOWN,ACTION_MOVE 的時候,會調用 Scrolling Child 的stopNestedScroll ,經過 NestedScrollingChildHelper 調用 Scrolling parent 的 stopNestedScroll 方法。
  5. 若是須要處理 Fling 動做,咱們能夠經過 VelocityTrackerCompat 得到相應的速度,並在 ACTION_UP 的時候,調用 dispatchNestedPreFling 方法,經過 NestedScrollingChildHelper 詢問 Parent 是否須要先於 Child 進行 Fling 動做
  6. 在 Child 處理完 Fling 動做時候,若是 Scrolling Parent 還須要處理 Fling 動做,咱們能夠調用 dispatchNestedFling 方法,經過 ChildHelper ,調用 Parent 的 onNestedFling 方法

 

子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 事件的時候調用的
相關文章
相關標籤/搜索