View的绘制流程
View的绘制主要指measure、layout、draw三大流程,即测量、布局和绘制。其中measure确定view的测量宽高,layout确定view的最终宽高和四个顶点的位置,draw则是将view绘制在屏幕上。
一、measure过程
measure过程要分开来看,如果是单纯的原始view,那么通过measure就可以完成其测量过程。如果是一个ViewGroup,除了完成自身的测量过程之外,还会去遍历调用所有的子元素的measure方法,各个子元素再递归去执行这个流程。
1.View的measure过程
首先我们来看一下View的measure方法:
/**
* <p>
* This is called to find out how big a view should be. The parent
* supplies constraint information in the width and height parameters.
* </p>
*
* <p>
* The actual measurement work of a view is performed in
* {@link #onMeasure(int, int)}, called by this method. Therefore, only
* {@link #onMeasure(int, int)} can and must be overridden by subclasses.
* </p>
*
*
* @param widthMeasureSpec Horizontal space requirements as imposed by the
* parent
* @param heightMeasureSpec Vertical space requirements as imposed by the
* parent
*
* @see #onMeasure(int, int)
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
、、、、、、
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
我们发现这个方法在View中定义的是final的,意味着无法被子类重写,那么View的子类如何进行自身测量?注释中给出了结果:
* <p>
* The actual measurement work of a view is performed in
* {@link #onMeasure(int, int)}, called by this method. Therefore, only
* {@link #onMeasure(int, int)} can and must be overridden by subclasses.
* </p>
实际的测量工作是在onMeasure方法中进行的,因此子类必须自己去重写这个方法。那么我们先看一下View的onMeasure方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
这个方法很简洁,setMeasuredDimension顾名思义就是去设置view的测量的宽高,说明测试方法是在getDefaultSize这个方法中已经完成的,那么我们直接看这个方法的代码以及getSuggestedMinimumWidth方法:
/**
* 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;
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
我们只需要关注specMode为AT_MOST和EXACTLY这两种情况。可以看到,onMeasure返回的值其实就是measureSpec中的specSize,而这个specSize其实就是view测量之后的大小。
这里需要注意的是这里的值是view测量之后的值,实际显示之后的大小还要看layout中的布局大小,当然一般来说测量值和实际值是相等的。
这里getSuggestedMinimumWidth方法只有在specMode=UNSPECIFIED时才会有意义,而这个情况比较少见,总的来说就是如果这个view没有设置背景,就将这个view的android:minWidth属性指定的值返回,如果没有指定默认为0,;如果view设置了背景,就将背景这个Drawable的大小和minWidth对比返回大的那个值。这个值就是在specMode=UNSPECIFIED情况下测量到的宽高值。
从getDefaultSize方法的实现来看,如果我们自定义直接继承View的控件,就需要重写onMeasure方法并设置wrap-content时的宽高具体大小,否则在布局中wrap-content和match-parent就会有相同的表现,如何解决这个问题呢?需要以下这段代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecSize=MeasureSpec.getSize(widthMeasureSpec);
int widthSpecMode=MeasureSpec.getMode(widthMeasureSpec);
int heightSpecSize=MeasureSpec.getSize(heightMeasureSpec);
int heightSpecMode=MeasureSpec.getMode(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);
}
}
其中的mWidth和mHeight就是你需要自已定义的view的宽高。
2.ViewGroup的measure过程
ViewGroup不仅仅是要测量自身,还会遍历所有子元素的measure方法,各个子元素再递归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,他没有重写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);
}
}
}
这个方法很简洁的表明,measureChildren内部会遍历子元素,然后调用measureChild方法对子元素进行测量,继续看:
/**
* 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方法就是将子元素的LayoutParams 取出,然后通过getChildMeasureSpec创建出子元素的MeasureSpec,然后直接将MeasureSpec传递给View的measure方法进行测量。
ViewGroup作为一个抽象类,没有直接实现onMeasure方法,这就需要各个子类去根据不同的布局特性进行不同的测量细节,而ViewGroup无法统一,具体Layout可以具体分析。
3.如何在Activity启动时获取到view的宽高
获取view宽高的方法很多,这边推荐两种方式进行:
1.根据Activity的生命周期,在onWindowFocusChanged方法中进行获取。
//当activity获取到焦点时调用
//如果频繁切换activity的前后台,这个方法会多次调用
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus){
//获取view的宽高
}
}
2.通过view.post方法将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,view已经初始化完毕。
view.post(new Runnable() {
@Override
public void run() {
//这里获取宽高
}
});
二、layout布局过程
Layout相对于Measure来说简单的多。
Layout的作用主要是用来确定子元素的位置,当ViewGroup的位置被确定之后,它在onLayout中会遍历所有子元素并调用其layout方法,在layout方法中onLayout方法又会被调用。Layout方法确定自身的位置,onLayout方法确定所有子元素的位置。
下面是View的layout方法代码:(源码注释尽量多看)
/**
* Assign a size and position to a view and all of its
* descendants
*
* <p>This is the second phase of the layout mechanism.
* (The first is measuring). In this phase, each parent calls
* layout on all of its children to position them.
* This is typically done using the child measurements
* that were stored in the measure pass().</p>
*
* <p>Derived classes should not override this method.
* Derived classes with children should override
* onLayout. In that method, they should
* call layout on each of their children.</p>
*
* @param l Left position, relative to parent
* @param t Top position, relative to parent
* @param r Right position, relative to parent
* @param b Bottom position, relative to parent
*/
@SuppressWarnings({"unchecked"})
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;
//这个地方调用setFrame方法确定view的四个顶点位置
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;
if (!wasLayoutValid && isFocused()) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
if (canTakeFocus()) {
// We have a robust focus, so parents should no longer be wanting focus.
clearParentsWantFocus();
} else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
// This is a weird case. Most-likely the user, rather than ViewRootImpl, called
// layout. In this case, there's no guarantee that parent layouts will be evaluated
// and thus the safest action is to clear focus here.
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
clearParentsWantFocus();
} else if (!hasParentWantsFocus()) {
// original requestFocus was likely on this view directly, so just clear focus
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
// otherwise, we let parents handle re-assigning focus during their layout passes.
} else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
View focused = findFocus();
if (focused != null) {
// Try to restore focus as close as possible to our starting focus.
if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
// Give up and clear focus once we've reached the top-most parent which wants
// focus.
focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
}
}
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
setFrame方法确定view的四个顶点位置之后,那么view在父容器中的位置也就确定了下来,接着就会调用onLayout方法区确定子元素的位置。但是查看View和ViewGroup中的onLayout方法,这个方法也是子类去重写的。
View的onLayout方法代码
/**
* Called from layout when this view should
* assign a size and position to each of its children.
*
* Derived classes with children should override
* this method and call layout on each of
* their children.
* @param changed This is a new size or position for this view
* @param left Left position, relative to parent
* @param top Top position, relative to parent
* @param right Right position, relative to parent
* @param bottom Bottom position, relative to parent
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
ViewGroup的onLayout方法代码
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
我们接下来以LinerLayout的onLayout方法进行简单分析:
@Override
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);
}
}
/**
* Position the children during a layout pass if the orientation of this
* LinearLayout is set to {@link #VERTICAL}.
*
* @see #getOrientation()
* @see #setOrientation(int)
* @see #onLayout(boolean, int, int, int, int)
* @param left
* @param top
* @param right
* @param bottom
*/
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);
}
}
}
以vertical状态下的LinearLayout为例,可以看到此方法是遍历所有子元素,通过childTop这个属性的不断增大来确定每一个元素顶部坐标变大,位置往下不断摆放,最后通过调用setChildFrame方法来进行子元素的摆放。
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
子元素调用自身的layout方法,然后再通过onLayout来确定自己的位置,层层传递完成整个view的layout过程。
View的测量宽高和实际宽高的区别
测量宽高是通过measure得到的,而实际宽高还需要经过layout阶段进行最终的确定。
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
、、、、、、
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
以Linearlayout的onLayout方法为例,setChildFrame来设置子元素宽高其实获取的就是子元素测量之后的宽高,两种是相同的。那么看一下View的获取宽高的方法:
public final int getHeight() {
return mBottom - mTop;
}
public final int getWidth() {
return mRight - mLeft;
}
可以看到,获取view的宽高其实是通过四个顶点的计算得出的,因此我们可以得出我们的结论:
在View的默认实现中,View的测量宽高和最终宽高是相等的,只不过测量宽高形成于measure阶段,而最终宽高形成于layout阶段,即两者的赋值时机不同。因此我们在日常开发中,可以认为View的测量宽高等同于最终宽高。
当然也存在某些特殊情况,人为的去将View的坐标点进行更改,测量的宽高必然会与最终宽高不一致。比如重写layout代码:
@Override
public void layout(int l, int t, int r, int b) {
super.layout(l, t, r+100, b+100);
}
另一方面,如果一个View的measure方法多次调用去测量才能确定自己的测量宽高,那么在前几次的测量过程中,得出的测量宽高和最终宽高也可能不一致,但是测量结束时得到的宽高和最终宽高还是相同的。
三、draw过程
Draw过程相对比较简单,它的作用是将view绘制到屏幕上面。
View的绘制过程遵循如下几步:
1.绘制背景:background.draw(canvas);
1.绘制自身:onDraw;
1.绘制children:dispatchDraw;
1.绘制装饰:onDrawScrollBars;
截取源码如下:
/**
* Manually render this view (and all of its children) to the given Canvas.
* The view must have already done a full layout before this function is
* called. When implementing a view, implement
* {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
* If you do need to override this method, call the superclass version.
*
* @param canvas The Canvas to which the View is rendered.
*/
@CallSuper
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);
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;
}
、、、、、、
}
View的绘制流程是通过dispatchDraw方法进行分发,dispatchDraw会遍历所有子元素的draw方法,如此draw事件就一层层传递下去。