目录
(4)MeasureSpec和LayoutParams的对应关系
? ? ? ?View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure、layout和draw三个过程才能最终将一个View绘制出来。
? ? ? 如图所示,performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout和draw这三大流程,其中在performMeasure中会调用measure方法,在measure方法中会调用onMeasure方法,在onMeasure方法这则会对所有的子元素进行measure过程,这时候meaure流程就从父容器传递到子元素中了,这样就完成了一次measure过程。同理,performLayout和performDraw的传递流程和performMeasure是类似的。
? ??? ? ?MeasureSpec代表一个32位int值,高2位代表SpecMode,就是测量模式,低30位代表SpecSize,就是规格大小。
- UNSPECIFIED? ? ? ?
父容器对子 View 没有施加任何限制,子 View 可以任意大小。一般用于系统内部。
- EXACTLY? ? ? ? ? ? ?
?父容器已经为子 View 精确指定了大小,子 View 应该匹配这个大小。它对应于LayoutParams中的match_parent和具体的数值这两个模式。
- AT_MOST? ? ? ? ? ? ?
?子 View 可以是任何大小,但不能超过父容器指SpecSize。它对应于LayoutParams中的wrap_content。
MeasureSpec measureSpec=MeasureSpec.makeMeasureSpec(size,mode);
? ? ? ?MeasureSpec 是通过静态方法 MeasureSpec.makeMeasureSpec() 创建的,该方法接受两个参数:大小和测量模式。
int selfwidthSpecMode=MeasureSpec.getMode(widthMeasureSpec);
int selfwidthSpecSize=MeasureSpec.getSize(widthMeasureSpec);
? ? ? ? ?我们可以通过 MeasureSpec.getMode() 和 MeasureSpec.getSize() 方法来获取测量模式和大小,然后根据这些信息来确定 View 的最终大小。
? ? ? ? 在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽/高。需要注意的是,MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec从而决定View的宽高。
MeasureSpec的转换流程:
对于顶级的View(DecorView),其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同确定。
其中LayoutParams中的宽高的参数
对于普通的View,其MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams来共同确定。
? View的工作流程主要是measure、layout、draw这三大流程,即测量、布局和绘制。
? ? ? ? 1?? ?View的measure过程
? ? ? View的measure过程由其measure方法来完成,measure方法是一个final类型的方法,子类不能重写该方法,在View的measure方法中会去调用View的onMeasure方法,View的onMeasure方法如下所示:
#onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension方法会设置View宽高的测量值。
#getDefaultSize
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;
}
AT_MOST和EXACTLY情况下,getDefaultSize方法返回值为MeasureSpec的specSize 。
UNSPECIFIED情况下,getDefaultSize方法返回值宽/高为getSuggestedMinimumWidth()和getSuggestedMinimumHeight()。
#getSuggestedMinimumWidth()和getSuggestedMinimumHeight()
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
? ? ? 如果 View 没有设置背景,那么返回android:minWidth 这个属性所指定的值,这个值可以为 0; 如果 View 设置了背景,则返回 android:minWidth 和背景的最小宽度这两者中的最大值,getSuggestedMinimumWidth和getSuggestedMinimumHeight 的返回值就是 View 在 UNSPECIFIED 情况下的测量宽/高。
? ? ? ? 2??ViewGroup的measure过程
? 对于ViewGroup来说,除了完成自己的measure过程以外,还会去遍历调用所有子元素的measure方法,各个子元素再递归去执行这个过程。ViewGroup是一个抽象类,因此它没有重写View的OnMeasure方法,提供了一个measureChildren方法。
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);
}
}
}
上面代码得出,ViewGroup在measure时,会对每一个子元素进行measure,measureChildren这个方法的实现如下所示:
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
? ? ? ?measureChild 的思想就是取出子元素的 LayoutParams,然后再通过getChildMeasureSpec 来创建子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给 View的measure 方法来进行测。
? ? ? Layout的作用是ViewGroup用来确认子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中onLayout方法会被调用。
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);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
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);
}
}
}
final boolean wasLayoutValid = isLayoutValid();
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
? ? ? ?layout方法的大致流程如下:首先会通过 setFrame方法来设定 View的四个顶点的位置即初始化 mLeft、mRight、mTop 和 mBottom 这四个值,View 的四个顶点一旦确定,那View 在父容器中的位置也就确定了; 接着会调用 onLayout 方法,这个方法的用途是父容器确定子元素的位置,和onMeasure 方法类似,onLayout 的具体实现同样和具体的布局有关,所以 View 和 ViewGroup 均没有真正实现 onLayout 方法。
??View的getMeasureWidth和getWidth这两个方法有什么区别?
在View的默认实现中,View的测量宽/高和最终宽/高是相等的,只不过测量宽/高在measure过程,而最终宽/高在layout过程,即两者的赋值时机不同。但是存在某些特殊情况会导致两者不一致,下面举例说。
如果重写View的layout方法,代码如下:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right+100, bottom+100);
}
上述代码会导致在任何情况下View的最终宽/高总是比测量宽/高大100px。
Draw过程的作用是将View绘制到屏幕上面。View的绘制过程遵循如下几步:
上面可通过源码看出:
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
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)
* 7. If necessary, draw the default focus highlight
*/
// Step 1, draw the background, if needed
int saveCount;
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
onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (isShowingLayoutBounds()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
...
}