记一次removeView失败引起的崩溃

记一次removeView失败引起的崩溃

Android小彩虹2021-07-16 20:58:55250A+A-

崩溃日志

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) 

问题发现

在开启了转场动画的Fragment中,从一个Fragment跳转到下一个Fragment中,在上一个转场动画未结束时,马上返回上一个页面。先来分析下问题代码:

   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
    }

但是悲催的是,问题依旧。

问题分析

上面明明已经从parent中移除了fragment的mView啊,难道是移除不成功?我们加一下打印:

            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失败的问题。 我们来分析下,找到抛出异常的代码源码:

 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:animateLayoutChanges

    <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:animateLayoutChanges="true" android:orientation="vertical">

设置了该属性会调用:

 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)

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1

联系我们