完整的异常信息
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:5099)
at android.view.ViewGroup.addView(ViewGroup.java:4930)
at android.view.ViewGroup.addView(ViewGroup.java:4870)
at android.view.ViewGroup.addView(ViewGroup.java:4843)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1434)
at android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1759)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1827)
at android.support.v4.app.BackStackRecord.executeOps(BackStackRecord.java:797)
at android.support.v4.app.FragmentManagerImpl.executeOps(FragmentManager.java:2596)
at android.support.v4.app.FragmentManagerImpl.executeOpsTogether(FragmentManager.java:2383)
at android.support.v4.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManager.java:2338)
at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:2245)
at android.support.v4.app.FragmentManagerImpl$1.run(FragmentManager.java:703)
at android.os.Handler.handleCallback(Handler.java:891)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:207)
at android.app.ActivityThread.main(ActivityThread.java:7470)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:524)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:958)
发生场景
- 带有转场动画的fragment快速切换(包含回退)
具体问题代码片段
fragment 容器
public class ContainerActivity extends BaseActivity {
private FrameLayout mContainer;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.sample_activity_container);
mContainer = findViewById(R.id.fl_container);
}
/**
* The specified child already has a parent. You must call removeView() on the child's parent first.
* 发生场景: 需要持有几个fragment,来回快速切换(包含回退),并且有有转场动画的场景下,就会出现此问题
*/
private Fragment fragment00, fragment01, fragment02;
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
private void openTestFragment(boolean isNeedAnimation, int type) {
FragmentManager manager = getSupportFragmentManager();
FragmentTransaction ft = manager.beginTransaction();
//设置替换和退栈的动画
if (isNeedAnimation) {
ft.setCustomAnimations(R.anim.sample_anim_right_in, R.anim.sample_anim_right_out, R.anim.sample_anim_left_in, R.anim.sample_anim_left_out);
}
Fragment tempFragment = null;
switch (type) {
case 0:
fragment00 = manager.findFragmentByTag("" + type);
if (fragment00 == null) {
fragment00 = TestFragment.newInstance(type);
} else {
Log.d("Test", "fragment00 不为空");
}
tempFragment = fragment00;
break;
case 1:
fragment01 = manager.findFragmentByTag("" + type);
if (fragment01 == null) {
fragment01 = TestFragment.newInstance(type);
} else {
Log.d("Test", "fragment01 不为空");
}
tempFragment = fragment01;
break;
case 2:
fragment02 = manager.findFragmentByTag("" + type);
if (fragment02 == null) {
fragment02 = TestFragment.newInstance(type);
} else {
Log.d("Test", "fragment02 不为空");
}
tempFragment = fragment02;
break;
default:
Log.d("Test", "异常");
tempFragment = TestFragment.newInstance(type);
break;
}
ft.replace(R.id.fl_container, tempFragment, "" + type);
if (isNeedAnimation) {
ft.addToBackStack(null);
}
if (isDestroyed() || isFinishing()) {
return;
}
ft.commitAllowingStateLoss();
}
public int i = 0;
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
public void switchFragment(View view) {
openTestFragment(true, i % 3);
i++;
}
@Override
public void onBackPressed() {
super.onBackPressed();
if (i > 0)
i--;
}
fragment
public class TestFragment extends BaseFragment {
private static final String TYPE_KEY = "type_key";
private View mVRoot;
private int mType;
public static TestFragment newInstance( int type) {
TestFragment fragment = new TestFragment();
Bundle info = new Bundle();
info.putInt(TYPE_KEY, type);
fragment.setArguments(info);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle arguments = getArguments();
if (arguments != null) {
mType = arguments.getInt(TYPE_KEY);
}
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
if (mVRoot == null) {
log("mVRoot 为空");
mVRoot = inflater.inflate(R.layout.sample_fragment_test, container, false);
initView(mVRoot);
} else {
ViewGroup parentView = (ViewGroup) mVRoot.getParent();
if (parentView != null) {
//The specified child already has a parent. You must call removeView() on the child's parent first.
parentView.removeView(mVRoot);//这样操作,有动画且动画尚未结束的场景下是remove不掉的,就会产生上面的崩溃信息
}
}
initData();
return mVRoot;
}
@Override
public void onResume() {
super.onResume();
}
private void initData() {
}
private void initView(View view) {
TextView textView = view.findViewById(R.id.tv_test);
textView.setText("当前位置" + mType);
}
}
发生原因
- 快速切换转场动画尚未结束,removeView无效,造成同一个view尚未被移除出上一个parent,就被添加到下一个parent,从而引发此异常现象。
removeView 源码分析
- step1
@Override
public void removeView(View view) {
if (removeViewInternal(view)) {
requestLayout();
invalidate(true);
}
}
- step2
private void removeViewInternal(int index, View view) {
if (mTransition != null) {
mTransition.removeChild(this, view);
}
····略
if (view.getAnimation() != null ||
(mTransitioningViews != null && mTransitioningViews.contains(view))) {
addDisappearingView(view);
} else if (view.mAttachInfo != null) {
view.dispatchDetachedFromWindow();
}
····略
}
可以看出如果有动画是走了addDisappearingView(view)
- step3
/**
* Add a view which is removed from mChildren but still needs animation
*
* @param v View to add
*/
private void addDisappearingView(View v) {
ArrayList disappearingChildren = mDisappearingChildren;
if (disappearingChildren == null) {
disappearingChildren = mDisappearingChildren = new ArrayList();
}
disappearingChildren.add(v);
}
就是添加到了一个集合里面,然后看看用这个集合都做了什么。
- step 4
@Override
protected void dispatchDraw(Canvas canvas) {
····略
// Draw any disappearing views that have animations
if (mDisappearingChildren != null) {
final ArrayList disappearingChildren = mDisappearingChildren;
final int disappearingCount = disappearingChildren.size() - 1;
// Go backwards -- we may delete as animations finish
for (int i = disappearingCount; i >= 0; i--) {
final View child = disappearingChildren.get(i);
more |= drawChild(canvas, child, drawingTime);
}
}
····略
}
可以看出这些view 还是被画出来了,并没有被实际移除
- step 5
public void endViewTransition(View view) {
if (mTransitioningViews != null) {
mTransitioningViews.remove(view);
final ArrayList disappearingChildren = mDisappearingChildren;
if (disappearingChildren != null && disappearingChildren.contains(view)) {
disappearingChildren.remove(view);
if (mVisibilityChangingChildren != null &&
mVisibilityChangingChildren.contains(view)) {
mVisibilityChangingChildren.remove(view);
} else {
if (view.mAttachInfo != null) {
view.dispatchDetachedFromWindow();
}
if (view.mParent != null) {
view.mParent = null;
}
}
invalidate();
}
}
}
private LayoutTransition.TransitionListener mLayoutTransitionListener =
new LayoutTransition.TransitionListener() {
@Override
public void startTransition(LayoutTransition transition, ViewGroup container,
View view, int transitionType) {
// We only care about disappearing items, since we need special logic to keep
// those items visible after they've been 'removed'
if (transitionType == LayoutTransition.DISAPPEARING) {
startViewTransition(view);
}
}
@Override
public void endTransition(LayoutTransition transition, ViewGroup container,
View view, int transitionType) {
if (mLayoutCalledWhileSuppressed && !transition.isChangingLayout()) {
requestLayout();
mLayoutCalledWhileSuppressed = false;
}
if (transitionType == LayoutTransition.DISAPPEARING && mTransitioningViews != null) {
endViewTransition(view);
}
}
};
动画结束的时候才是真正的被移除
最后也可以追溯下fragment添加动画,最终是调用的
public void startViewTransition(View view) {
if (view.mParent == this) {
if (mTransitioningViews == null) {
mTransitioningViews = new ArrayList();
}
mTransitioningViews.add(view);
}
}
处理方案1:
在fragment 复写此方法,添加如下逻辑,亲测有效(未看源码)
@Override
public void onDestroyView() {
super.onDestroyView();
if(mVRoot!=null){
ViewGroup parentView = (ViewGroup) mVRoot.getParent();
if (parentView != null) {
parentView.removeView(mVRoot);
log("onDestroyView mVRoot parentView 不为空,onDestroyView里面 走了移除逻辑");
} else {
log("onDestroyView mVRoot parentView 为空");
}
}
}
处理方案2:
经过上面的源码分析很显然了,主动调用下清除转场动画的view就行了endViewTransition
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
if (mVRoot == null) {
log("mVRoot 为空");
mVRoot = inflater.inflate(R.layout.sample_fragment_test, container, false);
initView(mVRoot);
} else {
ViewGroup parentView = (ViewGroup) mVRoot.getParent();
if (parentView != null) {
//The specified child already has a parent. You must call removeView() on the child's parent first.
parentView.endViewTransition(mVRoot);//主动调用清除动画
parentView.removeView(mVRoot);
}
}
initData();
return mVRoot;
}