Android 老生常談LayoutInflater的新認(rèn)知
現(xiàn)在看我文章的多數(shù)是一些老Android了,相信每個(gè)人使用起LayoutInflater都是家常便飯,信手拈來(lái)。
但即使是這樣,我仍然覺(jué)得這個(gè)知識(shí)點(diǎn)有可以分析的地方,看完之后或許你對(duì)LayoutInflater又會(huì)有一些新的認(rèn)識(shí)。
首先概括一下LayoutInflater是用來(lái)做什么的。
我們都知道,在開(kāi)發(fā)Android應(yīng)用程序的時(shí)候,編寫(xiě)布局基本都是通過(guò)xml文件來(lái)編寫(xiě)的。當(dāng)然你也完全可以在代碼中純手寫(xiě)布局,但是寫(xiě)過(guò)的人都清楚,這樣編寫(xiě)布局會(huì)非常麻煩。
那么通過(guò)xml編寫(xiě)的布局文件是如何轉(zhuǎn)換成Android中的一個(gè)View對(duì)象從而顯示在應(yīng)用程序當(dāng)中的呢?這就是LayoutInflater的作用了。
簡(jiǎn)單來(lái)說(shuō),LayoutInflater的工作就是將使用xml文件編寫(xiě)的布局轉(zhuǎn)換成Android里的View對(duì)象,并且這也是Android中將xml布局轉(zhuǎn)換成View的唯一方式。
可能有些朋友會(huì)說(shuō),不對(duì)啊,我平時(shí)也沒(méi)怎么用過(guò)LayoutInflater,xml布局轉(zhuǎn)換成View不是調(diào)用Activity里的setContentView()方法就可以了嗎?
這是因?yàn)锳ndroid SDK在上層給我們做了一些很好的封裝,讓開(kāi)發(fā)工作變得更加簡(jiǎn)單。如果你打開(kāi)setContentView()方法的源碼去了解一下,就會(huì)發(fā)現(xiàn)它的底層同樣也是使用的LayoutInflater:
@Override public void setContentView(int resId) { ensureSubDecor(); ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); LayoutInflater.from(mContext).inflate(resId, contentParent); mAppCompatWindowCallback.getWrapped().onContentChanged(); }
那么LayoutInflater又是如何將一個(gè)xml布局轉(zhuǎn)換成一個(gè)View對(duì)象的呢?
這當(dāng)然是一個(gè)非常復(fù)雜的過(guò)程,但是如果簡(jiǎn)要概括的話,最重要的無(wú)非就是兩步:
- 通過(guò)解析器來(lái)將xml文件中的內(nèi)容解析出來(lái)。
- 使用反射將解析出來(lái)的元素創(chuàng)建成View對(duì)象。
這里我不想在文章中帶著大家一步步追源碼,這樣文章看起來(lái)可能會(huì)又累又枯燥,因此我就只貼出一些我認(rèn)為比較關(guān)鍵的代碼。
解析xml文件內(nèi)容的代碼片段:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) { ... XmlResourceParser parser = res.getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } }
可以看到,這里獲取到了一個(gè)XmlResourceParser對(duì)象,用于對(duì)xml文件進(jìn)行解析。由于具體的解析規(guī)則過(guò)于復(fù)雜,我們就不跟進(jìn)去看了。
使用反射創(chuàng)建View對(duì)象的代碼片段:
public final View createView(@NonNull Context viewContext, @NonNull String name, @Nullable String prefix, @Nullable AttributeSet attrs) throws ClassNotFoundException, InflateException { ... if (constructor == null) { // Class not found in the cache, see if it's real, and try to add it clazz = Class.forName(prefix != null ? (prefix + name) : name, false, mContext.getClassLoader()).asSubclass(View.class); constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); sConstructorMap.put(name, constructor); } ... try { final View view = constructor.newInstance(args); if (view instanceof ViewStub) { // Use the same context when inflating ViewStub later. final ViewStub viewStub = (ViewStub) view; viewStub.setLayoutInflater(cloneInContext((Context) args[0])); } return view; } ... }
看到這里,我們就將LayoutInflater大體的工作原理基本了解了。
但是正如前面所說(shuō),本篇文章并不是要帶著大家去讀源碼的,而是想要從用法層面對(duì)LayoutInflater有些新的理解。
那么LayoutInflater最常見(jiàn)的用法如下:
View view = LayoutInflater.from(context).inflate(resourceId, parent, false);
這段代碼的意思是,首先調(diào)用LayoutInflater的from()方法去獲取一個(gè)LayoutInflater的實(shí)例,然后再調(diào)用它的inflate()方法去解析并加載一個(gè)布局,從而轉(zhuǎn)換成一個(gè)View對(duì)象并返回。
然而我認(rèn)為這段代碼對(duì)于新手來(lái)說(shuō)卻及其不友好,甚至對(duì)于很多的老手來(lái)說(shuō)也是。
我們來(lái)看一下inflate()方法的參數(shù)定義:
public View inflate(int resource, @Nullable ViewGroup root, boolean attachToRoot) { ... }
inflate()方法接收3個(gè)參數(shù),第一個(gè)參數(shù)resource還比較好理解,就是我們要解析加載的xml文件的資源id。第二個(gè)參數(shù)root,和第三個(gè)參數(shù)attachToRoot是什么意思?可能即使不少做過(guò)多年Android開(kāi)發(fā)的程序員也未必能解釋得清楚。
而這段代碼在我們使用RecyclerView,或者使用Fragment時(shí)都是一定會(huì)用到的。我在寫(xiě)《第一行代碼》時(shí)由于在很早的章節(jié)就要講RecyclerView的用法,但是卻又感覺(jué)很難向初學(xué)者解釋清楚LayoutInflater的相關(guān)內(nèi)容,所以我一直都覺(jué)得這塊內(nèi)容沒(méi)有講好。只能先用死記硬背的方式,暫時(shí)就記著這部分代碼必須這么寫(xiě)。
而今天,我希望能將LayoutInflater真正講講清楚。
我們知道,Android的布局結(jié)構(gòu)是一種樹(shù)狀結(jié)構(gòu)。每個(gè)布局都可以包含若干個(gè)子布局,每個(gè)子布局又可以繼續(xù)包含子布局,以此構(gòu)建出任意樣式的View呈現(xiàn)給用戶。
因此,我們大致可以明白,每個(gè)布局它都是要有一個(gè)父布局的。
這也是inflate()方法第二個(gè)參數(shù)root的作用,就是給當(dāng)前要解析加載的xml布局指定一個(gè)父布局。
那么一個(gè)布局可不可以沒(méi)有父布局呢?當(dāng)然也是可以的,這也是為什么root參數(shù)被標(biāo)為@Nullable的原因。
但是如果我們inflate出來(lái)了一個(gè)沒(méi)有父布局的布局,又該如何去展示它呢?那自然是沒(méi)有辦法去展示的,所以只能后面再用addView的方式將它添加到某個(gè)現(xiàn)有的布局下面。又或者你inflate出來(lái)的布局就是個(gè)頂層布局,所以它不需要有父布局。但是這些場(chǎng)景都比較少見(jiàn),因此大多數(shù)情況下,我們?cè)谑褂肔ayoutInflater的inflate()方法時(shí)都是要指定父布局的。
另外,如果不為inflate出來(lái)的布局指定父布局,還會(huì)出現(xiàn)另外一種問(wèn)題,我們通過(guò)一個(gè)例子來(lái)講解一下。
這里我們定義一個(gè)button_layout.xml布局文件,代碼如下所示:
<?xml version="1.0" encoding="utf-8"?> <Button xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" />
這個(gè)布局文件非常簡(jiǎn)單,里面只有一個(gè)按鈕。
接下來(lái)我們使用LayoutInflater來(lái)加載這個(gè)布局文件,并將它添加到一個(gè)現(xiàn)有的布局當(dāng)中:
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); LinearLayout mainLayout = (LinearLayout) findViewById(R.id.main_layout); View buttonLayout = LayoutInflater.from(this).inflate(R.layout.button_layout, null); mainLayout.addView(buttonLayout); } }
可以看到,這里我們并沒(méi)有給button_layout指定父布局,而是傳入了一個(gè)null。當(dāng)?shù)诙€(gè)參數(shù)傳入null時(shí),第三個(gè)參數(shù)就沒(méi)有意義了,因此可以不用指定。
但是前面也說(shuō)了,一個(gè)布局如果沒(méi)有父布局的話沒(méi)辦法顯示出來(lái)呀,所以我們又使用了addView()方法將它添加到了一個(gè)現(xiàn)有布局當(dāng)中。
代碼就是這么簡(jiǎn)單,現(xiàn)在我們可以運(yùn)行一下程序,效果如下圖所示:
看上去好像沒(méi)啥問(wèn)題,按鈕已經(jīng)可以正常顯示出來(lái)了,說(shuō)明button_layout.xml這個(gè)布局確實(shí)成功加載出來(lái)并且添加到現(xiàn)有的布局當(dāng)中了。
但是如果你嘗試去調(diào)整一下按鈕的大小,你會(huì)發(fā)現(xiàn)不管你如何調(diào)整,按鈕的大小都是不會(huì)變的:
<?xml version="1.0" encoding="utf-8"?> <Button xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="300dp" android:layout_height="100dp" android:text="Button" />
這里我們將按鈕的寬高指定成了300dp,高度指定成了100dp,重新運(yùn)行程序界面毫無(wú)變化。
為什么會(huì)出現(xiàn)這樣的情況呢?
其實(shí)這里不管你將Button的layout_width和layout_height的值修改成多少,都不會(huì)有任何效果的,因?yàn)檫@兩個(gè)值現(xiàn)在已經(jīng)完全失去了作用。平時(shí)我們經(jīng)常使用layout_width和layout_height來(lái)設(shè)置View的大小,并且一直都能正常工作,就好像這兩個(gè)屬性確實(shí)是用于設(shè)置View的大小的。
而實(shí)際上則不然,它們其實(shí)是用于設(shè)置View在布局中的大小的,也就是說(shuō),首先View必須存在于一個(gè)布局中才行。這也是為什么這兩個(gè)屬性叫作layout_width和layout_height,而不是width和height。
而我們因?yàn)樵谑褂肔ayoutInflater加載button_layout.xml這個(gè)布局時(shí)并沒(méi)有為它指定父布局,因此這里layout_width和layout_height屬性就都失去了作用。更準(zhǔn)確點(diǎn)來(lái)講,所有以layout_開(kāi)頭的屬性都會(huì)失去作用。
現(xiàn)在我們將代碼進(jìn)行如下修改:
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); LinearLayout mainLayout = (LinearLayout) findViewById(R.id.main_layout); View buttonLayout = LayoutInflater.from(this).inflate(R.layout.button_layout, mainLayout, false); mainLayout.addView(buttonLayout); } }
可以看到,這里將inflate()方法的第二個(gè)參數(shù)指定成了mainLayout。也就是說(shuō),我們?yōu)閎utton_layout.xml這個(gè)布局指定了一個(gè)父布局。這樣的話,layout_width和layout_height屬性就可以生效了。
重新運(yùn)行程序,效果如下圖所示:
到這里為止,我們就將inflate()方法的第二個(gè)參數(shù)root的作用解釋得非常清楚了。那么還有一個(gè)問(wèn)題就是,第三個(gè)參數(shù)attachToRoot又是什么意思呢?
注意觀察上述代碼,我們將第二個(gè)參數(shù)指定成mainLayout的同時(shí),將第三個(gè)參數(shù)指定成了false。如果你嘗試將第三個(gè)參數(shù)指定成true,然后重新運(yùn)行代碼,程序?qū)?huì)直接崩潰。崩潰信息如下:
這個(gè)崩潰信息是在說(shuō),我們正在添加一個(gè)子View,但是這個(gè)子View已經(jīng)有父布局了,需要讓父布局先調(diào)用removeView()移除子View后才能添加。
為什么修改第三個(gè)參數(shù)之后會(huì)出現(xiàn)這樣的錯(cuò)誤呢?我們現(xiàn)在就來(lái)分析一下。
首先關(guān)注一下第三個(gè)參數(shù)的名字是什么,attachToRoot。從字面意思上看,是在問(wèn)我們是否要添加到root上面。那么root是什么呢?再次觀察inflate()方法的定義,你會(huì)發(fā)現(xiàn)第二個(gè)參數(shù)不就是root嗎?
public View inflate(int resource, @Nullable ViewGroup root, boolean attachToRoot) { ... }
也就是說(shuō),attachToRoot的意思,就是在問(wèn)我們要不要將當(dāng)前加載的xml布局添加到第二個(gè)參數(shù)傳入的父布局上面。如果傳入true,那么就意味著會(huì)添加,傳入false就表示不會(huì)添加。
所以在剛才的代碼當(dāng)中,我們一開(kāi)始在inflate()方法的第三個(gè)參數(shù)中傳入false,那么button_layout.xml布局是不會(huì)被添加到mainLayout當(dāng)中的,我們后面就可以手動(dòng)調(diào)用addView()方法將它添加到mainLayout當(dāng)中。
而如果將第三個(gè)參數(shù)改成true,就表示button_layout.xml布局已經(jīng)自動(dòng)被添加到mainLayout當(dāng)中了,此時(shí)再去調(diào)用一遍addView()方法,發(fā)現(xiàn)button_layout.xml已經(jīng)有父布局了,自然就會(huì)拋出上面的異常。
經(jīng)過(guò)這樣的解釋之后,你是否就對(duì)inflate()方法中的每一個(gè)參數(shù)的作用都理解清楚了呢?
其實(shí)理解到了這里,我們可以回過(guò)頭來(lái)再去看一看過(guò)去寫(xiě)的代碼。比如說(shuō)大家肯定都用過(guò)Fragment,在Fragment中加載一個(gè)布局我們通常都會(huì)這么寫(xiě):
public class MyFragment extends Fragment { @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_layout, container, false); } }
不知道你過(guò)去有沒(méi)有想過(guò),為什么這里inflate()方法的最后一個(gè)參數(shù)一定要傳入false?
那么現(xiàn)在可以想一想了。觀察一下Fragment的相關(guān)源碼,你會(huì)發(fā)現(xiàn)它會(huì)將我們?cè)趏nCreateView()方法中返回的View添加到一個(gè)Container當(dāng)中:
void addViewToContainer() { // Ensure that our new Fragment is placed in the right index // based on its relative position to Fragments already in the // same container int index = mFragmentStore.findFragmentIndexInContainer(mFragment); mFragment.mContainer.addView(mFragment.mView, index); }
這個(gè)情況和我們剛才的例子非常類似,也就是說(shuō),后續(xù)Fragment自己會(huì)有一個(gè)addView的操作,如果我們將inflate()方法的第三個(gè)參數(shù)傳入true,那么就會(huì)直接將inflate出來(lái)的布局添加到父布局當(dāng)中。這樣后面再次addView的時(shí)候就會(huì)發(fā)現(xiàn)它已經(jīng)有一個(gè)父布局了,從而拋出與上面同樣的崩潰信息。
不信的話你可以自己動(dòng)手試一試。
除了Fragment之外,RecyclerView中對(duì)于LayoutInflater的用法也是基于一模一樣的原因,這里就不再展開(kāi)討論了。
希望通過(guò)閱讀本文之后,你對(duì)LayoutInflater又能有一些新的認(rèn)識(shí)。
到此這篇關(guān)于Android 老生常談LayoutInflater的新認(rèn)知的文章就介紹到這了,更多相關(guān)Android LayoutInflater內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
一款非常簡(jiǎn)單酷炫的LoadingView動(dòng)畫(huà)效果
這篇文章主要為大家詳細(xì)介紹了一款非常簡(jiǎn)單酷炫的LoadingView動(dòng)畫(huà)效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08Android計(jì)時(shí)器的三種實(shí)現(xiàn)方式(Chronometer、Timer、handler)
這篇文章主要介紹了Android計(jì)時(shí)器的三種實(shí)現(xiàn)方式,Chronometer、Timer、handler計(jì)時(shí)器的實(shí)現(xiàn)方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11Android使用Sensor感應(yīng)器獲取用戶移動(dòng)方向(指南針原理)
這篇文章主要介紹了Android使用Sensor感應(yīng)器獲取用戶移動(dòng)方向的方法,實(shí)例分析了指南針原理極其應(yīng)用,需要的朋友可以參考下2015-12-12AndroidStudio Gradle基于友盟的多渠道打包方法
這篇文章主要介紹了AndroidStudio Gradle基于友盟的多渠道打包方法,需要的朋友可以參考下2017-09-09Android編程使用加速度傳感器實(shí)現(xiàn)搖一搖功能及優(yōu)化的方法詳解
這篇文章主要介紹了Android編程使用加速度傳感器實(shí)現(xiàn)搖一搖功能及優(yōu)化的方法,結(jié)合實(shí)例形式分析了Android傳感器的調(diào)用方法、參數(shù)含義及具體使用技巧,需要的朋友可以參考下2017-08-08Android自定義view之太極圖的實(shí)現(xiàn)教程
這篇文章主要給大家介紹了關(guān)于Android自定義view之太極圖的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01Android Flutter實(shí)現(xiàn)彈幕效果
這篇文章主要為大家詳細(xì)介紹如何利用Android FLutter實(shí)現(xiàn)彈幕效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06Android studio 引用aar 進(jìn)行java開(kāi)發(fā)的操作步驟
這篇文章主要介紹了Android studio 引用aar 進(jìn)行java開(kāi)發(fā)的操作步驟,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-09-09Android RecyclerView的Item點(diǎn)擊事件實(shí)現(xiàn)整理
這篇文章主要介紹了Android RecyclerView的Item點(diǎn)擊事件實(shí)現(xiàn)整理的相關(guān)資料,需要的朋友可以參考下2017-01-01