这篇将重点讲解AppBarLayout的滑动原理以及behavior是如何影响onTouchEvent与onInterceptTouchEvent的。
基本原理
介绍AppBarLayout的mTotalScrollRange,mDownPreScrollRange,mDownScrollRange,滑动的基本概念
mTotalScrollRange内部可以滑动的view的高度(包括上下margin)总和
官方介绍
先来看看google的介绍
AppBarLayout is a vertical LinearLayout which implements many of the features of material designs app bar concept, namely scrolling gestures.
Children should provide their desired scrolling behavior through setScrollFlags(int) and the associated layout xml attribute: app:layout_scrollFlags.
This view depends heavily on being used as a direct child within a CoordinatorLayout. If you use AppBarLayout within a different ViewGroup, most of it’s functionality will not work.
AppBarLayout also requires a separate scrolling sibling in order to know when to scroll. The binding is done through the AppBarLayout.ScrollingViewBehavior behavior class, meaning that you should set your scrolling view’s behavior to be an instance of AppBarLayout.ScrollingViewBehavior. A string resource containing the full class name is available.
简单的整理下,AppBarLayout是一个vertical的LinearLayout,实现了很多material的概念,主要是跟滑动相关的。AppBarLayout的子view需要提供layout_scrollFlags参数。AppBarLayout和CoordinatorLayout强相关,一般作为CoordinatorLayout的子类,配套使用。
按我的理解,AppBarLayout内部有2种view,一种可滑出(屏幕),另一种不可滑出,根据app:layout_scrollFlags区分。一般上边放可滑出的下边放不可滑出的。
举个例子如下,内有个Toolbar、TextView,Toolbar写了app:layout_scrollFlags=”scroll”表示可滑动,Toolbar高200dp,TextView高100dp。Toolbar就是可滑出的,TextView就是不可滑出的。此时框高300(200+100),内容300,可滑动范围200
总高度300,可滑出部分高度200,剩下100不可滑出
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll"
app:popupTheme="@style/AppTheme.PopupOverlay" />
<TextView
android:background="#ff0000"
android:layout_width="match_parent"
android:layout_height="100dp"></TextView>
</android.support.design.widget.AppBarLayout>
效果如下所示
这个跟ScrollView有所不同,框的大小和内容大小一样,这样上滑的时候,底部必然会空出一部分(200),ScrollView的实现是通过修改scrollY,而AppBarLayout的实现是直接修改top和bottom的,其实就是把整个AppBarLayout内部的东西往上平移。
down事件
来看看上图的事件传递的顺序,先看down。简单来说,这个down事件被传递下来,一直无人处理,然后往上传到CoordinatorLayout被处理。但实际上CoordinatorLayout本身无法处理事件(他只是个壳),内部实际交由AppBarLayout的behavior处理。
总体分析
首先,down事件从CoordinatorLayout传到AppBarLayout再到TextView,没人处理,然后回传回来到AppBarLayout的onTouchEvent,不处理,再回传给CoordinatorLayout的onTouchEvent,这里主要看L10 performIntercept,type为TYPE_ON_TOUCH。
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean cancelSuper = false;
MotionEvent cancelEvent = null;
final int action = MotionEventCompat.getActionMasked(ev);
//此处会分发事件给behavior
if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
// Safe since performIntercept guarantees that
// mBehaviorTouchView != null if it returns true
final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
final Behavior b = lp.getBehavior();
if (b != null) {
handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
}
}
// Keep the super implementation correct
if (mBehaviorTouchView == null) {
handled |= super.onTouchEvent(ev);
} else if (cancelSuper) {
if (cancelEvent == null) {
final long now = SystemClock.uptimeMillis();
cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
}
super.onTouchEvent(cancelEvent);
}
if (!handled && action == MotionEvent.ACTION_DOWN) {
}
if (cancelEvent != null) {
cancelEvent.recycle();
}
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors();
}
return handled;
}
再看performIntercept,type为TYPE_ON_TOUCH,首先获取topmostChildList,这是把child按照z轴排序,最上面的排前面,CoordinatorLayout跟FrameLayout类似,越后边的child,在z轴上越靠上。所以,这里topmostChildList就是FloatingActionButton、AppBarLayout。然后在for循环里调用behavior的onTouchEvent。此时AppBarLayout.Behavior的onTouchEvent会返回true(具体后边分析, 这里第一次调用onTouchEvent()),所以intercepted就为true,mBehaviorTouchView就会设置为AppBarLayout,然后performIntercept结束返回true。这个mBehaviorTouchView就相当于一般的ViewGroup里的mFirstTouchTarget的作用。
再回头看上边代码,performIntercept返回true了,那就能进入L13,会调用mBehaviorTouchView.behavior.onTouchEvent(这里第二次调用onTouchEvent()),在这里把CoordinatorLayout的onTouchEvent,传递给了AppBarLayout.Behavior的onTouchEvent。
而L16也会返回true,那整个CoordinatorLayout的onTouchEvent就返回true了,按照事件分发的规则,此时这个down事件被CoordinatorLayout消费了。但是实际上down事件的处理者是AppBarLayout.Behavior。他们之间通过mBehaviorTouchView连接。
private boolean performIntercept(MotionEvent ev, final int type) {
boolean intercepted = false;
boolean newBlock = false;
MotionEvent cancelEvent = null;
final int action = MotionEventCompat.getActionMasked(ev);
final List<View> topmostChildList = mTempList1;
getTopSortedChildren(topmostChildList);
// Let topmost child views inspect first
final int childCount = topmostChildList.size();
for (int i = 0; i < childCount; i++) {
final View child = topmostChildList.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior b = lp.getBehavior();
。。。
if (!intercepted && b != null) {
switch (type) {
case TYPE_ON_INTERCEPT:
intercepted = b.onInterceptTouchEvent(this, child, ev);
break;
case TYPE_ON_TOUCH:
intercepted = b.onTouchEvent(this, child, ev);
break;
}
if (intercepted) {
mBehaviorTouchView = child;
}
}
...
}
topmostChildList.clear();
return intercepted;
}
AppBarLayout.Behavior的onTouchEvent为何返回true
上文说了“此时AppBarLayout.Behavior的onTouchEvent会返回true”,我们来具体分析下。来看AppBarLayout.Behavior的onTouchEvent。AppBarLayout.Behavior的onTouchEvent代码在HeaderBehavior内,看L12只要触摸点在AppBarLayout内,而且canDragView,那就返回true,否则返回false。在AppBarLayout内明显是满足的,那就看canDragView。
@Override
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
if (mTouchSlop < 0) {
mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
}
switch (MotionEventCompat.getActionMasked(ev)) {
case MotionEvent.ACTION_DOWN: {
final int x = (int) ev.getX();
final int y = (int) ev.getY();
if (parent.isPointInChildBounds(child, x, y) && canDragView(child)) {
mLastMotionY = y;
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
ensureVelocityTracker();
} else {
return false;
}
break;
}
。。。
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(ev);
}
return true;
}
下边是AppBarLayout的canDragView,此时mLastNestedScrollingChildRef为null,所以走的是L16,返回true,那回头看上边的onTouchEvent也返回true。
@Override
boolean canDragView(AppBarLayout view) {
if (mOnDragCallback != null) {
// If there is a drag callback set, it's in control
return mOnDragCallback.canDrag(view);
}
// Else we'll use the default behaviour of seeing if it can scroll down
if (mLastNestedScrollingChildRef != null) {
// If we have a reference to a scrolling view, check it
final View scrollingView = mLastNestedScrollingChildRef.get();
return scrollingView != null && scrollingView.isShown()
&& !ViewCompat.canScrollVertically(scrollingView, -1);
} else {
// Otherwise we assume that the scrolling view hasn't been scrolled and can drag.
return true;
}
}
ps
可以看出在CoordinatorLayout的onTouchEvent处理down事件的过程中,调用了2次AppBarLayout.Behavior的onTouchEvent
MOVE事件
由上文可知down事件被CoordinatorLayout消费,所以move事件不会走到CoordinatorLayout的onInterceptTouchEvent,而直接进入onTouchEvent。此时mBehaviorTouchView就是AppBarLayout。看L10,直接进入,然后把move事件发给了AppBarLayout.Behavior。
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean cancelSuper = false;
MotionEvent cancelEvent = null;
final int action = MotionEventCompat.getActionMasked(ev);
//此处会分发事件给behavior
if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
// Safe since performIntercept guarantees that
// mBehaviorTouchView != null if it returns true
final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
final Behavior b = lp.getBehavior();
if (b != null) {
handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
}
}
。。。
return handled;
}
AppBarLayout.Behavior处理move事件的代码比较简单,判断超过mTouchSlop就调用scroll,而scroll等于调用setHeaderTopBottomOffset。这里主要关注scroll的后2个参数,minOffset和maxOffset,minOffset传的是getMaxDragOffset(child)即AppBarlayout的-mDownScrollRange。这里就是AppBarlayout的可滑动范围,即toolbar的高度(包括margin)的负值。minOffset和maxOffset代表的是滑动上下限制,这个很好理解,因为移动的时候改的是top和bottom,比如top范围就是[initTop-滑动范围,initTop],所以这里的minOffset是-mDownScrollRange,maxOffset是0.
//HeaderBehavior
@Override
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
if (mTouchSlop < 0) {
mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
}
switch (MotionEventCompat.getActionMasked(ev)) {
case MotionEvent.ACTION_MOVE: {
final int activePointerIndex = MotionEventCompat.findPointerIndex(ev,
mActivePointerId);
if (activePointerIndex == -1) {
return false;
}
final int y = (int) MotionEventCompat.getY(ev, 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;
}
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(ev);
}
return true;
}
final int scroll(CoordinatorLayout coordinatorLayout, V header,
int dy, int minOffset, int maxOffset) {
return setHeaderTopBottomOffset(coordinatorLayout, header,
getTopBottomOffsetForScrollingSibling() - dy, minOffset, maxOffset);
}
再看scroll里面,简单调用setHeaderTopBottomOffset,重点看第三个参数getTopBottomOffsetForScrollingSibling() - dy,这个算出来的就是经过这次move即将到达的offset(不是top哦,top=offset+mLayoutTop)。getTopBottomOffsetForScrollingSibling就是获取当前的偏移量,这个命名我不太理解。setHeaderTopBottomOffset就是给header设置一个新的offset,这个offset用一个min一个max来制约,很简单。setHeaderTopBottomOffset可以认为就是view的offsetTopAndBottom,调整top和bottom达到平移的效果
发现AppBarlayout对getTopBottomOffsetForScrollingSibling复写了,加了个mOffsetDelta,但是mOffsetDelta一直是0.
@Override
int getTopBottomOffsetForScrollingSibling() {
return getTopAndBottomOffset() + mOffsetDelta;
}
measure过程
在http://blog.csdn.net/litefish/article/details/52327502曾经分析过简单情况下CoordinatorLayout的布局过程。这里稍有变化,主要在于第三次measure RelativeLayout的时候getScrollRange不再是0
final int height = availableHeight - header.getMeasuredHeight()
- getScrollRange(header);
就是availableHeight-AppBar.measuredheight+toolbar高度,结果就是availableHeight。
所以此时RelativeLayout的最终measure高度是1731,这个高度是有意义的,他比不可滚动的appbar多了一个toolbar的高度,这么高的一个RelativeLayout在当前屏幕是放不下的,所以RelativeLayout往往会用一个可滚动的view来替换,比如Recyclerview或者NestedScrollView。
上滑可以滑到状态栏
上滑用的是setTopAndBottomOffset,并不会重新measure,layout,而fitSystemWindow是在measure,layout的时候发挥作用的
总结
1、ScrollView滑动的实现是通过修改scrollY,而AppBarLayout的实现是通过直接修改top和bottom的,其实就是把整个AppBarLayout内部的东西往上平移。
2、CoordinatorLayout里的mBehaviorTouchView就相当于一般的ViewGroup里的mFirstTouchTarget的作用
3、和嵌套滑动一样始终只有一个view可以fling,不可能A fling完 B fling
参考文章
http://dk-exp.com/2016/03/30/CoordinatorLayout/
http://www.jianshu.com/p/99adaad8d55c
https://code.google.com/p/android/issues/detail?id=177729