View 的 layout 布局和 draw 绘制过程

上篇文章View 的测量分析了 View 的工作原理中最复杂的测量过程,接着测量过程的是布局和绘制的过程,这里两个过程相对比较简单,所以放到一篇文章中来写

View 的测量过程中,确定了 View 的测量宽高的信息,布局过程则是为了确定 View 在其父 View 中的位置以及 ViewGroup 确定其所有子 View 元素的位置;布局结束后会执行绘制过程,绘制过程将 View 需要显示的内容绘制到屏幕上

一、layout (布局)过程

依旧从 ViewRootImpl 的 performLayout 方法开始,其中调用 DecorView 的 layout 方法,layout 方法是在 View 类中定义和实现的,其中 getMeasuredWidth、getMeasuredHeight 方法得到的是 DecorView 在测量过程中确定的测量宽高

// ViewRootImpl
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

// View
public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }

    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;

    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);
        ...
    }
    ...
}
// View
protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;
    ...
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;
    ...
    }

layout 方法中调用 setFrame 方法,可以看到 setFrame 方法中保存了 View 四个顶点在父 View 中的位置,四个顶点确定,View在父容器中的位置也就确定,View 的实际宽高也就确定,接着就会调用 onLayout 方法

View 的 onLayout 方法是个空实现,说明 View 在布局过程中的任务就是确定自己在父 View 中的位置,确定了在父 View 中的位置后也就确定了子 View 的最终宽高,如果不重写 View 的 layout 方法,其最终宽高与测量宽高相等,如果重写了 layout 方法,并且子 View 的位置不以测量宽高来确定,此时 View 的最终宽高将不等于测量宽高。测量宽高赋值于测量过程,最终宽高赋值于布局过程,两者赋值时机不同

ViewGroup 中 onLayout 是个抽象方法,其子类必须重写该方法以确定其所有子 View 的位置,下面重点分析 ViewGroup 的 onLayout 方法

1、ViewGroup 的 onLayout 方法

由于 ViewGroup 的 onLayout 方法是个抽象方法,所以我们选一个特定的 ViewGroup 实现类来分析,这里分析 LinearLayout 的 onLayout() 方法

// LinearLayout
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);
    }
}

// LinearLayout
void layoutVertical(int left, int top, int right, int bottom) {
    final int paddingLeft = mPaddingLeft;

    int childTop;
    int childLeft;
        
    // Where right end of child should go
    final int width = right - left;
    int childRight = width - mPaddingRight;
    
    // Space available for child
    int childSpace = width - paddingLeft - mPaddingRight;
    
    final int count = getVirtualChildCount(); // 子 View 数量

    final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
    final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

    switch (majorGravity) {
       case Gravity.BOTTOM:
           // mTotalLength contains the padding already
           childTop = mPaddingTop + bottom - top - mTotalLength;
           break;

           // mTotalLength contains the padding already
           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
           break;

       case Gravity.TOP:
       default:
           childTop = mPaddingTop;
           break;
    }
    
    // 遍历所有子 View 
    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            childTop += measureNullChild(i);
        } else if (child.getVisibility() != GONE) {
        
            // 获取子 View 由测量过程确定的的测量宽高
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();
            
            final LinearLayout.LayoutParams lp =
                    (LinearLayout.LayoutParams) child.getLayoutParams();
            
            int gravity = lp.gravity;
            if (gravity < 0) {
                gravity = minorGravity;
            }
            final int layoutDirection = getLayoutDirection();
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            
            // 确定子 View 的 left 位置
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                            + lp.leftMargin - lp.rightMargin;
                    break;

                case Gravity.RIGHT:
                    childLeft = childRight - childWidth - lp.rightMargin;
                    break;

                case Gravity.LEFT:
                default:
                    childLeft = paddingLeft + lp.leftMargin;
                    break;
            }

            if (hasDividerBeforeChildAt(i)) {
                childTop += mDividerHeight;
            }

            childTop += lp.topMargin; // 确定子 View 的 top 位置
            
            // 
            setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                    childWidth, childHeight);
            
            // 确定下一子 View 的 top 位置
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

            i += getChildrenSkipCount(child, i);
        }
    }
}

// LinearLayout
private void setChildFrame(View child, int left, int top, int width, int height) {        
    child.layout(left, top, left + width, top + height);
}

LinearLayout 的 onLayout 方法中根据设置的 View 线性排列的方向确定如果实现布局,以竖直排列的 情况为例,会调用 layoutVertical 方法

layoutVertical 中会遍历所有的子 View,由子 View 的测量宽高和 ViewGroup 自身业务逻辑确定子 View 的四个顶点在 ViewGroup 中的位置,并通过 setChildFrame 方法来调用子 View 的 layout 方法。从而将布局过程从 ViewGroup 传递到 View 中,View 中的 layout 方法上面已经分析了,作用为确定自己四个顶点在父 View 中的位置。这样一层一层传递下去就完成了 View 视图树的 layout 过程。

1. ViewGroup 的布局过程的作用为先确定自己在父容器的位置,再确定子 View 在该 ViewGroup 中的位置,子 View 的 layout 结果不会影响 ViewGroup 的layout

2. view 的布局过程的作用为确定自己四个顶点在父 View 中的位置

2、View 的 getMeasuredWidth() 和 getWidth() 的区别

  1. 子元素的layout() 方法中会根据父容器中传递的顶点位置为 mLeft , mTop , mRight , mBottom 等属性赋值,View 的 getWidth() 方法得到的值为 mRight - mLeft

  2. View 的 getMeasuredWidth() 方法得到的值是 View 的 mMeasuredWidth 参数的值,该参数的赋值是在 onMeasure() 方法中

  3. 这两个方法得到的值不是同一个参数的值,两个参数的赋值时间是不同的,如果View 重写 layout 方法,修改四个顶点的位置,这样两个方法得到的值就是不同的

  4. 所以不能说这两值一定相等。getWidth() 方法得到的是 View 的最终宽高,getMeasuredWidth() 方法得到的是 View 的测量宽高

  • getHeight() 和 getMeasuredHeight() 方法同理。

二、View 的 draw() 绘制过程

测量和布局过程完成之后,ViewRootImpl 会接着调用 performDraw 方法,该方法最终会调用 DecorView 的 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);
    }
    
    // View
    protected void onDraw(Canvas canvas) {}  
    
    // ViewGroup
    protected void dispatchDraw(Canvas canvas) {
        
        for (int i = 0; i < childrenCount; i++) {
            ...
            drawChild(canvas, transientChild, drawingTime);
            ... 
        }
    }
    
    // ViewGroup
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

由 View 的 draw 方法可分析得出以下绘制流程

1. View 绘制的流程

  1. 绘制背景 background.draw(canvas)
  2. 保存 canvas 图层
  3. 绘制自己 调用 onDraw(canvas) 方法
  4. 绘制 children (dispatchDraw)
  5. 绘制渐变效果和恢复 canvas 图层
  6. 绘制装饰 (onDrawScrollBars)

onDraw() 为空实现,需要子类根据需要显示的内容重写此方法

dispatchDraw() 方法也是空实现,ViewGroup 中重写了此方法,dispatchDraw 方法中遍历所有子 View,并调用其 draw() 方法,将绘制过程一层层传递,完成了 View 树的绘制过程。

2. setWillNotDraw() 方法

/**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

setWillNotDraw() 方法的作用可以从其注释中看出,如果一个 View 不需要绘制任何内容,那么设置这个标记位 true 后,系统会进行相应的优化。

默认情况下 View 没有开启这个标记,而 ViewGroup 则开启了这个标记。

在开发过程中,如果我们的 View 继承自 ViewGroup 且没有进行绘制时就可以开启这个标记以便于系统对其进行优化,如果该 ViewGroup 需要通过 onDraw 来绘制内容,则需要通过调用 setWillNotDraw() 方法来关闭此标记

好啦,到这里 View 的工作过程中测量、布局、绘制三大过程的分析就结束啦,接下来将是 View 的事件分发机制和自定义 View 的文章,敬请期待

推荐阅读更多精彩内容