自定義控制元件知識儲備-LayoutParams的那些事
在上一篇文章裡,我總結了一下自定義控制元件需要了解的基礎知識:View的繪製流程——《自定義控制元件知識儲備-View的繪製流程》。其中,在View的測量流程裡,View的測量寬高是由父控制元件的MeasureSpec和View自身的LayoutParams共同決定的。MeasureSpec是什麼,上一篇文章裡已經說得很清楚了(啥,沒看過?快去路克路克,(๑•̀ㅂ•́)و✧)。而LayoutParams呢?是時候在這裡做個了斷了。
LayoutParams是什麼?
LayoutParams,顧名思義,就是Layout Parameters :佈局引數。
很久很久以前,我就知道LayoutParams了,並且幾乎天天見面。那時候在佈局檔案XML裡,寫的最多的肯定是android:layout_width = "match_parent"
<TextView
style="@style/text_flag_01"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginLeft="10dp"
android:layout_gravity="center"
android:gravity="center"
android:text="英明神武蘑菇君"
android:textColor="@color/white"
android:background="@color /colorAccent"/>
我們都知道layout_width
和layout_height
這兩個屬性是為View指定寬度的。不過,當時年輕的我心裡一直有個疑問:為什麼要加上”layout_”字首修飾呢?其它的描述屬性,如textColor
和background
,都很正常啊!講道理,應該用width
和height
描述寬高才對啊?
後來呀,我遇到了LayoutParams,它說layout_width
是它的屬性而非View的,並且不只是針對這一個,而是所有以”layout_”開頭的屬性都與它有關!所以,它的東西當然要打上自己的標識”layout_”。(呵呵,囂張個啥,到頭來你自己還不是屬於View的一部分( ̄┰ ̄*))
既然layout_width
這樣的屬性是LayoutParams定義的,那為何會出現在描述View的xml屬性裡呢?View和LayoutParams之間有什麼恩怨糾纏呢?
不吹不黑,咱們來看看官方文件是怎麼說的:
LayoutParams are used by views to tell their parents how they want to be laid out.
– LayoutParams是View用來告訴它的父控制元件如何放置自己的。The base LayoutParams class just describes how big the view wants to be for both width and height.
– 基類LayoutParams(也就是ViewGroup.LayoutParams)僅僅描述了這個View想要的寬度和高度。There are subclasses of LayoutParams for different subclasses of ViewGroup.
– 不同ViewGroup的繼承類對應著不同的ViewGroup.LayoutParams的子類。
看著我妙到巔峰的翻譯,想必大家都看懂了<( ̄▽ ̄)/。看不懂?那我再來畫蛇添足稍微解釋一下:
上面我們提到過,描述View直接用它們自己的屬性就好了,如
textColor
和background
等等,為什麼還需要引入LayoutParams呢?在我看來,textColor
和background
這樣的屬性都是隻與TextView自身有關的,無論這個TextView處於什麼環境,這些屬性都是不變的。而layout_width
與layout_marginLeft
這樣的屬性是與它的父控制元件息息相關的,是父控制元件通過LayoutParams提供這些”layout_”屬性給孩子們用的;是父控制元件根據孩子們的要求(LayoutParams)來決定怎麼測量,怎麼安放孩子們的;是父控制元件……(寫不下去了,我都快被父控制元件感動了,不得不再感慨一句,當父母的都不容易啊(′⌒`)) )。所以,View的LayoutParams離開了父控制元件,就沒有意義了。基類LayoutParams是ViewGroup類裡的一個靜態內部類(看吧,這就證明了LayoutParams是與父控制元件直接相關的),它的功能很簡單,只提供了
width
和height
兩個屬性,對應於xml裡的layout_width
和layout_height
。所以,對任意系統提供的容器控制元件或者是自定義的ViewGroup,其chid view總是能寫layout_width
和layout_height
屬性的。自從有了
ViewGroup.LayoutParams
後,我們就可以在自定義ViewGroup時,根據自己的邏輯實現自己的LayoutParams,為孩子們提供更多的佈局屬性。不用說,系統裡提供給我們的容器控制元件辣麼多,肯定也有很多LayoutParams的子類啦。let us see see:
果然,我們看到了很多ViewGroup.LayoutParams
的子類,裡面大部分我們應該都比較熟悉。如果你覺得和它們不熟,那就是你一廂情願啦,你早就“偷偷摸摸”的用過它們好多次了→_→
ViewGroup.MarginLayoutParams
我們首先來看看ViewGroup.MarginLayoutParams
,看名字我們也能猜到,它是用來提供margin屬性滴。margin屬性也是我們在佈局時經常用到的。看看這個類裡面的屬性:
public static class MarginLayoutParams extends ViewGroup.LayoutParams {
public int leftMargin;
public int topMargin;
public int rightMargin;
public int bottomMargin;
private int startMargin = DEFAULT_MARGIN_RELATIVE;
private int endMargin = DEFAULT_MARGIN_RELATIVE;
...
}
前面4個屬性是我們以前在佈局檔案裡常用的,而後面的startMargin
和endMargin
是為了支援RTL設計出來代替leftMargin
和rightMargin
的。
一般情況下,View開始部分就是左邊,但是有的語言目前為止還是按照從右往左的順序來書寫的,例如阿拉伯語。在Android 4.2系統之後,Google在Android中引入了RTL佈局,更好的支援了從右往左文字佈局的顯示。為了更好的相容RTL佈局,google推薦使用MarginStart和MarginEnd來替代MarginLeft和MarginRight,這樣應用可以在正常的螢幕和從右往左顯示文字的螢幕上都保持一致的使用者體驗。
我們除了在佈局檔案裡用layout_marginLeft
和layout_marginTop
這樣的屬性來指定單個方向的間距以外,還會用layout_margin
來表示四個方向的統一間距。我們來通過原始碼看看這一過程:
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 {
leftMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
UNDEFINED_MARGIN);
if (leftMargin == UNDEFINED_MARGIN) {
mMarginFlags |= LEFT_MARGIN_UNDEFINED_MASK;
leftMargin = DEFAULT_MARGIN_RESOLVED;
}
...
}
...
}
在這個MarginLayoutParams
的建構函式裡,將獲取到的xml佈局檔案裡的屬性轉化成了leftMagrin
與rightMagrin
等值。先獲取xml裡的layout_margin
值,如果未設定,則再去獲取layout_marginLeft
與layout_marginRight
等值。所以從這裡可以得出一個小結論:
在xml佈局裡,
layout_margin
屬性的值會覆蓋layout_marginLeft
與layout_marginRight
等屬性的值。
以前我還很傻很天真的猜測,屬性寫在後面,就會覆蓋前面的屬性。雖然經過實踐,也能發現上述的結論,但是自己瞭解了背後的原理,再去看看原始碼實現,自然就有更深刻的印象了。<( ̄ˇ ̄)/
揭開隱藏的LayoutParams
在上文中提到,我們初學Android的時候經常在“偷偷摸摸”的使用著LayoutParams,而自己卻還
。
因為我們常用它的方式是在XML佈局檔案裡,使用容器控制元件的LayoutParams裡的各種屬性來給孩子們佈局。這種方式直觀方便,直接就能在預覽介面看到效果,但是同時佈局也被我們寫死了,無法動態改變。想要動態變化,那還是得不怕麻煩,使用程式碼來寫。(實際上,我們寫的XML佈局最終也是通過程式碼來解析滴)
好的,那還是讓我們通過原始碼來揭開隱藏在ViewGroup
裡的LayoutParams
吧!<( ̄︶ ̄)↗[GO!]……等會,我們該從哪裡開始看原始碼呢?我認為有句名言說的在理:
脫離場景談原始碼,都是在耍流氓 ——英明神武蘑菇君
上文提到,LayoutParams
其實是父控制元件提供給child view的,好讓child view選擇如何測量和放置自己。所以肯定在child view新增到父控制元件的那一刻,child view就應該有LayoutParams
了。我們來看看幾個常見的新增View的方式:
LinearLayout parent = (LinearLayout) findViewById(R.id.parent);
// 1.直接新增一個“裸”的TextView,不主動指定LayoutParams
TextView textView = new TextView(this);
textView.setText("紅色蘑菇君");
textView.setTextColor(Color.RED);
parent.addView(textView);
// 2.先手動給TextView設定好LayoutParams,再新增
textView = new TextView(this);
textView.setText("綠色蘑菇君");
textView.setTextColor(Color.GREEN);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(300,300);
textView.setLayoutParams(lp);
parent.addView(textView);
// 3.在新增的時候傳遞一個建立好的LayoutParams
textView = new TextView(this);
textView.setText("藍色蘑菇君");
textView.setTextColor(Color.BLUE);
LinearLayout.LayoutParams lp2 = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,300);
parent.addView(textView, lp2);
上面程式碼展示的是3種往LinearLayout裡動態新增TextView的方式,其中都涉及到了addView
這個方法。我們來看看addView
的幾個過載方法:
//這3個方法都來自於基類ViewGroup
public void addView(View child) {
addView(child, -1);
}
/*
* @param child the child view to add
* @param index the position at which to add the child
/
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);
}
public void addView(View child, LayoutParams params) {
addView(child, -1, params);
}
可以看出addView(View child)
是呼叫了addView(View child, int index)
方法的,在這個裡面對child的LayoutParams做了判斷,如果為null的話,則呼叫了generateDefaultLayoutParams
方法為child生成一個預設的LayoutParams。這也合情合理,畢竟現在這個社會呀,像蘑菇君我這麼懶的人太多,你要是不給個預設的選項,那別說友誼的小船了,就算泰坦尼克,那也說翻就翻!<( ̄︶ ̄)>……好的,那讓我們看看LinearLayout為我們這群懶人生成了怎樣的預設LayoutParams:
@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;
}
顯然,LinearLayout是重寫了基類ViewGroup裡的generateDefaultLayoutParams
方法的:如果佈局是水平方向,則孩子們的寬高都是WRAP_CONTENT
,而如果是垂直方向,高仍然是WRAP_CONTENT
,但寬卻變成了MATCH_PARENT
。所以,這一點大家得注意,因為很有可能因為我們的懶,導致佈局效果和我們理想中的不一樣。因此呢,第1種新增View的方式是不推薦滴,像第2或第3種方式,新增的時候指定了LayoutParams,不僅明確,而且易修改。(果然還是勤勞致富呀…)
上面三個過載的addView
方法最終都呼叫了addView(View child, int index, LayoutParams params)
這個引數最多的方法:
public void addView(View child, int index, LayoutParams params) {
...
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);
}
if (preventRequestLayout) {
child.mLayoutParams = params;
} else {
child.setLayoutParams(params);
}
...
}
addView
方法又呼叫了方法addViewInner
,在這個私有方法裡,又幹了哪些偷偷摸摸的事呢?接著來看看:
//這兩個方法都重寫了基類ViewGroup裡的方法
// Override to allow type-checking of LayoutParams.
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LinearLayout.LayoutParams;
}
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
checkLayoutParams
方法的作用是檢查傳遞進來的LayoutParams是不是LinearLayout的LayoutParam。如果不是呢?再通過generateLayoutParams
方法根據你傳遞的LayoutParams的屬性構造一個LinearLayout的LayoutParams。不得不再次感慨父容器控制元件的不容易:我們懶得設定child view的LayoutParams,甚至是設定了錯誤的LayoutParams,父控制元件都在竭盡所能的糾正我們的錯誤,只為了給孩子提供一個舒適的環境。(╥╯^╰╥)
不過呀,雖然父控制元件可以在新增View時幫我們糾正部分錯誤,但我們在其他情況下錯誤的修改child View的LayoutParams,那父控制元件也愛莫能助了。比如下面這種情況:
LinearLayout parent = (LinearLayout) findViewById(R.id.parent);
textView = new TextView(this);
textView.setText("此處有BUG");
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(200,200);
parent.addView(textView, lp);
textView.setLayoutParams(new ViewGroup.LayoutParams(100,100));
會直接報ClassCastException
:
java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to android.widget.LinearLayout$LayoutParams
上面這種異常熟悉麼?反正我是相當熟悉的〒▽〒……原因就是上面程式碼裡的textView是LinearLayout的孩子,而我們呼叫textView的setLayoutParams
方法強行給它設定了一個ViewGroup的LayoutParams,所有在LinearLayout重新進行繪製流程的時候,在onMeasure
方法裡,會進行強制型別轉換操作:
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
所以App斯巴達了。也許你會說,我才不會這麼傻,我知道textView的父控制元件是LinearLayout了,我肯定會給它設定相應的LayoutParams的!這是當然的啦,在這種明確的情況下,我們當然不會這麼傻。但是,很不幸的是,有很多時候我們並不能一眼就看出來一個View的LayoutParams是什麼型別的LayoutParams,這就需要動用你的智慧去分析分析啦,希望這篇文章能給你一些幫助。♪(^∀^●)ノ
自定義LayoutParams
在本文的開頭就提到過:每個容器控制元件幾乎都會有自己的LayoutParams實現,像LinearLayout、FrameLayout和RelativeLayout等等。所以,我們在自定義ViewGroup時,幾乎都要自定義相應的LayoutParams。這一節呢,就是對如何自定義LayoutParams進行一個總結。
我以一個簡單的流佈局FlowLayout為例,流佈局的簡單定義如下:
FlowLayout:新增到此容器的控制元件自左往右依次排列,如果當前行的寬度不足以容納下一個控制元件,就會將此控制元件放置到下一行。
假設這個FlowLayout可以給它的孩子們提供一個gravity屬性,效果就是讓孩子能在某一行的垂直方向上選擇三個位置:top(處於頂部)、center(居中)、bottom(處於底部)。咦?這個效果是不是和LinearLayout提供給孩子的layout_gravity
屬性很像?那好,我們來參考一下LinearLayout裡的LayoutParams原始碼:
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
public float weight;
public int gravity = -1;
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();
}
public LayoutParams(ViewGroup.LayoutParams p) {
super(p);
}
public LayoutParams(ViewGroup.MarginLayoutParams source) {
super(source);
}
public LayoutParams(LayoutParams source) {
super(source);
this.weight = source.weight;
this.gravity = source.gravity;
}
}
首先,LinearLayout裡的靜態內部類LayoutParams是繼承ViewGroup.MarginLayoutParams
的,所以它的孩子們都可以用margin屬性。事實上,絕大部分容器控制元件都是直接繼承ViewGroup.MarginLayoutParams
而非ViewGroup.LayoutParams
。所以我們的FlowLayout也直接繼承ViewGroup.MarginLayoutParams
。
其次,LinearLayout支援兩個屬性weight
和gravity
,這兩個屬性在xml對應的就是layout_weight
和layout_gravity
。在它的建構函式LayoutParams(Context c, AttributeSet attrs)
裡,將獲取到的xml佈局檔案裡的屬性轉化成了weight
與gravity
的值。不過com.android.internal.R.styleable.LinearLayout_Layout
這個東西是什麼鬼?其實這是系統在xml屬性檔案裡配置的declare-styleable
,好讓系統知道LinearLayout能為它的孩子們提供哪些屬性支援。我們在佈局的時候IDE也會給出這些快捷提示。而對於自定義的FlowLayout來說,模仿LinearLayout的寫法,可以在attrs.xml檔案裡這麼寫:
<declare-styleable name="FlowLayout_Layout">
<attr name="android:layout_gravity"/>
</declare-styleable>
而剩下的幾個構造方法起的作用就是從傳遞的LayoutParams引數裡克隆屬性了。
依葫蘆畫瓢,FlowLayout的LayoutParams如下:
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
public int gravity = -1;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a =
c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);
weight = a.getFloat(R.styleable.FlowLayout_Layout, 0);
gravity = a.getInt(R.styleable.FlowLayout_Layout_android_layout_gravity, -1);
a.recycle();
}
public LayoutParams(ViewGroup.LayoutParams p) {
super(p);
}
public LayoutParams(ViewGroup.MarginLayoutParams source) {
super(source);
}
public LayoutParams(LayoutParams source) {
super(source);
this.gravity = source.gravity;
}
}
看起來還是挺簡單的吧?好,那我們這篇文章到此結束……等一等!好像忘記了點什麼……
如果對上面分析ViewGroup的addView
方法的流程還有印象,可能你會注意ViewGroup裡的這幾個方法:
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return p;
}
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p != null;
}
為了能在新增child view時給它設定正確的LayoutParams,我們還需要重寫上面幾個方法(還問為啥要重寫?快翻到前面再see see)。同樣的,我們還是先來看看LinearLayout是怎麼處理的吧:
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LinearLayout.LayoutParams(getContext(), attrs);
}
@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;
}
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
// Override to allow type-checking of LayoutParams.
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LinearLayout.LayoutParams;
}
那FlowLayout該如何重寫上面的幾個方法呢?相信聰明的你已經知道了。(๑•̀ㅂ•́)و✧
總結
這一篇文章從自定義控制元件的角度,並結合原始碼和表情包生動形象的談了談我所理解的LayoutParams。(生動,形象?真不要臉…(¯﹃¯))。不得不說,結合原始碼來學習某個知識點,的確是能起到事半功倍的作用。蘑菇君初來乍到,文章裡如有錯誤和疏漏之處,歡迎指正和補充。
預告
下一篇文章打算記錄一個簡單的自定義ViewGroup:流佈局FlowLayout
的實現過程,將自定義控制元件知識儲備-View的繪製流程裡的知識點和本篇文章的LayoutParams結合起來。
PS:寫部落格的初始階段果然是有些艱辛,腦海裡想寫的很多,而真到了要以文字表達出來時,卻有一種“愛你在心口難開”的尷尬。不過,感覺到艱難也就意味著自己在走上坡路,堅持下去,希望能給自己和大家帶來更多的幫助。
我是蘑菇君,我為自己帶鹽