LinearLayout measure流程学习

作者: 英勇青铜5 | 来源:发表于2018-01-01 15:21 被阅读90次

    LinearLayoutViewGroup的子类,ViewGroupView的子类

    不考虑View上层绘制传递过程的,View的测量,是从measure()方法开始看

    View 层测量起点

    UI界面架构图

    一个Activity,通过在onCreate()方法中,setContentView()方法,当作Content放在DecorView

    注意: DecorView 虽然宽高和手机屏幕一样,但是状态栏是不属于DecorView的

    至于Activity如何通过setContentView()DecorView建立起联系的,ViewRoot如何将WindowManagerDecorView以及自身关联的,没看懂,先不管

    Viewmeasure,layout,draw三个流程都受ViewRoot控制,当一个ViewRootDecorView建立联系后,便会通过ViewRootImpl.performTraversals()来开始加载View

    也就是说,View加载的起始点在ViewRootImpl.performTraversals()方法中开始


    LinearLayout的测量,起点也就是这里

    去除n多代码后:

    private void performTraversals(){
    
       ...
       
       performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        
       ...
        
       performLayout(lp, mWidth, mHeight);
        
       ...
        
       performDraw();
        
       ....
       
    }
    

    很直白的说明View的加载流程顺序,在performMeasure()方法中,调用了Viewmeasure()方法

    measure()方法是个final的,内部调用了onMeasure()方法


    View.onMeasure()

    当做一个直接继承自View自定义View时,需要重写这个方法,主要是为了处理宽和高使用wrap_content以及padding

    注意:Viewmeasure过程Activity的生命周期方法是异步的,无法保证在onCreate(),onStart(),onResume()生命周期时,View已经测量完毕

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(
             // 设置最终确定的宽 Width
             getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
             
             // 设置最终确定的高 Height
             getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
        );
    }
    

    由于LinearLayout重写了onMeasure()方法,也就是说,当measure()调用内部的onMeasure()方法时,会直接调用Linearlayout重写的onMearsure()方法


    LinearLayout的测量onMeasure()

    SDK源代码版本是25

    代码:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }
    

    VerticalHorizontal两种情况


    measureVertical()方法

    有4个疑问:

    1. LinearLayout内的所有childView的高度height如何累加的
    2. LinearLayout的宽度width如何确定的
    3. LinearLayout自身的height使用了wrap_content时,高度height如何确定
    4. LinearLayout内的childView的高度使用了权重weight时,LinearLayout自身高度height如何确定,childView的高度如何确定

    变量

    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        // 统计所有的 verticalChildView  的高度和
        mTotalLength = 0;
    
        // 最大宽度
        int maxWidth = 0;
    
        // 子 View 的状态
        int childState = 0;
    
        // 可代替的最大宽度
        int alternativeMaxWidth = 0;
    
        // 使用 weight 属性的 childView 最大宽度
        int weightedMaxWidth = 0;
    
        // childView 是否都是 match_parent 
        // 判断是否需要重新测量
        boolean allFillParent = true;
    
        // 所的 Weight 总和
        float totalWeight = 0;
    
        // 获取 Virtual 的 childView 数量
        final int count = getVirtualChildCount();
        
        // LinearLayout 的 width 的 测量模式
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    
         // LinearLayout 的 hight 的 测量模式
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    
        // 
        boolean matchWidth = false;
    
        // 是否跳过某个 childView,使用 weight 时,为true
        boolean skippedMeasure = false;
    
        // 基线对齐 childView 的 index
        final int baselineChildIndex = mBaselineAlignedChildIndex;  
    
        //       
        final boolean useLargestChild = mUseLargestChild;
    
        // 
        int largestChildHeight = Integer.MIN_VALUE;
    
        // 
        int consumedExcessSpace = 0;
        
        ... 
    }
    

    遍历累加 childView 的 height

    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    
         // 局部变量
         ...
    
    
        // See how tall everyone is. Also remember max width.
        // 遍历数组统计 hight
        for (int i = 0; i < count; ++i) {
    
        // 获取一个 childView
        final View child = getVirtualChildAt(i);
    
        // 是否为 null
        if (child == null) {
            // measureNullChild() 目前返回值为 0
            mTotalLength += measureNullChild(i);
            continue;
        }
    
        // 判断 childView 的可见属性 Visibility 值 
        // 若不开见,就跳过测量
        if (child.getVisibility() == View.GONE) {
           // getChildrenSkipCount() 目前返回值 为 0
           // 估计是预留给以后再做其他优化处理
           i += getChildrenSkipCount(child, i);
           continue;
        }
    
        // 是否需要加上 DividerHeight
        if (hasDividerBeforeChildAt(i)) {
            mTotalLength += mDividerHeight;
        }
    
        // 获取 childView 的 LayoutParams
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    
        // 累加权重
        totalWeight += lp.weight;
    
        // childView 是否使用 weight
        final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
    
        // 此时 LiLinearLayout 的 heightMode 为 MeasureSpec.EXACTLY
        // 并且 childView 使用了 weight 权重
        if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
            // Optimization: don't bother measuring children who are only
            // laid out using excess space. These views will get measured
            // later if we have space to distribute.
            final int totalLength = mTotalLength;
            mTotalLength = 
            Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
            skippedMeasure = true;
        } else {
    
            // 判断 useExcessSpace 的值
            // 若 useExcessSpace 为 true,说明 heightMode != EXACTLY
            if (useExcessSpace) {
                // The heightMode is either UNSPECIFIED or AT_MOST, and
                // this child is only laid out using excess space. Measure
                // using WRAP_CONTENT so that we can find out the view's
                // optimal height. We'll restore the original height of 0
                // after measurement.
    
                // 此时,LinearLayout 的 heightMode 为 UNSPECIFIED 或者 AT_MOST
                lp.height = LayoutParams.WRAP_CONTENT;
            }
    
            // Determine how big this child would like to be. If this or
            // previous children have given a weight, then we allow it to
            // use all available space (and we will shrink things later
            // if needed).
            
            // 已知的使用过的高度
            // 先对 totalWeight 值进行判断,若为 0,说明到目前为止,遍历到的
            // childView 没有使用 weight 属性的
            // 若,有,就先将 usedHeight 置为0
            final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
    
            // 对当前的 childView 进行测量
            measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                    heightMeasureSpec, usedHeight);
            
            // 获取 childView 的测量高
            final int childHeight = child.getMeasuredHeight();
    
            // 判断 useExcessSpace 值
            if (useExcessSpace) {
                // Restore the original height and record how much space
                // we've allocated to excess-only children so that we can
                // match the behavior of EXACTLY measurement.
                lp.height = 0;
                consumedExcessSpace += childHeight;
            }
            
            // 记录临时总的 height
            final int totalLength = mTotalLength;
    
            // 统计 childView 使用 Margin 情况
            mTotalLength = 
            Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                   lp.bottomMargin + getNextLocationOffset(child));
    
            // largestChildHeight,先不管
            if (useLargestChild) {
                largestChildHeight = Math.max(childHeight, largestChildHeight);
            }
        }   
        
        ...
    }
    

    问题1,2

    • LinearLayout内的所有childView的高度height如何累加的
    • LinearLayout的宽度width如何确定的

    LinearLayout宽高都为match_parent,所有childView都没有使用weight时:

    例如:

    一个LinearLayout内,有两个TextView

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        <TextView
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:background="@color/cardview_dark_background"
            android:gravity="center"
            android:text="@string/app_name"
            android:textColor="@color/write"
            android:textSize="30sp" />
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:background="@color/colorPrimaryDark"
            android:gravity="center"
            android:text="@string/measure_name"
            android:textColor="@color/write"
            android:textSize="30sp" />
    </LinearLayout>
    
    宽高都match_parent

    遍历 childView

    当执行到LinearLayoutmeasureVertical()方法内时,进入到遍历childViewfor(int i = 0; i < count; ++i)循环后

    直接开始考虑执行if (heightMode == MeasureSpec.EXACTLY && useExcessSpace){}else{}

    由于两个TextView都没有使用weightuseExcessSpacefalse,也就走else{}内的逻辑

    进入else{}内,最关键的点在于

    final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
    measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                            heightMeasureSpec, usedHeight)
    

    measureChildBeforeLayout()方法便是内调用了ViewGroupmeasureChildWithMargins()


    measureChildWithMargins()内,主要做了两件事:

    1. 根据LinearLayoutMeasureSpec测量模式及自身paddingchildViewLayoutParamsMargin,已经用掉的widthUsed,heightUsed,通过getChildMeasureSpec()计算出childViewMeasureSpec测量模式
    2. 拿到childViewMeasureSpec测量模式之后,
      child.measure(childWidthMeasureSpec, childHeightMeasureSpec),开始进入childViewmeasure()方法

    measureChildWithMargins()方法内,childView的一些列测量回调方法完成后,此时也就可以拿到childHeight = child.getMeasuredHeight()

    每拿到一个childView.height,将拿到的结果,累加进mTotalLength

    final int totalLength = mTotalLength;
    
    // 之前累加的高度,再加上当前childView的height,margin
    // 目前getNextLocationOffset()返回结果都为 0,预留给以后做扩展的吧
    mTotalLength = Math.max(totalLength, totalLength + childHeight + 
    lp.topMargin +lp.bottomMargin + getNextLocationOffset(child));
    

    累加过当前的childViewhight之后,根据LinearLayout及两个TextView的宽高设置,此时matchWidthLocally是为false

    接着便是在遍历childView累加mTotalLength的同时,确定所有childView中的maxWidth


    final int margin = lp.leftMargin + lp.rightMargin;
    final int measuredWidth = child.getMeasuredWidth() + margin;
    maxWidth = Math.max(maxWidth, measuredWidth);
    

    先记录childViewleftMargin,rightMargin和,之后确定当前childViewmeasuredWidth,最后比较当前的childView的宽度与之前记录过的maxWidth做比较


    TODO

    childState = combineMeasuredStates(childState, child.getMeasuredState());
    

    由于两个TextView都没有使用weight,所有if(lp.weight> 0){}else{}走的是else{}分支

    else{}内,会记录所有childView中最大的alternativeMaxWidth,当matchWidthLocallyfalse时,alternativeMaxWidth == maxWidth

    for(){}也便结束,意味着childView遍历完成,接下来便是LiearLayout开始测量自身

    mTotalLength便是当前LinearLayoutheight,针对当前案例,maxWidth = alternativeMaxWidth = txet内容为RetrofitL的TextView的width


    测量自身

    根据LinearLayout的宽高及两个TextView的情况,useLargestChild是不考虑的

    if (useLargestChild ... ) {}内的代码也就无需考虑

    mTotalLength += mPaddingTop + mPaddingBottom加上自身的顶部和底部的padding

    接着heightSize = mTotalLength,与背景background的高度做对比,取大值

    之后再次计算LinaerLayout的精确高度

    接着,走if (skippedMeasure ...) {}else{}else{}分支,在else{}分支内,再次判断alternativeMaxWidth的大小,也就出了else{}分支

    if (!allFillParent && widthMode != MeasureSpec.EXACTLY){},条件不满足,也就不会执行内部。此时,alternativeMaxWidthmaxWidth值是相等的

    maxWidth += mPaddingLeft + mPaddingRight,加上LinearLayout自身左右内边距

    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()),比较maxWidth与背景的宽度width

    最终也就调用了setMeasuredDimension()回调方法,设置最终的测量结果


    问题3

    • LinearLayout自身的height使用了wrap_content时,高度height如何确定

    LinearLayoutheight,两个TextView都没有使用weight时,整个逻辑和当当LinearLayoutheight使用了mathch_parent一样

    在遍历统计了所有的childView高度得到mTotalLength之后

    // Add in our padding
    mTotalLength += mPaddingTop + mPaddingBottom;
    
    int heightSize = mTotalLength;
    
    // Check against our minimum height
    heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
    
    int heightSizeAndState = 
            resolveSizeAndState(heightSize, heightMeasureSpec, 0);
    heightSize = heightSizeAndState & MEASURED_SIZE_MASK
    
    1. 加上LinearLayout自身的TopPadding,BottomPadding
    2. LinearLayout自身的高度与背景高度的大值
    3. 重新计算LinearLayout自身的精确高度

    问题4

    • LinearLayout内的childView的高度使用了权重weight时,LinearLayout自身高度height如何确定,childView的高度如何确定

    加一个TextView,一个100dp,一个使用weight = 1,一个200dp

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        <TextView
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:background="@color/cardview_dark_background"
            android:gravity="center"
            android:text="@string/app_name"
            android:textColor="@color/write"
            android:textSize="30sp" />
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:background="@color/colorPrimaryDark"
            android:gravity="center"
            android:text="@string/measure_name"
            android:textColor="@color/write"
            android:textSize="30sp" />
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="200dp"
            android:background="@color/colorAccent"
            android:gravity="center"
            android:text="@string/get_name"
            android:textColor="@color/write"
            android:textSize="30sp" />
    </LinearLayout>
    

    LinearLayout内有3个childViewgetVirtualChildCount()也就为3


    childView 遍历

    • 第1个TextView

    遍历childView时,第一个TextView为固定高度100dpmTotalLength100dp * 3,而alternativeMaxWidth等于measuredWidthTextViewmeasuredWidth

    • 第2个TextView

    2TextViewheight0dpweight1

    final boolean useExcessSpace 
        = lp.height == 0 && lp.weight > 0; // true
    
    // 进入 if 分支
    if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
       final int totalLength = mTotalLength;
       
       mTotalLength = 
          Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
          
       skippedMeasure = true;
    }else{
    
      ...
      
    }
    

    此时,LinearLayout也不知道当前这个TextViewweight1的情况下高度为多少,会先跳过测量

    在进入if分支语句内,只是统计了下当前childViewlp.topMargin + lp.bottomMargin,并将skippedMeasure设置为true,便结束if(){}else{}分支

    计算统计maxWidth之后

    if (lp.weight > 0) {
       /*
        * Widths of weighted Views are bogus if we end up
        * remeasuring, so keep them separate.
        */
       weightedMaxWidth = Math.max(weightedMaxWidth,
           matchWidthLocally ? margin : measuredWidth);
    } else {
           alternativeMaxWidth = Math.max(alternativeMaxWidth,
                       matchWidthLocally ? margin : measuredWidth);
    }
    

    进入if(){}分支,记录下weightedMaxWidth

    • 第3个TextView

    3TextView和上面不用weight的情况下一样,将height累加到mTotalLength,之后再比较下当前的TextViewmeasuredWidth和之前的记录过的maxWidth的大小


    统计自身及测量使用 weight 的 TextView

    结束遍历childView后,LinearLayout统计自身的流程和不使用weight一样

    mTotalLength先加水顶部和底部的padding,再比较mTotalLength和背景的高度,取大值,再根据heightMeasureSpec确认下自身的高度,这个高度就是LinearLayout最终要在屏幕显示时的高度,再确认在height方向是否还有剩余空间

    由于第2TextView使用了weight,跳过了测量,而LinearLayout自身高度使用match_parent,这时,肯定会有预留了空间,需要再次进行遍历测量

    if()内,条件用的是||,在遍历到第2TextView时,已经将skippedMeasure置为true

    还有一种情况,skippedMeasure == false,但remainingExcess != 0 && totalWeight > 0.0ftrue。这个时候,LinearLayout的高使用wrap_content,而内部的childView使用weight

    int remainingExcess = 
          heightSize - mTotalLength
              + (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
    
     if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {
         // 重置 
         mTotalLength = 0;
         
         // 再次遍历
         for (int i = 0; i < count; ++i) {
         
             ...
             
             if(childWeight){
             
                // 统计并确认使用 weight 的 childView 高度
                ...
             }
             
         }
      }else{
     
        ...
        
     }
    
    

    for(){}内,先查看每个当前的childViewweight是否大于0if (childWeight > 0) { ... }

    在遍历前,mTotalLength被重置为了0


    • 第1个TextView

    1TextView并没有使用weight,不会走if(childWeight){}分支内逻辑

    记录TextView的宽度,保存所有的childView中最大的宽度maxWidth

    final int margin =  lp.leftMargin + lp.rightMargin;
    final int measuredWidth = child.getMeasuredWidth() + margin;
    maxWidth = Math.max(maxWidth, measuredWidth);
    
    

    接着是将TextView的高度累加到mTotalLength

    final int totalLength = mTotalLength;
    mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() 
    +lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
    

    mTotalLength在遍历前被重置为0,根据当前例子,此时mTotalLengthchild.getMeasuredHeight()


    • 第2个TextView

    2TextView使用了weight,会进入if(childWeight){}语句内

    首先,根据weight和剩余空间大小来确定可用的空间高度

    final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
    remainingExcess -= share;
    remainingWeightSum -= childWeight;
    

    remainingExcess是在遍历之前计算出来的剩余空间,remainingWeightSum是所有childViewweight值和

    当前的TextView可用的空间高度,就是share = weight /remainingWeightSum * 剩余总高度

    计算得到share之后,可用空间就要减去share高度,同时remainingWeightSum也要减去当前TextViewweight


    得到高度空间后,根据条件,将share分配给TextView

    final int childHeight;
    if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {
        childHeight = largestChildHeight;
        
    } else if (lp.height == 0 && (!mAllowInconsistentMeasurement
                                || heightMode == MeasureSpec.EXACTLY)) {
        // This child needs to be laid out from scratch using
        // only its share of excess space.
        childHeight = share;
        
    } else {
        // This child had some intrinsic height to which we
        // need to add its share of excess space.
        childHeight = child.getMeasuredHeight() + share;
    }
    

    mUseLargestChild默认值为false,在这个例子中为false,不会进入if( ... ){}分支

    在四个参数的构造方法中

    mAllowInconsistentMeasurement = version <= Build.VERSION_CODES.M;
    

    手机版本是6.0(23)mAllowInconsistentMeasurementtrue,! mAllowInconsistentMeasurement就为false

    heightMode == MeasureSpec.EXACTLYtrue,最终会进入到else if ( ... ){ }
    分支中,进行childHeight = share赋值,之后便结束整个if(){}分支

    进行打包确认childWidthMeasureSpec, childHeightMeasureSpec之后进入View.measure()方法

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec),这里便进入到了TextView,measure()if(childWeight){ ... }便结束

    之后便可以拿到第2TextView的宽高,再次记录maxWidth,再把height累加到mTotalLength

    3TextView的过程便和第1个一样

    3TextView遍历结束后,mTotalLength += mPaddingTop + mPaddingBottom,累加顶部个底部的paddding,结束if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {}else{}

    最后进行setMeasuredDimension( ... ),最终,LinearLayout的宽高便确定


    总结

    LinearLayout使用vertical

    • 自身的height使用match_parent,wrap_content时,在测量阶段都是先对内部childViews遍历一次,拿到累积的高度,及childViews中最大的maxWidth,之后再测量确定自身的高度和宽度

    • childViews有使用weight并设置height = 0dp时,在第一次遍历chidlViews时,LinearLayout会先测量没有使用weightchildView,拿到高度后与根据heightMeasureSpec计算出来的高度作对比计算,可以得到剩余空间高度,再次遍历childViews,再根据weight进行计算出使用weightchildView的高度。拿到所有的childView的宽高信息后,LinearLayout再确定自身的宽高信息

    相关文章

      网友评论

      • qhandroid:可以,再分析一下LinearLayout使用wrap_content并且child使用weight并且height不为0的情况,基本就全了。
        qhandroid:@英勇青铜5 已经很好了:smile:
        英勇青铜5:@qhandroid 写的挺乱的,之前准备面试自己写的笔记

      本文标题:LinearLayout measure流程学习

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