Android开发——View绘制流程
2017-01-18 09:55
225 查看
网上讲解View的绘制流程有很多优秀的文章。主要分为三个步骤:分别是measure、layout和draw。measure根据父布局的尺寸以及自己想要的尺寸得到最终自己的尺寸,layout用于确定子View的位置,draw负责绘制自己。View分为View和ViewGroup,这两种所经历的流程稍有不同,下面分别介绍。
measure方法根据父布局的约束条件得到自己的尺寸,而具体的测量工作在onMeasure方法中,因此如果子类需要提供具体的测量工作,需要重写onMeasure方法。那么我们看一下View的onMeasure方法是如何实现的?
可以看到,默认的实现是调用setMeasureDimension设置宽度和高度。以宽度为例,调用了getDefaultSize方法,其中getSuggestedMinimumWidth方法用于返回建议的最小宽度。
getDefalutSize的实现如下:
从上面可以看到,当父布局传入的约束条件为固定尺寸时,子View使用最小的建议宽度;其他两个参数时,使用父布局的宽度。
每个View在onMeasure中设置自己的宽度和高度,当宽度和高度知道后,父布局就可以根据子View确定自己的宽度和高度。
从View的draw方法可以看出,draw负责的是总的绘制流程,包括背景的绘制、内容的绘制以及每个子View的绘制等。而内容的绘制的实现在onDraw方法中,draw事件的分发给子View在dispatchDraw方法中,而View中该方法是个空实现,因为View是没有子View的,自然也没有实现,而ViewGroup需要实现该方法。
View的onDraw方法负责绘制内容,并且是个空实现,留给每个View实现。
LinearLayout的onMeasure方法如下:
我们知道LinearLayout有垂直和水平方向之后,上面的方法也根据方向不同调用不同的方法。这儿我们就以垂直方向为例,看一下measureVertical方法,如下:
从上面的代码可以看到,主要做的就是遍历各个子View,让每个子View测量得到自己的宽度和高度,然后垂直的LinearLayout的高度就是需要考虑是不是各个子View的高度和或者是个确定值。当第一次测量完后,如果其中有View有weight属性,那么还需要二次测量,用于处理weight属性,这时有的子View需要重新设置高度,那么自然LinearLayout的高度也需要重新调整。经过两次测量后,可以得到最终的宽度和高度,最后调用setMeasureDimension方法。
LinearLayout的onLayout方法如下:
可以看到依旧是根据方向有两个方法,我们这还以垂直方向为例,看layoutVertical方法,实现如下:
从上面的代码,就是遍历所有子View,因为是垂直方向,从上到下给每个子View设置四个角的坐标。其中需要注意的就是对LinearLayout的gravity属性和各个子View的layout_gravity属性的处理,因为这会对子View的位置有关。
当得到子View的四个角的坐标后,调用了setChildFrame方法,该方法的实现如下:
可以看到调用了View的layout方法,而从前面我们知道了layout方法会分配自己的位置。
我们发现在LinearLayout中没有找到onDraw方法的实现,但是在其父类ViewGroup中找到了dispatchDraw方法,如下:
从上面的代码可以看到,调用了drawChild方法,方法如下:
可以看到最终又调用了View的绘制,子View都绘制完成后,ViewGroup的绘制也就完成了。
这里的mView就是DecorView,调用了DecorView的measure方法,DecorView继承自FrameLayout,是一个ViewGroup,接下来会让每个子View进行绘制,这样当每个View得到了宽度,高度后,这样整个Activity的View都得到了测量。
测量完成之后,perfromTravservals方法会调用perfromLayout执行布局步骤,方法如下:
从上面可以看到,调用了DecorView的layout方法进行layout。接下来看DecorView的onLayout对子View的放置:
可以看到首先调用父类的onLayout方法,FrameLayout的onLayout如下:
从上面可以看到FrameLayout遍历子View,对子View进行计算坐标,得到坐标后调用layout方法。这样整个layout过程就完成了。
接下来,performTravserals方法中会调用perfromDraw方法进行递归绘制。
在Activity调用了setContentView时,在onResume方法中会令DecorView为可见,此时会调用其draw方法,然后整个View树就可以都绘制出来了。
理解View的绘制流程有助于自定义View和ViewGroup。
View的绘制流程
View的绘制流程和ViewGroup有些不同,主要有measure和draw两个步骤。measure方法根据父类的宽度和高度再结合自己设置的宽高属性得到自己最终的宽度和高度;由于layout用于设置子View的位置,而View是没有子View的,所以也就不含有layout过程;但是由于每个具体的View都有各自的样式,所以有draw步骤。measure步骤
View的measure是绘制的入口,实现如下: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); } // Suppress sign extension for the low bytes long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL; if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2); final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT; // Optimize layout by avoiding an extra EXACTLY pass when the view is // already measured as the correct size. In API 23 and below, this // extra pass is required to make LinearLayout re-distribute weight. final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec; final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY; final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec) && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec); final boolean needsLayout = specChanged && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize); if (forceLayout || needsLayout) { // first clears the measured dimension flag mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; resolveRtlPropertiesIfNeeded(); int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key); if (cacheIndex < 0 || sIgnoreMeasureCache) { // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } else { long value = mMeasureCache.valueAt(cacheIndex); // Casting a long to int drops the high 32 bits, no mask needed setMeasuredDimensionRaw((int) (value >> 32), (int) value); mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } // flag not set, setMeasuredDimension() was not invoked, we raise // an exception to warn the developer if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) { throw new IllegalStateException("View with id " + getId() + ": " + getClass().getName() + "#onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; } mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 | (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension }
measure方法根据父布局的约束条件得到自己的尺寸,而具体的测量工作在onMeasure方法中,因此如果子类需要提供具体的测量工作,需要重写onMeasure方法。那么我们看一下View的onMeasure方法是如何实现的?
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
可以看到,默认的实现是调用setMeasureDimension设置宽度和高度。以宽度为例,调用了getDefaultSize方法,其中getSuggestedMinimumWidth方法用于返回建议的最小宽度。
getDefalutSize的实现如下:
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; }
从上面可以看到,当父布局传入的约束条件为固定尺寸时,子View使用最小的建议宽度;其他两个参数时,使用父布局的宽度。
每个View在onMeasure中设置自己的宽度和高度,当宽度和高度知道后,父布局就可以根据子View确定自己的宽度和高度。
layout步骤
View的layout入口在layout方法,layout方法向View以及其子View分配尺寸。当View可以包含子View时,需要重写onLayout方法给每个子View分配尺寸和位置。draw步骤
View的draw方法负责绘制步骤,方法的实现如下: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); //绘制子View // Step 4, draw the children dispatchDraw(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); // we're done... return; } /* * Here we do the full fledged routine... * (this is an uncommon case where speed matters less, * this is why we repeat some of the tests that have been * done above) */ boolean drawTop = false; boolean drawBottom = false; boolean drawLeft = false; boolean drawRight = false; float topFadeStrength = 0.0f; float bottomFadeStrength = 0.0f; float leftFadeStrength = 0.0f; float rightFadeStrength = 0.0f; // Step 2, save the canvas' layers int paddingLeft = mPaddingLeft; final boolean offsetRequired = isPaddingOffsetRequired(); if (offsetRequired) { paddingLeft += getLeftPaddingOffset(); } int left = mScrollX + paddingLeft; int right = left + mRight - mLeft - mPaddingRight - paddingLeft; int top = mScrollY + getFadeTop(offsetRequired); int bottom = top + getFadeHeight(offsetRequired); if (offsetRequired) { right += getRightPaddingOffset(); bottom += getBottomPaddingOffset(); } final ScrollabilityCache scrollabilityCache = mScrollCache; final float fadeHeight = scrollabilityCache.fadingEdgeLength; int length = (int) fadeHeight; // clip the fade length if top and bottom fades overlap // overlapping fades produce odd-looking artifacts if (verticalEdges && (top + length > bottom - length)) { length = (bottom - top) / 2; } // also clip horizontal fades if necessary if (horizontalEdges && (left + length > right - length)) { length = (right - left) / 2; } if (verticalEdges) { topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength())); drawTop = topFadeStrength * fadeHeight > 1.0f; bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength())); drawBottom = bottomFadeStrength * fadeHeight > 1.0f; } if (horizontalEdges) { leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength())); drawLeft = leftFadeStrength * fadeHeight > 1.0f; rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength())); drawRight = rightFadeStrength * fadeHeight > 1.0f; } saveCount = canvas.getSaveCount(); int solidColor = getSolidColor(); if (solidColor == 0) { final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG; if (drawTop) { canvas.saveLayer(left, top, right, top + length, null, flags); } if (drawBottom) { canvas.saveLayer(left, bottom - length, right, bottom, null, flags); } if (drawLeft) { canvas.saveLayer(left, top, left + length, bottom, null, flags); } if (drawRight) { canvas.saveLayer(right - length, top, right, bottom, null, flags); } } else { scrollabilityCache.setFadeColor(solidColor); } // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); // Step 5, draw the fade effect and restore layers final Paint p = scrollabilityCache.paint; final Matrix matrix = scrollabilityCache.matrix; final Shader fade = scrollabilityCache.shader; if (drawTop) { matrix.setScale(1, fadeHeight * topFadeStrength); matrix.postTranslate(left, top); fade.setLocalMatrix(matrix); p.setShader(fade); canvas.drawRect(left, top, right, top + length, p); } if (drawBottom) { matrix.setScale(1, fadeHeight * bottomFadeStrength); matrix.postRotate(180); matrix.postTranslate(left, bottom); fade.setLocalMatrix(matrix); p.setShader(fade); canvas.drawRect(left, bottom - length, right, bottom, p); } if (drawLeft) { matrix.setScale(1, fadeHeight * leftFadeStrength); matrix.postRotate(-90); matrix.postTranslate(left, top); fade.setLocalMatrix(matrix); p.setShader(fade); canvas.drawRect(left, top, left + length, bottom, p); } if (drawRight) { matrix.setScale(1, fadeHeight * rightFadeStrength); matrix.postRotate(90); matrix.postTranslate(right, top); fade.setLocalMatrix(matrix); p.setShader(fade); canvas.drawRect(right - length, top, right, bottom, p); } canvas.restoreToCount(saveCount); // 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); }
从View的draw方法可以看出,draw负责的是总的绘制流程,包括背景的绘制、内容的绘制以及每个子View的绘制等。而内容的绘制的实现在onDraw方法中,draw事件的分发给子View在dispatchDraw方法中,而View中该方法是个空实现,因为View是没有子View的,自然也没有实现,而ViewGroup需要实现该方法。
View的onDraw方法负责绘制内容,并且是个空实现,留给每个View实现。
ViewGroup的绘制流程
ViewGroup继承自View,一般会包含多个View或ViewGroup,是个容器。它的绘制也遵循View绘制流程三步曲,下面以LinearLayout为例,具体介绍。measure步骤
在View的measure步骤中知道了,如果子类需要实现具体的测量工作,需要实现onMeasure方法。LinearLayout有自己的测量工作,那么直接看onMeasure方法。LinearLayout的onMeasure方法如下:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); } }
我们知道LinearLayout有垂直和水平方向之后,上面的方法也根据方向不同调用不同的方法。这儿我们就以垂直方向为例,看一下measureVertical方法,如下:
// 传入的两个参数是LinearLayout父布局的宽度和高度 void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { mTotalLength = 0; int maxWidth = 0; int childState = 0; int alternativeMaxWidth = 0; int weightedMaxWidth = 0; boolean allFillParent = true; float totalWeight = 0; //获得子View的个数 final int count = getVirtualChildCount(); //得到宽度模式 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); //得到高度模式 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); boolean matchWidth = false; boolean skippedMeasure = false; final int baselineChildIndex = mBaselineAlignedChildIndex; final boolean useLargestChild = mUseLargestChild; int largestChildHeight = Integer.MIN_VALUE; int consumedExcessSpace = 0; // 遍历每个子View,叠加每个View的高度,记录最大宽度。子View的最大宽度可能会是LinearLayout的宽度 for (int i = 0; i < count; ++i) { //获得子View final View child = getVirtualChildAt(i); //如果为null,measureNullChild()方法返回0 if (child == null) { mTotalLength += measureNullChild(i); continue; } //如果子View的可见性为GONE,跳过 if (child.getVisibility() == View.GONE) { i += getChildrenSkipCount(child, i); continue; } //如果在该View之前需要有Divider,加上Divider的高度 if (hasDividerBeforeChildAt(i)) { mTotalLength += mDividerHeight; } //得到子View的LayoutParams final LayoutParams lp = (LayoutParams) child.getLayoutParams(); //记录Weight属性的值 totalWeight += lp.weight; //如果子View的高度为0,weight大于0,说明该子View需要额外空间 final boolean useExcessSpace = lp.height == 0 && lp.weight > 0; //如果高度模式是确定的并且需要使用额外控件 if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) { //高度加上该View的Margin final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin); //跳过此次测量,二次测量时再确定该View的高度 skippedMeasure = true; } else { //如果高度模式是AT_MOST或UNSPECIFIED并且需要使用额外控件 if (useExcessSpace) { //令该View的高度为WRAP_CONTENT,此时最佳 lp.height = LayoutParams.WRAP_CONTENT; } //如果weight不为0,那么usedHeight为0 final int usedHeight = totalWeight == 0 ? mTotalLength : 0; //测量该子View,之后便可以得到子View的宽度和高度 measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, usedHeight); //得到View的测量高度 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); } if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) { mTotalLength += mDividerHeight; } if (useLargestChild && (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) { mTotalLength = 0; for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); if (child == null) { mTotalLength += measureNullChild(i); continue; } if (child.getVisibility() == GONE) { i += getChildrenSkipCount(child, i); continue; } final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); // Account for negative margins final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + largestChildHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child)); } } // Add in our padding mTotalLength += mPaddingTop + mPaddingBottom; int heightSize = mTotalLength; // Check against our minimum height heightSize = Math.max(heightSize, getSuggestedMinimumHeight()); // Reconcile our calculated size with the heightMeasureSpec int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0); heightSize = heightSizeAndState & MEASURED_SIZE_MASK; // Either expand children with weight to take up available space or // shrink them if they extend beyond our current bounds. If we skipped // measurement on any children, we need to measure them now. int remainingExcess = heightSize - mTotalLength + (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace); //需要二次测量,包含weight的情况。根据第一次测量的结果算出剩余空间,weight就在这个剩余空间里取份 if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) { float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight; mTotalLength = 0; for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); if (child == null || child.getVisibility() == View.GONE) { continue; } final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final float childWeight = lp.weight; if (childWeight > 0) { final int share = (int) (childWeight * remainingExcess / remainingWeightSum); remainingExcess -= share; remainingWeightSum -= childWeight; final int childHeight; if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) { childHeight = largestChildHeight; } else if (lp.height == 0 && (!mAllowInconsistentMeasurement || heightMode == MeasureSpec.EXACTLY)) { // This child needs to be laid out from scratch using // only its share of excess space. childHeight = share; } else { // This child had some intrinsic height to which we // need to add its share of excess space. childHeight = child.getMeasuredHeight() + share; } final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( Math.max(0, childHeight), MeasureSpec.EXACTLY); final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin, lp.width); //子View二次测量 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); // Child may now not fit in vertical dimension. childState = combineMeasuredStates(childState, child.getMeasuredState() & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT)); } final int margin = lp.leftMargin + lp.rightMargin; final int measuredWidth = child.getMeasuredWidth() + margin; maxWidth = Math.max(maxWidth, measuredWidth); boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT; alternativeMaxWidth = Math.max(alternativeMaxWidth, matchWidthLocally ? margin : measuredWidth); allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT; final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child)); } // Add in our padding mTotalLength += mPaddingTop + mPaddingBottom; // TODO: Should we recompute the heightSpec based on the new total length? } else { alternativeMaxWidth = Math.max(alternativeMaxWidth, weightedMaxWidth); // We have no limit, so make all weighted views as tall as the largest child. // Children will have already been measured once. if (useLargestChild && heightMode != MeasureSpec.EXACTLY) { for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); if (child == null || child.getVisibility() == View.GONE) { continue; } final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); float childExtra = lp.weight; if (childExtra > 0) { child.measure( MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(largestChildHeight, MeasureSpec.EXACTLY)); } } } } if (!allFillParent && widthMode != MeasureSpec.EXACTLY) { maxWidth = alternativeMaxWidth; } maxWidth += mPaddingLeft + mPaddingRight; // Check against our minimum width maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); //得到了自己的宽度和高度后,调用该方法设置 setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState); if (matchWidth) { forceUniformWidth(count, heightMeasureSpec); } }
从上面的代码可以看到,主要做的就是遍历各个子View,让每个子View测量得到自己的宽度和高度,然后垂直的LinearLayout的高度就是需要考虑是不是各个子View的高度和或者是个确定值。当第一次测量完后,如果其中有View有weight属性,那么还需要二次测量,用于处理weight属性,这时有的子View需要重新设置高度,那么自然LinearLayout的高度也需要重新调整。经过两次测量后,可以得到最终的宽度和高度,最后调用setMeasureDimension方法。
onLayout步骤
在View的layout步骤中,知道了包含子View的View,那么需要重写onLayout方法用于给每个子View分配位置。LinearLayout的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); } }
可以看到依旧是根据方向有两个方法,我们这还以垂直方向为例,看layoutVertical方法,实现如下:
//用于设置每个子View的位置 void layoutVertical(int left, int top, int right, int bottom) { final int paddingLeft = mPaddingLeft; int childTop; int childLeft; // LineaLayout可布局的宽度 final int width = right - left; //得到最右边可放置的尺寸 int childRight = width - mPaddingRight; // 子View可以的空间,去两边的padding int childSpace = width - paddingLeft - mPaddingRight; //得到子View的数量 final int count = getVirtualChildCount(); final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; //判断Gravity属性,计算开始的高度 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; } //遍历View for (int i = 0; i < count; i++) { //得到子View final View child = getVirtualChildAt(i); //如果子View为null,高度+0 if (child == null) { childTop += measureNullChild(i); } //如果可见性不为GONE else if (child.getVisibility() != GONE) { //得到子View的宽度 final int childWidth = child.getMeasuredWidth(); //得到子View的高度 final int childHeight = child.getMeasuredHeight(); final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); //判断子View的layout_gravity属性 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; //默认是从左到右的话,那么childLeft为LinearLayout的左padding与子View的左margin的和 case Gravity.LEFT: default: childLeft = paddingLeft + lp.leftMargin; break; } //如果在该View之前有分隔线,再加上分隔线的高度 if (hasDividerBeforeChildAt(i)) { childTop += mDividerHeight; } //再加上topMargin的值 childTop += lp.topMargin; //设置该子View的左、右、上、下四个左边的值 setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); //总高度再加上该View的宽度bottomMargin childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); i += getChildrenSkipCount(child, i); } } }
从上面的代码,就是遍历所有子View,因为是垂直方向,从上到下给每个子View设置四个角的坐标。其中需要注意的就是对LinearLayout的gravity属性和各个子View的layout_gravity属性的处理,因为这会对子View的位置有关。
当得到子View的四个角的坐标后,调用了setChildFrame方法,该方法的实现如下:
private void setChildFrame(View child, int left, int top, int width, int height) { child.layout(left, top, left + width, top + height); }
可以看到调用了View的layout方法,而从前面我们知道了layout方法会分配自己的位置。
onDraw步骤
View绘制流程中第三步是draw,子类如果需要绘制流程,那么需要重写onDraw方法;而如果有子View需要绘制,需要重写dispatchDraw方法。至于LinearLayout,我们可以猜想它是没有内容可以绘制的,具体的绘制是每个View绘制,那么也就是说它需要重写dispatchDraw方法,而无需重写onDraw方法,下面验证我们的猜想。我们发现在LinearLayout中没有找到onDraw方法的实现,但是在其父类ViewGroup中找到了dispatchDraw方法,如下:
@Override protected void dispatchDraw(Canvas canvas) { boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode); final int childrenCount = mChildrenCount; //子View final View[] children = mChildren; int flags = mGroupFlags; //动画 if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) { final boolean buildCache = !isHardwareAccelerated(); for (int i = 0; i < childrenCount; i++) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { final LayoutParams params = child.getLayoutParams(); attachLayoutAnimationParameters(child, params, i, childrenCount); bindLayoutAnimation(child); } } final LayoutAnimationController controller = mLayoutAnimationController; if (controller.willOverlap()) { mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE; } controller.start(); mGroupFlags &= ~FLAG_RUN_ANIMATION; mGroupFlags &= ~FLAG_ANIMATION_DONE; if (mAnimationListener != null) { mAnimationListener.onAnimationStart(controller.getAnimation()); } } int clipSaveCount = 0; final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK; if (clipToPadding) { clipSaveCount = canvas.save(); canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop, mScrollX + mRight - mLeft - mPaddingRight, mScrollY + mBottom - mTop - mPaddingBottom); } // We will draw our child's animation, let's reset the flag mPrivateFlags &= ~PFLAG_DRAW_ANIMATION; mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED; boolean more = false; final long drawingTime = getDrawingTime(); if (usingRenderNodeProperties) canvas.insertReorderBarrier(); final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size(); int transientIndex = transientCount != 0 ? 0 : -1; // Only use the preordered list if not HW accelerated, since the HW pipeline will do the // draw reordering internally final ArrayList<View> preorderedList = usingRenderNodeProperties ? null : buildOrderedChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); for (int i = 0; i < childrenCount; i++) { while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) { final View transientChild = mTransientViews.get(transientIndex); if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE || transientChild.getAnimation() != null) { more |= drawChild(canvas, transientChild, drawingTime); } transientIndex++; if (transientIndex >= transientCount) { transientIndex = -1; } } final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex); if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { more |= drawChild(canvas, child, drawingTime); } } while (transientIndex >= 0) { // there may be additional transient views after the normal views final View transientChild = mTransientViews.get(transientIndex); if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE || transientChild.getAnimation() != null) { more |= drawChild(canvas, transientChild, drawingTime); } transientIndex++; if (transientIndex >= transientCount) { break; } } if (preorderedList != null) preorderedList.clear(); // Draw any disappearing views that have animations if (mDisappearingChildren != null) { final ArrayList<View> disappearingChildren = mDisappearingChildren; final int disappearingCount = disappearingChildren.size() - 1; // Go backwards -- we may delete as animations finish for (int i = disappearingCount; i >= 0; i--) { final View child = disappearingChildren.get(i); more |= drawChild(canvas, child, drawingTime); } } if (usingRenderNodeProperties) canvas.insertInorderBarrier(); if (debugDraw()) { onDebugDraw(canvas); } if (clipToPadding) { canvas.restoreToCount(clipSaveCount); } // mGroupFlags might have been updated by drawChild() flags = mGroupFlags; if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) { invalidate(true); } if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 && mLayoutAnimationController.isDone() && !more) { // We want to erase the drawing cache and notify the listener after the // next frame is drawn because one extra invalidate() is caused by // drawChild() after the animation is over mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER; final Runnable end = new Runnable() { @Override public void run() { notifyAnimationListener(); } }; post(end); } }
从上面的代码可以看到,调用了drawChild方法,方法如下:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime); }
可以看到最终又调用了View的绘制,子View都绘制完成后,ViewGroup的绘制也就完成了。
Activity的绘制流程
在了解了View的绘制流程后,那么一个Activity是如何将所有的View绘制的?Activity的顶层View是一个叫做DecorView的类,而控制DecorView是从ViewRootImpl的performTraversals方法开始,下面是该方法的实现,其中mView就是DecorView。perfromTraversals方法中会调用measureHierarchy方法,而该方法又会调用perfromMeasure方法,如下:private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); try { mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } }
这里的mView就是DecorView,调用了DecorView的measure方法,DecorView继承自FrameLayout,是一个ViewGroup,接下来会让每个子View进行绘制,这样当每个View得到了宽度,高度后,这样整个Activity的View都得到了测量。
测量完成之后,perfromTravservals方法会调用perfromLayout执行布局步骤,方法如下:
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) { mLayoutRequested = false; mScrollMayChange = true; mInLayout = true; final View host = mView; if (DEBUG_ORIENTATION || DEBUG_LAYOUT) { Log.v(mTag, "Laying out " + host + " to (" + host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")"); } Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout"); try { host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); mInLayout = false; int numViewsRequestingLayout = mLayoutRequesters.size(); if (numViewsRequestingLayout > 0) { // requestLayout() was called during layout. // If no layout-request flags are set on the requesting views, there is no problem. // If some requests are still pending, then we need to clear those flags and do // a full request/measure/layout pass to handle this situation. ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, false); if (validLayoutRequesters != null) { // Set this flag to indicate that any further requests are happening during // the second pass, which may result in posting those requests to the next // frame instead mHandlingLayoutInLayoutRequest = true; // Process fresh layout requests, then measure and layout int numValidRequests = validLayoutRequesters.size(); for (int i = 0; i < numValidRequests; ++i) { final View view = validLayoutRequesters.get(i); Log.w("View", "requestLayout() improperly called by " + view + " during layout: running second layout pass"); view.requestLayout(); } measureHierarchy(host, lp, mView.getContext().getResources(), desiredWindowWidth, desiredWindowHeight); mInLayout = true; host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); mHandlingLayoutInLayoutRequest = false; // Check the valid requests again, this time without checking/clearing the // layout flags, since requests happening during the second pass get noop'd validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true); if (validLayoutRequesters != null) { final ArrayList<View> finalRequesters = validLayoutRequesters; // Post second-pass requests to the next frame getRunQueue().post(new Runnable() { @Override public void run() { int numValidRequests = finalRequesters.size(); for (int i = 0; i < numValidRequests; ++i) { final View view = finalRequesters.get(i); Log.w("View", "requestLayout() improperly called by " + view + " during second layout pass: posting in next frame"); view.requestLayout(); } } }); } } } } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } mInLayout = false; }
从上面可以看到,调用了DecorView的layout方法进行layout。接下来看DecorView的onLayout对子View的放置:
@Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); getOutsets(mOutsets); if (mOutsets.left > 0) { offsetLeftAndRight(-mOutsets.left); } if (mOutsets.top > 0) { offsetTopAndBottom(-mOutsets.top); } if (mApplyFloatingVerticalInsets) { offsetTopAndBottom(mFloatingInsets.top); } if (mApplyFloatingHorizontalInsets) { offsetLeftAndRight(mFloatingInsets.left); } // If the application changed its SystemUI metrics, we might also have to adapt // our shadow elevation. updateElevation(); mAllowUpdateElevation = true; if (changed && mResizeMode == RESIZE_MODE_DOCKED_DIVIDER) { getViewRootImpl().requestInvalidateRootRenderNode(); } }
可以看到首先调用父类的onLayout方法,FrameLayout的onLayout如下:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { layoutChildren(left, top, right, bottom, false /* no force left gravity */); } void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) { final int count = getChildCount(); final int parentLeft = getPaddingLeftWithForeground(); final int parentRight = right - left - getPaddingRightWithForeground(); final int parentTop = getPaddingTopWithForeground(); final int parentBottom = bottom - top - getPaddingBottomWithForeground(); //对所有子View进行遍历安置 for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final int width = child.getMeasuredWidth(); final int height = child.getMeasuredHeight(); int childLeft; int childTop; int gravity = lp.gravity; if (gravity == -1) { gravity = DEFAULT_CHILD_GRAVITY; } final int layoutDirection = getLayoutDirection(); final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + lp.leftMargin - lp.rightMargin; break; case Gravity.RIGHT: if (!forceLeftGravity) { childLeft = parentRight - width - lp.rightMargin; break; } case Gravity.LEFT: default: childLeft = parentLeft + lp.leftMargin; } switch (verticalGravity) { case Gravity.TOP: childTop = parentTop + lp.topMargin; break; case Gravity.CENTER_VERTICAL: childTop = parentTop + (parentBottom - parentTop - height) / 2 + lp.topMargin - lp.bottomMargin; break; case Gravity.BOTTOM: childTop = parentBottom - height - lp.bottomMargin; break; default: childTop = parentTop + lp.topMargin; } //放置子View child.layout(childLeft, childTop, childLeft + width, childTop + height); } } }
从上面可以看到FrameLayout遍历子View,对子View进行计算坐标,得到坐标后调用layout方法。这样整个layout过程就完成了。
接下来,performTravserals方法中会调用perfromDraw方法进行递归绘制。
在Activity调用了setContentView时,在onResume方法中会令DecorView为可见,此时会调用其draw方法,然后整个View树就可以都绘制出来了。
总结
View的绘制流程分为measure、layout和draw三个步骤,measure负责确定自己的大小,layout负责安排子View的位置,draw负责绘制自己。每个步骤中,整个View树都是从上往下进行迭代,而具体的操作则是由下往上,以measure为例,只有每一个子View确定了自己的大小,容器才可以确定自己的大小。理解View的绘制流程有助于自定义View和ViewGroup。
相关文章推荐
- Android中View绘制流程以及invalidate()等相关方法分析
- Android View绘制流程
- Android View绘制流程
- Android中View绘制流程以及invalidate()等相关方法分析
- Android中View绘制流程以及invalidate()等相关方法分析
- Android中View绘制流程以及invalidate()等相关方法分析
- Android中View绘制流程以及invalidate()等相关方法分析
- Android中View绘制流程以及invalidate()等相关方法分析 .
- Android中View绘制流程
- Android中View绘制流程
- Android中View绘制流程以及invalidate()等相关方法分析
- Android中View绘制流程以及invalidate()等相关方法分析
- Android View绘制流程以及invalidate()等相关方法分析
- Android中View绘制流程以及invalidate()等相关方法分析
- Android View绘制流程
- Android中View绘制流程以及invalidate()等相关方法分析
- Android中View绘制流程以及invalidate()等相关方法分析
- Android中View绘制流程以及invalidate()等相关方法分析
- android view树的绘制流程
- Android中View绘制流程以及invalidate()等相关方法分析