• Android View体系(七)从源码解析View的measure流程


    前言

    在上一篇我们了解了Activity的构成后,开始了解一下View的工作流程,就是measure、layout和draw。measure用来测量View的宽高,layout用来确定View的位置,draw则用来绘制View。这一讲我们来看看measure流程,measure流程分为View的measure流程和ViewGroup的measure流程,只不过ViewGroup的measure流程除了要完成自己的测量还要遍历去调用子元素的measure()方法。

    1.View的measure流程

    先来看看onMeasure()方法(View.java):

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                 getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
     }
    View Code

    看看setMeasuredDimension()方法:

    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
           boolean optical = isLayoutModeOptical(this);
           if (optical != isLayoutModeOptical(mParent)) {
               Insets insets = getOpticalInsets();
               int opticalWidth  = insets.left + insets.right;
               int opticalHeight = insets.top  + insets.bottom;
    
               measuredWidth  += optical ? opticalWidth  : -opticalWidth;
               measuredHeight += optical ? opticalHeight : -opticalHeight;
           }
           setMeasuredDimensionRaw(measuredWidth, measuredHeight);
       }
    View Code

    很显然是用来设置View的宽高的,先来看看getDefaultSize()方法处理了什么:

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
    
        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
    View Code

    specMode是View的测量模式,而specSize是View的测量大小,看到这里我们有必要先看看MeasureSpec类:

    public static class MeasureSpec {
            private static final int MODE_SHIFT = 30;
            private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    
            /**
             * Measure specification mode: The parent has not imposed any constraint
             * on the child. It can be whatever size it wants.
             */
            public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    
            /**
             * Measure specification mode: The parent has determined an exact size
             * for the child. The child is going to be given those bounds regardless
             * of how big it wants to be.
             */
            public static final int EXACTLY     = 1 << MODE_SHIFT;
    
            /**
             * Measure specification mode: The child can be as large as it wants up
             * to the specified size.
             */
            public static final int AT_MOST     = 2 << MODE_SHIFT;
    
    ...
    
     public static int getMode(int measureSpec) {
                return (measureSpec & MODE_MASK);
            }
      public static int getSize(int measureSpec) {
                return (measureSpec & ~MODE_MASK);
            }
    ...        
    }
    View Code

    MeasureSpec类帮助我们来测量View,它是一个32位的int值,高两位为specMode (测量的模式),低30位为specSize (测量的大小),测量模式分为三种:

    • UNSPECIFIED:未指定模式,View想多大就多大,父容器不做限制,一般用于系统内部的测量。
    • AT_MOST:最大模式,对应于wrap_comtent属性,只要尺寸不超过父控件允许的最大尺寸就行。
    • EXACTLY:精确模式,对应于match_parent属性和具体的数值,父容器测量出View所需要的大小,也就是specSize的值。

    让我们回头看看getDefaultSize()方法,很显然在AT_MOST和EXACTLY模式下,都返回specSize这个值,也就是View测量后的大小,而在UNSPECIFIED模式返回的是getDefaultSize()方法的第一次个参数的值,这第一个参数从onMeasure()方法来看是getSuggestedMinimumWidth()方法和getSuggestedMinimumHeight()得到的,那我们来看看getSuggestedMinimumWidth()方法做了什么,我们只需要弄懂getSuggestedMinimumWidth()方法,因为这两个方法原理是一样的:

    protected int getSuggestedMinimumWidth() {
          return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
      }
    View Code

    如果View没有设置背景则取值为mMinWidth,mMinWidth是可以设置的,它对应于android:minWidth这个属性设置的值或者View的setMinimumWidth的值,如果不指定的话则默认为0:

    public void setMinimumWidth(int minWidth) {
        mMinWidth = minWidth;
        requestLayout();
    }
    View Code

    如果View设置了背景在取值为max(mMinWidth, mBackground.getMinimumWidth()),取值mMinWidth和mBackground.getMinimumWidth()的最大值,上面我们说过了mMinWidth,那来看看mBackground.getMinimumWidth(),这个mBackground是Drawable类型的,看一下Drawable类的getMinimumWidth()方法(Drawable.java):

    public int getMinimumWidth() {
           final int intrinsicWidth = getIntrinsicWidth();
           return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }
    View Code

    intrinsicWidth得到的是这个Drawable的固有的宽度,如果固有宽度大于0则返回固有宽度,否则返回0。
    总结一下getSuggestedMinimumWidth()方法就是:如果View没有设置背景则返回mMinWidth ,如果设置了背景就返回mMinWidth 和Drawable最小宽度两个值的最大值。

    2.ViewGroup的measure流程

    ViewGroup的measure原理

    讲完了View的measure流程,接下来看看ViewGroup的measure流程,对于ViewGroup,它不只要measure自己本身,还要遍历的调用子元素的measure()方法,ViewGroup中没有定义onMeasure()方法,但他定义了measureChildren()方法(ViewGroup.java):

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
           final int size = mChildrenCount;
           final View[] children = mChildren;
           for (int i = 0; i < size; ++i) {
               final View child = children[i];
               if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                   measureChild(child, widthMeasureSpec, heightMeasureSpec);
               }
           }
       }
    View Code

    就是遍历子元素并调用measureChild()方法:

    protected void measureChild(View child, int parentWidthMeasureSpec,
               int parentHeightMeasureSpec) {
           final LayoutParams lp = child.getLayoutParams();
    
           final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                   mPaddingLeft + mPaddingRight, lp.width);
           final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                   mPaddingTop + mPaddingBottom, lp.height);
    
           child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
       }
    View Code

    调用child.getLayoutParams()方法来获得子元素的LayoutParams属性,并获取到子元素的MeasureSpec并调用子元素的measure()方法进行测量。getChildMeasureSpec()方法里写了什么呢?

    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 = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
    View Code

    很显然这是根据父容器的MeasureSpec的模式再结合子元素的LayoutParams属性来得出子元素的MeasureSpec属性,有一点需要注意的是如果父容器的MeasureSpec属性为AT_MOST,子元素的LayoutParams属性为WRAP_CONTENT,那根据代码我们会发现子元素的MeasureSpec属性也为AT_MOST,它的specSize值为父容器的specSize减去padding的值,也就是说跟这个子元素设置LayoutParams属性为MATCH_PARENT效果是一样的,为了解决这个问题需要在LayoutParams属性为WRAP_CONTENT时指定一下默认的宽和高。

    LinearLayout的measure流程

    ViewGroup并没有提供onMeasure()方法,而是让其子类来各自实现测量的方法,究其原因就是ViewGroup有不同的布局的需要很难统一,接下来我们来简单分析一下ViewGroup的子类LinearLayout的measure流程,先来看看它的onMeasure()方法(LinearLayout.java):

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

    来看看垂直measureVertical()方法的部分源码:

    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
            mTotalLength = 0;
         mTotalLength = 0;       
     ...
      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;
                }
    
                if (hasDividerBeforeChildAt(i)) {
                    mTotalLength += mDividerHeight;
                }
    
                LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
    
                totalWeight += lp.weight;
                
                if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
                    // Optimization: don't bother measuring children who are going to use
                    // leftover space. These views will get measured again down below if
                    // there is any leftover space.
                    final int totalLength = mTotalLength;
                    mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                    skippedMeasure = true;
                } else {
                    int oldHeight = Integer.MIN_VALUE;
    
                    if (lp.height == 0 && lp.weight > 0) {
                        // heightMode is either UNSPECIFIED or AT_MOST, and this
                        // child wanted to stretch to fill available space.
                        // Translate that to WRAP_CONTENT so that it does not end up
                        // with a height of 0
                        oldHeight = 0;
                        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).
                    measureChildBeforeLayout(
                           child, i, widthMeasureSpec, 0, heightMeasureSpec,
                           totalWeight == 0 ? mTotalLength : 0);
    
                    if (oldHeight != Integer.MIN_VALUE) {
                       lp.height = oldHeight;
                    }
    
                    final int childHeight = child.getMeasuredHeight();
                    final int totalLength = mTotalLength;
                    mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                           lp.bottomMargin + getNextLocationOffset(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();
                    // 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
    View Code

    定义了mTotalLength用来存储LinearLayout在垂直方向的高度,然后遍历子元素,根据子元素的MeasureSpec模式分别计算每个子元素的高度,如果是wrap_content则将每个子元素的高度和margin垂直高度等值相加并赋值给mTotalLength得出整个LinearLayout的高度。如果布局高度设置为match_parent者具体数值则和View的测量方法一样。

  • 相关阅读:
    十步完全理解 SQL
    Oracle VM Virtual
    Pycharm 使用
    Open Yale course:Listening to Music
    SQL 必知必会
    安装 SQL server 2008 R2
    Ubuntu安装mysql之后,编译找不到头文件
    core dump文件的生成
    Linux静态库与动态库制作过程
    GEC6818连接Ubuntu,下载程序至开发板
  • 原文地址:https://www.cnblogs.com/ganchuanpu/p/6035866.html
Copyright © 2020-2023  润新知