<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
現在看我文章的多數是一些老Android了,相信每個人使用起LayoutInflater都是家常便飯,信手拈來。
但即使是這樣,我仍然覺得這個知識點有可以分析的地方,看完之後或許你對LayoutInflater又會有一些新的認識。
首先概括一下LayoutInflater是用來做什麼的。
我們都知道,在開發Android應用程式的時候,編寫佈局基本都是通過xml檔案來編寫的。當然你也完全可以在程式碼中純手寫佈局,但是寫過的人都清楚,這樣編寫佈局會非常麻煩。
那麼通過xml編寫的佈局檔案是如何轉換成Android中的一個View物件從而顯示在應用程式當中的呢?這就是LayoutInflater的作用了。
簡單來說,LayoutInflater的工作就是將使用xml檔案編寫的佈局轉換成Android裡的View物件,並且這也是Android中將xml佈局轉換成View的唯一方式。
可能有些朋友會說,不對啊,我平時也沒怎麼用過LayoutInflater,xml佈局轉換成View不是呼叫Activity裡的setContentView()方法就可以了嗎?
這是因為Android SDK在上層給我們做了一些很好的封裝,讓開發工作變得更加簡單。如果你開啟setContentView()方法的原始碼去了解一下,就會發現它的底層同樣也是使用的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又是如何將一個xml佈局轉換成一個View物件的呢?
這當然是一個非常複雜的過程,但是如果簡要概括的話,最重要的無非就是兩步:
這裡我不想在文章中帶著大家一步步追原始碼,這樣文章看起來可能會又累又枯燥,因此我就只貼出一些我認為比較關鍵的程式碼。
解析xml檔案內容的程式碼片段:
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(); } }
可以看到,這裡獲取到了一個XmlResourceParser物件,用於對xml檔案進行解析。由於具體的解析規則過於複雜,我們就不跟進去看了。
使用反射建立View物件的程式碼片段:
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大體的工作原理基本瞭解了。
但是正如前面所說,本篇文章並不是要帶著大家去讀原始碼的,而是想要從用法層面對LayoutInflater有些新的理解。
那麼LayoutInflater最常見的用法如下:
View view = LayoutInflater.from(context).inflate(resourceId, parent, false);
這段程式碼的意思是,首先呼叫LayoutInflater的from()方法去獲取一個LayoutInflater的範例,然後再呼叫它的inflate()方法去解析並載入一個佈局,從而轉換成一個View物件並返回。
然而我認為這段程式碼對於新手來說卻及其不友好,甚至對於很多的老手來說也是。
我們來看一下inflate()方法的引數定義:
public View inflate(int resource, @Nullable ViewGroup root, boolean attachToRoot) { ... }
inflate()方法接收3個引數,第一個引數resource還比較好理解,就是我們要解析載入的xml檔案的資源id。第二個引數root,和第三個引數attachToRoot是什麼意思?可能即使不少做過多年Android開發的程式設計師也未必能解釋得清楚。
而這段程式碼在我們使用RecyclerView,或者使用Fragment時都是一定會用到的。我在寫《第一行程式碼》時由於在很早的章節就要講RecyclerView的用法,但是卻又感覺很難向初學者解釋清楚LayoutInflater的相關內容,所以我一直都覺得這塊內容沒有講好。只能先用死記硬背的方式,暫時就記著這部分程式碼必須這麼寫。
而今天,我希望能將LayoutInflater真正講講清楚。
我們知道,Android的佈局結構是一種樹狀結構。每個佈局都可以包含若干個子佈局,每個子佈局又可以繼續包含子佈局,以此構建出任意樣式的View呈現給使用者。
因此,我們大致可以明白,每個佈局它都是要有一個父佈局的。
這也是inflate()方法第二個引數root的作用,就是給當前要解析載入的xml佈局指定一個父佈局。
那麼一個佈局可不可以沒有父佈局呢?當然也是可以的,這也是為什麼root引數被標為@Nullable的原因。
但是如果我們inflate出來了一個沒有父佈局的佈局,又該如何去展示它呢?那自然是沒有辦法去展示的,所以只能後面再用addView的方式將它新增到某個現有的佈局下面。又或者你inflate出來的佈局就是個頂層佈局,所以它不需要有父佈局。但是這些場景都比較少見,因此大多數情況下,我們在使用LayoutInflater的inflate()方法時都是要指定父佈局的。
另外,如果不為inflate出來的佈局指定父佈局,還會出現另外一種問題,我們通過一個例子來講解一下。
這裡我們定義一個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" />
這個佈局檔案非常簡單,裡面只有一個按鈕。
接下來我們使用LayoutInflater來載入這個佈局檔案,並將它新增到一個現有的佈局當中:
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); } }
可以看到,這裡我們並沒有給button_layout指定父佈局,而是傳入了一個null。當第二個引數傳入null時,第三個引數就沒有意義了,因此可以不用指定。
但是前面也說了,一個佈局如果沒有父佈局的話沒辦法顯示出來呀,所以我們又使用了addView()方法將它新增到了一個現有佈局當中。
程式碼就是這麼簡單,現在我們可以執行一下程式,效果如下圖所示:
看上去好像沒啥問題,按鈕已經可以正常顯示出來了,說明button_layout.xml這個佈局確實成功載入出來並且新增到現有的佈局當中了。
但是如果你嘗試去調整一下按鈕的大小,你會發現不管你如何調整,按鈕的大小都是不會變的:
<?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,重新執行程式介面毫無變化。
為什麼會出現這樣的情況呢?
其實這裡不管你將Button的layout_width和layout_height的值修改成多少,都不會有任何效果的,因為這兩個值現在已經完全失去了作用。平時我們經常使用layout_width和layout_height來設定View的大小,並且一直都能正常工作,就好像這兩個屬性確實是用於設定View的大小的。
而實際上則不然,它們其實是用於設定View在佈局中的大小的,也就是說,首先View必須存在於一個佈局中才行。這也是為什麼這兩個屬性叫作layout_width和layout_height,而不是width和height。
而我們因為在使用LayoutInflater載入button_layout.xml這個佈局時並沒有為它指定父佈局,因此這裡layout_width和layout_height屬性就都失去了作用。更準確點來講,所有以layout_開頭的屬性都會失去作用。
現在我們將程式碼進行如下修改:
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()方法的第二個引數指定成了mainLayout。也就是說,我們為button_layout.xml這個佈局指定了一個父佈局。這樣的話,layout_width和layout_height屬性就可以生效了。
重新執行程式,效果如下圖所示:
到這裡為止,我們就將inflate()方法的第二個引數root的作用解釋得非常清楚了。那麼還有一個問題就是,第三個引數attachToRoot又是什麼意思呢?
注意觀察上述程式碼,我們將第二個引數指定成mainLayout的同時,將第三個引數指定成了false。如果你嘗試將第三個引數指定成true,然後重新執行程式碼,程式將會直接崩潰。崩潰資訊如下:
這個崩潰資訊是在說,我們正在新增一個子View,但是這個子View已經有父佈局了,需要讓父佈局先呼叫removeView()移除子View後才能新增。
為什麼修改第三個引數之後會出現這樣的錯誤呢?我們現在就來分析一下。
首先關注一下第三個引數的名字是什麼,attachToRoot。從字面意思上看,是在問我們是否要新增到root上面。那麼root是什麼呢?再次觀察inflate()方法的定義,你會發現第二個引數不就是root嗎?
public View inflate(int resource, @Nullable ViewGroup root, boolean attachToRoot) { ... }
也就是說,attachToRoot的意思,就是在問我們要不要將當前載入的xml佈局新增到第二個引數傳入的父佈局上面。如果傳入true,那麼就意味著會新增,傳入false就表示不會新增。
所以在剛才的程式碼當中,我們一開始在inflate()方法的第三個引數中傳入false,那麼button_layout.xml佈局是不會被新增到mainLayout當中的,我們後面就可以手動呼叫addView()方法將它新增到mainLayout當中。
而如果將第三個引數改成true,就表示button_layout.xml佈局已經自動被新增到mainLayout當中了,此時再去呼叫一遍addView()方法,發現button_layout.xml已經有父佈局了,自然就會丟擲上面的異常。
經過這樣的解釋之後,你是否就對inflate()方法中的每一個引數的作用都理解清楚了呢?
其實理解到了這裡,我們可以回過頭來再去看一看過去寫的程式碼。比如說大家肯定都用過Fragment,在Fragment中載入一個佈局我們通常都會這麼寫:
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); } }
不知道你過去有沒有想過,為什麼這裡inflate()方法的最後一個引數一定要傳入false?
那麼現在可以想一想了。觀察一下Fragment的相關原始碼,你會發現它會將我們在onCreateView()方法中返回的View新增到一個Container當中:
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); }
這個情況和我們剛才的例子非常類似,也就是說,後續Fragment自己會有一個addView的操作,如果我們將inflate()方法的第三個引數傳入true,那麼就會直接將inflate出來的佈局新增到父佈局當中。這樣後面再次addView的時候就會發現它已經有一個父佈局了,從而丟擲與上面同樣的崩潰資訊。
不信的話你可以自己動手試一試。
除了Fragment之外,RecyclerView中對於LayoutInflater的用法也是基於一模一樣的原因,這裡就不再展開討論了。
希望通過閱讀本文之後,你對LayoutInflater又能有一些新的認識。
到此這篇關於Android 老生常談LayoutInflater的新認知的文章就介紹到這了,更多相關Android LayoutInflater內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45