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测试一下这一关系。