Android 源碼分析二 View 測量

第一篇說完 View 建立,接着講講 View 的測量和佈局。先講講總體思想,View 的 測量是自上而下,一層一層進行。涉及到的核心方法就是 View 中的 measure() layout() 對於咱們來講,更應該關心的就是 onMeasure()onLayout() 的回調方法。本文着重關注測量相關代碼,至於 layout ,這個是 ViewGroup 的具體邏輯。android

onMeasure

說到 onMeasure() 方法就必須提一嘴它涉及到的測量模式。以及模式對子 view 的約束。程序員

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
複製代碼

這是 ViewonMeasure() 方法默認實現,這裏又涉及到三個重要的方法, setMeasuredDimension()getDefaultSize()setMeasuredDimension() 這個方法很是重要,它是咱們設置測量寬高值的官方惟一指定方法。也是咱們在 onMeasure() 方法中必須調用的方法。若是你想了下,本身彷佛在 onMeasre() 沒有手動調用過該方法,而且也沒有啥異常,不要猶豫,你必定是調用了 super.onMeasure() ,setMeasuredDimension()最終會完成對 measureHeightmeasureWidth 賦值,具體操做往下看。app

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
複製代碼

setMeasuredDimension() 中調用私有的 setMeasuredDimensionRaw() 方法完成對 mMeasuredWidthmMeasuredHeight 賦值,而後更新 flag 。ide

getSuggestedMinimumWidth/Height

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

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

}
複製代碼

這兩個方法的默認實現就是去獲取 View 設置的背景和最小值中最小的那個。背景設置就不用說了,至於這個寬高最小值,其實就是經過 xml 中 minWidth 或者 API 動態設置。佈局

getDefaultSize()

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;
}
複製代碼

這個方法也比較重要,由於它涉及到測量模式。先分析下參數,輸入的第一個 size 是剛剛獲取的最小值。第二個就是父佈局回調過來的測量參數。post

經過上面能夠看到,測量模式一共有三種。MeasureSpec.UNSPECIFIED MeasureSpec.AT_MOST MeasureSpec.EXACTLYspa

若是是 MeasureSpec.UNSPECIFIED ,那麼就直接使用獲取的最小值。若是是其餘兩種模式,那麼就從測量參數中獲取對應的 size。注意,在這個方法中,根本沒有對 AT_MOST 和 EXACTLY 作區分處理。code

MeasureSpec 測量模式和size

經過上面 getDefaultSize() 方法咱們已經看到 MeasureSpec 中包含有測量模式和對應 size。那麼它是怎麼作到一個 int 類型,表示兩種信息呢?程序員的小巧思上線。xml

一個 int 類型,32位。這裏就是使用了高2位來表示測量模式,低 30 位用來記錄 size。ip

//左移常量 shift 有轉變的意思 並且在 Kotlin 中 左移使用 shl() 表示
private static final int MODE_SHIFT = 30;
//二進制就是這樣11000...000
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
//00 000...000
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
//01 000...000
public static final int EXACTLY     = 1 << MODE_SHIFT;
//10 000...000
public static final int AT_MOST     = 2 << MODE_SHIFT;  
複製代碼

接着看看是怎麼賦值和取值的呢。

public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                    @MeasureSpecMode int mode) {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
複製代碼

看着是否是比較高大上?都說了這是程序員小巧思,代碼固然比較溜。這裏涉及到與或非三種運算。直接舉個例子吧,好比我要建立一個 size 爲 16 模式是 EXACTLY 的 MeasureSpec 那麼就是這樣的。

size    對應   00 000... 1111
    mode    對應   01 000... 0000
    mask    對應   11 000... 0000
    ~mask   對應   00 111... 1111
    size & ~mask  00 000... 1111 = size
    mode & mask   01 000... 0000 = mode
    size | mode   01 000... 1111 = 最終結果
複製代碼

經過這麼一對應,結果很是清晰,有沒有以爲 makeMeasureSpec() 方法中前兩次 & 操做都是很無效的?其實它能保證 mode 和 size 不越界,不會互相污染。反正你也別瞎傳值。賦值時,方法上已經對兩個參數都有輸入限制。

再說完三種模式定義以後,接着就須要考慮 xml 中的 寬高指定最後是怎麼轉換爲對應的 模式。好比說,咱們寫 wrap_content, 那麼對應的測量模式究竟是怎樣的呢?

舉個例子,好比說以下的一個佈局。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/parent_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center"
    android:background="@color/colorAccent">

    <ProgressBar
        android:id="@+id/child"
        android:layout_width="20dp"
        android:layout_height="20dp"
        android:layout_gravity="center"
        android:indeterminate="true"
        android:indeterminateTint="@color/colorPrimary"
        android:indeterminateTintMode="src_in"/>

</FrameLayout>
複製代碼

效果經過預覽就能看到,FrameLayout 佔據全屏,ProgressBar 居中顯示,size 就是 20 dp 。

ProgressBaronMeasure() 方法以下:

@Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int dw = 0;
    int dh = 0;

    final Drawable d = mCurrentDrawable;
    if (d != null) {
        dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth()));
        dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight()));
    }

    updateDrawableState();

    dw += mPaddingLeft + mPaddingRight;
    dh += mPaddingTop + mPaddingBottom;

    final int measuredWidth = resolveSizeAndState(dw, widthMeasureSpec, 0);
    final int measuredHeight = resolveSizeAndState(dh, heightMeasureSpec, 0);
    setMeasuredDimension(measuredWidth, measuredHeight);
}
複製代碼

能夠看到,ProgressBar 複寫了 View 的 onMeasure() 方法,而且沒有調用 super 。因此,最上面那一套分析對於它無效。所以,它也本身在最後調用了 setMeasuredDimension() 方法完成一次測量。在這裏,又涉及到一個 View 的靜態方法 -- resolveSizeAndState()

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}
複製代碼

入參 size 是背景大小,MeasureSpeconMeasure() 方法傳入,參數由 parent 指定。 state 的問題先不考慮,咱們這裏主要看 size 。對比剛剛說過的 getDefaultSize() , 這個方法已經將 AT_MOSTEXACTLY 作了區分處理,一共又四種狀況。

AT_MOST 下,若是測量值小於背景大小,即 View 須要的 size 比 parent 能給的最大值還要大。這個時候仍是設置爲 測量值,而且加入了 MEASURED_STATE_TOO_SMALL 這個狀態。若是測量值大於背景大小,正常狀況也就是這樣,這時候就設置爲背景大小。EXACTLY 下,那就是測量值。UNSPECIFIED 下,就是背景 size。

###數值傳遞

上面其實都是說的是 ViewonMeasure 中測量本身的狀況,可是,parent 傳入的 MeasureSpec 參數究竟是怎麼確認的呢?child 設置 match_parent 或者 wrap_content 或者 精確值,會影響對應的 MeasureSpec 的模式和 size 嗎?

帶着這些問題,咱們看看 FrameLayoutonMeasure() 方法的部分實現。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    // 若是本身的寬高有一個不是精確值,measureMatchParentChildren flag 就 爲 true
    final boolean measureMatchParentChildren =
            MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
            MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
    mMatchParentChildren.clear();

    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) {
            // 經過本身的 MeasureSpec 測量child
            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());
            // 若是 child 是 match_parent 可是 本身又不是一個精確值,那就要從新再次測量
            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());
    }
    // 經過上面的步驟,拿到了最大的寬高值,調用 setMeasuredDimension 肯定本身的size
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));
    // 最後,以前有指定 match_parent 的 child 須要根據最新的寬高值進行再次測量
    count = mMatchParentChildren.size();
    if (count > 1) {
        for (int i = 0; i < count; i++) {
            final View child = mMatchParentChildren.get(i);
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

            final int childWidthMeasureSpec;
            // 肯定寬度
            if (lp.width == LayoutParams.MATCH_PARENT) {
                final int width = Math.max(0, getMeasuredWidth()
                        - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                        - lp.leftMargin - lp.rightMargin);
                // match_parent 的狀態更改成 精確值
                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                        width, MeasureSpec.EXACTLY);
            } else {
                // 其餘狀況 getChildMeasureSpec() 從新肯定 MeasureSpec
                childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                        lp.leftMargin + lp.rightMargin,
                        lp.width);
            }
            // 肯定高度代碼同上,省略
            ...

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

在第一次測量 child 時,調用了 measureChildWithMargins() 方法,該方法中,最後會調用 getChildMeasureSpec() 方法,在第二次確認寬高時,也是經過這個方法肯定相關的 MeasureSpec 。 能夠看出,**getChildMeasureSpec() 是一個很是重要的靜態方法。**它的做用是根據 parent 的相關參數 和 child 的相關參數,肯定 child 相關的 MeasureSpec 生成。在這裏,三種測量模式和 xml 中的 match_parent wrap_content 或者 具體值 在這裏產生關聯。

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;

    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } 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;
        } 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:
        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);
}
複製代碼

代碼是這樣的,爲便於理解,製做瞭如下這個表格,能夠對號入座。

Parent(pSize)
------
Child(size)
EXACTLY AT_MOST UNSPECIFIED
EXACTLY EXACTLY (size) EXACTLY (size) EXACTLY (size)
MATCH_PARENT EXACTLY (pSize) AT_MOST (pSize) UNSPECIFIED (pSize)
WRAP_CONTENT AT_MOST (pSize) AT_MOST (pSize) UNSPECIFIED (pSize)

經過這個方法,就生成了最後用於測量 child 的相關 MeasureSpec 。接着就能夠調用 child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 讓 child 開始測量本身,最後就會回調到 child 的 onMeasure() 方法中。

上面這個佈局,若是直接 setContentView() 加載的話,那麼在 FrameLayout 中,FrameLayoutMeasureSpecEXACTLY + pSize 這種狀況。

###LayoutParameter 特徵類

上面的寬高信息是從 LayoutParameter 這個類中取出來的。 這個類能夠說是至關重要,沒有它的話,咱們寫的 xml 相關屬性就沒法轉化爲對應的代碼。在這裏繼續拋出一個問題,在 LinearLayout 佈局中咱們能夠直接使用 layout_weight 屬性,可是若是改成 FrameLayout 以後,這個屬性就會沒效果;同時,FrameLayout 中定義的 gravity 屬性,在 LinearLayout 中也沒有效果。爲何呢?代碼層面到底實現的呢?

這就是 LayoutParams 的做用,LayoutParameter 定義在 ViewGroup 中,是最頂級,它有不少子類,第一個子類就是 MarginLayoutParams ,其餘具體實現跟着具體的 ViewGroup ,好比說 FrameLayout.LayoutParameter LinearLayout.LayoutParameter 或者 RecyclerView.LayoutParameter

ViewGroup 中定義了生成 LayoutParams 的方法 generateLayoutParams(AttributeSet attrs)

// ViewGroup
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}

//ViewGroup.LayoutParams
public LayoutParams(Context c, AttributeSet attrs) {
    TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
    setBaseAttributes(a,
            R.styleable.ViewGroup_Layout_layout_width,
            R.styleable.ViewGroup_Layout_layout_height);
    a.recycle();
}

//ViewGroup.LayoutParams
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
    width = a.getLayoutDimension(widthAttr, "layout_width");
    height = a.getLayoutDimension(heightAttr, "layout_height");
}
複製代碼

經過上面的代碼,全部的 ViewGroup 都有 generateLayoutParams() 的能力。在默認的 ViewGroup 中,它只關心最基礎的寬高兩個參數。接着對比 FrameLayoutLinearLayout, 看看相關方法。

//FrameLayout.LayoutParams
public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) {
    super(c, attrs);

    final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FrameLayout_Layout);
    gravity = a.getInt(R.styleable.FrameLayout_Layout_layout_gravity, UNSPECIFIED_GRAVITY);
    a.recycle();
}
//LinearLayout.LayoutParams
public LayoutParams(Context c, AttributeSet attrs) {
    super(c, attrs);
    TypedArray a =
            c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);

    weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
    gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);

    a.recycle();
}
複製代碼

能夠看到,在 FrameLayout 中 額外解析了 gravity ,在 LinearLayout 中 額外解析了 weightgravity

視圖異常緣由

回到上篇文章 View 的建立過程當中的 Layoutinflater.inflate() 方法。

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        ...
        try {
            ...
                // Temp is the root view that was found in the xml
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

                if (root != null) {
                    // Create layout params that match root, if supplied
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        // Set the layout params for temp if we are not
                        // attaching. (If we are, we use addView, below)
                        temp.setLayoutParams(params);
                    }
                }
               ...
            }
    }
}
複製代碼

這裏有一個大坑須要填一下。LayoutInflater.inflate() 方法中,須要咱們指定 parent ,若是不指定,會出現啥狀況呢,就是 LayoutParams 沒有被建立出來。最後在 addView() 方法中:

public void addView(View child, int index) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    // inflate 的時候並無生成相關 LayoutParams
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        // 沒有生成的話,就建立一個 default LayoutParams
        params = generateDefaultLayoutParams();
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}

protected LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
複製代碼

默認的 LayoutParams 只會設置 寬高信息,至於剛剛說的 gravity weight 這些屬性就被丟棄,若是你 inflate() 的頂層佈局真的帶有這些屬性,很差意思,就這樣丟失了。 這也是有人抱怨本身 inflate 佈局時,佈局樣式異常的一個重要緣由。要避免這個問題,就要作到 inflate 時必定要傳入對應的 parent 。不要有 inflate(R.layout.xx,null) 這種寫法,並且這種寫法,目前 Studio 會直接警告你。

inflate 的場景其實不太多,在 Fragment 或者 建立 ViewHolder 時,系統都會將對應的 parent 傳給你,這個好解決。可是在使用 WindowManager.addView() PopupWindow Dialog 時,可能很差找到對應的 parent。這時候咋辦呢?這個時候能夠拿 window.decorView 或者,你直接 new 一個具體的 ViewGroup 都行。

到這裏,關於 LayoutParams 彷佛就說完了。 inflate() 這個大 bug 彷佛也解決了。

Dialog 視圖異常

建立過 Dialog 或者 DialogFragment 的小夥伴都清楚,Dialog 佈局中,你寫 match_parent 是沒有效果,結果老是 wrap_content 的樣子。經過上面一波分析,一開始我覺得是 inflate 那個錯誤,而後,即便我指定上對應的 parent ,想固然覺得佈局能夠符合預期。結果仍是老樣子。

爲何會這樣呢?

這又要回到剛剛上面 getChildMeasureSpec() 方法和表格中。咱們每次寫 match_parent 時,默認 parent 是什麼 size 呢?固然想固然就是屏幕寬高那種 size。 在 Dialog 中,會建立對應的 PhoneWindowPhoneWindow 中有對應的 DecorViewDecorView 並非直接添加咱們佈局的根 View,這裏還有一個 mContentParent ,這纔是展示咱們添加 View 的直接領導,老爹。在 PhoneWindow 中建立 mContentParent 時,有這麼一個判斷。

protected ViewGroup generateLayout(DecorView decor) {
    if (mIsFloating) {
        setLayout(WRAP_CONTENT, WRAP_CONTENT);
        setFlags(0, flagsToUpdate);
    }
}
複製代碼

而咱們使用各類樣式的 Dialog 時,其實會加載默認的 style ,最基本的 dialog style 中,分明寫了這麼一個默認屬性。

<style name="Base.V7.Theme.AppCompat.Light.Dialog" parent="Base.Theme.AppCompat.Light">
    ...
    <item name="android:windowIsFloating">true</item>
    ...
</style>
複製代碼

這兩個代碼放一塊,問題開始轉化。當 parent(decorView) 爲 精確值,child(mContentParent) 爲 wrap_content 時,最後在 child 中對應的 MeasureSpec 是什麼樣呢? 查上面的表就知道,這個時候的 child measureSpec 應該是 AT_MOST + pSize 。 當 parent (mContentParent) 爲 AT_MOST ,child (填充佈局) 爲 match_parent 時,最後 child 中對應的 MeasureSpec 是什麼樣呢? 繼續查表,顯然,這裏也是 AT_MOST + pSize 這種狀況。注意,這裏就和上面第一次分析的 EXACTLY + pSize 不同了。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/parent_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center"
    android:background="@color/colorAccent"
    android:clipChildren="false">

    <ProgressBar
        android:id="@+id/progressbar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:indeterminate="true"
        android:indeterminateTint="@color/colorPrimary"
        android:indeterminateTintMode="src_in"/>

</FrameLayout>
複製代碼

假設在 Dialog 中咱們就填充如上佈局。結合上面 FrameLayout 分析, child 的 size 要再次測量。關鍵在 FrameLayout onMeasure() 方法最後的 setMeasuredDimension()方法中會調用 resolveSizeAndState()

setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
        resolveSizeAndState(maxHeight, heightMeasureSpec,
                childState << MEASURED_HEIGHT_STATE_SHIFT));

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}
複製代碼

第一次是 EXACTLY ,因此就是 pSize 。 這一次是 AT_MOST ,因此就成了 childSize 。那最後效果其實就是 wrap_content 。到這裏 Dialog 顯示異常從代碼上分析完成。那麼須要怎麼解決呢? 首先能夠從根源上,將 windowIsFloating 設置爲 false 。

//styles.xml
<style name="AppTheme.AppCompat.Dialog.Alert.NoFloating" parent="Theme.AppCompat.Light.Dialog.Alert">
    <item name="android:windowIsFloating">false</item>
</style>
//DialogFragment
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setStyle(android.support.v4.app.DialogFragment.STYLE_NO_TITLE, R.style.AppTheme_AppCompat_Dialog_Alert)
}
複製代碼

退而求其次,既然它默認設置爲 wrap_content ,那麼咱們能夠直接設置回來啊。

//DialogFragment
override fun onStart() {
    super.onStart()
    dialog?.window?.setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
}
複製代碼

到這裏,咱們也能回答一個問題,若是 parent 指定爲 wrap_content 。child 指定爲 match_parent 那麼最後,child 到底有多大? 這個其實就是上面這個問題,若是要回答得簡單,那麼就是它就是 View 本身的 最小值。

要詳細說的話,若是 View 沒有複寫 onMeasure() 方法,那就是默認 onMeasure() 方法中 getDefaultSize() 的返回值,就是 pSize 。

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;
}
複製代碼

若是是其餘控件,好比說剛剛說的 ProgressBar,其實就是 resolveSizeAndState() 或者測量出來的最小值。 咱們自定義 View 時視圖預覽發現它總會填充父佈局,緣由就是你沒有複寫 onMeasure() 方法。還有就是在寫佈局時,儘可能避免 parent 是 wrap_content , child 又是 match_parent 的狀況,這樣 parent 會重複測量,形成沒必要要的開銷。

總結

View 的測量是一個博弈的過程,最核心方法就是 setMeasuredDimension(),具體值則須要 parent 和 child 相互協商。數值的傳遞和肯定依賴於 MeasureSpecLayoutParams,填充佈局時 inflate() 方法 root 參數不要給空,這樣會致使填充佈局一些參數丟失,Dialog 老是 wrap_content ,這是由於默認帶有 windowIsFloating 的屬性 。

相關文章
相關標籤/搜索