LayoutInflater笔记杂谈
在初学Android的时候,学到Fragment知识点,一些教程里面都在讲:“Inflate的时候,要把最后一个参数设置成false,暂时不用理解为什么”…
所以在这里挂个参数: attachToRoot
,它到底是干啥,它到底有多坑
来自群里讨论的一个问题
为什么我在Fragment创建View的时候
attachToRoot = true
,创建的时候会崩,报错The specified child already has a parent. You must call removeView
说到这个错误,还是之前在拆分view的时候遇见,触发的原因是:一个view被重复添加到Container之中(之前认为的是,一个view被addview到ViewGroup1,然后又把它addview到ViewGroup2,就抛出异常)。那么在Fragment创建view的时候爆出这个异常,那想必是触发了这个异常
为什么会触发
我们已知的:
- 一个view被重复添加到Container的时候触发
- 我们对view的操作就是Inflate,然后作为重写函数的返回值
探索:attachToRoot做了什么
当然要点进去源码看了(QAQ
我根据经验的猜测是,attachToRoot = true
的时候,会自动addview
到parent
中。
attachToRoot 文档注释->
boolean: Whether the inflated hierarchy should be attached to the root parameter? If false, root is only used to create the correct subclass of LayoutParams for the root view in the XML.
看源码:追踪attachToRoot
// 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就是传入的Container
所以在attachToRoot = true
的时候,会自动添加到parent
里面
为什么会出现重复添加异常
你可以会疑问,attachToRoot也就是自动addview啊,为什么会异常?
怎么办?为什么?
这个异常是由view被两次添加触发的,所以我们的追踪线索应该是 -> 这个view
View哪去了?
当然被oncreateView给return了。所以我们开始追踪onCreateView
的返回值
它被performCreateView
调用
View performCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
if (mChildFragmentManager != null) {
mChildFragmentManager.noteStateNotSaved();
}
mPerformedCreateView = true;
return onCreateView(inflater, container, savedInstanceState);
}
然后追踪performCreateView
我们来到了FragmentManager
f.mContainer = container;
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) {
container.addView(f.mView); // 这里!看到了吧!热心的FragmentManager帮你addview了
}
if (f.mHidden) {
f.mView.setVisibility(View.GONE);
}
f.onViewCreated(f.mView, f.mSavedFragmentState);
dispatchOnFragmentViewCreated(f, f.mView, f.mSavedFragmentState,
false);
// Only animate the view if it is visible. This is done after
// dispatchOnFragmentViewCreated in case visibility is changed
f.mIsNewlyAdded = (f.mView.getVisibility() == View.VISIBLE)
&& f.mContainer != null;
} else {
f.mInnerView = null;
}
结果我们发现热心的FragmentManager帮我们addview了,所以两次addview直接导致异常
我们可以从
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."); } ....... }
很明显了,只要
child.getParent() != null
就直接崩了
事后疑惑
FragmentManger里面f.mContainer = container;
的这个Container是谁?是不是我们oncreateView的那个?
if (f.mContainerId != 0) {
if (f.mContainerId == View.NO_ID) {
throwException(new IllegalArgumentException(
"Cannot create fragment "
+ f
+ " for a container view with no id"));
}
container = (ViewGroup) mContainer.onFindViewById(f.mContainerId);
然后我们来看mContainerId
是个啥
// When a fragment is being dynamically added to the view hierarchy, this
// is the identifier of the parent container it is being added to.
int mContainerId;
但是我查找了mContainerId
的调用却并没有找到什么😓
于是我们来倒着找,从oncreateView
的调用里面找(谁用了oncreateview)
f.mView = f.performCreateView(f.performGetLayoutInflater(
f.mSavedFragmentState), container, f.mSavedFragmentState);
View performCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
if (mChildFragmentManager != null) {
mChildFragmentManager.noteStateNotSaved();
}
mPerformedCreateView = true;
return onCreateView(inflater, container, savedInstanceState);
}
onCreateView(inflater, container, savedInstanceState);
所以就很明显了,调用performCreateView
的时候,传入的Container就是那个Container
这个方法怎么这么眼熟呢???
没错,就是它,这句调用的后面,就把view给add到Container里面了
f.mContainer = container;
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) {
container.addView(f.mView); // 这里
}
小结
问题在:attachToRoot = true
的时候,view被两次add到Container里面,触发异常
题外坑
attachToRoot = true
只有这一个坑吗?不
当attachToRoot = true
的时候,Inflate方法返回的view不是你想要的view,而是那个ViewGroup
官方文档悄悄地写在了return里面
The root View of the inflated hierarchy. If root was supplied and attachToRoot is true, this is root; otherwise it is the root of the inflated XML file.
一直没留意...背锅
(我差点就要把源码分析写了… 因为我当时是看源码发现这个“坑”的)
一点小思考
既然我不需要自动
addview
了,那么parent
是用来干嘛的?
ViewGroup: Optional view to be the parent of the generated hierarchy (if attachToRoot is true), or else simply an object that provides a set of LayoutParams values for root of the returned hierarchy (if attachToRoot is false.)
This value may be null.
用来提供LayoutParams来调整生成的view (也可以为空)