崩溃日志
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)