CoordinatorLayout 實現了 NestedScrollingParent 接口,是一個容器。做爲一個 「super-powered FrameLayout」,主要有如下兩個做用:java
使用 CoordinatorLayout 須要在 build.gradle 加入:android
implementation 'com.android.support:design:XXXXXX'
首先看看FloatingActionButton單獨使用時的狀況,佈局以下:app
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:onClick="onClick" android:layout_marginRight="10dp" android:layout_marginBottom="10dp"/> </RelativeLayout>
點擊這個FloatingActionButton後,將彈出一個 Snackbar:ide
public void onClick(View v) { switch (v.getId()) { case R.id.fab: Snackbar.make(findViewById(R.id.contentView), "Snackbar", Snackbar.LENGTH_SHORT).show(); break; ... } }
效果以下:佈局
此時FloatingActionButton會被 Snackbar 遮擋,此時就須要 CoordinatorLayout登場了。gradle
調整後的佈局以下:ui
<?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:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:id="@+id/anchorView" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"/> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_anchor="@id/anchorView" app:layout_anchorGravity="bottom|right" android:onClick="onClick" android:layout_marginRight="10dp" android:layout_marginBottom="10dp"/> </android.support.design.widget.CoordinatorLayout>
CoordinatorLayout 提供了兩個屬性用來設置 FloatingActionButton 的位置:this
bottom|right
表示 FloatingActionButton 位於錨點的右下角。再次運行程序,此時Snackbar 顯示和隱藏的時候,CoordinatorLayout 會動態調整 FAB 的位置,效果圖以下:spa
AppBarLayout 是一個垂直佈局的 LinearLayout,它主要是爲了實現 「Material Design」風格的標題欄的特性,好比動態聯動:.net
不使用 CoordinatorLayout,實現這個效果的方案有兩種:
關於上面兩種實現方式,能夠參考個人另一篇文章:NestedScrolling機制原理解析。而CoordinatorLayout 實現了 NestedScrollingParent 接口,因此咱們配合一個實現了 NestedScrollingChild 接口的 View 就能夠輕鬆的實現以上效果。
AppBarLayout必須做爲CoordinatorLayout的直接子View,不然它的大部分功能將不會生效,如layout_scrollFlags等。
AppBarLayout必須做爲CoordinatorLayout的直接子View,不然它的大部分功能將不會生效,包括layout_scrollFlags
簡單起見,咱們使用 AppBarLayout 包裹 TextView 來實現上面的效果:
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical" 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"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="標題" android:textSize="20sp" android:gravity="center" android:paddingTop="10dp" android:paddingBottom="10dp" android:textColor="@android:color/white" android:background="@color/colorPrimary" app:layout_scrollFlags="scroll"/> </android.support.design.widget.AppBarLayout> <android.support.v4.widget.NestedScrollView android:id="@+id/scrollView" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <TextView android:id="@+id/tv" android:layout_width="match_parent" android:layout_height="match_parent" android:textSize="30sp" android:gravity="center_horizontal"/> </android.support.v4.widget.NestedScrollView> </android.support.design.widget.CoordinatorLayout>
注意layout_behavior這個屬性:
app:layout_behavior="@string/appbar_scrolling_view_behavior"
這個值是系統提供的,表示使用android.support.design.widget.AppBarLayout.ScrollingViewBehavior
來處理 NestedScrollView 與 AppBarLayout 的關係,關於Behavior的概念這裏暫不介紹,後面會講解。
再回過頭來看例子中的 layout_scrollFlags屬性:
scroll:當上劃的時候,先總體向上滾動,直到 AppBarLayout 徹底隱藏,再開始滾動 Scrolling View;當下拉的時候,直到 Scrolling View 頂部徹底出現後,再開始滾動 AppBarLayout 到徹底顯示。
enterAlways:須要與scroll一塊兒使用(scroll|enterAlways
),具體做用與 scroll 相似, 只不過下劃的時候先顯示 AppBarLayout 到徹底,再滾動 Scrolling View。
enterAlwaysCollapsed:須要和scroll 和 enterAlways 一塊兒使用(scroll|enterAlways|enterAlwaysCollapsed
),和 enterAlways 不同的是,向下滾動時不會顯示 AppBarLayout 到徹底再滾動 Scrolling View,而是先滾動 AppBarLayout 到最小高度,再滾動 Scrolling View,最後再滾動 AppBarLayout 到徹底顯示。
注意:須要定義 View 的最小高度(minHeight)纔有效果:
android:minHeight="10dp" app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"
exitUntilCollapsed:須要和scroll 一塊兒使用,與enterAlwaysCollapsed不一樣的是,exitUntilCollapsed定義了 AppBarLayout 消失的規則。當上劃時,AppBarLayout 向上滾動退出直至最小高度(minHeight),而後 Scrolling View 開始滾動,也就是說AppBarLayout 不會徹底退出屏幕。當向下滾動的時候,直到 Scrolling View 頂部徹底出現後,纔會開始滾動 AppBarLayout 到徹底顯示。
CollapsingToolbarLayout繼承自FrameLayout, 被設計做爲AppBarLayout的子View,並做爲Toolbar的包裝器。主要實現如下功能
- Collapsing title(能夠摺疊 的 標題 )
- Content scrim(內容裝飾),當咱們滑動的位置 到達必定閥值的時候,內容 裝飾將會被顯示或者隱藏
- Status bar scrim(狀態欄布)
- Parallax scrolling children,滑動的時候孩子呈現視覺特差效果
- Pinned position children,固定位置的 孩子,它是用來實現 Toolbar 的摺疊效果,通常它的直接子 View 是 Toolbar,固然也能夠是其它類型的 View。
下面咱們一塊兒來看一下幾個常量
常量 | 解釋說明 |
---|---|
int COLLAPSE_MODE_OFF | The view will act as normal with no collapsing behavior.(這個 View將會 呈現正常的結果,不會表現出摺疊效果) |
int COLLAPSE_MODE_PARALLAX | The view will scroll in a parallax fashion. See setParallaxMultiplier(float) to change the multiplier used.(在滑動的時候這個View 會呈現 出 視覺特差效果 ) |
int COLLAPSE_MODE_PIN | The view will pin in place until it reaches the bottom of the CollapsingToolbarLayout.(當這個View到達 CollapsingToolbarLayout的底部的時候,這個View 將會被放置,即代替整個CollapsingToolbarLayout) |
咱們有兩種方法能夠設置這個常量,
方法一:在代碼中直接設置
setCollapseMode(int collapseMode)
方法 二:在佈局文件中使用自定義屬性
app:layout_collapseMode="pin"
若是你不使用 Toolbar,有些效果無法直接實現,好比下圖的「My files」文字在摺疊和展開的時候,有一個過渡效果:
也就意味着 CollapsingToolbarLayout 設置 title 的相關方法無效,好比:setTitle、setCollapsedTitleTextColor、setExpandedTitleGravity
等,更多方法能夠自行查閱 API 。
另外,layout_scrollFlags 中的 exitUntilCollapsed 屬性也會失效,即便你設置了 minHeight,因此官方也說明了CollapsingToolbarLayout 是爲了配合 Toolbar 而設計:
2.2.1 CollapsingToolbarLayout的幾種效果:
<?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:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="150dp"> <android.support.design.widget.CollapsingToolbarLayout android:id="@+id/collapsingToolbarLayout" android:layout_width="match_parent" android:layout_height="match_parent" app:contentScrim="@color/colorPrimary" app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="50dp" app:layout_collapseMode="pin" android:minHeight="10dp" android:background="@color/colorPrimary"/> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout> <android.support.v4.widget.NestedScrollView android:id="@+id/scrollView" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <TextView android:id="@+id/tv" android:layout_width="match_parent" android:layout_height="match_parent" android:textSize="30sp" android:gravity="center_horizontal"/> </android.support.v4.widget.NestedScrollView> </android.support.design.widget.CoordinatorLayout>
修改下 CollapsingToolbarLayout 的 layout_scrollFlags:
app:layout_scrollFlags="scroll|exitUntilCollapsed"
2.2.2 layout_collapseMode屬性
上述例子中設置了屬性app:layout_collapseMode="pin"
確保 CollapsingToolbarLayout 摺疊完成以前,Toolbar 一直固定在頂部不動。除了pin 以外還可使用 parallax,視差的意思就是:移動過程當中兩個 View 的位置產生了必定的視覺差別。
<?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:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="150dp"> <android.support.design.widget.CollapsingToolbarLayout android:id="@+id/collapsingToolbarLayout" android:layout_width="match_parent" android:layout_height="match_parent" app:contentScrim="@color/colorPrimary" app:layout_scrollFlags="scroll|exitUntilCollapsed"> <ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/bg" android:scaleType="centerCrop" app:layout_collapseParallaxMultiplier="0.9" app:layout_collapseMode="parallax"/> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="50dp" app:layout_collapseMode="pin"/> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout> <android.support.v4.widget.NestedScrollView android:id="@+id/scrollView" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <TextView android:id="@+id/tv" android:layout_width="match_parent" android:layout_height="match_parent" android:textSize="30sp" android:gravity="center_horizontal"/> </android.support.v4.widget.NestedScrollView> </android.support.design.widget.CoordinatorLayout>
注意這個屬性:
app:layout_collapseParallaxMultiplier="0.9"
使用的視差因子值爲0.9,那麼試着設置視察因子爲0.1:
app:layout_collapseParallaxMultiplier="0.9"
效果圖:
能夠看到0.9和0.1背景圖片移動的距離是不同的。
在 NestedScrolling機制原理解析 一文中已經介紹過:Scrolling child是使用 NestedScrollingChildHelper 做爲代理來完成和scrolling parent的交互的,具體原理本文再也不一一闡述。
咱們已知的NestedScrollingParent接口包含如下方法:
在 Behavior 接口中一樣裏面也包含這些方法,與 NestedScrollingParent 方法幾乎一一對應。在 CoordinatorLayout 裏面。NestedScrollingParent 接口的方法的具體實現邏輯都會交給 Behavior 對應的方法去處理,咱們能夠從CoordinatorLayout源碼中找到答案:
遍歷全部的孩子 ,若是可見性是 GONE,跳過。若是可見性不是 GONE,經過 layoutParams 拿到 Behavior,判斷 behavior 是否爲空,不爲空,調用 behavior 的對應方法 onStartNestedScroll 和 acceptNestedScroll 方法。
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; }
首先調用 mNestedScrollingParentHelper 的相關方法,然後遍歷孩子,經過 layoutParams 判斷是否要處理滑動事件,處理的 話,回調 Behavior 的相關方法,不處理的話,跳過當前 View。
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); mNestedScrollingDirectChild = child; mNestedScrollingTarget = target; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View view = getChildAt(i); final LayoutParams lp = (LayoutParams) view.getLayoutParams(); if (!lp.isNestedScrollAccepted()) { continue; } final Behavior viewBehavior = lp.getBehavior(); if (viewBehavior != null) { viewBehavior.onNestedScrollAccepted(this, view, child, target, nestedScrollAxes); } } }
咱們知道 onNestedPreScroll 是在 Scrolling child 滑動以前回調的,提供機會給 Scrooling Parent 先於 child 進行滑動的。
在 CoordinatorLayout 裏面,它的處理流程是這樣的。 遍歷全部的孩子,判斷可見性是否爲 GONE,若是是 ,跳過當前 子 View,經過 LayoutParams 判斷是否處理滑動事件,不處理滑動 事件,跳過,拿到 Behavior,判斷 Behavior 是否爲空,不過空,回調 Behavior 的 onNestedPreScroll 方法。
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { int xConsumed = 0; int yConsumed = 0; boolean accepted = false; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View view = getChildAt(i); final LayoutParams lp = (LayoutParams) view.getLayoutParams(); if (!lp.isNestedScrollAccepted()) { continue; } final Behavior viewBehavior = lp.getBehavior(); if (viewBehavior != null) { mTempIntPair[0] = mTempIntPair[1] = 0; viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair); xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0]) : Math.min(xConsumed, mTempIntPair[0]); yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1]) : Math.min(yConsumed, mTempIntPair[1]); accepted = true; } } consumed[0] = xConsumed; consumed[1] = yConsumed; if (accepted) { dispatchOnDependentViewChanged(true); } }
在 Scrolling Child 滑動以後,提供機會給 Scrolling Parent 滑動,事件的處理邏輯參見上文。
@Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { final int childCount = getChildCount(); boolean accepted = false; for (int i = 0; i < childCount; i++) { final View view = getChildAt(i); if (view.getVisibility() == GONE) { // If the child is GONE, skip... continue; } final LayoutParams lp = (LayoutParams) view.getLayoutParams(); // 若是以前沒有處理滑動事件,直接返回,不調用 onStopNestedScroll 方法 if (!lp.isNestedScrollAccepted()) { continue; } final Behavior viewBehavior = lp.getBehavior(); if (viewBehavior != null) { // 調用 behavior 的相應方法 viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); accepted = true; } } if (accepted) { onChildViewsChanged(EVENT_NESTED_SCROLL); } }
public boolean onNestedPreFling(View target, float velocityX, float velocityY) { 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(); if (!lp.isNestedScrollAccepted()) { continue; } final Behavior viewBehavior = lp.getBehavior(); if (viewBehavior != null) { handled |= viewBehavior.onNestedPreFling(this, view, target, velocityX, velocityY); } } return handled; }
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { 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(); if (!lp.isNestedScrollAccepted()) { continue; } final Behavior viewBehavior = lp.getBehavior(); if (viewBehavior != null) { handled |= viewBehavior.onNestedFling(this, view, target, velocityX, velocityY, consumed); } } if (handled) { dispatchOnDependentViewChanged(true); } return handled; }
public void onStopNestedScroll(View target) { mNestedScrollingParentHelper.onStopNestedScroll(target); final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View view = getChildAt(i); final LayoutParams lp = (LayoutParams) view.getLayoutParams(); if (!lp.isNestedScrollAccepted()) { continue; } final Behavior viewBehavior = lp.getBehavior(); if (viewBehavior != null) { viewBehavior.onStopNestedScroll(this, view, target); } lp.resetNestedScroll(); lp.resetChangedAfterNestedScroll(); } mNestedScrollingDirectChild = null; mNestedScrollingTarget = null; }
Behavior 相比 NestedScrollingParent 獨有的方法
默認返回false, 若是返回 true,則當 dependency 改變的 時候,將會回調 onDependentViewChanged 方法。好比,當使用依賴於 AppBarLayout 的 ScrollingViewBehavior 時,它會重寫方法:
@Override public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) { // We depend on any AppBarLayouts return dependency instanceof AppBarLayout; }
默認返回false, 與 layoutDependsOn 息息相關,當 layoutDependsOn 返回true的時候,纔會回調這個方法。
那麼 onDependentViewChanged 是如何監聽獲得 View 變化和移除的?實際上是在 CoordinatorLayout 的 onAttachedToWindow 方法裏面,他會爲 ViewTreeObserver 視圖樹添加 OnPreDrawListener 監聽。
@Override public void onAttachedToWindow() { super.onAttachedToWindow(); resetTouchBehaviors(); if (mNeedsPreDrawListener) { if (mOnPreDrawListener == null) { mOnPreDrawListener = new OnPreDrawListener(); } final ViewTreeObserver vto = getViewTreeObserver(); vto.addOnPreDrawListener(mOnPreDrawListener); } mIsAttachedToWindow = true; }
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener { @Override public boolean onPreDraw() { dispatchOnDependentViewChanged(false); return true; } }
在 OnPreDrawListener 監聽裏面會調用 dispatchOnDependentViewChanged 方法,在該方法裏面會根據 View的狀態回調 onDependentViewChanged 方法:
void dispatchOnDependentViewChanged(final boolean fromNestedScroll) { final int layoutDirection = ViewCompat.getLayoutDirection(this); final int childCount = mDependencySortedChildren.size(); for (int i = 0; i < childCount; i++) { final View child = mDependencySortedChildren.get(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); // Check child views before for anchor for (int j = 0; j < i; j++) { final View checkChild = mDependencySortedChildren.get(j); if (lp.mAnchorDirectChild == checkChild) { offsetChildToAnchor(child, layoutDirection); } } // Did it change? if not continue final Rect oldRect = mTempRect1; final Rect newRect = mTempRect2; getLastChildRect(child, oldRect); getChildRect(child, true, newRect); if (oldRect.equals(newRect)) { continue; } recordLastChildRect(child, newRect); // Update any behavior-dependent views for the change for (int j = i + 1; j < childCount; j++) { final View checkChild = mDependencySortedChildren.get(j); final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams(); final Behavior b = checkLp.getBehavior(); if (b != null && b.layoutDependsOn(this, checkChild, child)) { if (!fromNestedScroll && checkLp.getChangedAfterNestedScroll()) { // If this is not from a nested scroll and we have already been changed // from a nested scroll, skip the dispatch and reset the flag checkLp.resetChangedAfterNestedScroll(); continue; } final boolean handled = b.onDependentViewChanged(this, checkChild, child); if (fromNestedScroll) { // If this is from a nested scroll, set the flag so that we may skip // any resulting onPreDraw dispatch (if needed) checkLp.setChangedAfterNestedScroll(handled); } } } } }
咱們知道當 View 被銷燬的時候,會回調 onDetachedFromWindow 這個方法,所以相對在這個方法裏面移除 View 視圖樹的 PreDrawListener 監聽:
@Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); resetTouchBehaviors(); if (mNeedsPreDrawListener && mOnPreDrawListener != null) { final ViewTreeObserver vto = getViewTreeObserver(); vto.removeOnPreDrawListener(mOnPreDrawListener); } if (mNestedScrollingTarget != null) { onStopNestedScroll(mNestedScrollingTarget); } mIsAttachedToWindow = false; }
回過頭來看看 CoordinatorLayout 的 onMeasure 和 onLayout 過程
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //處理 child 的一些 相關屬性 ,好比 Behavior等 prepareChildren(); //若是有依賴的話,添加 OnPreDrawListener 監聽,沒有的話,移除 OnPreDrawListener 監聽 ensurePreDrawListener(); //省略了處理padding值的部分 ... final int childCount = mDependencySortedChildren.size(); for (int i = 0; i < childCount; i++) { final View child = mDependencySortedChildren.get(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); int keylineWidthUsed = 0; if (lp.keyline >= 0 && widthMode != MeasureSpec.UNSPECIFIED) { final int keylinePos = getKeyline(lp.keyline); final int keylineGravity = GravityCompat.getAbsoluteGravity( resolveKeylineGravity(lp.gravity), layoutDirection) & Gravity.HORIZONTAL_GRAVITY_MASK; if ((keylineGravity == Gravity.LEFT && !isRtl) || (keylineGravity == Gravity.RIGHT && isRtl)) { keylineWidthUsed = Math.max(0, widthSize - paddingRight - keylinePos); } else if ((keylineGravity == Gravity.RIGHT && !isRtl) || (keylineGravity == Gravity.LEFT && isRtl)) { keylineWidthUsed = Math.max(0, keylinePos - paddingLeft); } } final Behavior b = lp.getBehavior(); // 回調 Behavior 的 onMeasureChild 方法 if (b == null || !b.onMeasureChild(this, child, widthMeasureSpec, keylineWidthUsed, heightMeasureSpec, 0)) { onMeasureChild(child, widthMeasureSpec, keylineWidthUsed, heightMeasureSpec, 0); } widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); heightUsed = Math.max(heightUsed, heightPadding + child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); childState = ViewCompat.combineMeasuredStates(childState, ViewCompat.getMeasuredState(child)); } final int width = ViewCompat.resolveSizeAndState(widthUsed, widthMeasureSpec, childState & ViewCompat.MEASURED_STATE_MASK); final int height = ViewCompat.resolveSizeAndState(heightUsed, heightMeasureSpec, childState << ViewCompat.MEASURED_HEIGHT_STATE_SHIFT); setMeasuredDimension(width, height); }
進入 prepareChildren 方法裏面,能夠發現它對 CoordinatorLayout 裏面的子 View 進行了排序,排序的結果是 最後被依賴的 View 會排在最前面。舉個例子 A 依賴於 B,那麼 B會排在前面,A 會排在 B 的 後面。這樣的排序結果是合理的,由於 A 既然依賴於 B,那麼 B 確定要有限 measure。
private void prepareChildren() { final int childCount = getChildCount(); boolean resortRequired = mDependencySortedChildren.size() != childCount; for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final LayoutParams lp = getResolvedLayoutParams(child); if (!resortRequired && lp.isDirty(this, child)) { resortRequired = true; } lp.findAnchorView(this, child); } if (resortRequired) { mDependencySortedChildren.clear(); for (int i = 0; i < childCount; i++) { mDependencySortedChildren.add(getChildAt(i)); } Collections.sort(mDependencySortedChildren, mLayoutDependencyComparator); } }
接下來 咱們進入 ensurePreDrawListener 方法裏面,看看裏面到底作了什麼
/** * Add or remove the pre-draw listener as necessary. */ void ensurePreDrawListener() { boolean hasDependencies = false; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (hasDependencies(child)) { hasDependencies = true; break; } } if (hasDependencies != mNeedsPreDrawListener) { if (hasDependencies) { addPreDrawListener(); } else { removePreDrawListener(); } } }
其實就是遍歷全部子view,判斷是否有存在依賴的一個,若是有,添加onPreDrawListener,若是沒有,移除onPreDrawListener。
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int layoutDirection = ViewCompat.getLayoutDirection(this); final int childCount = mDependencySortedChildren.size(); for (int i = 0; i < childCount; i++) { final View child = mDependencySortedChildren.get(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final Behavior behavior = lp.getBehavior(); if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) { onLayoutChild(child, layoutDirection); } } }
layout 過程相對比較簡單,遍歷全部孩子,若是可見性爲 GONE ,跳過該孩子的 Layout。接着經過 LayoutParams 拿到 Behavior,若是 Behavior 爲空或者 Behavior 沒有處理本身的 layout 過程,調用 onLayoutChild 方法 去處理子View的位置擺放;若是 Behavior 有處理本身的 layout 過程,交給 Behavior 去處理 。
再看看CoordinatorLayout觸摸事件傳遞,CoordinatorLayout 並不會直接處理事件,而是會盡量地交給子 View 的Behavior 進行處理。onInterceptTouchEvent 和 onToucheEvent 這兩個方法都會調用 performIntercept 來處理事件。
/** * Populate a list with the current child views, sorted such that the topmost views * in z-order are at the front of the list. Useful for hit testing and event dispatch. */ private void getTopSortedChildren(List<View> out) { out.clear(); final boolean useCustomOrder = isChildrenDrawingOrderEnabled(); final int childCount = getChildCount(); for (int i = childCount - 1; i >= 0; i--) { final int childIndex = useCustomOrder ? getChildDrawingOrder(childCount, i) : i; final View child = getChildAt(childIndex); out.add(child); } if (TOP_SORTED_CHILDREN_COMPARATOR != null) { Collections.sort(out, TOP_SORTED_CHILDREN_COMPARATOR); } } private boolean performIntercept(MotionEvent ev) { boolean intercepted = false; boolean newBlock = false; MotionEvent cancelEvent = null; final int action = ev.getActionMasked(); 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 || newBlock) && action != MotionEvent.ACTION_DOWN) { // Cancel all behaviors beneath the one that intercepted. // If the event is "down" then we don't have anything to cancel yet. if (b != null) { if (cancelEvent != null) { final long now = SystemClock.uptimeMillis(); cancelEvent = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); } b.onInterceptTouchEvent(this, child, cancelEvent); } continue; } if (!intercepted && b != null && (intercepted = b.onInterceptTouchEvent(this, child, ev))) { mBehaviorTouchView = child; } // Don't keep going if we're not allowing interaction below this. // Setting newBlock will make sure we cancel the rest of the behaviors. final boolean wasBlocking = lp.didBlockInteraction(); final boolean isBlocking = lp.isBlockingInteractionBelow(this, child); newBlock = isBlocking && !wasBlocking; if (isBlocking && !newBlock) { // Stop here since we don't have anything more to cancel - we already did // when the behavior first started blocking things below this point. break; } } topmostChildList.clear(); return intercepted; }
能夠看到首先經過 getTopSortedChildren 方法將child view按照 Z軸上往下排序(在5.0以上,按照z屬性來排序,如下,則是按照添加順序或者自定義的繪製順序來排列);
遍歷排序好的全部 Child,若是以前有Child 的 Behavior 對事件進行了攔截消費,就經過 onInterceptTouchEvent 發送Cancel事件給後續的全部Behavior; 若是以前沒有 Child 消費過且當前 Child進行了消費,則記錄下該 child。