View的工作流程主要是指measure、layout、draw三大流程,即测量,布局和绘制,其中measure确定View的宽高,layout确定View的最终宽高和四个顶点的位置,而draw则将View绘制到屏幕上。
measure过程
measure过程要分两种情况来看,如果是View,那么通过measure方法就完成了其测量的过程,如果是VIewGroup,除了完成自己的测量之外,还要遍历调用所有子View的measure方法,如果子View仍是ViewGroup的话就递归这个过程。下面从源码去分析View和ViewGroup的measure过程。
View的measure过程
虽然View的测量过程是从measure方法开始的,但是measure方法是一个final方法,我们没办法重写,所有View都一样,主要做些测量的条件判断和初始化工作,最终会调用onMeasure方法。我们在实际中自定义View的触及不到,所以我们直接从onMeasure方法开始看,以下是View的onMeasure源码:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
从源码来看,onMeasure方法很简洁,但不要小瞧这几行代码,完成了默认的View的 测量。我们一个一个来看,方法参数widthMeasureSpec,heightMeasureSpec是父控件的MeasureSpec,关于MeasureSpec上篇文章已经详细聊过,接下来我们看方法内实现,从setMeasuredDimension方法开始
/**
* <p>This method must be called by {@link #onMeasure(int, int)} to store the
* measured width and measured height. Failing to do so will trigger an
* exception at measurement time.</p>
*
* @param measuredWidth The measured width of this view. May be a complex
* bit mask as defined by {@link #MEASURED_SIZE_MASK} and
* {@link #MEASURED_STATE_TOO_SMALL}.
* @param measuredHeight The measured height of this view. May be a complex
* bit mask as defined by {@link #MEASURED_SIZE_MASK} and
* {@link #MEASURED_STATE_TOO_SMALL}.
*/
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
boolean optical = isLayoutModeOptical(this);
这句意思是先判断下当前View是不是属于ViewGroup,如果为false就命中下面的条件语句进行值计算,最后调用setMeasuredDimensionRaw()将值进行保存。从源码中我们看出setMeasuredDimension方法作用是将计算的宽高做最后调整后保存。
接下来我们看setMeasuredDimension方法的参数,将getSuggestedMinimumWidth()/getSuggestedMinimumHeight()和父控件的MeasureSpec通过getDefaultSize得来,我们看getDefaultSize的源码:
/**
* Utility to return a default size. Uses the supplied size if the
* MeasureSpec imposed no constraints. Will get larger if allowed
* by the MeasureSpec.
*
* @param size Default size for this view
* @param measureSpec Constraints imposed by the parent
* @return The size this view should be.
*/
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的逻辑很简单,就是通过父控件的MeasureSpecMode的三种情况来计算具体的测量值。
当MeasureSpec.UNSPECIFIED时,结果为getSuggestedMinimumWidth;
当MeasureSpec.AT_MOST和MeasureSpec.EXACTLY时,结果为MeasureSpec.getSize(measureSpec)。和我们在Android View工作原理详解(一)中说的计算MeasureSpec的方式一致。
我们再来看看getSuggestedMinimumWidth和getSuggestedMinimumHeight两个方法
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
/**
* Returns the suggested minimum width that the view should use. This
* returns the maximum of the view's minimum width
* and the background's minimum width
* ({@link android.graphics.drawable.Drawable#getMinimumWidth()}).
* <p>
* When being used in {@link #onMeasure(int, int)}, the caller should still
* ensure the returned width is within the requirements of the parent.
*
* @return The suggested minimum width of the view.
*/
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
两个方法逻辑相同,我们以getSuggestedMinimumWidth来分析,从代码中我们可以看出:
1.如果View没有设置背景,那么View的宽度即为mMinWidth,而mMinWidth 对应android:mMinWidth 这个控件属性,如果不指定,默认值为0。
2.如果View设置了背景,view的宽度为max(mMinWidth, mBackground.getMinimumWidth(),mMinWidth的值我们已经知道了,那么mBackground.getMinimumWidth()的值又是什么呢,这里的mBackground是一个Drawable对象,我们看看Drawable的getMinimumWidth方法
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
计算规则是如果图片有原始宽度,就返回原始宽度,否则返回0,所以mBackground.getMinimumWidth()取的是背景图的原始大小宽度。
ViewGroup的measure过程
和View不同的是,ViewGroup是一个抽象类,因此它没有重写View的onMeasure方法,但其提供了一个叫measureChildren的方法。
/**
* Ask all of the children of this view to measure themselves, taking into
* account both the MeasureSpec requirements for this view and its padding.
* We skip children that are in the GONE state The heavy lifting is done in
* getChildMeasureSpec.
*
* @param widthMeasureSpec The width requirements for this view
* @param heightMeasureSpec The height requirements for this view
*/
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的过程中会调用measureChid方法去测量每个子VIew,我们来看看measureChid方法:
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding.
* The heavy lifting is done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param parentHeightMeasureSpec The height requirements for this view
*/
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这个方法也很简单,首先取出当前的child View的LayoutParams,然后结合parent的MeasureSpec计算出自己的MeasureSpec(我们在Android View工作原理详解(一)说过自己的MeasureSpec是通过父控件的MeasureSpec和自身的layoutParams计算出来的),最后调用child的measure方法进行子View的测量。
如果只看ViewGroup的measure过程还不能完全理解容器控件的measure过程,毕竟ViewGroup是一个抽象类,很多实现都在其子类中实现,我们以LinearLayout为例,来看看LinearLayout的measure过程。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
上面代码是LinearLayout的onMeasure方法源码,通过布局方向来选择measure过程。measureVerticalf方法太长,我们截取部分看下
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
nonSkippedChildCount++;
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
// Optimization: don't bother measuring children who are only
// laid out using excess space. These views will get measured
// later if we have space to distribute.
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
if (useExcessSpace) {
// The heightMode is either UNSPECIFIED or AT_MOST, and
// this child is only laid out using excess space. Measure
// using WRAP_CONTENT so that we can find out the view's
// optimal height. We'll restore the original height of 0
// after measurement.
lp.height = LayoutParams.WRAP_CONTENT;
}
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
// Restore the original height and record how much space
// we've allocated to excess-only children so that we can
// match the behavior of EXACTLY measurement.
lp.height = 0;
consumedExcessSpace += childHeight;
}
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
/**
* If applicable, compute the additional offset to the child's baseline
* we'll need later when asked {@link #getBaseline}.
*/
if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
mBaselineChildTop = mTotalLength;
}
// if we are trying to use a child index for our baseline, the above
// book keeping only works if there are no children above it with
// weight. fail fast to aid the developer.
if (i < baselineChildIndex && lp.weight > 0) {
throw new RuntimeException("A child of LinearLayout with index "
+ "less than mBaselineAlignedChildIndex has weight > 0, which "
+ "won't work. Either remove the weight, or don't set "
+ "mBaselineAlignedChildIndex.");
}
boolean matchWidthLocally = false;
if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
// The width of the linear layout will scale, and at least one
// child said it wanted to match our width. Set a flag
// indicating that we need to remeasure at least that view when
// we know our width.
matchWidth = true;
matchWidthLocally = true;
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
childState = combineMeasuredStates(childState, child.getMeasuredState());
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
if (lp.weight > 0) {
/*
* Widths of weighted Views are bogus if we end up
* remeasuring, so keep them separate.
*/
weightedMaxWidth = Math.max(weightedMaxWidth,
matchWidthLocally ? margin : measuredWidth);
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}
i += getChildrenSkipCount(child, i);
}
从代码可以看出,LinearLayou会遍历元素对每个子元素执行MeasureChildBeforeLayout方法,这个方法内部会地调用子元素的measure方法,这样各个子元素就开始依次进行measure过程,通过全局变量mTotalLength 保存每一行的的高度之和,当子View测量完成之后会测量自己的大小:
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
getMeasuredWidth/Height
View的measure过程是三大流程中最复杂的一个,measure完成之后就可以通过getMeasuredWidth/Height方法就可以获取到View的测量宽高了。但有时候View会进行多次测量,所以最好是在layout中获取测量宽高。
需要注意的是View的测量过程和Activity的生命周期是不同步的,我们不能在onCreate、onStart、onResume中获取测量宽高。建议在通过以下三种方法获取:
1.onWindowFocusChanged方法中获取
2.通过view.post(runnable)在run中获取
3.通过ViewTreeObserber获取
Layout过程
Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup 的位置被确定后,它在onIayout中会遍历所有的子元素并调用其layout 方法,在layout方法中onLayout方法又会被调用。Layout 过程和measure过程相比就简单多了,layout 方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置,先看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;
}
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.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;
}
layout方法的大致流程如下:首先会通过setFrame方法来设定View的四个顶点的位置, 即初始化mLeft、mRight、 mTop 和mBottom这四个值,View 的四个顶点一旦确定, 那么 View在父容器中的位置也就确定了:接着会调用onLayout方法,这个方法的用途是父容 器确定子元素的位置,和onMeasure方法类似,onL ayout的具体实现同样和具体的布局有 关,所以View和ViewGroup均没有真正实现onLayout 方法。接下来,我们可以看-下 LinearLayout的onLayout方法,如下所示。
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);
}
}
LinearLayout中onLayout 的实现逻辑和onMeasure 的实现逻辑类似,这里选择layoutVertical继续看。
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
// Where right end of child should go
final int width = right - left;
int childRight = width - mPaddingRight;
// Space available for child
int childSpace = width - paddingLeft - mPaddingRight;
final int count = getVirtualChildCount();
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
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();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
这里分析一下layoutVertical的代码逻辑,可以看到,此方法会遍历所有子元素并调用
setChildFrame方法来为子元素指定对应的位置,其中childTop会逐渐增大,这就意味着后
面的子元素会被放置在靠下的位置,这刚好符合竖直方向的LinearLayout 的特性。至于
setChildFrame它仅仅是调用子元素的layout方法而已,这样父元素在layout 方法中完成
自己的定位以后,就通过onLayout方法去调用子元素的layout 方法,子元素又会通过自己
的layout方法来确定自己的位置,这样- -层一 层地传递 下去就完成了整个View树的layout
过程。setChildFrame 方法的实现如下所示。
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
getMeasureWidth(Height)和getWidth(Height)的区别
View 的getMeasuredWidth和getWidth这两个方法有什么区别, 至于getMeasuredHeight和getHeight的区别和前两者完全一样。 为了回答这个问题,首先,我们看一下getwidth和getHeight这两个方法的具体实现:
@ViewDebug.ExportedProperty(category = "layout")
public final int getWidth() {
return mRight - mLeft;
}
/**
* Return the height of your view.
*
* @return The height of your view, in pixels.
*/
@ViewDebug.ExportedProperty(category = "layout")
public final int getHeight() {
return mBottom - mTop;
}
从getWidth和getHeight的源码再结合mLef、mRight、 mTop 和mBottom这四个变量的赋值过程来看,getWidth 方法的返回值刚好就是View的测量宽度,而getHeight方法的返回值也刚好就是View 的测量高度。经过上述分析,现在我们可以回答这个问题了:在.View的默认实现中, View的测量宽/高和最终宽/高是相等的,只不过测量宽高形成于View的measure过程,而最终宽/高形成于View的layout 过程,即两者的赋值时机不同,测量宽高的赋值时机稍微早- -些。因此,在日常开发中,我们可以认为View的测量宽/高就等于最终宽高,但当我们重写view的layout方法,修改其宽高,那么两个方法的结果就不一样了。
@Override
public void layout(int l, int t, int r, int b) {
super.layout(l, t, r+100, b+200);
}
还有另一种情况,如果measure需要多次时,getWidth和getMeasureWidth也会不一致。
Draw过程
Draw过程就比较简单了,它的作用是将View绘制到屏幕上面。View的绘制过程遵循如下几步:
(1)绘制背景background .draw(canvas)。
(2)绘制自己(onDraw)。
(3)绘制children (dispatchDraw)。
(4)绘制装饰( onDrawForeground)。
(5) 绘制焦点相关(drawDefaultFocusHighlight/debugDrawFocus)
这一点通过draw方法的源码可以明显看出来,如下所示。
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)
*/
// 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 (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}