1.方法原型及参数的意义
public View inflate(@LayoutRes int resource,
@Nullable ViewGroup root,
boolean attachToRoot)
初次接触这个方法还是在1年多以前使用RecycleView的Adapter的时候,当时怎么也理解不了这是在干嘛,后来勉强知道这个方法主要是用于把编写好的XML转化为能在屏幕上显示的View,再后来知道了各个参数的意义,但是没深究过原因。恰逢最近在看源码,就结合方法的源码来分析一下各个参数的不同取值的效果。
三个参数中的第一个很好理解,第二个和第三个如果就初次接触的话还是会有点懵逼的。
resource
:需要转换成View的布局文件。
root
: 可选的参数,表示为resource提供一个根布局,使得他的宽高属性生效。
attachToRoot
:是否将resource的View加入到root中,然后返回。
2.举例探究参数不同取值的情形
首先,创建一个Activity和他对应的布局文件,并且新建一个待加载的布局文件layout_to_be_added.xml。
TestActivity.java
public class TestActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
}
}
activity_test.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="@+id/ll_activity_test"
android:background="#8af47a"
>
</LinearLayout>
layout_to_be_added.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/layout_to_be_add"
android:background="#dc4444"
>
<Button
android:id="@+id/btn_test"
android:text="一个Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true" />
</RelativeLayout>
此时的界面如下图:
2.1 root != null, attachToRoot == true
在这种情况下,如果想把layout_to_be_added.xml
加载到activity_test.xml
中,按照常规的思路,可能会写出这样的代码。
public class TestActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
LinearLayout activityLayout = (LinearLayout) findViewById(R.id.ll_activity_test);//获取到activity的ViewGroup
View layoutToBeAdded =
LayoutInflater.from(this).inflate(R.layout.layout_to_be_added,activityLayout,true);
activityLayout.addView(layoutToBeAdded);
}
}
看似没有一点毛病,但是run一下竟然报错了:
java.lang.IllegalStateException: The specified child already has a parent.
You must call removeView() on the child's parent first.
报错的主要原因是子布局早已有了父布局,无法添加。
其实这个错误发生的原因很好理解,由于root
不为空(指定成了Activity的布局),而attachToRoot
又为true,这会使得layout_to_be_added.xml
对应的View会在inflate
方法内部被加载到activityLayout
之中,然后返回。既然已经加载进去了,自然不需要再次单独调用addView
方法。
既然如此,将activityLayout.addView(layoutToBeAdded);
注释掉即可,注释掉后会显示如下界面:
2.2 root != null, attachToRoot == false
那么,如果root
设置为activityLayout
,attachToRoot
为false又会如何呢?显然,这时候返回的View就是layout_to_be_added.xml
对应的View,最后需要手动调用addView
方法添加到Activity中:
public class TestActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
LinearLayout activityLayout = (LinearLayout) findViewById(R.id.ll_activity_test);//获取到activity的ViewGroup
View layoutToBeAdded =
LayoutInflater.from(this).inflate(R.layout.layout_to_be_added,activityLayout,false);
activityLayout.addView(layoutToBeAdded);
}
}
当然,此时显示肯定是一切正常:
到了这里心中难免会有疑惑:明明只需要返回layout_to_be_added.xml
对应的View,把root设置为activityLayout
干啥?可不可以把它设置成其他的ViewGroup?答案是当然可以。
这里的root的作用仅仅是为了让layout_to_be_added.xml
设置的布局参数(如layout_width
,layout_height
,layout_gravity
等)生效,因为view的onMeasure过程是由父容器传递过来的,如果没有指定父容器,id为id="@+id/layout_to_be_add"
的这个RelativeLayout的布局参数就会失效,所以必须得指定ViewGroup,当然这里可以指定任意的ViewGroup,因为它的作用仅仅是为了让View的布局参数有效,完全可以直接new一个Layout,比如activityLayout
是一个LinearLayout,这里完全就可以这样写:
LinearLayout l = new LinearLayout(this);
View layoutToBeAdded =
LayoutInflater.from(this).inflate(R.layout.layout_to_be_added,l,false);
2.3 root == null, attachToRoot == true /false
结合第二种情况的分析,如果不指定root
,那么layout_to_be_added.xml
对应的View的布局参数将失效,这时候无论attachToRoot
设置为什么值都会出现如下的显示效果:
此时由于Button是有父布局的,所以Button的布局参数还是有效的。
3.结合源码分析不同参数的意义
LayoutInflater.java
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
//省略无关代码...
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);//又调用了另一个重载方法
} finally {
parser.close();
}
}
很明显,在try的语句块里调用了另一个重载方法:
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;
//默认返回root
View result = root;
try {
// Look for the root node.
int type;
//...
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 {
// 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) {
//...
// Create layout params that match root, if supplied
//构建ViewGroup.LayoutParams,以便设置给temp
params = root.generateLayoutParams(attrs);
//attachToRoot为false,则直接将ViewGroup.LayoutParams设置给temp
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
//...
//重点看这里,如果root不为null,且attachToRoot为true,则将View添加到root中去
// to root. Do that now.
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,或者attachToRoot为false,则直接将View赋值给result,最后返回,由于没有root,故temp的布局参数会失效(因为从上面的代码可以看出并没有给temp设置ViewGroup.LayoutParams)
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;
}
}
结合源码,我也在里面给出了关键步骤的注释,整个逻辑还是不难理解。
总之,如果没有设置root
,则不会给temp设置正确LayoutParams,这样一来temp的布局参数就成了默认的了(相当于没有)。当然如果将attachToRoot
设置为true,在设置了root的情况下,temp会被直接加入root,然后返回,否则,将会返回没有布局参数的temp(这里就是指layout_to_be_added.xml
对应的View)。而如果attachToRoot
设置为了flase,这不会将View加载到root
中。
4.不同情形下参数该如何传值?
这里的传值主要以attachToRoot
的值分情况讨论:
4.1 常见到的情形是attachToRoot==flase
的情况:
如果希望单纯滴把一个layout转换成View,attachToRoot
就应该为false,比如:
1.经典的RecyclerView的Adapter的场景:
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
View view = LayoutInflater.from(
parent.getContext()).inflate(R.layout.item_home, parent,
false)
MyViewHolder holder = new MyViewHolder(view);
return holder;
}
这里如果将attachToRoot
设置成true
是肯定会报错的,为啥?当然是因为RecyclerView的item的添加与删除是由Adapter全权负责的,并不需要我们手动操作,控制权不在我们这里,当然,这一点在其源码里也能够体现:
if (child.getParent() != null) {
throw new IllegalStateException("The specified child already has a parent. " +
"You must call removeView() on the child's parent first.");
}
}
2.给Fragment设置布局的时候
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_class_running_ranking, container, false);
}
以上代码是ViewPager+Fragment+TabLayout的经典使用场景下的Fragment,这里Fragment何时添加到容器中,也全靠adapter来处理,不用我们手动操作。
3.在自定义View中用于将某个布局转化成View的时候。
4.2 再看看attachToRoot==true
的情况:
这种情况下主要是会用到inflate的另一个重载方法:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root)
很显然这样用会直接将layout转换成View,然后加载到我们希望的ViewGoup中,多用于自定义ViewGroup中,比较简单。
最后,为了View的能像布局文件里描绘的那样显示,能传root的时候还是传吧~