View繪製體系(二)——View的inflate詳解
前言
上一篇博客講到setContentView
最後會調用mLayoutInflater.inflate
來創建了自定義xml中的佈局視圖,添加到mContentParent
中,這裏我們就來學習下inflate
的具體實現以及它的基本使用方法。
inflate的基本使用
首先我們需要明確的是,inflate
方法是講xml
文件反射成一個View
,但是並不執行View的繪製。
inflate
常用的重載有兩種方式:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
所以我們只需要知道第二種重載的參數意義就OK了:
resource
:需要反射的xml文件的資源idroot
:關於root我們需要注意的是,它並不代表將創建好的佈局加入到root中,而是表示將root作爲父容器來創建指定的View。
將root作爲父容器創建View,其含義是讓root協助View的根節點生成佈局參數,如果沒有父容器的話,View的根節點的寬高屬性(match_parent,wrap_parent等)將沒任何的意義,即不會產生任何的效果
attachToRoot
:是否將創建好的View
添加到root
中
通常我們使用inflate
有以下三種方式:
LayoutInflater.inflate
:在獲取到LayoutInflater
實例後,可以通過如下代碼加載佈局:
LayoutInflater inflater = LayoutInflater.from(this);
View view1 = inflater.inflate(R.layout.view1, null);
View view2 = inflater.inflate(R.layout.view2, null, false);
View.inflate
:只有一種形式如下:
public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
LayoutInflater factory = LayoutInflater.from(context);
return factory.inflate(resource, root);
}
其實質是通過LayoutInflater
來創建View的
Context.getSystemService
:通過Context.getSystemService
來獲得LayoutInflater
,實質也是通過LayoutInflater
來創建View
LayoutInflater inflater = (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view1 = inflater.inflate(R.layout.view1, null);
inflate源碼解析
現在我們就來詳細的分析下inflate的整個流程,源碼如下:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
//省略debug相關代碼
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
上面代碼中關鍵的地方在於res.getLayout(resource)
返回了一個XmlResourceParser
,XmlResourceParser
是繼承了XmlPullParser
和AttributeSet
兩個接口的接口,其實現類是Parser
。
(關於AttributeSet和XmlPullParser的內容詳見View繪製體系(三)——AttributeSet與TypedArray詳解)
而Resources
類中提供了getLayout
,getXml
,getAnim
等方法解析對應資源id的xml文件,返回對應的解析器Parser。
接下來我們來看下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;
View result = root;
try {
// Look for the root node.
int type;
// 找到第一個tag
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
// 找不到tag,拋出異常
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
// merge標籤的處理
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 {
// 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) {
//如果root不爲空,獲取它的佈局參數
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
//如果不添加到root中,則調用setLayoutParams給temp設置佈局參數
//否則,使用addView(View, LayoutParmas)來設置佈局參數(在下面)
temp.setLayoutParams(params);
}
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
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) {
result = temp;
}
}
} catch (XmlPullParserException e) {
//省略異常捕獲的代碼
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
上述代碼,首先是attrs = Xml.asAttributeSet(parser)
,這是Xml
類提供的根據XmlPullParser
生成對應的AttributeSet
的方法。
然後就是一些xml解析的判斷與循環,並且獲取根節點即xml文件第一個tag位的屬性:
-
如果爲
merge
標籤,那麼必須將該view放入一個父容器之中,即root
不爲空且attachToRoot
爲true。然後調用rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs, boolean finishInflate)
方法,它會遞歸地遍歷xml的層次結構並創建對應的View,創建完後根據傳入的最後一個參數finishInflate
決定是否在創建完成後調用parent.onFinishInflate
方法。
創建好之後直接返回root
-
如果不爲
merge
標籤,那麼調用createViewFromTag
創建根節點的視圖temp
,如果root不爲空,且attachToRoot
爲false,那麼就將root的LayoutParams
傳給temp
。接下來就是調用rInflateChildren
以temp爲根節點遞歸地去創建視圖。
創建好了之後,如果attachToRoot
爲true,則調用addView
將創建好的temp
添加到root中,返回root,否則不添加,直接返回temp
。
rInflate與rInflateChildren源碼解析
在inflate
中會調用rInflate
和rInflateChildren
來遞歸的創建視圖,我們來看下他們的源碼:
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
//獲取當前元素在View樹中的深度,根節點深度爲1
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
//1.如果沒有遇到結束標誌(/>),則繼續循環下去
//2.如果遇到結束標誌,但沒有到文檔末尾,且當前深度大於起始節點深度,則繼續循環下去
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();
//根據name設置對應屬性
if (TAG_REQUEST_FOCUS.equals(name)) {
//如果遇到requestFocus標籤,那麼使當前元素獲得焦點
pendingRequestFocus = true;
//消費掉requestFocus標籤下的所有子標籤
//該方法內部是一個空循環
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
//處理tag標籤
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
//處理include標籤
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
//如果遇見merge標籤則拋出異常
throw new InflateException("<merge /> must be the root element");
} else {
//創建對應的View
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
//遞歸創建view下面的所有子view
rInflateChildren(parser, view, attrs, true);
//將創建好的View添加到parent中
viewGroup.addView(view, params);
}
}
if (pendingRequestFocus) {
//給包含requestFocus標籤的父元素設置焦點
parent.restoreDefaultFocus();
}
if (finishInflate) {
parent.onFinishInflate();
}
}
從上述源碼可以看出,rInflateChildren
實質就是調用了rInflate
方法,個人認爲區別可能在於處理merge
標籤是傳入的Context
參數,並且rInflateChildren
只用於創建parent下面的子view,不用來創建根節點view。
而rInflate
方法的主要就是一個循環,判斷是否已經處理完parent view下面的所有子View(通過深度和eventType來判斷的),如果沒有,則根據獲取的標籤類型依次處理。在處理一般標籤(非requestFocus,merge,include,tag)標籤時,通過createViewFromTag
來創建view,並添加到parent上
以上就是LayoutInflater的inflate創建View機制的解析了,後續會繼續介紹View繪製的相關知識,敬請關注!