Android 进阶之旅

Android 进阶学习(二) LinearLayout测量过程

2020-11-06  本文已影响0人  Tsm_2020

想必大家在面试的过程中都遇到过一些关于LinearLayout 和RelativeLayout 关于测量的问题,下面我就从源码的角度来分析这个过程,其实这个过程自己看源码并读懂他的原理对自己的提升还是很大的,不管是从知识储备还是激发自己的学习兴趣方面.
下面上源码

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

整个LinearLayout的测量过程分为水平和垂直两个方向,他们两个测量的过程是差不多的,我们只需要分析一个就可以了,我们来分析一下垂直方向上的

measureVertical

我们先来说一下LinearLayout的Child 的高度包含哪几个部分

image.png

我们再来说一说整个测量过程,这样在梳理整个测量的过程中便于理解

在整个测量过程中根据LinearLayout 的heightMode 分为了2中情况,

1. heightMode 为EXACTLY

即 LinearLayout 的高度是确定的,你写死了height 是多少,或者 你给定的是match_parent fill_parent,在计算过程中先不计算height=0 并且weight >0 的child ,其他都计算,并找到高度最大的child,在第二次计算的过程中,已经计算的child的高度已经知道了,根据剩余高度计算weight >0 的child 可以分到的高度,如果使用前面的最大值,则使用第一次计算的最大值,不使用分配的高度,,如果height=0 则child 的高度就是就是根据weight 分到的高度,如果heighti >0 并且weight >0 ,由于在第一次计算的过程中已经计算了他的高度,本次计算会将第一次计算的高度加上本次计算分到的高度加到一起,即为child 的最后高度,这就是在使用Android Studio 的时候,如果你使用了weight>0 ,但是还给定了height!=0,这样他就会提示你,这样做会浪费性能,原因是他会被测量2次,他的child都是测量一次的,虽然执行了2次计算,但是测量只进行了一次,

2.heightMode 为MeasureSpec.AT_MOST 或者 UNSPECIFIED

即高度不确定, warp_content ,在第一次计算过程中,会计算所有child的高度,但是如果child 的weight >0 并且height=0 ,他会按照warp_content 来计算child,找到所有child的最大高度,第二次计算过程中如果使用了前面计算的最大值,那么需要重新将weight>0 的child 的高度重新计算,修改为 第一次计算的最大值,其他的则不变

我把整个计算过程分为3步,

第一步:

mTotalLength作为整个LinearLayout的高度,先叠加child的Divider的高度,
如果child heightMode == MeasureSpec.EXACTLY (精确地高度或者FILL_PARENT)&&
( boolean useExcessSpace = LayoutParams.height == 0 && LayoutParams.weight > 0)(高度为0并且weight>0,即weight 属性生效),
那么他的高度先不做计算,只添加的他的marginTop和marginBottom 的高度,

否则先调用measureChildBeforeLayout 先测量child的真正的高度,为什么说是真正的高度呢,那就是在测量之前,如果child的useExcessSpace 为true ,那么将child 的LayoutParams的height 设置为LayoutParams.WRAP_CONTENT,因为他可能是UNSPECIFIED 或者 AT_MOST
在测量完成后,如果useExcessSpace 为true ,将它的实际高度统计起来,可能也会和上一个child的高度比较取最大值给largestChildHeight ,并还原child 的 height 为0, 在还原高度之后继续计算mTotalLength,也就是 此时mTotalLength增加的是Child的Divider marginTop marginBottom,还要加上Child的高度,

从这这里我们可以猜想,如果LinearLayout作为xml 的根view,他的高度是match_parent , 其中存在多个view,如果其中一个view 没有使用weight 并且高度设置为match_parent ,那么其他使用了weight 的控件就没有了可分配的空间

      for (int i = 0; i < count; ++i) {
          final View child = getVirtualChildAt(i);
          if (child == null) {
              mTotalLength += measureNullChild(i);
              continue;
          }
          if (child.getVisibility() == View.GONE) {
             i += getChildrenSkipCount(child, i);
             continue;
          }
          nonSkippedChildCount++;
          if (hasDividerBeforeChildAt(i)) {///如果有Divider 则加上Divider 的高度
              mTotalLength += mDividerHeight;
          }
          final LayoutParams lp = (LayoutParams) child.getLayoutParams();
          totalWeight += lp.weight;
          //// weight 属性生效的条件  height=0 并且weight>0
          final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
          ///当前LinearLayout的高度为精确值,并且child 的weight 属性生效
          if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
              final int totalLength = mTotalLength;
              mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
              skippedMeasure = true;
          } 
          else ////LinearLayout 的高度为WARP_CONTENT 或者child的weight >0 并且height>0
         {
              ///如果weight 属性生效,则测量child 的真实高度
               if (useExcessSpace) {
                  lp.height = LayoutParams.WRAP_CONTENT;
              }
              final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
              measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, usedHeight);
                ///如果weight 属性生效,将child的高度还原为0
              final int childHeight = child.getMeasuredHeight();
              if (useExcessSpace) {
                  lp.height = 0;
                  consumedExcessSpace += childHeight;
              }
              ///最后计算高度,如果weight 属性生效,
             /// 此时mTotalLength增加的是Child的Divider  marginTop  marginBottom,还要加上Child的高度,
              final int totalLength = mTotalLength;
              mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                     lp.bottomMargin + getNextLocationOffset(child));
              if (useLargestChild) {
                  largestChildHeight = Math.max(childHeight, largestChildHeight);
              }
          }
          if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
             mBaselineChildTop = mTotalLength;
          }
          if (i < baselineChildIndex && lp.weight > 0) {
              throw new RuntimeException("");
          }
          boolean matchWidthLocally = false;
          /// 没有精确的宽度,并且宽度是MATCH_PARENT
          if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
              matchWidth = true;
              matchWidthLocally = true;
          }
          final int margin = lp.leftMargin + lp.rightMargin;
          final int measuredWidth = child.getMeasuredWidth() + margin;
          maxWidth = Math.max(maxWidth, measuredWidth);
          childState = combineMeasuredStates(childState, child.getMeasuredState());
          allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
          ///计算宽度
          if (lp.weight > 0) {
              weightedMaxWidth = Math.max(weightedMaxWidth,
                      matchWidthLocally ? margin : measuredWidth);
          } else {
              alternativeMaxWidth = Math.max(alternativeMaxWidth,
                      matchWidthLocally ? margin : measuredWidth);
          }

          i += getChildrenSkipCount(child, i);
      }

第二步:

第二步其实是一个特定的使用场景,那就是使用了LinearLayout的measureWithLargestChild 属性,并且LinearLayout 的高度有最大值或者未指定高度,在这种情况下,重新计算高度,注意并不是测量,但是所有child的高度使用第一步中计算出来的那个最大的高度

        if (useLargestChild &&(heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
            mTotalLength = 0;///重新计算高度
            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);
                if (child == null) {
                    mTotalLength += measureNullChild(i);
                    continue;
                }
                if (child.getVisibility() == GONE) {
                    i += getChildrenSkipCount(child, i);
                    continue;
                }
                final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)child.getLayoutParams();
                final int totalLength = mTotalLength;
                ///所有child 的高度使用第一步计算出来的那个最大值
                mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }
        }

第三步:

第三部其实分为两种情况,else 里面是heightMode是UNSPECIFIED 或者 AT_MOST 使用了最大child 的高度并且weight>0的child 重新测量一遍就是我们上面总结的第二点,至于if里面,就是我们前面总结的第一点 heightMode是 EXACTLY ,并且weight>0 的child会触发重新测量,需要根据weight 属性对child 的高度进行缩放

      if (skippedMeasure || ((sRemeasureWeightedChildren || remainingExcess != 0) && totalWeight > 0.0f)) {
            float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
            mTotalLength = 0;
            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);
                if (child == null || child.getVisibility() == View.GONE) {
                    continue;
                }
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                final float childWeight = lp.weight;
                if (childWeight > 0) {///如果weight >0 根据weight 占比计算高度
                    final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
                    remainingExcess -= share;
                    remainingWeightSum -= childWeight;
                    final int childHeight;
                    if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {///如果使用最大child的高度,则child的高度为最大的那个
                        childHeight = largestChildHeight;
                    } else if (lp.height == 0 && (!mAllowInconsistentMeasurement
                            || heightMode == MeasureSpec.EXACTLY)) {///如果高度为0,并且 指定了heightMode为 EXACTLY,则高度就是剩余控件的比例
                        childHeight = share;
                    } else {///其他的情况则是自身的高度加上 所分配的剩余空间
                        childHeight = child.getMeasuredHeight() + share;
                    }
                  ////下面就是计算高度的过程
                    final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            Math.max(0, childHeight), MeasureSpec.EXACTLY);
                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
                            lp.width);
                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

                    // Child may now not fit in vertical dimension.
                    childState = combineMeasuredStates(childState, child.getMeasuredState()
                            & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
                }
                ////计算宽度
                final int margin =  lp.leftMargin + lp.rightMargin;
                final int measuredWidth = child.getMeasuredWidth() + margin;
                maxWidth = Math.max(maxWidth, measuredWidth);

                boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
                        lp.width == LayoutParams.MATCH_PARENT;

                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);

                allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
                ///累计高度
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }

            // Add in our padding
            mTotalLength += mPaddingTop + mPaddingBottom;
            // TODO: Should we recompute the heightSpec based on the new total length?
        } else
            {
            alternativeMaxWidth = Math.max(alternativeMaxWidth,
                                           weightedMaxWidth);


            // We have no limit, so make all weighted views as tall as the largest child.
            // Children will have already been measured once.
            if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
                for (int i = 0; i < count; i++) {
                    final View child = getVirtualChildAt(i);
                    if (child == null || child.getVisibility() == View.GONE) {
                        continue;
                    }

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

                    float childExtra = lp.weight;
                    if (childExtra > 0) {
                        child.measure(
                                MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
                                        MeasureSpec.EXACTLY),
                                MeasureSpec.makeMeasureSpec(largestChildHeight,
                                        MeasureSpec.EXACTLY));
                    }
                }
            }
        }

看完了整个测量过程,在LinearLayout 的测量过程,整理一下什么情况下LinearLayout 会测量2次,

1.heightMode 为 EXACTLY 并且使用已测量的最大高度的控件的高度, 而child height>0 weight>0 ,此时会计算2次,

2. heightModel 为UNSPECIFIED 或者 AT_MOST,并且使用已测量的最大高度的控件的高度, 并且weight>0 的child 会被测量2次

上一篇 下一篇

猜你喜欢

热点阅读