Android View - 测量Measure

Android在绘制View之前,必须对View进行测量,测量之后,Android才知道要绘制多大的View。View的测量过程,用到MeasureSpec,我们要理解了这个类,才能更好理解View的测量过程。

MeasureSpec

应用别人的MeasureSpec源码:

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...0(11后跟30个0)  
        // (遮罩的作用是用1标注需要的值,0标注不要的值。因为1与任何数做与运算都得任何数,0与任何数做与运算都得0)  
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  

        // 0向左进位30,就是00 00000000000...0(00后跟30个0)  
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;  
        // 1向左进位30,就是01 00000000000...0(01后跟30个0)  
        public static final int EXACTLY     = 1 << MODE_SHIFT;  
        // 2向左进位30,就是10 00000000000...0(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;  
        } 

}  

代码注释很清晰,也容易理解。其实MeasureSpec是一个工具类,通过这个工具类,可以将本来2个int(mode和size)信息的值通过2进制形式合成一个,减少内存分配。因为测量方法调用相当的频繁,能省则省,GoodiDea。

mode有3中模式:

EXACTLY
精确值模式。如在定义View时,设置layout_width=”100dp”或者 layout_width=”match_parent”,此时View的宽度为精确值。

AT_MOST
最大值模式。如在定义View时,设置layout_width=”wrap_content”,此时View的宽度会根据内容而定,但是不能超过父控件可用的大小。

UNSPECIFIED
不限制模式。View想要多大有多大,不做限制。一般用于系统内部,表示测量状态。

View的onMeasure方法

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

我们自定义View时,一般要重写这个方法根据模式设置View大小。getSuggestedMinimumWidth和getSuggestedMinimumHeight方法获取建议(默认)值:

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

从代码中看出,如果view设置了背景,则返回背景Drawable的大小(前提这个Drawable设置了大小,否则返回0)。如果没有设置背景,则返回mMinWidth(mMinHeight),mMinWidth对应xml属性android:minWidth,mMinHeight对应xml属性android:minHeight。
getDefaultSize方法获取默认的大小:

/**
 * 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时使用提供的默认大小  
        case MeasureSpec.UNSPECIFIED:  
            result = size;  
            break;  
        // Mode = AT_MOST,EXACTLY时使用测量的大小
        case MeasureSpec.AT_MOST:  
        case MeasureSpec.EXACTLY:  
            result = specSize;  
            break;  
    }  
    return result;  
}  

然后通过setMeasuredDimension方法储存View的宽高:

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {  
    mMeasuredWidth = measuredWidth;  
    mMeasuredHeight = measuredHeight;  
    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;  
}  

代码中显示,setMeasuredDimension方法是储存View测量之后的宽高值,所以我们重写onMeasure方法时,一定要调用setMeasuredDimension方法。

我们从getDefaultSize方法知道,我们自定义View时(继承View),如果没有重写onMeasure方法,那么wrap_content将不起作用。从代码上看,当specMode为MeasureSpec.AT_MOST时,result的值为specSize。结合下面的表,specSize值是父View的可用大小,那么wrap_content就相当于match_parent了,所以我们得重写onMeasure方法满足wrap_content属性。一般处理方法是,当specMode为MeasureSpec.AT_MOST时,就设定result为某个精确值。

ViewGroup的onMeasure方法

因为View是一种树状结构,父控件要测量,那么便会通知所有子View先测量,然后再测量自己。

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

我们看看测量所有子View的方法measureChildren:

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方法:

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方法的作用是根据父控件MeasureSpec,父控件的padding和子View的width获取测量大小:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  
    // 父View的模式和大小  
    int specMode = MeasureSpec.getMode(spec);     
    int specSize = MeasureSpec.getSize(spec);     
    // 计算出父View可用大小,specSize为父view本来的大小,padding是父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的话子View大小为0  
            resultSize = 0;  
            resultMode = MeasureSpec.UNSPECIFIED;  
        }  
        break;  
    }  
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
}

再配合一张表看:

估计这样看就不会懵了。获取到子View的测量大小后,调用child.measure方法完成子View的测量。到此,完成View的测量。

参考资料

ANDROID自定义视图——onMeasure,MeasureSpec源码 流程 思路详解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值