由LayoutInflater談起
layoutInflater.inflate(int resource, ViewGroup root)
layoutInflater.inflate(int resource, ViewGroup root, boolean attachToRoot)
還記得這2個方法吧,剛看的時候,是不是比較頭痛了?
LayoutInflater,剛學安卓就會遇到的東西,剛開始時,不明白含義,只能死記硬背。但是我這個人不理解的話,記憶起來的記憶力比較差,可能是死記硬背的太用力,導致后來看到這個類,竟然隱隱約約的有些頭痛。T_T。從頭梳理一下把。
UI,這個詞,聽到無數(shù)次了。User Interface,用戶界面,咱手機又不是座機,就算是座機,那撥打電話的數(shù)字按鍵,那也是屬于UI啊,別說智能機了。
官方文檔 (SDK的training部分的Building a Simple User Interface章節(jié))
The graphical user interface for an Android app is built using a hierarchy of View and ViewGroup objects.
(Android 的應(yīng)用程序的圖形用戶界面是使用view和viewgroup的樹形結(jié)構(gòu)來生成的。)
Android provides an XML vocabulary that corresponds to the subclasses of View and ViewGroup so you can define your UI in XML using a hierarchy of UI elements.
(android提供了xml的詞匯,這些詞匯基于view和viewgroup,用這些詞匯你可以在xml中來定義你的UI)
既然官方都發(fā)話了,要使用view和viewgroup來顯示UI,那還猶豫什么,用唄~
但是這些元素要顯示在哪里呢?大聲告訴我,沒錯,作為android四大組件之一,Activity閃亮登場。
官方文檔 (SDK Reference部分-->android.app.activity章節(jié))
An activity is a single, focused thing that the user can do. Almost all activities interact with the user, so the Activity class takes care of creating a window for you in which you can place your UI with setContentView(View).
一個activity(活動)是一個單一,集中的且被用戶處理的事物。幾乎所有的activity都會和用戶相互作用,交流(交互),所以activity類專注于創(chuàng)建一個窗口,這個窗口通過setContentView(View)來放置你的UI(view和viewgroup)
首先,一個應(yīng)用要顯示的話,要通過activity,但是,activity的翻譯是活動啊,活動跟顯示的關(guān)系不大啊。其實,activity是一個控制單元,即可視的人機交互界面。而真正顯示的部分(看上面的文檔),是activity會創(chuàng)建一個窗口(window),然后這個窗口調(diào)用了setcontentview的方法來顯示UI(view和viewgroup)。
打個比喻: (引用于 Android_View,ViewGroup,Window之間的關(guān)系)
Activity是一個工人,它來控制Window;Window是一面顯示屏,用來顯示信息;View就是要顯示在顯示屏上的信息,這些View都是層層重疊在一起(通過infalte()和addView())放到Window顯示屏上的。而LayoutInfalter就是用來生成View的一個工具,XML布局文件就是用來生成View的原料
而setContentView()
又是怎么工作的呢?這又要牽扯到窗口的構(gòu)成了,走你~
activity窗口的構(gòu)成
看下源碼:
activity中setcontentview部分
/**
* Set the activity content from a layout resource. The resource will be
* inflated, adding all top-level views to the activity.
*
* @param layoutResID Resource ID to be inflated.
*
* @see #setContentView(android.view.View)
* @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
*/
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
- 實際上是調(diào)用了
getWindow().setContentView(layoutResID);
的方法。
而getWindow()
就是得到了上圖中的PhoneWindow對象,DecoView則是一個窗口的根視圖,包含TitleView和ContentView。
TitlView就是標題欄了,還記得之前去掉標題欄把requestWindowFeature(Window.FEATURE_NO_TITLE);
這里隱藏的就是TitleView了。
而setContentView()
則是對于上圖中的contentview進行設(shè)置(當然了,名字都是以一毛一樣的,這里的setContentVIew中的contentView,就是指的上圖的Contentview,有點點繞口了。。)這里的contentview,就是我們設(shè)置xml的主力戰(zhàn)場啦。 - 好吧,解釋完了PhoneWinDow和titleView,ContentView的關(guān)系,接著走
源碼
public Window getWindow() {
return mWindow;
}
接著上文,getWindow()
方法返回了一個mWindow對象,所以,setContentview()
里面實際上也是調(diào)用的這個mWindow
的setContentView()
的方法,那么,這個mWindow
對象是從哪里來的呢?想肯定想不出來,繼續(xù)看源碼~
+_+....終于找到啦!! 請看
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this); //--------------分割線要長才能被看到~我在這里,吼吼吼!!
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}
if (info.uiOptions != 0) {
mWindow.setUiOptions(info.uiOptions);
}
mUiThread = Thread.currentThread();
mMainThread = aThread;
...
}
上面的代碼挺多的,其實,關(guān)鍵的代碼,就是一行,就是我在代碼中標注的地方。(就一行標注的地方,好找~)
在activity里的attach
方法里,終于是找到了mWindow
被賦值的地方了。
至此,activity設(shè)置setContentView
總算告一個段落了。
但是,有些好奇的寶寶又要發(fā)問了,這個attach
方法是在哪里運用的呀。activity基本的生命周期沒有這個方法呀。而且上面的attach方法又是聲明final的,也不能覆蓋,咋整哩。反正我當時是沒想明白,于是又陷入了糾結(jié)中。。。
于是在網(wǎng)上慢慢搜索,終于發(fā)現(xiàn)了老羅的一系列文章,簡直令人豁然開朗、茅塞頓開.
Android應(yīng)用程序窗口(Activity)的窗口對象(Window)的創(chuàng)建過程分析 這有一系列,慢慢的看,收獲很多啊。悄悄的告訴你,精華在每篇文章的結(jié)尾總結(jié)部分哦。前面的代碼看著頭痛的,留著慢慢看,可以先看下總結(jié)。 下面摘取下我們比較關(guān)心的部分吧~
好了,回到主題,attch
到底在哪里?
這又要跟activity的創(chuàng)建扯上關(guān)系了。大家都知道,activity的生命周期,onCreate
到onDestroy()
,但是作為建立在java語言上,在java中,咱平時創(chuàng)建對象,
- 要么是new創(chuàng)建對象
- 要么是通過反射創(chuàng)建對象,Class.newInstance()方式,
- 要么clone(),
- 要么運用反序列化
其實,見得最多的,就是前2種了,那么activity對象的產(chǎn)生,到底是怎么產(chǎn)生的呢?是new出來的?是反射嗎?
雖然說系統(tǒng)幫咱都做好了,但是沒有看見創(chuàng)建的語句,老是覺得心里有點慌慌噠。看到老羅的博客,終于找到啦。
以下內(nèi)容摘自Android應(yīng)用程序窗口(Activity)的運行上下文環(huán)境(Context)的創(chuàng)建過程分析
注意上圖中的第二步和第六步
2:new Activity
public class Instrumentation {
......
public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
return (Activity)cl.loadClass(className).newInstance();
}
......
}
沒錯!原來activity是通過反射來進行實例化的!!好吧,心安了~
6: attach
這里就是我們一直魂牽夢繞的attach
了,在這個方法里,我們對activity的實例進行了初始化,包括得到了我們的mWindow對象。
總結(jié)下:
一個Android應(yīng)用窗口的運行上下文環(huán)境是使用一個ContextImpl對象來描述的,這個ContextImpl對象會分別保存在Activity類的父類ContextThemeWrapper和ContextWrapper的成員變量mBase中,即ContextThemeWrapper類和ContextWrapper類的成員變量mBase指向的是一個ContextImpl對象。
Activity組件在創(chuàng)建過程中,即在它的成員函數(shù)attach被調(diào)用的時候,會創(chuàng)建一個PhoneWindow對象,并且保存在成員變量mWindow中,用來描述一個具體的Android應(yīng)用程序窗口。
Activity組件在創(chuàng)建的最后,即在它的子類所重寫的成員函數(shù)onCreate中,會調(diào)用父類Activity的成員函數(shù)setContentView來創(chuàng)建一個Android應(yīng)用程序窗口的視圖。
說了這么多,跟LayoutInflater也沒關(guān)系啊。
前面的過程只是說了下窗口誕生的大概過程,之前在官方文檔里,說了用xml來定義ui,說了界面是有view和view group構(gòu)成的,那么,到底是怎么由xml轉(zhuǎn)化為對象跑在程序中的呢?是怎么由setContentView化為圖形界面的呢?
LayoutInflater表示,終于到我的出場機會了。
先到官方文檔里走一遭
Instantiates a layout XML file into its corresponding View objects. It is never used directly. Instead, use getLayoutInflater() or getSystemService(Class) to retrieve a standard LayoutInflater instance that is already hooked up to the current context and correctly configured for the device you are running on.
實例化一個布局的xml文件為一個和它相對應(yīng)的view對象。它不會直接使用,相應(yīng)的,我們使用getLayoutInflater()
和getSystemService(Class)
來得到一個LayoutInflater
的實例,這個實例已經(jīng)和當前的context綁定好了。
其實,LayoutInflater主要用來加載布局,而在setContentView()里,最終也是用到了LayoutInflater。
LayoutInflater,有3種獲取方式
- LayoutInflater layoutInflater = LayoutInflater.from(context);(其實第一種就是第二種的簡化)
- LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- LayoutInflater layoutInflater = Activity.getLayoutInflater();(第三種是要在activity里才有對應(yīng)方法)
而其常見的用法有2種
- layoutInflater.inflate(int resource, ViewGroup root)
- layoutInflater.inflate(int resource, ViewGroup root, boolean attachToRoot)
當時看到這個用法,頭是很大的啊,第一個參數(shù)是傳的xml的id,這都沒問題,最讓人害怕的,就是后2個參數(shù),到底是嘛意思嘛!
哎,看到就煩。
看看第一種方式源碼
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
其實就是調(diào)用了第二種方式,如果roota==null,就相當于inflate(id,root,false),如果不為null,就相當于inflate(id,root,true)
所以,我們只要看第二種就好了。
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
這里提到了inflate(parser, root, attachToRoot)
,跟進去
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
1.注意啦!!!非常核心的一步,先把要返回的view用root來賦值,為什么這樣做哩,下面就知道了。
View result = root;
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
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, inflaterContext, attrs, false);
} else {
2. 注意啦!!!我在這里額,下面的createViewFromTag會返回xml布局里的最外層view。
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
3. 注意啦!!!如果傳的root不為null的時候,得到布局參數(shù)。
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
4.注意啦!!!不被綁定的時候,而root又不為空,會把xml的view的參數(shù)設(shè)置
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
5.注意啦!!! 如果root不為空,且第三個參數(shù)為true的時候,把這個xml渲染得到view作為子view加到root里去。
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
6.注意啦!!!當root為空,或者(是或者喲!)第三個參數(shù)為空的時候,把返回的view用xml的view來賦值,
注意標注點1的時候,是一進來就把result用root來賦值喲。
result = temp;
}
}
} catch (XmlPullParserException e) {
InflateException ex = new InflateException(e.getMessage());
ex.initCause(e);
throw ex;
} catch (Exception e) {
InflateException ex = new InflateException(
parser.getPositionDescription()
+ ": " + e.getMessage());
ex.initCause(e);
throw ex;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
return result;
}
先得出結(jié)論,再看下文分析
ayoutInflater.inflate(int resource, ViewGroup root, boolean attachToRoot)
對于這3個參數(shù)的理解
- root為空的情況,這個方法返回的是resource所渲染的最外層根view,(別忘了UI是建立在樹形結(jié)構(gòu)上的,如果有子view的話,當然也包含在最外層根view里了。),雖然標注1是把返回的view用root來賦值,但是請注意標注6,由于root等于空,所以又用temp(這個是resource這個xml所渲染的最外層view)來代替了。
- root不為空,并且attachToRoot為true的情況下,返回的是root,root!呀!!標注1把返回的view用root賦值,然后標注5,把temp(resource這個xml的最外層view)作為子view添加到root里去,后來再無修改,所以,返回的result還是等于root,只是這個root里包含了xml所渲染的view。
- root不為空,且attachToRoot為false,注意標注6,返回的是xml渲染的view,但是在標注3里得到了root的布局參數(shù)(layoutparams),在標注4中, temp.setLayoutParams(params)進行賦值。所以這個view是有了layoutparams的view。得到了root的內(nèi)力傳輸。。。
注意下標注2,view的產(chǎn)生,這里就是由xml轉(zhuǎn)化為對象的一步了,跟進去,最終我們會發(fā)現(xiàn)
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);
Class<? extends View> clazz = null;
...
final View view = constructor.newInstance(args);
...
}
在這個方法了,也是通過反射的方式得到了view的對象.而且,android是通過pull方式來解析xml的哦.
彩蛋:注意標注3是吧params = root.generateLayoutParams(attrs);
,使用過自定義viewgroup的童鞋們是不是有點眼熟了?自定義LayoutParams是要重寫幾個方法還記得吧~當時我是找要傳參AttributeSet這個方法時找了好久,沒想到在這里哦。具體的內(nèi)容,我會在下一篇自定義viewgroup的時候詳細談?wù)劇?/p>
所以,上面的root是否為空的主要區(qū)別,就是有沒有得到root的layoutparams.
當然,layoutParams又有什么用?,就算我是上述的情況2,得到了layoutparams又有什么用啊?還是不明白.
下面來走個例子~
一個簡單的TextView的布局
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="300dp"
android:layout_height="200dp"
android:background="#deb887"
android:orientation="vertical"
android:text="好好學習天天向上">
</TextView>
注意這里的最外層布局啊~
activity的布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rl_main" //注意,id擱這了.
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.tx.laoluo.activity.TestLayoutButtonActivity">
</RelativeLayout>
public class TestLayoutTextViewActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test_layout_button);
RelativeLayout rl_main = (RelativeLayout) findViewById(R.id.rl_main);
View textView = getLayoutInflater().inflate(R.layout.view_button,null);
rl_main.addView(textView);
}
}
這里Layout.inflater是第一個種方式,root為null的情況
看看結(jié)果
屬性完全沒用啊.說好的android:layout_width="300dp"
android:layout_height="200dp"呢?
這是為什么,又要看addview的源碼了...
addview最終會來到這個方法里
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
如果 child.getLayoutParams()
為空的話,則會生成一個默認的layoutparams,自己的屬性,就不會生效了.至于為什么,這要牽扯到view的繪制了.關(guān)于view,viewgroup,laytoutparams他們之間的愛恨情仇,相愛相殺,這又是另一個故事了...這個故事具體的將在下篇博客里為大家娓娓道來.
那麼,讓我們打印一下.注意,這個打印要在addview前,不然,后面就有默認的了.
RelativeLayout rl_main = (RelativeLayout) findViewById(R.id.rl_main);
View textView = getLayoutInflater().inflate(R.layout.view_button,null);
Log.d("taxi","textView.getLayoutParams()="+textView.getLayoutParams());
rl_main.addView(textView);
結(jié)果
09-09 11:00:07.504 15425-15425/com.example.admin.myapplication D/taxi: textView.getLayoutParams()=null
ok,果然為空.
讓我們看看另外2種方式
root不為空,attachtoroot為false
View textView = getLayoutInflater().inflate(R.layout.view_button,rl_main,false);
rl_main.addView(textView);
結(jié)果
ok,生效了.
第三種方式
root不為空, attachtoroot為true
View textView = getLayoutInflater().inflate(R.layout.view_button,rl_main,true);
// rl_main.addView(textView);
結(jié)果
ok,依然生效.
注意,這里由于是true,在渲染時,會默認把咱們的textview作為子view添加到rl_main中.不用在addview了,不然會報錯.不能把一個有了parent的view再當作子view添加給別人. (
throw The specified child already has a parent. You must call removeView() on the child's parent first
)
ok.回顧一下吧, 本文從基本的UI顯示入手,xml與顯示的關(guān)系,UI與view,viewgroup的關(guān)系,然后說到activity的作用,activity的內(nèi)容,窗口的概念,setContentView的概念,再引入了mWindow的概念,activity創(chuàng)建過程,這些都是LayoutInflater的前身,然后,由setContentView開始,真正的引入了LayoutInflater的概念,再經(jīng)過源碼的學習,了解到了LayoutInflater的實例化以及參數(shù)的含義.至此,咱們的LayoutInflater傳就吿一段落了,至于之前上文提過的view,viewgroup,以及l(fā)ayoutParams的關(guān)系,那是另外的故事了,欲知后事如何,且聽下回分解~