一.前言
在 Android 中,通常需要指定一个布局的时候,通常都是在 xml 文件中通过不同的标签指定我们需要的控件,但是在 Activity 中是可以直接通过 Java 对象获取到我们需要的 View 对象的,而从一个 xml 文件转换为一个 View 对象或者 ViewGroup 对象的这个过程,就是通过 LayoutInflater 完成的。
二.原理
1.setContentView
在一个 Activity 中,指定一个 Activity 的布局就是直接通过 setContentView 方法来完成的。
public void setContentView(View view) {
getWindow().setContentView(view);
initWindowDecorActionBar();
}
可知 Activity 的 setContentView 实际上又是调用 window 的 setContentView ,而 window 的实现类就是 PhoneWindow 。所以在 Phone 中可以看到下面的实现。
// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
ViewGroup mContentParent;
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
// 注意这里,使用了 mLayoutInflater
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
从 PhoneWindow 中可以看到 setContentView 最后还是调用了 mLayoutInflater.inflate 方法,而 mContentParent 从注释中就可以这出这个要么是 DecorView 也就是 FrameLayout,要么就是 FrameLayout 的一个子布局。下面就看 LayoutInflater 的 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) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
//获取一个 xml 的解析器
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
// 再次调用下面的
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;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
//如果根节点的 标签和 start 和 end 不一致就抛出异常
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("**************************");
}
//如果是 Merge 标签
if (TAG_MERGE.equals(name)) {
//但是 root 为 null 或者每一个 依附于一个 root
//就需要抛出异常,因为 Merge 不能单独存在
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
//对 Merge 标签去递归解析下面的 标签
rInflate(parser, root, inflaterContext, attrs, false);
} else {
//如果是普通的标签
//就通过 createViewFromTag 反射出来一个 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);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 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.
//如果 root 不为 空并且 attachToRoot 为 true
//就把生成的 View 添加到 root 中。
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.
//如果没有就 直接返回生成的 View
if (root == null || !attachToRoot) {
result = temp;
}
}
....
return result;
}
}
对于 setContentView 来说,因为指定了 root 为 FrameLayout ,所以上述过程实际上就是 将生成的View 添加到 FrameLayout。
2.LayoutInflate
在 RecyclerView 中添加一个 item 的时候经常可以见到类似下面这段代码。
LayoutInflater.from(mContext).inflate(R.layout.item_app, parent, false)
这实际上就是 Android 中获取 LayoutInflater 的一种方式,最后会调用到
LayoutInflater layoutInflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
可见 layoutInflater 也是一个系统的服务。inflate 方法接收三个参数:
- int resource:布局文件 xml 的资源 id
- ViewGroup root:如果 attchToRoot 为 true 的话,root 作为父布局
- boolean attachToRoot:是否加载到 root 布局中
对于这个后面两个的作用就是: - 如果 root 为 null,就直接将生成的 View 返回
-
如果 root 不为 null,attachToRoot 为 true,则会给加载的布局文件的指定一个父布局,即 root,并将生成的 View 添加到 Root 中,但是当该 view 被添加到父 view 当中时, 布局文件中的最外层的 View 的 layout 属性就没作用。
-
如果 root 不为 null,attachToRoot 为 false,则当该 view 被添加到父 view 当中时,布局文件最外层的所有 layout 属性生效。
-
在不设置 attachToRoot 参数的情况下,attachToRoot = root != null;
以 RecyclerView 中添加一个 item 为例,最后的 attachToRoot 为 false 时
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="300dp"
android:layout_height="60dp"
android:orientation="vertical">
</LinearLayout>
layout_width 和 layout_height 都没有作用,这也就是说 layout_xx 属性的生效前提是 他们有一个 父布局,对于 setContentView 那肯定就是 FrameLayout ,因此如果要让这两个属性生效的话,attachToRoot 就应该置为 false ,这也就是为什么在 给 RecyclerView 添加 item 的时候需要设置为 false ,因为一个 item 并不需要填满整个 View .
三.总结
LayoutInflater 生成 View 树
- 1.调用 XML 解析器将 xml 资源解析成 XmlResourceParser 对象作为参数传递给 inflate(parser, root, attachToRoot); 方法
- 2.遍历查找当前 xml 资源文件的根节点
如果没有就抛出异常,解析失败 - 3.用 createViewFromTag 方法,该方法根据当前根节点的名称和属性名创建一个 root View。
- 4.调用 rInflate 方法,遍历 xml 资源根布局 root view 下的子元素,并且将子元素view依次添加到 root view下面
- 5.根据当前view的节点名称和属性调用 createViewFromTag 方法创建子元素 view,然后递归调用 rInflate 方法,遍历子元素 View 的所有子元素,依次类推。最终生成 View 树.
inflate 三个参数的作用:
- 如果 root 为 null,就直接将生成的 View 返回
- 如果 root 不为 null,attachToRoot 为 true,则会给加载的布局文件的指定一个父布局,即 root,并将生成的 View 添加到 Root 中,但是当该 view 被添加到父 view 当中时, 布局文件中的最外层的 View 的 layout 属性就没作用。
- 如果 root 不为 null,attachToRoot 为 false,则当该 view 被添加到父 view 当中时,布局文件最外层的所有 layout 属性生效。
- 在不设置 attachToRoot 参数的情况下,attachToRoot = root != null;
网友评论