今天学习整理一下AppBarLayout与CoordinatorLayout以及Behavior交互逻辑的过程,首先使用一张图先概括一下各个类主要功能吧(本文章使用NestedScrollView充当滑动的内嵌子View)。
- CoordinatorLayout实现NestedScrollingParent2接口,用于处理与滑动子View的联动交互(这里使用的是NestedScrollView),实际上交由Behavior进行处理,CoordinatorLayout为其代理类。
- AppBarLayout中默认使用了AppBarLayout.Behavior,主要功能是接收CoordinatorLayout传输过来的滑动事件,并且相对应的进行处理,如NestedScrollView往上滑动到头时候,继续滑动则移动AppBarLayout到头。
- NestedScrollView实现了NestedScrollingChild2接口,用于传输给CoordinatorLayout,并且消费CoordinatorLayout不消费的触摸事件,其中还是使用了AppBarLayout.ScrollingViewBehavior,功能是进行监听AppBarLayout的位移变化,从而进行相对应的变化,最明显的例子就是AppBarLayout上移过程中,NestedScrollView一起上移。
底下代码分析建立在下面例子之中:
....
那么现在我们直接看是看源码吧,这里主要弄明白两个逻辑:
- 当手指触摸AppBarLayout时候的滑动逻辑。
- 当手指触摸NestedScrollView时的滑动逻辑。
当手指触摸AppBarLayout时候的滑动逻辑
在弄清手指触摸AppBarLayout时候的滑动逻辑,需要了解一下AppBarLayout.Behavior
这个类,ApprBarLayout的默认Behavior就是AppBarLayout.Behavior
这个类,而AppBarLayout.Behavior
继承自HeaderBehavior
,HeaderBehavior
又继承自ViewOffsetBehavior
,这里先总结一下两个类的作用,需要详细的实现的请自行阅读源码吧:
- ViewOffsetBehavior:该Behavior主要运用于View的移动,从名字就可以看出来,该类中提供了上下移动,左右移动的方法。
- HeaderBehavior:该类主要用于View处理触摸事件以及触摸后的fling事件。
由于上面两个类功能的实现,使得AppBarLayout.Behavior
具有了同时移动本身以及处理触摸事件的功能,在这篇文章又说明了CoordinateLayout的NestedScrollingParent2的实现全权委托给了Behavior类,所以AppBarLayout.Behavior
就提供了ApprBarLayout对应的联动的方案。
那么我们直接从一开始入手,当我们手碰到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(...)做处理:
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
中的源码即可:
@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
源码中发现其实现了如下方法:
@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; }
通过上面方法,并且结合该文章的分析可以知道,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时的滑动逻辑,在文章中分析过,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处理,而手指向上滑动时候则相反。