通常情況下,我們需要將一個xml解析為view,我們都可以很順手的寫出這樣的語句:
LayoutInflater.from(context).inflate(id,parent,false);
或者是這樣
LayoutInflater.from(context).inflate(id,null);
又或者是這樣
View.inflate(context,id,parent);
一般而言,前兩種用的比較多,最后一種估計比較少人用吧。
但我們今天不討論inflate應該如何使用,而是討論一下以下幾個問題:
- inflate()的第三個參數是用來干啥的。
- 第三個參數為啥我們平時都寫false,或者直接不要第二第三個給null
- 什么情況下第三個參數應該給true
而我們的探索步驟也很簡單:
- 測試inflate()第三個參數的不同值的情況
- 往源碼挖掘
- 總結
Step 1:測試
測試之前,我們先進行一下基本的布局
activity的布局很簡單,只有一個RelativeLayout,其id命名為parent:
而測試的view則是一個textview,其id命名為test_text:
首先我們測試一下第三個參數為true的情況:
在java中find出parent,然后通過LayoutInflater將我們的測試的view的xml轉換為view,其中第三個參數設定為true:
public class MainActivity extends AppCompatActivity {
private RelativeLayout parent;
@Override
protected void onCreate(Bundle savedInstanceState) {
...略
parent= (RelativeLayout) findViewById(R.id.parent);
View view=LayoutInflater.from(this).inflate(R.layout.item_test,parent,true);
}
}
結果:
結果很普通,跟我們平時addView沒差別。
然后我們到Hierarchy View工具查看view的層次,得到以下結果:
留意一下紅框位置,我們的測試view變成了parent的子view。
接下來我們測試一下第三個參數為false的情況:
基本步驟同上,不過我們將textview的text改為"false of the 3rd param",并將第三個參數改為false
結果:
這次我們得到的是一片空白。
同樣我們到Hierarchy View工具查看:
跟上次比較,我們發現我們的textview并沒有被add到parent里面。
到這里我們不妨猜測一下,這第三個參數會不會是用來將xml轉化得到的view給add到第二個參數root中的?
Step 2:源碼
事實上,在第一步我們的猜測基本可以確認這個結論了。
不過了解一個框架原理或代碼原理,最直接有效的還是去查看源碼,查看源碼之前,還需要查看注釋。
關于inflate第三個參數的注釋是這樣的:
@param attachToRoot
- Whether the inflated hierarchy should be attached to
the root parameter? If false, root is only used to create the
correct subclass of LayoutParams for the root view in the XML.
渣翻:
這個參數決定被inflate的view是否會附加(add)到根(父,即第二個參數的viewgroup)布局。如果不是,那么根(父)布局只會被用來創建適合子類的LayoutParams(測量)。
我們都知道,子view的LayoutParams實際上是通過其父View得到的,父View是什么viewgroup,那么子view就會得到什么樣的LayoutParams。
而這第三個參數的作用只有一個:是否將inflate出來的view添加到第二個參數的父view中,而無論該參數如何,被轉換出來的view都會創建第二個參數的父view的LayoutParams。
看完注釋我們大概了解到第三個參數的作用,接下來我們扎入到源碼中,看看到底是怎么玩的。
inflate()源碼:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \\\\\\\\"" + res.getResourceName(resource) + "\\\\\\\\" ("
+ Integer.toHexString(resource) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
在這里我們可以看到,在調用這個方法的時候會做以下兩個步驟:
- 通過上下文得到Resource類
- 通過得到的Resource再得到xml解析類,如果寫過Android解析xml文件的話,那么對這個東東應該不難理解
接下來我們看看調用的inflate方法
代碼有100多行,我們就挑選與第三個參數有關的代碼查看
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
...Trace配置,attrs獲取
View result = root;
try {
...略
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
...略
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
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;
}
}
}
...異常捕獲(xml解析異常/inflate異常)等
return result;
}
}
整理過后的代碼如上,我們逐步分析一下:
首先需要注意的是,在超大的try/catch前,會將root賦值給result,這個就是最終返回的結果。
-
在第一次出現attachToRoot參數,是在判斷標簽是否為merge的時候。
我們都知道,merge是減少布局層次的神器,特別是相同viewgroup的嵌套里面真的很好用,但因為merge并不會產生根布局(本身就是根標簽),所以如果我們inflate的話,就必須傳入root,這樣很好理解為什么inflate帶merge標簽時為什么必須傳入rootView
同時,我們在別的布局里使用merge的布局文件的話,都是通過<include>把該布局給引用進來,也就是說,merge并不能單獨存在,必須依附在父布局里面。
所以如果父控件和attachToRoot不滿足其一就會拋異常。
rInflate()方法我們暫時略過,因為在這個if里面執行到rInflate只會是繼續拋異常。
-
第二次出現attachToRoot參數則是在接下來的else里面,在看它的方法前,不妨看看它前面執行了什么:
-
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
在這段代碼里,創建了一個臨時的view,至于createViewFromTag是什么,這里只告訴你它通過工廠創建一個view,并且根據是否需要賦予屬性來創建對應的view。
-
第二次出現attachToRoot在臨時的view創建了之后,通過代碼我們可以看到,此時因為在root!=null的if里面,所以通過root生成它的LayoutParams,如果第三個參數為false,就把這個LayoutParams賦值給這個臨時的view
-
第三次和第四次出現attachToRoot參數,就是在返回view之前了,這里可以看到
- 如果root不空而且attachToRoot=true,就把臨時的view給添加到root里面(帶LayoutParams),此時result依然等于root,最終返回
- 如果root為空或者attachToRoot=false,那么就將臨時的view返回賦值給result,最終返回。
上面就是對這段代碼的分析,從中我們可以總結一下:
-
無論root是否為空,都會產生一個臨時的view。
- 如果root不為空,則這個臨時的view會有root的LayoutParams
- 如果root為空,則這個臨時的view就僅僅是一個不帶參數的view,跟我們直接new出一個對象差不多,只是包含有attr屬性而已。
-
第二個參數(root)與第三個參數(attachToRoot)的問題
- 如果root不空同時attachToRoot=true,那么返回的不是這個view,而是root
- 如果上述條件不成立,那么返回的才是這個臨時的view
Step 3:attachToRoot的取值問題
說了這么多,那么我們的attachToRoot到底應該如何取。
在這里,我更傾向于給false。因為通過第二步的分析我們知道,如果同時滿足root不空同時attachToRoot=true,我們得到的是root。
普通情況下可能沒問題,但如果有時候遇到跟本例一樣,需要inflate的xml是一個控件作為根節點,而我們需要得到這個view,如果第二個和第三個參數都滿足的話,我們得到的view如果強轉則會拋出cast異常。當然,我們可以通過view.findViewById,但何必多此一舉呢對吧。
當然,這個還是看情況,畢竟知道了里面做了什么后我們就不怕為何無端端報錯了對吧。