美文网首页
通过抽象的方式来讲一讲View的绘制流程

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

作者: Amter | 来源:发表于2020-02-19 12:17 被阅读0次

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

    视觉效果

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

    image

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

    View的层级关系

    1,首先,先来看一下View的层级关系,View的最顶层是Activity,Activity里面是PhoneWindow,而PhoneWindow里面则是最顶层的View(DecorView),DecorView里面包含的就是我们肉眼可以看到的图形界面了,也就是上面的淘宝首页,而绘制的起点也正是从DecorView开始的;

    image

    从上面的图可以清楚的看出各个层级的关系,到了DecorView这一层,就是我们最熟悉的View树结构了;

    那么到了这里,又会有一个疑问了,DrcorView里面是怎么将View树绘制出来的呢?

    别急,且听我细细道来;

    image

    下面我们先来看一张图:

    image

    这张图详细表明了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()里面的源码很长很复杂,这里我们只需要关注测量,布局,绘制这几个方法即可;

    看一下视图绘制的流程图:

    image

    让我们回忆一下之前的问题,答案现在已经很明显了,就是系统调用ViewRootIml类里的performTraversals()去绘制整个界面的;

    什么?这就没了???

    image

    这位大侠,请放下你手中的刀,我还没讲完呢!

    前面已经把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.<font color=#ff0000 face="黑体">MeasureSpec.EXACTLY</font>:确定模式,父容器希望子视图View的大小是固定,也就是specSize大小。这里可以理解为有具体的大小,比如MATCH_PARENT或者10dp这种;

    2.<font color=#ff0000 face="黑体">MeasureSpec.AT_MOST</font>:最大模式,父容器希望子视图View的大小不超过父容器希望的大小,也就是不超过specSize大小。这里理解为没有固定的大小,由子类去计算,对应WRAP_CONTENT这种;

    3.<font color=#ff0000 face="黑体">MeasureSpec.UNSPECIFIED</font>: 不确定模式,子视图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()方法的流程就可以总结为下面这张图:

    image

    整个View树的测量流程就是通过这种递归的方式,一步步的测量完成的;

    总结:
    1,View树的测量是通过递归的方式测量完成的,递归的方法为View的measure()方法;
    2,View和ViewGroup都有自己的测量模式的方法,当然子View也可以自定义获取测量模式的方法;
    3,View树测量结束之后,会调用setMeasuredDimension()让之前测量的宽高设置生效,这个方法是在递归结束之后,通过View树的最底层往上传递的;
    4,子View的大小是由父视图和子视图共同决定的;

    测量的流程已经讲完了,接下来开始讲布局的流程,既然测量的流程是通过递归的方式,那么布局的流程是不是也?

    是的,没错,也是通过递归的方式;

    image

    别急,且请我细细道来!

    布局

    对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;

    到这里,布局的流程就分析完了,看一下流程图:

    image

    总结:
    1,View树的布局是通过递归的方式测量完成的,递归的方法为View的layout()方法;
    2,View和ViewGroup都有onLayout()方法,但是ViewGroup的方法是抽象的,必须由子类实现,View的布局是由ViewGroup来控制的,也就是说View并不需要进行onLayout();
    3,使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值;

    那么到这里,布局的流程就已经讲完了,接下来分析绘制的流程;

    既然测量,和布局都是用递归的方式,那绘制岂不是也?

    是的,继续往下看,理解了一个之后,其他理解起来也不难!

    image

    绘制

    最后一步的绘制会将页面展示在我们面前,前面的操作都只是准备工作;

    先来看一下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);

    总结为流程图如下:

    image

    这里需要关注的是第二步和第四步,第二步是通过调用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()方法的流程就讲完了,看一下总结流程图:

    image

    总结:
    1,View树的绘制流程也是通过递归的方式来进行绘制的,递归的方法为View的draw()方法;
    2,ViewGroup和View都可以重新onDraw()方法来实现绘制的逻辑,子View不需要重写dispatchDraw()方法;
    3,绘制视图的背景,渐变效果和装饰都是在View的draw()方法里面调用的;

    最后总结:

    performTraversals()方法的流程分析完毕了,现在终于知道了View的绘制流程为什么分为onMeasure(),onLayout(),onDraw()这三个步骤了;贴出来的源码省略了很多细节,主要是为了把绘制的流程理清,建议可以自己跟着源码去走一遍;

    参考:

    1,https://www.jianshu.com/p/58d22426e79e
    2,https://blog.csdn.net/feiduclear_up/article/details/46772477
    3,https://blog.csdn.net/luoshengyang/article/details/8303098
    4,https://www.jianshu.com/p/4a68f9dc8f7c
    5,https://www.2cto.com/kf/201512/454595.html

    关于我

    如果我的文章对你有帮助的话,请给我点个❤️,也可以关注一下我的Github博客;

    欢迎和我沟通交流技术;

    相关文章

      网友评论

          本文标题:通过抽象的方式来讲一讲View的绘制流程

          本文链接:https://www.haomeiwen.com/subject/pbfafhtx.html