View 的 layout 布局和 draw 繪制過程

上篇文章View 的測量分析了 View 的工作原理中最復雜的測量過程,接著測量過程的是布局和繪制的過程,這里兩個過程相對比較簡單,所以放到一篇文章中來寫

View 的測量過程中,確定了 View 的測量寬高的信息,布局過程則是為了確定 View 在其父 View 中的位置以及 ViewGroup 確定其所有子 View 元素的位置;布局結束后會執(zhí)行繪制過程,繪制過程將 View 需要顯示的內容繪制到屏幕上

一、layout (布局)過程

依舊從 ViewRootImpl 的 performLayout 方法開始,其中調用 DecorView 的 layout 方法,layout 方法是在 View 類中定義和實現(xiàn)的,其中 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 方法是個空實現(xiàn),說明 View 在布局過程中的任務就是確定自己在父 View 中的位置,確定了在父 View 中的位置后也就確定了子 View 的最終寬高,如果不重寫 View 的 layout 方法,其最終寬高與測量寬高相等,如果重寫了 layout 方法,并且子 View 的位置不以測量寬高來確定,此時 View 的最終寬高將不等于測量寬高。測量寬高賦值于測量過程,最終寬高賦值于布局過程,兩者賦值時機不同

ViewGroup 中 onLayout 是個抽象方法,其子類必須重寫該方法以確定其所有子 View 的位置,下面重點分析 ViewGroup 的 onLayout 方法

1、ViewGroup 的 onLayout 方法

由于 ViewGroup 的 onLayout 方法是個抽象方法,所以我們選一個特定的 ViewGroup 實現(xiàn)類來分析,這里分析 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 數(shù)量

    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 方法中根據(jù)設置的 View 線性排列的方向確定如果實現(xiàn)布局,以豎直排列的 情況為例,會調用 layoutVertical 方法

layoutVertical 中會遍歷所有的子 View,由子 View 的測量寬高和 ViewGroup 自身業(yè)務邏輯確定子 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() 的區(qū)別

  1. 子元素的layout() 方法中會根據(jù)父容器中傳遞的頂點位置為 mLeft , mTop , mRight , mBottom 等屬性賦值,View 的 getWidth() 方法得到的值為 mRight - mLeft

  2. View 的 getMeasuredWidth() 方法得到的值是 View 的 mMeasuredWidth 參數(shù)的值,該參數(shù)的賦值是在 onMeasure() 方法中

  3. 這兩個方法得到的值不是同一個參數(shù)的值,兩個參數(shù)的賦值時間是不同的,如果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() 為空實現(xiàn),需要子類根據(jù)需要顯示的內容重寫此方法

dispatchDraw() 方法也是空實現(xiàn),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 后,系統(tǒng)會進行相應的優(yōu)化。

默認情況下 View 沒有開啟這個標記,而 ViewGroup 則開啟了這個標記。

在開發(fā)過程中,如果我們的 View 繼承自 ViewGroup 且沒有進行繪制時就可以開啟這個標記以便于系統(tǒng)對其進行優(yōu)化,如果該 ViewGroup 需要通過 onDraw 來繪制內容,則需要通過調用 setWillNotDraw() 方法來關閉此標記

好啦,到這里 View 的工作過程中測量、布局、繪制三大過程的分析就結束啦,接下來將是 View 的事件分發(fā)機制和自定義 View 的文章,敬請期待

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容