下圖是CoordinatorLayout佈局中很常見的一種效果,很多人應該都見過,當我們用手指滑動RecyclerView的時候,不單止RecyclerView會上下滑動,頂部的Toolbar也會隨着RecyclerView的滑動隱藏或顯現,實現代碼的佈局如下:
具體代碼:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways|snap"
android:theme=
"@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme=
"@style/ThemeOverlay.AppCompat.Light" />
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior=
"@string/appbar_scrolling_view_behavior" />
</android.support.design.widget.CoordinatorLayout>
只要父佈局是CoordinatorLayout,然後在Toolbar的外層包上一個AppBarLayout,在Toolbar上添加屬性layout_scrollFlags=”scroll|enterAlways|snap”,在RecyclerView上添加屬性layout_behavior=”@string/appbar_scrolling_view_behavior”,並把AppBarLayout與RecyclerView作爲CoordinatorLayout的子控件,就能實現。
實現的方法知道了,但是我們不能單純滿足於此,接下來我們對原理進行分析
實現以上效果主要是涉及了嵌套滑動機制和Behavior兩個知識點。
1、嵌套滑動機制(NestedScrolling)
根據事件分發機制,我們知道觸摸事件最終只會由一個控件進行處理,當我們滑動RecyclerView時,事件最終肯定是傳給了RecyclerView,並交給它進行處理,Toolbar是不應該能夠接收到事件並響應的。我們無法依靠默認的事件分發機制完成gif圖上的效果的(當然,我們通過自定義View,修改事件分發是可以實現這個效果)。
因此Google給我們提供了嵌套滑動機制。通過嵌套滑動機制,RecyclerView能夠把自身接受到的點擊滑動事件傳遞給父佈局CoordinatorLayout,然後CoordinatorLayout把接收到的事件傳遞給子佈局AppBarLayout(Toolbar的父佈局),最終滑動事件交給了AppBarLayout進行處理,完成使Toolbar滾出滾進界面等效果。
這裏 NestedScrolling 兩個重要的概念提及一下
- NestedScrollingParent NestedScrollingParentHelper
- NestedScrollingChild NestedScrollingChildHelper
巧合的是 CoordinatorLayout 已經實現了 NestedScrollingParent 接口,所以我們配合一個實現了 NestedScrollingChild 接口的 View 就可以輕鬆的實現以上效果
一般而言,父佈局會實現NestedScrollingParent,而滑動列表作爲子控件實現NestedScrollingChild,並把事件傳給父佈局,父佈局再根據情況把事件分發到其它子View。而NestedScrollingParentHelper和NestedScrollingChildHelper分別是NestedScrollingParent和NestedScrollingChild的輔助類,具體的邏輯會委託給它們執行。
接下來我們看一下CoordinatorLayout和RecyclerView的源碼:
public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent {
......
}
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {
......
}
通過源碼可發現CoordinatorLayout實現了NestedScrollingParent,而RecyclerView實現了NestedScrollingChild。毫無疑問,RecyclerView就是通過嵌套滑動機制把滑動事件傳給了CoordinatorLayout,然後CoordinatorLayout把事件傳遞到AppBarLayout中。
那麼實現這些接口需要實現哪些方法呢?我們通過源碼來了解下:
public interface NestedScrollingChild {
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
public boolean hasNestedScrollingParent();
public boolean isNestedScrollingEnabled();
public void setNestedScrollingEnabled(boolean enabled);
public boolean startNestedScroll(int axes);
public void stopNestedScroll();
}
public interface NestedScrollingParent {
public int getNestedScrollAxes();
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
public void onStopNestedScroll(View target);
}
我整理的是NestedScrollingChild&NestedScrollingParent接口,26版本又添加了新的方法分別繼承自NestedScrollingChild&NestedScrollingParent接口 ,看起來要實現的方法很多,也很複雜的樣子,但是實質上通過輔助類NestedScrollingChildHelper和NestedScrollingParentHelper能大大減輕工作量,而且有些方法僅僅是作一個判斷,並不需要很複雜的邏輯。在後面的源碼驗證環節中我們也只會着重分析到重點的幾個方法。
在這裏先說幾個比較重要的方法的調用流程與對應關係:
- NestedScrollingChild接口的startNestedScroll會在Down事件觸發的時候調用,對應NestedScrollingParent的onStartNestedScroll。
- NestedScrollingChild接口的dispatchNestedPreScroll會在Move事件觸發的時候調用,對應NestedScrollingParent的onNestedPreScroll。
- NestedScrollingChild接口的dispatchNestedScroll會在Move事件觸發的時候調用,對應NestedScrollingParent的onNestedScroll。
- NestedScrollingChild接口的stopNestedScroll會在Up事件觸發的時候調用,對應NestedScrollingParent的onStopNestedScroll。
2、深入理解 Behavior
2.1攔截 Touch 事件
當我們爲一個 CoordinatorLayout
的直接子 View 設置了 Behavior 時,這個 Behavior 就能攔截髮生在這個 View 上的 Touch 事件,那麼它是如何做到的呢?實際上, CoordinatorLayout
重寫了 onInterceptTouchEvent()
方法,並在其中給 Behavior 開了個後門,讓它能夠先於 View 本身處理 Touch 事件。
具體來說, CoordinatorLayout
的 onInterceptTouchEvent()
方法中會遍歷所有直接子 View ,對於綁定了 Behavior 的直接子 View 調用 Behavior 的 onInterceptTouchEvent() 方法,若這個方法返回 true, 那麼後續本該由相應子 View 處理的 Touch 事件都會交由 Behavior 處理,而 View 本身表示懵逼,完全不知道發生了什麼。
CoordinatorLayout 的onInterceptTouchEvent 方法
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
// Make sure we reset in case we had missed a previous important event.
if (action == MotionEvent.ACTION_DOWN) {
// 先讓子 view 種包含Behavior的控件 處理觸摸事件
resetTouchBehaviors();
}
final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
if (cancelEvent != null) {
cancelEvent.recycle();
}
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors();
}
return intercepted;
}
resetTouchBehaviors 方法內部實現:
private void resetTouchBehaviors() {
if (mBehaviorTouchView != null) {
final Behavior b = ((LayoutParams) mBehaviorTouchView.getLayoutParams()).getBehavior();
if (b != null) {
final long now = SystemClock.uptimeMillis();
final MotionEvent cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
b.onTouchEvent(this, mBehaviorTouchView, cancelEvent);
cancelEvent.recycle();
}
mBehaviorTouchView = null;
}
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
lp.resetTouchBehaviorTracking();
}
mDisallowInterceptReset = false;
}
2.2 攔截測量及佈局
瞭解了 Behavior 是怎養攔截 Touch 事件的,想必大家已經猜出來了它攔截測量及佈局事件的方式 —— CoordinatorLayout 重寫了測量及佈局相關的方法併爲 Behavior 開了個後門。沒錯,真相就是如此。
CoordinatorLayout 在 onMeasure()
方法中,會遍歷所有直接子 View ,若該子 View 綁定了一個 Behavior ,就會調用相應 Behavior 的 onMeasureChild()
方法,若此方法返回 true,那麼 CoordinatorLayout 對該子 View 的測量就不會進行。這樣一來, Behavior 就成功接管了對 View 的測量。
同樣,CoordinatorLayout 在 onLayout()
方法中也做了與 onMeasure()
方法中相似的事,讓 Behavior 能夠接管對相關子 View 的佈局。
我們可以通過Behaviour觀察我們感興趣的控件的事件,並作出相應的操作。
通過在xml中添加layout_behavior屬性可以給控件設置Behaviour,比如在上面的代碼中,就是在RecyclerView中添加屬性
layout_behavior="@string/appbar_scrolling_view_behavior"
將RecyclerView的Behaviour指定成AppBarLayout的內部類ScrollingViewBehavior。
或者通過註解的方式給控件設置Behaviour,比如AppBarLayout就是通過
@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
定義自身的Behavior爲AppBarLayout.Behavior
注意的是,Behavior是CoordinatorLayout的專屬屬性,設置Behavior的控件需要是CoordinatorLayout的子控件。
在我們上面的事例代碼中一共設置有兩個Behavior,第一個就是RecyclerView中通過layout_behavior屬性進行設置的ScrollingViewBehavior,第二個就是AppBarLayout的代碼中通過註解默認設置的一個AppBarLayout.Behavior.class。
當我們要依賴另一個view的狀態變化,例如大小、顯示、位置狀態,我們至少應該重寫以下兩個方法:
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
return false;
}
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
return false;
}
第一個方法負責決定依賴哪個View,第二個方法負責根據依賴的View的變化做出響應。
我們的事例中給RecycleView設置的ScrollingViewBehavior也實現了這兩個方法,使得RecycleView一直處於AppBarLayout的下方。
當我們要依賴某個實現了NestedScrollingChild的View的滑動狀態時,應該重寫以下方法:
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
V child, View directTargetChild, View target, int nestedScrollAxes) {
return false;
}
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
int dx, int dy, int[] consumed) {
// Do nothing
}
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
float velocityX, float velocityY) {
return false;
}
onStartNestedScroll決定Behavior是否接收嵌套滑動機制傳過來的事件;onNestedPreScroll負責接收依賴的View滑動的滑動事件並處理;onNestedPreFling負責接收快速滑動時的慣性滑動事件並處理。
我們的事例中AppBarLayout通過註解設置的AppBarLayout.Behavior實現了這3個方法,使得AppBarLayout能夠接收到RecycleView傳來的滑動事件並響應。
3、聯動分析
我們滑動RecyclerView的時候,RecyclerView會通過滑動嵌套機制把接收到的事件傳給CoordinatorLayout,然後CoordinatorLayout把事件傳給AppBarLayout,AppBarLayout再根據自身的Behavior(AppBarLayout.Behavior.class)做相應的處理,判斷是否處理該滑動事件,如果不處理,則事件仍交還給RecyclerView,如果處理,就做出相應的操作,例如將Toolbar滾出或者滾進屏幕,並消耗掉需要的滑動事件。
這時候可能會有人有疑問:當AppBarLayout處理並消耗了RecyclerView傳遞的滑動事件的時候(既Toolbar上下滑動時),RecyclerView爲什麼也還能跟隨着手指上下移動呢?其實這裏RecyclerView並不是跟隨着手指移動,而是一直保持在AppBarLayout的正下方。這是因爲我們在RecyclerView中添加屬性『layout_behavior="@string/appbar_scrolling_view_behavior"』
給RecyclerView指定了AppBarLayout$ScrollingViewBehavior,這個Behavior會觀察AppBarLayout,當AppBarLayout發生變化時做出相應的操作。正是因爲這樣,就算RecyclerView把滑動事件交給AppBarLayout處理並消耗掉,它也還能一直保持在AppBarLayout的正下方。
總結:當我們滑動RecyclerView時,Toolbar能上下滾動是由嵌套滑動機制和AppBarLayout.Behavior共同工作完成的。而在Toolbar上下滾動時,RecyclerView也能始終保持在其正下方的功能是由ScrollingViewBehavior實現的。
4、源碼分析
4.1 Toolbar能隨RecycleView上下滾動原理:
我們先來分析一下RecyclerView是如何把滑動事件傳給CoordinatorLayout,即NestedScrollingChild把事件傳給NestedScrollingParent,以及接收到事件的CoordinatorLayout又如何把事件分發到AppBarLayout的Behavior上。
事件分發是從Down開始的,因此我們先從RecyclerView的Down事件開始分析
@Override
public boolean onTouchEvent(MotionEvent e) {
......
switch (action) {
case MotionEvent.ACTION_DOWN: {
mScrollPointerId = e.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis);
} break;
......
}
可以看到在RecyclerView的Down事件的最後一行,我們調用了NestedScrollingChild接口的startNestedScroll(nestedScrollAxis)方法,並把支持的滾動方向作爲參數傳了進去,這個方法也是嵌套滑動機制中被調用的第一個方法,在這個方法內會決定是否啓用嵌套滑動,以及誰來接收處理嵌套滑動傳過來的事件。
然後我們來看看startNestedScroll(nestedScrollAxis)方法的內部實現。
@Override
public boolean startNestedScroll(int axes) {
return getScrollingChildHelper().startNestedScroll(axes);
}
......
private NestedScrollingChildHelper getScrollingChildHelper() {
if (mScrollingChildHelper == null) {
mScrollingChildHelper = new NestedScrollingChildHelper(this);
}
return mScrollingChildHelper;
}
startNestedScroll(int axes)方法實質上是通過代理的方式,把邏輯委託給了NestedScrollingChildHelper。那麼我們來看下NestedScrollingChildHelper的startNestedScroll(int axes)做了什麼:
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
mNestedScrollingParent = p;
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
首先調用了NestedScrollingChild接口的實現方法hasNestedScrollingParent(),其內部邏輯是判斷mNestedScrollingParent是否等於null,如果不是,則代表嵌套滑動已經開始,就直接return true,不繼續往下走。
一般開始的時候mNestedScrollingParent在這裏都是還沒賦值,是爲null的,所以可以繼續往下走,接下來通過NestedScrollingChild接口的isNestedScrollingEnabled()方法判斷是不是支持NestedScrolling,這裏默認是爲ture,所以我們繼續往下走。
接下來調用了mView.getParent(),通過查看RecyclerView的getScrollingChildHelper()方法,以及NestedScrollingChildHelper的構造函數可知,其實就是調用了RecyclerView的getParent()方法,而RecyclerView的父佈局是CoordinatorLayout,所以得到的ViewParent p就是CoordinatorLayout。
然後在while循環中通過ViewParentCompat.onStartNestedScroll(p, child, mView, axes)方法不斷尋找需要接收處理RecyclerView分發過來的事件的父佈局,如果找到了,就返回true,這時候就會執行if語句中的代碼,把接收事件的父佈局賦值mNestedScrollingParent。並且調用ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes),並且最後整個方法再返回true,startNestedScroll方法就算是跑完了。
在我們的事例代碼中,while循環就只執行一次,把CoordinatorLayout、RecyclerView和axes作爲值傳了進去。在這裏child和mView都是同一個RecyclerView。
既然while循環只執行一次,那就代表ViewParentCompat.onStartNestedScroll(p, child, mView, axes)方法在第一次執行的時候就已經返回true了,也就是代表RecyclerView的直接父佈局CoordinatorLayout會接收處理RecyclerView分發過來的事件。那麼我們就來看下ViewParentCompat.onStartNestedScroll到底寫了什麼邏輯。爲了方便,我們分析5.0以上的源碼(與5.0以下的源碼的主要區別在於5.0以下的源碼多做了一些版本兼容工作)。
ViewParentCompat.onStartNestedScroll最終調用到ViewParentCompatLollipop的onStartNestedScroll方法:
class ViewParentCompatLollipop {
private static final String TAG = "ViewParentCompat";
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes) {
try {
return parent.onStartNestedScroll(child, target, nestedScrollAxes);
} catch (AbstractMethodError e) {
Log.e(TAG, "ViewParent " + parent + " does not implement interface " +
"method onStartNestedScroll", e);
return false;
}
}
......
}
在ViewParentCompatLollipop的onStartNestedScroll方法中,其實主要就一句話:
『return parent.onStartNestedScroll(child, target, nestedScrollAxes);』
這個parent則是從ViewParentCompat.onStartNestedScroll(p, child, mView, axes)方法傳過來的p,也就是CoordinatorLayout。
通過這麼一系列的調用,最終從RecyclerView的startNestedScroll方法,調用到了CoordinatorLayout的onStartNestedScroll方法。那麼接下來我們就去看下CoordinatorLayout的onStartNestedScroll方法中做了什麼。
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
nestedScrollAxes);
handled |= accepted;
lp.acceptNestedScroll(accepted);
} else {
lp.acceptNestedScroll(false);
}
}
return handled;
}
在這個方法中,CoordinatorLayout遍歷了它的子佈局並獲取對應的Behavior,如果Behavior不爲空,則根據該Behavior的onStartNestedScroll來決定是否把接收來的事件發放給該Behavior所屬的View,並返回Behavior的onStartNestedScroll方法的返回值。由於handled |= accepted,只要有一個Behavior的onStartNestedScroll方法返回true,handled就會是ture。
也就是:AppBarLayout是否接收事件並處理,是RecyclerView通過嵌套滑動原理,把事件傳給CoordinatorLayout,CoordinatorLayout通過遍歷自身的子佈局,找到了AppBarLayout,並根據AppBarLayout的Behavior是否對事件感興趣來決定。
在我們這個實例中一共有兩個View設置了Behavior,究竟是哪個Behavior處理了事件呢?我們先去看下AppBarLayout的Behavior的源碼。AppBarLayout的Behavior我們在上面也已經說過了,是通過註解設置的『@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)』
我們接下來到AppBarLayout.Behavior裏看看它的onStartNestedScroll做了些什麼。
@Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
View directTargetChild, View target, int nestedScrollAxes) {
// Return true if we're nested scrolling vertically, and we have scrollable children
// and the scrolling view is big enough to scroll
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;
}
該方法最終返回一個布爾值started,只有當可垂直滑動、AppBarLayout裏有可以滑動的子View、並且CoordinatorLayout的高減去RecyclerView的高小於等於AppBarLayout的高的時候,started等於true,這些條件在上面的事例中都是符合的,因此最終AppBarLayout.Behavior的onStartNestedScroll方法返回true,也就是嵌套滑動的事件交給了AppBarLayout處理。
我們再去看下RecyclerView中設置的ScrollingViewBehavior的源碼,ScrollingViewBehavior以及它的父類並沒有重寫onStartNestedScroll,所以它的onStartNestedScroll方法既是CoordinatorLayout.Behavior:
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
V child, View directTargetChild, View target, int nestedScrollAxes) {
return false;
}
我們可以看到,ScrollingViewBehavior的onStartNestedScroll方法居然直接返回false了,也就是說它肯定是不會接收通過該方法傳來的事件了。
就這樣,Down事件就大致分析完了。在Down事件中主要是決定嵌套滑動的接收者,以及對相應的View進行標記,方便Move事件的相關滑動操作。
Down事件分析完了,接下來我們就來分析Move事件,由於代碼比較長,我就只截取一部分:
case MotionEvent.ACTION_MOVE: {
......
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
......
break;
}
我們主要關注NestedScrollingParent接口的dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)方法,該方法最終調用到CoordinatorLayout的onNestedPreScroll方法,CoordinatorLayout的onNestedPreScroll又會調用到AppBarLayout.Behavir的onNestedPreScroll,調用流程跟Down事件差不多,具體原理限於篇幅就不再分析了,我們主要dispatchNestedPreScroll方法主要實現了什麼功能。
該方法主要是決定是否需要把Coordinatorlayout接收到的事件分發給AppBarLayout。假設AppBarLayout中的Toolbar已經完全顯示了,而此時RecycleView是在往下滑,這時候Toolbar完全不需要接收事件使自己顯示,此時dispatchNestedPreScroll就會返回false。
接下來我們來關注dispatchNestedPreScroll方法的參數,前兩個分別是橫座標和縱座標的偏移量,這沒啥好解釋的,我們主要來分析後兩個參數的作用。後兩個參數分別是父view消費掉的 scroll長度(CoordinatorLayout分發給AppBarlayout消費掉)和子View(RecycleView)的窗體偏移量。
如果dispatchNestedPreScroll返回true,則會根據後兩個參數來進行修正,例如通過mScrollConsumed更新dx和dy,以及通過mScrollOffset更新RecycleView的窗體偏移量。
假設RecyclerView的滾動事件沒有被消費完,在RecycleView的Move事件最後scrollByInternal方法會繼續處理剩下的滾動事件,並調用NestedScrollingChild接口的dispatchNestedScroll方法,而且最終還是會通過NestedScrollingParent的onNestedScroll調用到Behavior的對應方法。scrollByInternal方法部分核心代碼如下:
......
if (x != 0) {
consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
unconsumedX = x - consumedX;
}
if (y != 0) {
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
}
......
if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
// Update the last touch co-ords, taking any scroll offset into account
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
if (ev != null) {
ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
......
}
RecyclerView處理了剩餘的滾動距離之後,計算出對剩餘滾動事件的消費情況,通過 dispatchNestedScroll 方法分發給CoordinatorLayout,CoordinatorLayout 則通過 onNestedScroll 方法分發給感興趣的 子View 的 Behavior 處理。然後根據mScrollOffset更新窗體偏移量。具體實現可以自行去查看源碼。
最後我們來分析Up事件,先來看代碼:
case MotionEvent.ACTION_UP: {
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
final float xvel = canScrollHorizontally ?
-VelocityTrackerCompat.getXVelocity(mVelocityTracker, mScrollPointerId) : 0;
final float yvel = canScrollVertically ?
-VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0;
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetTouch();
} break;
在Up事件最後一行中調用了resetTouch(),而resetTouch又調用了NestedScrollingChild的stopNestedScroll(),然後又是跟上面的流程一樣一路調用到NestedScrollingParent的onStopNestedScroll方法,然後再調用對應的Behavior的onStopNestedScroll方法,流程都類似,就不貼代碼了。
在Stop的這一流程中主要是將之前在Start流程中的設置清空,比如將mNestedScrollingParent = null(不執行這句的話嵌套滑動就執行不起來了,具體可參考NestedScrollingChild的startNestedScroll方法第一行)。
由嵌套滑動機制和AppBarLayout.Behavior共同工作完成的Toolbar上下滾動效果的原理就分析到這吧。
4.2 RecycleView一直保持在AppBarLayout下方原理:
接下來我們再通過源碼分析下RecycleView是如何一直保持在AppBarLayout下方的吧。
在文章開頭我已經簡單分析過ScrollingViewBehavior主要是依靠layoutDependsOn和onDependentViewChanged方法監聽並響應的。ScrollingViewBehavior的layoutDependsOn方法:
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
// We depend on any AppBarLayouts
return dependency instanceof AppBarLayout;
}
很明顯,ScrollingViewBehavior就是依賴於AppBarLayout的,那麼我們來看下onDependentViewChanged方法:
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
View dependency) {
offsetChildAsNeeded(parent, child, dependency);
return false;
}
......
private void offsetChildAsNeeded(CoordinatorLayout parent, View child, View dependency) {
final CoordinatorLayout.Behavior behavior =
((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
if (behavior instanceof Behavior) {
// Offset the child, pinning it to the bottom the header-dependency, maintaining
// any vertical gap and overlap
final Behavior ablBehavior = (Behavior) behavior;
ViewCompat.offsetTopAndBottom(child, (dependency.getBottom() - child.getTop())
+ ablBehavior.mOffsetDelta
+ getVerticalLayoutGap()
- getOverlapPixelsForOffset(dependency));
}
}
這兩個方法在CoordinatorLayout的onChildViewsChanged會被調用到,而每次重繪時,都會調用onChildViewsChanged,從而使其一直位於AppBarLayout的下方。
完畢