通过抽象的方式来讲一讲View的绘制流程

2,280 阅读16分钟

前言: 很多时候,我们在看源码时,看的时候可以理解其原理,但是过后不久又容易忘记,这是因为没有留下一个印象,知道自己看了啥,但是又感觉说不出来,这就是没有归纳总结导致的原因;
那么当前已经有很多人写了View 的绘制流程,这里会通过图文的方式来进行总结!希望对你有所帮助;

视觉效果

在开始之前,我们先来看一张图片:

很熟悉的淘宝首页,而Android的大部分界面都是图形界面,这些图形到底是怎么来的呢?系统是怎么绘制出来的呢?让我们带着思考继续看下去;

View的层级关系

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;

ViewRootIml

在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的测量

到这里,测量规格弄清楚了,接下来分析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的测量

举个熟悉的例子,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的布局

看看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的布局

看一下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的绘制

接下来分析一下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的绘制

看一下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博客;

欢迎和我沟通交流技术;