Android View原理解析之繪製流程(draw)

提示:本文的源碼均取自Android 7.0(API 24)

前言

自定義View是Android進階路線上必須攻克的難題,而在這之前就應該先對View的工作原理有一個系統的理解。本系列將分爲4篇博客進行講解,本文主要對View的繪製流程進行講解。相關內容如下:

從View的角度看draw流程

在本系列的第一篇文章中講到整個視圖樹(ViewTree)的根容器是DecorView,ViewRootImpl通過調用DecorView的draw方法開啓佈局流程。draw是定義在View中的方法,我們先從View的角度來看看佈局過程中發生了什麼。

首先來看一下draw方法中的邏輯,關鍵代碼如下:

/**
 * Manually render this view (and all of its children) to the given Canvas.
 * The view must have already done a full layout before this function is
 * called.  
 * 
 * View的子類不應該重寫這個方法,而應該重寫onDraw方法繪製自己的內容
 *
 * @param canvas The Canvas to which the View is rendered.
 */
@CallSuper
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;

    /*
     * 完整地繪製流程將按順序執行以下6步
     * 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 繪製子View
     * 5. If necessary, draw the fading edges and restore layers 恢復保存的圖層
     * 6. Draw decorations (scrollbars for instance) 繪製裝飾(比如滑動條)
     */

    // ① Step 1, 繪製背景(如果有必要的話)
    int saveCount;

    if (!dirtyOpaque) {
        drawBackground(canvas);
    }

    // 通常情況下會跳過第2步和第5步
    // 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, 繪製內容
        if (!dirtyOpaque) onDraw(canvas);

        // ③ Step 4, 繪製子View
        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, 繪製View的裝飾 (foreground, scrollbars)
        onDrawForeground(canvas);

        // ⑤ Step 7, 繪製默認的焦點高亮
        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)
     */
    ........
}

這個方法的邏輯非常清晰,這裏咱們再來總結一下在draw中要執行的步驟:

  1. 繪製背景
  2. 保存Canvas圖層信息(如果有必要的話)
  3. 繪製View的內容
  4. 繪製子View
  5. 繪製保存的Canvas圖層信息(如果有必要的話)
  6. 繪製View的裝飾(比如滑動條)

其中第2步和第5步在通常情況下是不會執行的,所以我們也就不再深究它們了。首先在代碼①的位置,調用了drawBackground方法繪製View的背景,那讓我們首先來看看在這個方法中做了些什麼:

private void drawBackground(Canvas canvas) {
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }

    setBackgroundBounds();

    // Attempt to use a display list if requested.
    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;
        }
    }

    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    if ((scrollX | scrollY) == 0) {
    	// 繪製View背景
        background.draw(canvas);
    } else {
    	// 如果View發生了移動,先移動畫布,再繪製View背景
        canvas.translate(scrollX, scrollY);
        background.draw(canvas);
        canvas.translate(-scrollX, -scrollY);
    }
}

在這個方法中的background其實就是個Drawable對象,繪製背景的時候只要調用Drawable#draw方法就行了。當然,如果View的位置發生了移動(scrollX或scrollY不爲0),需要先平移畫布,再繪製background。

然後在View#draw代碼②的位置,調用了onDraw方法繪製自己的內容,這個方法的代碼如下:

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

這是個空方法,意味着View的子類需要自己負責繪製內容。如果通過繼承View實現自定義View,就應該重寫onDraw方法,並在這個方法中繪製自身的內容。View#draw方法的註釋也提到,View的子類不應該直接重寫draw方法,而應該重寫onDraw方法。

隨後在View#draw代碼③的位置,調用了dispatchDraw方法繪製自己的子View,這個方法同樣是空實現。因爲一個純粹的View是沒有子View的,自然也沒必要執行相應的繪製邏輯。代碼如下:

/**
 * Called by draw to draw the child views. This may be overridden
 * by derived classes to gain control just before its children are drawn
 * (but after its own view has been drawn).
 */
protected void dispatchDraw(Canvas canvas) {
}

緊接着在View#draw代碼④的位置,調用了onDrawForeground方法繪製View的裝飾,比如前景、滑動條等,代碼如下:

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

在這個方法中會依次調用onDrawScrollIndicatorsonDrawScrollbars繪製滑動條的指示器和滑動條,最後繪製View的前景色。代碼中的foreground是一個Drawable對象,因此只需要調用Drawable#draw方法就完成了對View前景色的繪製。

最後在View#draw代碼⑤的位置,調用drawDefaultFocusHighlight方法繪製View的默認焦點高亮狀態,代碼如下:


private void drawDefaultFocusHighlight(Canvas canvas) {
    if (mDefaultFocusHighlight != null) {
        if (mDefaultFocusHighlightSizeChanged) {
            mDefaultFocusHighlightSizeChanged = false;
            final int l = mScrollX;
            final int r = l + mRight - mLeft;
            final int t = mScrollY;
            final int b = t + mBottom - mTop;
            mDefaultFocusHighlight.setBounds(l, t, r, b);
        }
        mDefaultFocusHighlight.draw(canvas);
    }
}

mDefaultFocusHighlight同樣是一個Drawable對象,這裏調用Drawable#draw方法完成了對默認焦點高亮狀態的繪製。

從ViewGroup的角度看draw流程

說完了View的繪製流程,接下來再從ViewGroup角度看看繪製過程中發生了什麼。

ViewGroup並沒有重寫draw方法,說明ViewGroup也是遵循上文提到的繪製步驟的。此外,ViewGroup也沒有重寫onDraw方法,說明ViewGroup默認也不會繪製自身的內容。如果我們通過繼承ViewGroup實現自定義View,且有繪製自身的需求,就應該重寫onDraw方法。

ViewGroup重寫了dispatchDraw方法,這個方法將負責繪製子View,關鍵代碼如下:

@Override
protected void dispatchDraw(Canvas canvas) {
    boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    int flags = mGroupFlags;

    .......

    boolean more = false;
    final long drawingTime = getDrawingTime();

    if (usingRenderNodeProperties) canvas.insertReorderBarrier();
    final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
    int transientIndex = transientCount != 0 ? 0 : -1;
    // Only use the preordered list if not HW accelerated, since the HW pipeline will do the
    // draw reordering internally
    final ArrayList<View> preorderedList = usingRenderNodeProperties
            ? null : buildOrderedChildList();
    final boolean customOrder = preorderedList == null
            && isChildrenDrawingOrderEnabled();
    
    // 循環處理子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);
            }
            transientIndex++;
            if (transientIndex >= transientCount) {
                transientIndex = -1;
            }
        }

        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            // 繪製子View
        	more |= drawChild(canvas, child, drawingTime);
        }
    }
    .......
}

在這個方法中會循環處理子View,並調用drawChild方法完成對子View的繪製。一般而言,如果通過繼承ViewGroup實現自定義View,是不用重寫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方法,這裏是View#draw的重載版本,關鍵代碼如下:

/**
 * This method is called by ViewGroup.drawChild() to have each child view draw itself.
 *
 * This is where the View specializes rendering behavior based on layer type,
 * and hardware acceleration.
 */
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
    /* If an attached view draws to a HW canvas, it may use its RenderNode + DisplayList.
     *
     * If a view is dettached, its DisplayList shouldn't exist. If the canvas isn't
     * HW accelerated, it can't handle drawing RenderNodes.
     */
    boolean drawingWithRenderNode = mAttachInfo != null
            && mAttachInfo.mHardwareAccelerated
            && hardwareAcceleratedCanvas;

    boolean more = false;
    
    ........

    final boolean drawingWithDrawingCache = cache != null && !drawingWithRenderNode;
    
    ........

    if (!drawingWithDrawingCache) {
        if (drawingWithRenderNode) {
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
        } else {
            // ① 判斷是否需要跳過對自身的繪製流程
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                // 跳過View的繪製流程,直接調用dispatchView繪製子View
                dispatchDraw(canvas);
            } else {
            	// 繪製View
                draw(canvas);
            }
        }
    } else if (cache != null) {
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) {
            // 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();
            if (alpha < 1) {
                mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
            }
            canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
            if (alpha < 1) {
                mLayerPaint.setAlpha(layerPaintAlpha);
            }
        }
    }

    ........

    return more;
}

這個方法中的代碼很多,這裏就只保留了關鍵部分的代碼。在代碼①的位置會先判斷是否需要跳過對自己的繪製流程:如果當前的View是ViewGroup,並且不需要繪製背景時,就會直接調用dispatchDraw方法繪製子View;否則會調用自身的draw方法,後續步驟就和View中的繪製流程一致了。

到了這裏,ViewGroup的繪製流程也分析完畢了,整體看來還是比較簡單的。如果要繼承ViewGroup實現自定義View,繪製流程其實完全可以保持默認實現。除非我們有一些特殊的繪製邏輯,比如像LinearLayout一樣在子View之間繪製分割線,那就可以通過重寫onDraw方法實現。

整體的流程圖

上面分別從View和ViewGroup的角度講解了繪製流程,這裏再以流程圖的形式歸納一下整個draw過程,便於加深記憶:

繪製流程

小結

繪製流程中的許多工作已經被系統完成了,相比前兩個步驟還是比較容易的。但是如果想要獲得更好的學習效果,最好還是打開AndroidStudio,循着本文的脈絡試着一步步探索源碼中的邏輯。

參考資料

https://blog.csdn.net/lfdfhl/article/details/51435968
https://blog.csdn.net/a553181867/article/details/51570854

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章