LayoutInflater在我们的日常开发中非常常见,通过它可以将我们的布局文件加载成一个View实例,那么LayoutInflater是如何将一个布局文件加载成View实例的呢?
一、LayoutInflater的获取
LayoutInflater的获取有三种方式:
- LayoutInflater.from(context)。
- context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)。
- 在Activity种还可以通过getLayoutInflater()方法获取。
第一种方式和第二种本质上其实是同一种。只是系统给我们封装了一下方便我们调用。
/**
* Obtains the LayoutInflater from the given context.
*/
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
//。。。
return LayoutInflater;
}
对于第三种Activity#getLayoutInflater()方法:
public LayoutInflater getLayoutInflater() {
return getWindow().getLayoutInflater();
}
可以看到它其实调用了Window的getLayoutInflater方法,Window的唯一实现类是PhoneWindow,点进去看看。
@Override
public LayoutInflater getLayoutInflater() {
return mLayoutInflater;
}
enmmm直接返回了mLayoutInflater对象,那么mLayoutInflater在哪里初始化的呢?找找…
public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
}
可以看到在PhoneWindow构造方法中初始化mLayoutInflater,原来还是通过LayoutInflater.from(context)获取的啊!所以不管通过哪种方式最终都是通过context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)获取的。
二、LayoutInflater的inflate方法
为了更好的理解源码,先来理解inflate方法中下面这几个入参的含义:
inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
这里写个demo做测试,demo的Activity根布局就是一个LinearLayout,其id为ll_content。然后在创建一个布局文件layout_inflate_test.xml如下:
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="80dp"
android:background="@android:color/darker_gray"
android:text="Hello World!">
</TextView>
然后在代码中通过LayoutInflater的inflate方法将layout_inflate_test.xml这个布局文件加载到id为ll_content的LinearLayout中。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_layout_inflate_demo);
LinearLayout ll_content=findViewById(R.id.ll_content);
LayoutInflater inflater= LayoutInflater.from(this);
//方式1
inflater.inflate(R.layout.layout_inflate_test,ll_content);
}
一切正常,我们再换种方式加载。
//方式2
TextView textView= (TextView) inflater.inflate(R.layout.layout_inflate_test,ll_content,false);
ll_content.addView(textView);
这里将第三个参数传一个false,此时inflate方法不会将加载后的布局add到ll_content中,而是返回了加载后得到的View实例。然后需要我们自己调用addView添加到ll_content中。
一切正常,如果我们调用inflate方法时不传第二个参数会如何?
//方式3
TextView textView= (TextView) inflater.inflate(R.layout.layout_inflate_test,null);
ll_content.addView(textView);
当第二个参数传入null时。layout_inflate_test.xml中TextView的高好像没起作用。
下面具体看下inflate方法的源码,搞清楚究竟时怎么回事?
三、inflate方法源码分析
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
当调用inflate的两个参数的重载方法时,内部会调用三参的,并且如果第二个参数root != null,那么调用3参数的inflate方法时,第三个参数就是true。
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
//XmlResourceParser 用来解析XML
final XmlResourceParser parser = res.getLayout(resource);
//.....
return inflate(parser, root, attachToRoot);
}
这里获取到parser对象,通过它可以解析传入的布局文件,它使用的是Pull解析。然后又调用了inflate的重载,不过这时传入的第一个参数就是parser了。
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
View result = root;
try {
//查找布局文件的根节点
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
//获取到布局文件的根节点的名称
final String name = parser.getName();
//处理根节点是merge标签的情况
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
//从异常可以看出,如果被加载布局的根节点是merge,那么必须满足root !=null && attachToRoot==true。
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
//调用rInflate递归加载,注意这里传入的第二个参数是root。等会看
rInflate(parser, root, inflaterContext, attrs, false);
} else {
//创建name标签对应的View。(现在是根标签)
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
//重点:如果传入的root不为空,会通过root的generateLayoutParams为当前布局生成LayoutParams
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
//如果attachToRoot为false就会为生成的temp这个View设置tLayoutParams
temp.setLayoutParams(params);
}
}
//加载完根View后就会去递归加载它下面所有的孩子。
rInflateChildren(parser, temp, attrs, true);
//如果root != null && attachToRoo==true就调用root.addView方法将temp这个View添加进去,并且传入生成的布局参数。
if (root != null && attachToRoot) {
root.addView(temp, params);
}
//如果root == null || attachToRoot==false 就将temp赋值给result然后返回。
if (root == null || !attachToRoot) {
result = temp;
}
}
return result;
}
}
通过这段源码我们知道:
- 如果root不为空,那么会借助root的generateLayoutParams来为生成的view获取其LayoutParams,包含了layout_width和layout_height。
- 如果root不为空 并且attachToRoot是false,会通过root.generateLayoutParams(attrs)为这个view生成LayoutParams,并且将其设置到view中。最后返回这个view而不是root。
- 如果root不为空 并且attachToRoot是true,会通过root.generateLayoutParams(attrs)为这个view生成LayoutParams,并且会调用root的addView(temp, params)方法将temp加入到root中,同时添加的还有params。
- 如果root为空,不会为生成的View生成LayoutParams,最后返回的是这个view而不是root。
到这里应该能理解上面的demo中TextView高度未生效的原因了吧。因为root为空就没有为这个view设置LayoutParams,因此我们自己调用addView时,采用的是默认的LayoutParams。
ok,继续回归源码,注意到这行:
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
根据节点的名称创建对应的view,比如我们传入的标签名称是TextView那么就会根据这个名称生成一个TextView实例。
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
//...如果标签名称是blink 就生成一个BlinkLayout布局,blink 标签很少使用,它可以达到一闪一闪的效果,不重点看了。
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
try {
View view;
if (mFactory2 != null) {
//如果mFactory2 不为空就调用它的onCreateView来创建View。
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
//如果mFactory不为空就调用它的onCreateView来创建View。
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
//如果上面没有创建View 并且mPrivateFactory不为空就调用它的onCreateView来创建View。
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
//如果经过上面的步骤view没有创建,就调用它自己的方法来创建view
try {
if (-1 == name.indexOf('.')) {
//如果标签名中不包含.比如TextView,ImageView就调用onCreateView方法
view = onCreateView(parent, name, attrs);
} else {
//如果包含.比如support库中的View或者我们自定义的View都是全路径xx.xxx.xxx。就调用createView方法。
view = createView(name, null, attrs);
}
}
}
return view;
}
}
从createViewFromTag源码可以看出,它会先尝试通过mFactory2、mFactory的onCreateView方法来创建View实例。从这里我们可以看出这个Factory其实是系统给我们预留的钩子,我们可以调用setFactory、setFactory2的方法来设置mFactory和mFactory2拦截LayoutInflate自带的createView方式。常见的换肤,统一字体大小等都可以通过这种手段实现。
LayoutInflate在创建View分为两种情况调用分别调用了onCreateView、createView。
- 如果这个标签名不含.这说明这个View是系统自带的,比如TextView、ImageView等,就调用onCreateView方法
- 如果是support中的View或者我们自定义的View都是全路径的,包含.的。就调用createView方法
LayoutInflater是一个抽象类,它的实现类是PhoneLayoutInflater,实现类重写了onCreateView方法看看它做了啥?
public class PhoneLayoutInflater extends LayoutInflater {
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
//。。。
@Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
View view = createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException e) {
// In this case we want to let the base class take a crack
// at it.
}
}
return super.onCreateView(name, attrs);
}
//。。。
}
遍历sClassPrefixList,然后还是调用了LayoutInflate的createView方法,并且将sClassPrefixList 中保存的一前缀传进去了。如果遍历完都没有创建View实例,就调用super.onCreateView
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
//哈哈,原来是前面的前缀都不行,就用android.view再试试
return createView(name, "android.view.", attrs);
}
最终都会走到createView方法,点进去看看:
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
//1、先尝试从缓存中获取
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;
try {
//2、缓存中获取不到
if (constructor == null) {
//通过前缀和标签名拼接获取全路径,然后获取到该类的class
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
//。。。
//通过class获取到构造
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
//保存到缓存中
sConstructorMap.put(name, constructor);
} else {
//。。。
}
//。。。
//通过反射创建View实例
final View view = constructor.newInstance(args);
//。。。
return view;
}
}
通过分析源码知道了createViewFromTag方法是如何通过节点的name实例化出一个具体的View,当然还有一些细节没说,不过不难自己看看就能明白。分析完createViewFromTag再接着分析inflate中的另一个方法,再大致回顾下inflate方法。
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
final String name = parser.getName();
if (TAG_MERGE.equals(name)) {
//。。。
} else {
// 根据布局根节点的name创建View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
temp.setLayoutParams(params);
}
}
//去加载根节点下面的孩子,注意这里传入了temp,是我们当前节点代表的view。
rInflateChildren(parser, temp, attrs, true);
if (root != null && attachToRoot) {
//将布局根节点代表的View添加到传进来的root中
root.addView(temp, params);
}
if (root == null || !attachToRoot) {
result = temp;
}
}
return result;
}
}
rInflateChildren又去调用了rInflate方法,这个r应该就是递归的意思吧。
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
重点分析下rInflate方法,这个方法用来解析parent下的子节点。
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
//。。。。。
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)) {
//处理节点名称是requestFocus的情况(PS:没用过,不看了)
pendingRequestFocus = true;
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标签就抛异常,可以看出merge标签只能作为布局文件的根节点。
throw new InflateException("<merge /> 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);
//继续递归加载当前节点下的子View
rInflateChildren(parser, view, attrs, true);
加载完成后将当前节点添加到parent中。(PS:此时当前节点下的子View已经add到当前节点了)
viewGroup.addView(view, params);
}
}
if (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
if (finishInflaiewte) {
//如果parent下所有的子View都递归加载添加完成就调用onFinishInflate方法。(又看到一个熟悉的方法)
parent.onFinishInflate();
}
}
可以看到整个过程其实就是一个递归的过程,调用rInflate加载入参有个parent,加载的过程中parent下的所以子节点都会被创建,又去调用rInflateChildren去加载子节点下的子节点,这个过程会被递归,这些节点都会被add到自己的parent中。最终返回一个包含了所有解析好的子View的布局根View,这个根View的数据结构已经是一个view tree了。
整体流程看完了,回过头看下如果根节点是merge的情况。
在最开始加载布局文件的根节点时如果根节点是一个merge标签:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
// 查找布局文件的根节点
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
//获取节点的名称
final String name = parser.getName();
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方法 并且parent传入的参数的root
rInflate(parser, root, inflaterContext, attrs, false);
}
return result;
}
如果是merge标签,还是会调用rInflate去递归加载merge标签下所有的子View,不过最终是被add到root的,这样就知道了merge标签是如何减少布局嵌套了吧,因为在merge标签根本没被创建view它的直接子节点都被直接add到root了。并且此时rInflate函数最后一个参数是false,这意味着直接子view都添加到root后,不会调用root的onFinishInflate。
if (finishInflate) {
parent.onFinishInflate();
}
千万不要把root和根节点搞混淆了,这里的root是调用LayoutInflater的inflate方法传入的。