Crash logs
java.lang.IllegalStateException: The specified child already has a parent. You must call removeView(a) on the child's parent first.
at android.view.ViewGroup.addViewInner(ViewGroup.java:4417)
at android.view.ViewGroup.addView(ViewGroup.java:4258)
at android.view.ViewGroup.addView(ViewGroup.java:4198)
at android.view.ViewGroup.addView(ViewGroup.java:4171)
at androidx.fragment.app.g.a(SourceFile:216)
at androidx.fragment.app.g.a(SourceFile:436)
at androidx.fragment.app.g.b(SourceFile:60)
at androidx.fragment.app.g.c(SourceFile:58)
at androidx.fragment.app.g.a(SourceFile:22)
at androidx.fragment.app.g.h(SourceFile:2)
at androidx.fragment.app.g.w(SourceFile:3)
at androidx.fragment.app.g$a.a(SourceFile:1)
at androidx.activity.OnBackPressedDispatcher.a(SourceFile:12)
at androidx.activity.ComponentActivity.onBackPressed(SourceFile:1)
Copy the code
The problem found
Jump from one Fragment to the next in a Fragment that has a transition animation enabled, and immediately return to the previous page before the transition animation ends. Let’s analyze the problem code first:
private var mView: View? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup? , savedInstanceState:Bundle?).: View? {
if (mView == null) {
mView = inflater.inflate(rootLayoutId, container, false)}return mView
}
Copy the code
If the mView already exists, the parent may already be added to the mView.
private var mView: View? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup? , savedInstanceState:Bundle?).: View? {
if (mView == null) {
mView = inflater.inflate(rootLayoutId, container, false)}else{ (mView? .parentasViewGroup?) ? .removeView(mView) }return mView
}
Copy the code
But sadly, the problem remains.
Problem analysis
The fragment mView has been removed from parent. Let’s add print:
varparent = (rootView? .parentas ViewGroup?)
Log.d("onCreateView "."$parent ")
if(parent ! =null) { parent.removeView(rootView) } parent= (rootView? .parentas ViewGroup?)
Log.d("onCreateView "."$parent ")
Copy the code
Sure enough, the second print shows that parent is not null. RemoveView failed. Let’s analyze it and find the source code that throws the exception:
private void addViewInner(View child, int index, ViewGroup.LayoutParams params, boolean preventRequestLayout) {
if(child.getParent() ! =null) {
throw new IllegalStateException("The specified child already has a parent. You must call removeView() on the child's parent first.");
} else{... }Copy the code
So we need to find the code that nullifes child.getparent () and explain why the removeViewInternal method is called when the removeView(View View) procedure is not executed:
private void removeViewInternal(int index, View view) {... Omit some code.....// Determine whether the current view is playing, or whether the animation is scheduled to play
if(view.getAnimation() ! =null|| (mTransitioningViews ! =null && mTransitioningViews.contains(view))) {
addDisappearingView(view);
} else if(view.mAttachInfo ! =null) { view.dispatchDetachedFromWindow(); }... Omit some code..... removeFromArray(start, count); }Copy the code
// This method also sets the child's mParent to null
private void removeFromArray(int index) {
final View[] children = mChildren;
if(! (mTransitioningViews ! =null && mTransitioningViews.contains(children[index]))) {
children[index].mParent = null; // This code does not satisfy the adjustment is not executed}... Omit some code..... }Copy the code
So the main culprit for this crash is mTransitioningViews.
// The set of views that are currently being transitioned. This list is used to track views
// being removed that should not actually be removed from the parent yet because they are
// being animated.
private ArrayList<View> mTransitioningViews;
Copy the code
That means it’s an arraylist of views that have transition animations. Because they are already animated, they should not actually be removed from the superview. The transition animation here refers to the layout container animation, which is the animation effect when the child view is added and hidden. Seems not fragments animated transitions, but set a layoutAnimation, see layout file, did set the android: animateLayoutChanges
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:orientation="vertical">
Copy the code
Setting this property calls:
public void setLayoutTransition(LayoutTransition transition) {
if(mTransition ! =null) {
LayoutTransition previousTransition = mTransition;
previousTransition.cancel();
previousTransition.removeTransitionListener(mLayoutTransitionListener);
}
mTransition = transition;
if(mTransition ! =null) { mTransition.addTransitionListener(mLayoutTransitionListener); }}Copy the code
Then mLayoutTransitionListener will add the view at the beginning of the animation to mTransitioningViews:
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); }}... Omit some code..... };Copy the code
Problem solving
Can cancel set android: animateLayoutChanges properties, can also remove the animation first, then remove the View:
parent.endViewTransition(mView) mView!! .clearAnimation() parent.removeView(mView)Copy the code