Android View的绘制过程主要有三步:
首先理解MeasureSpec的含义,然后跟踪ViewGroup的measure、layout、draw三个方法即可
view的绘制流程是我们在自定义View中通常会使用到的一个知识点,也是一个面试常问的点。简直是Android开发必备知识。
DecorView我们相对比较熟悉,因为开发中就会不时的用到,它是整个Activity的顶层View,我们设置的布局文件都是它的子view。
而ViewRootImpl是连接WindowManager和DecorView的纽带,View的绘制三大流程都是通过ViewRootImpl来完成的。Activity创建后,会把DecorView添加到WindowManager中,同时会创建ViewRootImpl对象,使DecorView和ViewRootImpl建立联系,代码如下:
//代码地址:frameworks/base/core/java/android/view/WindowManagerGlobal.java addView方法
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
...
}
View的绘制也是从ViewRootImpl的performTraversals方法开始的,然后调用view的measure、layout、draw方法,流程图如下:
在真正调用measure方法进行测量前,必须哟啊计算出MeasureSpec,然后才能用MeasureSpec去测出View的大小,那什么是MeasureSpec?
Android系统通过MeasureSpec来测量View的宽高,并且把测量模式和测量数据都放在MeasureSpec中,它由一个32位int值组成,其中高2位代表测量模式,也就是SepcMode,低30位代表测量值,也就是SpecSize,表示View宽或高的具体大小。MeasureSpec是View.java的一个内部类, MeasureSpec以及View的源码路径: /frameworks/base/core/java/android/view/View.java
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public @interface MeasureSpecMode {}
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
MeasureSpec主要就提供了三个方法,上述的makeMeasureSpec方法相当于把mode和size组装在一起,形成一个MeasureSpec。
低30位表示宽或高的大小,那实际可以表示的大小为2^30 -1 ,是一个非常大的值了,完全够用。
高两位代表测量模式,按照00, 01, 10, 11四种组合来看,可以形成四种测量模式,实际上有三种测量模式:
第2节提到, Android系统通过MeasureSpec来进行View的测量,并保存View的宽或高数据。正常情况下,我们在xml布局里面设置宽高或match_parent等, 或者在代码里通过LayoutParams来设置数据, 然后在系统测量View时,系统会将LayoutParams在父容器的约束下转换为对应的MeasureSpec,然后根据这个MeasureSpec测量子view的宽高。注意,是由LayoutParams和父View一起决定View的MeasureSpec。
但是,有一个特殊情况,那就是最顶层的DecorView, 它没有父view,它的MeasureSpec是由手机屏幕的尺寸和自身的LayoutParams来共同决定的。DecorView的MeasureSpec在ViewRootImpl中的measureHierarchy方法中创建:
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
其中,desiredWindowWidth和desiredWindowHeight分别表示屏幕的宽高。 测量DecorView的宽高都调用了getRootMeasureSpec:
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
顶级View的测量属性中,测量大小就是屏幕大小,测量模式就是EXACTLY。
普通View的测量,View的测量是通过ViewGroup传递过来的,因为每个view肯定都是存在于一个ViewGroup中, 先看ViewGroup中的measureChildWithMargins方法:
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);
}
以上代码主要分三步:
进入getChildMeasureSpec方法查看一下是如何得到childView的测量模式的:
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);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {//注释1 通过父view的测量模式,做不同操作
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {//注释2 如果childView的宽高是一个具体的值, 如100dp
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;//注释3 如果父view是EXACTLY,子view是match_parent, 则子view也是EXACTLY
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him 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);
}
上述代码逻辑很简单,就是通过父view和子view的LayoutParams, 共同确定子view的测量模式MeasureSpec,总结如下:
1、当View为固定宽高时,测量模式是EXACTLY模式,测量值就是布局参数中的大小。
2、当View为WRAP_CONTENT时,测量模式是AT_MOST模式,测量值是父容器的剩余空间大小。
3、当View为MATCH_PARENT时,测量值是父容器的剩余空间大小,测量模式分两种情况,如果父容器是EXACTLY模式,那就是EXACTLY模式,如果父容器是AT_MOST模式,那么View也是AT_MOST模式。
如第3.2小节开头的代码所展现的,当获取了子view的宽和高的MeasureSpec,就可以真正开始对view进行测量了:
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
今天先写到这里,具体的measure、layout、draw过程的分析,请见下一篇博客Android View的测量、布局、绘制过程详解(下)