美文网首页NestedScrolling与Behavior
3CoordinatorLayout的measure和layou

3CoordinatorLayout的measure和layou

作者: chefish | 来源:发表于2016-09-27 16:00 被阅读87次

    本文分析下上篇文章的布局情况。CoordinatorLayout的布局跟普通viewgroup不太一样,behavior会插一手。本篇主要介绍behavior如何影响measure和layout。

    提出问题

    上文activity的xml如下

    <?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"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        tools:context="com.fish.behaviordemo.MainActivity">
    
        <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:theme="@style/AppTheme.AppBarOverlay">
    
            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="?attr/colorPrimary"
                app:popupTheme="@style/AppTheme.PopupOverlay" />
    
        </android.support.design.widget.AppBarLayout>
    
        <include layout="@layout/content_fab" />
    
        <android.support.design.widget.FloatingActionButton
            android:id="@+id/fab"
    
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|end"
            android:layout_margin="@dimen/fab_margin"
            android:src="@android:drawable/ic_dialog_email"
            app:layout_behavior="com.fish.behaviordemo.fab.MyBehavior" />
    
    </android.support.design.widget.CoordinatorLayout>
    
    

    界面显示效果如下所示

    最外层是CoordinatorLayout,里面放了个AppBarLayout、content_fab、FloatingActionButton。我们先不管FloatingActionButton。都说CoordinatorLayout是个super FrameLayout,那这里的布局应该是content_fab叠在AppBarLayout上咯?可是我们看到的是content_fab在AppBarLayout下方,这可不像FrameLayout,难道是被盖住了一部分没看到吗?错了,content_fab的的确却是在AppBarLayout的下方,这是CoordinatorLayout布局的时候定下来的。
    我们这里的style如下,这个style的view tree内是没有statusbar的,可参考http://blog.csdn.net/litefish/article/details/52034813

        <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
            <!-- Customize your theme here. -->
            <item name="colorPrimary">@color/colorPrimary</item>
            <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
            <item name="colorAccent">@color/colorAccent</item>
        </style>
    

    那就有另一个问题了,为何AppBarLayout不在屏幕的顶上,而在statubar的下方。
    带着这2个问题,我们来看CoordinatorLayout的布局过程。
    问题1:为何AppBarLayout会在statusbar的下方
    问题2:为何content_fab在AppBarLayout的下方

    列举behavior

    behavior是会介入onMeasure和onLayout过程的,我们先把各个子view的behavior找出来
    AppBarLayout的behavior是由注解决定的
    而content_fab的behavior是什么?content_fab是个RelativeLayout,后文我们称RelativeLayout。
    xml内有这么一句话

     app:layout_behavior="@string/appbar_scrolling_view_behavior"
    

    @string/appbar_scrolling_view_behavior是什么?

        <string name="appbar_scrolling_view_behavior" translatable="false">android.support.design.widget.AppBarLayout$ScrollingViewBehavior</string>
    
    

    其实从下面代码可以看出是AppBarLayout.ScrollingViewBehavior

    所以此处的CoordinatorLayout的3个子view和behavior如下所示

    view behavior
    AppBarLayout AppBarLayout.Behavior
    RelativeLayout AppBarLayout.ScrollingViewBehavior
    FloatingActionButton MyBehavior

    我们不管FloatingActionButton,看AppBarLayout和RelativeLayout,都挺复杂的,下图是累关系图,可以看到都是从ViewOffsetBehavior派生而来的。

    再来看AppBarLayout.ScrollingViewBehavior内的代码,可以看到依赖于AppBarLayout,所以这里RelativeLayout依赖于AppBarLayout

    //AppBarLayout.ScrollingViewBehavior
          @Override
            public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
                // We depend on any AppBarLayouts
                return dependency instanceof AppBarLayout;
            }
    

    onMeasure分析

    CoordinatorLayout的onMeasure方法如下所示,比较简单,主要流程是,先算出insets的值,然后measure的时候去掉这些insets,在measure 子view的时候,behavior先measure,返回false的话,才轮到view本身measure。
    我们知道CoordinatorLayout是userRoot的根节点,所以第一次measure CoordinatorLayout的heightMeasureSpec.size是第一次用1668(1794-126),第二次用1794(1920-126)。上边这些数字都是在我手机上的值,1794是DisplayMetrics的高度值,126是navigatorbar的高度,1920是屏幕高度。
    所以下边代码里,onMeasure参数heightMeasureSpec的size第一次是1668,第二次是1794。
    下面开始具体分析

       @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            prepareChildren();
            ensurePreDrawListener();
            。。。
            final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
            final int widthPadding = paddingLeft + paddingRight;
            final int heightPadding = paddingTop + paddingBottom;
            ...
    
            final int childCount = mDependencySortedChildren.size();
            for (int i = 0; i < childCount; i++) {
    
                int childWidthMeasureSpec = widthMeasureSpec;
                int childHeightMeasureSpec = heightMeasureSpec;
                if (applyInsets && !ViewCompat.getFitsSystemWindows(child)) {
                    // We're set to handle insets but this child isn't, so we will measure the
                    // child as if there are no insets
                    final int horizInsets = mLastInsets.getSystemWindowInsetLeft()
                            + mLastInsets.getSystemWindowInsetRight();
                    //获取vertInsets值,其实这里就是statubar的高度
                    final int vertInsets = mLastInsets.getSystemWindowInsetTop()
                            + mLastInsets.getSystemWindowInsetBottom();
    
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            widthSize - horizInsets, widthMode);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            heightSize - vertInsets, heightMode);
                }
    
                final Behavior b = lp.getBehavior();
                //behavior先measure,返回false的话,才轮到view本身measure
                if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                        childHeightMeasureSpec, 0)) {
                    onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                            childHeightMeasureSpec, 0);
                }
            }
    
            。。。
        }
    
        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);
    }
    

    第一次measure,heightMeasureSpec.size为1668
    prepareChildren和ensurePreDrawListener,之前在 这里分析过,不清楚的可以回顾下。
    因为子view没有写fitsSystemWindows,所以L20会进去,在L23会算出vertInsets,这里实际就是statubar的高度(63)。然后L29,修改childHeightMeasureSpec.size为1668-63=1605。从这里可以看出在measure的过程中,其实是除掉了statubar的高度,然后走到L35,先交给behavior measure。这里我们主要看下RelativeLayout这个child是如何measure的,RelativeLayout的behavior是AppBarLayout.ScrollingViewBehavior,他没有复写onMeasureChild方法,所以看父类HeaderScrollingViewBehavior。先看L7,只有MATCH_PARENT或WRAP_CONTENT,我们behavior才处理,否则直接返回false,丢给view自己处理,我们这里是MATCH_PARENT,由behavior处理。 然后看L13,找到一个header,这是个view,这个非常重要,是当前view的第一个依赖view(可以称为header),当前view的各种操作都会依赖于header,明显,我们的RelativeLayout的header就是AppBarLayout,再看L28,如果head没有layout过,那直接返回false,意思就是必须在head 布局完成之后,再来measure我们RelativeLayout。这个行为是比较奇怪的,一个view的measure居然依赖另一个view的layout。此时,我们肯定没有layout过,所以直接返回false,然后走上边代码的L39,之后RelativeLayout的measuredHeight变为1605.

    第二次measure,先看上文代码,heightMeasureSpec.size为1794,在L28改为1794-63=1731,进入下边代码,此时header还没layout,所以返回false,依然是view自身measure,之后RelativeLayout的measuredHeight变为1794.

    上2次measure都在layout之前,是通过view本身来measure的,我们在看看layout之后的measure

    第三次measure(这次measure是怎么触发的呢?),此时已经layout过了
    先看上文代码,heightMeasureSpec.size为1794,在L29改为1794-63=1731,进入下边代码,此时header已经layout,所以进入L28的if内,重点关注L35,

    final int height = availableHeight - header.getMeasuredHeight()
    + getScrollRange(header);
    翻一下就是parent的高度-header的measuredHeight+header的滚动范围

    这里减掉了一个header.getMeasuredHeight(),加上了getScrollRange(header),后者我们暂时不考虑,此时其实就是减去了AppBarlayout的measuredHeight,在这里就是1731-147=1584,然后调用 CoordinatorLayout.onMeasureChild,最后measure结果就是1584,下边代码返回true。这其实是HeaderScrollingViewBehavior的一个特性,让当前view处于header的下方。

    好了,三轮measure下来,最终RelativeLayout的measuredHeight被定为1584

    //HeaderScrollingViewBehavior
        @Override
        public boolean onMeasureChild(CoordinatorLayout parent, View child,
                int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
                int heightUsed) {
            final int childLpHeight = child.getLayoutParams().height;
            if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
                    || childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
                // If the menu's height is set to match_parent/wrap_content then measure it
                // with the maximum visible height
    
                final List<View> dependencies = parent.getDependencies(child);
                final View header = findFirstDependency(dependencies);
                if (header != null) {
                    if (ViewCompat.getFitsSystemWindows(header)
                            && !ViewCompat.getFitsSystemWindows(child)) {
                        // If the header is fitting system windows then we need to also,
                        // otherwise we'll get CoL's compatible measuring
                        ViewCompat.setFitsSystemWindows(child, true);
    
                        if (ViewCompat.getFitsSystemWindows(child)) {
                            // If the set succeeded, trigger a new layout and return true
                            child.requestLayout();
                            return true;
                        }
                    }
                       //header未layout,我就不measure
                    if (ViewCompat.isLaidOut(header)) {
                        int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
                        if (availableHeight == 0) {
                            // If the measure spec doesn't specify a size, use the current height
                            availableHeight = parent.getHeight();
                        }
    
                        final int height = availableHeight - header.getMeasuredHeight()
                                + getScrollRange(header);
                        final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height,
                                childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
                                        ? View.MeasureSpec.EXACTLY
                                        : View.MeasureSpec.AT_MOST);
    
                        // Now measure the scrolling view with the correct height
                        parent.onMeasureChild(child, parentWidthMeasureSpec,
                                widthUsed, heightMeasureSpec, heightUsed);
    
                        return true;
                    }
                }
            }
            return false;
        }
    

    onLayout分析

    看下边CoordinatorLayout的onLayout,发现布局的子view的时候,先由behavior处理,behavior未处理成功再交给child处理,跟onMeasure类似。

     @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);
                }
            }
        }
    

    我们再来分析,因为RelativeLayout依赖于AppBarLayout,所以在mDependencySortedChildren内,AppBarLayout在前,RelativeLayout在后。

    布局AppBarLayout

    先看如何布局AppBarLayout。

    step1

    看AppBarLayout.Behavior的onLayoutChild,首先调用了super.onLayoutChild,会调用到ViewOffsetBehavior的onLayoutChild

          //AppBarLayout.Behavior
          @Override
            public boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout abl,
                    int layoutDirection) {
                boolean handled = super.onLayoutChild(parent, abl, layoutDirection);
                ...
            }
    
    

    step2

    再看ViewOffsetBehavior的onLayoutChild,调用layoutChild,然后调用parent.onLayoutChild,parent是谁,CoordinatorLayout

       public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
            // First let lay the child out
            layoutChild(parent, child, layoutDirection);
    
            if (mViewOffsetHelper == null) {
                mViewOffsetHelper = new ViewOffsetHelper(child);
            }
            mViewOffsetHelper.onViewLayout();
    
            if (mTempTopBottomOffset != 0) {
                mViewOffsetHelper.setTopAndBottomOffset(mTempTopBottomOffset);
                mTempTopBottomOffset = 0;
            }
            if (mTempLeftRightOffset != 0) {
                mViewOffsetHelper.setLeftAndRightOffset(mTempLeftRightOffset);
                mTempLeftRightOffset = 0;
            }
    
            return true;
        }
         
           protected void layoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
            // Let the parent lay it out by default
            parent.onLayoutChild(child, layoutDirection);
        }
    

    step3

    CoordinatorLayout的onLayoutChild一般会调用layoutChild,看完这段代码,就应该能明白问题1。里面有个parent叫Rect,这个Rect代表CoordinatorLayout内部可以放子view的空间,一开始的时候parent的top为0,在L15把statusbar的高度加到top里去,这其实就是为了让AppbarLayout不要和statusbar重叠。在L21根据AppbarLayout的getMeasuredHeight()和parent,算出一个Rect out,用这个Rect来给AppbarLayout布局,out里的top必定是statusbar的高度,在L23 child.layout内AppbarLayout的mTop必然被设置为statusbar的高度,所以问题1解决,核心代码就是layoutChild。注意layoutChild不是针对AppbarLayout,所以任何子view都不可能跑到statubar上。

        private void layoutChild(View child, int layoutDirection) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final Rect parent = mTempRect1;
            parent.set(getPaddingLeft() + lp.leftMargin,
                    getPaddingTop() + lp.topMargin,
                    getWidth() - getPaddingRight() - lp.rightMargin,
                    getHeight() - getPaddingBottom() - lp.bottomMargin);
    
            if (mLastInsets != null && ViewCompat.getFitsSystemWindows(this)
                    && !ViewCompat.getFitsSystemWindows(child)) {
                // If we're set to handle insets but this child isn't, then it has been measured as
                // if there are no insets. We need to lay it out to match.
                parent.left += mLastInsets.getSystemWindowInsetLeft();
                //这里把statusbar的高度算进去了
                parent.top += mLastInsets.getSystemWindowInsetTop();
                parent.right -= mLastInsets.getSystemWindowInsetRight();
                parent.bottom -= mLastInsets.getSystemWindowInsetBottom();
            }
    
            final Rect out = mTempRect2;
            GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
                    child.getMeasuredHeight(), parent, out, layoutDirection);
            child.layout(out.left, out.top, out.right, out.bottom);
        }
    

    step4 mViewOffsetHelper.onViewLayout

    此时,其实相当于给AppBarLayout设置了一个额外的padding,这个值会被记录下来,在onLayoutChild的L8,有mViewOffsetHelper.onViewLayout.由下边代码可知mLayoutTop就记录了这个额外的padding。ViewOffsetHelper内部有mLayoutTop,mOffsetTop。mLayoutTop代表基本top,mOffsetTop代表额外top偏移量,实际view的top为 mLayoutTop+ mOffsetTop。ViewOffsetHelper内的setTopAndBottomOffset的参数offset是一个绝对值,但是view的offsetTopAndBottom的参数offset是一个delta值,mLayoutTop就可以把绝对值转化为delta值。

        public void onViewLayout() {
            // Now grab the intended top
            mLayoutTop = mView.getTop();
            mLayoutLeft = mView.getLeft();
    
            // And offset it as needed
            updateOffsets();
        }
    

    布局RelativeLayout

    RelativeLayout的behavior是AppBarLayout.ScrollingViewBehavior

    step1

    和布局AppBarLayout一样,会调用ViewOffsetBehavior的onLayoutChild

    step2

    ViewOffsetBehavior的onLayoutChild,调用layoutChild,此时layoutChild可不一样了,因为HeaderScrollingViewBehavior复写了。看下边代码可以解决我们的第二个问题,首先寻找依赖的view,我们的RelativeLayout依赖AppBarLayout,然后看L13,这个代码非常关键,available这个Rect设置在AppBarLayout的下方,然后类似的在L20啊,根据getMeasuredHeight()和available计算出out,再用out来layout RelativeLayout。所以RelativeLayout必然在AppBarLayout的下方,这是由它的behavior决定的。HeaderScrollingViewBehavior要求排在首个依赖view(header)的下方

     @Override
        protected void layoutChild(final CoordinatorLayout parent, final View child,
                final int layoutDirection) {
            final List<View> dependencies = parent.getDependencies(child);
            //寻找依赖
            final View header = findFirstDependency(dependencies);
    
            if (header != null) {
                final CoordinatorLayout.LayoutParams lp =
                        (CoordinatorLayout.LayoutParams) child.getLayoutParams();
                final Rect available = mTempRect1;
                //这个rect在依赖view的下边
                available.set(parent.getPaddingLeft() + lp.leftMargin,
                        header.getBottom() + lp.topMargin,
                        parent.getWidth() - parent.getPaddingRight() - lp.rightMargin,
                        parent.getHeight() + header.getBottom()
                                - parent.getPaddingBottom() - lp.bottomMargin);
    
                final Rect out = mTempRect2;
                GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
                        child.getMeasuredHeight(), available, out, layoutDirection);
    
                final int overlap = getOverlapPixelsForOffset(header);
    
                child.layout(out.left, out.top - overlap, out.right, out.bottom - overlap);
                mVerticalLayoutGap = out.top - header.getBottom();
            } else {
                // If we don't have a dependency, let super handle it
                super.layoutChild(parent, child, layoutDirection);
                mVerticalLayoutGap = 0;
            }
        }
    

    总结

    1、CoordinatorLayout像一个FrameLayout,但是里面的布局受behavior影响,我们可以通过改写behavior来修改布局策略
    2、CoordinatorLayout的高度包括statubar,如果CoordinatorLayout的子view没有写fitsSystemWindows,都不可能跑到statubar上,因为measure的时候会除掉statubar,layoutChild的时候也会处理。
    3、如果CoordinatorLayout的子view重写了fitsSystemWindows,那么子view的范围会包括statubar
    4、HeaderScrollingViewBehavior有个特性,使用此behavior的view 必然排在他的首个依赖view(简称header)的下方,因为复写了layoutChild

    相关文章

      网友评论

        本文标题:3CoordinatorLayout的measure和layou

        本文链接:https://www.haomeiwen.com/subject/gyncyttx.html