Android LayoutInflater原理分析

有段時間沒寫博客了朦蕴,感覺都有些生疏了呢。最近繁忙的工作終于告一段落弟头,又有時間寫文章了吩抓,接下來還會繼續(xù)堅持每一周篇的節(jié)奏。
有不少朋友跟我反應(yīng)赴恨,都希望我可以寫一篇關(guān)于View的文章疹娶,講一講View的工作原理以及自定義View的方法。沒錯伦连,承諾過的文章我是一定要兌現(xiàn)的雨饺,而且在View這個話題上我還準(zhǔn)備多寫幾篇钳垮,盡量能將這個知識點講得透徹一些。那么今天就從LayoutInflater開始講起吧额港。
相信接觸Android久一點的朋友對于LayoutInflater一定不會陌生饺窿,都會知道它主要是用于加載布局的。而剛接觸Android的朋友可能對LayoutInflater不怎么熟悉移斩,因為加載布局的任務(wù)通常都是在Activity中調(diào)用setContentView()方法來完成的肚医。其實setContentView()方法的內(nèi)部也是使用LayoutInflater來加載布局的,只不過這部分源碼是internal的向瓷,不太容易查看到忍宋。那么今天我們就來把LayoutInflater的工作流程仔細(xì)地剖析一遍,也許還能解決掉某些困擾你心頭多年的疑惑风罩。
先來看一下LayoutInflater的基本用法吧,它的用法非常簡單舵稠,首先需要獲取到LayoutInflater的實例超升,有兩種方法可以獲取到,第一種寫法如下:
[java] view plaincopy

[
派生到我的代碼'(https://code.csdn.net/assets/ico_fork.svg)
派生到我的代碼'(https://code.csdn.net/assets/ico_fork.svg)

LayoutInflater layoutInflater = LayoutInflater.from(context);

當(dāng)然哺徊,還有另外一種寫法也可以完成同樣的效果:[java] view plaincopy

派生到我的代碼片
派生到我的代碼片

LayoutInflater layoutInflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

其實第一種就是第二種的簡單寫法室琢,只是Android給我們做了一下封裝而已。得到了LayoutInflater的實例之后就可以調(diào)用它的inflate()方法來加載布局了落追,如下所示:[java] view plaincopy

派生到我的代碼片
派生到我的代碼片

layoutInflater.inflate(resourceId, root);

inflate()方法一般接收兩個參數(shù)盈滴,第一個參數(shù)就是要加載的布局id,第二個參數(shù)是指給該布局的外部再嵌套一層父布局轿钠,如果不需要就直接傳null巢钓。這樣就成功成功創(chuàng)建了一個布局的實例,之后再將它添加到指定的位置就可以顯示出來了疗垛。
下面我們就通過一個非常簡單的小例子症汹,來更加直觀地看一下LayoutInflater的用法。比如說當(dāng)前有一個項目贷腕,其中MainActivity對應(yīng)的布局文件叫做activity_main.xml背镇,代碼如下所示:
[html] view plaincopy

派生到我的代碼片
派生到我的代碼片

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent" >

</LinearLayout>

這個布局文件的內(nèi)容非常簡單,只有一個空的LinearLayout泽裳,里面什么控件都沒有瞒斩,因此界面上應(yīng)該不會顯示任何東西。
那么接下來我們再定義一個布局文件涮总,給它取名為button_layout.xml胸囱,代碼如下所示:
[html] view plaincopy

派生到我的代碼片
派生到我的代碼片

<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button" >

</Button>

這個布局文件也非常簡單,只有一個Button按鈕而已∑俟#現(xiàn)在我們要想辦法旺矾,如何通過LayoutInflater來將button_layout這個布局添加到主布局文件的LinearLayout中蔑鹦。根據(jù)剛剛介紹的用法,修改MainActivity中的代碼箕宙,如下所示:[java] view plaincopy

派生到我的代碼片
派生到我的代碼片

public class MainActivity extends Activity {

private LinearLayout mainLayout;  

@Override  
protected void onCreate(Bundle savedInstanceState) {  
    super.onCreate(savedInstanceState);  
    setContentView(R.layout.activity_main);  
    mainLayout = (LinearLayout) findViewById(R.id.main_layout);  
    LayoutInflater layoutInflater = LayoutInflater.from(this);  
    View buttonLayout = layoutInflater.inflate(R.layout.button_layout, null);  
    mainLayout.addView(buttonLayout);  
}  

}

可以看到嚎朽,這里先是獲取到了LayoutInflater的實例,然后調(diào)用它的inflate()方法來加載button_layout這個布局柬帕,最后調(diào)用LinearLayout的addView()方法將它添加到LinearLayout中哟忍。
現(xiàn)在可以運行一下程序,結(jié)果如下圖所示:


Button在界面上顯示出來了陷寝!說明我們確實是借助LayoutInflater成功將button_layout這個布局添加到LinearLayout中了锅很。LayoutInflater技術(shù)廣泛應(yīng)用于需要動態(tài)添加View的時候,比如在ScrollView和ListView中凤跑,經(jīng)常都可以看到LayoutInflater的身影爆安。
當(dāng)然,僅僅只是介紹了如何使用LayoutInflater顯然是遠(yuǎn)遠(yuǎn)無法滿足大家的求知欲的仔引,知其然也要知其所以然扔仓,接下來我們就從源碼的角度上看一看LayoutInflater到底是如何工作的。
不管你是使用的哪個inflate()方法的重載咖耘,最終都會輾轉(zhuǎn)調(diào)用到LayoutInflater的如下代碼中:
[java] view plaincopy
在CODE上查看代碼片
在CODE上查看代碼片
派生到我的代碼片
派生到我的代碼片

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
final AttributeSet attrs = Xml.asAttributeSet(parser);
mConstructorArgs[0] = mContext;
View result = root;
try {
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
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, attrs);
} else {
View temp = createViewFromTag(name, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
temp.setLayoutParams(params);
}
}
rInflate(parser, temp, attrs);
if (root != null && attachToRoot) {
root.addView(temp, params);
}
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
InflateException ex = new InflateException(e.getMessage());
ex.initCause(e);
throw ex;
} catch (IOException e) {
InflateException ex = new InflateException(
parser.getPositionDescription()
+ ": " + e.getMessage());
ex.initCause(e);
throw ex;
}
return result;
}
}

從這里我們就可以清楚地看出翘簇,LayoutInflater其實就是使用Android提供的pull解析方式來解析布局文件的。不熟悉pull解析方式的朋友可以網(wǎng)上搜一下儿倒,教程很多版保,我就不細(xì)講了,這里我們注意看下第23行夫否,調(diào)用了createViewFromTag()這個方法彻犁,并把節(jié)點名和參數(shù)傳了進(jìn)去』舜龋看到這個方法名袖裕,我們就應(yīng)該能猜到,它是用于根據(jù)節(jié)點名來創(chuàng)建View對象的溉瓶。確實如此急鳄,在createViewFromTag()方法的內(nèi)部又會去調(diào)用createView()方法,然后使用反射的方式創(chuàng)建出View的實例并返回堰酿。
當(dāng)然疾宏,這里只是創(chuàng)建出了一個根布局的實例而已,接下來會在第31行調(diào)用rInflate()方法來循環(huán)遍歷這個根布局下的子元素触创,代碼如下所示:
[java] view plaincopy

派生到我的代碼片
派生到我的代碼片

private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs)
throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
final View view = createViewFromTag(name, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflate(parser, view, attrs);
viewGroup.addView(view, params);
}
}
parent.onFinishInflate();
}

可以看到坎藐,在第21行同樣是createViewFromTag()方法來創(chuàng)建View的實例,然后還會在第24行遞歸調(diào)用rInflate()方法來查找這個View下的子元素,每次遞歸完成后則將這個View添加到父布局當(dāng)中岩馍。
這樣的話碉咆,把整個布局文件都解析完成后就形成了一個完整的DOM結(jié)構(gòu),最終會把最頂層的根布局返回蛀恩,至此inflate()過程全部結(jié)束疫铜。
比較細(xì)心的朋友也許會注意到,inflate()方法還有個接收三個參數(shù)的方法重載双谆,結(jié)構(gòu)如下:
[java] view plaincopy

派生到我的代碼片
派生到我的代碼片

inflate(int resource, ViewGroup root, boolean attachToRoot)

那么這第三個參數(shù)attachToRoot又是什么意思呢壳咕?其實如果你仔細(xì)去閱讀上面的源碼應(yīng)該可以自己分析出答案,這里我先將結(jié)論說一下吧顽馋,感興趣的朋友可以再閱讀一下源碼谓厘,校驗我的結(jié)論是否正確。

  1. 如果root為null寸谜,attachToRoot將失去作用竟稳,設(shè)置任何值都沒有意義。
  2. 如果root不為null熊痴,attachToRoot設(shè)為true他爸,則會在加載的布局文件的最外層再嵌套一層root布局蝉娜。
  3. 如果root不為null,attachToRoot設(shè)為false移国,則root參數(shù)失去作用衙解。
  4. 在不設(shè)置attachToRoot參數(shù)的情況下,如果root不為null梁棠,attachToRoot參數(shù)默認(rèn)為true。
    好了,現(xiàn)在對LayoutInflater的工作原理和流程也搞清楚了惜论,你該滿足了吧。額止喷。馆类。。弹谁。還嫌這個例子中的按鈕看起來有點小乾巧,想要調(diào)大一些?那簡單的呀预愤,修改button_layout.xml中的代碼沟于,如下所示:
    [html] view plaincopy
    在CODE上查看代碼片
    在CODE上查看代碼片
    派生到我的代碼片
    派生到我的代碼片

<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="300dp"
android:layout_height="80dp"
android:text="Button" >

</Button>

這里我們將按鈕的寬度改成300dp,高度改成80dp植康,這樣夠大了吧旷太?現(xiàn)在重新運行一下程序來觀察效果。咦?怎么按鈕還是原來的大小供璧,沒有任何變化存崖!是不是按鈕仍然不夠大,再改大一點呢睡毒?還是沒有用来惧!
其實這里不管你將Button的layout_width和layout_height的值修改成多少,都不會有任何效果的吕嘀,因為這兩個值現(xiàn)在已經(jīng)完全失去了作用违寞。平時我們經(jīng)常使用layout_width和layout_height來設(shè)置View的大小,并且一直都能正常工作偶房,就好像這兩個屬性確實是用于設(shè)置View的大小的趁曼。而實際上則不然,它們其實是用于設(shè)置View在布局中的大小的棕洋,也就是說挡闰,首先View必須存在于一個布局中,之后如果將layout_width設(shè)置成match_parent表示讓View的寬度填充滿布局掰盘,如果設(shè)置成wrap_content表示讓View的寬度剛好可以包含其內(nèi)容摄悯,如果設(shè)置成具體的數(shù)值則View的寬度會變成相應(yīng)的數(shù)值。這也是為什么這兩個屬性叫作layout_width和layout_height愧捕,而不是width和height奢驯。
再來看一下我們的button_layout.xml吧,很明顯Button這個控件目前不存在于任何布局當(dāng)中次绘,所以layout_width和layout_height這兩個屬性理所當(dāng)然沒有任何作用瘪阁。那么怎樣修改才能讓按鈕的大小改變呢?解決方法其實有很多種邮偎,最簡單的方式就是在Button的外面再嵌套一層布局管跺,如下所示:
[html] view plaincopy

派生到我的代碼片
派生到我的代碼片

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >

<Button  
    android:layout_width="300dp"  
    android:layout_height="80dp"  
    android:text="Button" >  
</Button>  

</RelativeLayout>

可以看到,這里我們又加入了一個RelativeLayout禾进,此時的Button存在與RelativeLayout之中豁跑,layout_width和layout_height屬性也就有作用了。當(dāng)然泻云,處于最外層的RelativeLayout艇拍,它的layout_width和layout_height則會失去作用。現(xiàn)在重新運行一下程序宠纯,結(jié)果如下圖所示:


OK淑倾!按鈕的終于可以變大了,這下總算是滿足大家的要求了吧征椒。
看到這里娇哆,也許有些朋友心中會有一個巨大的疑惑。不對呀!平時在Activity中指定布局文件的時候碍讨,最外層的那個布局是可以指定大小的呀治力,layout_width和layout_height都是有作用的。確實勃黍,這主要是因為宵统,在setContentView()方法中,Android會自動在布局文件的最外層再嵌套一個FrameLayout覆获,所以layout_width和layout_height屬性才會有效果马澈。那么我們來證實一下吧,修改MainActivity中的代碼弄息,如下所示:
[java] view plaincopy
在CODE上查看代碼片
在CODE上查看代碼片
派生到我的代碼片
派生到我的代碼片

public class MainActivity extends Activity {

private LinearLayout mainLayout;  

@Override  
protected void onCreate(Bundle savedInstanceState) {  
    super.onCreate(savedInstanceState);  
    setContentView(R.layout.activity_main);  
    mainLayout = (LinearLayout) findViewById(R.id.main_layout);  
    ViewParent viewParent = mainLayout.getParent();  
    Log.d("TAG", "the parent of mainLayout is " + viewParent);  
}  

}

可以看到痊班,這里通過findViewById()方法,拿到了activity_main布局中最外層的LinearLayout對象摹量,然后調(diào)用它的getParent()方法獲取它的父布局涤伐,再通過Log打印出來。現(xiàn)在重新運行一下程序缨称,結(jié)果如下圖所示:


非常正確凝果!LinearLayout的父布局確實是一個FrameLayout,而這個FrameLayout就是由系統(tǒng)自動幫我們添加上的睦尽。
說到這里器净,雖然setContentView()方法大家都會用,但實際上Android界面顯示的原理要比我們所看到的東西復(fù)雜得多当凡。任何一個Activity中顯示的界面其實主要都由兩部分組成山害,標(biāo)題欄和內(nèi)容布局。標(biāo)題欄就是在很多界面頂部顯示的那部分內(nèi)容宁玫,比如剛剛我們的那個例子當(dāng)中就有標(biāo)題欄粗恢,可以在代碼中控制讓它是否顯示柑晒。而內(nèi)容布局就是一個FrameLayout欧瘪,這個布局的id叫作content,我們調(diào)用setContentView()方法時所傳入的布局其實就是放到這個FrameLayout中的匙赞,這也是為什么這個方法名叫作setContentView()佛掖,而不是叫setView()。
最后再附上一張Activity窗口的組成圖吧涌庭,以便于大家更加直觀地理解:

好了芥被,今天就講到這里了,支持的坐榆、吐槽的拴魄、有疑問的、以及打醬油的路過朋友盡管留言吧 v 感興趣的朋友可以繼續(xù)閱讀 Android視圖繪制流程完全解析,帶你一步步深入了解View(二) 匹中。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末夏漱,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子顶捷,更是在濱河造成了極大的恐慌挂绰,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件服赎,死亡現(xiàn)場離奇詭異葵蒂,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)重虑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進(jìn)店門践付,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人嚎尤,你說我怎么就攤上這事荔仁。” “怎么了芽死?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵乏梁,是天一觀的道長。 經(jīng)常有香客問我关贵,道長遇骑,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任揖曾,我火速辦了婚禮落萎,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘炭剪。我一直安慰自己练链,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布奴拦。 她就那樣靜靜地躺著媒鼓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪错妖。 梳的紋絲不亂的頭發(fā)上绿鸣,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天,我揣著相機(jī)與錄音暂氯,去河邊找鬼潮模。 笑死,一個胖子當(dāng)著我的面吹牛痴施,可吹牛的內(nèi)容都是我干的擎厢。 我是一名探鬼主播究流,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼动遭!你這毒婦竟也來了梯嗽?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤沽损,失蹤者是張志新(化名)和其女友劉穎灯节,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绵估,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡炎疆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了国裳。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片形入。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖缝左,靈堂內(nèi)的尸體忽然破棺而出亿遂,到底是詐尸還是另有隱情,我是刑警寧澤渺杉,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布蛇数,位于F島的核電站,受9級特大地震影響是越,放射性物質(zhì)發(fā)生泄漏耳舅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一倚评、第九天 我趴在偏房一處隱蔽的房頂上張望浦徊。 院中可真熱鬧,春花似錦天梧、人聲如沸盔性。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽冕香。三九已至,卻和暖如春敷燎,著一層夾襖步出監(jiān)牢的瞬間暂筝,已是汗流浹背箩言。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工硬贯, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人陨收。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓饭豹,卻偏偏與公主長得像鸵赖,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子拄衰,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內(nèi)容