View繪製——怎麼畫?

Taylor發表於2019-02-27

這是Android檢視繪製系列文章的第三篇,系列文章目錄如下:

  1. View繪製——畫多大?
  2. View繪製——畫在哪?
  3. View繪製——怎麼畫?

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

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

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

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

這一篇將從原始碼的角度分析“繪製(draw)”。View繪製系統中的draw其實是講的是繪製的順序,至於具體畫什麼東西是各個子View自己決定的。

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個步驟,他們分別是:

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

為啥提煉了主線後還是覺得好複雜。。。還好註釋又幫我們省去了一些步驟,註釋說“通常情況下第二步和第五步會跳過。”在剩下的步驟中有三個步驟最最重要:

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

讀到這裡可以得出結論:View繪製順序是先畫背景(drawBackground()),再畫自己(onDraw()),接著畫孩子(dispatchDraw())。晚畫的東西會蓋在上面。

先看下drawBackground()

    /**
     * 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()

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

相關文章