今天学习整理一下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处理,而手指向上滑动时候则相反。