Android 源碼分析二 View 測量

第一篇說完 View 創(chuàng)建组橄,接著講講 View 的測量和布局荞膘。先講講整體思想,View 的 測量是自上而下晨炕,一層一層進(jìn)行衫画。涉及到的核心方法就是 View 中的 measure() layout() 對于我們來說,更應(yīng)該關(guān)心的就是 onMeasure()onLayout() 的回調(diào)方法瓮栗。本文著重關(guān)注測量相關(guān)代碼削罩,至于 layout 瞄勾,這個是 ViewGroup 的具體邏輯。

onMeasure

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

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

這是 ViewonMeasure() 方法默認(rèn)實現(xiàn),這里又涉及到三個重要的方法, setMeasuredDimension()getDefaultSize() 微服。
setMeasuredDimension() 這個方法非常重要趾疚,它是我們設(shè)置測量寬高值的官方唯一指定方法。也是我們在 onMeasure() 方法中必須調(diào)用的方法以蕴。如果你想了下糙麦,自己似乎在 onMeasre() 沒有手動調(diào)用過該方法,并且也沒有啥異常丛肮,不要猶豫赡磅,你一定是調(diào)用了 super.onMeasure() ,setMeasuredDimension()最終會完成對 measureHeightmeasureWidth 賦值,具體操作往下看宝与。

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

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

setMeasuredDimension() 中調(diào)用私有的 setMeasuredDimensionRaw() 方法完成對 mMeasuredWidthmMeasuredHeight 賦值焚廊,然后更新 flag 。

getSuggestedMinimumWidth/Height

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

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

}

這兩個方法的默認(rèn)實現(xiàn)就是去獲取 View 設(shè)置的背景和最小值中最小的那個习劫。背景設(shè)置就不用說了咆瘟,至于這個寬高最小值,其實就是通過 xml 中 minWidth 或者 API 動態(tài)設(shè)置诽里。

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;
}

這個方法也比較重要袒餐,因為它涉及到測量模式。先分析下參數(shù)谤狡,輸入的第一個 size 是剛剛獲取的最小值匿乃。第二個就是父布局回調(diào)過來的測量參數(shù)。

通過上面可以看到豌汇,測量模式一共有三種幢炸。MeasureSpec.UNSPECIFIED MeasureSpec.AT_MOST MeasureSpec.EXACTLY

如果是 MeasureSpec.UNSPECIFIED ,那么就直接使用獲取的最小值拒贱。如果是其他兩種模式宛徊,那么就從測量參數(shù)中獲取對應(yīng)的 size。注意逻澳,在這個方法中闸天,根本沒有對 AT_MOST 和 EXACTLY 做區(qū)分處理。

MeasureSpec 測量模式和size

通過上面 getDefaultSize() 方法我們已經(jīng)看到 MeasureSpec 中包含有測量模式和對應(yīng) size斜做。那么它是怎么做到一個 int 類型苞氮,表示兩種信息呢?程序員的小巧思上線瓤逼。

一個 int 類型笼吟,32位库物。這里就是使用了高2位來表示測量模式,低 30 位用來記錄 size贷帮。

//左移常量 shift 有轉(zhuǎn)變的意思 而且在 Kotlin 中 左移使用 shl() 表示
private static final int MODE_SHIFT = 30;
//二進(jìn)制就是這樣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);
}

看著是不是比較高大上?都說了這是程序員小巧思撵枢,代碼當(dāng)然比較溜民晒。這里涉及到與或非三種運算。直接舉個例子吧锄禽,比如我要創(chuàng)建一個 size 為 16 模式是 EXACTLY 的 MeasureSpec 那么就是這樣的潜必。

    size    對應(yīng)   00 000... 1111
    mode    對應(yīng)   01 000... 0000
    mask    對應(yīng)   11 000... 0000
    ~mask   對應(yīng)   00 111... 1111
    size & ~mask  00 000... 1111 = size
    mode & mask   01 000... 0000 = mode
    size | mode   01 000... 1111 = 最終結(jié)果

通過這么一對應(yīng),結(jié)果非常清晰沃但,有沒有覺得 makeMeasureSpec() 方法中前兩次 & 操作都是很無效的刮便?其實它能保證 mode 和 size 不越界,不會互相污染绽慈。反正你也別瞎傳值。賦值時辈毯,方法上已經(jīng)對兩個參數(shù)都有輸入限制坝疼。

再說完三種模式定義之后,接著就需要考慮 xml 中的 寬高指定最后是怎么轉(zhuǎn)換為對應(yīng)的 模式谆沃。比如說钝凶,我們寫 wrap_content, 那么對應(yīng)的測量模式到底是怎樣的呢唁影?

舉個例子耕陷,比如說如下的一個布局。

<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>

效果通過預(yù)覽就能看到据沈,FrameLayout 占據(jù)全屏哟沫,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 復(fù)寫了 View 的 onMeasure() 方法,并且沒有調(diào)用 super 孔祸。所以隆敢,最上面那一套分析對于它無效。因此崔慧,它也自己在最后調(diào)用了 setMeasuredDimension() 方法完成一次測量拂蝎。在這里,又涉及到一個 View 的靜態(tài)方法 -- 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);
}

入?yún)?size 是背景大小惶室,MeasureSpeconMeasure() 方法傳入温自,參數(shù)由 parent 指定玄货。 state 的問題先不考慮,我們這里主要看 size 捣作。對比剛剛說過的 getDefaultSize() , 這個方法已經(jīng)將 AT_MOSTEXACTLY 做了區(qū)分處理誉结,一共又四種情況。

AT_MOST 下券躁,如果測量值小于背景大小惩坑,即 View 需要的 size 比 parent 能給的最大值還要大。這個時候還是設(shè)置為 測量值也拜,并且加入了 MEASURED_STATE_TOO_SMALL 這個狀態(tài)以舒。如果測量值大于背景大小,正常情況也就是這樣慢哈,這時候就設(shè)置為背景大小蔓钟。EXACTLY 下,那就是測量值卵贱。UNSPECIFIED 下滥沫,就是背景 size。

數(shù)值傳遞

上面其實都是說的是 ViewonMeasure 中測量自己的情況键俱,但是兰绣,parent 傳入的 MeasureSpec 參數(shù)到底是怎么確認(rèn)的呢?child 設(shè)置 match_parent 或者 wrap_content 或者 精確值编振,會影響對應(yīng)的 MeasureSpec 的模式和 size 嗎缀辩?

帶著這些問題,我們看看 FrameLayoutonMeasure() 方法的部分實現(xiàn)踪央。

@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);
            // 狀態(tài)相關(guān) 先不考慮
            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());
    }
    // 通過上面的步驟畅蹂,拿到了最大的寬高值健无,調(diào)用 setMeasuredDimension 確定自己的size
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));
    // 最后,之前有指定 match_parent 的 child 需要根據(jù)最新的寬高值進(jìn)行再次測量
    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 的狀態(tài)更改為 精確值
                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 時睬涧,調(diào)用了 measureChildWithMargins() 方法,該方法中旗唁,最后會調(diào)用 getChildMeasureSpec() 方法畦浓,在第二次確認(rèn)寬高時,也是通過這個方法確定相關(guān)的 MeasureSpec 检疫。 可以看出讶请,getChildMeasureSpec() 是一個非常重要的靜態(tài)方法。它的作用是根據(jù) parent 的相關(guān)參數(shù) 和 child 的相關(guān)參數(shù),確定 child 相關(guān)的 MeasureSpec 生成夺溢。在這里论巍,三種測量模式和 xml 中的 match_parent wrap_content 或者 具體值 在這里產(chǎn)生關(guān)聯(lián)。

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 的相關(guān) MeasureSpec 。接著就可以調(diào)用 child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 讓 child 開始測量自己持搜,最后就會回調(diào)到 child 的 onMeasure() 方法中密似。

上面這個布局,如果直接 setContentView() 加載的話葫盼,那么在 FrameLayout 中残腌,FrameLayoutMeasureSpecEXACTLY + pSize 這種情況。

LayoutParameter 特征類

上面的寬高信息是從 LayoutParameter 這個類中取出來的贫导。 這個類可以說是相當(dāng)重要抛猫,沒有它的話,我們寫的 xml 相關(guān)屬性就無法轉(zhuǎn)化為對應(yīng)的代碼孩灯。在這里繼續(xù)拋出一個問題闺金,在 LinearLayout 布局中我們可以直接使用 layout_weight 屬性,但是如果改為 FrameLayout 之后钱反,這個屬性就會沒效果;同時匣距,FrameLayout 中定義的 gravity 屬性面哥,在 LinearLayout 中也沒有效果。為什么呢毅待?代碼層面到底實現(xiàn)的呢尚卫?

這就是 LayoutParams 的作用,LayoutParameter 定義在 ViewGroup 中尸红,是最頂級吱涉,它有很多子類,第一個子類就是 MarginLayoutParams ,其他具體實現(xiàn)跟著具體的 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() 的能力。在默認(rèn)的 ViewGroup 中盅蝗,它只關(guān)心最基礎(chǔ)的寬高兩個參數(shù)鳖链。接著對比 FrameLayoutLinearLayout, 看看相關(guān)方法墩莫。

//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 的創(chuàng)建過程中的 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 侧啼,如果不指定牛柒,會出現(xiàn)啥情況呢,就是 LayoutParams 沒有被創(chuàng)建出來慨菱。最后在 addView() 方法中:

public void addView(View child, int index) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    // inflate 的時候并沒有生成相關(guān) LayoutParams
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        // 沒有生成的話焰络,就創(chuàng)建一個 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);
}

默認(rèn)的 LayoutParams 只會設(shè)置 寬高信息,至于剛剛說的 gravity weight 這些屬性就被丟棄符喝,如果你 inflate() 的頂層布局真的帶有這些屬性闪彼,不好意思,就這樣丟失了协饲。
這也是有人抱怨自己 inflate 布局時畏腕,布局樣式異常的一個重要原因。要避免這個問題茉稠,就要做到 inflate 時一定要傳入對應(yīng)的 parent 描馅。不要有 inflate(R.layout.xx,null) 這種寫法,而且這種寫法而线,目前 Studio 會直接警告你铭污。

inflate 的場景其實不太多,在 Fragment 或者 創(chuàng)建 ViewHolder 時膀篮,系統(tǒng)都會將對應(yīng)的 parent 傳給你嘹狞,這個好解決。但是在使用 WindowManager.addView() PopupWindow Dialog 時誓竿,可能不好找到對應(yīng)的 parent磅网。這時候咋辦呢?這個時候可以拿 window.decorView 或者筷屡,你直接 new 一個具體的 ViewGroup 都行涧偷。

到這里,關(guān)于 LayoutParams 似乎就說完了毙死。 inflate() 這個大 bug 似乎也解決了燎潮。

Dialog 視圖異常

創(chuàng)建過 Dialog 或者 DialogFragment 的小伙伴都清楚,Dialog 布局中扼倘,你寫 match_parent 是沒有效果跟啤,結(jié)果總是 wrap_content 的樣子。通過上面一波分析,一開始我以為是 inflate 那個錯誤隅肥,然后竿奏,即使我指定上對應(yīng)的 parent ,想當(dāng)然以為布局可以符合預(yù)期腥放。結(jié)果還是老樣子泛啸。

為什么會這樣呢?

這又要回到剛剛上面 getChildMeasureSpec() 方法和表格中秃症。我們每次寫 match_parent 時候址,默認(rèn) parent 是什么 size 呢?當(dāng)然想當(dāng)然就是屏幕寬高那種 size种柑。
Dialog 中岗仑,會創(chuàng)建對應(yīng)的 PhoneWindowPhoneWindow 中有對應(yīng)的 DecorView 聚请,DecorView 并不是直接添加我們布局的根 View荠雕,這里還有一個 mContentParent ,這才是展現(xiàn)我們添加 View 的直接領(lǐng)導(dǎo),老爹。在 PhoneWindow 中創(chuàng)建 mContentParent 時证芭,有這么一個判斷。

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

而我們使用各種樣式的 Dialog 時盖文,其實會加載默認(rèn)的 style ,最基本的 dialog style 中蚯姆,分明寫了這么一個默認(rèn)屬性五续。

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

這兩個代碼放一塊,問題開始轉(zhuǎn)化龄恋。當(dāng) parent(decorView) 為 精確值疙驾,child(mContentParent) 為 wrap_content 時,最后在 child 中對應(yīng)的 MeasureSpec 是什么樣呢篙挽?
查上面的表就知道荆萤,這個時候的 child measureSpec 應(yīng)該是 AT_MOST + pSize 镊靴。
當(dāng) parent (mContentParent) 為 AT_MOST ,child (填充布局) 為 match_parent 時铣卡,最后 child 中對應(yīng)的 MeasureSpec 是什么樣呢?
繼續(xù)查表偏竟,顯然煮落,這里也是 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>

假設(shè)在 Dialog 中我們就填充如上布局。結(jié)合上面 FrameLayout 分析, child 的 size 要再次測量。關(guān)鍵在 FrameLayout onMeasure() 方法最后的 setMeasuredDimension()方法中會調(diào)用 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 設(shè)置為 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)
}

退而求其次底洗,既然它默認(rèn)設(shè)置為 wrap_content 腋么,那么我們可以直接設(shè)置回來啊。

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

到這里亥揖,我們也能回答一個問題珊擂,如果 parent 指定為 wrap_content 。child 指定為 match_parent 那么最后徐块,child 到底有多大未玻?
這個其實就是上面這個問題,如果要回答得簡單胡控,那么就是它就是 View 自己的 最小值扳剿。

要詳細(xì)說的話,如果 View 沒有復(fù)寫 onMeasure() 方法昼激,那就是默認(rèn) 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 時視圖預(yù)覽發(fā)現(xiàn)它總會填充父布局,原因就是你沒有復(fù)寫 onMeasure() 方法凡傅。還有就是在寫布局時辟狈,盡量避免 parent 是 wrap_content , child 又是 match_parent 的情況夏跷,這樣 parent 會重復(fù)測量哼转,造成不必要的開銷。

總結(jié)

View 的測量是一個博弈的過程槽华,最核心方法就是 setMeasuredDimension(),具體值則需要 parent 和 child 相互協(xié)商壹蔓。數(shù)值的傳遞和確定依賴于 MeasureSpecLayoutParams,填充布局時 inflate() 方法 root 參數(shù)不要給空猫态,這樣會導(dǎo)致填充布局一些參數(shù)丟失佣蓉,Dialog 總是 wrap_content 披摄,這是因為默認(rèn)帶有 windowIsFloating 的屬性 。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末勇凭,一起剝皮案震驚了整個濱河市疚膊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌虾标,老刑警劉巖酿联,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異夺巩,居然都是意外死亡贞让,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門柳譬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來喳张,“玉大人,你說我怎么就攤上這事美澳∠浚” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵制跟,是天一觀的道長舅桩。 經(jīng)常有香客問我,道長雨膨,這世上最難降的妖魔是什么擂涛? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮聊记,結(jié)果婚禮上撒妈,老公的妹妹穿的比我還像新娘。我一直安慰自己排监,他們只是感情好狰右,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著舆床,像睡著了一般棋蚌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上挨队,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天谷暮,我揣著相機與錄音,去河邊找鬼瞒瘸。 笑死坷备,一個胖子當(dāng)著我的面吹牛熄浓,可吹牛的內(nèi)容都是我干的情臭。 我是一名探鬼主播省撑,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼俯在!你這毒婦竟也來了竟秫?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤跷乐,失蹤者是張志新(化名)和其女友劉穎肥败,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體愕提,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡馒稍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了浅侨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片纽谒。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖如输,靈堂內(nèi)的尸體忽然破棺而出鼓黔,到底是詐尸還是另有隱情,我是刑警寧澤不见,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布澳化,位于F島的核電站,受9級特大地震影響稳吮,放射性物質(zhì)發(fā)生泄漏缎谷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一灶似、第九天 我趴在偏房一處隱蔽的房頂上張望慎陵。 院中可真熱鬧,春花似錦喻奥、人聲如沸席纽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽润梯。三九已至,卻和暖如春甥厦,著一層夾襖步出監(jiān)牢的瞬間纺铭,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工刀疙, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留舶赔,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓谦秧,卻偏偏與公主長得像竟纳,于是被迫代替她去往敵國和親撵溃。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容