相信只要接触过Android的同学都有用过LayoutInflater这个类,它的作用说起来很简单,通过:
LayoutInflater.inflate(intresource,ViewGroup root, boolean attachToRoot)
将我们的布局文件转化成我们java类中的一个View,但这个inflate方法的参数也总让人摸不着头脑,resource很简单,要转化的布局资源。root?恩.......那就是根布局吧。attachToRoot?恩......那就是是否附属在根布局上。话说英文翻译它们还是不难的,毕竟桌面右下角还有个“有道”呢。但是这翻译出来后好像并没什么用,感觉还是不知道怎么去用它们,root到底要传什么?到底要不要让这个view附属在根布局上?接下来带着这两个参数的问题我们进入主题。
1、获取LayoutInflater
我们获取LayoutInflater一般有以下方法:
方法一:LayoutInflater.from(Context context);
方法二:(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
方法一源码如下:
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;
}
可以看到方法一就是给方法二加了个非空保护。
2、使用LayoutInflater
首先看一个很简单的demo,新建一个module,在主页面通过LayoutInflate动态加入一个布局。
activity_main:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ll_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"/>
</LinearLayout>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="200dp"
android:layout_height="80dp"
android:background="#88FF00"
android:orientation="vertical">
<TextView
android:layout_width="100dp"
android:layout_height="60dp"
android:background="#00FFFF"
android:gravity="center"
android:text="ADD_VIEW"/>
</LinearLayout>
MainActivity代码如下:
public class MainActivity extends AppCompatActivity {
private ViewGroup llContent;
private View addView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
llContent = (ViewGroup) findViewById(R.id.ll_content);
addView = LayoutInflater.from(this).inflate(R.layout.add_view, null);
// addView = LayoutInflater.from(this).inflate(R.layout.add_view, llContent);
// addView = LayoutInflater.from(this).inflate(R.layout.add_view, null, false);
// addView = LayoutInflater.from(this).inflate(R.layout.add_view, null, true);
// addView = LayoutInflater.from(this).inflate(R.layout.add_view, llContent, false);
// addView = LayoutInflater.from(this).inflate(R.layout.add_view, llContent, true);
llContent.addView(addView);
}
}
代码再简单不过了,第10行拿到MainActivity顶部布局llContent,第11行通过LayoutInflater获取到需要添加到MainAcitivity中的布局addView,第17行将addView添加到llContent。注释的部分是root和attachToRoot为不同值时的情况。
3、分析LayoutInflater
我们进入inflate方法,看它到底是如何工作。
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
return inflate(parser, 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) + ")");
}
//通过传入的资源返回一个xmlPaser.
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;
//默认将返回的result赋值为传入的root
View result = root;
try {
//首先解析资源根节点(下面都将传入资源的顶层节点称为根节点,需与传入的root区分)。
int type;
//如果不是start节点也不是end节点,则资源为空
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(布局优化时会使用到),如果为merge,那么传入的root就不能为null,
//且attachToRoot必须为true,否组抛出异常。
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(parser, root, inflaterContext, attrs, false);
} else {
//如果根节点不为merge节点,这里创建出根节点的View-->temp
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
//声明根节点的params
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
//如果根节点不为null,通过root初始化params。
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
//如果不附着在root上,则将params设置给temp
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
//开始解析子节点
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
//如果root不为空且附着在root上时,root直接add添加temp。
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// 如果root等于空或者不附着在root上时,直接将temp返回。
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;
}
}
inflate一共有4个重载方法,可以看出最终都进入了最后的inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)方法,所以直接说明一下最后的这个inflate方法。关键部分都已注释,下面再来梳理一下。
4、结论
我们来看下上面代码的几个关键部分:
4.1、第35行,定义inflate方法返回的View,默认为传入的root。它在109行条件满足时被重新复制,也就是当root为空或者attachToRoot为false时,它会被重新赋值为传入的资源的根节点对应的view(temp)。换种说法:当我们使用inflate方法传入的root不为空且attachToRoot为true时,inflate最终会返回传入的root,否则inflate方法会返回传入资源的根节点对应的view。
4.2、第104行和109行其实为两个对立的if判断(不能理解为什么不用else),第104行结合结论1有:当我们使用inflate方法传入的root不为空且attachToRoot为true时,inflate最终会返回传入的root,且该root已经add过传入的资源布局。
4.3、第74行初始化传入资源的根view(temp)。第77行声明temp的params。第79行当传入的root不为空,在第85行通过root为params赋值,且只有当第86行temp不附着在root上时才将params设置给temp(这里可以解释为什么我们有时通过inflate获取到的view设置的大小不受控制)。
最后我们回过头看看我们MainActivity中的几种不同inflate方法到底会有怎么样的结果。
1、addView = LayoutInflater.from(this).inflate(R.layout.add_view, null):
add_view最外层大小并不是布局中设置的大小,这里刚好验证了以上结论4.3点的root为空的情况。
2、addView = LayoutInflater.from(this).inflate(R.layout.add_view, llContent):
Caused by: java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
at android.view.ViewGroup.addViewInner(ViewGroup.java:3940)
at android.view.ViewGroup.addView(ViewGroup.java:3790)
at android.view.ViewGroup.addView(ViewGroup.java:3731)
at android.view.ViewGroup.addView(ViewGroup.java:3704)
at com.minhao.myapplication.MainActivity.onCreate(MainActivity.java:25)
at android.app.Activity.performCreate(Activity.java:6084)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1117)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2472)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2586)
at android.app.ActivityThread.access$900(ActivityThread.java:163)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1462)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5566)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:962)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:757)
这种情况下APP直接崩溃了,错误日志如上,简单的说就是你添加的view已经有了parent,不能重复添加。为什么会出现这种情况呢,通过第一个inflate方法可以看到,当值传入root时,只要root不为空,attachToRoot就为true。结合以上结论4.2,这种情况下,root已将temp添加了,这时在MainActivity第17行不需要重复添加,去掉该行即可。
下面的addView = LayoutInflater.from(this).inflate(R.layout.add_view, llContent, true)同样也是这种结果。
3、addView = LayoutInflater.from(this).inflate(R.layout.add_view, null, false)和
addView = LayoutInflater.from(this).inflate(R.layout.add_view, null, false),与1致。
4、addView = LayoutInflater.from(this).inflate(R.layout.add_view, llContent, false);
表现出了我们想要的效果,也证明了结论4.3点的正常情况。
到这里LayoutInflater的初步解析就到这里了,希望能帮助到对LayoutInflater.inflate()方法有疑惑的同学!!