View绘制过程

抄的《Android开发艺术探索》第四章
ViewRoot对应于ViewRootImpl类,它是连接WindowManagerDecorView的纽带,View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联,View的绘制流程是从ViewRootperformTraversals方法开始的,它经过measurelayoutdraw三个过程才能最终将一个View绘制出来,其中measure用来测量View的宽和高,layout用来确定View在父容器中的放置位置,而draw则负责将View绘制在屏幕上。

performTraversals会依次调用performMeasure、performLayoutperformDraw三个方法,这三个方法分别完成顶级Viewmeasurelayoutdraw这三大流程,其中在performMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了一次measure过程。接着子元素会重复父容器的measure过程,如此反复就完成了整个View树的遍历。同理,performLayoutperformDraw的传递流程和performMeasure是类似的,唯一不同的是,performDraw的传递过程是在draw方法中通过dispatchDraw来实现的,不过这并没有本质区别。

measure过程决定了View的宽/高,Measure完成以后,可以通过getMeasuredWidthgetMeasuredHeight方法来获取到View测量后的宽/高,在几乎所有的情况下它都等同于View最终的宽/高,但是特殊情况除外,这点在本章后面会进行说明。Layout过程决定了View的四个顶点的坐标和实际的View的宽/高,完成以后,可以通过getTopgetBottomgetLeftgetRight来拿到View的四个顶点的位置,并可以通过getWidthgetHeight方法来拿到View的最终宽/高。Draw过程则决定了View的显示,只有draw方法完成以后View的内容才能呈现在屏幕上。

DecorView作为顶级View,一般情况下它内部会包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下两个部分(具体情况和Android版本及主题有关),上面是标题栏,下面是内容栏。在Activity中我们通过setContentView所设置的布局文件其实就是被加到内容栏之中的,而内容栏的id是content,因此可以理解为Activity指定布局的方法不叫setview而叫setContentView,因为我们的布局的确加到了idcontentFrameLayout中。如何得到content呢?可以这样:ViewGroup content= findViewById(R.android.id.content)。如何得到我们设置的View呢?可以这样:content.getChildAt(0)。同时,通过源码我们可以知道,DecorView其实是一个FrameLayoutView层的事件都先经过DecorView,然后才传递给我们的View。

MeasureSpec

MeasureSpec在很大程度上决定了一个View的尺寸规格,之所以说是很大程度上是因为这个过程还受父容器的影响,因为父容器影响View的MeasureSpec的创建过程。在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个measureSpec来测量出View的宽/高。这里的宽/高是测量宽/高,不一定等于View的最终宽/高。
MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSizeSpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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(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通过将SpecModeSpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包方法。SpecModeSpecSize也是一个int值,一组SpecModeSpecSize可以打包为一个MeasureSpec,而一个MeasureSpec可以通过解包的形式来得出其原始的SpecModeSpecSize,需要注意的是这里提到的MeasureSpec是指MeasureSpec所代表的int值,而并非MeasureSpec本身。
SpecMode有三类,每一类都表示特殊的含义,如下所示。
** UNSPECIFIED **
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
** EXACTLY **
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。
** AT_MOST **
父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。

MeasureSpec和LayoutParams

在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽/高。需要注意的是,MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定ViewMeasureSpec,从而进一步决定View的宽/高。另外,对于顶级View(即DecorView)和普通View来说,MeasureSpec的转换过程略有不同。对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同确定;对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定,MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽/高。
对于DecorView来说,在ViewRootImpl中的measureHierarchy方法中有如下一段代码,它展示了DecorViewMeasureSpec的创建过程,其中desiredWindowWidthdesiredWindowHeight是屏幕的尺寸:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth,lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight,lp.height);
performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);

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,Measure-Spec.EXACTLY);
break;
}
return measureSpec;
}

通过上述代码,DecorViewMeasureSpec的产生过程就很明确了,具体来说其遵守如下规则,根据它的LayoutParams中的宽/高的参数来划分。

  • LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小;
  • LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小;
  • 固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小。
    对于普通View来说,这里是指我们布局中的ViewViewmeasure过程由ViewGroup传递而来,先看一下ViewGroupmeasureChildWithMargins方法:
    1
    2
    3
    4
    5
    6
    protected void measureChildWithMargins(View child,int parentWidthMeasureSpec,int widthUsed,int parentHeightMeasureSpec,int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayout-Params();
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin+ widthUsed,lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeight-MeasureSpec,mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin+ heightUsed,lp.height);
    child.measure(childWidthMeasureSpec,childHeightMeasureSpec);
    }
    上述方法会对子元素进行measure,在调用子元素的measure方法之前会先通过getChildMeasureSpec方法来得到子元素的MeasureSpec。从代码来看,很显然,子元素的MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams有关,此外还和Viewmarginpadding有关,具体情况可以看一下ViewGroupgetChildMeasureSpec方法,如下所示:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    public static int getChildMeasureSpec(int spec,int padding,int child-Dimension) {
    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 = 0;
    resultMode = MeasureSpec.UNSPECIFIED;
    } else if (childDimension == LayoutParams.WRAP_CONTENT) {
    // Child wants to determine its own size.... find out how
    // big it should be
    resultSize = 0;
    resultMode = MeasureSpec.UNSPECIFIED;
    }
    break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize,resultMode);
    }
    它的主要作用是根据父容器的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec,参数中的padding是指父容器中已占用的空间大小,因此子元素可用的大小为父容器的尺寸减去padding,具体代码如下所示:
    1
    2
    int specSize = MeasureSpec.getSize(spec);
    int size = Math.max(0,specSize -padding);
    这里简单说一下,当View采用固定宽/高的时候,不管父容器的MeasureSpec是什么,ViewMeasureSpec都是精确模式并且其大小遵循Layoutparams中的大小。当View的宽/高是match_parent时,如果父容器的模式是精准模式,那么View也是精准模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。当View的宽/高是wrap_content时,不管父容器的模式是精准还是最大化,View的模式总是最大化并且大小不能超过父容器的剩余空间。在我们的分析中漏掉了UNSPECIFIED模式,那是因为这个模式主要用于系统内部多次Measure的情形,一般来说,我们不需要关注此模式。

View的工作流程

measure过程

measure过程要分情况来看,如果只是一个原始的View,那么通过measure方法就完成了其测量过程,如果是一个ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个流程。
** View的measure过程 **
Viewmeasure过程由其measure方法来完成,measure方法是一个final类型的方法,这意味着子类不能重写此方法,在Viewmeasure方法中会去调用ViewonMeasure方法,因此只需要看onMeasure的实现即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(),heightMeasureSpec));
}
public static int getDefaultSize(int size,int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

可以看出,getDefaultSize这个方法的逻辑很简单,对于我们来说,我们只需要看AT_MOSTEXACTLY这两种情况。简单地理解,其实getDefaultSize返回的大小就是measureSpec中的specSize,而这个specSize就是View测量后的大小,这里多次提到测量后的大小,是因为View最终的大小是在layout阶段确定的,所以这里必须要加以区分,但是几乎所有情况下View的测量大小和最终大小是相等的。
至于UNSPECIFIED这种情况,一般用于系统内部的测量过程,在这种情况下,View的大小为getDefaultSize的第一个参数size,即宽/高分别为getSuggestedMinimumWidthgetSuggestedMinimumHeight这两个方法的返回值,看一下它们的源码:

1
2
3
4
5
6
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight,mBackground.getMinimumHeight());
}

这里只分析getSuggestedMinimumWidth方法的实现,getSuggestedMinimumHeight和它的实现原理是一样的。从getSuggestedMinimumWidth的代码可以看出,如果View没有设置背景,那么View的宽度为mMinWidth,而mMinWidth对应于android:minWidth这个属性所指定的值,因此View的宽度即为android:minWidth属性所指定的值。这个属性如果不指定,那么mMinWidth则默认为0;如果View指定了背景,则View的宽度为max(mMinWidth,mBackground.getMinimumWidth())mMinWidth的含义我们已经知道了,那么mBackground.getMinimumWidth()是什么呢?我们看一下DrawablegetMinimumWidth方法,如下所示:

1
2
3
4
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

可以看出,getMinimumWidth返回的就是Drawable的原始宽度,前提是这个Drawable有原始宽度,否则就返回0。
这里再总结一下getSuggestedMinimumWidth的逻辑:如果View没有设置背景,那么返回android:minWidth这个属性所指定的值,这个值可以为0;如果View设置了背景,则返回android:minWidth和背景的最小宽度这两者中的最大值,getSuggestedMinimumWidthgetSuggestedMinimumHeight的返回值就是ViewUNSPECIFIED情况下的测量宽/高。
getDefaultSize方法的实现来看,View的宽/高由specSize决定,所以我们可以得出如下结论:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent
从上述代码中我们知道,如果View在布局中使用wrap_content,那么它的specModeAT_MOST模式,在这种模式下,它的宽/高等于specSize;这种情况下ViewspecSize是parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小。很显然,View的宽/高就等于父容器当前剩余的空间大小,这种效果和在布局中使用match_parent完全一致。如何解决这个问题呢?也很简单,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth,mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth,heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize,mHeight);
}
}

在上面的代码中,我们只需要给View指定一个默认的内部宽/高(mWidthmHeight),并在wrap_content时设置此宽/高即可。对于非wrap_content情形,我们沿用系统的测量值即可,至于这个默认的内部宽/高的大小如何指定,这个没有固定的依据,根据需要灵活指定即可。如果查看TextViewImageView等的源码就可以知道,针对wrap_content情形,它们的onMeasure方法均做了特殊处理。

** ViewGroup的measure过程 **
对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,因此它没有重写ViewonMeasure方法,但是它提供了一个叫measureChildren的方法,如下所示。

1
2
3
4
5
6
7
8
9
10
protected void measureChildren(int widthMeasureSpec,int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child,widthMeasureSpec,heightMeasureSpec);
}
}
}

从上述代码来看,ViewGroupmeasure时,会对每一个子元素进行measuremeasureChild这个方法的实现也很好理解,如下所示

1
2
3
4
5
6
protected void measureChild(View child,int parentWidthMeasureSpec,int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidth-MeasureSpec,mPaddingLeft + mPaddingRight,lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeight-MeasureSpec,mPaddingTop + mPaddingBottom,lp.height);
child.measure(childWidthMeasureSpec,childHeightMeasureSpec);
}

很显然,measureChild的思想就是取出子元素的LayoutParams,然后再通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给Viewmeasure方法来进行测量。我们知道,ViewGroup并没有定义其测量的具体过程,这是因为ViewGroup是一个抽象类,其测量过程的onMeasure方法需要各个子类去具体实现,比如LinearLayoutRelativeLayout等。

layout过程

Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中onLayout方法又会被调用。Layout过程和measure过程相比就简单多了,layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置,先看Viewlayout方法,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?setOpticalFrame(l,t,r,b) : setFrame(l,t,r,b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
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.mOnLayout-ChangeListeners.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;
}

layout方法的大致流程如下:首先会通过setFrame方法来设定View的四个顶点的位置,即初始化mLeftmRightmTopmBottom这四个值,View的四个顶点一旦确定,那么View在父容器中的位置也就确定了;接着会调用onLayout方法,这个方法的用途是父容器确定子元素的位置,和onMeasure方法类似,onLayout的具体实现同样和具体的布局有关,所以ViewViewGroup均没有真正实现onLayout方法。接下来,我们可以看一下LinearLayoutonLayout方法,如下所示。

1
2
3
4
5
6
7
protected void onLayout(boolean changed,int l,int t,int r,int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l,t,r,b);
} else {
layoutHorizontal(l,t,r,b);
}
}

LinearLayoutonLayout的实现逻辑和onMeasure的实现逻辑类似,这里选择layoutVertical继续讲解,为了更好地理解其逻辑,这里只给出了主要的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void layoutVertical(int left,int top,int right,int bottom) {
......
final int count = getVirtualChildCount();
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =(LinearLayout.LayoutParams) child.getLayoutParams();
......
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child,childLeft,childTop + getLocationOffset(child),childWidth,childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocation-Offset(child);
i += getChildrenSkipCount(child,i);
}
}
}

这里分析一下layoutVertical的代码逻辑,可以看到,此方法会遍历所有子元素并调用setChildFrame方法来为子元素指定对应的位置,其中childTop会逐渐增大,这就意味着后面的子元素会被放置在靠下的位置,这刚好符合竖直方向的LinearLayout的特性。至于setChildFrame,它仅仅是调用子元素的layout方法而已,这样父元素在layout方法中完成自己的定位以后,就通过onLayout方法去调用子元素的layout方法,子元素又会通过自己的layout方法来确定自己的位置,这样一层一层地传递下去就完成了整个View树的layout过程。setChildFrame方法的实现如下所示。

1
2
3
private void setChildFrame(View child,int left,int top,int width,int height) {
child.layout(left,top,left + width,top + height);
}

我们注意到,setChildFrame中的width和height实际上就是子元素的测量宽/高,从下面的代码可以看出这一点:

1
2
3
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
setChildFrame(child,childLeft,childTop + getLocationOffset(child),childWidth,childHeight);

而在layout方法中会通过setFrame去设置子元素的四个顶点的位置,在setFrame中有如下几句赋值语句,这样一来子元素的位置就确定了:

1
2
3
4
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;

View的测量宽/高和最终/宽高有什么区别?这个问题可以具体为:ViewgetMeasuredWidthgetWidth这两个方法有什么区别,至于getMeasuredHeightgetHeight的区别和前两者完全一样。为了回答这个问题,首先,我们看一下getwidth和getHeight这两个方法的具体实现:

1
2
3
4
5
6
public final int getWidth() {
return mRight -mLeft;
}
public final int getHeight() {
return mBottom -mTop;
}

getWidthgetHeight的源码再结合mLeftmRightmTopmBottom这四个变量的赋值过程来看,getWidth方法的返回值刚好就是View的测量宽度,而getHeight方法的返回值也刚好就是View的测量高度。经过上述分析,现在我们可以回答这个问题了:在View的默认实现中,View的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于Viewmeasure过程,而最终宽/高形成于Viewlayout过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。因此,在日常开发中,我们可以认为View测量宽/高等于``最终宽/高,但是的确存在某些特殊情况会导致两者不一致.

draw过程

Draw过程就比较简单了,它的作用是将View绘制到屏幕上面。View的绘制过程遵循
如下几步:

  • 绘制背景background.draw(canvas)。
  • 绘制自己(onDraw)。
  • 绘制children(dispatchDraw)。
  • 绘制装饰(onDrawScrollBars)。
    这一点通过draw方法的源码可以明显看出来,如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
    public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary,save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary,draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1,draw the background,if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3,draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4,draw the children
dispatchDraw(canvas);
// Step 6,draw decorations (scrollbars)
onDrawScrollBars(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// we're done...
return;
}
...
}

View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有子元素的draw方法,如此draw事件就一层层地传递了下去。View有一个特殊的方法setWillNotDraw,先看一下它的源码,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* If this view doesn't do any drawing on its own,set this flag to
* allow further optimizations. By default,this flag is not set on
* View,but could be set on some View subclasses such as ViewGroup.
*
* Typically,if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0,DRAW_MASK);
}

setWillNotDraw这个方法的注释中可以看出,如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统会进行相应的优化。默认情况下,View没有启用这个优化标记位,但是ViewGroup会默认启用这个优化标记位。这个标记位对实际开发的意义是:当我们的自定义控件继承于ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当然,当明确知道一个ViewGroup需要通过onDraw来绘制内容时,我们需要显式地关闭WILL_NOT_DRAW这个标记位。


以上


View绘制过程
https://blog.huangyuanlove.com/2018/07/29/View绘制过程/
作者
HuangYuan_xuan
发布于
2018年7月29日
许可协议
BY HUANG兄