CoordinatorLayout字面意思是「協調器佈局」,它是Design Support Library中提供的一個超級幀佈局,幫助咱們實現Material Design中各類複雜的動做響應和子控件間交互。我認爲它是這個庫中最難的一個新視圖,也是可定製化潛力最大的,應該能夠用較統一的方式實現不少舊的開源控件的效果。html
索引react
Android Developers Blog: Android Design Support Library: 全面介紹了新庫中的各個控件的使用,其中包含CoordinatorLayout的基本概念,它如何同FAB或者app bar結合起來使用。這是最基本的一篇文檔。github
CoordinatorLayout API文檔: 在Class Overview中對此佈局涉及到的重要概念(例如Behaviors)有簡單的說明。chrome
Handling Scrolls with CoordinatorLayout: 這篇博客中進一步介紹了該佈局的使用,另外還介紹了google員工寫的design library demo,最後還總結了GitHub上其它實現視差滾動的開源庫,提供了很好的替代方案。數組
GitHub: devunwired/coordinated-effort: 這個項目展現瞭如何自定義behavior,實現了一個有趣的SlidingCardLayout。app
The Design library introduces CoordinatorLayout, a layout which provides an additional level of control over touch events between child views, something which many of the components in the Design library take advantage of.ide
正如它的名字表示的那樣,CoordinatorLayout是一個協調器,讓它上面的子視圖在處理觸摸事件時更靈活。佈局
文中還介紹了兩處CoordinatorLayout的基本應用,一是讓FloatingActionButton隨着Snackbar出現和消失的動畫自動移動;二是與AppBarLayout配合起來使用,讓app bar能隨頁面主要內容一塊兒滑動,並實現豐富的收縮和視差效果。
One thing that is important to note is that CoordinatorLayout doesn’t have any innate understanding of a FloatingActionButton or AppBarLayout work - it just provides an additional API in the form of a Coordinator.Behavior, which allows child views to better control touch events and gestures as well as declare dependencies between each other and receive callbacks via onDependentViewChanged().
CoordinatorLayout能夠用來自定義視圖,它徹底獨立於FloatingActionButton或者AppBarLayout以外。它提供了一個額外的API(Coordinator.Behavior),讓子視圖更好的控制觸摸事件和手勢,聲明彼此之間的依賴關係,並經過onDependentViewChanged()方法接收回調。
Views can declare a default Behavior by using the CoordinatorLayout.DefaultBehavior(YourView.Behavior.class) annotation,or set it in your layout files by with the app:layout_behavior="com.example.app.YourView$Behavior" attribute.
經過註解或者xml屬性能夠聲明自定義控件的默認Behavior,例如:
// 經過註解聲明默認Behavior @CoordinatorLayout.DefaultBehavior(SlidingCardBehavior.class) public class SlidingCardLayout extends FrameLayout { ...... }
也能夠在xml中指明默認Behavior的類名:
<android.support.v7.widget.RecyclerView android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" />
CoordinatorLayout is a super-powered FrameLayout. CoordinatorLayout is intended for two primary use cases:
- As a top-level application decor or chrome layout
- As a container for a specific interaction with one or more child views
CoordinatorLayout是一個強化的FrameLayout,注意它仍是直接繼承於ViewGroup,而不是FrameLayout。這能夠理解爲它的子視圖也是以棧的形式依次堆疊在一塊兒,同FrameLayout的佈局方式同樣。
Behaviors may be used to implement a variety of interactions and additional layout modifications ranging from sliding drawers and panels to swipe-dismissable elements and buttons that stick to other elements as they move and animate.
這裏提出了幾個CoordinatorLayout的典型應用場景:側滑菜單,swipe-dismissable elements,隨其它元素移動的按鈕。
Children of a CoordinatorLayout may have an anchor. This view id must correspond to an arbitrary descendant of the CoordinatorLayout, but it may not be the anchored child itself or a descendant of the anchored child. This can be used to place floating views relative to other arbitrary content panes.
CoordinatorLayout的子視圖能夠錨定在其它子視圖上。這是用來將浮動的視圖關聯到其它任意內容上。例如,用Android Studio快速建立Scrolling Activity時,FAB是錨定在app bar上的:
<android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/fab_margin" app:layout_anchor="@id/app_bar" app:layout_anchorGravity="bottom|end" android:src="@android:drawable/ic_dialog_email"/>
下面是Behavior類的實現類:
Known Direct Subclasses
AppBarLayout.Behavior, AppBarLayout.ScrollingViewBehavior, FloatingActionButton.Behavior, SwipeDismissBehavior
其中值得關注的是SwipeDismissBehavior,Snackbar的側滑刪除功能就是繼承它實現的,咱們應該能夠仿照其代碼實現本身的側滑刪除功能。
Snackbar中定義的Behavior:
final class Behavior extends SwipeDismissBehavior<SnackbarLayout> { @Override public boolean canSwipeDismissView(View child) { return child instanceof SnackbarLayout; } @Override public boolean onInterceptTouchEvent(CoordinatorLayout parent, SnackbarLayout child, MotionEvent event) { // We want to make sure that we disable any Snackbar timeouts if the user is // currently touching the Snackbar. We restore the timeout when complete if (parent.isPointInChildBounds(child, (int) event.getX(), (int) event.getY())) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: SnackbarManager.getInstance().cancelTimeout(mManagerCallback); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: SnackbarManager.getInstance().restoreTimeout(mManagerCallback); break; } } return super.onInterceptTouchEvent(parent, child, event); } }
在Snackbar.showView()方法中設置Behavior,注意其中對CoordinatorLayout.LayoutParams的使用:
final ViewGroup.LayoutParams lp = mView.getLayoutParams(); if (lp instanceof CoordinatorLayout.LayoutParams) { // If our LayoutParams are from a CoordinatorLayout, we'll setup our Behavior final Behavior behavior = new Behavior(); behavior.setStartAlphaSwipeDistance(0.1f); behavior.setEndAlphaSwipeDistance(0.6f); behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END); behavior.setListener(new SwipeDismissBehavior.OnDismissListener() { @Override public void onDismiss(View view) { dispatchDismiss(Callback.DISMISS_EVENT_SWIPE); } @Override public void onDragStateChanged(int state) { switch (state) { case SwipeDismissBehavior.STATE_DRAGGING: case SwipeDismissBehavior.STATE_SETTLING: // If the view is being dragged or settling, cancel the timeout SnackbarManager.getInstance().cancelTimeout(mManagerCallback); break; case SwipeDismissBehavior.STATE_IDLE: // If the view has been released and is idle, restore the timeout SnackbarManager.getInstance().restoreTimeout(mManagerCallback); break; } } }); ((CoordinatorLayout.LayoutParams) lp).setBehavior(behavior); }
When a CoordinatorLayout notices this attribute declared in the RecyclerView, it will search across the other views contained within it for any related views associated by the behavior. In this particular case, the AppBarLayout.ScrollingViewBehavior describes a dependency between the RecyclerView and AppBarLayout. Any scroll events to the RecyclerView should trigger changes to the AppBarLayout layout or any views contained within it.
這一段解釋了RecyclerView和AppBarLayout之間協調工做的原理。RecyclerView聲明其默認Behavior爲AppBarLayout.ScrollingViewBehavior,當CoordinatorLayout注意到RecyclerView中聲明的這一屬性,它會到本身其它的子視圖中尋找這個Behavior相關聯的View,依據Behavior中的這一方法:
public static class ScrollingViewBehavior extends ViewOffsetBehavior<View> { ...... public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) { return dependency instanceof AppBarLayout; } ...... }
這樣,CoordinatorLayout會找到AppBarLayout,而後,RecyclerView上的任何滾動事件都會觸發AppBarLayout以及其子視圖的變化。
To define your own a CoordinatorLayout Behavior, the layoutDependsOn() and onDependentViewChanged() should be implemented. For instance, AppBarLayout.Behavior has these two key methods defined. This behavior is used to trigger a change on the AppBarLayout when a scroll event happens.
這一段話裏有一點小錯誤,AppBarLayout.Behavior是AppBarLayout本身的默認Behavior,它並無實現layoutDependsOn()和onDependentViewChanged()方法。實現這兩個方法的是AppBarLayout.ScrollingViewBehavior,這個纔是給別的視圖使用,來觸發AppBarLayout改變的。
在另外一篇博文Floating Action Buttons中舉了一個例子,繼承FloatingActionButton.Behavior來自定義一個Behavior,在原生FAB的效果上添加咱們想要的效果,文中具體實現的是讓FAB隨頁面內容的滾動自動消失和出現。這是學習自定義Behavior的一個很好的例子,效果以下:
代碼很簡單,易於理解:
public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior { // 提供帶參構造方法是爲了能在xml中使用。 public ScrollAwareFABBehavior(Context context, AttributeSet attrs) { super(); } // 指明咱們但願處理垂直方向的滾動事件。 // 滾動事件一樣是由本類處理,見下面的onNestedScroll()方法。 @Override public boolean onStartNestedScroll(final CoordinatorLayout coordinatorLayout, final FloatingActionButton child, final View directTargetChild, final View target, final int nestedScrollAxes) { // Ensure we react to vertical scrolling return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL || super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes); } // 檢查Y軸的距離,決定是顯示仍是隱藏FAB。 @Override public void onNestedScroll(final CoordinatorLayout coordinatorLayout, final FloatingActionButton child, final View target, final int dxConsumed, final int dyConsumed, final int dxUnconsumed, final int dyUnconsumed) { super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) { // User scrolled down and the FAB is currently visible -> hide the FAB child.hide(); } else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) { // User scrolled up and the FAB is currently not visible -> show the FAB child.show(); } } }
這裏面不太好理解的一點是,自定義方法裏沒有實現layoutDependsOn()方法,父類FloatingActionButton.Behavior中該方法以下:
public boolean layoutDependsOn(CoordinatorLayout parent, FloatingActionButton child, View dependency) { // SNACKBAR_BEHAVIOR_ENABLED這個參數只是爲了適配,能夠不考慮。 return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof SnackbarLayout; }
要進一步深刻理解,須要瞭解Snackbar的工做原理,暫時無論。
我想嘗試用CoordinatorLayout控件模仿Android日曆的收縮效果,以下:
初步實現了邏輯相同的滾動效果,以下:
用的佈局很簡單:
<LinearLayout> <com.ycj.learningdemo.a0_support_design_library.WeekLabelView/> <android.support.design.widget.CoordinatorLayout> <com.ycj.learningdemo.a0_support_design_library.MonthPager/> <android.support.v7.widget.RecyclerView/> </android.support.design.widget.CoordinatorLayout> </LinearLayout>
其中WeekLabelView是水平放置了七個TextView的線性佈局,用來模仿週一到週日的頂欄。
MonthPager繼承於ViewPager,用來模仿日曆中的主體內容,其中每個page是一個6 x 7的GridView。下方的RecyclerView用來模仿額外信息和事件列表。MonthPager和RecyclerView包含在一個CoordinatorLayout中。
要實現上圖中的嵌套滾動效果,關鍵就是MonthPager和RecyclerView的默認Behavior,MonthPager的默認Behavior以下:
public static class Behavior extends CoordinatorLayout.Behavior<MonthPager> { private int mTop; @Override public boolean layoutDependsOn(CoordinatorLayout parent, MonthPager child, View dependency) { // MonthView 依賴RecyclerView而移動 return dependency instanceof RecyclerView; } @Override public boolean onLayoutChild(CoordinatorLayout parent, MonthPager child, int layoutDirection) { // 因爲點擊GridView的item會觸發視圖從新佈局,須要重設偏移量。 parent.onLayoutChild(child, layoutDirection); child.offsetTopAndBottom(mTop); return true; } private int dependentViewTop = -1; @Override public boolean onDependentViewChanged(CoordinatorLayout parent, MonthPager child, View dependency) { if (dependentViewTop != -1) { // MonthPager 向上移動的區間比RecyclerView小。 // 只要在區間範圍內,MonthPager就跟隨RecyclerView一塊兒移動。 int dy = dependency.getTop() - dependentViewTop; int top = child.getTop(); if (dy > -top) dy = -top; if (dy < -top - child.getTopMovableDistance()) dy = -top - child.getTopMovableDistance(); child.offsetTopAndBottom(dy); } dependentViewTop = dependency.getTop(); mTop = child.getTop(); return true; } }
RecyclerView的默認Behavior以下:
public class EventListBehavior extends CoordinatorLayout.Behavior<RecyclerView> { private int mInitialOffset = -1; private int mTop; public EventListBehavior(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onLayoutChild(CoordinatorLayout parent, RecyclerView child, int layoutDirection) { parent.onLayoutChild(child, layoutDirection); MonthPager monthPager = getMonthPager(parent); if (monthPager.getBottom() > 0 && mInitialOffset == -1) { // 初始狀況下讓RecyclerView在MonthPager正下方。 mInitialOffset = monthPager.getBottom(); child.offsetTopAndBottom(mInitialOffset); mTop = mInitialOffset; } else if (mInitialOffset != -1) { // 不然恢復上次的偏移量。 child.offsetTopAndBottom(mTop); } return true; } @Override public boolean onMeasureChild(CoordinatorLayout parent, RecyclerView child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { // 爲了保證RecyclerView中的全部項均可見,要重設高度。 MonthPager monthPager = getMonthPager(parent); int measuredHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec) - heightUsed - monthPager.getHeight() / 6; int childMeasureSpec = View.MeasureSpec. makeMeasureSpec(measuredHeight, View.MeasureSpec.EXACTLY); child.measure(parentWidthMeasureSpec, childMeasureSpec); return true; } @Override public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, RecyclerView child, View directTargetChild, View target, int nestedScrollAxes) { boolean isVertical = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; return isVertical && child == directTargetChild; } @Override public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, RecyclerView child, View target, int dx, int dy, int[] consumed) { super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed); int minOffset = getMinOffset(coordinatorLayout); // 若是RecyclerView不在最頂端,移動RecyclerView自身的位置。 if (child.getTop() <= mInitialOffset && child.getTop() >= minOffset) { // 將值存在數組中,告訴parent這些滾動距離被消費掉了。 consumed[1] = MoveUtil.move(child, dy, minOffset, mInitialOffset); mTop = child.getTop(); } } private MonthPager getMonthPager(CoordinatorLayout coordinatorLayout) { return (MonthPager) coordinatorLayout.getChildAt(0); } // RecyclerView能夠移動的最上端位置。 private int getMinOffset(CoordinatorLayout coordinatorLayout){ MonthPager monthPager = getMonthPager(coordinatorLayout); return mInitialOffset - getMonthPager(coordinatorLayout).getWholeMovableDistance(); } }
上面只實現了最基本的嵌套滾動,但要模仿Android日曆的收縮效果,不能讓日曆控件停留在中間狀態,必需要麼顯示選中行,要麼顯示全部行。Behavior中有一個方法onStopNestedScroll(),當嵌套滾動中止時,須要銜接上一個位移動畫,讓視圖自動移動到想要的位置:
public void onStopNestedScroll(final CoordinatorLayout parent, final RecyclerView child, View target) { MonthPager monthPager = getMonthPager(parent); if (isGoingUp) { if (mInitialOffset - mTop > 60){ scrollToYCoordinate(parent, child, mMinOffset, 200); } else { scrollToYCoordinate(parent, child, mInitialOffset, 80); } } else { if (mTop - mMinOffset > 60){ scrollToYCoordinate(parent, child, mInitialOffset, 200); } else { scrollToYCoordinate(parent, child, mMinOffset, 80); } } }
在scrollToYCoordinate()方法中,用Scroller來移動RecyclerView的位置:
private void scrollToYCoordinate(final CoordinatorLayout parent, final RecyclerView child, final int y, int duration){ final Scroller scroller = new Scroller(parent.getContext()); scroller.startScroll(0, mTop, 0, y - mTop, duration); ViewCompat.postOnAnimation(child, new Runnable() { @Override public void run() { if (scroller.computeScrollOffset()) { int delta = scroller.getCurrY() - child.getTop(); child.offsetTopAndBottom(delta); saveTop(child.getTop()); parent.dispatchDependentViewsChanged(child); // Post ourselves so that we run on the next animation ViewCompat.postOnAnimation(child, this); } } }); }
注意當RecyclerView調用offsetTopAndBottom()方法時,並不會觸發從新layout過程,依賴RecyclerView的MonthPager也就不會接收到onDependentViewChanged()回調,因此須要主動調用CoordinatorLayout.dispatchDependentViewsChanged()方法。這也是沒有直接用TranslateAnimation的緣由。
最終效果以下:
CoordinatorLayout經過CoordinatorLayout.LayoutParams類中的dependsOn()判斷子視圖間的依賴關係:
boolean dependsOn(CoordinatorLayout parent, View child, View dependency) { return dependency == mAnchorDirectChild || (mBehavior != null && mBehavior.layoutDependsOn(parent, child, dependency)); }
兩種狀況下子視圖A依賴子視圖B:
B是A的mAnchorDirectChild,這裏的mAnchorDirectChild並不必定是xml中app:layout_anchor這個id對應的視圖,而是這個id對應的視圖所在的、CoorinatorLayout的直接子視圖。
A的Behavior中,layoutDependsOn()方法對B的判斷返回true。
根據視圖間的依賴關係,CoordinatorLayout構建了一個比較器:
final Comparator<View> mLayoutDependencyComparator = new Comparator<View>() { @Override public int compare(View lhs, View rhs) { if (lhs == rhs) { return 0; } else if (((LayoutParams) lhs.getLayoutParams()).dependsOn( CoordinatorLayout.this, lhs, rhs)) { return 1; } else if (((LayoutParams) rhs.getLayoutParams()).dependsOn( CoordinatorLayout.this, rhs, lhs)) { return -1; } else { return 0; } } };
簡單來講就是依賴別人的大,被依賴的小。
而後,CoordinatorLayout使用此比較器將它的子視圖作從小到大排序,用的是簡單的選擇排序,將被別人依賴的放在前面,依賴別人的放在後面:
private static void selectionSort(final List<View> list, final Comparator<View> comparator) { if (list == null || list.size() < 2) { return; } final View[] array = new View[list.size()]; list.toArray(array); final int count = array.length; for (int i = 0; i < count; i++) { int min = i; for (int j = i + 1; j < count; j++) { if (comparator.compare(array[j], array[min]) < 0) { min = j; } } if (i != min) { // We have a different min so swap the items final View minItem = array[min]; array[min] = array[i]; array[i] = minItem; } } // Finally add the array back into the collection list.clear(); for (int i = 0; i < count; i++) { list.add(array[i]); } }
若是兩個視圖互相依賴會怎樣?根據上面的代碼,在作選擇排序時,兩個視圖位置會交換兩次,其次序和它們在CoordinatorLayout上的index一致。
根據依賴關係排完序的子視圖放在一個List中,名爲mDependencySortedChildren,它在CoordinatorLayout的onMeasure()、onLayout()以及另外一個重要方法dispatchOnDependentViewChanged()中都用到了。
直接看onMeasure()方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 將全部直接子視圖按依賴關係排序。 prepareChildren(); // 根據子視圖間是否有依賴關係決定是否須要添加 // ViewTreeObserver中的OnPreDrawListener。 // 在此Listener中會調用dispatchOnDependentViewChanged()方法。 ensurePreDrawListener(); // 獲取padding和佈局方向。 final int paddingLeft = getPaddingLeft(); ...... // 解析傳入寬和高的mode和size。 final int widthMode = View.MeasureSpec.getMode(widthMeasureSpec); ...... // 最小寬度不小於paddingLeft + paddingRight,高度相似。 int widthUsed = getSuggestedMinimumWidth(); ...... // 按照依賴順序依次測量child for (int i = 0; i < childCount; i++) { final View child = mDependencySortedChildren.get(i); ...... if (lp.keyline >= 0 && widthMode != View.MeasureSpec.UNSPECIFIED) { // 針對keyline屬性的特殊處理 ...... } // Child的MeasureSpec同CoordinatorLayout的徹底同樣, // 這體現了它和FrameLayout的類似性。 int childWidthMeasureSpec = widthMeasureSpec; ...... if (applyInsets && !ViewCompat.getFitsSystemWindows(child)) { // 處理 CoordinatorLayout設置FitsSystemWindows爲true // 但child這一屬性爲false的狀況。 ...... } final Behavior b = lp.getBehavior(); // 由child的Behavior決定本身的測量結果,或者調用默認的 // onMeasureChild(), 這一段中調用了child.measure()。 if (b == null || !b.onMeasureChild(...)) { onMeasureChild(...); } // 使用的寬和高取全部child測量結果的最大值。 widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); ...... // 依次結合全部的measuredState (全部測量狀態作或運算), // 當前好像只有一個MEASURED_STATE_TOO_SMALL狀態。 childState = ViewCompat.combineMeasuredStates(childState, ViewCompat.getMeasuredState(child)); } // 獲取最終的測量結果,須要size,spec和child state三個參數。 // 寬和高的前8位是measuredState,後24位是measuredSize。 final int width = ViewCompat.resolveSizeAndState(...); ..... setMeasuredDimension(width, height); }
另外,CoordinatorLayout默認的onMeasureChild()方法是直接調用ViewGroup的measureChildWithMargins()方法,沒有作什麼特殊處理。
看一下CoordinatorLayout的onLayout()方法:
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); } } }
從上面的代碼中看到兩點:
CoordinatorLayout的佈局次序不是按照子視圖的index依次來作,而是按照子視圖的依賴關係次序來作的。
若是一個子視圖Behavior的onLayoutChild()方法返回true,那麼CoordinatorLayout默認的onLayoutChild()不會執行,徹底由子視圖本身決定本身的佈局。因此子視圖想在默認佈局的基礎上作修改,須要本身先調用CoordinatorLayout的onLayoutChild()方法,這和返回false而後再讓CoordinatorLayout作佈局的調用次序是不同的,效果應該也有所不一樣。
再來看一下CoordinatorLayout默認的onLayoutChild()方法,它針對不一樣的佈局參數配置調用不一樣的三個方法:
public void onLayoutChild(View child, int layoutDirection) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); ...... if (lp.mAnchorView != null) { layoutChildWithAnchor(child, lp.mAnchorView, layoutDirection); } else if (lp.keyline >= 0) { layoutChildWithKeyline(child, lp.keyline, layoutDirection); } else { layoutChild(child, layoutDirection); } }
這其中keyline這個屬性是幹什麼用的,還沒細看,直接看layoutChild()方法的註釋:
Lay out a child view with no special handling. This will position the child as if it were within a FrameLayout or similar simple frame.
爲子視圖佈局,不包含任何特殊處理。這會用同FramwLayout同樣的方式來放置視圖 —— 這就是文檔上說CoordinatorLayout是一個super-powered FrameLayout的緣由了,即便它並不繼承於FrameLayout。
下面看一下很重要的dispatchOnDependentViewChanged()方法,它的說明以下:
Dispatch any dependent view changes to the relevant CoordinatorLayout.Behavior instances. Usually run as part of the pre-draw step when at least one child view has a reported dependency on another view. This allows CoordinatorLayout to account for layout changes and animations that occur outside of the normal layout pass. It can also be ran as part of the nested scrolling dispatch to ensure that any offsetting is completed within the correct coordinate window. The offsetting behavior implemented here does not store the computed offset in the LayoutParams; instead it expects that the layout process will always reconstruct the proper positioning.
將任何被依賴的視圖的變化分派給相關的Behavior實例。若是至少有一個子視圖上報了對其它視圖的依賴,那麼此方法一般做爲繪製前準備工做的一部分來執行。這容許CoordinatorLayout來解釋常規佈局途徑以外的佈局變更和動畫。它也能夠做爲嵌套滾動事件分派過程的一部分來執行,保證位移是在正確的座標窗口內完成。這裏實現的位移行爲沒有在LayoutParams中存儲計算後的偏移量,反之它認爲佈局過程始終會從新進行恰當的定位。
翻譯過來的內容不太好理解,直接看代碼:
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(); // 查詢child是否錨定在另外一個子視圖上, // 注意由於已經排過序,因此只用查找0到i-1的子視圖。 for (int j = 0; j < i; j++) { final View checkChild = mDependencySortedChildren.get(j); if (lp.mAnchorDirectChild == checkChild) { // 按照錨定視圖的位置調整child的位置 offsetChildToAnchor(child, layoutDirection); } } // 檢查此child的位置是否有變化 final Rect oldRect = mTempRect1; final Rect newRect = mTempRect2; getLastChildRect(child, oldRect); getChildRect(child, true, newRect); if (oldRect.equals(newRect)) { // 若是沒有變化,繼續檢查下一個子視圖 continue; } // 保存這次child的佈局位置 recordLastChildRect(child, newRect); // 按照咱們的排序方式,認爲全部依賴此child的子視圖都在i+1以後 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(); // 下面就用到了Behavior中關鍵的兩個方法: // layoutDependsOn() 和 onDependentViewChanged()。 if (b != null && b.layoutDependsOn(this, checkChild, child)) { if (!fromNestedScroll && checkLp.getChangedAfterNestedScroll()) { checkLp.resetChangedAfterNestedScroll(); continue; } final boolean handled = b.onDependentViewChanged( this, checkChild, child); if (fromNestedScroll) { checkLp.setChangedAfterNestedScroll(handled); } } } } }
依據上面的方法,再考慮以前兩個子視圖互相依賴的問題,若是它們在mDependencySortedChildren中的次序和index次序一致,那麼xml中寫在前面的子視圖對後面的子視圖的依賴將無效。有時間能夠寫個簡單的demo測試一下這一關係。