zoukankan      html  css  js  c++  java
  • 【转】ANDROID自定义视图——onMeasure,MeasureSpec源码 流程 思路详解

    原文地址:http://blog.csdn.net/a396901990/article/details/36475213

    简介:

    在自定义view的时候,其实很简单,只需要知道3步骤:

    1.测量——onMeasure():决定View的大小

    2.布局——onLayout():决定View在ViewGroup中的位置

    3.绘制——onDraw():如何绘制这个View。

    而第3步的onDraw系统已经封装的很好了,基本不用我们来操心,只需要专注到1,2两个步骤就中好了。

    而这篇文章就来谈谈第一步,也是十分关键得一步:“测量(Measure)

    Measure():

    Measure的中文意思就是测量。所以它的作用就是测量View的大小。

    而决定View的大小只需要两个值:宽详细测量值(widthMeasureSpec)和高详细测量值(heightMeasureSpec)。也可以把详细测量值理解为视图View想要的大小说明(想要的未必就是最终大小)。

    对于详细测量值(measureSpec)需要两样东西来确定它,那就是大小(size)和模式(mode)。measureSpec,size,mode他们三个的关系,都封装在View类中的一个内部类里,名叫MeasureSpec


    MeasureSpec:

    因为MeasureSpec类很小,而且设计的很巧妙,所以我贴出了全部的源码并进行了详细的标注。(掌握MeasureSpec的机制后会对整个Measure方法有更深刻的理解。)

        /** 
         * MeasureSpec封装了父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求 
         * MeasureSpec由size和mode组成。 
         * 三种Mode: 
         * 1.UNSPECIFIED 
         * 父不没有对子施加任何约束,子可以是任意大小(也就是未指定) 
         * (UNSPECIFIED在源码中的处理和EXACTLY一样。当View的宽高值设置为0的时候或者没有设置宽高时,模式为UNSPECIFIED 
         * 2.EXACTLY 
         * 父决定子的确切大小,子被限定在给定的边界里,忽略本身想要的大小。 
         * (当设置width或height为match_parent时,模式为EXACTLY,因为子view会占据剩余容器的空间,所以它大小是确定的) 
         * 3.AT_MOST 
         * 子最大可以达到的指定大小 
         * (当设置为wrap_content时,模式为AT_MOST, 表示子view的大小最多是多少,这样子view会根据这个上限来设置自己的尺寸) 
         *  
         * MeasureSpecs使用了二进制去减少对象的分配。 
         */  
        public class MeasureSpec {  
                // 进位大小为2的30次方(int的大小为32位,所以进位30位就是要使用int的最高位和倒数第二位也就是32和31位做标志位)  
                private static final int MODE_SHIFT = 30;  
                  
                // 运算遮罩,0x3为16进制,10进制为3,二进制为11。3向左进位30,就是11 00000000000(11后跟30个0)  
                // (遮罩的作用是用1标注需要的值,0标注不要的值。因为1与任何数做与运算都得任何数,0与任何数做与运算都得0)  
                private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  
          
                // 0向左进位30,就是00 00000000000(00后跟30个0)  
                public static final int UNSPECIFIED = 0 << MODE_SHIFT;  
                // 1向左进位30,就是01 00000000000(01后跟30个0)  
                public static final int EXACTLY     = 1 << MODE_SHIFT;  
                // 2向左进位30,就是10 00000000000(10后跟30个0)  
                public static final int AT_MOST     = 2 << MODE_SHIFT;  
          
                /** 
                 * 根据提供的size和mode得到一个详细的测量结果 
                 */  
                // measureSpec = size + mode;   (注意:二进制的加法,不是10进制的加法!)  
                // 这里设计的目的就是使用一个32位的二进制数,32和31位代表了mode的值,后30位代表size的值  
                // 例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100  
                public static int makeMeasureSpec(int size, int mode) {  
                    return size + mode;  
                }  
          
                /** 
                 * 通过详细测量结果获得mode 
                 */  
                // mode = measureSpec & MODE_MASK;  
                // MODE_MASK = 11 00000000000(11后跟30个0),原理是用MODE_MASK后30位的0替换掉measureSpec后30位中的1,再保留32和31位的mode值。  
                // 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值  
                public static int getMode(int measureSpec) {  
                    return (measureSpec & MODE_MASK);  
                }  
          
                /** 
                 * 通过详细测量结果获得size 
                 */  
                // size = measureSpec & ~MODE_MASK;  
                // 原理同上,不过这次是将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size  
                public static int getSize(int measureSpec) {  
                    return (measureSpec & ~MODE_MASK);  
                }  
          
                /** 
                 * 重写的toString方法,打印mode和size的信息,这里省略 
                 */  
                public static String toString(int measureSpec) {  
                    return null;  
                }  
        }  

    源码中的onMeasure()

    知道了widthMeasureSpec和heightMeasureSpec是什么以后,我们就可以来看onMeasure方法了:

        /** 
         * 这个方法需要被重写,应该由子类去决定测量的宽高值, 
         */  
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
           setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
                   getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
        }  

    在onMeasure中只调用了setMeasuredDimension()方法,接受两个参数,这两个参数是通过getDefaultSize方法得到的,我们到源码里看看getDefaultSize究竟做了什么。

    getDefaultSize():

        /** 
           * 作用是返回一个默认的值,如果MeasureSpec没有强制限制的话则使用提供的大小.否则在允许范围内可任意指定大小 
           * 第一个参数size为提供的默认大小,第二个参数为测量的大小 
           */  
          public static int getDefaultSize(int size, int measureSpec) {  
              int result = size;  
              int specMode = MeasureSpec.getMode(measureSpec);  
              int specSize = MeasureSpec.getSize(measureSpec);  
          
              switch (specMode) {  
              // Mode = UNSPECIFIED,AT_MOST时使用提供的默认大小  
              case MeasureSpec.UNSPECIFIED:  
                  result = size;  
                  break;  
              case MeasureSpec.AT_MOST:  
              // Mode = EXACTLY时使用测量的大小   
              case MeasureSpec.EXACTLY:  
                  result = specSize;  
                  break;  
              }  
              return result;  
          }  

    getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),这里就是获取最小宽度作为默认值,然后再根据具体的测量值和选用的模式来得到widthMeasureSpec。heightMeasureSpec同理。之后将widthMeasureSpec,heightMeasureSpec传入setMeasuredDimension()方法。

    setMeasuredDimension():

        /** 
         * 这个方法必须由onMeasure(int, int)来调用,来存储测量的宽,高值。 
         */  
        protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {  
            mMeasuredWidth = measuredWidth;  
            mMeasuredHeight = measuredHeight;  
          
            mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;  
        }  

    这个方法就是我们重写onMeasure()所要实现的最终目的。它的作用就是存储我们测量好的宽高值。

    这下思路清晰了,现在的任务就是计算出准确的measuredWidth和heightMeasureSpec并传递进去,我们所有的测量任务就算完成了。

    源码中使用的getDefaultSize()只是简单的测量了宽高值,在实际使用时需要精细、具体的测量。而具体的测量任务就交给我们在子类中重写的onMeasure方法。


    在子类中重写的onMeasure:

    在测量之前首先要明确一点,需要测量的是一个View(例如TextView),还是 一个ViewGroup(例如LinearLayout),还是多个ViewGroup嵌套。如果只有一个View的话我们就测量这一个就可以了,如果有 多个View或者ViewGroup嵌套我们就需要循环遍历视图中所有的View。

    下面列出一个最简单的小例子,写一个自定义类CostomViewGroup继承自ViewGroup,然后重写它的构造方法,onMeasure和onLayout方法。用这个自定义的ViewGroup去写一个布局文件如下:

     1     <com.gxy.text.CostomViewGroup xmlns:android="http://schemas.android.com/apk/res/android"  
     2         android:layout_width="match_parent"  
     3         android:layout_height="match_parent"  
     4         android:background="#bbbaaa"  
     5         >  
     6         <Button  
     7             android:text="@string/hello_world"  
     8             android:layout_width="match_parent"  
     9             android:layout_height="wrap_content"  
    10             android:background="#aaabbb"  
    11             android:id="@+id/textView1" />  
    12     </com.gxy.text.CostomViewGroup>  

    将一个Button放入自定义的ViewGroup中,然后在MainActivity的onCreate回调方法中调用setContentView把整个布局文件设置进去。

    最后看一下自定义CostomViewGroup中的onMeasure方法的内容:

        @Override  
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
          
               //调用ViewGroup类中测量子类的方法  
               measureChildren(widthMeasureSpec, heightMeasureSpec);  
               //调用View类中默认的测量方法  
               super.onMeasure(widthMeasureSpec,heightMeasureSpec);  
          
        }  

    本文只是介绍测量,所以onLayout方法先省略,下面来看看效果图:


    在子类重写的onMeasure中只调用两个方法,第一个是父类的onMeasure方法,之前已经介绍了它的作用,它最后会调用setMeasuredDimension()将测量好的宽高值传递进去。第二个会调用measureChildren方法,它的作用是测量所有的子View,下面我们看看它是如何工作的。

    measureChildren()

    /** 
     * 遍历所有的子view去测量自己(跳过GONE类型View) 
     * @param widthMeasureSpec 父视图的宽详细测量值 
     * @param heightMeasureSpec 父视图的高详细测量值 
     */  
    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,如果View的状态不是GONE就调用measureChild去进行下一步的测量


    measureChild()

        /** 
         * 测量单个视图,将宽高和padding加在一起后交给getChildMeasureSpec去获得最终的测量值 
         * @param child 需要测量的子视图 
         * @param parentWidthMeasureSpec 父视图的宽详细测量值 
         * @param parentHeightMeasureSpec 父视图的高详细测量值 
         */  
        protected void measureChild(View child, int parentWidthMeasureSpec,  
                int parentHeightMeasureSpec) {  
            // 取得子视图的布局参数  
            final LayoutParams lp = child.getLayoutParams();  
          
            // 通过getChildMeasureSpec获取最终的宽高详细测量值  
            final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,  
                    mPaddingLeft + mPaddingRight, lp.width);  
            final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,  
                    mPaddingTop + mPaddingBottom, lp.height);  
          
            // 将计算好的宽高详细测量值传入measure方法,完成最后的测量  
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);  
        }  

     getChildMeasureSpec()

    可能看完后感觉有点迷糊,接下来通过几个例子演示一下,可能大家就会对getChildMeasureSpec方法中的逻辑清晰一些。

     1     /** 
     2      * 在measureChildren中最难的部分:找出传递给child的MeasureSpec。 
     3      * 目的是结合父view的MeasureSpec与子view的LayoutParams信息去找到最好的结果 
     4      * (也就是说子view的确切大小由两方面共同决定:1.父view的MeasureSpec 2.子view的LayoutParams属性) 
     5      *  
     6      * @param spec 父view的详细测量值(MeasureSpec) 
     7      * @param padding view当前尺寸的的内边距和外边距(padding,margin) 
     8      * @param childDimension child在当前尺寸下的布局参数宽高值(LayoutParam.width,height) 
     9      */  
    10     public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  
    11         //父view的模式和大小  
    12         int specMode = MeasureSpec.getMode(spec);     
    13         int specSize = MeasureSpec.getSize(spec);     
    14       
    15         //通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)   
    16         int size = Math.max(0, specSize - padding);  
    17       
    18         //子view想要的实际大小和模式(需要计算)  
    19         int resultSize = 0;  
    20         int resultMode = 0;  
    21       
    22         //通过1.父view的MeasureSpec 2.子view的LayoutParams属性这两点来确定子view的大小  
    23         switch (specMode) {  
    24         // 当父view的模式为EXACITY时,父view强加给子view确切的值  
    25         case MeasureSpec.EXACTLY:  
    26             // 当子view的LayoutParams>0也就是有确切的值  
    27             if (childDimension >= 0) {  
    28                 //子view大小为子自身所赋的值,模式大小为EXACTLY  
    29                 resultSize = childDimension;  
    30                 resultMode = MeasureSpec.EXACTLY;  
    31             // 当子view的LayoutParams为MATCH_PARENT时(-1)  
    32             } else if (childDimension == LayoutParams.MATCH_PARENT) {  
    33                 //子view大小为父view大小,模式为EXACTLY  
    34                 resultSize = size;  
    35                 resultMode = MeasureSpec.EXACTLY;  
    36             // 当子view的LayoutParams为WRAP_CONTENT时(-2)      
    37             } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
    38                 //子view决定自己的大小,但最大不能超过父view,模式为AT_MOST  
    39                 resultSize = size;  
    40                 resultMode = MeasureSpec.AT_MOST;  
    41             }  
    42             break;  
    43       
    44         // 当父view的模式为AT_MOST时,父view强加给子view一个最大的值。  
    45         case MeasureSpec.AT_MOST:  
    46             // 道理同上  
    47             if (childDimension >= 0) {  
    48                 resultSize = childDimension;  
    49                 resultMode = MeasureSpec.EXACTLY;  
    50             } else if (childDimension == LayoutParams.MATCH_PARENT) {  
    51                 resultSize = size;  
    52                 resultMode = MeasureSpec.AT_MOST;  
    53             } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
    54                 resultSize = size;  
    55                 resultMode = MeasureSpec.AT_MOST;  
    56             }  
    57             break;  
    58       
    59         // 当父view的模式为UNSPECIFIED时,子view为想要的值  
    60         case MeasureSpec.UNSPECIFIED:  
    61             if (childDimension >= 0) {  
    62                 // 子view大小为子自身所赋的值  
    63                 resultSize = childDimension;  
    64                 resultMode = MeasureSpec.EXACTLY;  
    65             } else if (childDimension == LayoutParams.MATCH_PARENT) {  
    66                 // 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0  
    67                 resultSize = 0;  
    68                 resultMode = MeasureSpec.UNSPECIFIED;  
    69             } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
    70                 // 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0  
    71                 resultSize = 0;  
    72                 resultMode = MeasureSpec.UNSPECIFIED;  
    73             }  
    74             break;  
    75         }  
    76         return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
    77     }  


    1.当父类View中宽高都为MATCH_PARENT(EXACTLY)时,宽高都为MATCH_PARENT(EXACTLY)时:

    2.当父类View中宽高都为MATCH_PARENT(EXACTLY)时,宽高都为WRAP_CONTENT(EXACTLY)时:

    3.当父类View中宽高都为MATCH_PARENT(EXACTLY)时。子类宽WRAP_CONTENT(AT_MOST),高为MATCH_PARENT(EXACTLY)时:



    1.当父类View中宽高都为WRAP_CONTENT(AT_MOST)时,子类宽高都为MATCH_PARENT(EXACTLY)时:

    2.当父类View中宽高都为WRAP_CONTENT(AT_MOST)时。子类宽WRAP_CONTENT(AT_MOST),高为MATCH_PARENT(EXACTLY)时:

     

    通过这两组简单的对比,其实大家就可以把测量子类大小的代码理解为:

    父类中MATCH_PARENT、WRAP_CONTENT、指定值,和子类中的MATCH_PARENT、WRAP_CONTENT、指定值。这两组三对值的相互作用。

    更复杂的情况则需要加上padding内边距和margin外边距等等一些其他对于View大小的约束。


    总结:

    今天介绍的都是系统提供的测量方法,除了这些以外还有一些其他的,大家可以看看源码。而且在真正的自定义View视图时,很大一部分都是借助这些系统提供的现成方法,并且根据需求再加上自己的特殊逻辑(当然也可以全部用自己的逻辑,但我们不要重复制造轮子)。

    这篇文章写了2个礼拜,写之前思路非常清晰,但是在写的时候越写越乱。写完以后感觉逻 辑仍然不是很清晰,因为有的内容我也是一知半解比如UNSPECIFIED。如果大家水平和我差不多都是菜鸟级别的,希望大家不要深入的去研究源码逻辑, 这样会导致越来越来混乱,从应用的角度出发可能会更好一些。

    下面会接着写onLayout和LayoutParams的相关内容(ANDROID自定义视图——onLayout源码 流程 思路详解)。最后再将onMeasure,onLayout结合起来写一个完整的例子(ANDROID自定义视图——仿瀑布布局)。也许这些都写完以后会对整个流程的思路会更加清晰。

  • 相关阅读:
    Linux IO接口 监控 (iostat)
    linux 防火墙 命令
    _CommandPtr 添加参数 0xC0000005: Access violation writing location 0xcccccccc 错误
    Visual Studio自动关闭
    Linux vsftpd 安装 配置
    linux 挂载外部存储设备 (mount)
    myeclipse 9.0 激活 for win7 redhat mac 亲测
    英文操作系统 Myeclipse Console 乱码问题
    Linux 基本操作命令
    linux 查看系统相关 命令
  • 原文地址:https://www.cnblogs.com/liangstudyhome/p/4121364.html
Copyright © 2011-2022 走看看