第一篇說完 View 創建,接著講講 View
的測量和布局。先講講整體思想,View
的 測量是自上而下,一層一層進行。涉及到的核心方法就是 View
中的 measure()
layout()
對于我們來說,更應該關心的就是 onMeasure()
和 onLayout()
的回調方法。本文著重關注測量相關代碼,至于 layout ,這個是 ViewGroup
的具體邏輯。
onMeasure
說到 onMeasure()
方法就必須提一嘴它涉及到的測量模式。以及模式對子 view
的約束。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
這是 View
中 onMeasure()
方法默認實現,這里又涉及到三個重要的方法, setMeasuredDimension()
和 getDefaultSize()
。
setMeasuredDimension()
這個方法非常重要,它是我們設置測量寬高值的官方唯一指定方法。也是我們在 onMeasure()
方法中必須調用的方法。如果你想了下,自己似乎在 onMeasre()
沒有手動調用過該方法,并且也沒有啥異常,不要猶豫,你一定是調用了 super.onMeasure()
,setMeasuredDimension()
最終會完成對 measureHeight
和 measureWidth
賦值,具體操作往下看。
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
setMeasuredDimension()
中調用私有的 setMeasuredDimensionRaw()
方法完成對 mMeasuredWidth
和 mMeasuredHeight
賦值,然后更新 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());
}
這兩個方法的默認實現就是去獲取 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
是剛剛獲取的最小值。第二個就是父布局回調過來的測量參數。
通過上面可以看到,測量模式一共有三種。MeasureSpec.UNSPECIFIED
MeasureSpec.AT_MOST
MeasureSpec.EXACTLY
如果是 MeasureSpec.UNSPECIFIED
,那么就直接使用獲取的最小值。如果是其他兩種模式,那么就從測量參數中獲取對應的 size。注意,在這個方法中,根本沒有對 AT_MOST 和 EXACTLY 做區分處理。
MeasureSpec 測量模式和size
通過上面 getDefaultSize()
方法我們已經看到 MeasureSpec
中包含有測量模式和對應 size。那么它是怎么做到一個 int 類型,表示兩種信息呢?程序員的小巧思上線。
一個 int 類型,32位。這里就是使用了高2位來表示測量模式,低 30 位用來記錄 size。
//左移常量 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 。
ProgressBar
的 onMeasure()
方法如下:
@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 是背景大小,MeasureSpec
是 onMeasure()
方法傳入,參數由 parent 指定。 state 的問題先不考慮,我們這里主要看 size 。對比剛剛說過的 getDefaultSize()
, 這個方法已經將 AT_MOST
和 EXACTLY
做了區分處理,一共又四種情況。
AT_MOST
下,如果測量值小于背景大小,即 View
需要的 size 比 parent 能給的最大值還要大。這個時候還是設置為 測量值,并且加入了 MEASURED_STATE_TOO_SMALL
這個狀態。如果測量值大于背景大小,正常情況也就是這樣,這時候就設置為背景大小。EXACTLY
下,那就是測量值。UNSPECIFIED
下,就是背景 size。
數值傳遞
上面其實都是說的是 View
在 onMeasure
中測量自己的情況,但是,parent 傳入的 MeasureSpec
參數到底是怎么確認的呢?child 設置 match_parent
或者 wrap_content
或者 精確值,會影響對應的 MeasureSpec
的模式和 size 嗎?
帶著這些問題,我們看看 FrameLayout
中 onMeasure()
方法的部分實現。
@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
中,FrameLayout
的 MeasureSpec
是 EXACTLY
+ 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
中,它只關心最基礎的寬高兩個參數。接著對比 FrameLayout
和 LinearLayout
, 看看相關方法。
//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
中 額外解析了 weight
和 gravity
。
視圖異常原因
回到上篇文章 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
中,會創建對應的 PhoneWindow
,PhoneWindow
中有對應的 DecorView
,DecorView
并不是直接添加我們布局的根 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 相互協商。數值的傳遞和確定依賴于 MeasureSpec
和 LayoutParams
,填充布局時 inflate() 方法 root 參數不要給空,這樣會導致填充布局一些參數丟失,Dialog 總是 wrap_content ,這是因為默認帶有 windowIsFloating
的屬性 。