提示:本文的源碼均取自Android 7.0
前言
在平時的開發過程中,我們一般是通過XML文件去定義佈局,所以對於LayoutParams的使用可能相對較少。但是在需要動態改變View的佈局參數(比如寬度、位置)時,就必須要藉助這個重要的類了。本文將結合具體源碼詳細講解LayoutParams的相關知識。
基礎知識
LayoutParams是什麼
LayoutParams翻譯過來就是佈局參數,子View通過LayoutParams告訴父容器(ViewGroup)應該如何放置自己。從這個定義中也可以看出來LayoutParams與ViewGroup是息息相關的,因此脫離ViewGroup談LayoutParams是沒有意義的。
事實上,每個ViewGroup的子類都有自己對應的LayoutParams類,典型的如LinearLayout.LayoutParams和FrameLayout.LayoutParams等,可以看出來LayoutParams都是對應ViewGroup子類的內部類。
最基礎的LayoutParams是定義在ViewGroup中的靜態內部類,封裝着View的寬度和高度信息,對應着XML中的layout_width
和layout_height
屬性。主要源碼如下:
public static class LayoutParams {
public static final int FILL_PARENT = -1;
public static final int MATCH_PARENT = -1;
public static final int WRAP_CONTENT = -2;
public int width;
public int height;
......
/**
* XML文件中設置的以layout_開頭的屬性將在這個方法中解析
*/
public LayoutParams(Context c, AttributeSet attrs) {
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
// 解析width和height屬性
setBaseAttributes(a,
R.styleable.ViewGroup_Layout_layout_width,
R.styleable.ViewGroup_Layout_layout_height);
a.recycle();
}
/**
* 使用傳入的width和height構建LayoutParams
*/
public LayoutParams(int width, int height) {
this.width = width;
this.height = height;
}
/**
* 通過傳入的LayoutParams構建新的LayoutParams
*/
public LayoutParams(LayoutParams source) {
this.width = source.width;
this.height = source.height;
}
......
}
弄清楚了LayoutParams的意義,就可以解釋爲什麼在XML中View的某些屬性是以layout_
開頭的了。因爲這些屬性並不直接屬於View,而是屬於這些View的LayoutParams,這樣的命名方式也就顯得很貼切了。
MarginLayoutParams
在ViewGroup中還定義一個LayoutParams的子類——MarginLayoutParams。從名字就可以猜出來,MarginLayoutParams是和外間距有關的。事實也確實如此,和LayoutParams相比,MarginLayoutParams只是增加了對上下左右外間距的支持。實際上大部分LayoutParams的實現類都是繼承自MarginLayoutParams,因爲基本所有的父容器都是支持子View設置外間距的。MarginLayoutParams的主要源碼如下:
public static class MarginLayoutParams extends ViewGroup.LayoutParams {
/**
* The left margin in pixels of the child. Margin values should be positive.
* Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value
* to this field.
*/
public int leftMargin;
public int topMargin;
public int rightMargin;
public int bottomMargin;
/**
* 解析XML中以layout_開頭的屬性
*/
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);
int verticalMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginVertical, -1);
if (horizontalMargin >= 0) {
leftMargin = horizontalMargin;
rightMargin = horizontalMargin;
} else {
leftMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
UNDEFINED_MARGIN);
rightMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginRight,
UNDEFINED_MARGIN);
}
.........
a.recycle();
}
}
從源碼中也可以看到,MarginLayoutParams主要就是增加了上下左右4種外間距。在構造方法中,先是獲取了margin屬性;如果該值不合法,就獲取horizontalMargin;如果該值不合法,再去獲取leftMargin和rightMargin屬性(verticalMargin、topMargin和bottomMargin同理)。我們可以據此總結出這幾種屬性的優先級:
margin > horizontalMargin和verticalMargin > leftMargin和RightMargin、topMargin和bottomMargin
優先級更高的屬性會覆蓋掉優先級較低的屬性。此外,還要注意一下這幾種屬性上的註釋:
Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value
也就是說,如果我們更改了MarginLayoutParams中這幾種屬性的值,就應該調用View的setLayoutParams
方法重新設置更改後的MarginLayoutParams,這樣我們所做的更改纔會生效(其實主要是因爲在setLayoutParams方法中調用了requestLayout
方法)。
LayoutParams與View如何建立聯繫
說了這麼多LayoutParams的作用,這裏再簡單談一下LayoutParams是何時被創建出來的,又是怎樣和View建立聯繫。歸納起來,View的使用方式無非有兩種:在XML中定義View和在Java代碼中直接生成View對應的實例對象,因此我們也分這兩個方向進行探索。
在Java代碼中實例化View
在代碼中實例化View後,如果調用setLayoutParams
方法爲View設置指定的LayoutParams,那麼LayoutParams就已經和View建立起聯繫了。針對不同的ViewGroup子類,我們要選擇合適的LayoutParams。
實例化View後,一般還會調用addView
方法將View對象添加到指定的ViewGroup中。可以想到,在ViewGroup中肯定也會爲還沒有LayoutParams的子View設置合適的LayoutParams,下文將通過分析代碼說明這一過程。ViewGroup實現了以下五種addView方法的重載版本:
/**
* 重載方法1:添加一個子View
* 如果這個子View還沒有LayoutParams,就爲子View設置當前ViewGroup默認的LayoutParams
*/
public void addView(View child) {
addView(child, -1);
}
/**
* 重載方法2:在指定位置添加一個子View
* 如果這個子View還沒有LayoutParams,就爲子View設置當前ViewGroup默認的LayoutParams
* @param index View將在ViewGroup中被添加的位置(-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();// 生成當前ViewGroup默認的LayoutParams
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
/**
* 重載方法3:添加一個子View
* 使用當前ViewGroup默認的LayoutParams,並以傳入參數作爲LayoutParams的width和height
*/
public void addView(View child, int width, int height) {
final LayoutParams params = generateDefaultLayoutParams(); // 生成當前ViewGroup默認的LayoutParams
params.width = width;
params.height = height;
addView(child, -1, params);
}
/**
* 重載方法4:添加一個子View,並使用傳入的LayoutParams
*/
@Override
public void addView(View child, LayoutParams params) {
addView(child, -1, params);
}
/**
* 重載方法4:在指定位置添加一個子View,並使用傳入的LayoutParams
*/
public void addView(View child, int index, LayoutParams params) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
以上代碼已經添加了必要的註釋,這裏就不再贅述了。總之,只要子View沒有LayoutParams,ViewGroup就會爲其設置默認的LayoutParams。默認的LayoutParams對象通過generateDefaultLayoutParams
方法生成,ViewGroup中的代碼實現如下:
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
實際上,addView的前四個重載方法最終都會調用第五個重載版本,即addView(View child, int index, LayoutParams params)。在這個方法中調用了requestLayout
和invalidate
方法,引起視圖重新佈局(onMeasure->onLayout->onDraw)和重繪。這也很好理解,既然我們添加了新的View,那麼原有的視圖結構自然會發生變化。同時,在這個方法中還調用了addViewInner
方法,關鍵代碼如下:
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
.....
if (mTransition != null) {
mTransition.addChild(this, child);
}
if (!checkLayoutParams(params)) { // ① 檢查傳入的LayoutParams是否合法
params = generateLayoutParams(params); // 如果傳入的LayoutParams不合法,將進行轉化操作
}
if (preventRequestLayout) { // ② 是否需要阻止重新執行佈局流程
child.mLayoutParams = params; // 這不會引起子View重新佈局(onMeasure->onLayout->onDraw)
} else {
child.setLayoutParams(params); // 這會引起子View重新佈局(onMeasure->onLayout->onDraw)
}
if (index < 0) {
index = mChildrenCount;
}
addInArray(child, index);
// tell our children
if (preventRequestLayout) {
child.assignParent(this);
} else {
child.mParent = this;
}
.....
}
可以看到,在代碼①的位置先判斷傳入的LayoutParams是否合法,ViewGroup中這個方法只是簡單判斷了傳入的LayoutParams是否爲空:
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p != null;
}
如果LayoutParams不合法,將使用generateLayoutParams
方法對其進行轉化,ViewGroup中這個方法僅僅將傳入的LayoutParams原樣返回:
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return p;
}
最後,在代碼②的位置爲子View設置LayoutParams。這裏分爲了兩種情況:如果不希望引起子View重新佈局(onMeasure->onLayout->onDraw)就直接爲子View的LayoutParams變量賦值;否則調用子View的setLayoutParams方法傳入LayoutParams。
到這一步,LayoutParams和View的聯繫就建立起來了。
在XML中定義View
在XML中定義的View首先會被解析爲對應的實例化對象,這項工作將通過LayoutInflater
的inflate
方法完成。inflater方法有多個重載版本,最終將會調用inflate(XmlPullParser parser,ViewGroup root, boolean attachToRoot),關鍵代碼如下:
/**
* 解析XML文件中的View
* @param parser 解析器
* @param root 父容器(可能爲null)
* @param attachToRoot View是否需要附加到父容器中
*/
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
......
View result = root;
......
final String name = parser.getName();
if (TAG_MERGE.equals(name)) { // 針對<merge>標籤
......
} else { // 針對普通標籤
// ① 通過XML生成對應的View對象
// Temp指的是XML文件中的根View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
// ② 通過XML中的佈局參數生成對應的LayoutParams
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// ③ 如果不需要將View附加到父容器中,就直接爲View設置LayoutParams
temp.setLayoutParams(params);
}
}
rInflateChildren(parser, temp, attrs, true); // 解析View中包含的子View(如果存在的話)
// ④ 如果父容器不爲null,且需要將View附加到父容器中,就使用addView方法
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
......
return result;
}
可以看到,如果父容器(ViewGroup)不爲空,在代碼②的位置將通過父容器的generateLayoutParams方法生成LayoutParams,這也間接說明了LayoutParams是與ViewGroup息息相關的,脫離ViewGroup談LayoutParams是沒有意義的。
在代碼③的位置,如果attachToRoot
參數爲false,代表不需要將View添加到父容器中,那就直接爲View設置LayoutParams;否則在代碼④的位置通過addView(temp, params)將View添加到父容器中。到了這一步,後續邏輯就和在Java代碼中實例化View是一樣的了。
其實最典型的例子就是在Activity中調用setContentView
方法,系統會通過LayoutInflater將整個XML文件解析爲View Tree,從根佈局開始爲每個View和ViewGroup設置相應的LayoutParams。
自定義LayoutParams
如果我們需要自定義ViewGroup的話,一般也會自定義LayoutParams,這樣可以提供一些個性化的佈局參數。爲了支持設置外間距,自定義的LayoutParams一般會選擇繼承ViewGroup.MarginLayoutParams。此外,還需要在XML文件中定義declare-styleable
資源屬性,一般會創建一個名爲attrs.xml
文件放置這些屬性。這裏假設我們要實現一個名爲SimpleViewGroup的自定義ViewGroup,示例代碼如下:
<resources>
<declare-styleable name="SimpleViewGroup_Layout">
<!-- 自定義的屬性 -->
<attr name="layout_simple_attr" format="integer"/>
<!-- 使用系統預置的屬性 -->
<attr name="android:layout_gravity"/>
</declare-styleable>
</resources>
這裏將declare-styleable
的name設置爲SimpleViewGroup_Layout,也就是自定義ViewGroup的名稱加上_Layout
。這裏一共定義了兩個屬性,第一個屬性使用了自定義的名稱,需要提供name
和format
參數,format用於限制自定義屬性的類型;第二個屬性使用了系統預置的屬性,比如這裏的android:layout_gravity
,好處是可以讓用戶使用熟悉的屬性(在系統提供的屬性語義合適時可以考慮這種方式)。不過要注意,這種情況下就不要爲它定義format參數了,因爲系統已經設置好了。
之後,需要在自定義的LayoutParams中解析這些屬性,下面是一個簡單的示例:
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
public int simpleAttr;
public int gravity;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
// 解析佈局屬性
TypedArray typedArray = c.obtainStyledAttributes(attrs, R.styleable.SimpleViewGroup_Layout);
simpleAttr = typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_layout_simple_attr, 0);
gravity=typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_android_layout_gravity, -1);
typedArray.recycle();//釋放資源
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
最後,我們還需要重寫ViewGroup中幾個與LayoutParams相關的方法,示例代碼如下:
// 檢查LayoutParams是否合法
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof SimpleViewGroup.LayoutParams;
}
// 生成默認的LayoutParams
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new SimpleViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
// 對傳入的LayoutParams進行轉化
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new SimpleViewGroup.LayoutParams(p);
}
// 對傳入的LayoutParams進行轉化
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new SimpleViewGroup.LayoutParams(getContext(), attrs);
}
這些方法的作用已經在前文介紹過了,同時代碼中也添加了註釋,這裏就不再贅述了。
LayoutParams常見的子類
在爲View設置LayoutParams的時候需要根據它的父容器選擇對應的LayoutParams,否則結果可能與預期不一致,這裏簡單羅列一些常見的LayoutParams子類:
- ViewGroup.MarginLayoutParams
- FrameLayout.LayoutParams
- LinearLayout.LayoutParams
- RelativeLayout.LayoutParams
- RecyclerView.LayoutParams
- GridLayoutManager.LayoutParams
- StaggeredGridLayoutManager.LayoutParams
- ViewPager.LayoutParams
- WindowManager.LayoutParams