在上一篇文章里,我總結(jié)了一下自定義控件需要了解的基礎(chǔ)知識:View的繪制流程——《自定義控件知識儲備-View的繪制流程》尉辑。其中帆精,在View的測量流程里,View的測量寬高是由父控件的MeasureSpec和View自身的LayoutParams共同決定的隧魄。MeasureSpec是什么卓练,上一篇文章里已經(jīng)說得很清楚了(啥,沒看過购啄?快去路克路克襟企,(??????)??)。而LayoutParams呢狮含?是時候在這里做個了斷了顽悼。
LayoutParams是什么?
LayoutParams辉川,顧名思義表蝙,就是Layout Parameters :布局參數(shù)。
很久很久以前乓旗,我就知道LayoutParams了府蛇,并且?guī)缀跆焯煲娒妗D菚r候在布局文件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指定寬度的。不過妆距,當(dāng)時年輕的我心里一直有個疑問:為什么要加上"layout_"前綴修飾呢穷遂?其它的描述屬性,如textColor
和background
娱据,都很正常膀胶凇!講道理,應(yīng)該用width
和height
描述寬高才對凹纱抒寂?
后來呀,我遇到了LayoutParams掠剑,它說layout_width
是它的屬性而非View的屈芜,并且不只是針對這一個,而是所有以"layout_"開頭的屬性都與它有關(guān)朴译!所以井佑,它的東西當(dāng)然要打上自己的標(biāo)識"layout_"。(呵呵眠寿,囂張個啥躬翁,到頭來你自己還不是屬于View的一部分( ̄┰ ̄*))
既然layout_width
這樣的屬性是LayoutParams定義的,那為何會出現(xiàn)在描述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的繼承類對應(yīng)著不同的ViewGroup.LayoutParams的子類蝶防。
看著我妙到巔峰的翻譯甚侣,想必大家都看懂了<( ̄▽ ̄)/〖溲В看不懂殷费?那我再來畫蛇添足稍微解釋一下:
上面我們提到過,描述View直接用它們自己的屬性就好了低葫,如
textColor
和background
等等详羡,為什么還需要引入LayoutParams呢?在我看來嘿悬,textColor
和background
這樣的屬性都是只與TextView自身有關(guān)的实柠,無論這個TextView處于什么環(huán)境,這些屬性都是不變的善涨。而layout_width
與layout_marginLeft
這樣的屬性是與它的父控件息息相關(guān)的窒盐,是父控件通過LayoutParams提供這些"layout_"屬性給孩子們用的;是父控件根據(jù)孩子們的要求(LayoutParams)來決定怎么測量钢拧,怎么安放孩子們的蟹漓;是父控件......(寫不下去了,我都快被父控件感動了源内,不得不再感慨一句葡粒,當(dāng)父母的都不容易啊(′⌒`)) )。所以,View的LayoutParams離開了父控件嗽交,就沒有意義了卿嘲。基類LayoutParams是ViewGroup類里的一個靜態(tài)內(nèi)部類(看吧,這就證明了LayoutParams是與父控件直接相關(guān)的)轮纫,它的功能很簡單腔寡,只提供了
width
和height
兩個屬性,對應(yīng)于xml里的layout_width
和layout_height
掌唾。所以放前,對任意系統(tǒng)提供的容器控件或者是自定義的ViewGroup,其chid view總是能寫layout_width
和layout_height
屬性的糯彬。自從有了
ViewGroup.LayoutParams
后凭语,我們就可以在自定義ViewGroup時,根據(jù)自己的邏輯實現(xiàn)自己的LayoutParams撩扒,為孩子們提供更多的布局屬性似扔。不用說,系統(tǒng)里提供給我們的容器控件辣么多搓谆,肯定也有很多LayoutParams的子類啦炒辉。let us see see:
果然,我們看到了很多ViewGroup.LayoutParams
的子類泉手,里面大部分我們應(yīng)該都比較熟悉黔寇。如果你覺得和它們不熟,那就是你一廂情愿啦斩萌,你早就“偷偷摸摸”的用過它們好多次了→_→
ViewGroup.MarginLayoutParams
我們首先來看看ViewGroup.MarginLayoutParams
缝裤,看名字我們也能猜到,它是用來提供margin屬性滴颊郎。margin屬性也是我們在布局時經(jīng)常用到的憋飞。看看這個類里面的屬性:
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設(shè)計出來代替leftMargin
和rightMargin
的榛做。
一般情況下,View開始部分就是左邊猾编,但是有的語言目前為止還是按照從右往左的順序來書寫的瘤睹,例如阿拉伯語。在Android 4.2系統(tǒng)之后答倡,Google在Android中引入了RTL布局轰传,更好的支持了從右往左文字布局的顯示。為了更好的兼容RTL布局瘪撇,google推薦使用MarginStart和MarginEnd來替代MarginLeft和MarginRight获茬,這樣應(yīng)用可以在正常的屏幕和從右往左顯示文字的屏幕上都保持一致的用戶體驗港庄。
我們除了在布局文件里用layout_marginLeft
和layout_marginTop
這樣的屬性來指定單個方向的間距以外,還會用layout_margin
來表示四個方向的統(tǒng)一間距恕曲。我們來通過源碼看看這一過程:
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
的構(gòu)造函數(shù)里鹏氧,將獲取到的xml布局文件里的屬性轉(zhuǎn)化成了leftMagrin
與rightMagrin
等值。先獲取xml里的layout_margin
值佩谣,如果未設(shè)置把还,則再去獲取layout_marginLeft
與layout_marginRight
等值。所以從這里可以得出一個小結(jié)論:
在xml布局里茸俭,
layout_margin
屬性的值會覆蓋layout_marginLeft
與layout_marginRight
等屬性的值吊履。
以前我還很傻很天真的猜測,屬性寫在后面调鬓,就會覆蓋前面的屬性艇炎。雖然經(jīng)過實踐,也能發(fā)現(xiàn)上述的結(jié)論腾窝,但是自己了解了背后的原理缀踪,再去看看源碼實現(xiàn),自然就有更深刻的印象了虹脯。<( ̄ˇ ̄)/
揭開隱藏的LayoutParams
在上文中提到驴娃,我們初學(xué)Android的時候經(jīng)常在“偷偷摸摸”的使用著LayoutParams,而自己卻還
因為我們常用它的方式是在XML布局文件里托慨,使用容器控件的LayoutParams里的各種屬性來給孩子們布局。這種方式直觀方便暇榴,直接就能在預(yù)覽界面看到效果,但是同時布局也被我們寫死了蕉世,無法動態(tài)改變蔼紧。想要動態(tài)變化,那還是得不怕麻煩狠轻,使用代碼來寫奸例。(實際上,我們寫的XML布局最終也是通過代碼來解析滴)
好的向楼,那還是讓我們通過源碼來揭開隱藏在ViewGroup
里的LayoutParams
吧查吊!<( ̄︶ ̄)↗[GO!]......等會,我們該從哪里開始看源碼呢湖蜕?我認為有句名言說的在理:
脫離場景談源碼逻卖,都是在耍流氓 ——英明神武蘑菇君
上文提到,LayoutParams
其實是父控件提供給child view的昭抒,好讓child view選擇如何測量和放置自己评也。所以肯定在child view添加到父控件的那一刻炼杖,child view就應(yīng)該有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設(shè)定好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.在添加的時候傳遞一個創(chuàng)建好的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里動態(tài)添加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)
是調(diào)用了addView(View child, int index)
方法的艇纺,在這個里面對child的LayoutParams做了判斷,如果為null的話邮弹,則調(diào)用了generateDefaultLayoutParams
方法為child生成一個默認的LayoutParams黔衡。這也合情合理,畢竟現(xiàn)在這個社會呀肠鲫,像蘑菇君我這么懶的人太多员帮,你要是不給個默認的選項,那別說友誼的小船了导饲,就算泰坦尼克捞高,那也說翻就翻!<( ̄︶ ̄)>......好的渣锦,那讓我們看看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
听盖。所以胀溺,這一點大家得注意,因為很有可能因為我們的懶皆看,導(dǎo)致布局效果和我們理想中的不一樣仓坞。因此呢,第1種添加View的方式是不推薦滴腰吟,像第2或第3種方式无埃,添加的時候指定了LayoutParams,不僅明確毛雇,而且易修改嫉称。(果然還是勤勞致富呀...)
上面三個重載的addView
方法最終都調(diào)用了addView(View child, int index, LayoutParams params)
這個參數(shù)最多的方法:
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
方法又調(diào)用了方法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
方法根據(jù)你傳遞的LayoutParams的屬性構(gòu)造一個LinearLayout的LayoutParams蒲稳。不得不再次感慨父容器控件的不容易:我們懶得設(shè)置child view的LayoutParams氮趋,甚至是設(shè)置了錯誤的LayoutParams,父控件都在竭盡所能的糾正我們的錯誤江耀,只為了給孩子提供一個舒適的環(huán)境剩胁。(╥╯^╰╥)
不過呀,雖然父控件可以在添加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
上面這種異常熟悉么舌稀?反正我是相當(dāng)熟悉的〒▽〒......原因就是上面代碼里的textView是LinearLayout的孩子啊犬,而我們調(diào)用textView的setLayoutParams
方法強行給它設(shè)置了一個ViewGroup的LayoutParams,所有在LinearLayout重新進行繪制流程的時候壁查,在onMeasure
方法里觉至,會進行強制類型轉(zhuǎn)換操作:
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
所以App斯巴達了。也許你會說睡腿,我才不會這么傻语御,我知道textView的父控件是LinearLayout了,我肯定會給它設(shè)置相應(yīng)的LayoutParams的席怪!這是當(dāng)然的啦应闯,在這種明確的情況下,我們當(dāng)然不會這么傻挂捻。但是碉纺,很不幸的是,有很多時候我們并不能一眼就看出來一個View的LayoutParams是什么類型的LayoutParams刻撒,這就需要動用你的智慧去分析分析啦骨田,希望這篇文章能給你一些幫助。?(^?^●)?
自定義LayoutParams
在本文的開頭就提到過:每個容器控件幾乎都會有自己的LayoutParams實現(xiàn)声怔,像LinearLayout盛撑、FrameLayout和RelativeLayout等等。所以捧搞,我們在自定義ViewGroup時,幾乎都要自定義相應(yīng)的LayoutParams狮荔。這一節(jié)呢胎撇,就是對如何自定義LayoutParams進行一個總結(jié)。
我以一個簡單的流布局FlowLayout為例殖氏,流布局的簡單定義如下:
FlowLayout:添加到此容器的控件自左往右依次排列晚树,如果當(dāng)前行的寬度不足以容納下一個控件,就會將此控件放置到下一行雅采。
假設(shè)這個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里的靜態(tài)內(nèi)部類LayoutParams是繼承ViewGroup.MarginLayoutParams
的,所以它的孩子們都可以用margin屬性沥寥。事實上碍舍,絕大部分容器控件都是直接繼承ViewGroup.MarginLayoutParams
而非ViewGroup.LayoutParams
。所以我們的FlowLayout也直接繼承ViewGroup.MarginLayoutParams
邑雅。
其次片橡,LinearLayout支持兩個屬性weight
和gravity
,這兩個屬性在xml對應(yīng)的就是layout_weight
和layout_gravity
淮野。在它的構(gòu)造函數(shù)LayoutParams(Context c, AttributeSet attrs)
里捧书,將獲取到的xml布局文件里的屬性轉(zhuǎn)化成了weight
與gravity
的值。不過com.android.internal.R.styleable.LinearLayout_Layout
這個東西是什么鬼录煤?其實這是系統(tǒng)在xml屬性文件里配置的declare-styleable
鳄厌,好讓系統(tǒng)知道LinearLayout能為它的孩子們提供哪些屬性支持。我們在布局的時候IDE也會給出這些快捷提示妈踊。而對于自定義的FlowLayout來說了嚎,模仿LinearLayout的寫法,可以在attrs.xml文件里這么寫:
<declare-styleable name="FlowLayout_Layout">
<attr name="android:layout_gravity"/>
</declare-styleable>
而剩下的幾個構(gòu)造方法起的作用就是從傳遞的LayoutParams參數(shù)里克隆屬性了廊营。
依葫蘆畫瓢歪泳,F(xiàn)lowLayout的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;
}
}
看起來還是挺簡單的吧?好露筒,那我們這篇文章到此結(jié)束......等一等呐伞!好像忘記了點什么......
如果對上面分析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時給它設(shè)置正確的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該如何重寫上面的幾個方法呢癣防?相信聰明的你已經(jīng)知道了。(??????)??
總結(jié)
這一篇文章從自定義控件的角度掌眠,并結(jié)合源碼和表情包生動形象的談了談我所理解的LayoutParams蕾盯。(生動,形象蓝丙?真不要臉...(ˉ﹃ˉ))级遭。不得不說望拖,結(jié)合源碼來學(xué)習(xí)某個知識點,的確是能起到事半功倍的作用挫鸽。蘑菇君初來乍到说敏,文章里如有錯誤和疏漏之處,歡迎指正和補充掠兄。
預(yù)告
下一篇文章打算記錄一個簡單的自定義ViewGroup:流布局FlowLayout
的實現(xiàn)過程像云,將自定義控件知識儲備-View的繪制流程里的知識點和本篇文章的LayoutParams結(jié)合起來。
PS:寫博客的初始階段果然是有些艱辛蚂夕,腦海里想寫的很多迅诬,而真到了要以文字表達出來時,卻有一種“愛你在心口難開”的尷尬婿牍。不過侈贷,感覺到艱難也就意味著自己在走上坡路,堅持下去等脂,希望能給自己和大家?guī)砀嗟膸椭?/p>
我是蘑菇君俏蛮,我為自己帶鹽