View繪製——畫多大?

Taylor發表於2019-02-25

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

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

如果想直接看結論可以移步到第三篇末尾。

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

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

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

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

這一篇將以原始碼中的幾個關鍵函式為線索分析“測量(measure)”。

View.measure()

“測量”要解決的問題是確定待繪製View的尺寸,以View.measure()為入口,一探究竟:

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    /**
     * <p>
     * This is called to find out how big a view should be. The parent
     * supplies constraint information in the width and height parameters.
     * 這個方法用於決定當前view到底有多大,父親提供寬高引數起到限制大小的作用
     *
     * The actual measurement work of a view is performed in
     * {@link #onMeasure(int, int)}, called by this method. Therefore, only
     * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
     * 真正的測量工作在onMeasure()中進行
     *
     * @param widthMeasureSpec Horizontal space requirements as imposed by the
     *        parent(父親施加的寬度要求)
     * @param heightMeasureSpec Vertical space requirements as imposed by the
     *        parent(父親施加的高度要求)
     *
     * @see #onMeasure(int, int)
     */
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        ...
    }
    
    /**
     * Measure the view and its content to determine the measured width and the
     * measured height. This method is invoked by {@link #measure(int, int)} and
     * should be overridden by subclasses to provide accurate and efficient
     * measurement of their contents.
     * View子類應該過載這個方法以定義自己尺寸
     *
     * <strong>CONTRACT:</strong> When overriding this method, you
     * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
     * measured width and height of this view. Failure to do so will trigger an
     * <code>IllegalStateException</code>, thrown by
     * {@link #measure(int, int)}. Calling the superclass'
     * {@link #onMeasure(int, int)} is a valid use.
     * 過載方法必須呼叫 setMeasuredDimension()
     */
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
}
複製程式碼

從註釋中得知這麼幾個資訊:

  1. 真正的測量工作在onMeasure()中進行,View的子類應該過載這個方法以定義自己尺寸。
  2. onMeasure()中必須呼叫setMeasuredDimension()
  3. View會通過傳入的寬高引數對子View的尺寸施加限制。

順帶便看了一下常見控制元件如何過載onMeasure(),其實套路都一樣,不管是TextView還是ImageView,在一系列計算得出寬高值後將傳入setMeasuredDimension()。所以,整個測量過程的終點是View.setMeasuredDimension()的呼叫,它表示著檢視大小已經有確定值。

ViewGroup.onMeasure()

View必然依附於一棵“View樹”,那父View是如何對子View的尺寸施加影響的?全域性搜尋View.measure()被呼叫的地方,在很多ViewGroup型別的控制元件中發現類似child.measure()的呼叫,以最簡單的FrameLayout為例:

public class FrameLayout extends ViewGroup {
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //獲得孩子數量
        int count = getChildCount();
        ...
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //遍歷可見孩子或者強制遍歷所有孩子
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                //測量孩子
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                //記憶孩子中最大寬度
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                //記憶孩子中最大高度
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

        ...

        //以最孩子中最大的尺寸作為自己的尺寸
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

        ...
    }
}
複製程式碼

FrameLayout會遍歷所有可見的孩子記憶其中最大寬度和最大高度,並以此作為自己的寬和高(這是FrameLayout的測量演算法,其他的ViewGroup應該也有自己獨特的測量演算法。)

ViewGroup.measureChildWithMargins()

父控制元件在遍歷每個孩子時會呼叫measureChildWithMargins()來測量孩子:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    /**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding
     * and margins. The child must have MarginLayoutParams The heavy lifting is
     * done in getChildMeasureSpec.
     * 要求孩子自己測量自己(考慮父親的要求和自己的邊距)
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view(來自父親的寬度要求)
     * @param widthUsed Extra space that has been used up by the parent
     *        horizontally (possibly by other children of the parent)
     * @param parentHeightMeasureSpec The height requirements for this view(來自父親的高度要求)
     * @param heightUsed Extra space that has been used up by the parent
     *        vertically (possibly by other children of the parent)
     */
    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        //獲得孩子佈局引數
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        //結合父親要求和孩子訴求分別獲得寬和高
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        //孩子自己測量自己
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
}
複製程式碼

讀到這裡應該可以總結出ViewGroup的測量過程: 遍歷所有的孩子,通過呼叫View.measure()觸發孩子們測量自己。測量完所有孩子之後,按照自有的測量演算法將孩子們的尺寸轉換成自己的尺寸並傳入View.setMeasuredDimension()

ViewGroup.getChildMeasureSpec()

觸發孩子測量自己的時候傳入了寬高兩個引數,它們是通過ViewGroup.getChildMeasureSpec()產生的:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        ...
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
        ...
        switch (specMode) {
        case MeasureSpec.EXACTLY:
            ...
            break;

        case MeasureSpec.AT_MOST:
            ...
            break;

        case MeasureSpec.UNSPECIFIED:
            ...
            break;
        }
        ...
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
}
複製程式碼

這個函式中有一個陌生的類MeasureSpec,點進去看看:

   /**
     * A MeasureSpec encapsulates the layout requirements passed from parent to child.
     * Each MeasureSpec represents a requirement for either the width or the height.
     * A MeasureSpec is comprised of a size and a mode. There are three possible
     * modes:
     * MeasureSpec包裝了父親對孩子的佈局要求,它是尺寸和模式的混合,它包含三種模式
     *
     * MeasureSpecs are implemented as ints to reduce object allocation.
     * MeasureSpec被實現成一個int值為了節約空間
     */
    public static class MeasureSpec {
        //前2位是模式,後30位是尺寸
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
            /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         * 散養父親:隨便孩子多大
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         * 圈養父親:強制指定孩子尺寸
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         * 折中父親:在有限範圍內允許孩子想多大就多大
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;
        ...
    }
複製程式碼

MeasureSpec用於在View測量過程中描述尺寸,它是一個包含了佈局模式和佈局尺寸的int值(32位),其中最高的2位代表佈局模式,後30位代表佈局尺寸。它包含三種佈局模式分別是UNSPECIFIEDEXACTLYAT_MOST

結合剛才的ViewGroup.getChildMeasureSpec()來探究下這些模式到底是什麼意思:

    /**
     * Does the hard part of measureChildren: figuring out the MeasureSpec to
     * pass to a particular child. This method figures out the right MeasureSpec
     * for one dimension (height or width) of one child view.
     * 獲得孩子佈局引數(寬或高):混合父親要求和孩子訴求
     *
     * The goal is to combine information from our MeasureSpec with the
     * LayoutParams of the child to get the best possible results. For example,
     * if the this view knows its size (because its MeasureSpec has a mode of
     * EXACTLY), and the child has indicated in its LayoutParams that it wants
     * to be the same size as the parent, the parent should ask the child to
     * layout given an exact size.
     *
     * @param spec The requirements for this view 父親要求:要求孩子多寬多高
     * @param padding The padding of this view for the current dimension and
     *        margins, if applicable
     * @param childDimension How big the child wants to be in the current
     *        dimension孩子訴求:想要多寬多高
     * @return a MeasureSpec integer for the child
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //獲得父親測量模式
        int specMode = MeasureSpec.getMode(spec);
        //獲得父親尺寸
        int specSize = MeasureSpec.getSize(spec);

        //從父親尺寸中去除padding
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        //結合父親要求和孩子訴求計算出孩子尺寸,父親有三種型別的要求,孩子有三種型別的訴求,孩子尺寸一共有9種結果。
        switch (specMode) {
        // Parent has imposed an exact size on us(父親有明確尺寸)
        case MeasureSpec.EXACTLY:
            //如果孩子對自己尺寸有明確要求,只能滿足它,不考慮padding
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            }
            //如果孩子要求和父親一樣大且父親有明確尺寸,則孩子尺寸有確定,考慮padding
            else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            }
            //如果孩子要求完全顯示自己內容,但它不能超過父親,考慮padding
            else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can’t be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us(父親有明確最大尺寸)
        case MeasureSpec.AT_MOST:
            //如果孩子對自己尺寸有明確要求,只能滿足它,不考慮padding
            if (childDimension >= 0) {
                // Child wants a specific size... so be it(父親其實很無奈)
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            }
            //如果孩子要求和父親一樣大,但父親只有明確最大尺寸,則孩子也能有明確最大尺寸,考慮padding
            else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            //如果孩子要求完全顯示自己內容,但它不能超過父親,考慮padding
            else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can‘t be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be(父親尺寸沒有約束)
        case MeasureSpec.UNSPECIFIED:
            //如果孩子對自己尺寸有明確要求,只能滿足它,不考慮padding
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
複製程式碼

這個函式揭示了一個“人間真相”:父親總是對孩子有要求,但孩子也總是有自己的訴求。最圓滿的結局莫過於充分考量兩方面的需求並調和之。ViewGroup.getChildMeasureSpec()將3種父親的要求和3種孩子的訴求進行了調和(詳見上述程式碼及註釋)

總結

  • 父控制元件在測量自己的時候會先遍歷所有子控制元件,並觸發它們測量自己。完成孩子測量後,根據孩子的尺寸來確定自己的尺寸。View繪製中的測量是一個從View樹開始自頂向下的遞迴過程,遞表示父控制元件觸發子控制元件測量自己,歸表示子控制元件完成測量後,父控制元件測量自己。
  • 父控制元件會將自己的佈局要求和子控制元件的佈局訴求結合成一個MeasureSpec物件傳遞給子控制元件以指導子控制元件測量自己。
  • MeasureSpec用於在View測量過程中描述尺寸,它是一個包含了佈局模式和佈局尺寸的int值(32位),其中最高的2位代表佈局模式,後30位代表佈局尺寸。
  • 整個測量過程的終點是View.setMeasuredDimension()的呼叫,它表示著檢視大小已經有確定值。

相關文章