Android View 绘制流程之一:measure测量
系列文章:
Android View 绘制流程之一:measure测量
Android View 绘制流程之二:layout布局
Android View 绘制流程之三:draw绘制
Android View 绘制流程之四:绘制流程触发机制
measure方法是View测绘系统的第一步,主要是需要重写onMeasure方法根据自己的规格来测量出真实尺寸,如果是ViewGroup的话,还要在onMeasure中根据规格和布局属性测量子view,调用其measure方法。
一.MeasureSpec测量规格
对于view的尺寸正如我们XML中定义的一样,可以是具体值xxxdp,也可以是相对值MATCH_PARENT、WRAP_CONTENT,如果都是具体值则其实不需要measure过程,直接遍历所有view为其设置具体的宽高即可完成大小的测量,而为了更好的支持"占满全部可用控件"、"包含子view的宽高"这些可以适用屏幕的情况,需要这些相对值,那么就需要一套复杂测量过程,通过父view的规格和view想要的规格计算出view实际的规格,于是View系统中使用MeasureSpec类来将其规格—也就是实际大小和模式(想要的规格)两个int值合成一个31位的int值,高两位代表模式,低30位代表大小。
整个view测量过程中会使用该种形式的值来设置view的规格:
-
MeasureSpec提供了三种模式:
-
EXACTLY:代表规格是一个确定的值
-
AT_MOST:代表规格是一个最大值
-
UNSPECIFIED:代表没有规格限制,view可以决定自己的大小
-
-
一些静态方法
-
makeMeasureSpec(size,mode):将大小和模式生成一个int的规格,高两位代表模式,其余代表尺寸,实际使用的是位运算生成
-
getMode(measureSpec):将一个规格的模式取出,实际使用的是位运算生成
-
getSize(measureSpec):将一个规格的尺寸取出,实际使用的是位运算生成
-
二.LayoutParams布局参数
我们使用view,为其设置各种布局属性,都是通过xml中或者代码动态设置,而在代码里获取属性时经常通过getLayoutParams拿到一个LayoutParam对象,而且还经常要强转(包括后面会看到view系统里也会调用该方法并强转);而我们一般在代码里设置具体LayoutParam子类(否则会因强转异常),或者根本不去手动设置他,但系统获取并强转时却没有错,那么LayoutParam是如何设计以及如何创建的呢?下面我们来看看。
1.View
addView
首先需要确定的是,每个view都需要各种属性(最起码得有宽高),并且最终的展示都需要加入到一个父view里(addView),下面来看下ViewGroup的addView方法
public void addView(View child, int index) {
...
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
...
}
addView(child, index, params);
}
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
可以看到,加入一个view到一个ViewGroup时,需要view的LayoutParams,如果view还没有,会生成一个默认的LayoutParam,而ViewGroup中生成的这个默认的对象就是ViewGroup.LayoutParam对象,而一般的ViewGroup(如LinearLayout)会拿到这个对象强转成MarginLayoutParam(继承自ViewGroup.LayoutParam)对象做有关margin参数的处理,怎么做的呢,其实是基本每个ViewGroup子类都会重写generateDefaultLayoutParams方法,返回其自己的一个继承自MarginLayoutParam的静态内部类,比如LinearLayout的实现:
LinearLayout的generateDefaultLayoutParams
protected LayoutParams generateDefaultLayoutParams() {
if (mOrientation == HORIZONTAL) {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
} else if (mOrientation == VERTICAL) {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
return null;
}
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
...
public float weight;
...
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();
}
public LayoutParams(int width, int height) {
super(width, height);
weight = 0;
}
...
}
这样一来,比如在LinearLayout内部使用其child的getLayoutParam强转成LinearLayout.LayoutParam就不会有问题了,因此,如果手动为view设置LayoutParam时(setLayoutParam),也需要设置成其相应父类的LayoutParam才行。
2.xml
对于xml中定义的view,LayoutParam如何设置的呢?下面我们来看下
一般我们写完布局文件后,会在activity的setContentView里设置,或是在某些地方使用LayoutInflater的inflate(layoutId,parent,attachToParent)来手动加载一个xml文件为view,而前者也是调用的后者的方法进行加载xml文件,所以我们看一下inflate方法是如何解析xml文件并生产LayoutParam对象的就可以了:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
...
if (TAG_MERGE.equals(name)) {
//merge标签的处理
...
} else {
// 从xml文件中找到第一个tag作为根view
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
// 根据传入的root来解析根view的LayoutParam,调用的是root的generateLayoutParam(正如之前说过的,一般ViewGroup会重载该方法返回特定的LayoutParam,这个LayoutParam不需要的attrs将失效)
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
//如果要直接addView则在后续的addView时带入该LayoutParam,不要addView时则给该view手动设置LayoutParam
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
// 继续加载xml中所有的子view,以该根view作为parent传入,用来解析每个子view的LayoutParam,解析完后addView到该根view中,且每个子view的加载会递归进行(子view是ViewGroup的情况)
// 所有子view加载完毕后调用view的onFinishInflate方法通知(可以重写该方法,在该方法内使用其子view)
rInflateChildren(parser, temp, attrs, true);
// 是否直接添加到root里
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// 如果没有加入到root里则返回的是xml中的根view,否则返回的是整个view(传进来的root)
if (root == null || !attachToRoot) {
result = temp;
}
}
...
return result;
}
}
上面代码注释以及解释的很详细,大概逻辑就是先解析出xml中的根view,根据传入的root设置其LayoutParam,然后以他作为parent去递归解析xml中的子view并将子view addView进去,解析完后,再去判断是否需要将整个view加入到传入的root当中去,并返回即可;注意点如下:
-
所以说如果我们没有传入root,那么xml中的根view的那些attrs都没有用
-
如果我们传入root,比如说一个LinearLayout,那么xml中的根view的LayoutParam会由LinearLayout的generateLayoutParam生成,也就是解析成LinearLayout.LayoutParam;此时如果你手动addView到一个RelativeLayout里就会出现异常了(因为需要的应该时RelativeLayout.LayoutParam)
-
setContentView()时系统会将最外层的一个ViewGroup当作root参数传入到inflate方法中,所以其根view的attrs是有效的;不过这个root是个FrameLayout,也就是说他只生成FrameLayout.LayoutParam,只能解析该LayoutParam的属性,所以,假设你xml的根view是个RelativeLayout,使用了其特有的attr比如android:layout_centerInParent时,就不会被解析进LayoutParam,就会失效,而用android:layout_gravity则可以,因为FrameLayout.LayoutParam里会解析该attr
三.Measure整体流程
上面解释了测量view时使用的规格数据类型,以及view的布局属性的设置,而view的measure过程就会根据要测量的规格和属性决定view的具体大小,下面就说一下measure的整体流程:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
//如果父类施加的规格发生了变化,或者该view有FORCE_LAYOUT标志则说明需要执行measure过程
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure过程中最重要的一个方法,交由子类重写,来决定自己的真实大小,如果是ViewGroup,要通过测量子view的大小来决定自己的大小
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
...
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;//测量完毕,指明view需要布局layout
}
//保存新的规格
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
大致步骤就是先回判断父类的规格较之前是否发生改变,或者view的flag里是否有FORCE_LAYOUT这个标志,如果满足则要进行实际的测量过程,否则不需要测量;测量时调用onMeasure方法交由子类实现自己的测量方式,最终计算出自己的大小(如果是ViewGroup还需递归调用子view的measure方法以便测量子view并根据子view测量结果决定自己的大小),并需要在onMeasure测量出实际大小后调用setMeasuredDimension将宽高保存;接着给view加入LAYOUT_REQUIRED标志使其将被重新布局;最后保存保存新的规格值以便下回比较使用;有几个注意点:
-
measure方法最初是由ViewRootImpl的performMeasure中调用根view的measure方法开始的,那么最初的规格是什么呢?是ViewRootImpl的getRootMeasureSpec生成的,一般就是窗口的宽高值(这部分内容可以参考系列文章)
-
measure方法传入的两个规格到底是什么意思呢?是父类通过他的规格结合子view的规格(width、height属性)来确定的一个规格,最终传给子view时对于子view来说就他自己的尺寸的规格,子view根据这个规格最终计算出自己的大小,具体的确定规则下面会说到
-
view系统的许多标志使用的不是一个一个的变量,而是使用的一个int值,将不同标志位的值通过位运算的方式加到这个变量上,在使用时也是根据位运算判断是否有该标志
-
有FORCE_LAYOUT说明需要重新布局,重新布局则必须要重新测量(这部分内容可以参考系列文章)
四.常见onMeasure实现
onMeasure方法view的measure过程的核心方法,子view应该重写其进行自己尺寸的计算,子ViewGroup也应该重写其进行子view的测量,并且根据子view的测量结果决定自己的尺寸,首先明确几个重要点:
1.测量子View方法
ViewGroup提供了几个重要的基础方法来测量子view
(1)measureChildren()
其实就是遍历children,调用measureChild方法(GONE掉的view不会测量)
(2)measureChild()
该方法里,会调用child的getLayoutParam方法拿到布局参数,然后结合父view传来的规格(也就是measure时传来的两个参数)调用getChildMeasureSpec()方法生成子view的具体宽高规格,并调用child.measure方法进行子view的测量;getChildMeasureSpec方法就是具体规格生成规则的方法,下面会说
(3)measureChildWithMargins()
该方法只是在measureChild方法基础上考虑到了子view的margin属性,所以这里需要将child的LayoutParam强转成MarginLayoutParam使用其margin相关属性,一般情况下的ViewGroup都会生产一个继承自MarginLayoutParam的LayoutParam类,所以在测量时可以调用此方法快捷测量,但是如果自定义的某个ViewGroup没有使用继承MarginLayoutParam的LayoutParam,那么就不能调用此方法,否则会强转时异常;那么getChildMeasureSpec如何使用这些值下面我们来看看
(4)getChildMeasureSpec(int spec, int padding, int childDimension)
这就是根据父类要求的规格和自身的想要规格计算出实际规格的方法,第一个参数是父view要求的规格(宽度或高度的);第二个参数是在该属性(宽度或高度)上已使用的值,计算时应该刨除;第三个是在该属性上子view自己想要的大小(具体值、MATCH_PARENT、WRAP_CONTENT)
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);//这是实际可以获得的规格大小,因为父view可能有padding,子view可能有margin
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// 父view的规格是一个固定值
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//如果子view的尺寸是一个固定值,那么子view的规格就是固定的(EXACTLY)一个值(childDimension),事实上,只要子view的尺寸固定,那么他的规格也就是某个固定的值,正如上面所说,都是这种情况则不需要有measure过程了
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//如果子view尺寸是想要占满父view全部,那么他的规格就是固定的可获得的尺寸(size),因为父view的尺寸是固定的,所以子view的尺寸也就知道了
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//如果子view尺寸时想要包含自身内容即可,那么他的规格就是最多(AT_MOST)是可获得的尺寸(size),这很合乎常理
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父view的规格是一个"最大值"
case MeasureSpec.AT_MOST: