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的時候還是傳吧~