对于inflate,我相信大家都不陌生,它的作用是将一个layout.xml布局文件变为一个View对象。尤其在ListView、GridView的Adapter中,我们继承BaseAdapter时必须重写的几个方法中有一个getView()方法,在这个方法中基本都会出现,使用inflate方法去加载一个布局,作为ListView、GridView的每个Item的布局。这篇博客就来分析一下Android中inflate的过程。
使用inflate,一般是调用两个类中的方法:
① 使用View中的静态方法View.inflate():
public static View inflate(Context context, @LayoutRes int resource, ViewGroup root)
② 使用LayoutInflater中的inflate()方法,在LayoutInflater类中有几个重载方法:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root)
public View inflate(XmlPullParser parser, @Nullable ViewGroup root)
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
其实,如果查看过这几个方法的调用过程我们就会发现这几个方法不管调用的是哪一个,最终调用的都是inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)这个方法。
View中的inflate()方法:
public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
LayoutInflater factory = LayoutInflater.from(context);
return factory.inflate(resource, root);
}
调用LayoutInflater中的inflate(@LayoutRes int resource, @Nullable ViewGroup root)方法,调用过程如下:
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();
// ... 打印Log
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){
// 解析xml文件,返回View对象
}
另外一个方法:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
return inflate(parser, root, root != null);
}
同样是调用了上面的方法。
在上面已经说到了所有的inflate()方法最终调用的LayoutInflate中一个,这个方法不是静态的,所以需要通过LayoutInflate对象调用,那么就有一个新的问题,怎样来获取LayoutInflater对象呢?是不是可以和Java中创建对象的一般形式一样直接new LayoutInflater()呢?通过查看LayoutInflater的构造方法我们发现是使用protected修饰的,所以在我们的代码中不能直接使用new来创建一个对象。在代码中获取LayoutInflate对象一般有一下三种方式:
public static LayoutInflater from(Context context)
LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)
// 在Activity中
public LayoutInflater getLayoutInflater()
在Activity中调用getLayoutInflater()方法,调用的是继承至Window的PhoneWindow中的getLayoutInflater()方法,在PhoneWindow中LayoutInflater对象的初始化在PhoneWindow的构造方法中,调用的是LayoutInflate.form(context)方法:
public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
}
在LayoutInflater中的from方法中通过getSystemService()方法获取LayoutInflate对象:
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
所以LayoutInflater对象最终都是通过getSystemService()方法获取的。
// parser:XML文档解析器对象,通过pull解析方式解析xml文档
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;
// 返回结果,默认返回root对象,和面的代码会对result进行操作
View result = root;
try {
// 对节点进行判断
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 (TAG_MERGE.equals(name)) {
// 如果节点是merge并且root为空或者attachToRoot为false,抛出异常
// (因为merge标签能够将该标签中的所有控件直接连在上一级布局上面,从而减少布局层级,假如一个线性布局替换为merge标签,那么原线性布局下的多个控件将直接连在上一层结构上)
if (root == null || !attachToRoot) {
throw new InflateException(" can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
// 初始化root中的子控件
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
// 通过createViewFromTag()方法用于根据节点名来创建View对象,这里是创建根视图。在方法内部调用了createView()方法,createView()方法中通过反射创建View对象并返回。
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);
}
// root不为null时,获取root中的布局参数
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// root不为null并且attachToRoot为false时,将root的布局参数设置给temp
temp.setLayoutParams(params);
}
}
// ... 打印日志
// 调用rInflateChildren()方法创建每一个孩子视图,在调用rInflateChildren()方法中调用rInflate()方法。
// 在SDK版本小于23当中就是直接调用rInflate()方法
rInflateChildren(parser, temp, attrs, true);
// ... 打印日志
// 如果root不为null 并且 attachToRoot为true时,就把temp加到root上,相当于给temp增加一个父节点
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// 如果root为null 或者attachToRoot为false,那么就将temp赋值给result作为结果返回
if (root == null || !attachToRoot) {
result = temp;
}
}
}
// ... 异常处理
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
return result;
}
}
在代码中有比较详细的注释,这里就不在做过多的说明了,避免显得啰嗦。继续查看源码:
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 {
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_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException(" cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException(" must be the root element");
} else {
// 调用createViewFromTag()方法创建View对象
final View view = createViewFromTag(parent, name, context, attrs);
// 父节点
final ViewGroup viewGroup = (ViewGroup) parent;
// 获取布局参数
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// 递归调用rInflateChildren()方法,继续往下层inflate
rInflateChildren(parser, view, attrs, true);
// 将通过createViewFromTag()方法创建View对象添加到父节点中
viewGroup.addView(view, params);
}
}
// 判断是否完成inflate,如果完成了,调用onFinishInflate()方法,在自定义控件时可以重写这个方法获取相关参数
if (finishInflate) {
parent.onFinishInflate();
}
}
这样,就把整个布局文件都解析完成,形成了一个完整的DOM结构,最终会把最顶层的根布局返回,至此inflate()过程全部结束。
1. 如果root为null,attachToRoot不管是true还是false,返回的result都是temp,attachToRoot没有意义(都是走result = temp语句);
2. 如果root不为null,attachToRoot设为true,则会给加载的布局文件的指定一个父布局,即root(会走root.addView(temp, params)这条语句,将temp增加到root中);
3. 如果root不为null,attachToRoot设为false,则会将布局文件最外层的所有layout属性进行设置,当该view被添加到父view当中时,这些layout属性会自动生效(会走temp.setLayoutParams(params)这条语句,将最外层的属性设置给temp);
4. 在不设置attachToRoot参数的情况下,attachToRoot = (root != null); root为null,attachToRoot 为false,root不为null,attachToRoot 为true。
其实这个总结并不需要记住,通过查看inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)这个方法的源码就很容易得到。
在下一篇博客《Android inflate实例解析》中,我会通过一个简单的实例来展示这两个参数不同时所展现出来的不同效果进行对比。
通过以上分析,我们知道了在使用inflate方法将一个xml文件变为View对象时,怎样传参就能使最外层的属性生效。但是我们在Activity中调用setContentView(layoutResID)方法设置Activity的布局文件时,最外层的属性是生效的。我们来查看一下setContentView(resId)方法的调用过程就知道了:
在Activity的onCreate()方法中调用setContentView(layoutResID)方法实际就是getWindow().setContentView(layoutResID);也就是PhoneWindow中的setContentView(layoutResID)方法,在PhoneWindow中的这个方法中,调用的是mLayoutInflater.inflate(layoutResID, mContentParent);这句代码,mLayoutInflater就是LayoutInflater对象,mContentParent其实就是FrameLayout对象(在《Android自定义View之Activity页面的组成》这篇博客中有说明)。也就是说,我们在调用setContentView(layoutResID)方法把自定义的布局文件添加到View树时,系统帮我们在最外层嵌套了一层FrameLayout,所以我们在写Activity的布局时最外层的属性都生效了。