記一次removeView失敗引發的崩潰

崩潰日誌

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: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) 複製代碼

問題發現java

在開啓了轉場動畫的Fragment中,從一個Fragment跳轉到下一個Fragment中,在上一個轉場動畫未結束時,立刻返回上一個頁面。先來分析下問題代碼:android

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
    }
複製代碼

問題就在於返回的mView若是已存在,則可能已經有了parent,致使被二次添加,修改代碼以下:數組

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?.parent as ViewGroup?)?.removeView(mView)
        }
        return mView
    }
複製代碼

可是悲催的是,問題依舊。bash

問題分析

上面明明已經從parent中移除了fragment的mView啊,難道是移除不成功?咱們加一下打印:app

var parent = (rootView?.parent as ViewGroup?)
            Log.d("onCreateView ", "$parent ")
            if (parent != null) {
                parent.removeView(rootView)
            }
            parent= (rootView?.parent as ViewGroup?)
            Log.d("onCreateView ", "$parent ")
複製代碼

果真,第二次打印的結果,parent仍然和第一次同樣,不爲null。問題定位到了,竟然是removeView失敗的問題。 咱們來分析下,找到拋出異常的代碼源碼:ide

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 {
          ...
        }
複製代碼

因此接下來須要找到將child.getParent()置空的代碼,分析下爲何沒執行 removeView(View view)的過程會調用removeViewInternal方法:佈局

private void removeViewInternal(int index, View view) {

		...省略若干代碼.....
		//判斷當前的view 正在播放,或預約播放的動畫
        if (view.getAnimation() != null ||
                (mTransitioningViews != null && mTransitioningViews.contains(view))) {
            addDisappearingView(view);
        } else if (view.mAttachInfo != null) {
           view.dispatchDetachedFromWindow();
        }
		...省略若干代碼.....
        removeFromArray(start, count);
    }
複製代碼
// 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; //這段代碼不知足調節沒有執行
        }
      	...省略若干代碼.....
    }
複製代碼

因此發生這次崩潰的罪魁禍首就是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;
複製代碼

意思就是它是來存儲有過渡動畫的view的一個數組列表。由於它們已經設置了動畫,所以實際上不該該從父視圖中刪除。 這裏的過渡動畫指的是佈局容器動畫(LayoutTransition),就是在添加、隱藏子view 的時候,會有動畫效果。 看來問題並非Fragment的轉場動畫問題,而是設置了layoutAnimation,來看下佈局文件,確實設置了android:animateLayoutChangesui

<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:animateLayoutChanges="true" android:orientation="vertical">
複製代碼

設置了該屬性會調用:google

public void setLayoutTransition(LayoutTransition transition) {
        if (mTransition != null) {
            LayoutTransition previousTransition = mTransition;
            previousTransition.cancel();
            previousTransition.removeTransitionListener(mLayoutTransitionListener);
        }
        mTransition = transition;
        if (mTransition != null) {
            mTransition.addTransitionListener(mLayoutTransitionListener);
        }
    }
複製代碼

而後再mLayoutTransitionListener會在開始動畫時將view加入到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);
            }
        }
	...省略若干代碼.....
      
    };
複製代碼

問題解決

能夠取消設置android:animateLayoutChanges屬性,也能夠先清除下動畫,再移除View:

parent.endViewTransition(mView)
    mView!!.clearAnimation()
    parent.removeView(mView)
複製代碼
相關文章
相關標籤/搜索