原文地址:http://blog.csdn.net/guolin_blog/article/details/12921889html
在此轉載,已背不時之需java
有段時間沒寫博客了,感受都有些生疏了呢。最近繁忙的工做終於告一段落,又有時間寫文章了,接下來還會繼續堅持每一週篇的節奏。android
有很多朋友跟我反應,都但願我能夠寫一篇關於View的文章,講一講View的工做原理以及自定義View的方法。沒錯,承諾過的文章我是必定要兌現的,並且在View這個話題上我還準備多寫幾篇,儘可能能將這個知識點講得透徹一些。那麼今天就從LayoutInflater開始講起吧。ide
相信接觸Android久一點的朋友對於LayoutInflater必定不會陌生,都會知道它主要是用於加載佈局的。而剛接觸Android的朋友可能對LayoutInflater不怎麼熟悉,由於加載佈局的任務一般都是在Activity中調用setContentView()方法來完成的。其實setContentView()方法的內部也是使用LayoutInflater來加載佈局的,只不過這部分源碼是internal的,不太容易查看到。那麼今天咱們就來把LayoutInflater的工做流程仔細地剖析一遍,也許還能解決掉某些困擾你心頭多年的疑惑。佈局
先來看一下LayoutInflater的基本用法吧,它的用法很是簡單,首先須要獲取到LayoutInflater的實例,有兩種方法能夠獲取到,第一種寫法以下:this
[java] view plaincopyspa
LayoutInflater layoutInflater = LayoutInflater.from(context); .net
固然,還有另一種寫法也能夠完成一樣的效果:code
[java] view plaincopyxml
LayoutInflater layoutInflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
其實第一種就是第二種的簡單寫法,只是Android給咱們作了一下封裝而已。獲得了LayoutInflater的實例以後就能夠調用它的inflate()方法來加載佈局了,以下所示:
[java] view plaincopy
layoutInflater.inflate(resourceId, root);
inflate()方法通常接收兩個參數,第一個參數就是要加載的佈局id,第二個參數是指給該佈局的外部再嵌套一層父佈局,若是不須要就直接傳null。這樣就成功成功建立了一個佈局的實例,以後再將它添加到指定的位置就能夠顯示出來了。
下面咱們就經過一個很是簡單的小例子,來更加直觀地看一下LayoutInflater的用法。好比說當前有一個項目,其中MainActivity對應的佈局文件叫作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>
這個佈局文件的內容很是簡單,只有一個空的LinearLayout,裏面什麼控件都沒有,所以界面上應該不會顯示任何東西。
那麼接下來咱們再定義一個佈局文件,給它取名爲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按鈕而已。如今咱們要想辦法,如何經過LayoutInflater來將button_layout這個佈局添加到主佈局文件的LinearLayout中。根據剛剛介紹的用法,修改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的實例,而後調用它的inflate()方法來加載button_layout這個佈局,最後調用LinearLayout的addView()方法將它添加到LinearLayout中。
如今能夠運行一下程序,結果以下圖所示:
Button在界面上顯示出來了!說明咱們確實是藉助LayoutInflater成功將button_layout這個佈局添加到LinearLayout中了。LayoutInflater技術普遍應用於須要動態添加View的時候,好比在ScrollView和ListView中,常常均可以看到LayoutInflater的身影。
固然,僅僅只是介紹瞭如何使用LayoutInflater顯然是遠遠沒法知足你們的求知慾的,知其然也要知其因此然,接下來咱們就從源碼的角度上看一看LayoutInflater究竟是如何工做的。
無論你是使用的哪一個inflate()方法的重載,最終都會展轉調用到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解析方式的朋友能夠網上搜一下,教程不少,我就不細講了,這裏咱們注意看下第23行,調用了createViewFromTag()這個方法,並把節點名和參數傳了進去。看到這個方法名,咱們就應該能猜到,它是用於根據節點名來建立View對象的。確實如此,在createViewFromTag()方法的內部又會去調用createView()方法,而後使用反射的方式建立出View的實例並返回。
固然,這裏只是建立出了一個根佈局的實例而已,接下來會在第31行調用rInflate()方法來循環遍歷這個根佈局下的子元素,代碼以下所示:
[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()方法來建立View的實例,而後還會在第24行遞歸調用rInflate()方法來查找這個View下的子元素,每次遞歸完成後則將這個View添加到父佈局當中。
這樣的話,把整個佈局文件都解析完成後就造成了一個完整的DOM結構,最終會把最頂層的根佈局返回,至此inflate()過程所有結束。
比較細心的朋友也許會注意到,inflate()方法還有個接收三個參數的方法重載,結構以下:
[java] view plaincopy
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
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="300dp"
android:layout_height="80dp"
android:text="Button" >
</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
<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屬性也就有做用了。固然,處於最外層的RelativeLayout,它的layout_width和layout_height則會失去做用。如今從新運行一下程序,結果以下圖所示:
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對象,而後調用它的getParent()方法獲取它的父佈局,再經過Log打印出來。如今從新運行一下程序,結果以下圖所示:
很是正確!LinearLayout的父佈局確實是一個FrameLayout,而這個FrameLayout就是由系統自動幫咱們添加上的。
說到這裏,雖然setContentView()方法你們都會用,但實際上Android界面顯示的原理要比咱們所看到的東西複雜得多。任何一個Activity中顯示的界面其實主要都由兩部分組成,標題欄和內容佈局。標題欄就是在不少界面頂部顯示的那部份內容,好比剛剛咱們的那個例子當中就有標題欄,能夠在代碼中控制讓它是否顯示。而內容佈局就是一個FrameLayout,這個佈局的id叫做content,咱們調用setContentView()方法時所傳入的佈局其實就是放到這個FrameLayout中的,這也是爲何這個方法名叫做setContentView(),而不是叫setView()。
最後再附上一張Activity窗口的組成圖吧,以便於你們更加直觀地理解:
好了,今天就講到這裏了,支持的、吐槽的、有疑問的、以及打醬油的路過朋友儘管留言吧 ^v^ 感興趣的朋友能夠繼續閱讀 Android視圖繪製流程徹底解析,帶你一步步深刻了解View(二) 。