自定義View事件之進階篇(一)-NestedScrolling(嵌套滑動)機制

最近一段時間,一直都在忙於找工做。雖然花費了三個月的時間,可是結果並非很美滿。想去大廠、想去好公司、想碰見更厲害的人的願望仍是沒有實現。或許是本身不夠強大,或許本身不夠努力,或許須要必定運氣。生活老是須要經歷一些波折。沒有誰老是能一路順風。接下來一段時間內,會繼續更新文章。但願你們能繼續關注。Thanks~java

前言

在Lollipop(Android 5.0)時,谷歌推出了NestedScrolling機制,也就是嵌套滑動。本文將帶領你們一塊兒去了解谷歌對該機制的設計。經過閱讀該文,你能瞭解以下知識點:git

  • 傳統事件分發機制中嵌套滑動的實現與侷限性。
  • 谷歌NestedScrolling機制的原理實現。
  • NestedScrollingChild與NestedScrollingParent接口的調用關係。
  • NestedScrollingChild2與NestedScrollingParent2接口出現的意義。

該博客中涉及到的示例,在NestedScrollingDemo項目中都有實現,你們能夠按需自取。github

傳統事件機制處理嵌套滑動的侷限性

在傳統的事件分發機制中,當一個事件產生後,它的傳遞過程遵循以下順序:父控件->子控件,事件老是先傳遞給父控件,當父控件不對事件攔截的時候,那麼當前事件又會傳遞給它的子控件。一旦父控件須要攔截事件,那麼子控件是沒有機會接受該事件的。算法

所以當在傳統事件分發機制中,若是有嵌套滑動場景,咱們須要手動解決事件衝突。具體嵌套滑動例子以下圖所示:數組

例子分析

上述效果實現,請參看NestedTraditionLayout.javaapp

想要實現上圖效果,在傳統滑動機制中,咱們須要如下幾個步驟:ide

  • 咱們須要調用父控件中onInterceptTouchEvent方法來攔截向上滑動。
  • 當父控件攔截事件後,須要控制自身的onTouchEvent處理滑動事件,使其滑動至HeaderView隱藏。
  • 當HeaderView滑動至隱藏後,父控件就不攔截事件了,而是交給內部的子控件(RecyclerView或ListView)處理滑動事件。

使用傳統的事件攔截機制來處理嵌套滑動,咱們會發現一個問題,就是整個嵌套滑動是不連貫的。也就是當父控件滑動至HeaderView隱藏的時候,這個時候若是想要內部的(RecyclerView或ListView)處理滑動事件。只有擡起手指,從新向上滑動。函數

熟悉事件分發機制的朋友應該知道,之因此產生不連貫的緣由,是由於父控件攔截了事件,因此同一事件序列的事件,仍然會傳遞給父控件,也就會調用其onTouchEvent方法。而不是調用子控件的onTouchEvent方法。post

NestedScrolling機制簡介

爲了實現連貫的嵌套滑動,谷歌在Lollipop(Android 5.0)時,推出了NestedScrolling機制。該機制並無脫離傳統的事件分發機制,而是在原有的事件分發機制之上,爲系統的自帶的ViewGroup和View都增長了手勢滑動與處理fling的方法。同時爲了兼容低版本(5.0如下,View與ViewGroup是沒有對應的API),谷歌也在support v4包中也提供了以下類與接口進行支撐:ui

父控件須要實現的接口與使用到的類:

  • NestedScrollingParent(接口)
  • NestedScrollingParent2(也是接口並繼承NestedScrollingParent)
  • NestedScrollingParentHelper(類)

子控件須要實現的接口與使用到的類:

  • NestedScrollingChild(接口)
  • NestedScrollingChild2(也是接口並繼承NestedScrollingChild)
  • NestedScrollingChildHelper(類)

須要注意的是,若是你的Android平臺在5.0以上,那麼你能夠直接使用系統ViewGoup與View自帶的方法。可是爲了向下兼容,建議仍是使用support v4包提供的相應接口來實現嵌套滑動。下文也會着重講解這些接口的的使用方式與方法說明。

NestedScrollingParent與NestedScrollingChild接口介紹

在瞭解嵌套滑動具體的使用方式以前,咱們須要瞭解父控件與子控件對應接口中方法的說明。這裏你們能夠先忽略掉NestedScrollingParent2與NestedScrollingChild2接口,由於這兩個接口是爲了解決以前對嵌套滑動處理fling效果的Bug。因此對於目前階段的咱們只須要了解基礎的嵌套滑動規則就夠了。關於NestedScrollingParent2與NestedScrollingChild2接口相關的知識點,會在下文具體描述。那如今咱們就先看看基礎的接口的方法介紹吧。

NestedScrollingParent

若是採用接口的方式實現嵌套滑動,咱們須要父控件要實現NestedScrollingParent接口。接口具體方法以下:

/** * 有嵌套滑動到來了,判斷父控件是否接受嵌套滑動 * * @param child 嵌套滑動對應的父類的子類(由於嵌套滑動對於的父控件不必定是一級就能找到的,可能挑了兩級父控件的父控件,child的輩分>=target) * @param target 具體嵌套滑動的那個子類 * @param nestedScrollAxes 支持嵌套滾動軸。水平方向,垂直方向,或者不指定 * @return 父控件是否接受嵌套滑動, 只有接受了纔會執行剩下的嵌套滑動方法 */
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {}

    /** * 當onStartNestedScroll返回爲true時,也就是父控件接受嵌套滑動時,該方法纔會調用 */
    public void onNestedScrollAccepted(View child, View target, int axes) {}

    /** * 在嵌套滑動的子控件未滑動以前,判斷父控件是否優先與子控件處理(也就是父控件能夠先消耗,而後給子控件消耗) * * @param target 具體嵌套滑動的那個子類 * @param dx 水平方向嵌套滑動的子控件想要變化的距離 dx<0 向右滑動 dx>0 向左滑動 * @param dy 垂直方向嵌套滑動的子控件想要變化的距離 dy<0 向下滑動 dy>0 向上滑動 * @param consumed 這個參數要咱們在實現這個函數的時候指定,回頭告訴子控件當前父控件消耗的距離 * consumed[0] 水平消耗的距離,consumed[1] 垂直消耗的距離 好讓子控件作出相應的調整 */
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {}

    /** * 嵌套滑動的子控件在滑動以後,判斷父控件是否繼續處理(也就是父消耗必定距離後,子再消耗,最後判斷父消耗不) * * @param target 具體嵌套滑動的那個子類 * @param dxConsumed 水平方向嵌套滑動的子控件滑動的距離(消耗的距離) * @param dyConsumed 垂直方向嵌套滑動的子控件滑動的距離(消耗的距離) * @param dxUnconsumed 水平方向嵌套滑動的子控件未滑動的距離(未消耗的距離) * @param dyUnconsumed 垂直方向嵌套滑動的子控件未滑動的距離(未消耗的距離) */
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {}

    /** * 嵌套滑動結束 */
    public void onStopNestedScroll(View child) {}

    /** * 當子控件產生fling滑動時,判斷父控件是否處攔截fling,若是父控件處理了fling,那子控件就沒有辦法處理fling了。 * * @param target 具體嵌套滑動的那個子類 * @param velocityX 水平方向上的速度 velocityX > 0 向左滑動,反之向右滑動 * @param velocityY 豎直方向上的速度 velocityY > 0 向上滑動,反之向下滑動 * @return 父控件是否攔截該fling */
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {}


    /** * 當父控件不攔截該fling,那麼子控件會將fling傳入父控件 * * @param target 具體嵌套滑動的那個子類 * @param velocityX 水平方向上的速度 velocityX > 0 向左滑動,反之向右滑動 * @param velocityY 豎直方向上的速度 velocityY > 0 向上滑動,反之向下滑動 * @param consumed 子控件是否能夠消耗該fling,也能夠說是子控件是否消耗掉了該fling * @return 父控件是否消耗了該fling */
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {}

    /** * 返回當前父控件嵌套滑動的方向,分爲水平方向與,垂直方法,或者不變 */
    public int getNestedScrollAxes() {}
複製代碼

NestedScrollingChild接口介紹

若是採用接口的方式實現嵌套滑動,子控件須要實現NestedScrollingChild接口。接口具體方法以下:

/** * 開啓一個嵌套滑動 * * @param axes 支持的嵌套滑動方法,分爲水平方向,豎直方向,或不指定 * @return 若是返回true, 表示當前子控件已經找了一塊兒嵌套滑動的view */
    public boolean startNestedScroll(int axes) {}

    /** * 在子控件滑動前,將事件分發給父控件,由父控件判斷消耗多少 * * @param dx 水平方向嵌套滑動的子控件想要變化的距離 dx<0 向右滑動 dx>0 向左滑動 * @param dy 垂直方向嵌套滑動的子控件想要變化的距離 dy<0 向下滑動 dy>0 向上滑動 * @param consumed 子控件傳給父控件數組,用於存儲父控件水平與豎直方向上消耗的距離,consumed[0] 水平消耗的距離,consumed[1] 垂直消耗的距離 * @param offsetInWindow 子控件在當前window的偏移量 * @return 若是返回true, 表示父控件已經消耗了 */
    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) {}


    /** * 當父控件消耗事件後,子控件處理後,又繼續將事件分發給父控件,由父控件判斷是否消耗剩下的距離。 * * @param dxConsumed 水平方向嵌套滑動的子控件滑動的距離(消耗的距離) * @param dyConsumed 垂直方向嵌套滑動的子控件滑動的距離(消耗的距離) * @param dxUnconsumed 水平方向嵌套滑動的子控件未滑動的距離(未消耗的距離) * @param dyUnconsumed 垂直方向嵌套滑動的子控件未滑動的距離(未消耗的距離) * @param offsetInWindow 子控件在當前window的偏移量 * @return 若是返回true, 表示父控件又繼續消耗了 */
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {}

    /** * 子控件中止嵌套滑動 */
    public void stopNestedScroll() {}


    /** * 當子控件產生fling滑動時,判斷父控件是否處攔截fling,若是父控件處理了fling,那子控件就沒有辦法處理fling了。 * * @param velocityX 水平方向上的速度 velocityX > 0 向左滑動,反之向右滑動 * @param velocityY 豎直方向上的速度 velocityY > 0 向上滑動,反之向下滑動 * @return 若是返回true, 表示父控件攔截了fling */
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {}

    /** * 當父控件不攔截子控件的fling,那麼子控件會調用該方法將fling,傳給父控件進行處理 * * @param velocityX 水平方向上的速度 velocityX > 0 向左滑動,反之向右滑動 * @param velocityY 豎直方向上的速度 velocityY > 0 向上滑動,反之向下滑動 * @param consumed 子控件是否能夠消耗該fling,也能夠說是子控件是否消耗掉了該fling * @return 父控件是否消耗了該fling */
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {}

    /** * 設置當前子控件是否支持嵌套滑動,若是不支持,那麼父控件是不可以響應嵌套滑動的 * * @param enabled true 支持 */
    public void setNestedScrollingEnabled(boolean enabled) {}

    /** * 當前子控件是否支持嵌套滑動 */
    public boolean isNestedScrollingEnabled() {}

    /** * 判斷當前子控件是否擁有嵌套滑動的父控件 */
    public boolean hasNestedScrollingParent() {}
複製代碼

谷歌嵌套滑動的方法調用設計

經過上文,我相信你們大概基本瞭解了NestedScrollingParent與NestedScrollingChild兩個接口方法的做用,可是咱們並不知道這些方法之間對應的關係與調用的時機。那麼如今咱們一塊兒來分析谷歌對整個嵌套滑動過程的實現與設計。爲了處理嵌套滑動,谷歌將整個過程分爲了如下幾個步驟:

  • 1.當父控件不攔截事件,子控件收到滑動事件後,會先詢問父控件是否支持嵌套滑動。
  • 2.若是父控件支持嵌套滑動,那麼父控件進行預先滑動。而後將處理剩餘的距離交由給子控件處理。
  • 3.子控件收到父控件剩餘的滑動距離並滑動結束後,若是滑動距離還有剩餘,又會再問一下父控件是否須要再繼續消耗剩下的距離。
  • 4.若是子控件產生了fling,會先詢問父控件是否預先攔截fling。若是父控件預先攔截。則交由給父控件處理。子控件則不處理fling
  • 5.若是父控件不預先攔截fling, 那麼會將fling傳給父控件處理。同時子控件也會處理fling。
  • 6.當整個嵌套滑動結束時,子控件通知父控件嵌套滑動結束。

對fling效果不熟悉的小夥伴能夠查看該篇文章---RecyclerView之Scroll和Fling

再結合以前咱們對NestedScrollingParent與NestedScrollingChild中的方法。咱們能夠獲得相應方法之間的調用關係。具體以下圖所示:

方法對應關係

子控件方法調用時機

當咱們瞭解了接口的調用關係後,咱們須要知道子控件對相應嵌套滑動方法的調用時機。由於在低版本下,子控件向父控件傳遞事件須要配合NestedScrollingChildHelper類與NestedScrollingChild接口一塊兒使用。因爲篇幅的限制。這裏就不向你們介紹如何構造一個支持嵌套滑動的子控件了。在接下來的知識點中都會在NestedScrollingChildView 的基礎上進行講解。但願你們能夠結合代碼與博客一塊兒理解。

在接下來的章節中,會先講解谷歌在NestedScrollingParent與NestedScrollingChild接口下嵌套滑動的API設計。關於NestedScrollingParent2與NestedScrollingChild2接口會單獨進行解釋。

子控件startNestedScroll方法調用時機

根據嵌套滑動的機制設定,子控件若是想要將事件傳遞給父控件,那麼父控件是不能攔截事件的。當子控件想要將事件交給父控件進行預處理,那麼必然會在其onTouchEvent方法,將事件傳遞給父控件。須要注意的是當子控件調用startNestedScroll方法時,只是判斷是否有支持嵌套滑動的父控件,並通知父控件嵌套滑動開始。這個時候並無真正的傳遞相應的事件。故該方法只能在子控件的onTouchEvent方法中事件爲MotionEvent.ACTION_DOWN時調用。僞代碼以下所示:

public boolean onTouchEvent(MotionEvent event) {
        int action = event.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                mLastX = x;
                mLastY = y;
                //查找嵌套滑動的父控件,並通知父控件嵌套滑動開始。這裏默認是設置的豎直方向
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
                break;
            }
        }
        return super.onTouchEvent(event);
    }
複製代碼

那子view僅僅經過startNestedScroll方法是如何找到父控件並通知父控件嵌套滑動開始的呢?咱們來看看startNestedScroll方法的具體實現,startNestedScroll方法內部會調用NestedScrollingChildHelper的startNestedScroll方法。具體代碼以下所示:

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
        if (hasNestedScrollingParent(type)) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {//判斷子控件是否支持嵌套滑動
            //獲取當前的view的父控件
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
                 //判斷當前父控件是否支持嵌套滑動
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                    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方法。咱們繼續查看該方法:

public static boolean onStartNestedScroll(ViewParent parent, View child, View target, int nestedScrollAxes, int type) {
        if (parent instanceof NestedScrollingParent2) {//判斷父控件是否實現NestedScrollingParent2
            // First try the NestedScrollingParent2 API
            return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
                    nestedScrollAxes, type);
        } else if (type == ViewCompat.TYPE_TOUCH) {//若是父控件實現NestedScrollingParent
            // Else if the type is the default (touch), try the NestedScrollingParent API
            return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes);
        }
        return false;
    }
複製代碼

觀察代碼,咱們能夠發現,當父控件實現NestedScrollingParent接口後,會走IMPL.onStartNestedScroll方法,咱們繼續跟下去:

public boolean onStartNestedScroll(ViewParent parent, View child, View target, int nestedScrollAxes) {
            if (parent instanceof NestedScrollingParent) {
                return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
                        nestedScrollAxes);
            }
            return false;
        }
複製代碼

最後會調用ViewParetCompat中的onStartNestedScroll方法,該方法最終會調用父控件的onStartNestedScroll方法。繞了一大圈,也就調用了父控件的onStartNestedScroll來判斷是否支持嵌套滑動。

那如今咱們再回到子控件的startNestedScroll方法中。咱們能夠得知,若是當前父控件不支持嵌套滑動,那麼會一直向上尋找,直到找到爲止。若是仍然沒有找到,那麼接下來的子父控件的嵌套滑動方法都不會調用。若是子控件找到了支持嵌套滑動的父控件,那麼接下來會調用父控件的onNestedScrollAccepted方法,表示父控件接受嵌套滑動。

子控件dispatchNestedPreScroll方法調用時機

當父控件接受嵌套滑動後,那麼子控件須要將手勢滑動傳遞給父控件,由於這裏已經產生了滑動,故會在onTouchEvent中篩選MotionEvent.ACTION_MOVE中的事件,而後調用dispatchNestedPreScroll方法這些將滑動事件傳遞給父控件。僞代碼以下所示:

private final int[] mScrollConsumed = new int[2];//記錄父控件消耗的距離

    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_MOVE: {
                int dy = mLastY - y;
                int dx = mLastX - x;
                //將事件傳遞給父控件,並記錄父控件消耗的距離。
                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
                    dx -= mScrollConsumed[0];
                    dy -= mScrollConsumed[1];
                }
            }
        }

        return super.onTouchEvent(event);
    }
複製代碼

在上述代碼中,dy與dx分別爲子控件豎直與水平方向上的距離,int[] mScrollConsumed豎直用於記錄父控件消耗的距離。那麼當咱們調用dispatchNestedPreScroll的方法,將事件傳遞給父控件進行消耗時,那麼子控件實際能處理的距離爲:

  • 水平方向: dx -= mScrollConsumed[0];
  • 豎直方向: dy -= mScrollConsumed[1];

接下來,咱們繼續查看dispatchNestedPreScroll的方法。

在dispatchNestedPreScroll方法內部會調用NestedScrollingChildHelper的dispatchNestedPreScroll方法具體代碼以下:

public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, @NestedScrollType int type) {
        if (isNestedScrollingEnabled()) {
            //獲取當前嵌套滑動的父控件,若是爲null,直接返回
            final ViewParent parent = getNestedScrollingParentForType(type);
            if (parent == null) {
                return false;
            }

            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;
                //調用父控件的onNestedPreScroll處理事件
                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;
    }
複製代碼

在該方法中,會先判斷獲取當前嵌套滑動的父控件。若是父控件不爲null且支持嵌套滑動,那麼接下來會調用ViewParentCompat.onNestedPreScroll()方法。代碼以下所示:

public void onNestedPreScroll(ViewParent parent, View target, int dx, int dy, int[] consumed) {
            if (parent instanceof NestedScrollingParent) {
                ((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed);
            }
        }

複製代碼

觀察代碼最終會調用父控件的onNestedPreScroll方法。須要注意的是,父控件可能會將子控件傳遞的滑動事件所有消耗。那麼子控件就沒有繼續可處理的事件了。

onNestedPreScroll方法在嵌套滑動時判斷父控件的滑動距離時尤其重要。

子控件dispatchNestedScroll方法調用時機

當父控件預先處理滑動事件後,也就是調用onNestedPreScroll方法並把消耗的距離傳遞給子控件後,子控件會獲取剩下的事件並消耗。若是子控件仍然沒有消耗完,那麼會調用dispatchNestedScroll將剩下的事件傳遞給父控件。若是父控件不處理。那麼又會傳遞給子控件進行處理。僞代碼以下所示:

private final int[] mScrollConsumed = new int[2];//記錄父控件消耗的距離

    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_MOVE: {
                int dy = mLastY - y;
                int dx = mLastX - x;
                //將事件傳遞給父控件,並記錄父控件消耗的距離。
                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
                    dx -= mScrollConsumed[0];
                    dy -= mScrollConsumed[1];
                    scrollNested(dx,dy);//處理嵌套滑動
                }
            }
        }

        return super.onTouchEvent(event);
    }
    //處理嵌套滑動
    private void scrollNested(int x, int y) {
        int unConsumedX = 0, unConsumedY = 0;
        int consumedX = 0, consumedY = 0;

        //子控件消耗多少事件,由本身決定
        if (x != 0) {
            consumedX = childConsumeX(x);
            unConsumedX = x - consumedX;
        }
        if (y != 0) {
            consumedY = childConsumeY(y);
            unConsumedY = y - consumedY;
        }

        //子控件處理事件
        childScroll(consumedX, consumedY);

        //子控件處理後,又將剩下的事件傳遞給父控件
        if (dispatchNestedScroll(consumedX, consumedY, unConsumedX, unConsumedY, mScrollOffset)) {
            //傳給父控件處理後,剩下的邏輯本身實現
        }
        //傳遞給父控件,父控件不處理,那麼子控件就繼續處理。
        childScroll(unConsumedX, unConsumedY);

    }
    /** * 子控件滑動邏輯 */
    private void childScroll(int x, int y) {
        //子控件怎麼滑動,本身實現
    }
    /** * 子控件水平方向消耗多少距離 */
    private int childConsumeX(int x) {
        //具體邏輯由本身實現
        return 0;
    }
    /** * 子控件豎直方向消耗距離 */
    private int childConsumeY(int y) {
        //具體邏輯由本身實現
        return 0;
    }
複製代碼

在上述代碼中,由於子控件消耗多少距離,是由子控件進行決定的,因此將這些方法抽象了出來了。在子控件的dispatchNestedScroll方法內部會調用NestedScrollingChildHelper的dispatchNestedScroll方法,具體代碼以下所示:

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type) {
        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];
                }
                //調用父控件的onNestedScroll方法。
                ViewParentCompat.onNestedScroll(parent, mView, dxConsumed,
                        dyConsumed, dxUnconsumed, dyUnconsumed, type);

                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;
    }
複製代碼

該方法內部會調用ViewParentCompat.onNestedScroll方法。繼續跟蹤最終會調用ViewParentCompat中非靜態的的onNestedScroll方法,代碼以下所示:

public void onNestedScroll(ViewParent parent, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
            if (parent instanceof NestedScrollingParent) {
                ((NestedScrollingParent) parent).onNestedScroll(target, dxConsumed, dyConsumed,
                        dxUnconsumed, dyUnconsumed);
            }
        }
複製代碼

該方法中,最終會調用父控件的onNestedScroll方法來處理子控件剩餘的距離。

子控件stopNestedScroll方法調用時機

當整個事件序列結束的時候(當手指擡起或取消滑動的時候),須要通知父控件嵌套滑動已經結束。故咱們須要在OnTouchEvent中篩選MotionEvent.ACTION_UP、MotionEvent.ACTION_CANCEL中的事件,並經過stopNestedScroll()方法通知父控件。僞代碼以下所示:

public boolean onTouchEvent(MotionEvent event) {

        int action = event.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_UP: {   //當手指擡起的時,結束事件傳遞
                stopNestedScroll();
                break;
            }
            case MotionEvent.ACTION_CANCEL: {   //當手指擡起的時,結束事件傳遞
                stopNestedScroll();
                break;
            }
        }
        return super.onTouchEvent(event);
    }
複製代碼

在stopNestedScroll()方法中,最終會調用父控件的onStopNestedScroll()方法,這裏就不作更多的分析了。

子控件fling分發時機

如今就剩下最後一個嵌套滑動的方法了!!!對!就是fling。在瞭解子控件對fling的處理過程以前,咱們先要知道fling表明什麼樣的效果。在Android系統下,手指在屏幕上滑動而後鬆手,控件中的內容會順着慣性繼續往手指滑動的方向繼續滾動直到中止,這個過程叫作fling。也就是咱們須要在onTouchEvent方法中篩選MotionEvent.ACTION_UP的事件並獲取須要的滑動速度。僞代碼以下:

fling的中文意思爲拋、扔、擲。

public boolean onTouchEvent(MotionEvent event) {
         //添加速度檢測器,用於處理fling
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);

        int action = event.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_UP: {
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                int xvel = (int) mVelocityTracker.getXVelocity();
                int yvel = (int) mVelocityTracker.getYVelocity();  
             if (!dispatchNestedPreFling(velocityX, velocityY)) {
                boolean consumed = canScroll();
                //將fling效果傳遞給父控件
                dispatchNestedFling(velocityX, velocityY, consumed);
                 //而後子控件再處理fling
                childFling();//子控件本身實現怎麼處理fling
                stopNestedScroll();//子控件通知父控件滾動結束
              }
              stopNestedScroll();//通知父控件結束滑動
                break;
            }
        }
        return super.onTouchEvent(event);
    }
複製代碼

這裏就不在對fling效果是怎麼分發到父控件進行解釋啦~~。必定要結合NestedScrollingChildView類進行理解。那麼假設你們都看了源碼,那麼咱們能夠獲得以下幾點:

  • 子控件dispatchNestedPreFling最終會調用父控件的onNestedPreFling方法。
  • 子控件的dispatchNestedFling最終會調用onNestedFling方法。
  • 若是父控件的攔截fling(也就是onNestedPreFling方法返回爲true)。那麼子控件是沒有機會處理fling的。
  • 若是父控件攔截fling(也就是onNestedPreFling方法返回爲false),則父控件會調用onNestedFling方法與子控件同時處理fling。
  • 當父控件與子控件同時處理fling時,子控件會當即調用stopNestedScroll方法通知父控件嵌套滑動結束。

NestedScrollingChild2與NestedScrollingParent2簡介

最後一個知識點了,你們加油啊!!!!!!

在本文章前半部,咱們都是圍繞NestedScrollingChild與NestedScrollingParent進行講解。並無說起NestedScrollingChild2與NestedScrollingParent2接口。那這兩個接口是處理什麼的呢?這又要回到上文咱們提到的NestedScrollingChild處理fling時的流程了,在谷歌以前的NestedScrollingParent與NestedScrollingChild的API設計中。並無考慮以下問題:

  • 父控件根本不可能知道子控件是否fling結束。子控件只是在ACTION_UP中調用了stopNestedScroll方法。雖然通知了父控件結束嵌套滑動,可是子控件仍然可能處於fling中。
  • 子控件沒有辦法將部分fling傳遞給父控件。父控件必須處理整個fling。

而使用NestedScrollingChild2與NestedScrollingParent2這兩個接口,子控件就能將fling傳遞給父控件,而且父控件處理了部分fling後,又能夠將剩餘的fling再傳遞給子控件。當子控件中止fling時,通知父控件fling結束了。這和咱們以前分析的嵌套滑動是否是很像呢?直接講知識點,你們不是很好理解,看下面這個例子:

NestedScrollingParent效果展現

上述效果實現,請參看NestedScrollingParentLayout.java

在上面例子中是實現了NestedScrollingChild(NestedScrollView或RecyclerView等)與NestedScrollingParent接口的嵌套滑動,咱們能夠明顯的看出,當咱們手指快速向下滑動並擡起的時,子控件將fling分發給父控件,由於處理的距離不一樣,這個時候父控件已經處理滑動並fling結束,而內部的子控件(RecyclerView或NestedScrollView還在滾動,這種給咱們的感受就很是不連貫,好像每一個控件在獨自滑動。

在一樣的滑動條件下,實現了NestedScrollingChild2(NestedScrollView或RecyclerView等)與NestedScrollingParent2接口的嵌套滑動.看下面的例子:

NestedScrollingParent2效果展現

上述效果實現,請參看NestedScrollingParent2Layout.java

觀察上圖,咱們能發現父控件與子控件(RecyclerView或NestedScrollView)的滑動更爲順暢與合理。那接下來咱們看看谷歌對其的設計。

NestedScrollingChild2與NestedScrollingParent2分別繼承了NestedScrollingChild與NestedScrollingParent,在繼承的接口部分方法上增長了type參數。其中type的取值爲TYPE_TOUCH(0)TYPE_NON_TOUCH(1)。用於區分手勢滑動與fling。具體差別以下圖所示:

接口差別

圖片較大,可能閱讀不清晰,建議放大觀看。

谷歌在fling的處理上也與以前的NestedScrollingChild與NestedScrollingParent有所差別,在onTouchEvent方法中的邏輯進行了修改,僞代碼以下所示:

@Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getActionMasked();

        int y = (int) event.getY();
        int x = (int) event.getX();

        //添加速度檢測器,用於處理fling效果
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);

        switch (action) {
            case MotionEvent.ACTION_UP: {//當手指擡起的時,結束嵌套滑動傳遞,並判斷是否產生了fling效果
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                int xvel = (int) mVelocityTracker.getXVelocity();
                int yvel = (int) mVelocityTracker.getYVelocity();
                fling(xvel, yvel);//具體處理fling的方法
                mVelocityTracker.clear();
                stopNestedScroll(ViewCompat.TYPE_TOUCH));//注意這裏stop的是帶了參數的
                break;
            }

        }
        return super.onTouchEvent(event);
    }
複製代碼

當子控件手指擡起的時候,咱們發現是調用stopNestedScroll(ViewCompat.TYPE_TOUCH)的方式來通知父控件當前手勢滑動已經結束,繼續查看fling方法。僞代碼以下所示:

private boolean fling(int velocityX, int velocityY) {
        //判斷速度是否足夠大。若是夠大才執行fling
        if (Math.abs(velocityX) < mMinFlingVelocity) {
            velocityX = 0;
        }
        if (Math.abs(velocityY) < mMinFlingVelocity) {
            velocityY = 0;
        }
        if (velocityX == 0 && velocityY == 0) {
            return false;
        }
        if (dispatchNestedPreFling(velocityX, velocityY)) {
            boolean canScroll = canScroll();
            //將fling效果傳遞給父控件
            dispatchNestedFling(velocityX, velocityY, canScroll);

            //子控件在處理fling效果
            if (canScroll) {
                //通知父控件開始fling事件,
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
                velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
                velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
                doFling(velocityX, velocityY);
                return true;
            }

        }
        return false;

    }
複製代碼

從代碼中,咱們能夠看見,在新接口的處理邏輯中,仍是會調用dispatchNestedPreFling與dispatchNestedFling方法。也就是以前的處理fling方式是沒有被替代的,可是這並不說明沒有變化。咱們發現子控件調用了startNestedScroll方法,並設置了當前類型爲TYPE_NON_TOUCH(fling),那麼也就是說,在實現了NestedScrollingParent2的父控件中,咱們能夠在onStartNestedScroll方法中知道當前的滑動類型究竟是fling,仍是手勢滑動。咱們繼續查看doFling方法。僞代碼以下:

/** * 實際的fling處理效果 */
    private void doFling(int velocityX, int velocityY) {
        mScroller.fling(0, 0, velocityX, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
        postInvalidate();
    }

複製代碼

doFling方法其實很簡單,就是調用OverScroller的fing方法,並調用postInvalidate方法(爲了幫助你們理解,這裏並無採用 postOnAnimation()的方式)。其中OverScroller的fing方法主要是根據當前傳入的速度,計算出在勻減速狀況下,實際運動的距離。這裏也就解釋了爲何,在只有速度的狀況下,子控件能夠將fling傳遞給父控件,由於速度最後變成了實際的運動距離。

這裏就不對Scroller的fling方法中如何將速度轉換成距離的算法進行講解了。不熟悉的小夥伴能夠自行谷歌或百度。

熟悉Scroller的小夥伴必定知道,爲了獲取到fling所產生的距離,咱們須要調用postInvalidate()方法或Invalidate()方法。同時在子控件的computeScroll()方法中獲取實際的運動距離。那麼也就說最終的子控件的fing的分發實際是在computeScroll()方法中。繼續查看該方法的僞代碼:

public void computeScroll() {
       if (mScroller.computeScrollOffset()) {
           int x = mScroller.getCurrX();
           int y = mScroller.getCurrY();
           int dx = x - mLastFlingX;
           int dy = y - mLastFlingY;

           mLastFlingX = x;
           mLastFlingY = y;
           //在子控件處理fling以前,先判斷父控件是否消耗
           if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, null, TYPE_NON_TOUCH)) {
               //計算父控件消耗後,剩下的距離
               dx -= mScrollConsumed[0];
               dy -= mScrollConsumed[1];

               //由於以前默認向父控件傳遞的豎直方向,因此這裏子控件也消耗剩下的豎直方向
               int hResult = 0;
               int vResult = 0;
               int leaveDx = 0;//子控件水平fling 消耗的距離
               int leaveDy = 0;//父控件豎直fling 消耗的距離

               if (dx != 0) {
                   leaveDx = childFlingX(dx);
                   hResult = dx - leaveDx;//獲得子控件消耗後剩下的水平距離
               }
               if (dy != 0) {
                   leaveDy = childFlingY(dy);//獲得子控件消耗後剩下的豎直距離
                   vResult = dy - leaveDy;
               }

               dispatchNestedScroll(leaveDx, leaveDy, hResult, vResult, null, TYPE_NON_TOUCH);

           }
       } else {
           //當fling 結束時,通知父控件
           stopNestedScroll(TYPE_NON_TOUCH);

       }
   }

複製代碼

觀察代碼,咱們能夠發現,子控件中分發fling的方式在與以前分發手勢滾動的邏輯很是一致。

  • 產生fing時,調用帶type(TYPE_NON_TOUCH)參數的dispatchNestedPreScroll方法,判斷父控件是否處理fling事件。
  • 若是父控件處理,那麼父控件消耗後,子控件再消耗剩餘的距離
  • 子控件消耗後,若是還有剩餘的距離,則調用帶type(TYPE_NON_TOUCH)參數的dispatchNestedScroll方法,將剩下的距離傳遞給父控件。
  • 當子控件fling結束時,則調用stopNestedScroll(TYPE_NON_TOUCH)方法,通知父控件fling已經結束。

那麼也就是說,NestedScrollingChild2與NestedScrollingParent2接口,只是在原有的方法中增長了TYPE_NON_TOUCH參數來讓父控件區分究竟是手勢滑動仍是fling。不得不佩服谷歌大佬的設計。不只兼容還解決了實際的問題。

總結

經過上文的分析,咱們能獲得以下結論:

  • NestedScrolling(嵌套滑動)機制是創建在原有的事件機制之上,要實現嵌套滑動,父控件是不能攔截事件。
  • NestedScrolling(嵌套滑動)機制中接口要成對使用。如NestedScrollingChild2與NestedScrollingParent2成對。NestedScrollingChild與NestedScrollingParent成對。
  • 當咱們須要子控件分發fling給父控件時,咱們須要使用NestedScrollingChild2與NestedScrollingParent2。並在相應的方法中經過type(TYPE_TOUCH(0)TYPE_NON_TOUCH(1)),來判斷是手勢滑動仍是fling。

最後

到如今整個NestedScrolling(嵌套滑動)機制就講解完畢了,在接下來的文章中,會講解相應嵌套滑動例子、CoordinatorLayout與Behavior、自定義Behavior等相關知識點,若是你們有興趣的話,能夠持續關注~。謝謝你們花時間閱讀文章啦。Thanks

相關文章
相關標籤/搜索