4AppBarLayout滑動(dòng)原理
在CoordinatorLayout的measure和layout里矗晃,其實(shí)介紹過一點(diǎn)AppBarLayout旗唁,這篇將重點(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.
簡單的整理下,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。簡單來說怎爵,這個(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(具體后邊分析)茉稠,所以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,在這里把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事件的代碼比較簡單桐玻,判斷超過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里面轿衔,簡單調(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來制約缨伊,很簡單摘刑。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)分析過簡單情況下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ā)揮作用的
AppBarLayout的range
mTotalScrollRange 525
mDownPreScrollRange -1
mDownScrollRange 525
總結(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.reibang.com/p/99adaad8d55c
https://code.google.com/p/android/issues/detail?id=177729