Android Design Support Library: 学习CoordinatorLayout

简述

  CoordinatorLayout字面意思是“协调器布局”,它是Design Support Library中提供的一个超级帧布局,帮助我们实现Material Design中各种复杂的动作响应和子控件间交互。我认为它是这个库中最难的一个新视图,也是可定制化潜力最大的,应该可以用较统一的方式实现很多旧的开源控件的效果。


索引


参考资料



学习摘要


Android Developers Blog: Android Design Support Library摘要

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.

  正如它的名字表示的那样,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 API文档摘要

CoordinatorLayout is a super-powered FrameLayout. CoordinatorLayout is intended for two primary use cases:

  1. As a top-level application decor or chrome layout
  2. 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);
}

Handling Scrolls with CoordinatorLayout摘要

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的工作原理,暂时不管。


Demo分析


  我想尝试用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;
        }
    }
};
  • 如果A和B是同一视图,则 A = B
  • 如果A依赖B,则 A>B
  • 如果B依赖A,则 B>A
  • 如果A和B没有依赖关系,则 A = B

  简单来说就是依赖别人的大,被依赖的小。

  然后,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()中都用到了。


CoordinatorLayout 的测量过程

  直接看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的布局方法

  看一下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()

下面看一下很重要的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测试一下这一关系。

原文地址:https://www.cnblogs.com/yuanchongjie/p/4997134.html