重溫View繪制原理(二)

好記性不如爛筆頭。生活中多做筆記,不僅可以方便自己,還可以方便他人。

(下面的源碼大部分是來(lái)自API 28)

緊接著上一篇文章重溫View繪制原理(一),繼續(xù)看view繪制原理。

1. View繪制流程

view的繪制是從根視圖 ViewRoot 的 performTraversals() 方法開(kāi)始,從上到下遍歷整個(gè)視圖樹(shù),每個(gè) View 控制負(fù)責(zé)繪制自己,而 ViewGroup 還需要負(fù)責(zé)通知自己的子 View 進(jìn)行繪制操作。視圖操作的過(guò)程可以分為三個(gè)步驟,分別是測(cè)量(Measure)、布局(Layout)和繪制(Draw)。performTraversals 方法在viewRoot的實(shí)現(xiàn)類 ViewRootImpl 里面:

  • measure方法用于測(cè)量View的寬高
  • layout方法用于確定View在父容器中的位置
  • draw方法負(fù)責(zé)將View繪制在屏幕上

view繪制流程圖:

流程圖1.png
流程圖2.png

看看performTraversals方法源碼:

  private void performTraversals() {
      ...
      int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
      int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
      ...
      // 測(cè)量
      performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
      ...
      // 布局
      performLayout(lp, mWidth, mHeight);
      ...
      // 繪制
      performDraw();
      ...
  }

依次調(diào)用performMeasure、performLayout、performDraw方法,分別完成頂級(jí)View的measure、layout、draw流程。performMeasure會(huì)調(diào)用measure方法,而measure又會(huì)調(diào)用onMeasure方法,在onMeasure方法中又會(huì)對(duì)子元素進(jìn)行measure,這樣重復(fù)下去就完成了整個(gè)View樹(shù)的遍歷。

performLayout、performDraw傳遞過(guò)程也非常類似,不過(guò)performDraw是在draw方法中通過(guò)dispatchDraw方法實(shí)現(xiàn)的。

measure過(guò)程決定了View的寬高,而Layout方法則確定了四個(gè)頂點(diǎn)的坐標(biāo)和實(shí)際的寬高(往往等于measure中計(jì)算的寬高),draw方法則決定了View的顯示。只有完成了draw方法才能正確顯示在屏幕上。

2. MeasureSpec

MeasureSpec是measure的重要參數(shù)。
MeasureSpec 表示的是一個(gè) 32 位的整數(shù)值,它的高 2 位表示測(cè)量模式 SpecMode,低 30 位表示某種測(cè)量模式下的規(guī)格大小 SpecSize(PS:這里用到了位運(yùn)算進(jìn)行狀態(tài)壓縮來(lái)節(jié)省內(nèi)存)。MeasureSpec 是 View 類的一個(gè)靜態(tài)內(nèi)部類,用來(lái)說(shuō)明應(yīng)該如何測(cè)量這個(gè)View,有三種模式:

  • UNSPECIFIED:不指定測(cè)量模式,父視圖沒(méi)有限制子視圖的大小,子視圖可以是想要的任何尺寸,通常用于系統(tǒng)內(nèi)部,應(yīng)用開(kāi)發(fā)中很少使用到。

  • EXACTLY:精確測(cè)量模式,當(dāng)該視圖的 layout_width 或者 layout_height 指定為具體數(shù)值或者 match_parent 時(shí)生效,表示父視圖已經(jīng)決定了子視圖的精確大小,這種模式下 View 的測(cè)量值就是 SpecSize 的值。

  • AT_MOST:最大值模式,當(dāng)前視圖的 layout_width 或者 layout_height 指定為 wrap_content 時(shí)生效,此時(shí)子視圖的尺寸可以是不超過(guò)父視圖運(yùn)行的最大尺寸的任何尺寸。

下表是普通View的MeasureSpec的創(chuàng)建規(guī)則對(duì)應(yīng)表:

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的繪制從測(cè)量開(kāi)始,看看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);
        }
    }

具體操作是分發(fā)給 ViewGroup 的,由 ViewGroup 在它的 measureChild 方法中傳遞給子 View。ViewGroup 通過(guò)遍歷自身所有的子 View,并逐個(gè)調(diào)用子 View 的 measure 方法實(shí)現(xiàn)測(cè)量操作。

3.1 View的measure過(guò)程

View的measure過(guò)程由measure方法來(lái)完成,measure方法是一個(gè)final方法,不能重寫(xiě),它會(huì)調(diào)用VIew的onMeasure方法。onMeasure方法中會(huì)調(diào)用getDefaultSize方法,而getDefault方法中又會(huì)調(diào)用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方法所返回的就是測(cè)量后的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());

    }

它在沒(méi)有指定background的情況下,返回的是minSize這一屬性對(duì)應(yīng)的值,而在指定了背景的情況下,返回的是背景drawable的getMinimumWidth / getMinimumHeight方法對(duì)應(yīng)的值

這兩個(gè)方法在Drawable有原始寬度的情況下返回原始寬度,否則返回0

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

3.2 ViewGroup的measure過(guò)程

ViewGroup除了完成自己的measure過(guò)程,還會(huì)遍歷調(diào)用子元素的measure方法,然后子元素再次遞歸執(zhí)行,ViewGroup是一個(gè)抽象類,因此沒(méi)有重寫(xiě)View的onMeasure方法。但它提供了一個(gè)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執(zhí)行measure時(shí),會(huì)遍歷子元素,調(diào)用measureChild方法對(duì)子元素進(jìn)行measure。

在measureChild方法中,取出子元素的LayoutParams,通過(guò)getChildMeasureSpec方法創(chuàng)建子元素MeasureSpec,然后傳遞給View的measure方法進(jìn)行測(cè)量。

ViewGroup沒(méi)有定義測(cè)量具體過(guò)程,因?yàn)樗莻€(gè)抽象類。具體的測(cè)量過(guò)程的onMeasure方法需要子類來(lái)實(shí)現(xiàn),由于它的子類的特性可能會(huì)很大不同,所以沒(méi)法做統(tǒng)一處理(如LinearLayout和RelativeLayout)。

4. Layout

Layout流程的作用是ViewGroup確定子元素的位置。當(dāng)ViewGroup被確定后,在onLayout中會(huì)遍歷所有子元素并調(diào)用layout方法,在layout方法中會(huì)調(diào)用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);
        }
    }

首先,通過(guò)setFrame方法設(shè)定View四個(gè)頂點(diǎn)的位置(初始化mLeft,mRight,mTop,mBottom)。四個(gè)頂點(diǎn)一旦確定,則在父容器中的位置也確定了,接著便會(huì)調(diào)用onLayout方法,來(lái)讓父容器確定子容器的位置。onLayout同樣和具體布局有關(guān),因此View和ViewGroup均沒(méi)有實(shí)現(xiàn)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);
     ...
  }

最終調(diào)用到每個(gè) View 的 draw 方法繪制每個(gè)具體的 View,繪制基本上可以分為六個(gè)步驟:

   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繪制過(guò)程的傳遞是通過(guò)dispatchDraw實(shí)現(xiàn)的。dispatchDraw會(huì)遍歷調(diào)用所有子元素的draw方法。這樣draw事件就一層層傳遞了下來(lái)。

它有個(gè)比較特殊的setWillNotDraw方法。如果一個(gè)View不需要繪制任何內(nèi)容,在我們?cè)O(shè)定這個(gè)標(biāo)記為true后,系統(tǒng)就會(huì)對(duì)其進(jìn)行相應(yīng)優(yōu)化。一般View沒(méi)有啟用這個(gè)標(biāo)記位。但ViewGroup是默認(rèn)啟用的。

它對(duì)實(shí)際開(kāi)發(fā)的意義在于:我們的自定義控件繼承于ViewGroup并且不具備繪制功能時(shí),可以開(kāi)啟這個(gè)標(biāo)記位方便系統(tǒng)進(jìn)行后續(xù)優(yōu)化。

6. 結(jié)束語(yǔ)

關(guān)于view繪制的原理在網(wǎng)上也特別多,時(shí)間久了也容易忘記,看一遍別人的,還不如順便把他們的記錄下來(lái),方便自己以后溫習(xí)。

重要參考:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容