Android LayoutInflater.inflate各个
Android LayoutInflater.inflate各个参数作用
简介
LayoutInflater这个类相信大家都不陌生,当我们平时需要加载layout文件来转换成View的场景都会用到它,其中最常用的有如下两个加载方法:
View inflate(int resource, ViewGroup root)
View inflate(int resource, ViewGroup root, boolean attachToRoot)
可能一直在使用,却并未了解其中参数真正的奥义所在,让我们从源码中看下这几个参数究竟代表着什么。
源码分析
先看下两个参数的inflate,源码如下:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
可以看到,其实它内部还是调用了三个参数的inflate,且只有root不为null时,attachToRoot这个参数才会传true。那我们可以直接看三个参数的inflate:
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();
}
}
首先获取资源对象然后将其转换为XmlResourceParser对象,XmlResourceParser对象可以解析layout.xml文件中的具体属性和标签等信息,那就进而看inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
这个方法:
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 {
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(parser, root, inflaterContext, attrs, false);
} else {
//标识二,正常的非merge标签都走这里
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
//标识三,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);
}
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
//标识四:root != null && attachToRoot,将temp添加到root中
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
//标识五:root == null || !attachToRoot,将temp返回,否则返回root
// Decide whether to return the root that was passed in or the
// top view found in 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;
}
}
标识一:异常兼容
你要加载的layout文件中根布局是merge就必须设置父布局的原因。
标识二:temp=你传入的lauout布局view
createViewFromTag(root, name, inflaterContext, attrs)最终走到createView(String name, String prefix, AttributeSet attrs),和一般的Activity加载布局一样,解析XML标签并且使用反射将XML布局创建出来;
标识三:创建和root相关的params
root不为空时,使用root去创建LayoutParams参数,那么此时该参数如果设置给layout加载好的View,那么root将会约束到layout,如果没有设置将无效
params = root.generateLayoutParams(attrs);
接着判断attachToRoot,如果attachToRoot为false,则将params设置给temp(你将要加载的布局)
标识四:root != null && attachToRoot,将temp添加到root中,并且设置了params
标识五:root == null || !attachToRoot,将temp返回,否则返回root
现象
1.如果root为null或者attachToRoot为false时,则调用layout.xml中的根布局的属性并且将其作为一个View对象返回。
2.如果root不为null,但attachToRoot为false时,则先将layout.xml中的根布局转换为一个View对象,再调用传进来的root的布局属性设置给这个View,然后将它返回。
3.如果root不为null,且attachToRoot为true时,则先将layout.xml中的根布局转换为一个View对象,再将它add给root,最终再把root返回出去。(两个参数的inflate如果root不为null也是相当于这种情况)
Demo验证
四种情况:
通过以上的源码可知:
inflate(R.layout.layout2, this);
inflate(R.layout.layout3, this, true);
这两种为同一种类型
inflate(R.layout.layout1, null);
inflate(R.layout.layout2, this);
inflate(R.layout.layout3, this, true);
inflate(R.layout.layout4, this, false);
验证代码:
public class MyViewGroup extends LinearLayout {
public MyViewGroup(Context context) {
this(context, null);
}
public MyViewGroup(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyViewGroup(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOrientation(VERTICAL);
setDividerDrawable(context.getResources().getDrawable(R.drawable.divider));
setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
//第一种
//加载resID的布局文件,并返回resID根布局类型RelativeLayout
View view1 = LayoutInflater.from(context).inflate(R.layout.layout1, null);
TextView tv1 = view1.findViewById(R.id.tv1);
tv1.setText(tv1.getText() + "第一种");
//resID根布局RelativeLayout的参数将会失去作用
addView(view1);
Log.d("inflate" , "root == null : " + view1.getClass().getName());
//第二种
//加载resID的布局文件,并返回当前布局类型MyViewGroup
View view2 = LayoutInflater.from(context).inflate(R.layout.layout2, this);
TextView tv2 = view2.findViewById(R.id.tv2);
tv2.setText(tv2.getText() + "第二种");
//addView(view2);此时不能添加View,否则将报错
//MyViewGroup
Log.d("inflate", "root == this, attachToRoot默认为 == true: " + view2.getClass().getName());
//第三种
//加载resID的布局文件,并返回当前布局类型MyViewGroup
View view3 = LayoutInflater.from(context).inflate(R.layout.layout3, this, true);
TextView tv3 = view3.findViewById(R.id.tv3);
tv3.setText(tv3.getText() + "第三种");
//addView(view3); view3此时不能添加View,否则将报错
Log.d("inflate", "root == this, attachToRoot == true: " + view3.getClass().getName());
//第四种
//加载resID的布局文件,并返回resID根布局类型RelativeLayout
View view4 = LayoutInflater.from(context).inflate(R.layout.layout4, this, false);
TextView tv4 = view4.findViewById(R.id.tv4);
tv4.setText(tv4.getText() + "第四种");
addView(view4);
Log.d("inflate", "root == this, attachToRoot == true: " + view4.getClass().getName());
// TODO: 2021/5/13 注意
// 当root不为空,attachToRoot=true,inflate之后会将当前布局加载到当前根布局,并且返回当前root,
// 那么也就是说inflate的结果包含了当前根布局的内容,如果之前的加载的布局包含某个ID=tv的组件,
// 那么执行findViewById将会找到布局中的第一个ID=tv的组件,
// 执行修改的时候则只会修改之前加载的第一个布局中ID=tv的组件,而不会修改当前这次加载的layout中的组件
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}
执行结果:
root == null : android.widget.RelativeLayout
root == this, attachToRoot默认为 == true: com.example.flowlayout.MyViewGroup
root == this, attachToRoot == true: com.example.flowlayout.MyViewGroup
root == this, attachToRoot == true: android.widget.RelativeLayout
界面效果:
image-20210513070641699表现结果
inflate(R.layout.layout1, null);
返回resID根布局类型,并且需要执行addView动作才能将layout添加到父容器中,且resID根布局的布局参数失效;
inflate(R.layout.layout2, this);
返回传入的root布局类型,并且inflate已经将resID的布局加到root布局中,resID根布局的布局参数有效;
inflate(R.layout.layout3, this, true);
返回传入的root布局类型,并且inflate已经将resID的布局加到root布局中,resID根布局的布局参数有效;
inflate(R.layout.layout4, this, false);
返回resID根布局类型,并且需要执行addView动作才能将layout添加到父容器中,resID根布局的布局参数有效;
总结
root:用来可以用来创建params并约束你要加载的temp,并且条件成立时将temp add到root中
attachToRoot:用来决定你要是否将temp放到root中
addView报错
当给一个ViewGroup添加view的时候都会调用addView,最终调用addViewInner
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
if (mTransition != null) {
// Don't prevent other add transitions from completing, but cancel remove
// transitions to let them complete the process before we add to the container
mTransition.cancel(LayoutTransition.DISAPPEARING);
}
//标识
if (child.getParent() != null) {
throw new IllegalStateException("The specified child already has a parent. " +
"You must call removeView() on the child's parent first.");
}
//...
}
可以看到if (child.getParent() != null) 时会抛出异常,因为Android中的View为树形结构,每一个View被添加到ViewGroup时,改View就持有父节点,重复添加将会造成一个View有多个Parent,显然不符合规范,所以在上述实例中,inflate已经添加过你传入的view时,如果你再次执行addView将会报错。
当root不为空,attachToRoot=true的特殊情况
当root不为空,attachToRoot=true,inflate之后会将当前布局加载到当前根布局,并且返回当前root,那么也就是说inflate的结果包含了当前根布局的内容,如果之前的加载的布局包含某个ID=tv的组件,那么执行findViewById将会找到布局中的第一个ID=tv的组件,执行修改的时候则只会修改之前加载的第一个布局中ID=tv的组件,而不会修改当前这次加载的layout中的组件
具体效果如下:
public class MyViewGroup extends LinearLayout {
public MyViewGroup(Context context) {
this(context, null);
}
public MyViewGroup(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyViewGroup(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOrientation(VERTICAL);
setDividerDrawable(context.getResources().getDrawable(R.drawable.divider));
setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
//第一种
//加载resID的布局文件,并返回resID根布局类型RelativeLayout
View view1 = LayoutInflater.from(context).inflate(R.layout.layout1, null);
TextView tv1 = view1.findViewById(R.id.tv1);
tv1.setText(tv1.getText() + "第一种");
//resID根布局RelativeLayout的参数将会失去作用
addView(view1);
Log.d("inflate" , "root == null : " + view1.getClass().getName());
//第二种
//加载resID的布局文件,并返回当前布局类型MyViewGroup
View view2 = LayoutInflater.from(context).inflate(R.layout.layout1, this);
TextView tv2 = view2.findViewById(R.id.tv1);
tv2.setText(tv2.getText() + "第二种");
//addView(view2);此时不能添加View,否则将报错
//MyViewGroup
Log.d("inflate", "root == this, attachToRoot默认为 == true: " + view2.getClass().getName());
//第三种
//加载resID的布局文件,并返回当前布局类型MyViewGroup
View view3 = LayoutInflater.from(context).inflate(R.layout.layout1, this, true);
TextView tv3 = view3.findViewById(R.id.tv1);
tv3.setText(tv3.getText() + "第三种");
//addView(view3); view3此时不能添加View,否则将报错
Log.d("inflate", "root == this, attachToRoot == true: " + view3.getClass().getName());
//第四种
//加载resID的布局文件,并返回resID根布局类型RelativeLayout
View view4 = LayoutInflater.from(context).inflate(R.layout.layout4, this, false);
TextView tv4 = view4.findViewById(R.id.tv4);
tv4.setText(tv4.getText() + "第四种");
addView(view4);
Log.d("inflate", "root == this, attachToRoot == true: " + view4.getClass().getName());
// TODO: 2021/5/13 注意
// 当root不为空,attachToRoot=true,inflate之后会将当前布局加载到当前根布局,并且返回当前root,
// 那么也就是说inflate的结果包含了当前根布局的内容,如果之前的加载的布局包含某个ID=tv的组件,
// 那么执行findViewById将会找到布局中的第一个ID=tv的组件,
// 执行修改的时候则只会修改之前加载的第一个布局中ID=tv的组件,而不会修改当前这次加载的layout中的组件
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}
执行结果:
root == null : android.widget.RelativeLayout
root == this, attachToRoot默认为 == true: com.example.flowlayout.MyViewGroup
root == this, attachToRoot == true: com.example.flowlayout.MyViewGroup
root == this, attachToRoot == true: android.widget.RelativeLayout
界面效果图:
image-20210513065847660