N天前博主面试的时候,面试官问我TextView是如何加载到界面上的。博主说LayoutInflator,interviewer问怎么操作的呢?我说:记不得了。。。翻看郭神的博客,发现早在14年底,我就在他的Android LayoutInflater原理分析,带你一步步深入了解View(一)里面有留言,可能是当时too young,对郭神的讲解不知所云,时隔一年多再看,觉得博主很会讲课,通俗易懂。拍郭神个马屁(他很可能看不到),他是我在CSDN上看到的,最会讲解知识的人之一。
不管是动态加载布局,还是在xml实现布局,控件是被显示的过程都是相似的,离不开LayoutInflater的支持。本文将讲述LayoutInflater如何将布局加载,以及setContentView如何将参数传递到LayoutInflater中。
本文要大力感谢 Android应用setContentView与LayoutInflater加载解析机制源码分析,博主是看了他的文章之后才有些明白的,本文不只总结,会讲细节!!!啊哈哈。
1. LayouInflater
LayoutInflater的使用流程一般是这样的:
//第一步实例化LayoutInflater
LayoutInflater layoutInflater = LayoutInflater.from(context);
//第二步inflate布局id并返回view
View view = layoutInflater.inflate(R.layout.***id, null);
实例化LayoutInflater有两种写法:
//第一种
LayoutInflater lif = LayoutInflater.from(Context context);
//第二种
LayoutInflater lif = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
第一种写法其实是对第二种写法的封装
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);//调用getSystemService获取LayoutInflater实例
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
实例化之后,就是layoutInflater.inflate()方法的调用,我们就从这儿说开去。
1. inflate()
public View inflate(int resource, ViewGroup root) {
return inflate(resource, root, root != null);//没有设置attachToRoot的情况下,如果root!=null,默认为true
}
继续看inflate(int resource, ViewGroup root, boolean attachToRoot)方法:
public View inflate(int resource, 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);//Android提供的pull解析方式解析布局文件
try {
return inflate(parser, root, attachToRoot);//关键的方法
} finally {
parser.close();
}
}
实际上,不管使用的哪个inflate()方法的重载,最终都会辗转调用到LayoutInflater的inflate(parser, root, attachToRoot)方法
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context)mConstructorArgs[0];
mConstructorArgs[0] = mContext;//定义返回值,初始化为传入的形参root
View result = root;
try {
// 寻找根节点
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
//如果一开始就是END_DOCUMENT,抛出异常
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
//通过了上面的while和if条件,说明这里type一定是START_TAG,也就是xml文件里的根节点
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)) {
//处理merge tag的情况(merge,APP的xml布局优化)
//root必须非空且attachToRoot为true,否则抛异常结束(使用merge时要注意的地方:merge的xml并不代表某个具体的view,只是将它包起来的其他xml的内容,加到某个上层ViewGroup中)
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
//递归rinflate方法调运,循环遍历布局下的子元素
rInflate(parser, root, attrs, false, false);
} else {
// xml文件中的root view,根据tag节点创建view对象
final View temp = createViewFromTag(root, name, attrs, false);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +root);
}
//根据root生成LayoutParams
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 设置temp的LayoutParams,如果attachToRoot为true,则在后面会调用root.addView()。
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
//递归调用rInflate绘制剩下的children
rInflate(parser, temp, attrs, true, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
if (root != null && attachToRoot) {
//root非空且attachToRoot=true则将xml文件的temp root加到形参提供的root里
root.addView(temp, params);
}
// 如果root为空,或者为绑定到root
if (root == null || !attachToRoot) {
//返回xml里解析的root view
result = temp;
}
}
} catch (XmlPullParserException e) {
InflateException ex = new InflateException(e.getMessage());
ex.initCause(e);
throw ex;
} catch (IOException e) {
InflateException ex = new InflateException(
parser.getPositionDescription()
+ ": " + e.getMessage());
ex.initCause(e);
throw ex;
} finally {
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
//返回参数root或xml文件里的temp root
return result;
}
}
代码的详细说明在注释里面都体现了,inflate()方法的流程概括而言就是:
- 找到根节点
- 处理merge
- 创建根View
- 按照root参数,attachToRoot参数确定LayoutParams以及返回
- 递归绘制子View。
inflate的返回结果和参数之间的关系总结成三类情况:
- inflate(xmlId, null)/inflate(xmlId,null,true/false):返回xml的root view。
- inflate(xmlId,parent,false):利用parent的layoutParam绘制xml中的root view,将xml的root view返回。
- inflate(xmlId,parent,true)/inflate(xmlId,parent):返回parent,xml中内容以parent.addView()的方式加入到parent中。
注意到inflate()中调用了addView()方法,并提到了LayoutParam,我们看下这两个是干嘛的。
2. addView
//添加一个子View
public void addView(View child) {
addView(child, -1);
}
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) {
//如果没有设置它的布局参数的话,会提供它的父布局的默认参数给子view
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);//按指定的布局添加子View
}
addView(child, index, params)的具体实现,我们暂且不关心,由上述流程可以看出,addView()的作用即可。
3. LayoutParams
再关心下LayoutParams布局参数有时干啥的?官方文档说:
LayoutParams are used by views to tell their parents how they want to be laid out——用于视图告诉父布局他们该如何摆放。不同的布局一般在ViewGroup的基础上,扩充了LayoutParams,在构建LayoutParams对象时,可调用父布局自身的构造函数创建。比如向LinearLayout添加子视图时,子视图可以
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT);
不同的需求情况,查看谷歌手册就好。
4. rInflate()
在inflate()函数中,只是创建了一个布局文件中的根部局(按照条件决定是否要将其添加到参数root),对于根部局下的子布局会循环调用rInflate()函数遍历解析。看下rInflate()的实现代码,和inflate()有相似之处
void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
boolean finishInflate, boolean inheritContext) throws XmlPullParserException,
IOException {
final int depth = parser.getDepth();
int type;
//XmlPullParser解析器的标准解析模式
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
//找到START_TAG节点程序才继续执行这个判断语句之后的逻辑
if (type != XmlPullParser.START_TAG) {
continue;
}
//获取Name标记
final String name = parser.getName();
//处理REQUEST_FOCUS的标记
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
//处理tag标记
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
//处理include标记
if (parser.getDepth() == 0) {
//include节点如果是根节点就抛异常
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, parent, attrs, inheritContext);
} else if (TAG_MERGE.equals(name)) {
//merge节点必须是xml文件里的根节点(这里不该再出现merge节点)
throw new InflateException("<merge /> must be the root element");
} else {
//创建View
final View view = createViewFromTag(parent, name, attrs, inheritContext);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
//继续递归解析子View
rInflate(parser, view, attrs, true, true);
viewGroup.addView(view, params);
}
}
//parent的所有子节点都inflate完毕的时候回onFinishInflate方法
if (finishInflate) parent.onFinishInflate();
}
rInflate的流程大概包括:
- 找到开始节点
- 处理requestFoucs,处理tag,处理include,处理merge
- 处理其他情况,并递归处理子节点,addView
- 全都绘制完后返回onFinish方法
可以看到不管在rInflate还是在inflate中,都是利用createViewFromTag创建View的。createViewFromTag方法内部又会去调用createView()方法,然后使用反射的方式创建出View的实例,createView方法不做深入分析。
总结
好啦,LayoutInflater就差不多说完了。从创建对象->inflate()->rInflate()完成从布局文件到View的建立,再到LayoutParams构建参数->addView,添加到父布局中。下篇再说setContentView到LayoutInflater中间又是如何衔接的。
很惭愧,做了一点微小的贡献!