接觸了Android的人也肯定不會對LayoutInflater陌生,至少在ListView等等這些常見控件中我們也經(jīng)常會使用這個類來進(jìn)行我們的item布局的解析,那么今天我們就來把LayoutInflater的工作流程仔細(xì)地分析一遍,爭取達(dá)到知其然知其所以然的境界。本文分析的源代碼均來自Android API 24。同時代碼分析在上半部分,下半部分將用demo來進(jìn)行驗證。
我們在日常開發(fā)寫代碼時一般會通過下面兩種方式來獲取LayoutInflater來進(jìn)行布局的解析:
1.LayoutInflater layoutInflater = LayoutInflater.from(context);
2.LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
而在第一種方法中我們跟進(jìn)from()方法去看源代碼就會發(fā)現(xiàn),其實第一種方法無非就是對第二種方法進(jìn)行封裝(代碼如下):從上圖中標(biāo)注處可以看出其實也是通過context.getSystemService方法來獲取,只不過增加了安全判斷更加安全而已。
我們獲取LayoutInflater對象后,就可以通過以下方法來進(jìn)行布局解析:
layoutInflater.inflate(resourceId, root,attachToRoot);
其中第一個參數(shù)就是要加載的布局id,第二個參數(shù)是傳入一個布局做為要解析布局的父布局。如果不需要就直接傳null。第三個參數(shù)指的是加載的布局是否添加到我們傳入的父布局中。
接下來我們再來接著具體分析一下inflate()方法。
圖一調(diào)用的inflate方法只需要傳入布局id,root父布局view對象,而第三個參數(shù)的值是通過判斷root是否為空來設(shè)置,再調(diào)用圖三方法。
圖二中第一個是XmlPullParser對象(是一種通過pull方式來解析xml的對象,有興趣的同志可以自行了解,只需要知道用來解析xml的對象即可),第二個參數(shù),第三個參數(shù)和圖一同理。
圖三我們可以看到,將傳入的布局id轉(zhuǎn)化為Resources類型,然后將它解析來獲取XmlResourceParser對象,再最終調(diào)用圖二方法。
至此其實已經(jīng)很清晰了,其實上面幾個不同的方法都只是做了一點預(yù)處理的過程,最終的實現(xiàn)的源代碼并沒有出現(xiàn)在這三個方法中,而是最終調(diào)用了:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot);
而這個方法,就是最后一個調(diào)用方法,也正是關(guān)鍵所在,接下來的內(nèi)容將對這個方法進(jìn)行講解。
先將方法源代碼貼出在下方:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
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);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// 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;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(parser.getPositionDescription()
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
上面是完整版本的源代碼,去除其中一些打印,異常處理和預(yù)處理無關(guān)代碼后。我們將關(guān)鍵代碼提取出來進(jìn)行分析,關(guān)鍵代碼如下:第一幅圖中我們可以在標(biāo)注處看到,一開始定義了一個View類型的result變量一開始默認(rèn)就是我們傳入的root對象。
而第二幅圖就是處理的關(guān)鍵代碼。
首先在圖中標(biāo)注1處通過注釋可看到:temp是我們xml布局中定義的根布局。因此在這里將我們的布局的根布局解析出來轉(zhuǎn)化為View類型的temp對象,然后定義一個params。
接下來在標(biāo)注2處判斷root是否為空,如果不為空,將自定義布局的參數(shù)解析出來賦值給params。
然后在標(biāo)注3處判斷我們傳入的attchToRoot是否為true,如果不是,意味著我們不把我們的自定義布局加入到某個父View中,因此將params設(shè)置給temp,將temp在xml設(shè)置的屬性進(jìn)行生效。
標(biāo)注3處和標(biāo)注4處之間的rInflateChildren()方法是通過遞歸的方法用來不斷解析我們自定義布局中的子View,這不再做展開。
接下來在圖中標(biāo)注4處判斷root是否為空,attachToRoot是否為true。如果root不為空并且attachToRoot為true,那么將我們的temp添加到父布局中。
上述代碼都是在root不為空的場景下設(shè)置的,如果root為空呢?
我們看看圖中標(biāo)注5處:如果root為空,或者attachToRoot為false,那么將temp直接賦值為result并返回。(result可以在上面的完整源碼中看到創(chuàng)建時就是root).
------------------------------一條假裝很華麗的分割線------------------------------
現(xiàn)在我們來總結(jié)一下幾個結(jié)論:
- 如果root為null,attachToRoot將沒有作用,inflate()方法會直接返回temp(也就是自定義布局的View,直接執(zhí)行上圖4處代碼);
- 如果root不為null,attachToRoot設(shè)為true,則會給自定義布局文件添加一個父布局,即root。方法返回root對象。(執(zhí)行2,4處)。
- 如果root不為null,attachToRoot設(shè)為false,則會將布局文件最外層的所有l(wèi)ayout屬性進(jìn)行設(shè)置,當(dāng)該view被添加到父view當(dāng)中時,這些layout屬性會自動生效。返回自定義布局View。(執(zhí)行2,3處)
- 不傳入attachToRoot參數(shù),如果root不為null,attachToRoot參數(shù)為true(參考上文幾個方法參數(shù)說明圖)。
(ps:個人理解:總之就是返回我們布局的根布局,如果我們沒有傳入root,那么明顯我們自定義布局根布局就是最頂級,返回它。如果我們傳入了root,但是attchToRoot為false,說明我們不想把自定義布局加入到root中,所以我們想要的布局的根布局也是自定義布局的根布局。而我們傳入root,attchToRoot為true,說明我們要把自定義布局加入到root中,所以root成了最頂級布局,所以返回root。)
------------------------------又是一條假裝很華麗的分割線----------------------------
至此代碼和結(jié)論就都已經(jīng)分析完畢了,按照常理我們就該撒花結(jié)束,但是呢?說了半天,沒啥直接的效果看看??!這干說半天沒意思是吧?因此現(xiàn)在我們來寫個Demo實際看一下效果驗證一下我們上述的分析和結(jié)論是否正確:
可以看到就是一個簡單的Button,Button就是根布局。
我們接下來在Activity中解析一下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//mainLayout就是MainActivity的布局,就是一個空的LinearLayout
LinearLayout mainLayout = (LinearLayout) LayoutInflater.from(this).inflate(R.layout.activity_main3, null);
setContentView(mainLayout);
//獲取layoutInflater
LayoutInflater layoutInflater = LayoutInflater.from(this);
//解析出buttonLayout
View buttonLayout = layoutInflater.inflate(R.layout.inlate_button, null);
//將buttonLayout添加到mainLayout中
mainLayout.addView(buttonLayout);
Log.d("result", "inflate返回的view為:" + buttonLayout.toString());
}
來看看實際效果:我曹???干啥呢?設(shè)置了沒用啊?還是一樣。先別慌別流汗。我們仔細(xì)來回憶一下之前分析的代碼順便來求證一下前面的分析是否正確。
首先我們在調(diào)用inflate()方法的時候傳入root為null,因此在inflate()方法中不會執(zhí)行之前分析的一系列代碼,只會執(zhí)行下圖(就是上文圖,拿過來避免翻上去看)中1,5處代碼,因此實際上我們填寫的參數(shù)都沒有被設(shè)置,因此我們的按鈕不管怎么設(shè)置都是默認(rèn)狀態(tài)。接下來我們改一下調(diào)用代碼,將我們的mainLayout作為root傳入,attachToRoot傳為false(其他代碼不變):
//修改前代碼
View buttonLayout = layoutInflater.inflate(R.layout.inlate_button, null);
//修改后前代碼
View buttonLayout = layoutInflater.inflate(R.layout.inlate_button, mainLayout, false);
運行一下。
有效果啦!(請原諒400dp效果太明顯了。。。)。這是為什么呢?(來來來,科普一下)其實安卓中l(wèi)ayout_width和layout_height是用于設(shè)置View在布局中的大小的(這種官方話我聽著都別扭)。
簡單來說,就是首先View必須存在于一個父布局中l(wèi)ayout_width等參數(shù)才會生效(給一個小孩子一包糖說你可以吃,去找你爹給你打開,如果孩子連爹都沒有,空有一包糖也打不開呀)。
例如如果將layout_width設(shè)置成match_parent表示讓View的寬度填充滿父布局,如果設(shè)置成wrap_content表示讓View的寬度在父布局中的寬度剛好可以包含自身內(nèi)容,如果設(shè)置成具體的數(shù)值則View的寬度會變成相應(yīng)的數(shù)值。
接下來我們再改代碼,把attachToRoot改為true傳入看看(其他代碼不變):
//修改前代碼
View buttonLayout = layoutInflater.inflate(R.layout.inlate_button, mainLayout, false);
mainLayout.addView(buttonLayout);
//修改后前代碼
View buttonLayout = layoutInflater.inflate(R.layout.inlate_button, mainLayout, true);
mainLayout.addView(buttonLayout);
運行,等待。
emmm...
emmm…
emm….
至此本文的分析就差不多結(jié)束啦,其中還有一些可以深究的地方如果感興趣的同學(xué)可以繼續(xù)深入探索!
ps:還是老話,本人萌新,如果文中有錯誤或者模糊的地方,希望大家能多多指正,還望多多包涵。
再ps:寫本文和分析源碼的過程中也參考了郭霖大神的文章,也算是站在前輩的肩膀上繼續(xù)進(jìn)行自己的探索把。附上鏈接:
http://blog.csdn.net/guolin_blog/article/details/12921889