zoukankan      html  css  js  c++  java
  • View绘制详解(三),扒一扒View的测量过程

    所有东西都是难者不会,会者不难,Android开发中有很多小伙伴觉得自定义View和事件分发或者Binder机制等是难点,其实不然,如果静下心来花点时间把这几个技术点都研究一遍,你会发现其实这些东西都很简单。OK,废话不多说,今天我们就来看看View的测量。View的测量纷繁复杂,不过如果能够做到提纲挈领,其实也不难。那么今天,我们就来扒一扒View的测量。本文主要涉及如下知识点:

    1.View的测量

    2.在父容器中对View进行测量

    3.LinearLayout测量举例

    4.最根上容器测量


    如果小伙伴们还没看过我之前关于View绘制的文章的话,请先移步这里,这两篇文章有助于你理解本篇文章:

    1.View绘制详解,从LayoutInflater谈起

    2.View绘制详解(二),从setContentView谈起 


    OK,那我们开始今天的话题吧。

    1.View的测量

    关于View的测量我其实在之前的一篇文章中已经提到过了(Android自定义View之ProgressBar出场记 ),在我们自定义View的时候,除了一个onDraw方法可以重写之外,还有一个onMeasure方法也可以重写,这个onMeasure方法就是用来确定一个View的宽和高的,onMeasure方法的方法头如下:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 

    大家看到,onMeasure方法有两个参数,第一个参数叫做widthMeasureSpec、第二个参数叫做heightMeasureSpec,在开发中我们把这两个参数称作测量规格。测量规格是一个32位的int型数据,其中高2位表示测量模式,低30位表示测量值,测量模式一共分为三种:

    1.EXACTLY:精确模式,对应我们在布局文件中设置宽高时给一个具体值或者match_parent

    2.AT_MOST:最大值模式:对应设置宽高时给一个wrap_content

    3.UNSPECIFIED:这种测量模式多用在ScrollView中,或者系统内部调用

    在实际开发过程中,我们一般通过MeasureSpec.getMode()方法来从测量规格中获取测量模式,然后通过MeasureSpec.getSize()方法来从测量规格中获取测量值。但是小伙伴们注意,这个时候获取到的测量值实际上是系统建议的测量值,并不是控件最终显示的大小,在onMeasure方法中我们可以根据自己的需求再对这些值做进一步的修正,修正完之后再调用setMeasuredDimension()方法,调用完该方法之后View才算是有了MeasureWidth和MeasureHeight了。OK,基于以上的表述,我们在自定义View中onMeasure方法的典型写法可以是如下样子:

    @Override  
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
        //获取宽的测量模式  
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);  
        //获取宽的测量值  
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);  
        //获取高的测量模式  
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);  
        //获取高的测量值  
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);  
        switch (widthMode) {  
            case MeasureSpec.EXACTLY:  
                break;  
            case MeasureSpec.AT_MOST:  
            case MeasureSpec.UNSPECIFIED:  
                //如果宽为wrap_content,则给定一个默认值  
                widthSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULTWIDTH, getResources().getDisplayMetrics());  
                break;  
        }  
        switch (heightMode) {  
            case MeasureSpec.EXACTLY:  
                break;  
            case MeasureSpec.AT_MOST:  
            case MeasureSpec.UNSPECIFIED:  
                heightSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULTHEIGHT, getResources().getDisplayMetrics());  
                break;  
        }  
        widthSize = heightSize = Math.min(widthSize, heightSize);  
        //设置测量结果  
        setMeasuredDimension(widthSize, heightSize);  
    }

    2.在父容器中对View进行测量

    看完了View的测量之后,很多小伙伴可能都会有疑问了,那么View的onMeasure方法到底是在哪里调用的呢(小伙伴们注意,我这里的View既包括普通控件,也包括容器)?其实就是在它的父容器中调用。那么这里就要我们来到ViewGroup中探究一番了,首先,在ViewGroup中,系统给我们提供了三个方法用来测量ViewGroup中子控件的大小,如下:

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec)
    protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) 


    第一个方法是measureChildren,Children是child的复数形式,很明显,这个是测量所有子控件的,这个方法中是一个for循环,遍历了容器中所有的子控件进行测量,第二个方法measureChild则是测量单个子控件,最后一个measureChildWidthMargins也是测量单个子控件,不过在测量的时候加入margin而已。OK,那我们这里就以measureChildWithMargins为例,来看看父容器到底是怎么样来测量子控件的:

    protected void measureChildWithMargins(View child,
                int parentWidthMeasureSpec, int widthUsed,
                int parentHeightMeasureSpec, int heightUsed) {
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    
            final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                    mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                            + widthUsed, lp.width);
            final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                    mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                            + heightUsed, lp.height);
    
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }


    首先在第四行获取到每一个子View的LayoutParams,然后在第6行通过getChildMeasureSpec方法获取一个childWidthMeasureSpec,高度的测量规格获取方式和宽度测量规格的获取方式一致,所以这里我就以宽度的测量规格获取方式为例,我们来看看系统是如何测来子控件的:

        public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
            int specMode = MeasureSpec.getMode(spec);
            int specSize = MeasureSpec.getSize(spec);
    
            int size = Math.max(0, specSize - padding);
    
            int resultSize = 0;
            int resultMode = 0;
    
            switch (specMode) {
            // Parent has imposed an exact size on us
            case MeasureSpec.EXACTLY:
                if (childDimension >= 0) {
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size. So be it.
                    resultSize = size;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size. It can't be
                    // bigger than us.
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
    
            // Parent has imposed a maximum size on us
            case MeasureSpec.AT_MOST:
                if (childDimension >= 0) {
                    // Child wants a specific size... so be it
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size, but our size is not fixed.
                    // Constrain child to not be bigger than us.
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size. It can't be
                    // bigger than us.
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
    
            // Parent asked to see how big we want to be
            case MeasureSpec.UNSPECIFIED:
                if (childDimension >= 0) {
                    // Child wants a specific size... let him have it
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size... find out how big it should
                    // be
                    resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                    resultMode = MeasureSpec.UNSPECIFIED;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size.... find out how
                    // big it should be
                    resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                    resultMode = MeasureSpec.UNSPECIFIED;
                }
                break;
            }
            //noinspection ResourceType
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
        }


    getChildMeasureSpec方法共接收三个参数,第一个参数表示父容器的测量规格,第二个参数表示子控件的padding,margin等,第三个参数表示子控件期望显示的宽高。在getChildMeasureSpec方法中,首先获取父容器宽度的测量模式和测量值,然后定义变量size,size是一个大于等于0的数,specSIze-padding为父容器宽度减去子控件的padding、margin之后所剩的大小。然后从第10行开始,通过一个switch来分别处理不同的情况。这里的逻辑都很简单,我以第一个case为例,如果父容器的测量模式是精确测量的话,childDimension分三种情况,如果childDimension>=0的话,即子控件的宽度为具体值(因为MATCH_PARENT对应的常量为-1,WRAP_CONTENT对应的常量为-2),则resultSize=childDimension,resultMode=EXACTLY。如果子控件宽度设置为MATCH_PARENT,则resultSize的大小为父容器的宽度的测量值减去子控件padding,margin等,此时resultMode依然是EXACTLY,如果子控件的宽度设置为了wrap_content,那么resultSize的大小依然是父容器中可容纳控件的最大大小,表示子控件最大宽度不能超过父容器可显示的宽度,只不过在这里把resultMode设置为了AT_MOST,OK,做完这些事情之后,最后一个通过一个makeMeasureSpec方法将resultSize和resultMode再整合成一个MeasureSpec。这就是为什么很多人嘴里经常念叨控件的具体大小并不是由控件本身决定,还包括它的父容器的原因。OK,针对以上三种case,我整理了如下一张表格:

    OK ,现在再回到我们ViewGroup的measureChildWithMargins方法中,在该方法中获取到子控件的测量规格之后,接下来调用child.measure(childWidthMeasureSpec, childHeightMeasureSpec);这一行代码,进入到View的measure方法中进行测量,我们点到这个方法里边来看看:

        public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
            boolean optical = isLayoutModeOptical(this);
            if (optical != isLayoutModeOptical(mParent)) {
                Insets insets = getOpticalInsets();
                int oWidth  = insets.left + insets.right;
                int oHeight = insets.top  + insets.bottom;
                widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
                heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
            }
    
            // Suppress sign extension for the low bytes
            long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
            if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
    
            final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
    
            // Optimize layout by avoiding an extra EXACTLY pass when the view is
            // already measured as the correct size. In API 23 and below, this
            // extra pass is required to make LinearLayout re-distribute weight.
            final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                    || heightMeasureSpec != mOldHeightMeasureSpec;
            final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                    && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
            final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                    && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
            final boolean needsLayout = specChanged
                    && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
    
            if (forceLayout || needsLayout) {
                // first clears the measured dimension flag
                mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
    
                resolveRtlPropertiesIfNeeded();
    
                int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
                if (cacheIndex < 0 || sIgnoreMeasureCache) {
                    // measure ourselves, this should set the measured dimension flag back
                    onMeasure(widthMeasureSpec, heightMeasureSpec);
                    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
                } else {
                    long value = mMeasureCache.valueAt(cacheIndex);
                    // Casting a long to int drops the high 32 bits, no mask needed
                    setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                    mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
                }
    
                // flag not set, setMeasuredDimension() was not invoked, we raise
                // an exception to warn the developer
                if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                    throw new IllegalStateException("View with id " + getId() + ": "
                            + getClass().getName() + "#onMeasure() did not set the"
                            + " measured dimension by calling"
                            + " setMeasuredDimension()");
                }
    
                mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
            }
    
            mOldWidthMeasureSpec = widthMeasureSpec;
            mOldHeightMeasureSpec = heightMeasureSpec;
    
            mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                    (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
        }

    首先小伙伴们注意到,这个方法是一个final类型的,也就是不可以被复写,然后我们在measure方法的第38行见到了一个久违的名字,onMeasure方法。这下终于回到了本文的第一小节了,这下小伙伴们也知道了View的onMeasure方法是在哪里调用了吧!就是在它的父容器中调用。

    3.LinearLayout测量举例

     OK,上文我们已经说过,View的测量其实是很简单的,难点在于ViewGroup的测量。其实这里不该用难点这个词,用麻烦可能更合适,因为并不难,只是麻烦而已。一般来说,我们在自定义ViewGroup的时候,控件宽高的测量都是比较费事的。一般来说,思路是这样的:如果用户已经指定了ViewGroup的宽高为固定值或者为MATCH_PARENT,那我们不用做过多处理,如果用户指定了ViewGroup的宽或者高为WRAP_CONTENT,WRAP_CONTENT表示ViewGroup的宽高是包裹内容,即容器中控件的宽高为多少,容器的宽高就为多少,这就需要我们先来遍历一遍容器中控件的宽高,算出来子控件整体的宽高在设置给容器就可以了,整体上思路就是这样,接下来我们就以LinearLayout的源码为例,来看看ViewGroup的宽高是如何测量的。一般来说,我们的所有容器都会复写onMeasure'方法进行控件宽高的重新测量,我们先来看看LinearLayout的onMeasure方法:

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

    这里是根据控件的排列方向来测量的,OK,那这里我们就以竖直方向上的测量为例:

        void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
            //记录总高度
            mTotalLength = 0;
            //记录每行总宽度
            int maxWidth = 0;
            int childState = 0;
            int alternativeMaxWidth = 0;
            int weightedMaxWidth = 0;
            boolean allFillParent = true;
              //记录总权重
            float totalWeight = 0;
    
              //获取子控件总数
            final int count = getVirtualChildCount();
              //获取宽度的测量模式
            final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
              //获取高度的测量模式
            final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    
            boolean matchWidth = false;
            boolean skippedMeasure = false;
    
            final int baselineChildIndex = mBaselineAlignedChildIndex;       
            final boolean useLargestChild = mUseLargestChild;
    
             //记录每一行最高子控件的高度
            int largestChildHeight = Integer.MIN_VALUE;
            int consumedExcessSpace = 0;
    
             //这个for循环用来记录总高度,同时记录最大宽度
            // See how tall everyone is. Also remember max width.
            for (int i = 0; i < count; ++i) {
              //获取第一个子View
                final View child = getVirtualChildAt(i);
                if (child == null) {
                   //measureNullChild方法的返回值为0
                    mTotalLength += measureNullChild(i);
                    continue;
                }
              //如果控件被隐藏,则不计入计算
                if (child.getVisibility() == View.GONE) {
                   i += getChildrenSkipCount(child, i);
                   continue;
                }
              
                if (hasDividerBeforeChildAt(i)) {
                    mTotalLength += mDividerHeight;
                }
              //获取子控件的lp
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
              //记录总权重
                totalWeight += lp.weight;
              //是否分配剩余空间
                final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
                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.
                   //将mTotalLength的值暂时赋值给totalLength
                    final int totalLength = mTotalLength;
                   //重新给mTotalLength赋值,这次加上上下margin,并获取最大值
                    mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                   //跳过测量(后面用到)
                    skippedMeasure = true;
                } else {
                   //如果子控件的高度为不为具体数值或者MATCH_PARENT
                    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.
                        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).
                   //如果目前还没有任何设置了权重的子控件,则在子控件测量时去除已经分配的父容器的空间
                    final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                   //子控件的测量,实际上调用了measureChildWithMargins方法
                    measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                            heightMeasureSpec, usedHeight);
    
                   //获取子控件的高度
                    final int childHeight = child.getMeasuredHeight();
                    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;
                        //记录非0dp或者非MATCH_PARENTA的控件 的总高度
                        consumedExcessSpace += childHeight;
                    }
    
                    final int totalLength = mTotalLength;
                   //重新计算当前总高度
                    mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                           lp.bottomMargin + getNextLocationOffset(child));
    
                    if (useLargestChild) {
                        largestChildHeight = Math.max(childHeight, largestChildHeight);
                    }
                }
    
                /**
                 * If applicable, compute the additional offset to the child's baseline
                 * we'll need later when asked {@link #getBaseline}.
                 */
                if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
                   mBaselineChildTop = mTotalLength;
                }
    
                // if we are trying to use a child index for our baseline, the above
                // book keeping only works if there are no children above it with
                // weight.  fail fast to aid the developer.
                if (i < baselineChildIndex && lp.weight > 0) {
                    throw new RuntimeException("A child of LinearLayout with index "
                            + "less than mBaselineAlignedChildIndex has weight > 0, which "
                            + "won't work.  Either remove the weight, or don't set "
                            + "mBaselineAlignedChildIndex.");
                }
    
                boolean matchWidthLocally = false;
                if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
                    // The width of the linear layout will scale, and at least one
                    // child said it wanted to match our width. Set a flag
                    // indicating that we need to remeasure at least that view when
                    // we know our width.
                    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) {
                    /*
                     * 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);
                }
    
                i += getChildrenSkipCount(child, i);
            }
    
            if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {
                mTotalLength += mDividerHeight;
            }
    
         //useLargestChild对应着xml文件中的measureWithLargestChild属性,默认为false
            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();
                    // Account for negative margins
                    final int totalLength = mTotalLength;
                    mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
                            lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
                }
            }
    
            // Add in our padding
            mTotalLength += mPaddingTop + mPaddingBottom;
    
            int heightSize = mTotalLength;
    
            // Check against our minimum height
            heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
    
            // Reconcile our calculated size with the heightMeasureSpec
            int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
            heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
    
            // Either expand children with weight to take up available space or
            // shrink them if they extend beyond our current bounds. If we skipped
            // measurement on any children, we need to measure them now.
              //remainingExcess表示当前LinearLayout剩余空间
            int remainingExcess = heightSize - mTotalLength
                    + (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
              //如果还有剩余空间需要再分配
            if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {
                   //remainingWeightSum表示总权重
                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) {
                        final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
                        remainingExcess -= share;
                        remainingWeightSum -= childWeight;
    
                        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;
                        }
                        //构造子控件的测量规格
                        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));
                    }
                   //获取控件的左右margin
                    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));
                        }
                    }
                }
            }
    
            if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
                maxWidth = alternativeMaxWidth;
            }
    
            maxWidth += mPaddingLeft + mPaddingRight;
    
            // Check against our minimum width
            maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
              //设置测量结果
            setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                    heightSizeAndState);
    
            if (matchWidth) {
                forceUniformWidth(count, heightMeasureSpec);
            }
        }
    

    OK,关键地方我已经在代码中注释了,整体思路就是先遍历一遍所有的子控件,测量出子控件的高度,这是第一步,第二步则将容器中剩余的空间根据权重再分配,整体思路就是这样,当然这里还有很多条条框框,我都已在注释中写明。

    4.最根上的容器测量

    OK,说到这里,相信小伙伴们对View的测量已经有了一个大体的认识了,一个View能够显示出来,它的大小要依靠它的父容器和它自己共同来决定,比如下面一段代码:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        <RelativeLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content" />
        </RelativeLayout>
    </LinearLayout>

    RelativeLayout的LayoutParams一方面它和LinearLayout的LayoutParams共同决定了RelativeLayout的大小,另一方面,它也和TextView的LayoutParams共同决定了TextView的大小。那么有小伙伴们可能会有疑问了,那么LinearLayout的大小又由谁决定呢?它的父容器又是谁呢?读过View绘制详解,从LayoutInflater谈起View绘制详解(二),从setContentView谈起 这两篇博客的小伙们应该因该知道,对于一个页面而言,最顶层的View是DecorView,所以针对本案例中LinearLayout的测量方式其实和普通容器的测量方式是一致的。不必赘述,问题是总会有一个View没有父容器的,那么这个View的宽高又是如何测量的呢?要弄清楚这个问题,我们首先需要明白View系统启动measure是从ViewRootImpl的performMeasure方法开始的,而performMeasure方法则是在performTraversals中调用的,关于performTraversals方法我贴出一部分源码如下(ViewRootImpl.java):

    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    
    if (DEBUG_LAYOUT) Log.v(mTag, "Ooops, something changed!  mWidth="
            + mWidth + " measuredWidth=" + host.getMeasuredWidth()
            + " mHeight=" + mHeight
            + " measuredHeight=" + host.getMeasuredHeight()
            + " framesChanged=" + framesChanged);
    
     // Ask host how big it wants to be
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    performTraversals方法原本太长了,我这里贴出其中一部分。小伙伴们注意,在这里系统调用了performMeasure方法进行控件的测量工作,测量的时候传递了两个参数,一个是childWidthMeasureSpec,另一个是childHeightMeasureSpec。这两个参数都是通过一个叫做getRootMeasureSpec方法获得到的,该方法接收两个参数,第一个参数mWidth/mHeight表示窗口期望显示的大小,在这里实际上就是手机屏幕的大小,第二个参数lp.width/lp.height实际都是MATCH_PARENT,这个从它们初始化的地方就能看出端倪。OK,那我们就来看看这个方法getRootMeasureSpec:

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
            int measureSpec;
            switch (rootDimension) {
    
            case ViewGroup.LayoutParams.MATCH_PARENT:
                // Window can't resize. Force root view to be windowSize.
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
                break;
            case ViewGroup.LayoutParams.WRAP_CONTENT:
                // Window can resize. Set max size for root view.
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
                break;
            default:
                // Window wants to be an exact size. Force root view to be that size.
                measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
                break;
            }
            return measureSpec;
        }

    这个方法很简单,根据传进来的windowSize和rootDimension,然后通过MeasureSpec.makeMeasureSpec方法将这两个数据组合成一个int类型数据。通过这个方式获取到最根上的widthMeasureSpec和heightMeasureSpec之后接下来就可以调用performMeasure方法来测量了,来看看performMeasure方法:

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
            try {
                mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
        }

    小伙伴们看到,从这里又进入到了measure方法中,后面的流程就和我们前面所述的一样了,这里我就不再赘述。

    OK,以上就是View测量过程的一个简单分析,有问题欢迎留言讨论。


    以上。




  • 相关阅读:
    在Xbox和Hololens 上部署、调试UWP App
    淘宝UWP中的100个为什么
    小娜追踪快递
    页面与ViewModel(下)
    页面与ViewModel(上)
    wxpython缩放图片
    java中Swing的GridBagLayout使用简介
    java中Swing组件设置容器随着窗体变化而自适应
    java中swing组件设置icon自适应按钮大小
    nginx开启网站目录浏览功能
  • 原文地址:https://www.cnblogs.com/qitian1/p/6461663.html
Copyright © 2011-2022 走看看