自定义View之OnMeasure方法学习

本文详细解析了Android中View和ViewGroup的onMeasure方法,包括MeasureSpec的构成与使用,以及如何测量子View的宽高。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言:前几天把onMeasure,onLayout的相关方法学习了一下,打算在做一个项目有深刻理解以后,在总结自己关于这两个方法的学习。学习当中,感觉又会遇到新的问题,新的要学习的知识点。感觉如果还不把之前学习的内容,记录下来,现在不抽出时间,那么以后也抽不出时间。所以,学习一个知识点,就要记录下,不要拖。或许,这就是解决拖延症的办法:做一件事就做彻底,不留尾巴。Just Do It.

首先明确一点,关于View的视图结构,是一个树形递归结构。不论我们是在执行onMeasure方法,还是onLayout方法,都是按照这个流程结构走的。无论我们测量、还是调整布局,都是从树的顶端,一层一层遍历,一个分支一个分支的执行measure,onMeasure,都测量完毕,在递归执行layout,onLayout。


所学onMeasure知识,关于对其总结,也是基于学习了各个博主的讲解,结合源码。最后给出相关博主连接。


Android 应用程序UI绘制分为三个过程(先后顺序是):测量measure,布局位置layout,绘制draw。


关于View的递归遍历,不仅是onMeasure适用,onLayout也一样适用。

一次遍历下来,第一个子控件以及这个子控件中的所有子控件都会完成测量工作;然后开始测量第二个子控件…;

最后父控件所有的子控件都完成测量以后会调用setMeasureDimension方法保存自己的测量大小。值得注意的是,

这个过程不只执行一次,也就是说有可能重复执行,因为有的时候,一轮测量下来,父控件发现某一个子控件的尺寸不符合要求,

就会重新测量一遍。上图:





一、在自定义View,一般情况下,只有View的宽、高属性需要wrap_content的时候,才会用到onMeasure方法。

在View的源码中,measure(w,h)方法测量View的实际大小。是个final类型,不允许外部修改调用。所以继承View的扩展类,只能调用measure(w,h)里的onMeasure()方法,对布局进行测量。但是子View计算自己宽高时,可以调用该方法。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {  
...  
  
onMeasure(widthMeasureSpec, heightMeasureSpec);  
...  
  
}  

看onMeasure()方法内部:

/** 
 * 这个方法需要被重写,应该由子类去决定测量的宽高值.onMeasure方法宽高得到的是父布局控件的宽高 
 */  
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
   setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
}
在onMeasure()方法内部调用了setMeasuredDimension()方法。,作用是来存储测量宽,高值。作用是来存储测量宽,高值。作用是来存储测量宽,高值。(重要说三遍。)

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

在返回onMeasure()方法内,getDefaultSize()方法。看内部方法:

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

可以看出,如果是mode为:MeasureSpec.UNSPECIFIED时,调用取传递进来默认值。剩余两种情况,则是根据传递进来的measureSpec来取size大小。

在getDefaultSize方法里传参measureSpec,里边封装了有我们需要去取两个东西,一个是mode,一个是size。

mode有三个值:UNSPECIFIED、EXACTLY、AT_MOST。在我们布局里,就是分别是:指定具体宽高值,match_parent,wrap_content.三种方式。

size就是指的宽、高。

而measureSpec、size、mode他们三个关系,都封装在View的一个内部类,MeasureSpec类当中。

来看下MeasureSpec类当中方法:

/** 
 * 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;   (注意:二进制的加法,不是十进制的加法!)  
        // 这里设计的目的就是使用一个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;  
        }  
}

注意的是mode与size 的关系,二者是通过integer32位二进制来表示的。最前两位表示mode值,后三十位表示size大小。

我们在重写onMeasure时候,需要用到测量宽、高的精确值可以用到MeasureSpec.makeMeasureSpec(size,mode),返回一个int measureSpecWidth,int measureSpecHeight.

获取size、mode大小就是通过该内部类静态方法

int size = MeasureSpec.getSize(int measureSpecWidth);//或者传参 measureSpecHeight 

int mode = MeasureSpec.getMode(int measureSpecWidth);//或者传参 measureSpecHeight 


二、在ViewGroup、或者多个ViewGroup中,就用到了视图树的递归调用。

在测量之前首先要明确一点,需要测量的是一个View,还是一个ViewGroup,还是多个ViewGroup。如果是一个View测量一个就可以了。如果是ViewGroup或者多个ViewGroup嵌套,我们就需要循环遍历视图中所有的View。遍历顺序按着遍历完一个二叉树分支,在遍历另一个分支...

<com.lhq.fight.CostomViewGroup xmlns:android="http://schemas.android.com/apk/res/android"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent"  
    android:background="#ffffefff"  
    >  
    <TextView  
        android:text="@string/tv_info"  
        android:layout_width="match_parent"  
        android:layout_height="wrap_content"  
        android:background="#5dff00ff"  
        android:id="@+id/textView1" />  
</com.gxy.text.CostomViewGroup> 

在自定义View中嵌套childView。在onMeasure中测量childView方法

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

super.onMeasure()测量值是父布局的详细宽高。而measureChildren()方法,是测量当前ViewGroup中childView的方法。

几个关于测量childView时,有用的API:

int count = getChildCount();//获取当前ViewGroup中所有ChildView的个数。

int childView = getChildAt(index);//根据下标或者指定的childView。在遍历视图树时候使用。

int realWidth = getMeasuredWidth();//测量得到View的真实宽、高

int realHeight =  getMeasuredHeight();



/** 
 * 遍历所有的子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);  
        }  
    }  
}  

在measureChidlren方法中,如果当前子View的状态不是View.GONE状态,则遍历每一个子View后,都会测量每一个子View的宽高。

/** 
 * 测量单个视图,将宽高和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);  
} 


还有一个在计算子View时,需要计算View的边距:

measureChild 和measureChildWithMargins的区别就是是否把margin作为子视图的大小。需要计算进去。

//widthUsed、heightUsed就是传入的额外使用空间,margin的距离。

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);
    }



最后childView去调用 measure方法。我们在最后measure 我们的childView时,需要传入childView的详细宽、高值。那么关于宽、高值得计算是在getChildMeasureSpec方法中

来看代码:

/** 
 * 在measureChildren中最难的部分:找出传递给child的MeasureSpec。 
 * 目的是结合父view的MeasureSpec与子view的LayoutParams信息去找到最好的结果 
 * (也就是说子view的确切大小由两方面共同决定:1.父view的MeasureSpec 2.子view的LayoutParams属性) 
 *  
 * @param spec 父view的详细测量值(MeasureSpec) 
 * @param padding view当前尺寸的的内边距和外边距(padding,margin) 
 * @param childDimension child在当前尺寸下的布局参数宽高值(LayoutParam.width,height) 
 */  
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  
    //父view的模式和大小  
    int specMode = MeasureSpec.getMode(spec);     
    int specSize = MeasureSpec.getSize(spec);     
  
    //通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)   
    int size = Math.max(0, specSize - padding);  
  
    //子view想要的实际大小和模式(需要计算)  
    int resultSize = 0;  
    int resultMode = 0;  
  
    //通过1.父view的MeasureSpec 2.子view的LayoutParams属性这两点来确定子view的大小  
    switch (specMode) {  
    // 当父view的模式为EXACITY时,父view强加给子view确切的值(一般是父view设置为match_parent或者固定值的ViewGroup)  
    case MeasureSpec.EXACTLY:  
        // 当子view的LayoutParams>0也就是有确切的值  
        if (childDimension >= 0) {  
            //子view大小为子自身所赋的值,模式大小为EXACTLY  
            resultSize = childDimension;  
            resultMode = MeasureSpec.EXACTLY;  
        // 当子view的LayoutParams为MATCH_PARENT时(-1)  
        } else if (childDimension == LayoutParams.MATCH_PARENT) {  
            //子view大小为父view大小,模式为EXACTLY  
            resultSize = size;  
            resultMode = MeasureSpec.EXACTLY;  
        // 当子view的LayoutParams为WRAP_CONTENT时(-2)      
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
            //子view决定自己的大小,但最大不能超过父view,模式为AT_MOST  
            resultSize = size;  
            resultMode = MeasureSpec.AT_MOST;  
        }  
        break;  
  
    // 当父view的模式为AT_MOST时,父view强加给子view一个最大的值。(一般是父view设置为wrap_content)  
    case MeasureSpec.AT_MOST:  
        // 道理同上  
        if (childDimension >= 0) {  
            resultSize = childDimension;  
            resultMode = MeasureSpec.EXACTLY;  
        } else if (childDimension == LayoutParams.MATCH_PARENT) {  
            resultSize = size;  
            resultMode = MeasureSpec.AT_MOST;  
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
            resultSize = size;  
            resultMode = MeasureSpec.AT_MOST;  
        }  
        break;  
  
    // 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大(多见于ListView、GridView)  
    case MeasureSpec.UNSPECIFIED:  
        if (childDimension >= 0) {  
            // 子view大小为子自身所赋的值  
            resultSize = childDimension;  
            resultMode = MeasureSpec.EXACTLY;  
        } else if (childDimension == LayoutParams.MATCH_PARENT) {  
            // 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0  
            resultSize = 0;  
            resultMode = MeasureSpec.UNSPECIFIED;  
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
            // 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0  
            resultSize = 0;  
            resultMode = MeasureSpec.UNSPECIFIED;  
        }  
        break;  
    }  
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
} 

这段代码,就是说明父View的mode最终来限定子View(childView)的mode,与size。通过size,与mode,最终计算出当前childView的详细宽、高。

在LayoutParams中

 FILL_PARENT = -1;//方法过时

 MATCH_PARENT = -1;

 WRAP_CONTENT = -2;

最后通过一系列条件判断,计算出size,mode,来返回子View的详细宽、高值。MeasureSpec.makeMeasureSpec(resultSize,resultMode);

仔细看,可以用一个表格来概括这一段代码。




最后给出学习参考:

大苞米onMeasure方法详解

苦咖啡的onMeasure方法详解

open_xu的onMeasure方法详解

最后感谢各位博主的分享知识,感谢。














评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值