這篇將重點(diǎn)講解AppBarLayout的滑動(dòng)原理以及behavior是如何影響onTouchEvent與onInterceptTouchEvent的。
基本原理
介紹AppBarLayout的mTotalScrollRange,mDownPreScrollRange,mDownScrollRange,滑動(dòng)的基本概念
mTotalScrollRange內(nèi)部可以滑動(dòng)的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.
簡(jiǎn)單的整理下,AppBarLayout是一個(gè)vertical的LinearLayout,實(shí)現(xiàn)了很多material的概念,主要是跟滑動(dòng)相關(guān)的。AppBarLayout的子view需要提供layout_scrollFlags參數(shù)。AppBarLayout和CoordinatorLayout強(qiáng)相關(guān),一般作為CoordinatorLayout的子類,配套使用。
按我的理解,AppBarLayout內(nèi)部有2種view,一種可滑出(屏幕),另一種不可滑出,根據(jù)app:layout_scrollFlags區(qū)分。一般上邊放可滑出的下邊放不可滑出的。
舉個(gè)例子如下,內(nèi)有個(gè)Toolbar、TextView,Toolbar寫了app:layout_scrollFlags=”scroll”表示可滑動(dòng),Toolbar高200dp,TextView高100dp。Toolbar就是可滑出的,TextView就是不可滑出的。此時(shí)框高300(200+100),內(nèi)容300,可滑動(dòng)范圍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>
效果如下所示
這個(gè)跟ScrollView有所不同,框的大小和內(nèi)容大小一樣,這樣上滑的時(shí)候,底部必然會(huì)空出一部分(200),ScrollView的實(shí)現(xiàn)是通過修改scrollY,而AppBarLayout的實(shí)現(xiàn)是直接修改top和bottom的,其實(shí)就是把整個(gè)AppBarLayout內(nèi)部的東西往上平移。
down事件
來看看上圖的事件傳遞的順序,先看down。簡(jiǎn)單來說,這個(gè)down事件被傳遞下來,一直無人處理,然后往上傳到CoordinatorLayout被處理。但實(shí)際上CoordinatorLayout本身無法處理事件(他只是個(gè)殼),內(nèi)部實(shí)際交由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);
//此處會(huì)分發(fā)事件給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循環(huán)里調(diào)用behavior的onTouchEvent。此時(shí)AppBarLayout.Behavior的onTouchEvent會(huì)返回true(具體后邊分析, 這里第一次調(diào)用onTouchEvent()),所以intercepted就為true,mBehaviorTouchView就會(huì)設(shè)置為AppBarLayout,然后performIntercept結(jié)束返回true。這個(gè)mBehaviorTouchView就相當(dāng)于一般的ViewGroup里的mFirstTouchTarget的作用。
再回頭看上邊代碼,performIntercept返回true了,那就能進(jìn)入L13,會(huì)調(diào)用mBehaviorTouchView.behavior.onTouchEvent(這里第二次調(diào)用onTouchEvent()),在這里把CoordinatorLayout的onTouchEvent,傳遞給了AppBarLayout.Behavior的onTouchEvent。
而L16也會(huì)返回true,那整個(gè)CoordinatorLayout的onTouchEvent就返回true了,按照事件分發(fā)的規(guī)則,此時(shí)這個(gè)down事件被CoordinatorLayout消費(fèi)了。但是實(shí)際上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
上文說了“此時(shí)AppBarLayout.Behavior的onTouchEvent會(huì)返回true”,我們來具體分析下。來看AppBarLayout.Behavior的onTouchEvent。AppBarLayout.Behavior的onTouchEvent代碼在HeaderBehavior內(nèi),看L12只要觸摸點(diǎn)在AppBarLayout內(nèi),而且canDragView,那就返回true,否則返回false。在AppBarLayout內(nèi)明顯是滿足的,那就看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,此時(shí)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事件的過程中,調(diào)用了2次AppBarLayout.Behavior的onTouchEvent
MOVE事件
由上文可知down事件被CoordinatorLayout消費(fèi),所以move事件不會(huì)走到CoordinatorLayout的onInterceptTouchEvent,而直接進(jìn)入onTouchEvent。此時(shí)mBehaviorTouchView就是AppBarLayout。看L10,直接進(jìn)入,然后把move事件發(fā)給了AppBarLayout.Behavior。
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean cancelSuper = false;
MotionEvent cancelEvent = null;
final int action = MotionEventCompat.getActionMasked(ev);
//此處會(huì)分發(fā)事件給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事件的代碼比較簡(jiǎn)單,判斷超過mTouchSlop就調(diào)用scroll,而scroll等于調(diào)用setHeaderTopBottomOffset。這里主要關(guān)注scroll的后2個(gè)參數(shù),minOffset和maxOffset,minOffset傳的是getMaxDragOffset(child)即AppBarlayout的-mDownScrollRange。這里就是AppBarlayout的可滑動(dòng)范圍,即toolbar的高度(包括margin)的負(fù)值。minOffset和maxOffset代表的是滑動(dòng)上下限制,這個(gè)很好理解,因?yàn)橐苿?dòng)的時(shí)候改的是top和bottom,比如top范圍就是[initTop-滑動(dòng)范圍,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里面,簡(jiǎn)單調(diào)用setHeaderTopBottomOffset,重點(diǎn)看第三個(gè)參數(shù)getTopBottomOffsetForScrollingSibling() - dy,這個(gè)算出來的就是經(jīng)過這次move即將到達(dá)的offset(不是top哦,top=offset+mLayoutTop)。getTopBottomOffsetForScrollingSibling就是獲取當(dāng)前的偏移量,這個(gè)命名我不太理解。setHeaderTopBottomOffset就是給header設(shè)置一個(gè)新的offset,這個(gè)offset用一個(gè)min一個(gè)max來制約,很簡(jiǎn)單。setHeaderTopBottomOffset可以認(rèn)為就是view的offsetTopAndBottom,調(diào)整top和bottom達(dá)到平移的效果
發(fā)現(xiàn)AppBarlayout對(duì)getTopBottomOffsetForScrollingSibling復(fù)寫了,加了個(gè)mOffsetDelta,但是mOffsetDelta一直是0.
@Override
int getTopBottomOffsetForScrollingSibling() {
return getTopAndBottomOffset() + mOffsetDelta;
}
measure過程
在http://blog.csdn.net/litefish/article/details/52327502曾經(jīng)分析過簡(jiǎn)單情況下CoordinatorLayout的布局過程。這里稍有變化,主要在于第三次measure RelativeLayout的時(shí)候getScrollRange不再是0
final int height = availableHeight - header.getMeasuredHeight()
- getScrollRange(header);
就是availableHeight-AppBar.measuredheight+toolbar高度,結(jié)果就是availableHeight。
所以此時(shí)RelativeLayout的最終measure高度是1731,這個(gè)高度是有意義的,他比不可滾動(dòng)的appbar多了一個(gè)toolbar的高度,這么高的一個(gè)RelativeLayout在當(dāng)前屏幕是放不下的,所以RelativeLayout往往會(huì)用一個(gè)可滾動(dòng)的view來替換,比如Recyclerview或者NestedScrollView。
上滑可以滑到狀態(tài)欄
上滑用的是setTopAndBottomOffset,并不會(huì)重新measure,layout,而fitSystemWindow是在measure,layout的時(shí)候發(fā)揮作用的
總結(jié)
1、ScrollView滑動(dòng)的實(shí)現(xiàn)是通過修改scrollY,而AppBarLayout的實(shí)現(xiàn)是通過直接修改top和bottom的,其實(shí)就是把整個(gè)AppBarLayout內(nèi)部的東西往上平移。
2、CoordinatorLayout里的mBehaviorTouchView就相當(dāng)于一般的ViewGroup里的mFirstTouchTarget的作用
3、和嵌套滑動(dòng)一樣始終只有一個(gè)view可以fling,不可能A fling完 B fling
參考文章
http://dk-exp.com/2016/03/30/CoordinatorLayout/
http://www.lxweimin.com/p/99adaad8d55c
https://code.google.com/p/android/issues/detail?id=177729