前言: 很多时候,我们在看源码时,看的时候可以理解其原理,但是过后不久又容易忘记,这是因为没有留下一个印象,知道自己看了啥,但是又感觉说不出来,这就是没有归纳总结导致的原因;
那么当前已经有很多人写了View 的绘制流程,这里会通过图文的方式来进行总结!希望对你有所帮助;
在开始之前,我们先来看一张图片:
很熟悉的淘宝首页,而Android的大部分界面都是图形界面,这些图形到底是怎么来的呢?系统是怎么绘制出来的呢?让我们带着思考继续看下去;
1,首先,先来看一下View的层级关系,View的最顶层是Activity,Activity里面是PhoneWindow,而PhoneWindow里面则是最顶层的View(DecorView),DecorView里面包含的就是我们肉眼可以看到的图形界面了,也就是上面的淘宝首页,而绘制的起点也正是从DecorView开始的;
从上面的图可以清楚的看出各个层级的关系,到了DecorView这一层,就是我们最熟悉的View树结构了;
那么到了这里,又会有一个疑问了,DrcorView里面是怎么将View树绘制出来的呢?
别急,且听我细细道来;
下面我们先来看一张图:
这张图详细表明了DecorView添加到Window的过程,这里面看到了一个很熟悉的方法,requestLayout(),这个方式是在ViewRootIml里面调用的,来看看官方API的解释:
Called when something has changed which has invalidated the layout of a child of this view parent.
这句话什么意思呢? 翻译过来的意思就是调用这个方法会导致当前视图的子View布局失效,也就是说调用这个方法会导致View树的重新layout;
在DecorView调用了requestLayout()方法之后,最终会走到View的绘制流程,前面流程的源码我这里就不贴出来了,建议看完自己跟着源码走一遍;
最终的绘制流程是在performTraversals()方法里面;
private void performTraversals() { // Ask host how big it wants to be, 执行测量操作 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); //执行布局操作 performLayout(lp, desiredWindowWidth, desiredWindowHeight); //执行绘制操作 performDraw(); } 复制代码
performTraversals()里面的源码很长很复杂,这里我们只需要关注测量,布局,绘制这几个方法即可;
看一下视图绘制的流程图:
让我们回忆一下之前的问题,答案现在已经很明显了,就是系统调用ViewRootIml类里的performTraversals()去绘制整个界面的;
什么?这就没了???
这位大侠,请放下你手中的刀,我还没讲完呢!
前面已经把View绘制流程的入口已经理清楚了,那么接下来就继续分析performTraversals()里的调用吧;
先来看一下performMeasure()方法的源码:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { if (mView == null) { return; } Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); try { mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } } 复制代码
这一段源码很简单,就是调用了View的mesure方法,这个mView就是最顶层的DecorView,这里传了(childWidthMeasureSpec, childHeightMeasureSpec)这两个参数,那么这两个参数是什么意思呢?有什么用呢?请继续往下看;
在调用performMeasure()方法之前,会先调用getRootMeasureSpec()方法来获取测量规格,也就是childWidthMeasureSpec, childHeightMeasureSpec这两个参数;
看一下getRootMeasureSpec()方法的源码:
private static int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT: // Window cant resize. Force root view to be windowSize. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); break; case ViewGroup.LayoutParams.WRAP_CONTENT: // Window can resize. Set max size for root view. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: // Window wants to be an exact size. Force root view to be that size. measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec; } 复制代码
在继续深入分析之前,先来看一下测量模式的类型:
1.MeasureSpec.EXACTLY:确定模式,父容器希望子视图View的大小是固定,也就是specSize大小。这里可以理解为有具体的大小,比如MATCH_PARENT或者10dp这种;
2.MeasureSpec.AT_MOST:最大模式,父容器希望子视图View的大小不超过父容器希望的大小,也就是不超过specSize大小。这里理解为没有固定的大小,由子类去计算,对应WRAP_CONTENT这种;
3.MeasureSpec.UNSPECIFIED: 不确定模式,子视图View请求多大就是多大,父容器不限制其大小范围,也就是size大小。这种可以理解为没有对子View添加束缚,比如列表控件,RecyclerView,ListView这种;
接下来再回到getRootMeasureSpec()这个方法中,源码根据传进来的宽高来获取测量的规格;
第一个case为ViewGroup.LayoutParams.MATCH_PARENT时:
使用了MeasureSpec.EXACTLY的测量模式,也就是有具体的大小;
第二个case为ViewGroup.LayoutParams.WRAP_CONTENT时:
使用了MeasureSpec.AT_MOST的测量模式,也就是没有具体的大小;
第三个case为MeasureSpec.UNSPECIFIED:
和第一个相同;
DecorView默认的宽高为MATCH_PARENT,那么这里就会走第一个case去获取测量规格,也就是说最顶层的测量规格就是从这里获取的;
到这里,测量规格弄清楚了,接下来分析View的measure()方法;
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { // 判断是否需要强制布局,也就是会触发重新测量 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) { onMeasure(widthMeasureSpec, heightMeasureSpec); } } 复制代码
这里主要判断是否需要重新测量,如果需要则调用onMeasure()去测量;
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } 复制代码
onMeasure()方法很简单就几行代码,如果子类没有重写这个方法去测量宽高,则使用默认的方法getDefaultSize()去获取宽高,然后再调用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; } 复制代码
通过宽高size和测量的规格measureSpec来计算最终的宽高,也就是说如果布局里面的子View没有重新onMeasure()时,则会使用默认的方法来获取宽高,那么布局使用测量模式为MeasureSpec.EXACTLY和MeasureSpec.AT_MOST时,宽高都是返回由测量模式和具体大小计算之后的值specSize;
那么到这里测量的方法差不多就分析完了,但是还有一个疑问,也就是View树是怎么测量的呢?
接下来继续分析;
前面分析的是子View没有重新onMeasure()的情况,接下来分析子View重写了onMeasure()的情况;
举个熟悉的例子,LinearLayout控件是我们最常用的ViewGroup控件,下面以这个为例子来进行分析;
看一下LinearLayout控件的onMeasure()方法:
rotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); } } 复制代码
再看一下measureVertical的方法:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { // See how tall everyone is. Also remember max width. for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i);\ ... measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, usedHeight); } ... // Check against our minimum width maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState); } 复制代码
这源码里面通过遍历当前的子View,然后通过measureChildBeforeLayout()去测量子View的宽高,并通过计算子View的宽高来调用setMeasuredDimension()设置LinearLayout的宽高,而测量子View 的方法里面最终调用的是ViewGroup的measureChildWithMargins()方法;
void measureChildBeforeLayout(View child, int childIndex, int widthMeasureSpec, int totalWidth, int heightMeasureSpec, int totalHeight) { measureChildWithMargins(child, widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight); } 复制代码
ViewGroup里的方法最终调用了View里的measure方法,而ViewGroup里面也自定义了获取测量模式的方法getChildMeasureSpec(); 这里细节就不过多关注了;
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); } 复制代码
那么到这里,ViewRootIml的performMeasure()方法的流程就可以总结为下面这张图:
整个View树的测量流程就是通过这种递归的方式,一步步的测量完成的;
总结:
1,View树的测量是通过递归的方式测量完成的,递归的方法为View的measure()方法;
2,View和ViewGroup都有自己的测量模式的方法,当然子View也可以自定义获取测量模式的方法;
3,View树测量结束之后,会调用setMeasuredDimension()让之前测量的宽高设置生效,这个方法是在递归结束之后,通过View树的最底层往上传递的;
4,子View的大小是由父视图和子视图共同决定的;
测量的流程已经讲完了,接下来开始讲布局的流程,既然测量的流程是通过递归的方式,那么布局的流程是不是也?
是的,没错,也是通过递归的方式;
别急,且请我细细道来!
对View进行布局的目的是计算出View的尺寸以及在其父控件中的位置,具体来说就是计算出View的四条边界分别到其父控件左边界、上边界的距离,即计算View的left、top、right、bottom的值。
先来看一下performLayout()方法的源码:
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) { // 标记布局开始 mInLayout = true; final View host = mView; ... try { host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); ... } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } // 标记布局结束 mInLayout = false; } 复制代码
这里调用了mView的layout()方法,这个mView就是最顶层的DecorView,而layout()方法则为View里的方法;
看看View的layout()方法里面做了啥?
public void layout(int l, int t, int r, int b) { ... int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; //如果isLayoutModeOptical()返回true,那么就会执行setOpticalFrame()方法, //否则会执行setFrame()方法。并且setOpticalFrame()内部会调用setFrame(), //所以无论如何都会执行setFrame()方法。 //setFrame()方法会将View新的left、top、right、bottom存储到View的成员变量中 //并且返回一个boolean值,如果返回true表示View的位置或尺寸发生了变化, //否则表示未发生变化 boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { //如果View的布局发生了变化,或者mPrivateFlags有需要LAYOUT的标签PFLAG_LAYOUT_REQUIRED, //那么就会执行以下代码 //首先会触发onLayout方法的执行,View中默认的onLayout方法是个空方法 //不过继承自ViewGroup的类都需要实现onLayout方法,从而在onLayout方法中依次循环子View, //并调用子View的layout方法 onLayout(changed, l, t, r, b); ... } ... } 复制代码
这里只需要关注setFrame()方法和onLayout()方法即可,onLayout()方法由子类实现,先来看一下setFrame()方法;
protected boolean setFrame(int left, int top, int right, int bottom) { boolean changed = false; if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) { //将新旧left、right、top、bottom进行对比,只要不完全相对就说明View的布局发生了变化, //则将changed变量设置为true changed = true; ... // 分别计算View的新旧尺寸 int oldWidth = mRight - mLeft; int oldHeight = mBottom - mTop; int newWidth = right - left; int newHeight = bottom - top; // 比较View的新旧尺寸是否相同,如果尺寸发生了变化,那么sizeChanged的值为true boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight); ... // 将新的left、top、right、bottom存储到View的成员变量中 mLeft = left; mTop = top; mRight = right; mBottom = bottom; mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom); ... //如果View的尺寸和之前相比发生了变化,那么就执行sizeChange()方法, //该方法中又会调用onSizeChanged()方法,并将View的新旧尺寸传递进去 if (sizeChanged) { sizeChange(newWidth, newHeight, oldWidth, oldHeight); } ... } return changed; } 复制代码
在该方法中,会将新旧left、right、top、bottom进行对比,只要不完全相同就说明View的布局发生了变化,则将changed变量设置为true。然后比较View的新旧尺寸是否相同,如果尺寸发生了变化,并将其保存到变量sizeChanged中。如果尺寸发生了变化,那么sizeChanged的值为true。
然后将新的left、top、right、bottom存储到View的成员变量中保存下来。并执行mRenderNode.setLeftTopRightBottom()方法会,其会调用RenderNode中原生方法的nSetLeftTopRightBottom()方法,该方法会根据left、top、right、bottom更新用于渲染的显示列表。
而onLayout()方法由子类实现,如果子类是View的话,则方法不需要实现,如果是ViewGroup的话,因为方法为抽象方法,那么必须由子类实现;这里通过LinearLayout的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); } } 复制代码
查看其中一种方法layoutVertical();
void layoutVertical(int left, int top, int right, int bottom) { ... for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); ... } } } 复制代码
通过遍历所有的子View,调用setChildFrame()进行布局,再看一下setChildFrame()方法的源码;
private void setChildFrame(View child, int left, int top, int width, int height) { child.layout(left, top, left + width, top + height); } 复制代码
这里调用了View的layout()的方法来进行子View的layout;
到这里,布局的流程就分析完了,看一下流程图:
总结:
1,View树的布局是通过递归的方式测量完成的,递归的方法为View的layout()方法;
2,View和ViewGroup都有onLayout()方法,但是ViewGroup的方法是抽象的,必须由子类实现,View的布局是由ViewGroup来控制的,也就是说View并不需要进行onLayout();
3,使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值;
那么到这里,布局的流程就已经讲完了,接下来分析绘制的流程;
既然测量,和布局都是用递归的方式,那绘制岂不是也?
是的,继续往下看,理解了一个之后,其他理解起来也不难!
最后一步的绘制会将页面展示在我们面前,前面的操作都只是准备工作;
先来看一下performDraw()的源码:
private void performDraw() { ... try { boolean canUseAsync = draw(fullRedrawNeeded); ... } finally { mIsDrawing = false; Trace.traceEnd(Trace.TRACE_TAG_VIEW); } } 复制代码
这里调用了ViewRootIml里面的draw()方法,跟踪源码发现最终调用的是drawSoftware()方法;
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty, Rect surfaceInsets) { try { // 从surface里面获取canvas对象 canvas = mSurface.lockCanvas(dirty); ... } catch (Surface.OutOfResourcesException e) { handleOutOfResourcesException(e); return false; } catch (IllegalArgumentException e) { ... return false; } finally { dirty.offset(dirtyXOffset, dirtyYOffset); // Reset to the original value. } try { if (DEBUG_ORIENTATION || DEBUG_DRAW) { Log.v(mTag, "Surface " + surface + " drawing to bitmap w=" + canvas.getWidth() + ", h=" + canvas.getHeight()); //canvas.drawARGB(255, 255, 0, 0); } ... try { ... // 调用View的draw()方法 mView.draw(canvas); } finally { ... } } finally { ... } return true; } 复制代码
drawSoftware()方法里面先从mSurface获取canvas对象,然后通过mView调用draw()方法时,将canvas作为参数传进去;最后调用的是View的draw()方法;
接下来分析一下View的draw()方法;
public void draw(Canvas canvas) { /* * 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; } /* * 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) { if (drawTop) { canvas.saveUnclippedLayer(left, top, right, top + length); } if (drawBottom) { canvas.saveUnclippedLayer(left, bottom - length, right, bottom); } if (drawLeft) { canvas.saveUnclippedLayer(left, top, left + length, bottom); } if (drawRight) { canvas.saveUnclippedLayer(right - length, top, right, bottom); } } 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); 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); if (debugDraw()) { debugDrawFocus(canvas); } } 复制代码
从上面的源码分析得知,View的draw()方法总共分为6步:
1,绘制视图的背景;
2,如果需要,保存画布的图层以备渐变用;
3,绘制当前视图的内容;
4,绘制子View的视图;
5,如果需要,绘制视图的渐变效果并恢复画布;
6,绘制装饰(比如滚动条scrollbars);
总结为流程图如下:
这里需要关注的是第二步和第四步,第二步是通过调用onDraw()方法绘制当前视图的内容,第四步是调用dispatchDraw()来绘制子View的视图;
先来看一下View的onDraw()方法:
protected void onDraw(Canvas canvas) {} 复制代码
是一个空方法,交由子类去实现;如果实现来自定义View,那么就得重新该方法去实现绘制的逻辑;
再看一下dispatchDraw()方法:
protected void dispatchDraw(Canvas canvas) {} 复制代码
这个方法也是个空方法,也是要交由子类去实现;
查看源码的实现有很多个,这里我们只关注ViewGroup的实现逻辑;
看一下ViewGroup的实现逻辑:
protected void dispatchDraw(Canvas canvas) { if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) { final boolean buildCache = !isHardwareAccelerated(); for (int i = 0; i < childrenCount; i++) { final View child = children[i]; // 遍历子View设置动画效果 if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { final LayoutParams params = child.getLayoutParams(); attachLayoutAnimationParameters(child, params, i, childrenCount); bindLayoutAnimation(child); } } ... } ... 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) { // 遍历子View进行绘制 more |= drawChild(canvas, transientChild, drawingTime); } ... } ... } ... } 复制代码
源码里面通过遍历所有的子View,调用drawChild()来进行绘制,继续跟进drawChild()方法里面;
protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime); } 复制代码
这里又看到了熟悉的View,调用了View的draw()方法绘制子View;
到这里,performDraw()方法的流程就讲完了,看一下总结流程图:
总结:
1,View树的绘制流程也是通过递归的方式来进行绘制的,递归的方法为View的draw()方法;
2,ViewGroup和View都可以重新onDraw()方法来实现绘制的逻辑,子View不需要重写dispatchDraw()方法;
3,绘制视图的背景,渐变效果和装饰都是在View的draw()方法里面调用的;
performTraversals()方法的流程分析完毕了,现在终于知道了View的绘制流程为什么分为onMeasure(),onLayout(),onDraw()这三个步骤了;贴出来的源码省略了很多细节,主要是为了把绘制的流程理清,建议可以自己跟着源码去走一遍;
1,www.jianshu.com/p/58d22426e…
2,blog.csdn.net/feiduclear_…
3,blog.csdn.net/luoshengyan…
4,www.jianshu.com/p/4a68f9dc8…
5,www.2cto.com/kf/201512/4…
如果我的文章对你有帮助的话,请给我点个❤️,也可以关注一下我的Github和博客;
欢迎和我沟通交流技术;