1.View的绘制流程从哪里开始
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是类似的。
2.什么是MeasureSpec?
MeasureSpec代表一个32位int值,高2位代表SpecMode,就是测量模式,低30位代表SpecSize,就是规格大小。
(1)SpecMode有三类
- UNSPECIFIED
父容器对子 View 没有施加任何限制,子 View 可以任意大小。一般用于系统内部。
- EXACTLY
父容器已经为子 View 精确指定了大小,子 View 应该匹配这个大小。它对应于LayoutParams中的match_parent和具体的数值这两个模式。
- AT_MOST
子 View 可以是任何大小,但不能超过父容器指SpecSize。它对应于LayoutParams中的wrap_content。
(2)创建MeasureSpec
MeasureSpec measureSpec=MeasureSpec.makeMeasureSpec(size,mode);
MeasureSpec 是通过静态方法 MeasureSpec.makeMeasureSpec() 创建的,该方法接受两个参数:大小和测量模式。
(3)获取测量模式SpecMode和大小SpecSize
int selfwidthSpecMode=MeasureSpec.getMode(widthMeasureSpec); int selfwidthSpecSize=MeasureSpec.getSize(widthMeasureSpec);
我们可以通过 MeasureSpec.getMode() 和 MeasureSpec.getSize() 方法来获取测量模式和大小,然后根据这些信息来确定 View 的最终大小。
(4)MeasureSpec和LayoutParams的对应关系
在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽/高。需要注意的是,MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec从而决定View的宽高。
MeasureSpec的转换流程:
对于顶级的View(DecorView),其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同确定。
其中LayoutParams中的宽高的参数
- LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小。
- LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小。
- 固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小。
对于普通的View,其MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams来共同确定。
- 当View采用固定宽/高时,View的MeasureSpec都是精确模式并且其大小遵循Layoutparams中的大小。
- 当View的宽/高是match_parent时,如果父容器的模式是精确模式,那么View也会是精确模式并且大小是父容器的剩余空间。如果父容器的模式是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。
- 当View的宽/高是wrap_content时,View的模式总是最大化并且大小不能超过父容器的剩余空间。
3.View的工作流程
View的工作流程主要是measure、layout、draw这三大流程,即测量、布局和绘制。
(1)measure过程
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 方法来进行测。
(2)layout过程
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。
(3)draw过程
Draw过程的作用是将View绘制到屏幕上面。View的绘制过程遵循如下几步:
- 绘制背景background.draw(canvas)。
- 绘制自己(onDraw)。
- 绘制children(dispatchDraw)。
- 绘制装饰(onDrawScrollBars)。
上面可通过源码看出:
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; } ... }