1. 程式人生 > >從ViewRootImpl類分析View繪製的流程

從ViewRootImpl類分析View繪製的流程

我們知道Activity中的PhoneWindow物件幫我們建立了一個PhoneWindow內部類DecorView(父類為FrameLayout)視窗頂層檢視,

然後通過LayoutInflater將xml內容佈局解析成View樹形結構新增到DecorView頂層檢視中id為content的FrameLayout父容器上面。到此,我們已經知道Activity的content內容佈局最終

會新增到DecorView視窗頂層檢視上面,相信很多人也會有這樣的疑惑:視窗頂層檢視DecorView是怎麼繪製到我們的手機螢幕上的呢?

這篇部落格來嘗試著分析DecorView的繪製流程。

這裡寫圖片描述

頂層檢視DecorView新增到視窗的過程

DecorView是怎麼新增到視窗的呢?這時候我們不得不從Activity是怎麼啟動的說起,當Activity初始化 Window和將佈局新增到

PhoneWindow的內部類DecorView類之後,ActivityThread類會呼叫handleResumeActivity方法將頂層檢視DecorView新增到PhoneWindow視窗,來看看handlerResumeActivity方法的實現:

0-1

Step1

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume) {

            ...
............... if (r.window == null && !a.mFinished && willBeVisible) { //獲得當前Activity的PhoneWindow物件 r.window = r.activity.getWindow(); //獲得當前phoneWindow內部類DecorView物件 View decor = r.window.getDecorView(); //設定視窗頂層檢視DecorView可見度
decor.setVisibility(View.INVISIBLE); //得當當前Activity的WindowManagerImpl物件 ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; if (a.mVisibleFromClient) { //標記根佈局DecorView已經新增到視窗 a.mWindowAdded = true; //將根佈局DecorView新增到當前Activity的視窗上面 wm.addView(decor, l); .....................

分析:詳細步驟以上程式碼都有詳細註釋,這裡就不一一解釋。handlerResumeActivity()方法主要就是通過第 23 行程式碼將

Activity的頂層檢視DecorView新增到視窗檢視上。我們來看看WindowManagerImpl類的addView()方法。

@Override
    public void addView(View view, ViewGroup.LayoutParams params) {
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }

原始碼很簡單,直接呼叫了 mGlobal物件的addView()方法。繼續跟蹤,mGlobal物件是WindowManagerGlobal類。進入WindowManagerGlobal類看addView()方法。

0-2

Step2

 public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {

        ............

        ViewRootImpl root;
        View panelParentView = null;

        ............

        //獲得ViewRootImpl物件root
         root = new ViewRootImpl(view.getContext(), display);

        ...........

        // do this last because it fires off messages to start doing things
        try {
            //將傳進來的引數DecorView設定到root中
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
          ...........
        }
    }

該方法中建立了一個ViewRootImpl物件root,然後呼叫ViewRootImpl類中的setView成員方法()。繼續跟蹤程式碼進入ViewRootImpl類分析:

0-3

Step3

 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
            //將頂層檢視DecorView賦值給全域性的mView
                mView = view;
            .............
            //標記已新增DecorView
             mAdded = true;
            .............
            //請求佈局
            requestLayout();

            .............     
        }
 }

該方法實現有點長,我省略了其他程式碼,直接看以上幾行程式碼:

  1. 將外部引數DecorView賦值給mView成員變數
  2. 標記DecorView已新增到ViewRootImpl
  3. 呼叫requestLayout方法請求佈局

0-4

跟蹤程式碼進入到 requestLayout()方法:
Step4

@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }
    ................

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
        }
    }

..............

final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

...............

 void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().removeSyncBarrier(mTraversalBarrier);

            try {
                performTraversals();
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
        }
    }

............

跟蹤程式碼,最後DecorView的繪製會進入到ViewRootImpl類中的performTraversals()成員方法,這個過程可以參考上面的程式碼流程圖。現在我們主要來分析下 ViewRootImpl類中的performTraversals()方法。

0-5

Step5

private void performTraversals() {
        // cache mView since it is used so much below...
        //我們在Step3知道,mView就是DecorView根佈局
        final View host = mView;
        //在Step3 成員變數mAdded賦值為true,因此條件不成立
        if (host == null || !mAdded)
            return;
        //是否正在遍歷
        mIsInTraversal = true;
        //是否馬上繪製View
        mWillDrawSoon = true;

        .............
        //頂層檢視DecorView所需要視窗的寬度和高度
        int desiredWindowWidth;
        int desiredWindowHeight;

        .....................
        //在構造方法中mFirst已經設定為true,表示是否是第一次繪製DecorView
        if (mFirst) {
            mFullRedrawNeeded = true;
            mLayoutRequested = true;
            //如果視窗的型別是有狀態列的,那麼頂層檢視DecorView所需要視窗的寬度和高度就是除了狀態列
            if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
                    || lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
                // NOTE -- system code, won't try to do compat mode.
                Point size = new Point();
                mDisplay.getRealSize(size);
                desiredWindowWidth = size.x;
                desiredWindowHeight = size.y;
            } else {//否則頂層檢視DecorView所需要視窗的寬度和高度就是整個螢幕的寬高
                DisplayMetrics packageMetrics =
                    mView.getContext().getResources().getDisplayMetrics();
                desiredWindowWidth = packageMetrics.widthPixels;
                desiredWindowHeight = packageMetrics.heightPixels;
            }
    }
............
//獲得view寬高的測量規格,mWidth和mHeight表示視窗的寬高,lp.widthhe和lp.height表示DecorView根佈局寬和高
 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
 int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

  // Ask host how big it wants to be
  //執行測量操作
  performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

........................
//執行佈局操作
 performLayout(lp, desiredWindowWidth, desiredWindowHeight);

.......................
//執行繪製操作
performDraw();

}

該方法主要流程就體現了View繪製渲染的三個主要步驟,分別是測量,佈局,繪製三個階段。

這裡寫圖片描述

這裡先給出Android系統View的繪製流程:依次執行View類裡面的如下三個方法:

  1. measure(int ,int) :測量View的大小
  2. layout(int ,int ,int ,int) :設定子View的位置
  3. draw(Canvas) :繪製View內容到Canvas畫布上

測量measure

1-1

從performTraversals方法我們可以看到,在執行performMeasure測量之前要通過getRootMeasureSpec方法獲得頂層檢視DecorView的測量規格,跟蹤程式碼進入getRootMeasureSpec()方法:

  /**
     * Figures out the measure spec for the root view in a window based on it's
     * layout params.
     *
     * @param windowSize
     *            The available width or height of the window
     *
     * @param rootDimension
     *            The layout params for one dimension (width or height) of the
     *            window.
     *
     * @return The measure spec to use to measure the root view.
     */
    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
        //匹配父容器時,測量模式為MeasureSpec.EXACTLY,測量大小直接為螢幕的大小,也就是充滿真個螢幕
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        //包裹內容時,測量模式為MeasureSpec.AT_MOST,測量大小直接為螢幕大小,也就是充滿真個螢幕
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        //其他情況時,測量模式為MeasureSpec.EXACTLY,測量大小為DecorView頂層檢視佈局設定的大小。
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

分析:該方法主要作用是在整個視窗的基礎上計算出root view(頂層檢視DecorView)的測量規格,該方法的兩個引數分別表示:

  1. windowSize:當前手機視窗的有效寬和高,一般都是除了通知欄的螢幕寬和高
  2. rootDimension 根佈局DecorView請求的寬和高,由前面的部落格我們知道是MATCH_PARENT

因此DecorView根佈局的測量模式就是MeasureSpec.EXACTLY,測量大小一般都是整個螢幕大小,所以一般我們的Activity

視窗都是全屏的。因此上面程式碼走第一個分支,通過呼叫MeasureSpec.makeMeasureSpec方法將

DecorView的測量模式和測量大小封裝成DecorView的測量規格。

1-2

由於performMeasure()方法呼叫了 View中measure()方法倆進行測量,並且DecorView(繼承自FrameLayout)的父類是

ViewGroup,祖父類是View。因此我們從View的成員函式measure開始分析整個測量過程。

這裡寫圖片描述

這個過程分為 3 步,我們來一一分析。

Step1


    int mOldWidthMeasureSpec = Integer.MIN_VALUE;

    int mOldHeightMeasureSpec = Integer.MIN_VALUE;

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

        ..................
        //如果上一次的測量規格和這次不一樣,則條件滿足,重新測量檢視View的大小
        if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
                widthMeasureSpec != mOldWidthMeasureSpec ||
                heightMeasureSpec != mOldHeightMeasureSpec) {

            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                    mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;

    }

分析:
1.程式碼第10行:判斷當前檢視View是否需要重新測量,當上一次檢視View測量的規格和本次檢視View測量規格不一樣時,就說明檢視View的大小有改變,因此需要重新測量。

2.程式碼第23行:呼叫了onMeasure方法進行測量,說明View主要的測量邏輯是在該方法中實現。

3.程式碼第35-36行:儲存本次檢視View的測量規格到mOldWidthMeasureSpec和mOldHeightMeasureSpec以便下次測量條件的判斷是否需要重新測量。

1-3

跟蹤程式碼,進入View類的 onMeasure方法

 /**
     * <p>
     * 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 overriden by subclasses to provide accurate and efficient
     * measurement of their contents.
     * </p>
     *
     * <p>
     * <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.
     * </p>
     *
     * <p>
     * The base class implementation of measure defaults to the background size,
     * unless a larger size is allowed by the MeasureSpec. Subclasses should
     * override {@link #onMeasure(int, int)} to provide better measurements of
     * their content.
     * </p>
     *
     * <p>
     * If this method is overridden, it is the subclass's responsibility to make
     * sure the measured height and width are at least the view's minimum height
     * and width ({@link #getSuggestedMinimumHeight()} and
     * {@link #getSuggestedMinimumWidth()}).
     * </p>
     *
     * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
     *                         The requirements are encoded with
     *                         {@link android.view.View.MeasureSpec}.
     * @param heightMeasureSpec vertical space requirements as imposed by the parent.
     *                         The requirements are encoded with
     *                         {@link android.view.View.MeasureSpec}.
     *
     * @see #getMeasuredWidth()
     * @see #getMeasuredHeight()
     * @see #setMeasuredDimension(int, int)
     * @see #getSuggestedMinimumHeight()
     * @see #getSuggestedMinimumWidth()
     * @see android.view.View.MeasureSpec#getMode(int)
     * @see android.view.View.MeasureSpec#getSize(int)
     */
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

分析:
該方法的實現也很簡單,直接呼叫setMeasuredDimension方法完成檢視View的測量。我們知道,Android中所有的檢視元件都是繼承自View實現的。因此該方法提供了一個預設測量檢視View大小的實現。

1-4

言外之意,如果你不想你自己的View使用預設實現來測量View的寬高的話,你可以在子類中重寫onMeasure方法來自定義測量方法。我們先來看看預設測量寬高的實現。跟蹤程式碼進入getDefaultSize方法:

 /**
     * 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);
        //獲得父親容器留給子檢視View的大小
        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;
    }

分析:該方法的作用是根據View佈局設定的寬高和父View傳遞的測量規格重新計算View的測量寬高。由此可以知道,我們佈局的

子View最終的大小是由佈局大小和父容器的測量規格共同決定的。如果自定義View你沒有重寫onMeasure使用系統預設方法的

話,測量模式MeasureSpec.AT_MOST和MeasureSpec.EXACTLY下的測量大小是一樣的。我們來總結一下測量模式的種類:

  1. MeasureSpec.EXACTLY:確定模式,父容器希望子檢視View的大小是固定,也就是specSize大小。
  2. MeasureSpec.AT_MOST:最大模式,父容器希望子檢視View的大小不超過父容器希望的大小,也就是不超過specSize大小。
  3. MeasureSpec.UNSPECIFIED: 不確定模式,子檢視View請求多大就是多大,父容器不限制其大小範圍,也就是size大小。

從上面程式碼可以看出,當測量模式是MeasureSpec.UNSPECIFIED時,View的測量值為size,當測量模式為

MeasureSpec.AT_MOST或者case MeasureSpec.EXACTLY時,View的測量值為specSize。我們知道,specSize是由父容器決

定,那麼size是怎麼計算出來的呢?getDefaultSize方法的第一個引數是呼叫getSuggestedMinimumWidth方法獲得。進入getSuggestedMinimumWidth方法看看實現:

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

原來size大小是獲取View屬性當中的最小值,也就是 android:minWidth和 android:minHeight的值,前提是View沒有設定背景屬性。否則就在最小值和背景的最小值中間取最大值。

sizeSpec大小是有父容器決定的,我們由 1-1節知道父容器DecorView的測量模式是MeasureSpec.EXACTLY,測量大小sizeSpec是整個螢幕的大小。

setp2
而DecorView是繼承自FrameLayout的,那麼我們來看看FrameLayout類中的onMeasure方法的實現

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        ..............
        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                //測量FrameLayout下每個子檢視View的寬和高
                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);
                    }
                }
            }
        }

        // Account for padding too
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // Check against our minimum height and width
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Check against our foreground's minimum height and width
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }
        //設定當前FrameLayout測量結果,此方法的呼叫表示當前View測量的結束。
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
}

分析:由以上程式碼發現,ViewGroup測量結果都是帶邊距的,程式碼第9-27行就是遍歷測量FrameLayout下子檢視View的大小了。

程式碼第44行,最後呼叫setMeasuredDimension方法設定當前View的測量結果,此方法的呼叫表示當前View測量結束。

那麼我們來分析下程式碼第12行measureChildWithMargins方法測量FrameLayout下的子檢視View的大小,跟蹤原始碼:

Step3:
由於FrameLayout父類是ViewGroup,measureChildWithMargins方法在ViewGroup下

/**
     * 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);
    }

分析:該方法中呼叫getChildMeasureSpec方法來獲得ViewGroup下的子檢視View的測量規格。然後將測量規格最為引數傳遞給

View的measure方法,最終完成所有子檢視View的測量。來看看這裡是怎麼獲得子檢視View的測量規格的,進入getChildMeasureSpec方法:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } 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;

       ...........

        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

分析:由1-1節我們知道根佈局DecorView的測量規格中的測量模式是MeasureSpec.EXACTLY,測量大小是整個視窗大小。因此上面程式碼分支走MeasureSpec.EXACTLY。子檢視View的測量規格由其寬和高參數決定。

  1. 當DecorView根佈局的子檢視View寬高為一個確定值childDimension時,該View的測量模式為MeasureSpec.EXACTLY,測量大小就是childDimension。
  2. 當子檢視View寬高為MATCH_PARENT時,該View的測量模式為MeasureSpec.EXACTLY,測量大小是父容器DecorView規定的大小,為整個螢幕大小MATCH_PARENT。
  3. 當子檢視View寬高為WRAP_CONTENT時,該View的測量模式為MeasureSpec.AT_MOST,測量大小是父容器DecorView規定的大小,為整個螢幕大小MATCH_PARENT。

這裡我們來驗證一下以上的結論,目的是進一步理解 View的幾種測量模式和View的測量規格。

1.定義一個佈局activity_main.xml如下:

<com.xjp.layoutdemo.MyView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Button"
    android:gravity="start"/>

這個佈局很簡單,直接將自定義的MyView作為Activity的內容佈局。
2.自定義MyView程式碼如下:

public class MyView extends View {

    private static final String TAG = "MyCustomView";
    private String titleText = "Hello world";

    private int titleColor = Color.BLACK;
    private int titleBackgroundColor = Color.RED;
    private int titleSize = 16;

    private Paint mPaint;
    private Rect mBound;

    public MyView(Context context) {
        this(context, null);
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int specMode = MeasureSpec.getMode(widthMeasureSpec);
        int specSize = MeasureSpec.getSize(widthMeasureSpec);

        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                Log.e(TAG, "UNSPECIFIED.....");
                break;
            case MeasureSpec.AT_MOST:
                Log.e(TAG, "AT_MOST.....");
                break;
            case MeasureSpec.EXACTLY:
                Log.e(TAG, "EXACTLY.....");
                break;
        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    /**
     * 初始化
     */
    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setTextSize(titleSize);
        /**
         * 得到自定義View的titleText內容的寬和高
         */
        mBound = new Rect();
        mPaint.getTextBounds(titleText, 0, titleText.length(), mBound);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        mPaint.setColor(titleBackgroundColor);
        canvas.drawCircle(getWidth() / 2f, getWidth() / 2f, getWidth() / 2f, mPaint);
        mPaint.setColor(titleColor);
        canvas.drawText(titleText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);
    }
}

這裡寫圖片描述
自定義的MyView也很簡單,僅僅重寫了onDraw方法,onMeasure方法呼叫父類方法。程式碼執行之後你會發現,

1.佈局中設定的MyView大小是wrap_content包裹內容的,但是View檢視卻充滿整個螢幕。看打印發現當前的測量模式是MeasureSpec.AT_MOST。

2.當MyView大小是match_parent填滿父容器時,View檢視也是充滿整個螢幕,看打印發現測量模式是MeasureSpec.EXACTLY。

3.當MyView大小是固定值,比如是1200dp和1200dp時,View檢視是超出整個螢幕的。
這裡寫圖片描述

原因是此處的Activity內容佈局的父容器也是一個id為content的FrameLayout佈局。這裡就不解釋以上三種情況的原因了,參考Stpe3解釋的很詳細了。

至此,整個View樹型結構的佈局測量流程可以歸納如下:

這裡寫圖片描述

measure總結

  1. View的measure方法是final型別的,子類不可以重寫,子類可以通過重寫onMeasure方法來測量自己的大小,當然也可以不重寫onMeasure方法使用系統預設測量大小。
  2. View測量結束的標誌是呼叫了View類中的setMeasuredDimension成員方法,言外之意是,如果你需要在自定義的View中重寫onMeasure方法,在你測量結束之前你必須呼叫setMeasuredDimension方法測量才有效。
  3. 在Activity生命週期onCreate和onResume方法中呼叫View.getWidth()和View.getMeasuredHeight()返回值為0的,是因為當前View的測量還沒有開始,這裡關係到Activity啟動過程,文章開頭說了當ActivityThread類中的performResumeActivity方法執行之後才將DecorView新增到PhoneWindow視窗上,開始測量。在Activity生命週期onCreate在中performResumeActivity還為執行,因此呼叫View.getMeasuredHeight()返回值為0。
  4. 子檢視View的大小是由父容器View和子檢視View佈局共同決定的。

佈局Layout

0-5節可知,View檢視繪製流程中的佈局layout是由ViewRootImpl中的performLayout成員方法開始的,看原始碼:

2-1

 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        ..................
        //標記當前開始佈局
        mInLayout = true;
        //mView就是DecorView
        final View host = mView;
        ..................
        //DecorView請求佈局
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
        //標記佈局結束
        mInLayout = false;
        ..................
}

分析:
程式碼第10行發現,DecorView的四個位置左=0,頂=0,右=螢幕寬,底=螢幕寬,說明DecorView佈局的位置是從螢幕最左最頂端開始佈局,到螢幕最低最右結束。因此DecorView根佈局是充滿整個螢幕的。

該方法主要呼叫了View類的layout方法,跟蹤程式碼進入View類的layout方法瞧瞧吧

2-2

/**
     * Assign a size and position to a view and all of its
     * descendants
     *
     * <p>This is the second phase of the layout mechanism.
     * (The first is measuring). In this phase, each parent calls
     * layout on all of its children to position them.
     * This is typically done using the child measurements
     * that were stored in the measure pass().</p>
     *
     * <p>Derived classes should not override this method.
     * Derived classes with children should override
     * onLayout. In that method, they should
     * call layout on each of their children.</p>
     *
     * @param l Left position, relative to parent
     * @param t Top position, relative to parent
     * @param r Right position, relative to parent
     * @param b Bottom position, relative to parent
     */
    @SuppressWarnings({"unchecked"})
    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;
        }
        //儲存上一次View的四個位置
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        //設定當前檢視View的左,頂,右,底的位置,並且判斷佈局是否有改變
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        //如果佈局有改變,條件成立,則檢視View重新佈局
            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //呼叫onLayout,將具體佈局邏輯留給子類實現
            onLayout(changed, l, t, r, b);
            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);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

分析:
1.程式碼第23-32行儲存本次佈局的四個位置,用於佈局變化的監聽事件,如果使用者設定了佈局變化的監聽事件,則程式碼第43-50就會執行設定監聽事件。

2.程式碼第34-35行設定當前View的佈局位置,也就是當呼叫了setFrame(l, t, r, b)方法之後,當前View佈局基本完成,既然這樣為什麼還要第39行 onLayout方法呢?稍後解答,這裡來分析一下setFrame是怎麼設定當前View的佈局位置的。

進入setFrame方法

2-3

/**
     * Assign a size and position to this view.
     *
     * This is called from layout.
     *
     * @param left Left position, relative to parent
     * @param top Top position, relative to parent
     * @param right Right position, relative to parent
     * @param bottom Bottom position, relative to parent
     * @return true if the new size and position are different than the
     *         previous ones
     * {@hide}
     */
    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;
        //當上,下,左,右四個位置有一個和上次的值不一樣都會重新佈局
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;
            //得到本次和上次的寬和高
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            //判斷本次View的寬高和上次View的寬高是否相等
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            // Invalidate our old position
            //清楚上次佈局的位置
            invalidate(sizeChanged);
            //儲存當前View的最新位置
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

            mPrivateFlags |= PFLAG_HAS_BOUNDS;

            //如果當前View的尺寸有所變化
            if (sizeChanged) {
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }

            ...............
        return changed;
    }

分析:
1.程式碼第17行,如果當前View檢視的最新位置和上一次不一樣時,則View會重新佈局。

2.程式碼第32-38行,儲存當前View的最新位置,到此當前View的佈局基本結束。從這裡我們可以看到,四個全域性變數 mLeft,mTop,mRight,mBottom在此刻賦值,聯想我們平時使用的View.getWidth()方法獲得View的寬高,你可以發現,其實View.getWidth()方法的實現如下:

public final int getWidth() {
        return mRight - mLeft;
    }
 public final int getHeight() {
        return mBot