view系列源码分析之CoordinatorLayout,嵌套滑动,自定义Behavior分析(2)

上文说了一个behavior的创建和也知道了依赖的产生原理。这篇文章来说说两点:
1appBarlayout为何在状态栏下面:
2..NestScrollView为何在appBarlayout的下面:

让我们继续看CoordinatorLayout的onMeasure方法

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        prepareChildren();
        ensurePreDrawListener();

        final int paddingLeft = getPaddingLeft();
        final int paddingTop = getPaddingTop();
        final int paddingRight = getPaddingRight();
        final int paddingBottom = getPaddingBottom();
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
        final boolean isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL;
        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;
        int widthUsed = getSuggestedMinimumWidth();
        int heightUsed = getSuggestedMinimumHeight();
        int childState = 0;

        final boolean applyInsets = mLastInsets != null && ViewCompat.getFitsSystemWindows(this);

        final int childCount = mDependencySortedChildren.size();
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            if (child.getVisibility() == GONE) {
                // If the child is GONE, skip...
                continue;
            }

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            int keylineWidthUsed = 0;
            
            }

            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();
                final int vertInsets = mLastInsets.getSystemWindowInsetTop()
                        + mLastInsets.getSystemWindowInsetBottom();

                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                        widthSize - horizInsets, widthMode);
                childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                        heightSize - vertInsets, heightMode);
            }

            final Behavior b = lp.getBehavior();
            if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0)) {
                onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                        childHeightMeasureSpec, 0);
            }

            widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() +
                    lp.leftMargin + lp.rightMargin);

            heightUsed = Math.max(heightUsed, heightPadding + child.getMeasuredHeight() +
                    lp.topMargin + lp.bottomMargin);
            childState = View.combineMeasuredStates(childState, child.getMeasuredState());
        }

        final int width = View.resolveSizeAndState(widthUsed, widthMeasureSpec,
                childState & View.MEASURED_STATE_MASK);
        final int height = View.resolveSizeAndState(heightUsed, heightMeasureSpec,
                childState << View.MEASURED_HEIGHT_STATE_SHIFT);
        setMeasuredDimension(width, height);
    }

我们可以很清楚的看到首先是获取了父view的Measurespec的宽高,当然这里会默认进行2次measure打印在模拟器上分别是1488和1704,很多人会觉得奇怪,其实任何自定义的view都会进行2次measure,因为会2次调用viewRootImp的测量方法,第一次是父view给的宽高是获取屏幕密度产生的高度,第二次才是我们通常所说的高度,由于在1920的手机上不包含状态栏和虚拟按键,所以1704就是coordinatorLayout的高度了,在代码上,如果applyInsets不为空的话,高度就是1776也就说会加上状态栏,那appBarlayout的话是不包含的,所以会减去状态栏高度,这在源码里也有体现。
在测量子view的时候,这里用到了Behavior,分别测量了appBarlayout和NestScrollview,首先看appBarlayout

 @Override
        public boolean onMeasureChild(CoordinatorLayout parent, AppBarLayout child,
                                      int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
                                      int heightUsed) {
            final CoordinatorLayout.LayoutParams lp =
                    (CoordinatorLayout.LayoutParams) child.getLayoutParams();
            if (lp.height == CoordinatorLayout.LayoutParams.WRAP_CONTENT) {
                // If the view is set to wrap on it's height, CoordinatorLayout by default will
                // cap the view at the CoL's height. Since the AppBarLayout can scroll, this isn't
                // what we actually want, so we measure it ourselves with an unspecified spec to
                // allow the child to be larger than it's parent
                parent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed,
                        MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), heightUsed);
                return true;
            }

            // Let the parent handle it as normal
            return super.onMeasureChild(parent, child, parentWidthMeasureSpec, widthUsed,
                    parentHeightMeasureSpec, heightUsed);
        }

当给他的高度是wrap_content时,可以看到他允许子view比他更大,也就是说是允许子view滑动的,不是的话,也没做特殊的处理,直接调用其父类的了,再来看下nestScrollview在其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;
                   }
               }

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

重点在

 final int height = availableHeight - header.getMeasuredHeight()
                      + getScrollRange(header);

可以看到他的高度是coordinatorLayout的高度-appBarlayout的高度
+appBarlayou可滑动的高度,当然我们从appBarlayout里可以看到

public final int getTotalScrollRange() {
        if (mTotalScrollRange != INVALID_SCROLL_RANGE) {
            return mTotalScrollRange;
        }

        int range = 0;
        for (int i = 0, z = getChildCount(); i < z; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final int childHeight = child.getMeasuredHeight();
            final int flags = lp.mScrollFlags;

            if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
                // We're set to scroll so add the child's height
                range += childHeight + lp.topMargin + lp.bottomMargin;

                if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
                    // For a collapsing scroll, we to take the collapsed height into account.
                    // We also break straight away since later views can't scroll beneath
                    // us
                    range -= ViewCompat.getMinimumHeight(child);
                    break;
                }
            } else {
                // As soon as a view doesn't have the scroll flag, we end the range calculation.
                // This is because views below can not scroll under a fixed view.
                break;
            }
        }
        return mTotalScrollRange = Math.max(0, range - getTopInset());
    }

他的高度和我们的scroll_flag属性以及状态栏有关

总的来说这个可滑动高度是<=appBarlayout的高度的

看完了onMeasure再来看下onLayout方法

@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);
           if (child.getVisibility() == GONE) {
               // If the child is GONE, skip...
               continue;
           }

           final LayoutParams lp = (LayoutParams) child.getLayoutParams();
           final Behavior behavior = lp.getBehavior();

           if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
               onLayoutChild(child, layoutDirection);
           }
       }
   }

也很简单是同样的操作
这里appBarlayout的layoutchild首先会调用其父类的onlayoutChild

  private void layoutChild(View child, int layoutDirection) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Rect parent = acquireTempRect();
        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();
            parent.top += mLastInsets.getSystemWindowInsetTop();
            parent.right -= mLastInsets.getSystemWindowInsetRight();
            parent.bottom -= mLastInsets.getSystemWindowInsetBottom();
        }

        final Rect out = acquireTempRect();
        GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
                child.getMeasuredHeight(), parent, out, layoutDirection);
        child.layout(out.left, out.top, out.right, out.bottom);

        releaseTempRect(parent);
        releaseTempRect(out);
    }

可以看到如果父view写了fitsysystemwindow为true而appBarlayout没写,会把他的位置手动移到其下面,

这里分析了为何appBarlayout会在状态栏的下方

再说一下nestScollview

 @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;
            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 WindowInsetsCompat parentInsets = parent.getLastWindowInsets();
            if (parentInsets != null && ViewCompat.getFitsSystemWindows(parent)
                    && !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 horizontally.
                // Top and bottom and already handled in the logic above
                available.left += parentInsets.getSystemWindowInsetLeft();
                available.right -= parentInsets.getSystemWindowInsetRight();
            }

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

可以看到available的位置是header.getBottom() + lp.topMargin也就是其top在appBarlayout的下面了(一开始就屈居人下),那有啥属性可以改变呢?可以看到getOverlapPixelsForOffset(head)也就是nestScrollview的Behivor的一个属性Overlay可以改变位置(当然滑动的交互也会改了)

这里分析了为何NestScrollview会在appBarlayout的下方

总结:
基本的布局位置都分析清楚了,主要是对onMeasure和onlayout进行behavior的处理,下篇将讨论滑动的原理.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容