View繪製原理——畫什麼?

這是Android視圖繪製系列文章的第三篇,系列文章目錄以下:canvas

  1. View繪製原理——畫多大?
  2. View繪製原理——畫在哪?
  3. View繪製原理——畫什麼?
  4. 讀源碼,懂原理,有什麼用?寫業務代碼又用不到?—— 自定義換行容器控件

View繪製就比如畫畫,先拋開Android概念,若是要畫一張圖,首先會想到哪幾個基本問題:bash

  • 畫多大?
  • 畫在哪?
  • 怎麼畫?

Android繪製系統也是按照這個思路對View進行繪製,上面這些問題的答案分別藏在:app

  • 測量(measure)
  • 定位(layout)
  • 繪製(draw)

這一篇將從源碼的角度分析「繪製(draw)」。View繪製系統中的draw實際上是講的是繪製的順序,至於具體畫什麼東西是各個子View本身決定的。ide

View.draw()

在分析View測量定位時,發現它們都是自頂向下進行地,即老是由父控件來觸發子控件的測量或定位。不知道「繪製」是否是也是這樣?,以View.draw()爲切入點,一探究竟:佈局

public void draw(Canvas canvas) {
        /*
         * 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);

            drawAutofilledHighlight(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);

            // Step 7, draw the default focus highlight
            //第七步:繪製默認高亮
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            // we’re done...
            return;
        }
    }
複製代碼

這個方法實在太長了。。。還好有註釋幫咱們提煉了一條主線。註釋說繪製一共有6個步驟,他們分別是:post

  1. 繪製控件背景
  2. 保存畫布層
  3. 繪製控件自身內容
  4. 繪製子控件
  5. 繪製褪色效果並恢復畫布層(感受這一步和第二步是對稱的)
  6. 繪製裝飾物

爲啥提煉了主線後仍是以爲好複雜。。。還好註釋又幫咱們省去了一些步驟,註釋說「一般狀況下第二步和第五步會跳過。」在剩下的步驟中有三個步驟最最重要:ui

  1. 繪製控件背景
  2. 繪製控件自身內容
  3. 繪製子控件

讀到這裏能夠得出結論:View繪製順序是先畫背景(drawBackground()),再畫本身(onDraw()),接着畫孩子(dispatchDraw())。晚畫的東西會蓋在上面。this

先看下drawBackground()spa

/**
     * Draws the background onto the specified canvas.
     *
     * @param canvas Canvas on which to draw the background
     */
    private void drawBackground(Canvas canvas) {
        //Drawable類型的背景圖
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        setBackgroundBounds();
        ...
        //繪製Drawable
        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);
        }
    }
複製代碼

背景是一張Drawable類型的圖片,直接調用Drawable.draw()將其繪製在畫布上。接着看下onDraw()debug

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    /**
     * Implement this to do your drawing.
     *
     * @param canvas the canvas on which the background will be drawn
     */
    protected void onDraw(Canvas canvas) {
    }
}
複製代碼

View.onDraw()是一個空實現。想一想也對,View是一個基類,它只負責抽象出繪製的順序,具體繪製什麼由子類來決定,看一下ImageView.onDraw()

public class ImageView extends View {
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        ...
        //繪製drawable
        if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
            mDrawable.draw(canvas);
        } else {
            final int saveCount = canvas.getSaveCount();
            canvas.save();

            if (mCropToPadding) {
                final int scrollX = mScrollX;
                final int scrollY = mScrollY;
                canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
                        scrollX + mRight - mLeft - mPaddingRight,
                        scrollY + mBottom - mTop - mPaddingBottom);
            }

            canvas.translate(mPaddingLeft, mPaddingTop);

            if (mDrawMatrix != null) {
                canvas.concat(mDrawMatrix);
            }
            mDrawable.draw(canvas);
            canvas.restoreToCount(saveCount);
        }
    }
}
複製代碼

ImageView的繪製方法和View繪製背景同樣,都是直接繪製Drawable

ViewGroup.dispatchDraw()

View.dispatchDraw()也是一個空實現,想一想也對,View是葉子結點,它沒有孩子:

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    /**
     * 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).
     * @param canvas the canvas on which to draw the view
     */
    protected void dispatchDraw(Canvas canvas) {

    }
}
複製代碼

因此ViewGroup實現了dispatchDraw()

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    @Override
    protected void dispatchDraw(Canvas canvas) {
        ...
        // Only use the preordered list if not HW accelerated, since the HW pipeline will do the
        // draw reordering internally
        //當沒有硬件加速時,使用預約義的繪製列表(根據z-order值升序排列全部子控件)
        final ArrayList<View> preorderedList = usingRenderNodeProperties
                ? null : buildOrderedChildList();
        //自定義繪製順序
        final boolean customOrder = preorderedList == null
                && isChildrenDrawingOrderEnabled();
        //遍歷全部子控件
        for (int i = 0; i < childrenCount; i++) {
            ...
            //若是沒有自定義繪製順序和預約義繪製列表,則按照索引i遞增順序遍歷子控件
            final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
            final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                //觸發子控件本身繪製本身
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        ...
    }
    
    private int getAndVerifyPreorderedIndex(int childrenCount, int i, boolean customOrder) {
        final int childIndex;
        if (customOrder) {
            final int childIndex1 = getChildDrawingOrder(childrenCount, i);
            if (childIndex1 >= childrenCount) {
                throw new IndexOutOfBoundsException("getChildDrawingOrder() "
                        + "returned invalid index " + childIndex1
                        + " (child count is " + childrenCount + ")");
            }
            childIndex = childIndex1;
        } else {
            //1.若是沒有自定義繪製順序,遍歷順序和i遞增順序同樣
            childIndex = i;
        }
        return childIndex;
    }
    
    private static View getAndVerifyPreorderedView(ArrayList<View> preorderedList, View[] children,
            int childIndex) {
        final View child;
        if (preorderedList != null) {
            child = preorderedList.get(childIndex);
            if (child == null) {
                throw new RuntimeException("Invalid preorderedList contained null child at index "
                        + childIndex);
            }
        } else {
            //2.若是沒有預約義繪製列表,則按i遞增順序遍歷子控件
            child = children[childIndex];
        }
        return child;
    }
    
}
複製代碼

結合註釋相信你必定看懂了:父控件會在dispatchDraw()中遍歷全部子控件並觸發其繪製本身。 並且還能夠經過某種手段來自定義子控件的繪製順序(對於本篇主題來講,這不重要)。

沿着調用鏈繼續往下:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    /**
     * 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.
     * 繪製ViewGroup的一個孩子
     *
     * @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);
    }
}

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    /**
     * 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) {
        ...
        if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            dispatchDraw(canvas);
        } else {
            //繪製
            draw(canvas);
        }
        ...
    }
複製代碼

ViewGroup.drawChild()最終會調用View.draw()。因此,View的繪製是自頂向下遞歸的過程,「遞」表示父控件在ViewGroup.dispatchDraw()中遍歷子控件並調用View.draw()觸發其繪製本身,「歸」表示全部子控件完成繪製後父控件繼續後序繪製步驟`

總結

通過三篇文章的分析,對View繪製流程有了一個大概的瞭解:

  • View繪製流程就比如畫畫,它按前後順序解決了三個問題 :
    1. 畫多大?(測量measure)
    2. 畫在哪?(定位layout)
    3. 怎麼畫?(繪製draw)
  • 測量、定位、繪製都是從View樹的根結點開始自頂向下進行地,即都是由父控件驅動子控件進行地。父控件的測量在子控件件測量以後,但父控件的定位和繪製都在子控件以前。
  • 父控件測量過程當中ViewGroup.onMeasure(),會遍歷全部子控件並驅動它們測量本身View.measure()。父控件還會將父控件的佈局要求與子控件的佈局訴求相結合造成一個MeasureSpec對象傳遞給子控件以指導其測量本身。View.setMeasuredDimension()是測量過程的終點,它表示View大小有了肯定值。
  • 父控件在完成本身定位以後,會調用ViewGroup.onLayout()遍歷全部子控件並驅動它們定位本身View.layout()。子控件老是相對於父控件左上角定位。View.setFrame()是定位過程的終點,它表示視圖矩形區域以及相對於父控件的位置已經肯定。
  • 控件按照繪製背景,繪製自身,繪製孩子的順序進行。父控件在完成繪製自身以後,會調用ViewGroup.dispatchDraw()遍歷全部子控件並驅動他們繪製本身View.draw()
相關文章
相關標籤/搜索