View的三大流程
基本概念
在了解View的三大流程之前,需要先了解一些和基础概念,才能更好的了解View的measure
、layout
和draw
过程。
以下先了解ViewRoot
和DecorView
的概念。
ViewRoot
ViewRoot
对应ViewRootImpl
类,它是连接WindowManager
和DecorView
的纽带,View的三大流程均是通过ViewRoot
来完成的。在ActivityThread
中,当ActivityThread
中,当Activity
对象创建完毕后,会将DecorView
添加到Window
中,同时创建ViewRootImpl
对象,并将ViewRootImpl
和DecorView
建立关联。代码大致如下。
root = new ViewRootImpl(view.getContext(), diaplay);
root.setView(view,wparams, panelParentView);
View的绘制流程是从ViewRoot
的performTraversals
方法开始的,它经过measure
、layout
和draw
三个过程才能最终将一个View绘制出来,而measure
用来测量View的宽和高,layout
用于测定View在父容器中的放置位置,而draw
则负责将View绘制到屏幕上,而performTraversals
的大致流程如下。
如上图所示,performTraversals
会依次调用performMeasure
,performLayout
,performDraw
三个方法,这三个方法会分别完成顶级View的measure
,layout
和draw
这三大流程。其中,performMeasure
会调用measure
方法,而在measure
方法中又会调用onMeasure
方法,在onMeasure
中则会对所有的子元素进行measure
过程,这个时候measure
流程就从父元素传递到子元素中,这样就完成依次measure
过程。接着子元素会重复父元素的measure
过程,如此反复便完成了整个View树的遍历。同理,performLayout
和performDraw
的传递过程也是类似的,而唯一不同的是,performDraw
的传递过程是在draw
方法中通过dispatchDraw
完成的,不过这没有本质区别。
measure
过程决定了View的宽高,Measure完成后,可以通过getMeasureWidth
和getMeasureHeight
来获取View测量后的宽高,而在几乎所有的情况下它都等同于View最终的宽高,但也有特殊情况。
layout
过程决定了View的四个顶点的坐标和实际的View宽高,Layout完成之后,可以通过getTop
、getBottom
、getLeft
、getRight
来拿到View四个顶点的位置,可以通过getWidth
和getHeight
来拿到View的最终宽高。
draw
过程决定了View的显示,只有draw
方法完成了之后View的内容才能显示在屏幕上。
DecorView
DecorView的结构DecorView作为一个顶级View,一般情况下它内部会包含一个竖直方向的LinearLayout
,在这个LinearLayout
里面有上下两部分(具体情况要看Android的版本及其应用的主题),上面是标题栏,下面是内容栏。在Activity
中我们通过setContentView
所设置的布局文件其实就是被加到内容栏之中的,而内容栏的id是android.R.id.content,所以Android的设置布局是setContentView
而不是setView
。而通过源码可知,DecorView
其实是一个FrameLayout
,View层的所有事件都经过DecorView
,之后才传递给我们的View。
理解MeasureSpec
在了解View的测量过程前,还需要理解MeasureSpec
。MeasureSpec
在很大的程度上决定了一个View的尺寸规格,而这个过程还同时受到父View的影响,因为父容器会影响子View的MeasureSpec
的创建过程。在测量过程中,系统会将View的LayoutParams
根据父容器所施加的规则转化为对应的MeasureSpec
,然后再根据这个measureSpec
来测出对应的view宽高,而这个宽高是测量宽高,不一定等同View的最终宽高。
MeasureSpec
MeasureSpec
代表一个32位的int
值,高2位代表SpecMode
,低30位代表SpecSize
,其中SpecMode
代表的是测量模式,而SpecSize
是指在某种测量模式下的规格大小。以下是MeasureSpec
内部的一些常量的定义,便于理解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;
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);
}
}
@MeasureSpecMode
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
从以上代码中可以看见,MeasureSpec
通过将SpecMode
和SpecSize
打包成一个int
值来避免过多的对象内存分配工作,并为了方便操作,提供了打包和解包方法。SpecMode
和SpceSize
也是一个int
值,一组SpecMode
和SpceSize
可以打包为MeasureSpec
,也可以通过解包获取。需要注意的是这里指的是MeasureSpec
的int值而并非其本身。
SpecMode
SpecMode
有三类,每一类都代表着特殊的含义,如下所示。
UNSPECIFED
父容器不对View有任何限制,想要多大就给多大,一般用于系统内部,表示一种测量的状态。
EXACTLY
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpceSize
所指定的值。它对应于LayoutParams
中的match_parent
和具体的数值两种模式。
AT_MOST
父容器指定一个可用大小的即SpecSize
,View的大小不能超过这个值,具体值要根据View的情况来判定,它对应于LayoutParams
中的wrap_content
。
Measure与LayoutParams的关系
系统内部是通过MeasureSpec
来进行View的测量,但是正常情况下,我们使用View指定MeasureSpec
。尽管如此,但是我们可以给View设置LayoutParams
。在View测量的时候,系统会将LayoutParams
在父容器的约束下转换成对应的MeasureSpec
,然后再根据这个MeasureSpec
来确定View测量后的宽高。需要注意速度是,MeasureSpec
不是唯一由LayoutParams
决定的,LayoutParams
需要和父容器一起才能决定View的MeasureSpec
,从而进一步决定View的宽高。另外,对于DecorView
和普通View来说,MeasureSpec
的转换过程略有不同。对于DecorView
,其MeasureSpec
由窗口的尺寸及其自身的LayoutParams
共同决定;而对于普通View,其MeasureSpec
由父容器的MeasureSpec
和自身的的LayoutParams
共同决定。
而MeasureSpec
一旦确定后,onMeasure
就可以确定View的测量宽高了。
对于DecorView
来说,在ViewRootImpl
的measureierarhy
有一段代码中有如下的一段代码,它展示了DecorView
的MeasureSpec
的创建过程,其中baseSize
和desiredWindowHeight
是屏幕的尺寸。
childWidthMeasureSpec = getRootMeasureSpec(baseSize, 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:
// 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;
}
上诉代码中,DecorView
的MesureSpec
产生过程就很明确了,根据LayoutParams
中的宽高参数来划分。
-
LayoutParams.MATCH_PARENT
:精确模式,大小就是窗口大小。 -
LayoutParams.WRAP_CONTENT
:最大模式,大小不能超过窗口大小。 - 固定大小(比如100dp):精确模式,大小为
LayoutParams
中指定的大小。
对于普通View来说,这里指的是我们布局中年的View,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);
}
上面的方法中会对子元素进行measure
,在调用子元素的measure
方法之前会先通过getChildMeasureSpec
来获取子元素的MeasureSpec
。从代码上看,子元素的MeasureSpec
与父容器的MeasureSpec
和子元素本身的LayoutParams
有关,此外还和View的margin
和padding
有关。具体的ViewGroup
的getChildMeasureSpec
代码如下。
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) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
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;
} 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);
}
上述代码中,主要作用是根据父容器的MeasureSpec
同时结合View本身的LayoutParams
来确定子元素的MeasureSpec
,参数中的padding
是指父容器已经占用的空间大小,因此子元素可用的大小为父容器的尺寸减去padding,具体代码如下。
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, spec - padding)
getChildMeasureSpec
清楚地展示了普通View的MeasureSpec
的创建规则,以下是getChildMeasureSpec
的逻辑表。其中parentSize是指父容器可用的大小。
结合之前分析和上表,其中,对于普通View,其MeasureSpec
由父容器的MeasureSpec
和自身的LayoutParams
来共同决定,那么针对不同的父容器和View本身不同的LayoutParams
,View就可以有多种MeasureSpec
。简而言之,当View采用固定宽高的时候, 不管其父容器的MeasureSpec
是什么,View的MeasureSpec
都是精确模式并且其大小是父容器的剩余空间;其他情况下,如果其父容器是最大模式,那么View也是最大模式并且不会超过父容器的剩余空间。当View的宽高是wrap_content
时,不过父容器的模式是精准还是最大化,View的模式总是最大化并且大小不能超过父容器的剩余空间。
UNSPECIFIED
模式主要用于系统内部多次Measure
的情形。
网友评论