View的工作原理
View的流程主要包括测量流程(measure)、布局流程(layout)、绘制流程(draw)。
当然除了View的这三大流程外,我们也应该了解一些View的基础概念以及View是怎么一步步绘制出来的。
我们先看下Android在UI方面的层级关系。
ViewRoot和DecorView
ViewRoot是ViewRootImpl类的实现,它是连接WindowManager
和DecorView
的纽带,View的三大流程均是通过ViewRoot来完成的。
当Activity对象被创建完毕后,会将DecorView添加到Window中,同时创建一个 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立连接,源码如下:
root = new ViewRootImpl(view.getContext(),display);
root.setView(view, params, panelParentView);
DecorView是整个Window界面的最顶层View,它的内部只有一个竖直方向的LinearLayout。这个LinearLayout里一般会分为两部分,标题栏和内容栏(根据Android版本和主题的不同,具体情况不同)。DecorView 实际是一个FrameLayout, View层的事件都先经过DecorView才传递给我们的子View。
View 的绘制流程是从 ViewRoot 的 performTraversals 方法开始的,它经过 measure、layout 和 draw 三个过程才能最终将一个 View 绘制出来。
- measure方法用于测量View的宽高
- layout方法用于确定View在父容器中的位置
- draw方法负责把View绘制在屏幕上
如上图,performTraversals
会依次调用 performMeasure
、performLayout
和 performDraw
三个方法,这三个方法分别完成顶级 View 的 measure
、layout
和 draw
这三大流程,其中在 performMeasure中会调用 measure 方法,在 measure方法中又会调用onMeasure方法,在 onMeasure 方法中则会对所有的子元素进行 measure 过程,这个时候 measure 流程就从父容器传递到子元素中了,这样就完成了一次 measure过程。接着子元素会重复父容器的 measure 过程,如此反复就完成了整个 View 树的遍历。
同理,performLayout
和 performDraw
的传递流程和 performMeasure
是类似的,唯一不同的是,performDraw
的传递过程是在 draw
方法中通过 dispatchDraw
来实现的,不过这并没有本质区别。
MeasureSpec
MeasureSpec在很大程度上决定了一个View的尺寸规格,之所以说很大程度上是因为这个过程还受父容器的影响。在测量过程中,系统会将View的LayoutParams根据父容器所施加的转换规则转换为对应的MeasureSpec,然后再根据这个MeasureSprc来测量出View的宽/高。
1.什么是MeasureSpec?
MeasureSpec代表一个32位的int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(在某种测量模式下的规格大小)。SpecMode和SpecSize都是int值,一组SpecMode和SpecSize可以打包成一个MeasureSpec,一个MeasureSpec也可以通过解包得到其对应的SpecMode和SpecSize:
//MeasureSpec源码——部分重点代码
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
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;
//将 size和mode打包成一个MeasureSpec
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
//MeasureSpec解包出mode
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
//MeasureSpec解包出size
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
由上述代码可以看出,SpecMode有三类:
- UNSPECIFIED,父容器不对View有任何限制,要多大给到大,这种情况一般用于系统内部,表示一种测量出状态。
- EXACTLY,父容器已经检测出View的精确的大小,这时候View的最终大小就是SpecSize确定的值,它对应LayoutParams中的match_parent和具体数值这两种模式。
- AT_MOST,父容器指定一个可用大小即SpecSize,View的大小不能大于这个值,它对应LayoutParent中的wrap_content。
2.MeasureSpec与LayoutParams的关系
在View测量的时候,系统会将LayoutParams在父容器的约束下转换为对应的MeasureSpec,然后在根据这个MeasureSpec来确定View测量后的宽\高。
MeasureSpec不是唯一有LayoutParams决定的,而是需要LayoutParams和父容器一起决定View的MeasureSpec,进而决定View的宽/高。
对于顶级View(DecorView) 和 普通View来说,MeaSureSpec的转换过程略有不同。
对于顶级View
对于DecorView,其MeasureSpec由窗口的尺寸和自身的LayoutParams来共同决定。
ViewRootImpl类的measureHierarchy方法中的一段代码展示了DecorView的MeasureSpec创建过程。其中desiredWindowWidth,desiredWindowHeight是屏幕尺寸,lp.width和lp.height是顶级View对应LayoutParams中设置的值:
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
接着我们看看getRootMeasureSpec
的实现:
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT: //精确模式,大小就是窗口的大小
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT: //最大模式,大小不定,但不能超酷窗口大小
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default: //精确模式,大小为LayoutParams中指定的大小
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
可以看出顶级View中的MeasureSpec就是根据自身的LayoutParams中的宽/高参数来决定的。
对于普通View
对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定,其MeasureSpec一旦确定,onMeasure中就可以确定其测量的宽/高。
已知View的measure过程是由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);
}
有上述代码可以看出子元素MeasureSpec的确定不止和父元素的MeasureSpec和自身的LayoutParams有关,还和View的margin和padding有关。
我们在看看具体是如何创建一个MeasureSpec的,这句需要看看ViewGroup中的getChildMeasureSpec
方法:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec); //父容器的specSize
/*
padding是指父容器中已占用的空间大小,因此子元素可用的大小为父容器的
尺寸减去padding
*/
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
//父容器的specMode
//可以看出父容器的specMode不同,结果不同
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//childLayoutParams是具体数值,dp/px
resultSize = childDimension;