自定義View原理篇(3)- draw過程

1. 簡介

  • View的繪制過程分為三部分:measure、layout、draw

measure用來測量View的寬和高。
layout用來計算View的位置。
draw用來繪制View。

2. draw的始點

measure、layout一樣,draw也是始于ViewRootImplperformTraversals():

2.1 ViewRootImpl的performTraversals

    private void performTraversals() {

        //...

        //獲得view寬高的測量規(guī)格,mWidth和mHeight表示窗口的寬高,lp.widthhe和lp.height表示DecorView根布局寬和高
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//執(zhí)行測量

        //...

        performLayout(lp, mWidth, mHeight);//執(zhí)行布局

        //...

        if (!cancelDraw && !newSurface) {
            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).startChangingAnimations();
                }
                mPendingTransitions.clear();
            }

            performDraw();//執(zhí)行繪制
        }

        //...
    }

再來看看performDraw():

2.2 ViewRootImpl的performDraw

    private void performDraw() {

        //...

        final boolean fullRedrawNeeded = mFullRedrawNeeded;

        draw(fullRedrawNeeded);

        //...
    }

下面重點來分析draw過程。

3.draw過程分析

draw,顧名思義,就是來繪制View。
同樣,draw過程根據(jù)View的類型也可以分為兩種情況:

  1. 繪制單一View時,只需View本身即可;
  2. 繪制ViewGroup時,不僅需要繪制ViewGroup本身,還需繪制其所有的子View。

我們對這兩種情況分別進行分析。

3.1 單一View的draw過程

3.1.1 View的draw

單一Viewdraw過程是從Viewdraw()方法開始:

    public void draw(Canvas canvas) {

        //...

        /*
         *  繪制流程如下:
         *
         *      1\. 繪制view背景
         *      2\. 如果有需要,就保存圖層
         *      3\. 繪制view內(nèi)容
         *      4\. 繪制子View
         *      5\. 如果有必要,繪制漸變框和恢復(fù)圖層
         *      6\. 繪制裝飾(滑動條等)
         */

        if (!dirtyOpaque) {
            drawBackground(canvas);//步驟1\. 繪制view背景
        }

        // 如果可能的話,跳過第2步和第5步(常見情況)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {

            if (!dirtyOpaque) onDraw(canvas);//步驟3\. 繪制view內(nèi)容

            dispatchDraw(canvas);//步驟4\. 繪制子View

            //...

            onDrawForeground(canvas);//步驟6\. 繪制裝飾(滑動條等)

            //...

            // 繪制完成,返回

            return;
        }

        //如果有需要,會執(zhí)行第2步和第5步

        //...

        //步驟2\. 保存圖層
        if (solidColor == 0) {

            if (drawTop) {
                //保存圖層
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }

            //...
        }

        if (!dirtyOpaque) onDraw(canvas);//步驟3\. 繪制view內(nèi)容

        dispatchDraw(canvas);//步驟4\. 繪制子View

        //步驟5\. 繪制漸變框和恢復(fù)圖層
        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);
        }

        //...

        //恢復(fù)圖層
        canvas.restoreToCount(saveCount);

        //...

        onDrawForeground(canvas);//步驟6\. 繪制裝飾(滑動條等)

        //...
    }

可以看到,中間繪制時可能會跳過第2步和第5步,這樣可以提高繪制的效率。
下面我們來分析一下drawBackground()onDraw()、dispatchDraw()onDrawForeground()等方法,即步驟1、3、4、6。

3.1.2 View的drawBackground

首先來看看drawBackground(),這個方法是用來繪制背景:

   private void drawBackground(Canvas canvas) {
        // 獲取背景 drawable
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }

        // 根據(jù)在 layout 過程中獲取的 View 的位置參數(shù),來設(shè)置背景的邊界
        setBackgroundBounds();

        //硬件加速渲染
        if (canvas.isHardwareAccelerated() && mAttachInfo != null
                && mAttachInfo.mThreadedRenderer != null) {
            mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);

            final RenderNode renderNode = mBackgroundRenderNode;
            if (renderNode != null && renderNode.isValid()) {
                setBackgroundRenderNodeProperties(renderNode);
                ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
                return;
            }
        }

        // 獲取 mScrollX 和 mScrollY值
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            // 調(diào)用 Drawable 的 draw 方法繪制背景
            background.draw(canvas);
        } else {
            // 若 mScrollX 和 mScrollY 有值,則對 canvas 的坐標進行偏移
            canvas.translate(scrollX, scrollY);
            // 調(diào)用 Drawable 的 draw 方法繪制背景
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }

3.1.3 View的onDraw

由于 View 的內(nèi)容各不相同,因此onDraw()方法在View類中是個空實現(xiàn),具體的View(如TextView等)需對其進行重寫,有興趣的可以去看看TextView等的onDraw()實現(xiàn)。

    protected void onDraw(Canvas canvas) {
        //具體內(nèi)容的繪制邏輯
    }

3.1.4 View的dispatchDraw

由于單一View沒有子View,所以其dispatchDraw()方法是個空實現(xiàn):

    protected void dispatchDraw(Canvas canvas) {

    }

3.1.5 View的onDrawForeground

onDrawForeground()方法就是用來繪制一些裝飾,比如滑動指示器、滑動條、前景等:

    public void onDrawForeground(Canvas canvas) {
        //繪制滑動指示器
        onDrawScrollIndicators(canvas);
        //繪制滑動條
        onDrawScrollBars(canvas);

        //繪制前景
        final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
        if (foreground != null) {
            if (mForegroundInfo.mBoundsChanged) {
                mForegroundInfo.mBoundsChanged = false;
                final Rect selfBounds = mForegroundInfo.mSelfBounds;
                final Rect overlayBounds = mForegroundInfo.mOverlayBounds;

                if (mForegroundInfo.mInsidePadding) {
                    selfBounds.set(0, 0, getWidth(), getHeight());
                } else {
                    selfBounds.set(getPaddingLeft(), getPaddingTop(),
                            getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
                }

                final int ld = getLayoutDirection();
                Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
                        foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
                foreground.setBounds(overlayBounds);
            }

            foreground.draw(canvas);
        }
    }

3.1.6 單一View的draw過程流程圖

來張流程圖簡單總結(jié)一下:

image

3.2 ViewGroup的draw過程

ViewGroupdraw過程同樣是從Viewdraw()方法開始,ViewGroup沒有重寫draw()方法,所以跟Viewdraw()代碼是一樣,其drawBackground()、onDraw()、 onDrawForeground()等方法實現(xiàn)也一樣,這里就不重述了,我們來看下唯一不同的地方:dispatchDraw()

3.2.1 ViewGroup的dispatchDraw

    @Override
    protected void dispatchDraw(Canvas canvas) {

        //子View數(shù)量
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;

        //...

        //遍歷子View
        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);
                }

               //...
        }
       //...
    }

我們再來看看drawChild()方法:

3.2.2 ViewGroup的drawChild

    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

drawChild()方法中就是調(diào)用了子Viewdraw()去繪制子View.

3.2.3 ViewGroup的draw過程流程圖

來張流程圖簡單總結(jié)一下:

image

4. 自定義View

4.1 自定義單一view

自定義單一View需要重寫onDraw()

    @Override
    protected void onDraw(Canvas canvas) {
        //具體內(nèi)容的繪制邏輯
    }

4.2 自定義ViewGroup

自定義的ViewGroup一般是作為容器來放置各種子View的,所以一般無需重寫onDraw()。
因此ViewGroup默認啟用了WILL_NOT_DRAW這個標志位,啟用這個標記位后系統(tǒng)會進行相應(yīng)的優(yōu)化。
所以,當(dāng)我們有特殊需求需要重寫ViewGrouponDraw()時,應(yīng)當(dāng)關(guān)閉這個標記位??梢酝ㄟ^調(diào)用setWillNotDraw(false)來關(guān)閉它。

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

友情鏈接更多精彩內(nèi)容