重溫View繪製原理(二)

緊接着上一篇文章重溫View繪製原理(一),繼續看view繪製原理。android

1. View繪製流程

view的繪製是從根視圖 ViewRoot 的 performTraversals() 方法開始,從上到下遍歷整個視圖樹,每一個 View 控制負責繪製本身,而 ViewGroup 還須要負責通知本身的子 View 進行繪製操做。視圖操做的過程能夠分爲三個步驟,分別是測量(Measure)、佈局(Layout)和繪製(Draw)。performTraversals 方法在viewRoot的實現類 ViewRootImpl 裏面:canvas

  • measure方法用於測量View的寬高
  • layout方法用於肯定View在父容器中的位置
  • draw方法負責將View繪製在屏幕上

view繪製流程圖:bash

流程圖1.png

流程圖2.png

看看performTraversals方法源碼:佈局

private void performTraversals() {
      ...
      int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
      int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
      ...
      // 測量
      performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
      ...
      // 佈局
      performLayout(lp, mWidth, mHeight);
      ...
      // 繪製
      performDraw();
      ...
  }
複製代碼

依次調用performMeasure、performLayout、performDraw方法,分別完成頂級View的measure、layout、draw流程。performMeasure會調用measure方法,而measure又會調用onMeasure方法,在onMeasure方法中又會對子元素進行measure,這樣重複下去就完成了整個View樹的遍歷。優化

performLayout、performDraw傳遞過程也很是相似,不過performDraw是在draw方法中經過dispatchDraw方法實現的。ui

measure過程決定了View的寬高,而Layout方法則肯定了四個頂點的座標和實際的寬高(每每等於measure中計算的寬高),draw方法則決定了View的顯示。只有完成了draw方法才能正確顯示在屏幕上。this

2. MeasureSpec

MeasureSpec是measure的重要參數。 MeasureSpec 表示的是一個 32 位的整數值,它的高 2 位表示測量模式 SpecMode,低 30 位表示某種測量模式下的規格大小 SpecSize(PS:這裏用到了位運算進行狀態壓縮來節省內存)。MeasureSpec 是 View 類的一個靜態內部類,用來講明應該如何測量這個View,有三種模式:lua

  • UNSPECIFIED:不指定測量模式,父視圖沒有限制子視圖的大小,子視圖能夠是想要的任何尺寸,一般用於系統內部,應用開發中不多使用到。spa

  • EXACTLY:精確測量模式,當該視圖的 layout_width 或者 layout_height 指定爲具體數值或者 match_parent 時生效,表示父視圖已經決定了子視圖的精確大小,這種模式下 View 的測量值就是 SpecSize 的值。.net

  • AT_MOST:最大值模式,當前視圖的 layout_width 或者 layout_height 指定爲 wrap_content 時生效,此時子視圖的尺寸能夠是不超過父視圖運行的最大尺寸的任何尺寸。

下表是普通View的MeasureSpec的建立規則對應表:

childLayoutParams/parentSpecParams EXACTLY AT_MOST UNSPECIFIED
dp/px EXACTLY childSIze EXACTLY childSIze EXACTLY childSIze
match_parent EXACTLY parentSize AT_MOST parentSize UNSPECIFIED 0
wrap_content AT_MOST parentSize AT_MOST parentSize UNSPECIFIED 0

3. Measure

View的繪製從測量開始,看看performMeasure()方法:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }
複製代碼

具體操做是分發給 ViewGroup 的,由 ViewGroup 在它的 measureChild 方法中傳遞給子 View。ViewGroup 經過遍歷自身全部的子 View,並逐個調用子 View 的 measure 方法實現測量操做。

3.1 View的measure過程

View的measure過程由measure方法來完成,measure方法是一個final方法,不能重寫,它會調用VIew的onMeasure方法。onMeasure方法中會調用getDefaultSize方法,而getDefault方法中又會調用getSuggestedWidth和getSuggestedHeight方法。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
複製代碼
/**
     * Utility to return a default size. Uses the supplied size if the
     * MeasureSpec imposed no constraints. Will get larger if allowed
     * by the MeasureSpec.
     *
     * @param size Default size for this view
     * @param measureSpec Constraints imposed by the parent
     * @return The size this view should be.
     */
    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
複製代碼

getDefaultSize方法所返回的就是測量後的View的大小。

接着看getSuggestedWidth和getSuggestedHeight方法:

/**
     * Returns the suggested minimum width that the view should use. This
     * returns the maximum of the view's minimum width * and the background's minimum width
     *  ({@link android.graphics.drawable.Drawable#getMinimumWidth()}).
     * <p>
     * When being used in {@link #onMeasure(int, int)}, the caller should still
     * ensure the returned width is within the requirements of the parent.
     *
     * @return The suggested minimum width of the view.
     */
    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

    protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

    }
複製代碼

它在沒有指定background的狀況下,返回的是minSize這一屬性對應的值,而在指定了背景的狀況下,返回的是背景drawable的getMinimumWidth / getMinimumHeight方法對應的值

這兩個方法在Drawable有原始寬度的狀況下返回原始寬度,不然返回0

從getDefaultSize方法能夠看出,View的寬高由specSize決定。

3.2 ViewGroup的measure過程

ViewGroup除了完成本身的measure過程,還會遍歷調用子元素的measure方法,而後子元素再次遞歸執行,ViewGroup是一個抽象類,所以沒有重寫View的onMeasure方法。但它提供了一個measureChildren的方法,以下:

/**
     * Ask all of the children of this view to measure themselves, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * We skip children that are in the GONE state The heavy lifting is done in
     * getChildMeasureSpec.
     *
     * @param widthMeasureSpec The width requirements for this view
     * @param heightMeasureSpec The height requirements for this view
     */
    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

    /**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * The heavy lifting is done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param parentHeightMeasureSpec The height requirements for this view
     */
    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
複製代碼

能夠看到,ViewGroup執行measure時,會遍歷子元素,調用measureChild方法對子元素進行measure。

在measureChild方法中,取出子元素的LayoutParams,經過getChildMeasureSpec方法建立子元素MeasureSpec,而後傳遞給View的measure方法進行測量。

ViewGroup沒有定義測量具體過程,由於它是個抽象類。具體的測量過程的onMeasure方法須要子類來實現,因爲它的子類的特性可能會很大不一樣,因此無法作統一處理(如LinearLayout和RelativeLayout)。

4. Layout

Layout流程的做用是ViewGroup肯定子元素的位置。當ViewGroup被肯定後,在onLayout中會遍歷全部子元素並調用layout方法,在layout方法中會調用onLayout方法。layout方法肯定View的位置,而onLayout方法則肯定全部子元素的位置。

ViewRootImpl 的 performLayout 以下:

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        mLayoutRequested = false;
        mScrollMayChange = true;
        mInLayout = true;

        final View host = mView;
        if (host == null) {
            return;
        }
        if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
            Log.v(mTag, "Laying out " + host + " to (" +
                    host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
        }

        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
        try {
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
            ...
            }
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        mInLayout = false;
    }
複製代碼

先看View的layout方法:

public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }

            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        final boolean wasLayoutValid = isLayoutValid();

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

        if (!wasLayoutValid && isFocused()) {
            mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
            if (canTakeFocus()) {
                // We have a robust focus, so parents should no longer be wanting focus.
                clearParentsWantFocus();
            } else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
                // This is a weird case. Most-likely the user, rather than ViewRootImpl, called
                // layout. In this case, there's no guarantee that parent layouts will be evaluated // and thus the safest action is to clear focus here. clearFocusInternal(null, /* propagate */ true, /* refocus */ false); clearParentsWantFocus(); } else if (!hasParentWantsFocus()) { // original requestFocus was likely on this view directly, so just clear focus clearFocusInternal(null, /* propagate */ true, /* refocus */ false); } // otherwise, we let parents handle re-assigning focus during their layout passes. } else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) { mPrivateFlags &= ~PFLAG_WANTS_FOCUS; View focused = findFocus(); if (focused != null) { // Try to restore focus as close as possible to our starting focus. if (!restoreDefaultFocus() && !hasParentWantsFocus()) { // Give up and clear focus once we've reached the top-most parent which wants
                    // focus.
                    focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                }
            }
        }

        if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
            mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
            notifyEnterOrExitForAutoFillIfNeeded(true);
        }
    }
複製代碼

首先,經過setFrame方法設定View四個頂點的位置(初始化mLeft,mRight,mTop,mBottom)。四個頂點一旦肯定,則在父容器中的位置也肯定了,接着便會調用onLayout方法,來讓父容器肯定子容器的位置。onLayout一樣和具體佈局有關,所以View和ViewGroup均沒有實現onLayout方法。

5. Draw

draw流程是將View繪製到屏幕上。先看看performDraw 方法:

private void performDraw() {
        ....
        try {
            boolean canUseAsync = draw(fullRedrawNeeded);
            ....
        } finally {
            mIsDrawing = false;
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

        ...
    }
複製代碼
private boolean draw(boolean fullRedrawNeeded) {
    ...
    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                        scalingRequired, dirty, surfaceInsets)) {
      return false;
    }
  }

  private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
     ...
     mView.draw(canvas);
     ...
  }
複製代碼

最終調用到每一個 View 的 draw 方法繪製每一個具體的 View,繪製基本上能夠分爲六個步驟:

public void draw(Canvas canvas) {
    ...
    // Step 1, draw the background, if needed
    if (!dirtyOpaque) {
      drawBackground(canvas);
    }
    ...
    // Step 2, save the canvas' layers saveCount = canvas.getSaveCount(); ... // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); // Step 5, draw the fade effect and restore layers canvas.drawRect(left, top, right, top + length, p); ... canvas.restoreToCount(saveCount); ... // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); } 複製代碼

View繪製過程的傳遞是經過dispatchDraw實現的。dispatchDraw會遍歷調用全部子元素的draw方法。這樣draw事件就一層層傳遞了下來。

它有個比較特殊的setWillNotDraw方法。若是一個View不須要繪製任何內容,在咱們設定這個標記爲true後,系統就會對其進行相應優化。通常View沒有啓用這個標記位。但ViewGroup是默認啓用的。

它對實際開發的意義在於:咱們的自定義控件繼承於ViewGroup而且不具有繪製功能時,能夠開啓這個標記位方便系統進行後續優化。

6. 結束語

關於view繪製的原理在網上也特別多,時間久了也容易忘記,看一遍別人的,還不如順便把他們的記錄下來,方便本身之後溫習。

重要參考:

相關文章
相關標籤/搜索