從xml中加載一個View,一般通過以下兩個方法:
View#inflate(Context context, @LayoutRes int resource, ViewGroup root)
方法和LayoutInflater#inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
方法從
View#inflate
方法源碼可以看到,它最終也是調(diào)用LayoutInflater#inflate
方法
<!----------View----------->
public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
LayoutInflater factory = LayoutInflater.from(context);
return factory.inflate(resource, root);
}
- 跟著進入
LayoutInflater#inflate
方法源碼
<!----------LayoutInflater------------>
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
- LayoutInflater兩個參數(shù)的inflate方法最終又調(diào)用了三個參數(shù)的inflate方法
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();
}
}
- 三個參數(shù)的方法最終又調(diào)用了它的重載方法郑什,真正的返回了View
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 {
// Look for the root node.
int type;
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 (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) {
if (DEBUG) {
System.out.println("Creating params from root: " +
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);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// 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);
}
// 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;
}
}
- 為了方便分析取出關(guān)鍵代碼如下
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
View result = root;
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 {
// 獲取xml中的根view
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
// 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)
//如果ViewGroup root不為null并且attachToRoot == false,為xml獲取的根view temp設(shè)置 ViewGroup.LayoutParams
temp.setLayoutParams(params);
}
}
// 將temp下所有的子view添加到temp中
rInflateChildren(parser, temp, attrs, true);
//如果ViewGroup root不為null并且attachToRoot == true侦香,將temp添加到root中,并設(shè)置 ViewGroup.LayoutParams
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// 如果傳進來的ViewGroup root為null 或者attachToRoot == false,返回xml中的根view temp朱庆,其它情況返回傳進來的ViewGroup root
if (root == null || !attachToRoot) {
result = temp;
}
}
return result;
}
- 從最后的方法中可以看到,關(guān)鍵的點在于傳入的ViewGroup root的值和boolean attachToRoot的值闷祥,這兩個值得取值不同娱颊,最終影響返回的是傳入的ViewGroup root還是獲取xml中的根view temp,并且影響是否為temp設(shè)置了ViewGroup.LayoutParams
-
ViewGroup root == null && attachToRoot == false
====> 返回獲取xml中的根view temp凯砍,同時temp并沒有設(shè)置了ViewGroup.LayoutParams(此時如果獲取LayoutParams可能為空) -
ViewGroup root == null && attachToRoot == true
====> 返回獲取xml中的根view temp箱硕,同時temp并沒有設(shè)置了ViewGroup.LayoutParams(此時如果獲取LayoutParams可能為空) -
ViewGroup root != null && attachToRoot == false
====>返回獲取xml中的根view temp,同時設(shè)置了ViewGroup.LayoutParams -
ViewGroup root != null && attachToRoot == true
====> 返回ViewGroup root,同時為獲取到xml中的根view temp設(shè)置了ViewGroup.LayoutParams,并添加到ViewGroup root
- 現(xiàn)在再來看
View#inflate(Context context, @LayoutRes int resource, ViewGroup root)
方法,它實際上調(diào)用了inflate(resource, root, root != null)
悟衩,也就是說有兩種情況:
- 一種是
ViewGroup root == null && attachToRoot == false
剧罩,此時需要注意的是返回的是xml布局的根View,并且并未為該根View設(shè)置ViewGroup.LayoutParams座泳,在這種情況需要獲取view的LayoutParams進行操作的需要特別注意 - 另外一種是ViewGroup root != null && attachToRoot == true,此時返回的是傳入的ViewGroup root斑响,同時為獲取到xml中的根view temp設(shè)置了ViewGroup.LayoutParams,并添加到ViewGroup root,這種情況需要注意的是xml中的布局已經(jīng)被添加到ViewGroup root中钳榨,如果需要添加到另外的地方,這種方法是不可行的
- 也就是說對于
View#inflate
方法纽门,想要滿足ViewGroup root != null && attachToRoot == false
是無法滿足的薛耻。而如果需要使用xml中的根view的ViewGroup.LayoutParams,首先需要滿足ViewGroup root不為空(當(dāng)然在onLayout之后使用是可以的赏陵,此時已經(jīng)有parent了)饼齿,另外對于ReclclerView/ListView來說饲漾,它會自己在合適的時機將child添加進來,所以attachToRoot必須需要false缕溉,否則在渲染View的時候就會首先添加到ViewGroup root中考传,導(dǎo)致重復(fù)添加到parent中報錯,因此View#inflate
方法是無法滿足的证鸥,需要使用LayoutInflater#inflate(resource,root,false)
方法
對于
LayoutInflater#inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
方法僚楞,同樣的也會根據(jù)傳入的值得不同得到不同的結(jié)果,在選擇使用哪種方法獲取View的時候就需要考慮對于接下來要對View進行的操作是否有影響擴展
在學(xué)習(xí)Fragment的時候,下面的寫法應(yīng)該是熟悉到不能再熟悉了
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(layoutRes, container, false);
return view ;
}
-
View view = inflater.inflate(layoutRes, container, false);
一直是我們的固定寫法枉层,我想一定有同學(xué)會跟我一樣覺得為什么一定要false泉褐,改成true可不可以
public class TestViewInflaterFragment extends Fragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.activity_main, container, true);
}
}
- 當(dāng)把它添加到activity中的時候
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction().add(R.id.fl_container, TestViewInflaterFragment()).commit()
}
}
- 下面的錯誤一定會教你老老實實做人,大家都是這樣說用false是有道理的鸟蜡,但是我們除了記得需要用false膜赃,還是需要知道為什么一定要用false,而用true就不行
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.test.demo/com.test.demo.MainActivity}: java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2817)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2892)
at android.app.ActivityThread.-wrap11(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1593)
at android.os.Handler.dispatchMessage(Handler.java:105)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6541)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
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:4915)
at android.view.ViewGroup.addView(ViewGroup.java:4746)
at android.view.ViewGroup.addView(ViewGroup.java:4686)
at android.view.ViewGroup.addView(ViewGroup.java:4659)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1425)
at android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1740)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1809)
at android.support.v4.app.BackStackRecord.executeOps(BackStackRecord.java:799)
at android.support.v4.app.FragmentManagerImpl.executeOps(FragmentManager.java:2580)
at android.support.v4.app.FragmentManagerImpl.executeOpsTogether(FragmentManager.java:2367)
at android.support.v4.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManager.java:2322)
at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:2229)
at android.support.v4.app.FragmentManagerImpl.dispatchStateChange(FragmentManager.java:3221)
at android.support.v4.app.FragmentManagerImpl.dispatchActivityCreated(FragmentManager.java:3171)
at android.support.v4.app.FragmentController.dispatchActivityCreated(FragmentController.java:192)
at android.support.v4.app.FragmentActivity.onStart(FragmentActivity.java:560)
at android.support.v7.app.AppCompatActivity.onStart(AppCompatActivity.java:177)
at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1333)
at android.app.Activity.performStart(Activity.java:6992)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2780)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2892)
at android.app.ActivityThread.-wrap11(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1593)
at android.os.Handler.dispatchMessage(Handler.java:105)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6541)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
仔細看一看錯誤日志
The specified child already has a parent. You must call removeView() on the child's parent first.
,看這句結(jié)合前面說過的內(nèi)容揉忘,我想大家肯定已經(jīng)知道為什么了跳座,當(dāng)我們寫成true的時候,會將xml創(chuàng)建的root view添加到container泣矛,然后推測Fragment被加載的時候在某個地方又將xml創(chuàng)建的root view再添加到一個ViewGroup中疲眷,所以會導(dǎo)致這個錯誤,繼續(xù)看一看推測是否正確先看
Fragment#onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)
在哪里被調(diào)用
View performCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
if (mChildFragmentManager != null) {
mChildFragmentManager.noteStateNotSaved();
}
mPerformedCreateView = true;
return onCreateView(inflater, container, savedInstanceState);
}
- 繼續(xù)看
Fragment#performCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
的調(diào)用
//-----------FramgnetManager---------------
void moveToState(Fragment f, int newState, int transit, int transitionStyle,
boolean keepActive) {
//省略部分代碼
```
switch (f.mState) {
//省略部分代碼
```
case Fragment.CREATED:
// This is outside the if statement below on purpose; we want this to run
// even if we do a moveToState from CREATED => *, CREATED => CREATED, and
// * => CREATED as part of the case fallthrough above.
ensureInflatedFragmentView(f);
if (newState > Fragment.CREATED) {
if (DEBUG) Log.v(TAG, "moveto ACTIVITY_CREATED: " + f);
if (!f.mFromLayout) {
ViewGroup container = null;
if (f.mContainerId != 0) {
if (f.mContainerId == View.NO_ID) {
throwException(new IllegalArgumentException(
"Cannot create fragment "
+ f
+ " for a container view with no id"));
}
//首先根據(jù)我們傳入的mContainerId(對于本案例來說就是R.id.fl_container)找到container
container = (ViewGroup) mContainer.onFindViewById(f.mContainerId);
if (container == null && !f.mRestored) {
String resName;
try {
resName = f.getResources().getResourceName(f.mContainerId);
} catch (NotFoundException e) {
resName = "unknown";
}
throwException(new IllegalArgumentException(
"No view found for id 0x"
+ Integer.toHexString(f.mContainerId) + " ("
+ resName
+ ") for fragment " + f));
}
}
f.mContainer = container;
//這個f.mView就是我們自己在onCreateView方法中創(chuàng)建返回的View
f.mView = f.performCreateView(f.performGetLayoutInflater(
f.mSavedFragmentState), container, f.mSavedFragmentState);
if (f.mView != null) {
f.mInnerView = f.mView;
f.mView.setSaveFromParentEnabled(false);
if (container != null) {
//關(guān)鍵地方乳蓄,在這里會將xml創(chuàng)建的view添加到container
container.addView(f.mView);
}
//省略部分代碼
```
}
//省略部分代碼
```
}
}
方法太長咪橙,為了便于查看省略了部分代碼,看注釋已經(jīng)說明了前面的推測是正確的虚倒,當(dāng)改為true的時候美侦,在創(chuàng)建xml view的時候會將view add到傳入的parent(container)中,然后將Fragment加載進來的時候魂奥,F(xiàn)ragmentManager會再一次將創(chuàng)建的xml view添加到container(也就是我們指定的container id指向的ViewGroup)中菠剩,所以導(dǎo)致了重復(fù)添加一個view到ViewGroup中的錯誤。
另外再說一個查看源碼的小技巧耻煤,首先源碼太多太復(fù)雜具壮,一頭扎進去可能就出不來了。首先需要抱著一個目的去查看源碼哈蝇,一般查看源碼是為了驗證一個東西或者學(xué)習(xí)源碼是怎么樣實現(xiàn)某種效果或者某個功能的棺妓,這就是這次查看源碼的目的,主線炮赦。像前面就是為了驗證是不是Fragment加載的時候會將創(chuàng)建的View添加到某個ViewGroup中怜跑,然后根據(jù)方法的調(diào)用去找尋可能是我們目的的代碼,有時候有些方法的調(diào)用是很復(fù)雜的,有些方法也特別長性芬,稍微不注意可能就錯過了想要的內(nèi)容峡眶。比如之前的
Fragment#performCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
還有另外一個地方唄調(diào)用,可能我們會進來了再一直深入植锉,然后很容易就被繞暈了
void ensureInflatedFragmentView(Fragment f) {
if (f.mFromLayout && !f.mPerformedCreateView) {
f.mView = f.performCreateView(f.performGetLayoutInflater(
f.mSavedFragmentState), null, f.mSavedFragmentState);
if (f.mView != null) {
f.mInnerView = f.mView;
f.mView.setSaveFromParentEnabled(false);
if (f.mHidden) f.mView.setVisibility(View.GONE);
f.onViewCreated(f.mView, f.mSavedFragmentState);
dispatchOnFragmentViewCreated(f, f.mView, f.mSavedFragmentState, false);
} else {
f.mInnerView = null;
}
}
}
- 另外
FragmentManager#moveToState(Fragment f, int newState, int transit, int transitionStyle, boolean keepActive)
也非常長辫樱,怎么樣才能提高找到線索的可能性呢? 還記的我們的目的嗎?驗證是不是Fragment加載的時候會將創(chuàng)建的View添加到某個ViewGroup,添加到ViewGroup,第一時間應(yīng)該想到ViewGroup#addView
方法俊庇,然后在對應(yīng)的方法里面搜索一下addView狮暑,如果找到了有,再前后代碼查看一下是不是我們的目的暇赤,一步一步排查下去最終解決我們的問題心例。 - 這只是一個查看源碼的小技巧,可能并不能每次都能解決所有的問題鞋囊。但是可以幫我們少走一點彎路止后。畢竟一入源碼深似海。而且最好不要抱著把源碼里面的所有東西都看懂溜腐,每一個變量代表什么都去糾結(jié)(當(dāng)然如果有這個能力的話也是可以的)译株。應(yīng)該抱著學(xué)習(xí)和解決問題的目的,有針對性的去看源碼挺益,沿著一條主線去搞懂并解決我們遇到的問題歉糜。