有段時間沒寫博客了朦蕴,感覺都有些生疏了呢。最近繁忙的工作終于告一段落弟头,又有時間寫文章了吩抓,接下來還會繼續(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
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
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é)論是否正確。
- 如果root為null寸谜,attachToRoot將失去作用竟稳,設(shè)置任何值都沒有意義。
- 如果root不為null熊痴,attachToRoot設(shè)為true他爸,則會在加載的布局文件的最外層再嵌套一層root布局蝉娜。
- 如果root不為null,attachToRoot設(shè)為false移国,則root參數(shù)失去作用衙解。
- 在不設(shè)置attachToRoot參數(shù)的情況下,如果root不為null梁棠,attachToRoot參數(shù)默認(rèn)為true。
好了,現(xiàn)在對LayoutInflater的工作原理和流程也搞清楚了惜论,你該滿足了吧。額止喷。馆类。。弹谁。還嫌這個例子中的按鈕看起來有點小乾巧,想要調(diào)大一些?那簡單的呀预愤,修改button_layout.xml中的代碼沟于,如下所示:
[html] view plaincopy
<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
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(二) 匹中。