CoordinatorLayout三部曲學習之三:AppBarLayout聯動源碼學習

今天學習整理一下AppBarLayout與CoordinatorLayout以及Behavior交互邏輯的過程,首先使用一張圖先歸納一下各個類主要功能吧(本文章使用NestedScrollView充當滑動的內嵌子View)。java

  • CoordinatorLayout實現NestedScrollingParent2接口,用於處理與滑動子View的聯動交互(這裏使用的是NestedScrollView),實際上交由Behavior進行處理,CoordinatorLayout爲其代理類。
  • AppBarLayout中默認使用了AppBarLayout.Behavior,主要功能是接收CoordinatorLayout傳輸過來的滑動事件,而且相對應的進行處理,如NestedScrollView往上滑動到頭時候,繼續滑動則移動AppBarLayout到頭。
  • NestedScrollView實現了NestedScrollingChild2接口,用於傳輸給CoordinatorLayout,而且消費CoordinatorLayout不消費的觸摸事件,其中仍是使用了AppBarLayout.ScrollingViewBehavior,功能是進行監聽AppBarLayout的位移變化,從而進行相對應的變化,最明顯的例子就是AppBarLayout上移過程當中,NestedScrollView一塊兒上移。

底下代碼分析創建在下面例子之中:android

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/coordinator"
    tools:context=".photo.TestActivity">
    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:id="@+id/appbar"
        android:layout_height="220dp"
        android:background="#ffffff">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_scrollFlags="scroll"
            android:orientation="vertical">

        </LinearLayout>
    </android.support.design.widget.AppBarLayout>
    <View
        android:layout_width="match_parent"
        android:id="@+id/edit"
        android:background="#e29de3"
        android:layout_height="50dp">

    </View>
    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="500dp"
        android:background="#1d9d29"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
		....
    </android.support.v4.widget.NestedScrollView>

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

那麼如今咱們直接看是看源碼吧,這裏主要弄明白兩個邏輯:app

  1. 當手指觸摸AppBarLayout時候的滑動邏輯。
  2. 當手指觸摸NestedScrollView時的滑動邏輯。

當手指觸摸AppBarLayout時候的滑動邏輯

在弄清手指觸摸AppBarLayout時候的滑動邏輯,須要瞭解一下AppBarLayout.Behavior這個類,ApprBarLayout的默認Behavior就是AppBarLayout.Behavior這個類,而AppBarLayout.Behavior繼承自HeaderBehaviorHeaderBehavior又繼承自ViewOffsetBehavior,這裏先總結一下兩個類的做用,須要詳細的實現的請自行閱讀源碼吧:ide

  • ViewOffsetBehavior:該Behavior主要運用於View的移動,從名字就能夠看出來,該類中提供了上下移動,左右移動的方法。
  • HeaderBehavior:該類主要用於View處理觸摸事件以及觸摸後的fling事件。

因爲上面兩個類功能的實現,使得AppBarLayout.Behavior具備了同時移動自己以及處理觸摸事件的功能,在CoordinatorLayout四部曲學習之二:CoordinateLayout源碼學習這篇文章又說明了CoordinateLayout的NestedScrollingParent2的實現全權委託給了Behavior類,因此AppBarLayout.Behavior就提供了ApprBarLayout對應的聯動的方案。post

那麼咱們直接從一開始入手,當咱們手碰到AppBarLayout的時候,最終方法經由CoordinateLayout.OnInterceptEvent(...)調用了AppBarLayout.Behavior的對應方法中,上面說了HeaderBehavior處理了觸摸事件,那麼咱們就看下對應的方法:學習

@Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
      ....
        if (action == MotionEvent.ACTION_MOVE && mIsBeingDragged) {
            return true;
        }

        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN: {
                mIsBeingDragged = false;
                final int x = (int) ev.getX();
                final int y = (int) ev.getY();
                if (canDragView(child) && parent.isPointInChildBounds(child, x, y)) {
                    mLastMotionY = y;
                    mActivePointerId = ev.getPointerId(0);
                    ensureVelocityTracker();
                }
                break;
            }

            case MotionEvent.ACTION_MOVE: {
                final int activePointerId = mActivePointerId;
                if (activePointerId == INVALID_POINTER) {
                    // If we don't have a valid id, the touch down wasn't on content.
                    break;
                }
                final int pointerIndex = ev.findPointerIndex(activePointerId);
                if (pointerIndex == -1) {
                    break;
                }

                final int y = (int) ev.getY(pointerIndex);
                final int yDiff = Math.abs(y - mLastMotionY);
                if (yDiff > mTouchSlop) {
                    mIsBeingDragged = true;
                    mLastMotionY = y;
                }
                break;
            }

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP: {
                mIsBeingDragged = false;
                mActivePointerId = INVALID_POINTER;
                if (mVelocityTracker != null) {
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }
                break;
            }
        }

        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(ev);
        }

        return mIsBeingDragged;
    }

上述代碼很是簡單,就是返回mIsBeingDragged,當移動過程當中大於TouchSlop的時候,攔截時間,進而交給onTouchEvent(...)作處理:this

public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
       ...
        switch (ev.getActionMasked()) {
            ...
            case MotionEvent.ACTION_MOVE: {
                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                if (activePointerIndex == -1) {
                    return false;
                }

                final int y = (int) ev.getY(activePointerIndex);
                int dy = mLastMotionY - y;

                if (!mIsBeingDragged && Math.abs(dy) > mTouchSlop) {
                    mIsBeingDragged = true;
                    if (dy > 0) {
                        dy -= mTouchSlop;
                    } else {
                        dy += mTouchSlop;
                    }
                }

                if (mIsBeingDragged) {
                    mLastMotionY = y;
                    // We're being dragged so scroll the ABL
                    scroll(parent, child, dy, getMaxDragOffset(child), 0);
                }
                break;
            }

            case MotionEvent.ACTION_UP:
                if (mVelocityTracker != null) {
                    mVelocityTracker.addMovement(ev);
                    mVelocityTracker.computeCurrentVelocity(1000);
                    float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
                    fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);
                }
         ...
        return true;
    }

主要邏輯仍是在ACTION_MOVE中,能夠看到在滑動過程當中調用了scroll(...)方法,scroll(...)方法在HeaderBehavior中進行實現,最終調用到了額setHeaderTopBottomOffset(...)方法,該方法在AppBarLayout.Behavior中進行了重寫,因此,咱們直接看AppBarLayout.Behavior中的源碼便可:spa

@Override
   //newOffeset傳入了dy,也就是咱們手指移動距離上一次移動的距離,
   //minOffset等於AppBarLayout的負的height,maxOffset等於0。
        int setHeaderTopBottomOffset(CoordinatorLayout coordinatorLayout,
                AppBarLayout appBarLayout, int newOffset, int minOffset, int maxOffset) {
            final int curOffset = getTopBottomOffsetForScrollingSibling();//獲取當前的滑動Offset
            int consumed = 0;
			//AppBarLayout滑動的距離若是超出了minOffset或者maxOffset,則直接返回0
            if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
               //矯正newOffset,使其minOffset<=newOffset<=maxOffset
                newOffset = MathUtils.clamp(newOffset, minOffset, maxOffset);
				//因爲默認沒設置Interpolator,因此interpolatedOffset=newOffset;
                if (curOffset != newOffset) {
                    final int interpolatedOffset = appBarLayout.hasChildWithInterpolator()
                            ? interpolateOffset(appBarLayout, newOffset)
                            : newOffset;
					//調用ViewOffsetBehvaior的方法setTopAndBottomOffset(...),最終經過
					//ViewCompat.offsetTopAndBottom()移動AppBarLayout
                    final boolean offsetChanged = setTopAndBottomOffset(interpolatedOffset);

                    //記錄下消費了多少的dy。
                    consumed = curOffset - newOffset;
                   //沒設置Interpolator的狀況, mOffsetDelta永遠=0
                    mOffsetDelta = newOffset - interpolatedOffset;
					....
                     //分發回調OnOffsetChangedListener.onOffsetChanged(...)
                    appBarLayout.dispatchOffsetUpdates(getTopAndBottomOffset());

                  
                    updateAppBarLayoutDrawableState(coordinatorLayout, appBarLayout, newOffset,
                            newOffset < curOffset ? -1 : 1, false);
                }
           ...
            return consumed;
        }

上面註釋也解釋的比較清楚了,經過setTopAndBottomOffset()來達到了移動咱們的AppBarLayout,那麼這裏AppBarLayout就能夠跟着手上下移動了,可是,NestedScrollView還沒跟着移動呢,若是按照上面的分析來看。上面的總結能夠得知,NestedScrollView也實現了一個ScrollingViewBehaviorScrollingViewBehavior也繼承自ViewOffsetBehavior,說明當前的NestedScrollView也具有上下移動的功能,在閱讀ScrollingViewBehavior源碼中發現其實現了以下方法:.net

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

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

經過上面方法,而且結合CoordinatorLayout四部曲學習之二:CoordinateLayout源碼學習該文章的分析能夠知道,NestedScrollView依賴於AppBarLayout,在AppBarLayout移動的過程當中,NestedScrollView會隨着AppBarLayout的移動回調onDependentViewChanged(...)方法,進而調用 offsetChildAsNeeded(parent, child, dependency)代理

private void offsetChildAsNeeded(CoordinatorLayout parent, View child, View dependency) {
            final CoordinatorLayout.Behavior behavior =
                    ((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
            if (behavior instanceof Behavior) {
                final Behavior ablBehavior = (Behavior) behavior;//獲取AppBarLayout的behavior
				//移動對應的距離
                ViewCompat.offsetTopAndBottom(child, (dependency.getBottom() - child.getTop())
                        + ablBehavior.mOffsetDelta
                        + getVerticalLayoutGap()
                        - getOverlapPixelsForOffset(dependency));
            }
        }

這樣咱們就知道了當手指移動AppBarLayout時候的過程,下面整理一下:

首先經過Behavior.onTouchEvent(...)收到滑動距離,進而通知AppBarLayout.Behavior調用ViewCompat.offsetTopAndBottom()進行滑動;在AppBarLayout滑動的過程當中,因爲NestedScrollView中的ScrollingViewBehavior會依賴於AppBarLayout,因此在AppBarLayout滑動時候,NestedScrollView也會隨着滑動,調用的方法也是ViewCompat.offsetTopAndBottom()

接下來再看下fling過程,fling過程在手指離開時候會判斷調用,即從ACTION_UP開始:

case MotionEvent.ACTION_UP:
                if (mVelocityTracker != null) {
                    mVelocityTracker.addMovement(ev);
                    mVelocityTracker.computeCurrentVelocity(1000);
                    float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
                    fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);
                }

能夠看到直接調用了fling(...)中:

final boolean fling(CoordinatorLayout coordinatorLayout, V layout, int minOffset,
            int maxOffset, float velocityY) {
			//重置FlingRunnable
        if (mFlingRunnable != null) {
            layout.removeCallbacks(mFlingRunnable);
            mFlingRunnable = null;
        }

        if (mScroller == null) {
            mScroller = new OverScroller(layout.getContext());
        }

        mScroller.fling(
                0, getTopAndBottomOffset(), // curr
                0, Math.round(velocityY), // velocity.
                0, 0, // x
                minOffset, maxOffset); // 最大距離不超過AppbarLayout的高度

        if (mScroller.computeScrollOffset()) {
            mFlingRunnable = new FlingRunnable(coordinatorLayout, layout);
            ViewCompat.postOnAnimation(layout, mFlingRunnable);
            return true;
        } else {
            onFlingFinished(coordinatorLayout, layout);
            return false;
        }
    }

代碼也比較簡單,主要經過FlingRunnable循環調用setHeaderTopBottomOffset()方法就把AppBarLayout進行了View的移動:

private class FlingRunnable implements Runnable {
        private final CoordinatorLayout mParent;
        private final V mLayout;

        FlingRunnable(CoordinatorLayout parent, V layout) {
            mParent = parent;
            mLayout = layout;
        }

        @Override
        public void run() {
            if (mLayout != null && mScroller != null) {
                if (mScroller.computeScrollOffset()) {
                    setHeaderTopBottomOffset(mParent, mLayout, mScroller.getCurrY());
                    ViewCompat.postOnAnimation(mLayout, this);
                } else {
                    onFlingFinished(mParent, mLayout);
                }
            }
        }
    }

再看下fling完後作了什麼,這裏從上述代碼能夠看到調用了onFlingFinished(mParent, mLayout)AppBarLayout.Behavior中實現了當前方法:

@Override
        void onFlingFinished(CoordinatorLayout parent, AppBarLayout layout) {
            // At the end of a manual fling, check to see if we need to snap to the edge-child
            snapToChildIfNeeded(parent, layout);
        }

snapToChildIfNeeded(...)方法會根據scrollFlags來進行處理,因爲在上面xml中使用的是layout_scrollFlags=scroll,因此在 當前方法中並不會進行對應的邏輯處理,那麼fling操做到此也完成了,這裏看到fling()操做只創建在AppBarLayout上,也就是說不管咱們多快速滑動,始終在AppBarLayout到達最大滑動距離,也就是AppBarLayout高度時候滑動就會中止,不會去聯動NestedScrollView。

當手指觸摸NestedScrollView時的滑動邏輯。

接下來來看當手指觸摸NestedScrollView時的滑動邏輯,在CoordinatorLayout四部曲學習之一:Nest接口的實現 原 文章中分析過,NestedScrollView做爲子View滑動時候會首先調用startNestedScroll(...)方法來詢問父View即CoordinatorLayout是否須要消費事件,CoordinatorLayout做爲代理作發給對應Behavior,這裏就分發給了AppBarLayout.Behavior的回調onStartNestedScroll(...),方法以下:

@Override
  	//directTargetChild=target=NestedScrollView
        public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
                View directTargetChild, View target, int nestedScrollAxes, int type) {
			//若是滑動方向爲VERTICAL且AppBarLayout的高度不等於0且NestedScrollView能夠滑動,started=true;
            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;
        }

上述Demo知足started=true,因此說明CoordinatorLayout須要進行消費事件的處理,而後回調AppBarLayout.Behavior.onNestedPreScroll():

@Override
        public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
                View target, int dx, int dy, int[] consumed, int type) {
            if (dy != 0) {
                int min, max;
                if (dy < 0) {
                    //手指向下滑動
                    min = -child.getTotalScrollRange();//getTotalScrollRange返回child的高度
                    max = min + child.getDownNestedPreScrollRange();//getDownNestedPreScrollRange()返回0
                } else {
                    // 手指向上滑動
                    min = -child.getUpNestedPreScrollRange();//同getTotalScrollRange
                    max = 0;
                }
                if (min != max) {
				//計算消費的距離
                    consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
                }
            }
        }

上面代碼中出現了許多get....Range()方法主要是爲了在咱們使用對應LayoutParam.scrollflage=COLLAPSED相關標誌的時候會使用到,因爲咱們分析代碼不涉及到,因此都是返回的AppBarLayout的滑動高度或者0,上面代碼已經註釋了。接下來計算comsumed[1]:

final int scroll(CoordinatorLayout coordinatorLayout, V header,
            int dy, int minOffset, int maxOffset) {
        return setHeaderTopBottomOffset(coordinatorLayout, header,
                getTopBottomOffsetForScrollingSibling() - dy, minOffset, maxOffset);
    }

這個方法上面已經分析過了,從新貼下:

@Override
   //newOffeset傳入了dy,也就是咱們手指移動距離上一次移動的距離,
   //minOffset等於AppBarLayout的負的height,maxOffset等於0。
        int setHeaderTopBottomOffset(CoordinatorLayout coordinatorLayout,
                AppBarLayout appBarLayout, int newOffset, int minOffset, int maxOffset) {
            final int curOffset = getTopBottomOffsetForScrollingSibling();//獲取當前的滑動Offset
            int consumed = 0;
			//AppBarLayout滑動的距離若是超出了minOffset或者maxOffset,則直接返回0
            if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
               //矯正newOffset,使其minOffset<=newOffset<=maxOffset
                newOffset = MathUtils.clamp(newOffset, minOffset, maxOffset);
				//因爲默認沒設置Interpolator,因此interpolatedOffset=newOffset;
                if (curOffset != newOffset) {
                    final int interpolatedOffset = appBarLayout.hasChildWithInterpolator()
                            ? interpolateOffset(appBarLayout, newOffset)
                            : newOffset;
					//調用ViewOffsetBehvaior的方法setTopAndBottomOffset(...),最終經過
					//ViewCompat.offsetTopAndBottom()移動AppBarLayout
                    final boolean offsetChanged = setTopAndBottomOffset(interpolatedOffset);

                    //記錄下消費了多少的dy。
                    consumed = curOffset - newOffset;
                   //沒設置Interpolator的狀況, mOffsetDelta永遠=0
                    mOffsetDelta = newOffset - interpolatedOffset;
					....
                     //分發回調OnOffsetChangedListener.onOffsetChanged(...)
                    appBarLayout.dispatchOffsetUpdates(getTopAndBottomOffset());

                  
                    updateAppBarLayoutDrawableState(coordinatorLayout, appBarLayout, newOffset,
                            newOffset < curOffset ? -1 : 1, false);
                }
           ...
            return consumed;
        }

consumed的值有兩種狀況:

  • 當滑動的距離在minOffset和maxOffset區間以內,則consume!=0,也就說明須要AppBarLayout進行消費,這裏對應着AppBarLayout還沒移出咱們的視線時候的消費狀況。
  • 當滑動的距離超出了minOffset或者maxOffset後,則consume==0,也就說明須要AppBarLayout不進行消費了,這裏對應着AppBarLayout移出咱們的視線時候的消費狀況。

回到AppBarLayout.Behavior中繼續看相關方法:

@Override
        public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
                View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
                int type) {
				//這個方法是作個兼容,在Demo中卻是沒試出來調用時機,選擇性忽略
            if (dyUnconsumed < 0) {
                // If the scrolling view is scrolling down but not consuming, it's probably be at
                // the top of it's content
                scroll(coordinatorLayout, child, dyUnconsumed,
                        -child.getDownNestedScrollRange(), 0);
            }
        }

        @Override
        public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl,
                View target, int type) {
            if (type == ViewCompat.TYPE_TOUCH) {
                // If we haven't been flung then let's see if the current view has been set to snap
                snapToChildIfNeeded(coordinatorLayout, abl);//這個方法內部邏輯不會走,緣由是scroll_flag=scroll
            }

            // Keep a reference to the previous nested scrolling child
            mLastNestedScrollingChildRef = new WeakReference<>(target);
        }

上面的方法比較簡單,就不介紹了,接下來看下手指離開時候的處理,這時候應該回調對應Behavior的fling()方法,可是AppBarLayout在ACTION_UP這裏並無作多餘的處理,甚至連fling相關回調都沒調用,那隻能從NestedScrollView的computeScroll()方法研究了:

public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            final int x = mScroller.getCurrX();
            final int y = mScroller.getCurrY();

            int dy = y - mLastScrollerY;

            // Dispatch up to parent
            if (dispatchNestedPreScroll(0, dy, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH)) {
                dy -= mScrollConsumed[1];
            }

            if (dy != 0) {
                final int range = getScrollRange();
                final int oldScrollY = getScrollY();

                overScrollByCompat(0, dy, getScrollX(), oldScrollY, 0, range, 0, 0, false);

                final int scrolledDeltaY = getScrollY() - oldScrollY;
                final int unconsumedY = dy - scrolledDeltaY;

                if (!dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, null,
                        ViewCompat.TYPE_NON_TOUCH)) {
                    final int mode = getOverScrollMode();
                    final boolean canOverscroll = mode == OVER_SCROLL_ALWAYS
                            || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
                    if (canOverscroll) {
                        ensureGlows();
                        if (y <= 0 && oldScrollY > 0) {
                            mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
                        } else if (y >= range && oldScrollY < range) {
                            mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
                        }
                    }
                }
            }

            // Finally update the scroll positions and post an invalidation
            mLastScrollerY = y;
            ViewCompat.postInvalidateOnAnimation(this);
        } else {
            // We can't scroll any more, so stop any indirect scrolling
            if (hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) {
                stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
            }
            // and reset the scroller y
            mLastScrollerY = 0;
        }
    }

一看到這咱們就明白了,其實fling也就是對應的由手機來模擬咱們觸摸的過程,因此回調調用dispatchNestedPreScroll()dispatchNestedScroll()來進行通知AppBarLayout進行滑動,滑動的過程仍是上面那一套,手指向下滑動時,當NestedScrollView滑動到頂的時候,就交付消費dy給AppBarLayout處理,而手指向上滑動時候則相反。

相關文章
相關標籤/搜索