本篇文章主要分析LayoutInflater的inflate方法,并解决inflate方法使用不当造成的问题。
LayoutInflater实际开发中使用较多,动态添加布局、列表适配器设置布局等场景大多数情况都需要LayoutInflater的参与。LayoutInflaterV是View由XML转化为Class过程中必不可少的,我们常用的 setContentView内部也调用了LayoutInflater,详情可阅读Activity加载UI流程。
获取LayoutInflater的方式有以下几种:
LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
LayoutInflater LayoutInflater = LayoutInflater getLayoutInflater();
LayoutInflater LayoutInflater = LayoutInflater.from(context);
它们没有本质区别,可以根据需要进行选择,都是通过如下方式获取的:
LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
熟悉getSystemService方法都知道,本质就是从Map中取出Context.LAYOUT_INFLATER_SERVICE这个key对应的服务,然后强转一下LayoutInflater即可。
我们在activity中调用 getLayoutInflater()即可获得LayoutInflater,其在activity中的实现如下:
public LayoutInflater getLayoutInflater() {
return getWindow().getLayoutInflater();
}
其在方法内调用了 getWindow()的getLayoutInflater()方法,getWindow()获取的Window对象本质就PhoneWindow,PhoneWindow的 getLayoutInflater()方法如下:
public LayoutInflater getLayoutInflater() {
return mLayoutInflater;
}
PhoneWindow直接返回了其成员变量mLayoutInflater,该成员变量的创建是在PhoneWindow的构造函数内。
public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
}
也就是说activity内调用 getLayoutInflater()获取 LayoutInflater在实质是调用了 LayoutInflater.from(context)方法,LayoutInflater的from方法实现如下:
/**
* Obtains the LayoutInflater from the given context.
*/
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;
}
所以最终还是调用了context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)来获得LayoutInflater对象的。
inflate
LayoutInflater的inflate方法有多个形式的重载,最终调用的是 inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) 这个方法。这几个方法都非常的重要,因为很多LayoutInflater相关的问题与这几个方法密切相关。
方法一:内部调用了方法三,如果root不为null,则第三个参数为true,否则为false
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
方法二:内部调用了方法四,如果root不为null,则第三个参数为true,否则为false
public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
return inflate(parser, root, root != null);
}
方法三:现创建 XmlResourceParser实例,然后内部调用了方法四
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();
}
}
方法四:对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;
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("**************************");
}
//如果标签是merge,则root不能为空,且attachToRoot不能为false,因为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");
}
//解析内部View,传入的父布局为root
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
//找到xml中的根布局
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
//设置LayoutParams为null,这个很关键,很多布局参数跟实际有出入的问题都是它导致的,xml里的LayoutParams刚解析出默认为null
ViewGroup.LayoutParams params = null;
//如果root布局(非XML)存在,则根据XML根布局的属性生成LayoutParams。也就是说加入root为null,XML里的根布局的View布局参数一定为null
//attachToRoot为false则将LayoutParams设置给XML里的根布局
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.
//解析所有的子View,并把其添加到XML的根布局里
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不为null,且attachToRoot为true则将xml根布局添加到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.
//如果root为null或者attachToRoot则直接返回xml的根布局
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(parser.getPositionDescription()
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
方法四整体的逻辑比较简单,而且我也给了中文注释,但有几点还是需要强调一下:
如果XML的根布局标签为Merge,则root(parent)不能为空且attachToRoot必须为true,因为Merge标签需要将其合并到root布局,解析内部View时传入的父布局必须是Root。
从XML解析出的根View布局参数是否为null与父布局root有关,root存在则会根据属性生成对应的布局参数,如果root不存在则布局参数为null。
如果父布局root存在且attachToRoot为true则将XML解析出的根布局添加到root中并返回root,否则返回XML的根布局。
常见问题解疑
如果能够理解上面的几点,那么就可以解决常见的跟LayoutInflater相关的问题了,如:
问题1:列表的item布局为啥inflate出来效果跟设置的布局参数不一致?
第一张图是为正确效果,第二张是因为inflate参数没传对导致的。
View view = LayoutInflater.from(mContext).inflate(R.layout.item_pic_zero, null);
传入的parent为null,所以导致inflate出来的View的布局参数为null,在adapter里动态添加布局虽然会自动创建一个布局参数,但是已经跟原有的设置的属性不一致了。
类似的还有这两种写法,其实parent为null了,后面的参数已经么有意义了,attachToRoot是添加到parent的标志。
View view = LayoutInflater.from(mContext).inflate(R.layout.item_pic_zero, null ,true);
View view = LayoutInflater.from(mContext).inflate(R.layout.item_pic_zero, null ,false);
问题2:为什么列表inflate的条目时会报The specified child already has a parent. You must call removeView() on the child’s parent first错误?
从错误日志很明显可以看到是因为子View已经存在父布局了只能移除后才能再次添加。
这是因为attachToRoot错误的传成了True;
View view = LayoutInflater.from(mContext).inflate(R.layout.item_pic_zero, parent, true);
attachToRoot代表的是将XML布局添加到parent,并返回parent,而parent本身是有父布局的,所以再次添加这个布局的时候就会报错。
正确的写法为:
View view = LayoutInflater.from(mContext).inflate(R.layout.item_pic_three, parent, false);
父布局不为空,但attachToRoot为false,也就是说解析出XML布局以及其布局参数并返回。
哪inflate(viewID, parent, true)什么时候用呢?子View需要被inflate出来并添加到父布局时使用,常见于一些自定义控件中。
rInflate
inflate方法根据是否为Merge标签分别调用了 rInflate和 rInflateChildren,从下面的代码可以看到rInflateChildren内部也是调用rInflate完成解析任务的。
/**
* Recursive method used to inflate internal (non-root) children. This
* method calls through to {@link #rInflate} using the parent context as
* the inflation context.
* <strong>Note:</strong> Default visibility so the BridgeInflater can
* call it.
*/
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
/**
* Recursive method used to descend down the xml hierarchy and instantiate
* views, instantiate their children, and then call onFinishInflate().
* <p>
* <strong>Note:</strong> Default visibility so the BridgeInflater can
* override it.
*/
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
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)) {
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
if (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
if (finishInflate) {
parent.onFinishInflate();
}
}
rInflate方法对View的tag以及include等进行了处理,结合inflate方法可以看到,标签为Merge则传入的parent为root,否则为解析出的XML的布局temp,这就是为什么Merge标签能将其内部布局合并到父布局的原因。
除此之外, rInflate也调用了createViewFromTag进行View的解析,这个方法非常的重要,是LayoutInflater.的核心知识点之一,后面会有专门文章介绍。