今天學習整理一下AppBarLayout與CoordinatorLayout以及Behavior交互邏輯的過程,首先使用一張圖先歸納一下各個類主要功能吧(本文章使用NestedScrollView充當滑動的內嵌子View)。java
底下代碼分析創建在下面例子之中: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
在弄清手指觸摸AppBarLayout時候的滑動邏輯,須要瞭解一下AppBarLayout.Behavior
這個類,ApprBarLayout的默認Behavior就是AppBarLayout.Behavior
這個類,而AppBarLayout.Behavior
繼承自HeaderBehavior
,HeaderBehavior
又繼承自ViewOffsetBehavior
,這裏先總結一下兩個類的做用,須要詳細的實現的請自行閱讀源碼吧:ide
因爲上面兩個類功能的實現,使得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也實現了一個ScrollingViewBehavior
,ScrollingViewBehavior
也繼承自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時的滑動邏輯,在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的值有兩種狀況:
回到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處理,而手指向上滑動時候則相反。