今天学习整理一下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一起上移。
底下代码分析建立在下面例子之中:
<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>
那么现在我们直接看是看源码吧,这里主要弄明白两个逻辑:
- 当手指触摸AppBarLayout时候的滑动逻辑。
- 当手指触摸NestedScrollView时的滑动逻辑。
当手指触摸AppBarLayout时候的滑动逻辑
在弄清手指触摸AppBarLayout时候的滑动逻辑,需要了解一下AppBarLayout.Behavior
这个类,ApprBarLayout的默认Behavior就是AppBarLayout.Behavior
这个类,而AppBarLayout.Behavior
继承自HeaderBehavior
,HeaderBehavior
又继承自ViewOffsetBehavior
,这里先总结一下两个类的作用,需要详细的实现的请自行阅读源码吧:
- ViewOffsetBehavior:该Behavior主要运用于View的移动,从名字就可以看出来,该类中提供了上下移动,左右移动的方法。
- HeaderBehavior:该类主要用于View处理触摸事件以及触摸后的fling事件。
由于上面两个类功能的实现,使得AppBarLayout.Behavior
具有了同时移动本身以及处理触摸事件的功能,在CoordinatorLayout四部曲学习之二:CoordinateLayout源码学习这篇文章又说明了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;
}
通过上面方法,并且结合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处理,而手指向上滑动时候则相反。