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