DecorView 和 ViewRoot
DecorView 就是整个Window窗口的最顶层View。
ViewRoot 对应于 ViewRootImpl 类,是连接 WindowManager 和 DecorView 的纽带。
ActivityThread 中当 activity对象被创建好后,会将DecorView加入到Window 中同时完成 ViewRootImpl 的创建并建立和DecorView 的联系。
DecorView是怎么添加到窗口的呢?这时候我们不得不从Activity是怎么启动的说起。
当Activity初始化Window,并将布局添加到 PhoneWindow的内部类 DecorView 类之后,ActivityThread 类会调用 handleResumeActivity() 将顶层视图DecorView添加到PhoneWindow窗口。来看看handlerResumeActivity() 的实现:
class ActivityThread{
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
..................
//通知Activity组件,让Activity的onResume被调用
ActivityClientRecord r = performResumeActivity(token, clearHide);
if (r.window == null && !a.mFinished && willBeVisible) {
//获得当前Activity的PhoneWindow对象
r.window = r.activity.getWindow();
//获得当前phoneWindow内部类DecorView对象
View decor = r.window.getDecorView();
//设置窗口顶层视图DecorView可见度
decor.setVisibility(View.INVISIBLE);
//得当当前Activity的WindowManagerImpl对象
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient) {
//标记根布局DecorView已经添加到窗口
a.mWindowAdded = true;
//将根布局DecorView添加到当前Activity的窗口上面
wm.addView(decor, l);
.....................
}
}
经过一系列方法调用,将 DecorView添加到窗口window,并开始绘制view的过程(如下图):
View绘制是从根节点(Activity中就是DecorView)开始,是一个自上而下的过程。
View的绘制经历三个过程:Measure、Layout、Draw。基本流程如下图:
既然如此,我们先从 performTraversals()源码看起 (traversal:遍历)
private void performTraversals() {
final View host = mView; //mView就是setView()中传来DecorView对象
...
host.measure(childWidthMeasureSpec, childHeightMeasureSpec); //测量
...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); //摆放
...
draw(fullRedrawNeeded); //绘制
}
可以看出,绘制过程跟流程图所展示的一模一样。
Mesure过程
Measure的作用就是计算视图大小。
一图概括measure过程:
View中的相关方法主要有3个:
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
调用关系: measure → onMeasure→ setMeasureDimension;
measure,setMeasureDimension是final类型,view的子类不需要重写,只有onMeasure可被重写。
- measure()
widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽高的规格和大小(mode & size)。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
onMeasure(widthMeasureSpec, heightMessureSpec);//measure调用自身的onMeasure方法
//如果在上面的onMeasure中没有用setMeasuredDimension设置视图的宽高,就抛异常
if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {
throw new IllegalStateException("onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
...
}
根据measure方法源码,如果要重写onMeasure,需要要调用setMeasuredDimension(或者super.onMeasure()即View.onMeasure)来设置自身的mMeasuredWidth和mMeasuredHeight,否则,就会抛出异常.
- View. onMeasure()
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//设置view的宽高值; getDefaultSize()获取视图大小,这里传入默认最小宽高
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
getSuggestedMinimumWidth()怎么获取默认的最小宽高?
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
返回的是View属性当中的最小值,也就是
android:minWidth 和
android:minHeight 的值,前提是View没有设置背景属性。否则就在最小值和背景的最小值中间取最大值。
setMeasuredDimension() 用来设置view的宽高(即,设置自身的mMeasuredWidth和mMeasuredHeight):
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= MEASURED_DIMENSION_SET;
}
再看一下onMeasure()中用到的 getDefaultSize()
public static int getDefaultSize(int size, int measureSpec) {
int result = size;//在onMeasure中size传递的是android:minWidth或minHeight
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) { //view的specMode参与决定view的size
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//AT_MOST和EXACTLY时,宽高为父容器测量子view得出的size
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
这里用到了MeasureSpec类
|— 在测量的过程中MeasureSpec类贯穿全程,了解View的测量过程,最合适的切入点就是MeasureSpec。
|— 自定义view复写 onMeasure()时,常常用到该类的方法:getMode()、getSize()、makeMeasureSpec()
/** MeasureSpec用1个二进制数同时记录mode和size,减少内存分配而提高性能 */
public static class MeasureSpec {
//int长度为32位,所以进位30位就是要使用int的最高位和第二高位也就是32和31位做标志位
private static final int MODE_SHIFT = 30;
/**运算遮罩,0x3为16进制,10进制为3;二进制为11,3向左进位30,就是11 0...00(11后跟30个0)
* 遮罩的作用是用1标注需要的值,0标注不要的值。
* 因为1与任何数做与运算都得任何数(1&1=1,1&0=0);0与任何数做与运算都得0
*/
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// 0向左进位30,就是00 0...00(00后跟30个0)
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// 1向左进位30,就是01 00...0(01后跟30个0)
public static final int EXACTLY = 1 << MODE_SHIFT;
// 2向左进位30,就是10 00...0(10后跟30个0)
public static final int AT_MOST = 2 << MODE_SHIFT;
//measureSpec=size+mode;得到一个32位的二进制数,最高2位代表mode,其他30位代表size
//例如 size=100(即4的二进制值),mode=AT_MOST,则measureSpec=100+100...00=100..00100
public static int makeMeasureSpec(int size, int mode) {
return size + mode;
}
// mode = measureSpec & MODE_MASK
//原理:MODE_MASK = 11 0...0(30个0),1&N=N(N为0或1),0&N=0
//即,用MODE_MASK后30位的0替换掉measureSpec后30位中的1,只保留32和31位的mode值
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
// size = measureSpec & ~MODE_MASK
//原理同上,MODE_MASK取反等于 00 11...11(30个1),运算后保留前30位的size值
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
MeasureSpec 的值由 specSize和specMode共同组成的,其中specSize表示大小,specMode表示模式。
MODE_MASK 是长度为30的二进制数,前两位标示Mode,后面的标示Size。
specMode 一共有三种模式:
1. EXACTLY
表示父容器已经检测出子View所需要的精确大小。在该模式下,View的测量大小即为specSize。
2. AT_MOST
表示父容器未能检测出子View所需要的精确大小,但是指定了一个可用大小即specSize 。在该模式下,子View的测量大小不能超过SpecSize。
子view最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。
3. UNSPECIFIED
父容器不对子View的大小做限制.
这种模式一般用作Android系统内部,或者ListView和ScrollView等滑动控件,在此不做讨论。这种情况比较少见,不太会用到。
系统给measure方法传入了 widthMeasureSpec 和 heightMeasureSpec,这两个值又是从哪里得到的呢?
通常情况下,这两个值都是由父视图经过计算后传递给子视图的,说明父视图会在一定程度上决定子视图的大小。
但是最外层的根视图,它的 widthMeasureSpec 和heightMeasureSpec 又是从哪里得到的呢?
这就需要去分析ViewRoot中的源码了,观察 performTraversals()方法可以发现如下代码:
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
可以看到,这里调用了 getRootMeasureSpec() 去获取widthMeasureSpec 和heightMeasureSpec 的值,
注意方法中传入的参数,其中 lp.width 和 lp.height 在创建ViewGroup实例的时候就被赋值了,它们都等于 MATCH_PARENT。
然后看下 getRootMeasureSpec() 中的代码,如下所示:
private 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:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
可以看到,这里使用了 MeasureSpec.makeMeasureSpec() 方法来组装一个MeasureSpec:
- 当 rootDimension = MATCH_PARENT 或 精确值 的时候,MeasureSpec的 specMode就等于EXACTLY,
- 当 rootDimension = WRAP_CONTENT 的时候,MeasureSpec的 specMode 就等于 AT_MOST。
另外,MATCH_PARENT和WRAP_CONTENT时的specSize都是等于windowSize的,也就意味着根视图总是会充满全屏的。
ViewGroup类没有onMeasure方法,其子类会各自复写View类的onMeasure()。
DecorView是继承自FrameLayout的,那么我们来看看FrameLayout类中的onMeasure方法的实现:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
..............
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
//遍历子view
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//测量FrameLayout下每个子视图View的宽和高
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
......
//设置当前FrameLayout测量结果,此方法的调用表示当前View测量的结束。
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
}
为什么说父View会影响子View的MeasureSpec的生成?来看上面用到的
ViewGroup.measureChildWithMargins() :
/**
* parentWidthMeasureSpec和parentHeigthMeasureSpec表示父容器的宽高的MeasureSpec
* widthUsed和heightUsed表示父容器在水平(竖直)方向已经占用的大小
*/
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// mPaddingLeft和mPaddingRight表示父容器左右两内侧的padding
// lp.leftMargin和 lp.rightMargin表示子View左右两外侧的margin
// 这些值相加得到父容器在水平方向已经占用的空间
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
//同理,mPaddingTop + mPaddingBottom +...表示父容器在竖直方向已经占用的空间
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
该方法中调用
getChildMeasureSpec方法来获得ViewGroup下的子视图的测量规格。然后将测量规格作为参数传递给View的measure方法,最终完成所有子视图View的测量。
我们来看getChildMeasureSpec() :
/**
* 在measureChildren中最难的部分:找出传递给child的MeasureSpec。
* 目的是结合父view的MeasureSpec与子view的LayoutParams信息去找到最好的结果
* (也就是说子view的确切大小由两方面共同决定:1.父view的MeasureSpec 2.子view的LayoutParams属性)
*
* @param spec 父view经过makeMeasureSpec计算的详细测量值(MeasureSpec)
* @param padding view当前尺寸的的内边距和外边距(padding,margin)
* @param childDimension 子view在当前尺寸下的布局参数宽高值(LayoutParam.width,height)
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//1.从父容器的MeasureSpec获取mode和size
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//2.得到父容器在水平方向或垂直方向剩余可用的宽高值(父的宽高减去已占用大小)
int size = Math.max(0, specSize - padding);
//子view想要的实际大小和模式(需要计算)
int resultSize = 0;
int resultMode = 0;
//3.根据父容器的MeasureSpec和子view的LayoutParams来确定子view的mode和size
switch (specMode) { //父
// 当父容器的模式为EXACITY时,父view强加给子view确切的值
case MeasureSpec.EXACTLY: //MATCH_PARENT或dp值
// 当子view的LayoutParams>0也就是有确切的值,而不是match_parent或wrap_content
if (childDimension >= 0) {
//子view大小为子自身所赋的值,模式大小为EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
// 当子view的LayoutParams为MATCH_PARENT时(-1)
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子view大小为父view大小,模式为EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
// 当子view的LayoutParams为WRAP_CONTENT时(-2)
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子view决定自己的大小,但最大不能超过父view,模式为AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 当父容器的模式为AT_MOST时,父view强加给子view一个最大的值。
case MeasureSpec.AT_MOST: //WRAP_CONTENT
// 道理同上
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 当父容器的模式为UNSPECIFIED时,子view为想要的值
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// 子view大小为子自身所赋的值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
从 getChildMeasureSpec() 的第一个参数spec和第二个参数padding也可以看出:
父容器的MeasureSpec和子View的LayoutParams共同决定了子View的MeasureSpec!
明白了该方法的参数,我们再来看方法的具体实现步骤。
第一步: 得到父容器的specMode和specSize;
第二步: 得到父容器在水平方向或垂直方向可用的最大空间值;
第三步: 确定子View的specMode和specSize。
在第三步出现了一个很关键的switch语句,在此根据父容器的specMode的不同来决定子View的specMode和specSize.
- 情况1: 父容器的specMode为MeasureSpec.EXACTLY
我们首先看到一个if判断 if (childDimension >= 0) ,childDimension是之前父容器传来的子View.getLayoutParams拿到的宽高
或许看到这有点懵了:childDimension>=0是啥意思?难道还有小于0的情况?是的,请注意两个系统常量:
LayoutParams.MATCH_PARENT=-1 和 LayoutParams.WRAP_CONTENT=-2
所以这里表示子View的宽或高不是match_parent,也不是wrap_content而是一个具体的数值,比如100px。
那么:子View的size就是childDimension即我们设置给它的px值,子View的mode也为MeasureSpec.EXACTLY
看完这个if,我们来看第一个else if: else if (childDimension == LayoutParams.MATCH_PARENT)
表示当子View的宽或高是LayoutParams.MATCH_PARENT,
子View的size就是父容器在水平方向或垂直方向剩余可用的最大空间值即size(size是第二步得出的值),子View的mode也为MeasureSpec.EXACTLY。
我们来看第二个else if: else if (childDimension == LayoutParams.WRAP_CONTENT)
表示子View的宽或高是LayoutParams.WRAP_CONTENT,
子View的size就是父容器在水平方向或垂直方向剩余可用的最大空间值即size,子View的mode为MeasureSpec.AT_MOST。
- 情况2:父容器的specMode为MeasureSpec.AT_MOST
还是先看这个if判断: if (childDimension >= 0)
表示子View的宽或高不是match_parent,也不是wrap_content而是一个具体的数值,比如100px。
那么:子View的size就是childDimension,子View的mode也为MeasureSpec.EXACTLY。
继续看第一个else if: else if (childDimension == LayoutParams.MATCH_PARENT)
表示子View的宽或高是LayoutParams.MATCH_PARENT,
那么:子View的size就是父容器在水平方向或垂直方向可用的最大空间值即size,子View的mode也为MeasureSpec.AT_MOST。
接着看第二个else if: else if (childDimension == LayoutParams.WRAP_CONTENT)
表示子View的宽或高是LayoutParams.WRAP_CONTENT。
那么:子View的size就是父容器在水平方向或垂直方向可用的最大空间值即size,子View的mode也为MeasureSpec.AT_MOST。
- 情况3:父容器的specMode为MeasureSpec.UNSPECIFIED
还是先看这个if判断 if (childDimension >= 0)
表示子View的宽或高不是match_parent,也不是wrap_content而是一个具体的数值,比如100px。
那么:子View的size就是childDimension,子View的mode也为MeasureSpec.EXACTLY。
继续看第一个else if: else if (childDimension == LayoutParams.MATCH_PARENT)
表示子View的宽或高是LayoutParams.MATCH_PARENT。
那么:子View的size为0,子View的mode为MeasureSpec.UNSPECIFIED
接着看第二个else if: else if (childDimension == LayoutParams.WRAP_CONTENT)
表示子View的宽或高是LayoutParams.WRAP_CONTENT。
那么:子View的size为0,子View的mode为MeasureSpec.UNSPECIFIED。
至此,我们可以清楚地看到:
子View的MeasureSpec由其父容器的MeasureSpec和该子View本身的布局参数LayoutParams共同决定。
在此经过测量得出的子View的MeasureSpec是系统给出的一个期望值(参考值),我们也可摒弃系统的这个测量流程,直接调用setMeasuredDimension( )设置子View的宽和高的测量值。
对于以上的分析可用表格来规整各一下MeasureSpec的生成:
为了便于理解和记忆,再用大白话再对该图进行详细的描述:
1. 当子View采用具体的值(如100px)时,不管父容器的MeasureSpec取何值,系统都会把该子View的specMode设为MeasureSpec.EXACTLY,把specSize设为子View自己指定的大小(childSize)。
2. 当子View的LayoutParams的宽(高)采用match_parent时:系统返回给该子View的specMode就为父容器的specMode,系统返回给该子View的specSize就为该父容器剩余空间的大小(parentLeftSize)。(暂不考虑unspecified情况)
换句话说,子View的大小很明确,就是要和父容器一样大,specMode也一样;更加直白地说就是父容器要怎样子View就要怎样。
3. 当子View的宽(高)为wrap_content是,系统返回给该子View的specMode就为AT_MOST,系统返回给该子View的specSize就为该父容器剩余空间的大小(parentLeftSize)。(暂不考虑unspecified情况)
补充说明:
自定义View在重写onMeasure()的过程中要处理View的宽或高为wrap_content的情况(请参见下图中的绿色标记部分)
我们该怎么处理呢?
情况1:
如果在xml布局中View的宽高均为wrap_content.那么需要设置View的宽高为默认宽高,即 mWidth和mHeight.
情况2:
如果在xml布局中View的宽和高其中一个为wrap_content,那么就将该值设置为默认的宽或高,另外的一个值采用系统测量的specSize即可.
具体的实现可以这样做:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec , heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpceSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode=MeasureSpec.getMode(heightMeasureSpec);
int heightSpceSize=MeasureSpec.getSize(heightMeasureSpec);
// view的宽高都为wrap_content时
if(widthSpecMode==MeasureSpec.AT_MOST&&heightSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth, mHeight);
}else if(widthSpecMode==MeasureSpec.AT_MOST){ //只有宽为wrap_content时
setMeasuredDimension(mWidth, heightSpceSize);
}else if(heightSpecMode==MeasureSpec.AT_MOST){ //只有高为wrap_content时
setMeasuredDimension(widthSpceSize, mHeight);
}
}
至此,整个View树型结构的布局测量流程可以归纳如下:
Measure总结
- measure方法是final类型的,子类不可以重写;子类可重写onMeasure方法来测量自己的大小:使用setMeasuredDimension或者super.onMeasure()。
- View测量结束的标志是调用了View类中的setMeasuredDimension成员方法。
- 在Activity生命周期onCreate和onResume中调用 View.getWidth() 和View.getMeasuredHeight() 返回值都为0,是因为当前View的测量还没有开始,这里关系到Activity启动过程,文章开头说了当ActivityThread类中的performResumeActivity() 会触发Activity执行onResume(), 执行之后才将DecorView添加到PhoneWindow窗口上,开始测量。在Activity生命周期onCreate中performResumeActivity还未执行,因此调用View.getMeasuredHeight() 返回值为0。
- 子视图View的大小是由父容器View和子视图View布局共同决定的。
Layout过程
回看performTraversals()源码,调用了layout() :
private void performTraversals() {
..................
//DecorView请求布局
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
..................
}
给DecorView传入了屏幕宽高,所以DecorView根布局是充满整个屏幕的。
来看View.layout()源码:
public void layout(int l, int t, int r, int b) {
//判断是否需要重新测量
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
//保存上一次View的四个位置,传递给下面的布局变化监听器的onLayoutChange方法
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//设置当前视图View的左,顶,右,底的位置,并且判断布局是否有改变
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//如果布局有改变,条件成立,则视图View重新布局
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//调用onLayout,将具体布局逻辑留给子类实现
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {//监听布局变化
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
代码第13-14行设置当前View的布局位置,也就是当调用了
setFrame(l, t, r, b)方法之后,当前View布局基本完成,
既然这样为什么还要第18行 onLayout方法呢?稍后解答,这里来分析一下setFrame是怎么设置当前View的布局位置的。
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
//当上,下,左,右四个位置有一个和上次的值不一样都会重新布局
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;
//得到本次和上次的宽和高
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
//判断本次View的宽高和上次View的宽高是否相等
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
//清除上次布局的位置
invalidate(sizeChanged);
//保存当前View的最新位置
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
mPrivateFlags |= PFLAG_HAS_BOUNDS;
//如果当前View的尺寸有所变化
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
...............
return changed;
}
1.代码第4行,如果当前View视图的最新位置和上一次不一样时,则View会重新布局。
2.代码第19-25行,保存当前View的最新位置,到此当前View的布局基本结束。从这里我们可以看到,四个全局变量 mLeft,mTop,mRight,mBottom在此刻赋值,联想我们平时使用的View.getWidth()获得View的宽高,你可以发现,其实View. getWidth()方法的实现如下:
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
以上两个方法是获得View布局时候的宽高,mLeft、mRight等几个变量都是在
layout()中才赋值,
因此,只有在View 布局完之后调用View.getWidth()、getHeight()才能真正获取到准确的宽高值。
补充:另一种获取宽高的方法:getMeasuredWidth()、getMeasuredHeight(),顾名思义,要在measure完了之后就可以获取宽高(setMeasuredDimesion里赋值)
上面提到,layout方法调用了setFrame(),也就是当前View的布局结束了,那么View中的onLayout方法又是干嘛的呢?
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
发现onLayout是一个空方法,为什么会这样呢?
因为onLayout()过程是为了确定view在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置。
既然如此,我们来看下ViewGroup中的onLayout()方法是怎么写的吧,代码如下:
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
可以看到,
ViewGroup.onLayout() 是一个抽象方法,这就意味着所有ViewGroup的子类都必须重写这个方法。没错,像LinearLayout、RelativeLayout等布局,都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的。
DecorView是继承自FrameLayout的,那么进入FarmeLayout类中看看 onLayout方法的实现吧:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false);
}
void layoutChildren(int left, int top, int right, int bottom,
boolean forceLeftGravity) {
final int count = getChildCount();
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
mForegroundBoundsChanged = true;
//遍历当前FrameLayout下的子View,调用子view的layout方法进行布局
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
//当子View的Visibility=GONE时,不对它进行布局,所以设置为GONE时,该view是不占据空间的。
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//获得子视图View的宽高
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
//一下代码获得子视图View的四个位置,用于子视图View布局。
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
//子视图布局
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
在FrameLayout中的
onLayout()中仅仅是调用了
layoutChildren(),从该方法名称我们不难看出,原来该方法的作用是给子View进行布局的。
第21行,对每个子View的可见度进行了判断,如果visibility=GONE,则当前子View不进行布局,这也就是为什么可见度为GONE时是不占据屏幕空间的,而其他两种VISIBLE和INVISIBLE是占据屏幕空间的。
到此,我们的布局流程可以用如下图表示:
Layout总结
- 视图View的布局逻辑是由父View,也就是ViewGroup容器布局来实现的。因此,我们如果自定义View一般都无需重写onLayout方法,但是如果自定义一个ViewGroup容器的话,就必须实现onLayout方法,因为该方法在ViewGroup是抽象的,所有ViewGroup的所有子类必须实现onLayout方法。
- View在布局中使用 android:visibility=”gone” 属性时,是不占据屏幕空间的。
- 必须在View布局完之后调用getHeight()和getWidth()方法才能获取到view的准确宽高。
Draw流程
View.draw() 的源码很长,这里只用一张流程图概括它的处理逻辑:
直接看第③步,绘制View视图内容,调用了 onDraw(),来看它的源码:
// Implement this to do your drawing.
protected void onDraw(Canvas canvas) {
}
该方法体里面是一个空实现,也就是视图View将绘制的逻辑留给继承它的子类去实现,
这也就是为什么我们在自定义View的时候必须去实现其父类的onDraw方法来完成自己对内容的绘制。
看第④步,绘制子视图内容,调用了 dispatchDraw(),来看它的源码:
/**
* Called by draw to draw the child views. This may be overridden
* by derived classes to gain control just before its children are drawn
* (but after its own view has been drawn).
* @param canvas the canvas on which to draw the view
*/
protected void dispatchDraw(Canvas canvas) {
}
这也是一个空实现,那么其实现的逻辑也会在它的子类中实现了;
由于只有ViewGroup容器才有其子视图,因此,该方法的实现应该在ViewGroup类中,我们进入ViewGroup类中看其源码如下:
@Override
protected void dispatchDraw(Canvas canvas) {
boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
int flags = mGroupFlags;
//判断当前ViewGroup容器是否设置的布局动画
if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;
final boolean buildCache = !isHardwareAccelerated();
//遍历给每个子视图View设置动画效果
for (int i = 0; i < childrenCount; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
final LayoutParams params = child.getLayoutParams();
attachLayoutAnimationParameters(child, params, i, childrenCount);
bindLayoutAnimation(child);
if (cache) {
child.setDrawingCacheEnabled(true);
if (buildCache) {
child.buildDrawingCache(true);
}
}
}
}
//获得布局动画的控制器
final LayoutAnimationController controller = mLayoutAnimationController;
if (controller.willOverlap()) {
mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
}
//开始布局动画
controller.start();
mGroupFlags &= ~FLAG_RUN_ANIMATION;
mGroupFlags &= ~FLAG_ANIMATION_DONE;
if (cache) {
mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE;
}
//设置布局动画的监听事件
if (mAnimationListener != null) {
mAnimationListener.onAnimationStart(controller.getAnimation());
}
}
int clipSaveCount = 0;
//是否需要剪裁边距
final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
if (clipToPadding) {
clipSaveCount = canvas.save();
//对画布进行边距剪裁
canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
mScrollX + mRight - mLeft - mPaddingRight,
mScrollY + mBottom - mTop - mPaddingBottom);
}
......
//遍历绘制当前视图的子视图View
for (int i = 0; i < childrenCount; i++) {
int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
if (preorderedList != null) preorderedList.clear();
......
//更新布局动画的监听事件
if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 &&
mLayoutAnimationController.isDone() && !more) {
// We want to erase the drawing cache and notify the listener after the
// next frame is drawn because one extra invalidate() is caused by
// drawChild() after the animation is over
mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;
final Runnable end = new Runnable() {
public void run() {
notifyAnimationListener();
}
};
post(end);
}
}
遍历绘制当前容器布局ViewGroup的子视图时,调用了成员方法
drawChild() 来完成子视图的绘制。它的源码如下:
/**
* Draw one child of this View Group. This method is responsible for getting
* the canvas in the right state. This includes clipping, translating so
* that the child's scrolled origin is at 0, 0, and applying any animation
* transformations.
*
* @param canvas The canvas on which to draw the child
* @param child Who to draw
* @param drawingTime The time at which draw is occurring
* @return True if an invalidate() was issued
*/
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
此处又调用
View.draw() 来绘制视图,因此形成了一个嵌套调用,直到所有的子视图View绘制结束。到此关于视图View绘制已经基本完成。
下一篇: View的状态、重绘
参考文章:
3. 自定义View系列教程02--onMeasure源码详尽分析