Android(安卓)View相关-View的绘制流程浅析
从本章节开始,我们来共同学习下Android中比较重要的View相关知识,这一节我们先来看View的绘制流程。
我们知道,Android中的任何一个布局、控件,其最终都会直接或者间接地继承View类(ViewGroup最终也继承了View),也就是说所有相关的控件或者布局都会使用同样的绘制流程。我们知道Android绘制流程的起点是在ViewRootImpl类的performTraversals()方法。这个方法主要作用是根据之前设置状态来判断是否需要重新计算视图大小(Measure),是否需要重新放置视图位置(layout),以及是否需要重新绘制View(draw)。这个方法里有巨量的代码和逻辑,详读下来太过伤神,我们删减代码看下这个类的核心内容:
private void performTraversals() { ... //获取了尺寸规格,这是一个32位的int值,前两位代表测量模式(SpecMode),后30位代表数值(SpecSize), int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); ... //后续所有View的MeasureSpec都是从这里来的 mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); ... mView.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); ... mView.draw(canvas);}
可以看到,这里通过performTraversals()来判断是否对控件进行测量放置绘制等等,与View绘制过程相关的几个重要步骤有measure(测量)、layout(位置)、draw(绘制),其执行流程是:
下面我们就这三个步骤一一来进行讲解:
Measure
我们知道,我们的所有控件布局归根结底都是ViewGroup或者View(ViewGroup也是一种特殊的View,在谈到绘制流程和事件分发的时候我们都会将两者区分开来讲)。我们先来看看View类的measure方法:
/** * 这个方法是为了确定View的大小,并且会为子View提供宽高的约束条件 *为整个View树计算实际的大小,然后设置实际的高和宽,每个View控件的实际宽高都是由父视图和自身决定的。 *实际的测量是在onMeasure方法进行,所以在View的子类需要重写onMeasure方法, *这是因为measure方法是final的,不允许重载,所以View子类只能通过重载onMeasure来实现自己的测量逻辑。 */public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth); heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight); ... onMeasure(widthMeasureSpec, heightMeasureSpec); ...}/** * 继承View若有需求需要复写该方法,因为measure是final的 * */protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}
我们从上面ViewRootImpl的performTraversals方法可以看到,这里measure方法的两个尺寸规格是由其父控件传过来的,宽高的尺寸规格是一个32位的int型变量,其中存储了测量所需要的信息。其中前两位存储了测量模式,测量模式总共有三种:
- MeasureSpec.EXACTLY:表示确定大小
- MeasureSpec.AT_MOST:表示获取最大尺寸
- MeasureSpec.UNSPECIFIED:不确定
后三十位保存了大小也就是SpecSize,也就是父控件的大小。对于系统Window类的DecorVIew对象Mode一般都为MeasureSpec.EXACTLY ,而size分别对应屏幕宽高。对于子View来说大小是由父View和子View共同决定的。
对于非ViewGroup的View来说,具体测量会在onMeasure中完成,View默认的setMeasuredDimension就可以很好地帮助系统设置View的大小。我们来看看View默认进行measure,最后设置的宽高是多少呢:
protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());}protected int getSuggestedMinimumHeight() { return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());}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;}
可以看到, 这里获取了测量模式,并且根据不同的测量模式对宽高进行了初始化,若是UNSPECIFIED,则设置为最小值(最小值由该View的背景和minHeight属性决定),若为AT_MOST或者EXACTLY,则设置为父控件的最大宽高。那么到这里最底部View的测量流程就完成了。
前面我们也说到ViewGroup的测量过程和View有些许不同:
我们可以看到ViewGroup中给我们提供了四个供我们测量使用的方法,分别是measureChildren、measureChild、measureChildWithMargin、getChildMeasureSpec
我们知道View是一个嵌套的树结构,其measure执行流程也是一个递归的过程。我们以一个ViewGroup的子类LinearLayout的测量过程来看:
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); }}void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { measureChildBeforeLayout( child, i, widthMeasureSpec, 0, heightMeasureSpec, totalWeight == 0 ? mTotalLength : 0);}void measureChildBeforeLayout(View child, int childIndex, int widthMeasureSpec, int totalWidth, int heightMeasureSpec, int totalHeight) { measureChildWithMargins(child, widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight);}
可以看到LinearLayout经过一系列调用之后,走到了ViewGroup的measureChildWithMargins方法,下面我们来看一看这个方法:
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}
可以很明显地看到,这里将子View的padding信息和margin信息以及其他子View已经占用的大小相加然后进行计算,获取了子View的尺寸规格,然后调用子View的measure方法完成测量。这里有一个很重要的方法getChildMeasureSpec(),我们来看看这个方法中完成了什么操作:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) { //获取测量模式和测量尺寸 int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); //获取最大尺寸 int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; //根据测量模式分别计算 switch (specMode) { //该ViewGroup测量模式是EXACTLY确定大小 case MeasureSpec.EXACTLY: if (childDimension >= 0) { //子View的宽高大于等于0,说明设置过,则将其设置为EXACTLY,size为原大小 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { //子View请求获取父控件大小,设置为EXACTLY并将父控件剩余最大尺寸设置给该测量模式 resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { //子View要求获取自适应大小,设置为AT_MOST并将父控件剩余最大尺寸设置给该测量模式 resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; //该ViewGroup测量模式为AT_MOST case MeasureSpec.AT_MOST: if (childDimension >= 0) { //子View的宽高大于等于0,说明设置过,则将其设置为EXACTLY,size为原大小 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { //子View请求获取父控件大小,设置为与父控件相同的AT_MOST并将父控件剩余最大尺寸设置给该测量模式 resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { //子View要求获取自适应大小,设置为AT_MOST并将父控件剩余最大尺寸设置给该测量模式 resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; //该ViewGroup测量模式为UNSPECIFIED case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { //子View的宽高大于等于0,说明设置过,则将其设置为EXACTLY,size为原大小 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { //子View请求获取父控件大小,设置为与父控件相同的UNSPECIFIED并将父控件剩余最大尺寸设置给该测量模式 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { //子View要求获取自适应大小,设置为UNSPECIFIED并将父控件剩余最大尺寸设置给该测量模式 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } //返回一个MesureSpec return MeasureSpec.makeMeasureSpec(resultSize, resultMode);}
可以看到,该方法使用自身的MesureSpec和子View的宽高值来最终确定子View的MesureSpec。
到这里我们就搞清楚了ViewGroup的测量过程:
- 首先调用ViewGroup的mesure方法,我们一般继承ViewGroup会复写其onMeasure方法,如前面的LinearLayout所示
- 调用ViewGroup的onMeasure方法,在方法里对其所有子View进行遍历并测量子View的大小
- 使用自身measure方法中产生的MeasureSpec和子View大小来确定子View的尺寸规格,并交给子VIew的measure方法去测量
- measure会默认调用setMeasuredDimension()通过最小尺寸和子VIew的MeasureSpec来完成最终测量。
测量过程总结
- MeasureSpec:测量规格,32位的int类型数字,前两位保存测量模式SpecMode,后三十位保存具体尺寸SpecSize。其中SpecMode分为三种:
MeasureSpec.EXACTLY //确定大小,父View希望子View的大小是确定的,由specSize决定;MeasureSpec.AT_MOST //最大尺寸,父View希望子View的大小最多是specSize指定的值;MeasureSpec.UNSPECIFIED //未指定,父View完全依据子View的设计值来决定;
- 若要复写View的measure方法,由于measure方法是final关键字修饰的因此不能被覆盖,若需要可以复写onMeasure方法
- 最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的(LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY指定大小,specSize为屏幕大小)
- ViewGroup内部提供了measureChildren、measureChild、measureChildWithMargin、getChildMeasureSpec等方法简化其子类获取MeasureSpec的方式
- 一个ViewGroup的大小由其子View和其父View共同决定
- View的测量流程是measure -> onMeasure -> setMeasuredDimension
layout
与测量流程类似,layout的过程也是一个递归调用,我们将目光转回到iewRootImpl类的performTraversals()方法中,可以看到继measure方法之后,紧着着调用了mView.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
这么一个方法,我们来查看这个方法的代码:
public void layout(int l, int t, int r, int b) { ... 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); ... }}
与测量过程相类似,最终layout方法调用了onLayout,View类中的onLayout是一个空方法
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}
那我们顺藤摸瓜,去看看ViewGroup中的layout和onLayout:
@Overridepublic final void layout(int l, int t, int r, int b) { if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) { if (mTransition != null) { mTransition.layoutChange(this); } super.layout(l, t, r, b); } else { // record the fact that we noop'd it; request layout when transition finishes mLayoutCalledWhileSuppressed = true; }}/** * {@inheritDoc} */@Overrideprotected abstract void onLayout(boolean changed, int l, int t, int r, int b);
这里可以看到layout方法是一个final修饰的,不能被子类复写的方法,而onLayout是一个抽象方法,这也就要求了其子类必须实现该方法。这里我们看不出头绪,那同样我们打开刚才的LinearLayout来看看它复写了onLayout在其中做了什么:
@Overrideprotected 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); }}//横向排列void layoutHorizontal(int left, int top, int right, int bottom) { final boolean isLayoutRtl = ViewUtils.isLayoutRtl(this); final int paddingTop = getPaddingTop(); int childTop; int childLeft; //获取当前ViewGroup高度(bottom - top) final int height = bottom - top; //获取底部View的位置 int childBottom = height - getPaddingBottom(); //ViewGroup可供子View 显示的高度 int childSpace = height - paddingTop - getPaddingBottom(); //获取子View个数 final int count = getVirtualChildCount(); //获取Gravity相关信息 final int majorGravity = mGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK; final int minorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; final boolean baselineAligned = mBaselineAligned; final int[] maxAscent = mMaxAscent; final int[] maxDescent = mMaxDescent; final int layoutDirection = ViewCompat.getLayoutDirection(this); //根据Gravity来设置childLeft值 switch (GravityCompat.getAbsoluteGravity(majorGravity, layoutDirection)) { case Gravity.RIGHT: // mTotalLength contains the padding already childLeft = getPaddingLeft() + right - left - mTotalLength; break; case Gravity.CENTER_HORIZONTAL: // mTotalLength contains the padding already childLeft = getPaddingLeft() + (right - left - mTotalLength) / 2; break; case Gravity.LEFT: default: childLeft = getPaddingLeft(); break; } int start = 0; int dir = 1; //In case of RTL, start drawing from the last child. if (isLayoutRtl) { start = count - 1; dir = -1; } //对各个子View进行遍历 for (int i = 0; i < count; i++) { int childIndex = start + dir * i; final View child = getVirtualChildAt(childIndex); if (child == null) { childLeft += measureNullChild(childIndex); } else if (child.getVisibility() != GONE) { //获取子View的宽度和高度(onMeasure计算出来的) final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); int childBaseline = -1; final LinearLayoutCompat.LayoutParams lp = (LinearLayoutCompat.LayoutParams) child.getLayoutParams(); if (baselineAligned && lp.height != LayoutParams.MATCH_PARENT) { childBaseline = child.getBaseline(); } int gravity = lp.gravity; if (gravity < 0) { gravity = minorGravity; } //根据Gravity来设置childTop switch (gravity & Gravity.VERTICAL_GRAVITY_MASK) { case Gravity.TOP: childTop = paddingTop + lp.topMargin; if (childBaseline != -1) { childTop += maxAscent[INDEX_TOP] - childBaseline; } break; case Gravity.CENTER_VERTICAL: // Removed support for baseline alignment when layout_gravity or // gravity == center_vertical. See bug #1038483. // Keep the code around if we need to re-enable this feature // if (childBaseline != -1) { // // Align baselines vertically only if the child is smaller than us // if (childSpace - childHeight > 0) { // childTop = paddingTop + (childSpace / 2) - childBaseline; // } else { // childTop = paddingTop + (childSpace - childHeight) / 2; // } // } else { childTop = paddingTop + ((childSpace - childHeight) / 2) + lp.topMargin - lp.bottomMargin; break; case Gravity.BOTTOM: childTop = childBottom - childHeight - lp.bottomMargin; if (childBaseline != -1) { int descent = child.getMeasuredHeight() - childBaseline; childTop -= (maxDescent[INDEX_BOTTOM] - descent); } break; default: childTop = paddingTop; break; } //累加childLeft if (hasDividerBeforeChildAt(childIndex)) { childLeft += mDividerWidth; } childLeft += lp.leftMargin; //调用子View的layout方法 setChildFrame(child, childLeft + getLocationOffset(child), childTop, childWidth, childHeight); //累加childLeft childLeft += childWidth + lp.rightMargin + getNextLocationOffset(child); i += getChildrenSkipCount(child, childIndex); } }}private void setChildFrame(View child, int left, int top, int width, int height) { child.layout(left, top, left + width, top + height);}
从代码中我们可以看出,一般获取到onMeasure设置好的宽高,然后对子View进行遍历,根据控件自身需求对子VIew的摆放位置进行设置并调用子View的layout方法进行摆放。
这就是整个layout过程的总结,比measure要简单一些,而且View中我们可以不用关心layout过程
layout过程总结
整个layout过程比较简单,从顶层View向下递归调用子View的layout方法的过程,其中ViewGroup通过measure过程中获取到的子View宽高来确定子View的left、top、right、bottom等值,并通过这些值来将各个View放到合适的位置
- View的layout方法可以被复写,而ViewGroup的layout方法被final关键字修饰,不能再被复写
- 使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值。
- measure操作完成后得到各View的measuredWidth和measuredHeight,layout操作完成后得到每个View的left、top、right、bottom(相对于父控件)
draw
接下来就到了View绘制流程的最后一步,我们可以在ViewRootImpl的performTraversals()方法看到:
final Canvas canvas;canvas = mSurface.lockCanvas(dirty);mView.draw(canvas);
ok,我们继续来看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); // 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);}
从注释中可以看出绘制过程分为6个步骤:
- 对View背景进行绘制,这一步调用其私有方法drawBackground来实现,其内部牵扯到Canvas的使用,后期会单开文章讲解
保存画布层次(可以略过)- 对View内容进行绘制,调用onDraw来实现,默认onDraw是一个空方法,需要我们自身来实现
- 使用dispatchDraw对所有子View进行draw操作(View类中是空方法,因为没有字View)
绘制渐变效果并回复图层(可以略过)- 绘制前景色、滚动条,调用onDrawForeground实现
这里我们需要关注ViewGroup中的dispatchDraw方法:
/** * {@inheritDoc} */@Overrideprotected void dispatchDraw(Canvas canvas) { //核心代码,遍历挨个调用子View的draw方法 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; } } int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(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; } }}protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime);}
可以看到最终调用还是回到了View的draw方法,而后开始执行上述流程。
绘制流程总结
绘制就是将View显示到屏幕上的一个过程,过程中有一些细节是需要我们多多注意的:
- View默认只会绘制背景,具体内容的绘制需要在子类中实现,也就是需要在onDraw方法中来进行绘制
- ViewGroup需要复写dispatchDraw方法来绘制所有子View
- Android中绘制流程离不开画布Canvas的支持(后面也会介绍paint)
- 默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以复写ViewGroup.getChildDrawingOrder()方法提供不同顺序。
总结
到这里我们Android的View绘制流程就算结束了,洋洋洒洒很多东西,但是核心的东西其实并不多,只要深入理解了递归,就能明白整个的流程是怎么样的了。
在接下来我会尝试去理解View和ViewGroup中使用较多的invalidate和postInvalidate以及requestLayout,来解析这三个方法的使用方法和原理以及它们的作用,在接下来也会尝试学习自定义View的过程和Canvas等的使用,敬请期待~
我的个人博客,欢迎前来交流~
enjoy~
更多相关文章
- 《Android开发艺术探索》第十章重点笔记
- android中shape绘制背景图片
- Android(安卓)Service
- Android(安卓)4.0硬件加速及绘制技巧
- Android中滑屏初探 ---- scrollTo 以及 scrollBy方法使用说明
- Android中集成第三方库的方法和问题
- android的intent使用方法
- 浅谈Java中Collections.sort对List排序的两种方法
- Python list sort方法的具体使用