你需要知道的 Android View 的繪製

guojun_fire發表於2017-02-27

經過上一篇AndroidView的佈局分析之後,我們繼續View的繪製分析講解。我們依舊從ViewRootImpl#performTraversals說起。

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

                performDraw();
            }
        } 
        ...
}複製程式碼

我們對performDraw()執行繪製方法進行分析:

private void performDraw() {
    ···
    final boolean fullRedrawNeeded = mFullRedrawNeeded;
    try {
        draw(fullRedrawNeeded);
    } finally {
        mIsDrawing = false;
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    ···
}複製程式碼

在ViewRootImpl#performDraw裡呼叫了ViewRootImpl#draw方法,並將fullRedrawNeeded形參傳進方法體中,我們通過變數的命名方式大概推斷出該成員變數的作用是判斷是否需要重新繪製全部檢視,因為我們從DecorView根佈局分析至今,我們顯然是繪製所有檢視的。那麼我們繼續分析ViewRootImpl#draw

private void draw(boolean fullRedrawNeeded) {
    ...
    //獲取mDirty,該值表示需要重繪的區域
    final Rect dirty = mDirty;
    if (mSurfaceHolder != null) {
        // The app owns the surface, we won't draw.
        dirty.setEmpty();
        if (animating) {
            if (mScroller != null) {
                mScroller.abortAnimation();
            }
            disposeResizeBuffer();
        }
        return;
    }

    //如果fullRedrawNeeded為真,則把dirty區域置為整個螢幕,表示整個檢視都需要繪製
    //第一次繪製流程,需要繪製所有檢視
    if (fullRedrawNeeded) {
        mAttachInfo.mIgnoreDirtyState = true;
        dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
    }

    ···

    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                return;
        }
}複製程式碼

由於這部分不算是重點,我們直接看關鍵程式碼部分,首先是獲取mDirty,該值指向的是需要重繪的區域的資訊,至於重繪部分的知識,我們會另起文章來分析,這裡只是順帶一下。然後是用我們傳遞進來的fullRedrawNeeded引數進行判斷是否需要重置dirty區域,最後呼叫了ViewRootImpl#drawSoftware方法,並把相關引數傳遞進去。

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {

    // Draw with software renderer.
    final Canvas canvas;
    try {
        final int left = dirty.left;
        final int top = dirty.top;
        final int right = dirty.right;
        final int bottom = dirty.bottom;

        //鎖定canvas區域,由dirty區域決定
        canvas = mSurface.lockCanvas(dirty);

        // The dirty rectangle can be modified by Surface.lockCanvas()
        //noinspection ConstantConditions
        if (left != dirty.left || top != dirty.top || right != dirty.right
                || bottom != dirty.bottom) {
            attachInfo.mIgnoreDirtyState = true;
        }

        canvas.setDensity(mDensity);
    } 

    try {

        if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
            canvas.drawColor(0, PorterDuff.Mode.CLEAR);
        }

        dirty.setEmpty();
        mIsAnimating = false;
        attachInfo.mDrawingTime = SystemClock.uptimeMillis();
        mView.mPrivateFlags |= View.PFLAG_DRAWN;

        try {
            canvas.translate(-xoff, -yoff);
            if (mTranslator != null) {
                mTranslator.translateCanvas(canvas);
            }
            canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
            attachInfo.mSetIgnoreDirtyState = false;

            //正式開始繪製
            mView.draw(canvas);

        }
    } 
    return true;
}複製程式碼

我們可以看到首先是例項化一個Canvas物件,然後對canvas進行一系列的賦值,最後呼叫mView.draw(canvas)方法。從之前的分析可以知道mView指向的就是DecorView。

View#draw。我們清楚知道DecorView、FrameLayout、ViewGroup、View之間的繼承關係。我們在FrameLayout裡面用方法搜尋的時候,搜到的draw(Canvas canvas)是View裡面的方法。那就是說,View的子類都是是呼叫View#draw()方法的。(原始碼註釋中不建議重寫draw方法)

public class View implements···{
    ···
    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;
        // 繪製背景,只有dirtyOpaque為false時才進行繪製
        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
            // 繪製子View
            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;
            ···
        }
}複製程式碼

我們可以看到官方給出的註釋是非常清晰地說明每一步的做法。我們首先來看一開始的標記位dirtyOpaque,這是標記的作用是判斷當前View是否是透明的,如果View是透明的,那麼根據下面的邏輯可以看出,有一些步驟就不進行。我們還是跟著註釋來分析一下有哪六個步驟:

  1. 對View背景的繪製
  2. 儲存當前的圖層資訊(可跳過)
  3. 繪製View的內容資訊
  4. 繪製子View
  5. 繪製陰影的邊緣和恢復層(有必要的話)
  6. 繪製裝飾(前景、滾動條)
    第二和第五步可以跳過,我這裡不作分析。

繪製背景

呼叫了View#drawBackground方法,我們看一下原始碼:

    private void drawBackground(Canvas canvas) {
        //mBackground是該View的背景引數,比如背景顏色
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        //根據View四個佈局引數來確定背景的邊界
        setBackgroundBounds();

        // Attempt to use a display list if requested.
        if (canvas.isHardwareAccelerated() && mAttachInfo != null
                && mAttachInfo.mHardwareRenderer != null) {
            mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);

            final RenderNode renderNode = mBackgroundRenderNode;
            if (renderNode != null && renderNode.isValid()) {
                setBackgroundRenderNodeProperties(renderNode);
                ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
                return;
            }
        }
        //獲取當前View的mScrollX和mScrollY值
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }複製程式碼

不是重點,我們記住這個步驟,往下看。

繪製View的內容資訊

    /**
     * Implement this to do your drawing.
     *
     * @param canvas the canvas on which the background will be drawn
     */
    protected void onDraw(Canvas canvas) {
    }複製程式碼

View#onDraw是一個空方法,就如我們上一個篇分析onLayout()一樣,由於不同的View有著不同的佈局,所以在檢視的繪製過程也同樣有所不同,這需要我們View的子類按照自己的需求去重寫onDraw()方法來實現。

繪製子View

因為是繪製子View,那麼我們可以在View找不到這個方法,在FrameLayout中方法查詢是指向ViewGroup#dispatchDraw的方法,由於這個方法實現主要是遍歷所以子View,每個子View呼叫drawChild。其實ViewGroup#dispatchDraw方法實現滿足了我們很多ViewGroup的子類,如LinearLayout、FrameLayout都是沒有重寫dispatchDraw方法的,如果我們自定義View需求比較特殊,可以重寫該方法。 另外ViewGroup#dispatchDraw方法程式碼過多,我們直接圈出重點ViewGroup#drawChild:

    /**
     * Draw one child of this View Group. This method is responsible for getting
     * the canvas in the right state. This includes clipping, translating so
     * that the child's scrolled origin is at 0, 0, and applying any animation
     * transformations.
     *
     * @param canvas The canvas on which to draw the child
     * @param child Who to draw
     * @param drawingTime The time at which draw is occurring
     * @return True if an invalidate() was issued
     */
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }複製程式碼

子類View的draw()方法,主要方法的過載,跟我們上面分析的draw(Canvas canvas)是不一樣的。

    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        ···
        if (!drawingWithDrawingCache) {
            if (drawingWithRenderNode) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
            } else {
                // Fast path for layouts with no backgrounds
                if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                    mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                    dispatchDraw(canvas);
                } else {
                    draw(canvas);
                }
            }
        } else if (cache != null) {
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            if (layerType == LAYER_TYPE_NONE) {
                // no layer paint, use temporary paint to draw bitmap
                Paint cachePaint = parent.mCachePaint;
                if (cachePaint == null) {
                    cachePaint = new Paint();
                    cachePaint.setDither(false);
                    parent.mCachePaint = cachePaint;
                }
                cachePaint.setAlpha((int) (alpha * 255));
                canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
            } else {
                // use layer paint to draw the bitmap, merging the two alphas, but also restore
                int layerPaintAlpha = mLayerPaint.getAlpha();
                mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
                canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
                mLayerPaint.setAlpha(layerPaintAlpha);
            }
        }
        ···
    }複製程式碼

從一開始的判斷的物件命名來理解,判斷時候有繪製快取,應該就是是否繪製過了,否的話將會呼叫draw(canvas)方法。而我們上面分析過,drawChild()的核心過程就是為子檢視分配的cavas畫布繪製區,在設定了一些位置、動畫等引數和一個Flag標記後就會呼叫子檢視的draw()函式進行具體的繪製了。

我們總體可以這樣理解ViewGroup的繪製過程,遍歷子View,過載draw方法對子View進行繪製,而子View又會呼叫自身的draw方法繪製自己,通過不斷遍歷子View及子View的不斷對自身的繪製,從而使得View樹完成繪製。(該方法實現過程較為複雜抽象,能力有限在參考下擷取這部分重點來說說,有需要大家可以結合其他分析進行自我總結歸納。)

繪製裝飾

繪製裝飾,其實對View的非背景、View內容的部分,如滾動條等等,我們看看View#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);
        }
    }複製程式碼

跟普通的繪製過程較為相似,先設定可繪製的區域,然後使用Canvas進行繪製,有興趣的同學可細分下去了解。

大家可以配合下面這圖進行理解和總結。

你需要知道的 Android View 的繪製

整體來說,View的繪製流程我們全部說完了,從一開始的View的建立、View的測量、View的佈局到這篇View的繪製,裡面包含著很多有價值的知識,由於自身水平有限不能面面俱到,主要配合自己對View的繪製流程梳理一遍,希望對大家也有幫助。謝謝你們的閱讀!

相關文章