安卓的布局有很多種,每種都有對應的LayoutParams類,那么它們之間到底是什么關系?
為什么在編寫Layout的XML文件時,有的layout_前綴屬性有用有的沒有用?
一句話道出LayoutParams的本質:LayoutParams是Layout提供給其中的Children使用的。我們來看一段不用XML文件創建布局的代碼。
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); LinearLayout ll = new LinearLayout(this); ll.setOrientation(LinearLayout.VERTICAL); TextView tv = new TextView(this); tv.setText("I am a text view, haha"); tv.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); ll.addView(tv); setContentView(ll); }
可見LayoutParams的對象是設置在Layout中的Child View上的,而不是設置在自己上。就是說View想要放到某種Layout中,就必須設置這個Layout規定的LayoutParams。
每一種Layout實現不同的布局方式都有其獨特的參數設置,例如LinearLayout.LayoutParams的weight屬性,用來設置Child View之間的大小比例,如果把這個設置給FrameLayout是沒有意義的,因為FrameLayout並不會排列Child View而是會都“擠在一起”。所以在XML文件里設置View的屬性的時候,有的layout屬性是無效的,也是沒有意義的。
如果仔細看上面那段代碼,會發現有個問題,設置TextView的LayoutParams是ViewGroup.LayoutParams,而不是LinearLayout.LayoutParams。查看源代碼可以看到LinearLayout.LayoutParams是ViewGroup.LayoutParams的子類,那么會不會產生運行時錯誤?下面通過分析源代碼找到這個問題的答案。
使Layout與View發生關系的語句是LinearLayout.addView(),先找到LinearLayout.java(frameworks/base/core/java/android/widget/LinearLayout.java)發現沒有override這個方法,到父類ViewGroup(frameworks/base/core/java/android/view/ViewGroup.java)中尋找addView。
public void addView(View child) { addView(child, -1); } public void addView(View child, int index) { LayoutParams params = child.getLayoutParams(); if (params == null) { // ...... } addView(child, index, params); } public void addView(View child, int index, LayoutParams params) { requestLayout(); invalidate(true); addViewInner(child, index, params, false); }
addView有N個重載(overload)的版本,注意第二個是直接獲取Child View的LayoutParams,這里沒有經過處理,最后傳遞給了addViewInner()方法,再來看addViewInner()方法的代碼:
private void addViewInner(View child, int index, LayoutParams params, boolean preventRequestLayout) { // ... if (!checkLayoutParams(params)) { params = generateLayoutParams(params); } // ... }
發現里面有這樣兩個調用checkLayoutParams()和generateLayoutParams()可能會修改或替換傳進來的params,找到這兩個函數發現ViewGroup中的實現幾乎是空的:
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p != null; } protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return p; }
讓我們回到LinearLayout里看看有沒有override這兩個方法:
@Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof LinearLayout.LayoutParams; } @Override public LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return new LinearLayout.LayoutParams(p); }
checkLayoutParams做的事情是檢查這個LayoutParam對象是否是LinearLayout.LayoutParams的實例,按照剛開始寫的代碼運行到這里就返回false了,於是執行generateLayoutParams,通過ViewGroup.LAyoutParams創建一個新的LinearLayout.LayoutParams。這樣就保證了使用的一定是LinearLayout.LayoutParams或者其子類。再看一下LinearLayout.LayoutParams的構造函數:
public float weight; public int gravity = -1; 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; }
顯然應該調用的是第一個構造函數,然后weight和gravity的值取默認值。為什么要定義第一個和第二個構造函數?因為LinearLayout.LayoutParams繼承自ViewGroup.MarginLayoutParams,而ViewGroup.MarginLayoutParams繼承自ViewGroup.LayoutParams。可見二者是LinearLayout.LayoutParams繼承樹上的所有祖先類。添加這兩個構造方法使能通過祖先類對象構造自己,配合上面的check和generate使不適配的LayoutParams變得可用。
根據以上分析,可以得出一個小結論,使用代碼構建界面的時候,可以隨意使用ViewGroup.LayoutParams或者ViewGroup.MarginLayoutParams,因為所有的Layout中的LayoutParams都繼承自他們,但是也會付出一點點代價就是會新創建一個對象。不過畢竟靠代碼構建界面的場景很少,一般用XML就可以了,這也算是個冷知識點吧。