在CoordinatorLayout的measure和layout里,其實介紹過1點AppBarLayout,這篇將重點講授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是1個vertical的LinearLayout,實現了很多material的概念,主要是跟滑動相干的。AppBarLayout的子view需要提供layout_scrollFlags參數。AppBarLayout和CoordinatorLayout強相干,1般作為CoordinatorLayout的子類,配套使用。
按我的理解,AppBarLayout內部有2種view,1種可滑出(屏幕),另外一種不可滑出,根據app:layout_scrollFlags辨別。1般上邊放可滑出的下邊放不可滑出的。
舉個例子以下,內有個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有所不同,框的大小和內容大小1樣,這樣上滑的時候,底部必定會空出1部份(200),ScrollView的實現是通過修改scrollY,而AppBarLayout的實現是直接修改top和bottom的,其實就是把全部AppBarLayout內部的東西往上平移。
來看看上圖的事件傳遞的順序,先看down。簡單來講,這個down事件被傳遞下來,1直無人處理,然后往上傳到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(具體后邊分析),所以intercepted就為true,mBehaviorTouchView就會設置為AppBarLayout,然后performIntercept結束返回true。這個mBehaviorTouchView就相當于1般的ViewGroup里的mFirstTouchTarget的作用。再回頭看上邊代碼,performIntercept返回true了,那就可以進入L13,會調用mBehaviorTouchView.behavior.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。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;
}
}
可以看出在CoordinatorLayout的onTouchEvent處理down事件的進程中,調用了2次AppBarLayout.Behavior的onTouchEvent
由上文可知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,重點看第3個參數getTopBottomOffsetForScrollingSibling() - dy,這個算出來的就是經過這次move行將到達的offset(不是top哦,top=offset+mLayoutTop)。getTopBottomOffsetForScrollingSibling就是獲得當前的偏移量,這個命名我不太理解。setHeaderTopBottomOffset就是給header設置1個新的offset,這個offset用1個min1個max來制約,很簡單。setHeaderTopBottomOffset可以認為就是view的offsetTopAndBottom,調劑top和bottom到達平移的效果
發現AppBarlayout對getTopBottomOffsetForScrollingSibling復寫了,加了個mOffsetDelta,但是mOffsetDelta1直是0.
@Override
int getTopBottomOffsetForScrollingSibling() {
return getTopAndBottomOffset() + mOffsetDelta;
}
在http://blog.csdn.net/litefish/article/details/52327502曾分析過簡單情況下CoordinatorLayout的布局進程。這里稍有變化,主要在于第3次measure RelativeLayout的時候getScrollRange不再是0
final int height = availableHeight - header.getMeasuredHeight()
+ getScrollRange(header);
就是availableHeight-AppBar.measuredheight+toolbar高度,結果就是availableHeight。
所以此時RelativeLayout的終究measure高度是1731,這個高度是成心義的,他比不可轉動的appbar多了1個toolbar的高度,這么高的1個RelativeLayout在當前屏幕是放不下的,所以RelativeLayout常常會用1個可轉動的view來替換,比如Recyclerview或NestedScrollView。
上滑用的是setTopAndBottomOffset,其實不會重新measure,layout,而fitSystemWindow是在measure,layout的時候發揮作用的
mTotalScrollRange 525
mDownPreScrollRange ⑴
mDownScrollRange 525
1、ScrollView滑動的實現是通過修改scrollY,而AppBarLayout的實現是通過直接修改top和bottom的,其實就是把全部AppBarLayout內部的東西往上平移。
2、CoordinatorLayout里的mBehaviorTouchView就相當于1般的ViewGroup里的mFirstTouchTarget的作用
3、和嵌套滑動1樣始終只有1個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