前面的文章中着重講解了 View 的測量流程。其中我提到了一句非常重要的話:View 的測量匡高是由父控件的 MeasureSpec 和 View 自身的 `LayoutParams 共同決定的。我們在前面的 每日一問:談談對 MeasureSpec 的理解 把 MeasureSpec 的重點進行了講解,其實另外一個 LayoutParams 同樣是非常非常重要。
從概念講起
LayoutParams,顧名思義,就是布局參數。而且大多數人對此都是司空見慣,我們 XML 文件里面的每一個 View 都會接觸到 layout_xxx 這樣的屬性,這實際上就是對布局參數的描述。大概大家也就清楚了,layout_ 這樣開頭的東西都不屬於 View,而是控制具體顯示在哪里。
LayoutParams 都有哪些初始化方法
通常來說,我們都會把我們的控件放在 XML 文件中,即使我們有時候需要對屏幕做比較「取巧」的適配,會直接通過 View.getLayoutParams() 這樣的方法獲取 LayoutParams 的實例,但我們接觸的少並不代表它的初始化方法不重要。
實際上,用代碼寫出來的 View 加載效率要比在 XML 中加載快上大約 1 倍。只是在如今手機配置都比較高的情況下,我們常常忽略了這種方式。
我們來看看 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();
}
public LayoutParams(int width, int height) {
this.width = width;
this.height = height;
}
public LayoutParams(LayoutParams source) {
this.width = source.width;
this.height = source.height;
}
LayoutParams() { }
MarginLayoutParams
除去最后一個放給 MarginLayoutParams 做處理的方法外,我們在 ViewGroup 中還有 3 個構造方法。他們分別負責給 XML 處理、直接讓用戶指定寬高、還有類似集合的 addAll() 這樣的方式的賦值方法。
實際上,ViewGroup 的子類的 LayoutParams 類擁有更多的構造方法,感興趣的自己翻閱源碼查看。在這里我想更加強調一下我上面提到的 MarginLayoutParams。
MarginLayoutParams 繼承於 ViewGroup.LayoutParams。
public static class MarginLayoutParams extends ViewGroup.LayoutParams {
@ViewDebug.ExportedProperty(category = "layout")
public int leftMargin;
@ViewDebug.ExportedProperty(category = "layout")
public int topMargin;
@ViewDebug.ExportedProperty(category = "layout")
public int rightMargin;
@ViewDebug.ExportedProperty(category = "layout")
public int bottomMargin;
@ViewDebug.ExportedProperty(category = "layout")
private int startMargin = DEFAULT_MARGIN_RELATIVE;
@ViewDebug.ExportedProperty(category = "layout")
private int endMargin = DEFAULT_MARGIN_RELATIVE;
public MarginLayoutParams(Context c, AttributeSet attrs) {
super();
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
setBaseAttributes(a,
R.styleable.ViewGroup_MarginLayout_layout_width,
R.styleable.ViewGroup_MarginLayout_layout_height);
int margin = a.getDimensionPixelSize(
com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
if (margin >= 0) {
leftMargin = margin;
topMargin = margin;
rightMargin= margin;
bottomMargin = margin;
} else {
int horizontalMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginHorizontal, -1);
// ... something
}
// ... something
}
}
一看代碼,自然就清楚了,為什么我們以前會發現在 XML 布局里, layout_margin 屬性的值會覆蓋 layout_marginLeft 與 layout_marginRight 等屬性的值。
實際上,事實上,絕大部分容器控件都是直接繼承
ViewGroup.MarginLayoutParams而非ViewGroup.LayoutParams。所以我們再自定義LayoutParams的時候記得繼承ViewGroup.MarginLayoutParams。
在代碼里面使用 LayoutParams
前面介紹了 LayoutParams 的幾種構造方法,我們下面以 LinearLayout.LayoutParams 來看看幾種簡單的使用方式。
val textView1 = TextView(this)
textView1.text = "不指定 LayoutParams"
layout.addView(textView1)
val textView2 = TextView(this)
textView2.text = "手動指定 LayoutParams"
textView2.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
layout.addView(textView2)
val textView3 = TextView(this)
textView3.text = "手動傳遞 LayoutParams"
textView3.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams(100, 100))
layout.addView(textView3)
我們看看 addView() 都做了什么。
public void addView(View child) {
addView(child, -1);
}
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
if (mOrientation == HORIZONTAL) {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
} else if (mOrientation == VERTICAL) {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
return null;
}
public void addView(View child, int index, LayoutParams params) {
if (DBG) {
System.out.println(this + " addView");
}
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
// ...
if (!checkLayoutParams(params)) {
params = generateLayoutParams(params);
}
// ...
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LinearLayout.LayoutParams;
}
看起來 ViewGroup 真是煞費苦心,如果我們沒有給 View 設置 LayoutParams,則系統會幫我們根據 orientation 設置默認的 LayoutParams。甚至是我們即使在 addView() 之前設置了錯誤的 LayoutParams 值,系統也會我們幫我們進行糾正。
雖然系統已經做的足夠完善,幫我們各種矯正錯誤,但在
addView()之后,我們還強行設置錯誤的LayoutParams,那還是一定會報ClassCastException的。

LayoutParams 很重要,每一名 Android 開發都應該盡力地去掌握,只有弄清楚了系統的編寫方式,應對上面類似簡書的流式布局才能更好處理。
實際上 Google 出的 FlexboxLayout 已經做的相當完美。
當然如果使用的RecyclerView,還可以自己寫一個FlowLayoutManager進行處理。
原文較多地參考自:https://blog.csdn.net/yisizhu/article/details/51582622
