LinearLayout measure流程学习

LinearLayoutViewGroup的子类,ViewGroupView的子类

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

View 层测量起点

LinearLayout measure流程学习_第1张图片
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 measure流程学习_第2张图片
宽高都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内有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再确定自身的宽高信息

你可能感兴趣的:(LinearLayout measure流程学习)