CoordinatorLayout 和 AppbarLayout 聯動原理解析

下圖是CoordinatorLayout佈局中很常見的一種效果,很多人應該都見過,當我們用手指滑動RecyclerView的時候,不單止RecyclerView會上下滑動,頂部的Toolbar也會隨着RecyclerView的滑動隱藏或顯現,實現代碼的佈局如下:

具體代碼:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|enterAlways|snap"
            android:theme=
              "@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:popupTheme=
            "@style/ThemeOverlay.AppCompat.Light" />
    </android.support.design.widget.AppBarLayout>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior=
        "@string/appbar_scrolling_view_behavior" />

</android.support.design.widget.CoordinatorLayout>

只要父佈局是CoordinatorLayout,然後在Toolbar的外層包上一個AppBarLayout,在Toolbar上添加屬性layout_scrollFlags=”scroll|enterAlways|snap”,在RecyclerView上添加屬性layout_behavior=”@string/appbar_scrolling_view_behavior”,並把AppBarLayout與RecyclerView作爲CoordinatorLayout的子控件,就能實現。

實現的方法知道了,但是我們不能單純滿足於此,接下來我們對原理進行分析
 

實現以上效果主要是涉及了嵌套滑動機制和Behavior兩個知識點。

1、嵌套滑動機制(NestedScrolling)

根據事件分發機制,我們知道觸摸事件最終只會由一個控件進行處理,當我們滑動RecyclerView時,事件最終肯定是傳給了RecyclerView,並交給它進行處理,Toolbar是不應該能夠接收到事件並響應的。我們無法依靠默認的事件分發機制完成gif圖上的效果的(當然,我們通過自定義View,修改事件分發是可以實現這個效果)。 

因此Google給我們提供了嵌套滑動機制。通過嵌套滑動機制,RecyclerView能夠把自身接受到的點擊滑動事件傳遞給父佈局CoordinatorLayout,然後CoordinatorLayout把接收到的事件傳遞給子佈局AppBarLayout(Toolbar的父佈局),最終滑動事件交給了AppBarLayout進行處理,完成使Toolbar滾出滾進界面等效果。

這裏 NestedScrolling 兩個重要的概念提及一下

  • NestedScrollingParent NestedScrollingParentHelper
  • NestedScrollingChild NestedScrollingChildHelper

巧合的是 CoordinatorLayout 已經實現了 NestedScrollingParent 接口,所以我們配合一個實現了 NestedScrollingChild 接口的 View 就可以輕鬆的實現以上效果

一般而言,父佈局會實現NestedScrollingParent,而滑動列表作爲子控件實現NestedScrollingChild,並把事件傳給父佈局,父佈局再根據情況把事件分發到其它子View。而NestedScrollingParentHelper和NestedScrollingChildHelper分別是NestedScrollingParent和NestedScrollingChild的輔助類,具體的邏輯會委託給它們執行。

接下來我們看一下CoordinatorLayout和RecyclerView的源碼:

public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent {

    ......

}
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {

    ......

}

通過源碼可發現CoordinatorLayout實現了NestedScrollingParent,而RecyclerView實現了NestedScrollingChild。毫無疑問,RecyclerView就是通過嵌套滑動機制把滑動事件傳給了CoordinatorLayout,然後CoordinatorLayout把事件傳遞到AppBarLayout中。
那麼實現這些接口需要實現哪些方法呢?我們通過源碼來了解下:

public interface NestedScrollingChild {

    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

    public boolean dispatchNestedPreFling(float velocityX, float velocityY);

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

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

    public boolean hasNestedScrollingParent();

    public boolean isNestedScrollingEnabled();

    public void setNestedScrollingEnabled(boolean enabled);

    public boolean startNestedScroll(int axes);

    public void stopNestedScroll();

}

public interface NestedScrollingParent {

    public int getNestedScrollAxes();

    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

    public boolean onNestedPreFling(View target, float velocityX, float velocityY);

    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);

    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

    public void onStopNestedScroll(View target);

}

我整理的是NestedScrollingChild&NestedScrollingParent接口,26版本又添加了新的方法分別繼承自NestedScrollingChild&NestedScrollingParent接口 ,看起來要實現的方法很多,也很複雜的樣子,但是實質上通過輔助類NestedScrollingChildHelper和NestedScrollingParentHelper能大大減輕工作量,而且有些方法僅僅是作一個判斷,並不需要很複雜的邏輯。在後面的源碼驗證環節中我們也只會着重分析到重點的幾個方法。

在這裏先說幾個比較重要的方法的調用流程與對應關係:

  1. NestedScrollingChild接口的startNestedScroll會在Down事件觸發的時候調用,對應NestedScrollingParent的onStartNestedScroll。
  2. NestedScrollingChild接口的dispatchNestedPreScroll會在Move事件觸發的時候調用,對應NestedScrollingParent的onNestedPreScroll。
  3. NestedScrollingChild接口的dispatchNestedScroll會在Move事件觸發的時候調用,對應NestedScrollingParent的onNestedScroll。
  4. NestedScrollingChild接口的stopNestedScroll會在Up事件觸發的時候調用,對應NestedScrollingParent的onStopNestedScroll。

2、深入理解 Behavior

2.1攔截 Touch 事件

當我們爲一個 CoordinatorLayout 的直接子 View 設置了 Behavior 時,這個 Behavior 就能攔截髮生在這個 View 上的 Touch 事件,那麼它是如何做到的呢?實際上, CoordinatorLayout 重寫了 onInterceptTouchEvent() 方法,並在其中給 Behavior 開了個後門,讓它能夠先於 View 本身處理 Touch 事件。

具體來說, CoordinatorLayout 的 onInterceptTouchEvent() 方法中會遍歷所有直接子 View ,對於綁定了 Behavior 的直接子 View 調用 Behavior 的 onInterceptTouchEvent() 方法,若這個方法返回 true, 那麼後續本該由相應子 View 處理的 Touch 事件都會交由 Behavior 處理,而 View 本身表示懵逼,完全不知道發生了什麼。

CoordinatorLayout 的onInterceptTouchEvent 方法

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        MotionEvent cancelEvent = null;

        final int action = ev.getActionMasked();

        // Make sure we reset in case we had missed a previous important event.
        if (action == MotionEvent.ACTION_DOWN) {
        // 先讓子 view 種包含Behavior的控件 處理觸摸事件
            resetTouchBehaviors();
        }

        final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);

        if (cancelEvent != null) {
            cancelEvent.recycle();
        }

        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
            resetTouchBehaviors();
        }

        return intercepted;
    }

resetTouchBehaviors 方法內部實現:

  private void resetTouchBehaviors() {
        if (mBehaviorTouchView != null) {
            final Behavior b = ((LayoutParams) mBehaviorTouchView.getLayoutParams()).getBehavior();
            if (b != null) {
                final long now = SystemClock.uptimeMillis();
                final MotionEvent cancelEvent = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                b.onTouchEvent(this, mBehaviorTouchView, cancelEvent);
                cancelEvent.recycle();
            }
            mBehaviorTouchView = null;
        }

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            lp.resetTouchBehaviorTracking();
        }
        mDisallowInterceptReset = false;
    }

2.2 攔截測量及佈局

瞭解了 Behavior 是怎養攔截 Touch 事件的,想必大家已經猜出來了它攔截測量及佈局事件的方式 —— CoordinatorLayout 重寫了測量及佈局相關的方法併爲 Behavior 開了個後門。沒錯,真相就是如此。
CoordinatorLayout 在 onMeasure() 方法中,會遍歷所有直接子 View ,若該子 View 綁定了一個 Behavior ,就會調用相應 Behavior 的 onMeasureChild() 方法,若此方法返回 true,那麼 CoordinatorLayout 對該子 View 的測量就不會進行。這樣一來, Behavior 就成功接管了對 View 的測量。
同樣,CoordinatorLayout 在 onLayout() 方法中也做了與 onMeasure() 方法中相似的事,讓 Behavior 能夠接管對相關子 View 的佈局。

我們可以通過Behaviour觀察我們感興趣的控件的事件,並作出相應的操作。

通過在xml中添加layout_behavior屬性可以給控件設置Behaviour,比如在上面的代碼中,就是在RecyclerView中添加屬性

layout_behavior="@string/appbar_scrolling_view_behavior"

將RecyclerView的Behaviour指定成AppBarLayout的內部類ScrollingViewBehavior。

或者通過註解的方式給控件設置Behaviour,比如AppBarLayout就是通過

@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)

定義自身的Behavior爲AppBarLayout.Behavior

注意的是,Behavior是CoordinatorLayout的專屬屬性,設置Behavior的控件需要是CoordinatorLayout的子控件。

在我們上面的事例代碼中一共設置有兩個Behavior,第一個就是RecyclerView中通過layout_behavior屬性進行設置的ScrollingViewBehavior,第二個就是AppBarLayout的代碼中通過註解默認設置的一個AppBarLayout.Behavior.class。

當我們要依賴另一個view的狀態變化,例如大小、顯示、位置狀態,我們至少應該重寫以下兩個方法:

 public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
            return false;
        }
 public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
            return false;
        }

第一個方法負責決定依賴哪個View,第二個方法負責根據依賴的View的變化做出響應。

我們的事例中給RecycleView設置的ScrollingViewBehavior也實現了這兩個方法,使得RecycleView一直處於AppBarLayout的下方。

當我們要依賴某個實現了NestedScrollingChild的View的滑動狀態時,應該重寫以下方法:
 

        public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
                V child, View directTargetChild, View target, int nestedScrollAxes) {
            return false;
        }
        public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
                int dx, int dy, int[] consumed) {
            // Do nothing
        }
        public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
                float velocityX, float velocityY) {
            return false;
        }

onStartNestedScroll決定Behavior是否接收嵌套滑動機制傳過來的事件;onNestedPreScroll負責接收依賴的View滑動的滑動事件並處理;onNestedPreFling負責接收快速滑動時的慣性滑動事件並處理。

我們的事例中AppBarLayout通過註解設置的AppBarLayout.Behavior實現了這3個方法,使得AppBarLayout能夠接收到RecycleView傳來的滑動事件並響應。

3、聯動分析

我們滑動RecyclerView的時候,RecyclerView會通過滑動嵌套機制把接收到的事件傳給CoordinatorLayout,然後CoordinatorLayout把事件傳給AppBarLayout,AppBarLayout再根據自身的Behavior(AppBarLayout.Behavior.class)做相應的處理,判斷是否處理該滑動事件,如果不處理,則事件仍交還給RecyclerView,如果處理,就做出相應的操作,例如將Toolbar滾出或者滾進屏幕,並消耗掉需要的滑動事件。

這時候可能會有人有疑問:當AppBarLayout處理並消耗了RecyclerView傳遞的滑動事件的時候(既Toolbar上下滑動時),RecyclerView爲什麼也還能跟隨着手指上下移動呢?其實這裏RecyclerView並不是跟隨着手指移動,而是一直保持在AppBarLayout的正下方。這是因爲我們在RecyclerView中添加屬性『layout_behavior="@string/appbar_scrolling_view_behavior"』

給RecyclerView指定了AppBarLayout$ScrollingViewBehavior,這個Behavior會觀察AppBarLayout,當AppBarLayout發生變化時做出相應的操作。正是因爲這樣,就算RecyclerView把滑動事件交給AppBarLayout處理並消耗掉,它也還能一直保持在AppBarLayout的正下方。

總結:當我們滑動RecyclerView時,Toolbar能上下滾動是由嵌套滑動機制和AppBarLayout.Behavior共同工作完成的。而在Toolbar上下滾動時,RecyclerView也能始終保持在其正下方的功能是由ScrollingViewBehavior實現的。
 

4、源碼分析 

4.1 Toolbar能隨RecycleView上下滾動原理:

我們先來分析一下RecyclerView是如何把滑動事件傳給CoordinatorLayout,即NestedScrollingChild把事件傳給NestedScrollingParent,以及接收到事件的CoordinatorLayout又如何把事件分發到AppBarLayout的Behavior上。

事件分發是從Down開始的,因此我們先從RecyclerView的Down事件開始分析

    @Override
    public boolean onTouchEvent(MotionEvent e) {

        ......

        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;
                }
                startNestedScroll(nestedScrollAxis);
            } break;

        ......

    }

可以看到在RecyclerView的Down事件的最後一行,我們調用了NestedScrollingChild接口的startNestedScroll(nestedScrollAxis)方法,並把支持的滾動方向作爲參數傳了進去,這個方法也是嵌套滑動機制中被調用的第一個方法,在這個方法內會決定是否啓用嵌套滑動,以及誰來接收處理嵌套滑動傳過來的事件。

然後我們來看看startNestedScroll(nestedScrollAxis)方法的內部實現。

    @Override
    public boolean startNestedScroll(int axes) {
        return getScrollingChildHelper().startNestedScroll(axes);
    }

    ......

    private NestedScrollingChildHelper getScrollingChildHelper() {
        if (mScrollingChildHelper == null) {
            mScrollingChildHelper = new NestedScrollingChildHelper(this);
        }
        return mScrollingChildHelper;
    }

startNestedScroll(int axes)方法實質上是通過代理的方式,把邏輯委託給了NestedScrollingChildHelper。那麼我們來看下NestedScrollingChildHelper的startNestedScroll(int axes)做了什麼:
 

    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;
    }

首先調用了NestedScrollingChild接口的實現方法hasNestedScrollingParent(),其內部邏輯是判斷mNestedScrollingParent是否等於null,如果不是,則代表嵌套滑動已經開始,就直接return true,不繼續往下走。

一般開始的時候mNestedScrollingParent在這裏都是還沒賦值,是爲null的,所以可以繼續往下走,接下來通過NestedScrollingChild接口的isNestedScrollingEnabled()方法判斷是不是支持NestedScrolling,這裏默認是爲ture,所以我們繼續往下走。

接下來調用了mView.getParent(),通過查看RecyclerView的getScrollingChildHelper()方法,以及NestedScrollingChildHelper的構造函數可知,其實就是調用了RecyclerView的getParent()方法,而RecyclerView的父佈局是CoordinatorLayout,所以得到的ViewParent p就是CoordinatorLayout。

然後在while循環中通過ViewParentCompat.onStartNestedScroll(p, child, mView, axes)方法不斷尋找需要接收處理RecyclerView分發過來的事件的父佈局,如果找到了,就返回true,這時候就會執行if語句中的代碼,把接收事件的父佈局賦值mNestedScrollingParent。並且調用ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes),並且最後整個方法再返回true,startNestedScroll方法就算是跑完了。

在我們的事例代碼中,while循環就只執行一次,把CoordinatorLayout、RecyclerView和axes作爲值傳了進去。在這裏child和mView都是同一個RecyclerView。

既然while循環只執行一次,那就代表ViewParentCompat.onStartNestedScroll(p, child, mView, axes)方法在第一次執行的時候就已經返回true了,也就是代表RecyclerView的直接父佈局CoordinatorLayout會接收處理RecyclerView分發過來的事件。那麼我們就來看下ViewParentCompat.onStartNestedScroll到底寫了什麼邏輯。爲了方便,我們分析5.0以上的源碼(與5.0以下的源碼的主要區別在於5.0以下的源碼多做了一些版本兼容工作)。

ViewParentCompat.onStartNestedScroll最終調用到ViewParentCompatLollipop的onStartNestedScroll方法:

class ViewParentCompatLollipop {
    private static final String TAG = "ViewParentCompat";

    public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
            int nestedScrollAxes) {
        try {
            return parent.onStartNestedScroll(child, target, nestedScrollAxes);
        } catch (AbstractMethodError e) {
            Log.e(TAG, "ViewParent " + parent + " does not implement interface " +
                    "method onStartNestedScroll", e);
            return false;
        }
    }

    ......

}

在ViewParentCompatLollipop的onStartNestedScroll方法中,其實主要就一句話:

『return parent.onStartNestedScroll(child, target, nestedScrollAxes);』

這個parent則是從ViewParentCompat.onStartNestedScroll(p, child, mView, axes)方法傳過來的p,也就是CoordinatorLayout。

通過這麼一系列的調用,最終從RecyclerView的startNestedScroll方法,調用到了CoordinatorLayout的onStartNestedScroll方法。那麼接下來我們就去看下CoordinatorLayout的onStartNestedScroll方法中做了什麼。
 

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        boolean handled = false;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
                        nestedScrollAxes);
                handled |= accepted;

                lp.acceptNestedScroll(accepted);
            } else {
                lp.acceptNestedScroll(false);
            }
        }
        return handled;
    }

在這個方法中,CoordinatorLayout遍歷了它的子佈局並獲取對應的Behavior,如果Behavior不爲空,則根據該Behavior的onStartNestedScroll來決定是否把接收來的事件發放給該Behavior所屬的View,並返回Behavior的onStartNestedScroll方法的返回值。由於handled |= accepted,只要有一個Behavior的onStartNestedScroll方法返回true,handled就會是ture。

也就是:AppBarLayout是否接收事件並處理,是RecyclerView通過嵌套滑動原理,把事件傳給CoordinatorLayout,CoordinatorLayout通過遍歷自身的子佈局,找到了AppBarLayout,並根據AppBarLayout的Behavior是否對事件感興趣來決定。

在我們這個實例中一共有兩個View設置了Behavior,究竟是哪個Behavior處理了事件呢?我們先去看下AppBarLayout的Behavior的源碼。AppBarLayout的Behavior我們在上面也已經說過了,是通過註解設置的『@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)』

我們接下來到AppBarLayout.Behavior裏看看它的onStartNestedScroll做了些什麼。

        @Override
        public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
                View directTargetChild, View target, int nestedScrollAxes) {
            // Return true if we're nested scrolling vertically, and we have scrollable children
            // and the scrolling view is big enough to scroll
            final boolean started = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
                    && child.hasScrollableChildren()
                    && parent.getHeight() - directTargetChild.getHeight() <= child.getHeight();

            if (started && mOffsetAnimator != null) {
                // Cancel any offset animation
                mOffsetAnimator.cancel();
            }

            // A new nested scroll has started so clear out the previous ref
            mLastNestedScrollingChildRef = null;

            return started;
        }

該方法最終返回一個布爾值started,只有當可垂直滑動、AppBarLayout裏有可以滑動的子View、並且CoordinatorLayout的高減去RecyclerView的高小於等於AppBarLayout的高的時候,started等於true,這些條件在上面的事例中都是符合的,因此最終AppBarLayout.Behavior的onStartNestedScroll方法返回true,也就是嵌套滑動的事件交給了AppBarLayout處理。

我們再去看下RecyclerView中設置的ScrollingViewBehavior的源碼,ScrollingViewBehavior以及它的父類並沒有重寫onStartNestedScroll,所以它的onStartNestedScroll方法既是CoordinatorLayout.Behavior:

        public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
                V child, View directTargetChild, View target, int nestedScrollAxes) {
            return false;
        }

我們可以看到,ScrollingViewBehavior的onStartNestedScroll方法居然直接返回false了,也就是說它肯定是不會接收通過該方法傳來的事件了。

就這樣,Down事件就大致分析完了。在Down事件中主要是決定嵌套滑動的接收者,以及對相應的View進行標記,方便Move事件的相關滑動操作。

Down事件分析完了,接下來我們就來分析Move事件,由於代碼比較長,我就只截取一部分:
 

            case MotionEvent.ACTION_MOVE: {

                ......

                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
                    dx -= mScrollConsumed[0];
                    dy -= mScrollConsumed[1];
                    vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                    // Updated the nested offsets
                    mNestedOffsets[0] += mScrollOffset[0];
                    mNestedOffsets[1] += mScrollOffset[1];
                }

                ......

                break;
            }

我們主要關注NestedScrollingParent接口的dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)方法,該方法最終調用到CoordinatorLayout的onNestedPreScroll方法,CoordinatorLayout的onNestedPreScroll又會調用到AppBarLayout.Behavir的onNestedPreScroll,調用流程跟Down事件差不多,具體原理限於篇幅就不再分析了,我們主要dispatchNestedPreScroll方法主要實現了什麼功能。

該方法主要是決定是否需要把Coordinatorlayout接收到的事件分發給AppBarLayout。假設AppBarLayout中的Toolbar已經完全顯示了,而此時RecycleView是在往下滑,這時候Toolbar完全不需要接收事件使自己顯示,此時dispatchNestedPreScroll就會返回false。

接下來我們來關注dispatchNestedPreScroll方法的參數,前兩個分別是橫座標和縱座標的偏移量,這沒啥好解釋的,我們主要來分析後兩個參數的作用。後兩個參數分別是父view消費掉的 scroll長度(CoordinatorLayout分發給AppBarlayout消費掉)和子View(RecycleView)的窗體偏移量。

如果dispatchNestedPreScroll返回true,則會根據後兩個參數來進行修正,例如通過mScrollConsumed更新dx和dy,以及通過mScrollOffset更新RecycleView的窗體偏移量。

假設RecyclerView的滾動事件沒有被消費完,在RecycleView的Move事件最後scrollByInternal方法會繼續處理剩下的滾動事件,並調用NestedScrollingChild接口的dispatchNestedScroll方法,而且最終還是會通過NestedScrollingParent的onNestedScroll調用到Behavior的對應方法。scrollByInternal方法部分核心代碼如下:

            ......

            if (x != 0) {
                consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
                unconsumedX = x - consumedX;
            }
            if (y != 0) {
                consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
                unconsumedY = y - consumedY;
            }

            ......

            if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
                // Update the last touch co-ords, taking any scroll offset into account
                mLastTouchX -= mScrollOffset[0];
                mLastTouchY -= mScrollOffset[1];
                if (ev != null) {
                    ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                }
                mNestedOffsets[0] += mScrollOffset[0];
                mNestedOffsets[1] += mScrollOffset[1];

                ......

            }

RecyclerView處理了剩餘的滾動距離之後,計算出對剩餘滾動事件的消費情況,通過 dispatchNestedScroll 方法分發給CoordinatorLayout,CoordinatorLayout 則通過 onNestedScroll 方法分發給感興趣的 子View 的 Behavior 處理。然後根據mScrollOffset更新窗體偏移量。具體實現可以自行去查看源碼。

最後我們來分析Up事件,先來看代碼:
 

            case MotionEvent.ACTION_UP: {
                mVelocityTracker.addMovement(vtev);
                eventAddedToVelocityTracker = true;
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                final float xvel = canScrollHorizontally ?
                        -VelocityTrackerCompat.getXVelocity(mVelocityTracker, mScrollPointerId) : 0;
                final float yvel = canScrollVertically ?
                        -VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0;
                if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                    setScrollState(SCROLL_STATE_IDLE);
                }
                resetTouch();
            } break;

在Up事件最後一行中調用了resetTouch(),而resetTouch又調用了NestedScrollingChild的stopNestedScroll(),然後又是跟上面的流程一樣一路調用到NestedScrollingParent的onStopNestedScroll方法,然後再調用對應的Behavior的onStopNestedScroll方法,流程都類似,就不貼代碼了。

在Stop的這一流程中主要是將之前在Start流程中的設置清空,比如將mNestedScrollingParent = null(不執行這句的話嵌套滑動就執行不起來了,具體可參考NestedScrollingChild的startNestedScroll方法第一行)。

由嵌套滑動機制和AppBarLayout.Behavior共同工作完成的Toolbar上下滾動效果的原理就分析到這吧。

4.2 RecycleView一直保持在AppBarLayout下方原理:
接下來我們再通過源碼分析下RecycleView是如何一直保持在AppBarLayout下方的吧。

在文章開頭我已經簡單分析過ScrollingViewBehavior主要是依靠layoutDependsOn和onDependentViewChanged方法監聽並響應的。ScrollingViewBehavior的layoutDependsOn方法:

        @Override
        public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
            // We depend on any AppBarLayouts
            return dependency instanceof AppBarLayout;
        }

很明顯,ScrollingViewBehavior就是依賴於AppBarLayout的,那麼我們來看下onDependentViewChanged方法:

        @Override
        public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
                View dependency) {
            offsetChildAsNeeded(parent, child, dependency);
            return false;
        }

        ......


        private void offsetChildAsNeeded(CoordinatorLayout parent, View child, View dependency) {
            final CoordinatorLayout.Behavior behavior =
                    ((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
            if (behavior instanceof Behavior) {
                // Offset the child, pinning it to the bottom the header-dependency, maintaining
                // any vertical gap and overlap
                final Behavior ablBehavior = (Behavior) behavior;
                ViewCompat.offsetTopAndBottom(child, (dependency.getBottom() - child.getTop())
                        + ablBehavior.mOffsetDelta
                        + getVerticalLayoutGap()
                        - getOverlapPixelsForOffset(dependency));
            }
        }

這兩個方法在CoordinatorLayout的onChildViewsChanged會被調用到,而每次重繪時,都會調用onChildViewsChanged,從而使其一直位於AppBarLayout的下方。

完畢

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章