ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

布局ViewGroup原理解析(三):RelativeLayout

2021-01-07 11:59:32  阅读:201  来源: 互联网

标签:ViewGroup RelativeLayout int width params child 解析 final View


Android中最基础的三大布局为FrameLayout、LinearLayout与RelativeLayout。

记得刚初学Android 的时候最喜欢用LinearLayout因为它简单易用,但是越做到后面越喜欢使用RelativeLayout,因为它的灵活性和适用范围都要比前两者要好,但是这里也要提醒初学者,不要用RelativeLayout做太多嵌套,会产生性能问题。今天讲的就是在项目开发过程中遇到的一个关于RelativeLayout的问题。

问题出现与初步解决

在一次版本迭代中,UI宝宝给出了一个类似于在布局上需要子View超出父布局的设计稿。因为之前没有做过类似的需求,在网上查了一下大概是这样:

Android View的绘制布局过程中,子布局默认是无法超出父布局显示的,如果有类似需要超出父布局的需求,可以通过设置根布局的clipChildren属性。

什么嘛?原来这么简单就可以了,于是我赶紧写了一个demo试了一下:

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#987"
    android:clipChildren="false">
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="400dp"
        android:background="@color/colorAccent">
        <ImageView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_marginTop="450dp"
            android:background="#fff"/>
    </RelativeLayout>
</RelativeLayout>

最后展示的样式如下,白色的ImageView超出了粉红的RelativeLayout显示了出来,基本达到了预期的效果。

在这里插入图片描述

初步解决失败

正当我欢天喜地的去做需求的时候,又发现了另外一个问题:我的需求是一个子View超出RelativeLayout显示,但是不是全部超出而是部分超出。于是我按照刚才的方式布局后发现,部分超出的布局和相信中不太一样,如下图:

在这里插入图片描述

整体布局与刚才的类似,这不过marginTop设置为350 dp,使得最后的ImageView有50 dp是超出父布局的,然而结果是超出的50 dp被裁掉了。

为了确定产生这个问题的原因,需要思考另一个问题:是ImageView有部分没有被绘制出来,还是ImageView的高度被压缩成50dp了?

这个也简单,把鼠标点在studio预览中ImageView的位置,会发现给出的ImageView的边界高度只有50dp了。这代表这在RelativeLayout在布局的过程中把ImageView的高度裁剪掉了。产生的最终原因我们已经知道,但是为什么RelativeLayout会产生这个问题呢?这个就需要我们去源码中寻找答案了。

不得不说源码的越多过程实在是很头疼,特别是像RelativeLayout这种相对复杂的系统控件。我在网上也看到了一些人的源码解析都很棒,但在这里我希望自己能以最简单的方式把这个解析说清楚。

源码解析

首先我们需要知道,任何一个View或者是ViewGroup在呈现到界面上都需要经过三个阶段:

  • 测量onMesure
  • 布局onLayout
  • 绘制onDraw

对于一个非布局View来说,测量和绘制是其中的比较重要的两个步骤;而对于一个布局ViewGroup来说测量和布局是其中比较重要的两个步骤。

布局阶段onLayout

ViewGroup测量过程需要确定自身的宽高与其子View的宽高,布局则是确定其子View与他的相对位置(主要ViewGroup其本身的位置不是在它的布局过程中确定的,而是在其父ViewGroup布局过程中确定的)。我们可以先看RelativeLayout布局过程是如何完成的:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //  The layout has actually already been performed and the positions
        //  cached.  Apply the cached values to the children.
        final int count = getChildCount();

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                RelativeLayout.LayoutParams st =
                        (RelativeLayout.LayoutParams) child.getLayoutParams();
                child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
            }
        }
    }

整个方法非常简单,对比其他ViewGroup(如FrameLayout)就会发现,其中并不包含关于子View该如何摆放的逻辑计算,直接把子View的LayoutParams中的mTop等布局属性设置进去就好了,那关键在于这些布局属性是在哪里被赋值的呢?

测量阶段onMeasure

我们再来看看它的onMeasure方法:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       /**对子View进行拓扑排序,是整个方法最核心的一环。*/
        if (mDirtyHierarchy) {
            mDirtyHierarchy = false;
            sortChildren();
        }

       /**获取父ViewGroup对其设置的期望宽高,可能是写死的也可能是不确定的。*/
        int myWidth = -1;
        int myHeight = -1;

        int width = 0;
        int height = 0;

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        // Record our dimensions if they are known;
        if (widthMode != MeasureSpec.UNSPECIFIED) {
            myWidth = widthSize;
        }

        if (heightMode != MeasureSpec.UNSPECIFIED) {
            myHeight = heightSize;
        }

        if (widthMode == MeasureSpec.EXACTLY) {
            width = myWidth;
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            height = myHeight;
        }

       /**提前准备好一些布局参数,如重心、长宽模式等,这些对后续测量都有影响。*/
        View ignore = null;
        int gravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
        final boolean horizontalGravity = gravity != Gravity.START && gravity != 0;
        gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final boolean verticalGravity = gravity != Gravity.TOP && gravity != 0;

        int left = Integer.MAX_VALUE;
        int top = Integer.MAX_VALUE;
        int right = Integer.MIN_VALUE;
        int bottom = Integer.MIN_VALUE;

        boolean offsetHorizontalAxis = false;
        boolean offsetVerticalAxis = false;

        if ((horizontalGravity || verticalGravity) && mIgnoreGravity != View.NO_ID) {
            ignore = findViewById(mIgnoreGravity);
        }

        final boolean isWrapContentWidth = widthMode != MeasureSpec.EXACTLY;
        final boolean isWrapContentHeight = heightMode != MeasureSpec.EXACTLY;

        // We need to know our size for doing the correct computation of children positioning in RTL
        // mode but there is no practical way to get it instead of running the code below.
        // So, instead of running the code twice, we just set the width to a "default display width"
        // before the computation and then, as a last pass, we will update their real position with
        // an offset equals to "DEFAULT_WIDTH - width".
        final int layoutDirection = getLayoutDirection();
        if (isLayoutRtl() && myWidth == -1) {
            myWidth = DEFAULT_WIDTH;
        }

       /**获取子View在水平方向上的拓扑排序,按照他们之间的约束测量各个子View的宽度。*/
        View[] views = mSortedHorizontalChildren;
        int count = views.length;

        for (int i = 0; i < count; i++) {
            View child = views[i];
            if (child.getVisibility() != GONE) {
                LayoutParams params = (LayoutParams) child.getLayoutParams();
                int[] rules = params.getRules(layoutDirection);

                applyHorizontalSizeRules(params, myWidth, rules);
                measureChildHorizontal(child, params, myWidth, myHeight);

                if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
                    offsetHorizontalAxis = true;
                }
            }
        }

        /**获取子View在垂直方向上的拓扑排序,按照他们之间的约束测量各个子View的高度。由于在此循环中子View的宽高都确定了,在这里面还会重新计算特殊情况下父View的宽高。*/
        views = mSortedVerticalChildren;
        count = views.length;
        final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;

        for (int i = 0; i < count; i++) {
            final View child = views[i];
            if (child.getVisibility() != GONE) {
                final LayoutParams params = (LayoutParams) child.getLayoutParams();

                applyVerticalSizeRules(params, myHeight, child.getBaseline());
                measureChild(child, params, myWidth, myHeight);
                if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
                    offsetVerticalAxis = true;
                }

                if (isWrapContentWidth) {
                    if (isLayoutRtl()) {
                        if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
                            width = Math.max(width, myWidth - params.mLeft);
                        } else {
                            width = Math.max(width, myWidth - params.mLeft - params.leftMargin);
                        }
                    } else {
                        if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
                            width = Math.max(width, params.mRight);
                        } else {
                            width = Math.max(width, params.mRight + params.rightMargin);
                        }
                    }
                }

                if (isWrapContentHeight) {
                    if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
                        height = Math.max(height, params.mBottom);
                    } else {
                        height = Math.max(height, params.mBottom + params.bottomMargin);
                    }
                }

               /**处理重心对子View的影响*/
                if (child != ignore || verticalGravity) {
                    left = Math.min(left, params.mLeft - params.leftMargin);
                    top = Math.min(top, params.mTop - params.topMargin);
                }

                if (child != ignore || horizontalGravity) {
                    right = Math.max(right, params.mRight + params.rightMargin);
                    bottom = Math.max(bottom, params.mBottom + params.bottomMargin);
                }
            }
        }

        // Use the top-start-most laid out view as the baseline. RTL offsets are
        // applied later, so we can use the left-most edge as the starting edge.
        View baselineView = null;
        LayoutParams baselineParams = null;
        for (int i = 0; i < count; i++) {
            final View child = views[i];
            if (child.getVisibility() != GONE) {
                final LayoutParams childParams = (LayoutParams) child.getLayoutParams();
                if (baselineView == null || baselineParams == null
                        || compareLayoutPosition(childParams, baselineParams) < 0) {
                    baselineView = child;
                    baselineParams = childParams;
                }
            }
        }
        mBaselineView = baselineView;

        if (isWrapContentWidth) {
            // Width already has left padding in it since it was calculated by looking at
            // the right of each child view
            width += mPaddingRight;

            if (mLayoutParams != null && mLayoutParams.width >= 0) {
                width = Math.max(width, mLayoutParams.width);
            }

            width = Math.max(width, getSuggestedMinimumWidth());
            width = resolveSize(width, widthMeasureSpec);

            if (offsetHorizontalAxis) {
                for (int i = 0; i < count; i++) {
                    final View child = views[i];
                    if (child.getVisibility() != GONE) {
                        final LayoutParams params = (LayoutParams) child.getLayoutParams();
                        final int[] rules = params.getRules(layoutDirection);
                        if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_HORIZONTAL] != 0) {
                            centerHorizontal(child, params, width);
                        } else if (rules[ALIGN_PARENT_RIGHT] != 0) {
                            final int childWidth = child.getMeasuredWidth();
                            params.mLeft = width - mPaddingRight - childWidth;
                            params.mRight = params.mLeft + childWidth;
                        }
                    }
                }
            }
        }
        /**重新计算在长宽为WRAP_CONTENT的情况下父View的整体大小。*/
        if (isWrapContentHeight) {
            // Height already has top padding in it since it was calculated by looking at
            // the bottom of each child view
            height += mPaddingBottom;

            if (mLayoutParams != null && mLayoutParams.height >= 0) {
                height = Math.max(height, mLayoutParams.height);
            }

            height = Math.max(height, getSuggestedMinimumHeight());
            height = resolveSize(height, heightMeasureSpec);

            if (offsetVerticalAxis) {
                for (int i = 0; i < count; i++) {
                    final View child = views[i];
                    if (child.getVisibility() != GONE) {
                        final LayoutParams params = (LayoutParams) child.getLayoutParams();
                        final int[] rules = params.getRules(layoutDirection);
                        if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_VERTICAL] != 0) {
                            centerVertical(child, params, height);
                        } else if (rules[ALIGN_PARENT_BOTTOM] != 0) {
                            final int childHeight = child.getMeasuredHeight();
                            params.mTop = height - mPaddingBottom - childHeight;
                            params.mBottom = params.mTop + childHeight;
                        }
                    }
                }
            }
        }

        if (horizontalGravity || verticalGravity) {
            final Rect selfBounds = mSelfBounds;
            selfBounds.set(mPaddingLeft, mPaddingTop, width - mPaddingRight,
                    height - mPaddingBottom);

            final Rect contentBounds = mContentBounds;
            Gravity.apply(mGravity, right - left, bottom - top, selfBounds, contentBounds,
                    layoutDirection);

            final int horizontalOffset = contentBounds.left - left;
            final int verticalOffset = contentBounds.top - top;
            if (horizontalOffset != 0 || verticalOffset != 0) {
                for (int i = 0; i < count; i++) {
                    final View child = views[i];
                    if (child.getVisibility() != GONE && child != ignore) {
                        final LayoutParams params = (LayoutParams) child.getLayoutParams();
                        if (horizontalGravity) {
                            params.mLeft += horizontalOffset;
                            params.mRight += horizontalOffset;
                        }
                        if (verticalGravity) {
                            params.mTop += verticalOffset;
                            params.mBottom += verticalOffset;
                        }
                    }
                }
            }
        }

        if (isLayoutRtl()) {
            final int offsetWidth = myWidth - width;
            for (int i = 0; i < count; i++) {
                final View child = views[i];
                if (child.getVisibility() != GONE) {
                    final LayoutParams params = (LayoutParams) child.getLayoutParams();
                    params.mLeft -= offsetWidth;
                    params.mRight -= offsetWidth;
                }
            }
        }

        setMeasuredDimension(width, height);
    }

可以看到整个onMesure方法很长,总结下来主要分为以下几个步骤:
1.对子View在水平和垂直方向上分别进行拓扑排序,最核心的一步。
2.在水平方向上根据各子View的约束计算各子View的宽度和水平位置。
3.在垂直方向上根据各子View的约束计算各子View的高度和垂直位置。
4.处理在自身宽高设置为WRAP_CONTENT时重新计算父View的大小。

代码中还有很多为了处理重心、宽高WRAP_CONTENT、左到右布局与右到左布局的临时逻辑,所以整体业务逻辑非常冗杂,我上面列出来的是我认为最重要的核心逻辑。

所以现在可以回答原来的问题了,子View的布局属性在onMeasure阶段就被确定了。其实对于相对布局来说,每一个子View的大小其实和与他相关的兄弟View的位置是有关系(比如设置V1在V2的底部,V1的高度设置为MATCH_PARENT,则此时V2的位置就可以决定V1的高度),所以如果说要确定一个子View的大小就要同时知道它的位置,这就是为什么布局属性会在onMeasure中被确定的原因。

最终原因

在源码解析中可以看到,能够影响子View大小和位置的阶段就是步骤3了,我们回到代码中看看具体做了什么:

                /**第一步是运用view的依赖规则,设置View的垂直方法上的限制。*/
                applyVerticalSizeRules(params, myHeight, child.getBaseline());
                 /**第二步是根据第一步给出的限制,调用子View的mesure方法,测量出子View的大小*/
                measureChild(child, params, myWidth, myHeight);
                /**第三步是根据第一步的限制和第二步的大小,计算出子View最终的位置。*/
                if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
                    offsetVerticalAxis = true;
                }

注释写的很清楚了,源码就不直接贴了。这说明能够影响到子View大小的只有第一步和第二步,我们深入方法中去排查,终于找到了出问题的地方。

private int getChildMeasureSpec(int childStart, int childEnd,
            int childSize, int startMargin, int endMargin, int startPadding,
            int endPadding, int mySize) {
        // Figure out start and end bounds.
        int tempStart = childStart;
        int tempEnd = childEnd;

        // If the view did not express a layout constraint for an edge, use
        // view's margins and our padding
        if (tempStart == VALUE_NOT_SET) {
            tempStart = startPadding + startMargin;
        }
        if (tempEnd == VALUE_NOT_SET) {
            tempEnd = mySize - endPadding - endMargin;
        }

        // Figure out maximum size available to this view
        final int maxAvailable = tempEnd - tempStart;
            ...
             if (childSize >= 0) {
                // Child wanted an exact size. Give as much as possible.
                childSpecMode = MeasureSpec.EXACTLY;
                if (maxAvailable >= 0) {
                    // We have a maximum size in this dimension.
                    childSpecSize = Math.min(maxAvailable, childSize);
                } else {
                    // We can grow in this dimension.
                    childSpecSize = childSize;
                }
            }
            ...
        return MeasureSpec.makeMeasureSpec(childSpecSize, childSpecMode);
    }

这个是根据约束生成子View的MeasureSpec的方法,大家都知道这个MeasureSpec在子View的onMeasure中会被用到,算是父布局对子View的大小期望。

其中,maxAvailable这个临时变量在子View设置margin完全超出父布局的情况下是小于0的(因为childStart大于mySize,而childEnd等于VALUE_NO_SET,具体的原因可以去看第一步applyVerticalSizeRules方法),在子View并为完全超出父布局的情况下是大于0且等于其被父布局挤压的剩余高度。

childSpecSize在maxAvailable大于0的时候是等于maxAvailable,小于0时才等于原来设置的高度,这就是为什么子View在完全超出父布局时大小正常而部分超出时会失败的原因。

后续

其实知道了产生原因也没什么用,因为如果不改变源码,这个问题也无法修改。但其中更重要的是我们在查找问题的过程中了解整个RelativeLayout的布局原理,这对我们以后使用和排查问题都很有帮助。

其实源码里面还有很多东西没有讲,比如在onMeasure中最重要的一步,对于子View的拓扑排序是如何进行的,这个有兴趣的同学可以打开源码学习学习,网上也有人做过分享,我有时间的话也写一个吧。

填一下之前埋的一个坑,为什么RelativeLayout不要做太多的嵌套?
在源码中我们可以看到,每一次onMeasure过程中,对于每一个子View都会调用两次的measure方法(水平、垂直),所以如果我们嵌套太多,这个方法调用会呈指数级别增长。

标签:ViewGroup,RelativeLayout,int,width,params,child,解析,final,View
来源: https://blog.csdn.net/bugmiao/article/details/112306356

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有