[Android] LayoutInflater原理分析,帶你一步步深刻了解View(一)

有段時間沒寫博客了,感受都有些生疏了呢。最近繁忙的工做終於告一段落,又有時間寫文章了,接下來還會繼續堅持每一週篇的節奏。html

有很多朋友跟我反應,都但願我能夠寫一篇關於View的文章,講一講View的工做原理以及自定義View的方法。沒錯,承諾過的文章我是必定要兌現的,並且在View這個話題上我還準備多寫幾篇,儘可能能將這個知識點講得透徹一些。那麼今天就從LayoutInflater開始講起吧。java

相信接觸Android久一點的朋友對於LayoutInflater必定不會陌生,都會知道它主要是用於加載佈局的。而剛接觸Android的朋友可能對LayoutInflater不怎麼熟悉,由於加載佈局的任務一般都是在Activity中調用setContentView()方法來完成的。其實setContentView()方法的內部也是使用LayoutInflater來加載佈局的,只不過這部分源碼是internal的,不太容易查看到。那麼今天咱們就來把LayoutInflater的工做流程仔細地剖析一遍,也許還能解決掉某些困擾你心頭多年的疑惑。android

先來看一下LayoutInflater的基本用法吧,它的用法很是簡單,首先須要獲取到LayoutInflater的實例,有兩種方法能夠獲取到,第一種寫法以下:app

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片ide

  1. LayoutInflater layoutInflater = LayoutInflater.from(context);  佈局

固然,還有另一種寫法也能夠完成一樣的效果:this

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片spa

  1. LayoutInflater layoutInflater = (LayoutInflater) context  .net

  2.         .getSystemService(Context.LAYOUT_INFLATER_SERVICE);  code

其實第一種就是第二種的簡單寫法,只是Android給咱們作了一下封裝而已。獲得了LayoutInflater的實例以後就能夠調用它的inflate()方法來加載佈局了,以下所示:

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. layoutInflater.inflate(resourceId, root);  

inflate()方法通常接收兩個參數,第一個參數就是要加載的佈局id,第二個參數是指給該佈局的外部再嵌套一層父佈局,若是不須要就直接傳null。這樣就成功成功建立了一個佈局的實例,以後再將它添加到指定的位置就能夠顯示出來了。

下面咱們就經過一個很是簡單的小例子,來更加直觀地看一下LayoutInflater的用法。好比說當前有一個項目,其中MainActivity對應的佈局文件叫作activity_main.xml,代碼以下所示:

[html] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  

  2.     android:id="@+id/main_layout"  

  3.     android:layout_width="match_parent"  

  4.     android:layout_height="match_parent" >  

  5.   

  6. </LinearLayout>  

這個佈局文件的內容很是簡單,只有一個空的LinearLayout,裏面什麼控件都沒有,所以界面上應該不會顯示任何東西。

那麼接下來咱們再定義一個佈局文件,給它取名爲button_layout.xml,代碼以下所示:

[html] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. <Button xmlns:android="http://schemas.android.com/apk/res/android"  

  2.     android:layout_width="wrap_content"  

  3.     android:layout_height="wrap_content"  

  4.     android:text="Button" >  

  5.   

  6. </Button>  

這個佈局文件也很是簡單,只有一個Button按鈕而已。如今咱們要想辦法,如何經過LayoutInflater來將button_layout這個佈局添加到主佈局文件的LinearLayout中。根據剛剛介紹的用法,修改MainActivity中的代碼,以下所示:

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. public class MainActivity extends Activity {  

  2.   

  3.     private LinearLayout mainLayout;  

  4.   

  5.     @Override  

  6.     protected void onCreate(Bundle savedInstanceState) {  

  7.         super.onCreate(savedInstanceState);  

  8.         setContentView(R.layout.activity_main);  

  9.         mainLayout = (LinearLayout) findViewById(R.id.main_layout);  

  10.         LayoutInflater layoutInflater = LayoutInflater.from(this);  

  11.         View buttonLayout = layoutInflater.inflate(R.layout.button_layout, null);  

  12.         mainLayout.addView(buttonLayout);  

  13.     }  

  14.   

  15. }  

能夠看到,這裏先是獲取到了LayoutInflater的實例,而後調用它的inflate()方法來加載button_layout這個佈局,最後調用LinearLayout的addView()方法將它添加到LinearLayout中。

如今能夠運行一下程序,結果以下圖所示:

                                             

Button在界面上顯示出來了!說明咱們確實是藉助LayoutInflater成功將button_layout這個佈局添加到LinearLayout中了。LayoutInflater技術普遍應用於須要動態添加View的時候,好比在ScrollView和ListView中,常常均可以看到LayoutInflater的身影。

固然,僅僅只是介紹瞭如何使用LayoutInflater顯然是遠遠沒法知足你們的求知慾的,知其然也要知其因此然,接下來咱們就從源碼的角度上看一看LayoutInflater究竟是如何工做的。

無論你是使用的哪一個inflate()方法的重載,最終都會展轉調用到LayoutInflater的以下代碼中:

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {  

  2.     synchronized (mConstructorArgs) {  

  3.         final AttributeSet attrs = Xml.asAttributeSet(parser);  

  4.         mConstructorArgs[0] = mContext;  

  5.         View result = root;  

  6.         try {  

  7.             int type;  

  8.             while ((type = parser.next()) != XmlPullParser.START_TAG &&  

  9.                     type != XmlPullParser.END_DOCUMENT) {  

  10.             }  

  11.             if (type != XmlPullParser.START_TAG) {  

  12.                 throw new InflateException(parser.getPositionDescription()  

  13.                         + ": No start tag found!");  

  14.             }  

  15.             final String name = parser.getName();  

  16.             if (TAG_MERGE.equals(name)) {  

  17.                 if (root == null || !attachToRoot) {  

  18.                     throw new InflateException("merge can be used only with a valid "  

  19.                             + "ViewGroup root and attachToRoot=true");  

  20.                 }  

  21.                 rInflate(parser, root, attrs);  

  22.             } else {  

  23.                 View temp = createViewFromTag(name, attrs);  

  24.                 ViewGroup.LayoutParams params = null;  

  25.                 if (root != null) {  

  26.                     params = root.generateLayoutParams(attrs);  

  27.                     if (!attachToRoot) {  

  28.                         temp.setLayoutParams(params);  

  29.                     }  

  30.                 }  

  31.                 rInflate(parser, temp, attrs);  

  32.                 if (root != null && attachToRoot) {  

  33.                     root.addView(temp, params);  

  34.                 }  

  35.                 if (root == null || !attachToRoot) {  

  36.                     result = temp;  

  37.                 }  

  38.             }  

  39.         } catch (XmlPullParserException e) {  

  40.             InflateException ex = new InflateException(e.getMessage());  

  41.             ex.initCause(e);  

  42.             throw ex;  

  43.         } catch (IOException e) {  

  44.             InflateException ex = new InflateException(  

  45.                     parser.getPositionDescription()  

  46.                     + ": " + e.getMessage());  

  47.             ex.initCause(e);  

  48.             throw ex;  

  49.         }  

  50.         return result;  

  51.     }  

  52. }  

從這裏咱們就能夠清楚地看出,LayoutInflater其實就是使用Android提供的pull解析方式來解析佈局文件的。不熟悉pull解析方式的朋友能夠網上搜一下,教程不少,我就不細講了,這裏咱們注意看下第23行,調用了createViewFromTag()這個方法,並把節點名和參數傳了進去。看到這個方法名,咱們就應該能猜到,它是用於根據節點名來建立View對象的。確實如此,在createViewFromTag()方法的內部又會去調用createView()方法,而後使用反射的方式建立出View的實例並返回。

固然,這裏只是建立出了一個根佈局的實例而已,接下來會在第31行調用rInflate()方法來循環遍歷這個根佈局下的子元素,代碼以下所示:

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs)  

  2.         throws XmlPullParserException, IOException {  

  3.     final int depth = parser.getDepth();  

  4.     int type;  

  5.     while (((type = parser.next()) != XmlPullParser.END_TAG ||  

  6.             parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {  

  7.         if (type != XmlPullParser.START_TAG) {  

  8.             continue;  

  9.         }  

  10.         final String name = parser.getName();  

  11.         if (TAG_REQUEST_FOCUS.equals(name)) {  

  12.             parseRequestFocus(parser, parent);  

  13.         } else if (TAG_INCLUDE.equals(name)) {  

  14.             if (parser.getDepth() == 0) {  

  15.                 throw new InflateException("<include /> cannot be the root element");  

  16.             }  

  17.             parseInclude(parser, parent, attrs);  

  18.         } else if (TAG_MERGE.equals(name)) {  

  19.             throw new InflateException("<merge /> must be the root element");  

  20.         } else {  

  21.             final View view = createViewFromTag(name, attrs);  

  22.             final ViewGroup viewGroup = (ViewGroup) parent;  

  23.             final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);  

  24.             rInflate(parser, view, attrs);  

  25.             viewGroup.addView(view, params);  

  26.         }  

  27.     }  

  28.     parent.onFinishInflate();  

  29. }  

能夠看到,在第21行一樣是createViewFromTag()方法來建立View的實例,而後還會在第24行遞歸調用rInflate()方法來查找這個View下的子元素,每次遞歸完成後則將這個View添加到父佈局當中。

這樣的話,把整個佈局文件都解析完成後就造成了一個完整的DOM結構,最終會把最頂層的根佈局返回,至此inflate()過程所有結束。

比較細心的朋友也許會注意到,inflate()方法還有個接收三個參數的方法重載,結構以下:

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. inflate(int resource, ViewGroup root, boolean attachToRoot)  

那麼這第三個參數attachToRoot又是什麼意思呢?其實若是你仔細去閱讀上面的源碼應該能夠本身分析出答案,這裏我先將結論說一下吧,感興趣的朋友能夠再閱讀一下源碼,校驗個人結論是否正確。

1. 若是root爲null,attachToRoot將失去做用,設置任何值都沒有意義。

2. 若是root不爲null,attachToRoot設爲true,則會在加載的佈局文件的最外層再嵌套一層root佈局。

3. 若是root不爲null,attachToRoot設爲false,則root參數失去做用。

4. 在不設置attachToRoot參數的狀況下,若是root不爲null,attachToRoot參數默認爲true。

好了,如今對LayoutInflater的工做原理和流程也搞清楚了,你該知足了吧。額。。。。還嫌這個例子中的按鈕看起來有點小,想要調大一些?那簡單的呀,修改button_layout.xml中的代碼,以下所示:

[html] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. <Button xmlns:android="http://schemas.android.com/apk/res/android"  

  2.     android:layout_width="300dp"  

  3.     android:layout_height="80dp"  

  4.     android:text="Button" >  

  5.   

  6. </Button>  

這裏咱們將按鈕的寬度改爲300dp,高度改爲80dp,這樣夠大了吧?如今從新運行一下程序來觀察效果。咦?怎麼按鈕仍是原來的大小,沒有任何變化!是否是按鈕仍然不夠大,再改大一點呢?仍是沒有用!

其實這裏無論你將Button的layout_width和layout_height的值修改爲多少,都不會有任何效果的,由於這兩個值如今已經徹底失去了做用。平時咱們常用layout_width和layout_height來設置View的大小,而且一直都能正常工做,就好像這兩個屬性確實是用於設置View的大小的。而實際上則否則,它們實際上是用於設置View在佈局中的大小的,也就是說,首先View必須存在於一個佈局中,以後若是將layout_width設置成match_parent表示讓View的寬度填充滿布局,若是設置成wrap_content表示讓View的寬度恰好能夠包含其內容,若是設置成具體的數值則View的寬度會變成相應的數值。這也是爲何這兩個屬性叫做layout_width和layout_height,而不是width和height。

再來看一下咱們的button_layout.xml吧,很明顯Button這個控件目前不存在於任何佈局當中,因此layout_width和layout_height這兩個屬性理所固然沒有任何做用。那麼怎樣修改才能讓按鈕的大小改變呢?解決方法其實有不少種,最簡單的方式就是在Button的外面再嵌套一層佈局,以下所示:

[html] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  

  2.     android:layout_width="match_parent"  

  3.     android:layout_height="match_parent" >  

  4.   

  5.     <Button  

  6.         android:layout_width="300dp"  

  7.         android:layout_height="80dp"  

  8.         android:text="Button" >  

  9.     </Button>  

  10.   

  11. </RelativeLayout>  

能夠看到,這裏咱們又加入了一個RelativeLayout,此時的Button存在與RelativeLayout之中,layout_width和layout_height屬性也就有做用了。固然,處於最外層的RelativeLayout,它的layout_width和layout_height則會失去做用。如今從新運行一下程序,結果以下圖所示:

                      

OK!按鈕的終於能夠變大了,這下總算是知足你們的要求了吧。

看到這裏,也許有些朋友心中會有一個巨大的疑惑。不對呀!平時在Activity中指定佈局文件的時候,最外層的那個佈局是能夠指定大小的呀,layout_width和layout_height都是有做用的。確實,這主要是由於,在setContentView()方法中,Android會自動在佈局文件的最外層再嵌套一個FrameLayout,因此layout_width和layout_height屬性纔會有效果。那麼咱們來證明一下吧,修改MainActivity中的代碼,以下所示:

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. public class MainActivity extends Activity {  

  2.   

  3.     private LinearLayout mainLayout;  

  4.   

  5.     @Override  

  6.     protected void onCreate(Bundle savedInstanceState) {  

  7.         super.onCreate(savedInstanceState);  

  8.         setContentView(R.layout.activity_main);  

  9.         mainLayout = (LinearLayout) findViewById(R.id.main_layout);  

  10.         ViewParent viewParent = mainLayout.getParent();  

  11.         Log.d("TAG""the parent of mainLayout is " + viewParent);  

  12.     }  

  13.   

  14. }  

能夠看到,這裏經過findViewById()方法,拿到了activity_main佈局中最外層的LinearLayout對象,而後調用它的getParent()方法獲取它的父佈局,再經過Log打印出來。如今從新運行一下程序,結果以下圖所示:

 

很是正確!LinearLayout的父佈局確實是一個FrameLayout,而這個FrameLayout就是由系統自動幫咱們添加上的。

說到這裏,雖然setContentView()方法你們都會用,但實際上Android界面顯示的原理要比咱們所看到的東西複雜得多。任何一個Activity中顯示的界面其實主要都由兩部分組成,標題欄和內容佈局。標題欄就是在不少界面頂部顯示的那部份內容,好比剛剛咱們的那個例子當中就有標題欄,能夠在代碼中控制讓它是否顯示。而內容佈局就是一個FrameLayout,這個佈局的id叫做content,咱們調用setContentView()方法時所傳入的佈局其實就是放到這個FrameLayout中的,這也是爲何這個方法名叫做setContentView(),而不是叫setView()。

最後再附上一張Activity窗口的組成圖吧,以便於你們更加直觀地理解:

            

好了,今天就講到這裏了,支持的、吐槽的、有疑問的、以及打醬油的路過朋友儘管留言吧 ^v^ 感興趣的朋友能夠繼續閱讀 Android視圖繪製流程徹底解析,帶你一步步深刻了解View(二) 。

相關文章
相關標籤/搜索