View工作原理 | 理解MeasureSpec和LayoutParams
前言
本篇文章是理解View的测量原理的前置知识,在说View的测量时,我相信很多开发者都会说出重写onMeasure方法,比如下面方法:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
但是这时你有没有想过这个方法中的参数即2个Int值是什么意思,以及是谁调用该方法的。
正文
本篇文章就从这个MeasureSpec值的意义以及如何得到MeasureSpec这2个角度来分析。
MeasureSpec
直接翻译就是测量规格,虽然我们在开发中会自己使用Java代码写布局或者在XML中直接进行布局,但是系统在真正测量以及确定其View大小的函数onMeasue中,参数却是MeasureSpec类型,那么它和普通的Int类型有什么区别呢?
其实在测量过程中,系统会将View的布局参数LayoutParams根据父View容器所施加的规则转换为对应的MeasureSpec,然后根据这个MeasureSpec便可以测量出View的宽高。注意一点,测量宽高不一定等于View的最终宽高。
其实这里就可以想一下为什么要如此设计,我们在XML中写布局的时候,在设置View的大小时就是通过下面2个属性:
android:layout_width="match_parent"
android:layout_height="wrap_content"
然后再加上padding、margin等共同确定该View的大小;这里虽然没啥问题,但是这个中间转换模式太麻烦了,需要开发者手动读取属性,而且读取各种padding、margin值等,不免会引起错误。
所以Android系统就把这个复杂个转换自己给做了,留给开发者的只有一个宽度MeasureSpec和高度MeasureSpec,可以方便开发者。
MeasureSpec是一个32位Int的值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。下面是MeasureSpec的源码,比较简单:
//静态类
public static class MeasureSpec {
//移位 30位
private static final int MODE_SHIFT = 30;
//MODE_MASK的值也就是110000...000即11后面跟30个0
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//UNSPECIFED模式的值也就是00...000即32个0
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
//EXACTLY模式的值也就是01000...000即01后面跟30个0
public static final int EXACTLY = 1 << MODE_SHIFT;
//AT_MOST模式的值也就是10000...000即10后面跟30个0
public static final int AT_MOST = 2 << MODE_SHIFT;
//根据最多30位二进制大小的值以及3个MODE创建出一个32位的MeasureSpec的值
//32位中高2位00 01 10分别表示模式,低30位代表大小
public static int makeMeasureSpec( int size,
@MeasureSpecMode int mode) {
//不考虑这种情况
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
//获取模式
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
//获取大小
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包方法。
其中SpecMode有3类,每一类都表示特殊的含义,如下所示:
SpecMode | 含义 |
---|---|
UNSPECIFIED | 表示父容器不对View做任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态 |
EXACTLY | 表示父容器已经监测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指的值。它对于于LayoutParams中的match_parent和具体的数值这俩种模式 |
AT_MOST | 表示父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值,需要看View的具体实现。对应于LayoutParams中的wrap_content。 |
从上表格可用发现,一个View的宽高MeasureSpec由它父View和自己的LayoutParams共同决定。
MeasureSpec和LayoutParams的对应关系
上面提到在系统中是以MeasureSpec来确定View测量后的宽高,而正常情况下我们会使用LayoutParams来约束View的大小,所以中间这个转换过程也就是将View的LayoutParams在父容器的MeasureSpec作用下,共同产生View的MeasureSpec。
LayoutParams
这个类在我们平时用代码来设置布局的时候非常常见,其实它就是用来解析XML中一些属性的,我们来看一下源码:
//这个是ViewGroup中的LayoutParams
public static class LayoutParams {
//对应于XML中的match_parent、wrap_parent
public static final int FILL_PARENT = -1;
public static final int MATCH_PARENT = -1;
public static final int WRAP_CONTENT = -2;
//宽度
public int width;
//高度
public int height;
//构造函数
public LayoutParams(Context c, AttributeSet attrs) {
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
//解析出XML定义的属性,赋值到宽和高2个属性上
setBaseAttributes(a,
R.styleable.ViewGroup_Layout_layout_width,
R.styleable.ViewGroup_Layout_layout_height);
a.recycle();
}
//构造函数,用于代码创建实例
public LayoutParams(int width, int height) {
this.width = width;
this.height = height;
}
//读取XML中的对应属性
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
width = a.getLayoutDimension(widthAttr, "layout_width");
height = a.getLayoutDimension(heightAttr, "layout_height");
}
}
这里我们会发现我们在XML中设置的宽高属性就会在这个ViewGroup的LayoutParams给记录起来。
既然说起来LayoutParams,我们就来扩展一下子,因为我们平时在代码中设置这个LayoutParams经常会犯的一个错误就是获取到这个View的LayoutParams,它通常不是ViewGroup.LayoutParams,而是其他的,如果不注意就会强转失败,这里多看2个常见子类。
MarginLayoutParams
第一个就是MarginLayoutParams,一般具体具体View的XXX.LayoutParams都是继承这个父类,代码如下:
public static class MarginLayoutParams extends ViewGroup.LayoutParams {
//4个方向间距的大小
public int leftMargin;
public int topMargin;
public int rightMargin;
public int bottomMargin;
//分别解析XML中的margin、topMargin、leftMargin、bottomMargin和rightMargin属性
public MarginLayoutParams(Context c, AttributeSet attrs) {
super();
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
setBaseAttributes(a,
R.styleable.ViewGroup_MarginLayout_layout_width,
R.styleable.ViewGroup_MarginLayout_layout_height);
//省略
a.recycle();
}
//省略
}
这个不论我们View在啥ViewGroup的里面,在XML中都可以设置其margin,而这些margin的值都会被保存起来。
具体的LayoutParams
第二个就是具体的LayoutParams,比如这里举例LinearLayout.LayoutParams。
首先回顾一下,线性布局的布局参数有什么特点,在XML中在线性布局里写新的View,这时你可以设置宽或者高为0dp,然后设置权重,以及设置layout_gravity这些属性,所以这些属性在解析XML时就会保存到相应的布局参数LayoutParams中,线性布局的布局参数代码如下:
//线性布局的LayoutParams
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
//权重属性
@InspectableProperty(name = "layout_weight")
public float weight;
//layout_gravity属性
@ViewDebug.ExportedProperty(category = "layout", mapping = {
@ViewDebug.IntToString(from = -1, to = "NONE"),
@ViewDebug.IntToString(from = Gravity.NO_GRAVITY, to = "NONE"),
@ViewDebug.IntToString(from = Gravity.TOP, to = "TOP"),
@ViewDebug.IntToString(from = Gravity.BOTTOM, to = "BOTTOM"),
@ViewDebug.IntToString(from = Gravity.LEFT, to = "LEFT"),
@ViewDebug.IntToString(from = Gravity.RIGHT, to = "RIGHT"),
@ViewDebug.IntToString(from = Gravity.START, to = "START"),
@ViewDebug.IntToString(from = Gravity.END, to = "END"),
@ViewDebug.IntToString(from = Gravity.CENTER_VERTICAL, to = "CENTER_VERTICAL"),
@ViewDebug.IntToString(from = Gravity.FILL_VERTICAL, to = "FILL_VERTICAL"),
@ViewDebug.IntToString(from = Gravity.CENTER_HORIZONTAL, to = "CENTER_HORIZONTAL"),
@ViewDebug.IntToString(from = Gravity.FILL_HORIZONTAL, to = "FILL_HORIZONTAL"),
@ViewDebug.IntToString(from = Gravity.CENTER, to = "CENTER"),
@ViewDebug.IntToString(from = Gravity.FILL, to = "FILL")
})
@InspectableProperty(
name = "layout_gravity",
valueType = InspectableProperty.ValueType.GRAVITY)
public int gravity = -1;
//一样从构造函数中获取对应的属性
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a =
c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);
weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);
a.recycle();
}
}
到这里我们就知道了,其实我们在XML布局中的写的各种大小属性,都会被解析为各种LayoutParams实例给保存起来。
转换关系
前面我们知道既然测量的过程需要这个MeasureSpec,而我们平时在开发中在XML里都是使用View的属性,而上面我们可知不论是XML还是代码最终View的宽高等属性都是赋值到了LayoutParams这个类实例中,所以搞清楚MeasureSpec和LayoutParams的转换关系非常重要。
正常来说,View的MeasureSpec由它父View的MeasureSpec和自己的LayoutParams来共同得到,但是对于不同的View,其转换关系是有一点差别的,我们挨个来说一下。
DecorView的MeasureSpec
因为DecorView作为顶级View,它没有父View,所以我们来看一下它的MeasureSpec是如何生成的,在ViewRootImpl的measureHierarchy方法中有,代码如下:
//获取decorView的宽高的MeasureSpec
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
//开始对DecorView进行测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
看一下这个getRootMeasureSpec方法:
//windowSize就是当前Window的大小
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
//当布局参数是match_parent时,测量模式是EXACTLY
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
/当布局参数是wrap_content时,测量模式是AT_MOST
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
//具体宽高时,对应也就是EXACTLY
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
这里我们就会发现DecorView的Measure的获取非常简单,当DecorView的LayoutParams是match_parent时,测量模式是EXACTLY,值是Window大小;当DecorView的LayoutParams是wrap_content时,测量模式是AT_MOST,值是window大小。
View的MeasureSpec
对于View的MeasureSpec的获取稍微不一样,因为它肯定有父View,所以它的MeasureSpec的创造不仅和自己的LayoutParam有关,还和父View的MeasureSpec有关。
在这里我们先不讨论ViewGroup以及View是如何分发这个测量流程的,后面再说,这里有个我们在自定义ViewGroup时常用的方法,它用来测量它下面的子View,代码如下:
//ViewGroup中的代码,用来自定义ViewGroup时遍历子view,然后挨个进行测量
protected void measureChildWithMargins(
//子View
View child,
//ViewGroup的MeasureSpec,即父View的MeasureSpec
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//子View的LayoutParams
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//获取子View的MeasureSpec
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);
//子View进行测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
这里先不讨论子View如何去测量,只关注在有父View的MeasureSpec和自己的LayoutParams时,它是如何得到自己的MeasureSpec的,代码如下:
//调用的代码
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
这里注意一下参数,第一个参数是父View的MeasureSpec,第三个参数是当前View的宽度,而这里的宽度有3种:wrap_content为-2,match_parent为-1,具体值大于等于0,虽然说是宽度,也包含了View的LayoutParams信息。
第二个参数表示间距,其中mPaddingLeft和mPaddingRight很重要,因为这个属性是不会记录在LayoutParams中的,而且它的涵义是内间距,这里它是写在父ViewGroup中的属性值,比如加了这个paddingLeft属性后,其子View不会从原点开始绘制,它所可用的宽度就会变小,所以View在测量其大小时要把padding排除在外。
然后看一下源码实现:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//父View的测量模式
int specMode = MeasureSpec.getMode(spec);
//父View的大小
int specSize = MeasureSpec.getSize(spec);
//padding是否大于父View的大小了
int size = Math.max(0, specSize - padding);
//子View的大小
int resultSize = 0;
//子View的测量模式
int resultMode = 0;
//这里要明白layoutParams中的wrap_content是-2,match_parent是-1,具体值才大于0
switch (specMode) {
//父View的测量模式是精确模式
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//子View当前写死了大小,所以测量模式必是精确模式
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子View和父View一样大,所以测量模式肯定是精确模式
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子View是包裹内容,其最大值是父View的大小
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
//父View的测量模式是至多模式
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
//子View大小写死,测量模式必须是精确模式
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//和父类一样,也是父类的至多模式
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//这里要稍微注意一下,由于父类最大多少,所以这个View也是至多模式
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 不分析
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let them 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);
}
由这里代码可以看出,和DecorView不同的是当前View的MeasureSpec的创建和父View的MeasureSpec和自己的LayoutParams有关。
普通View的MeasureSpec创建规则
对于DecorView的转换我们一般不会干涉,这里有一个普通View的MeasureSpce创建规则总结:
子View布局\父View Mode | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
dp/dx具体值 | EXACTLY+childSize | EXACTLY+childSize | EXACTLY+childSize |
match_parent | EXACTLY+parentSize | AT_MOST+parentSize | UNSPECIFIIED+0 |
wrap_conent | AT_MOST+parentSize | AT_MOST+parentSize | UNSPECIFIIED+0 |
这个规则必须牢记,在后面View的绘制中我们将具体解析。
总结
本篇文章主要是理解MeasureSpec的设计初衷以及其含义,然后就是一个View的MeasureSpec是通过什么规则转换而来。后面文章我们将具体分析如何利用MeasureSpce来进行测量,最终确定View的大小。
笔者水平有限,有错误希望大家评论、指正。
链接:https://juejin.cn/post/7051543108516839431
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。