引言
在日常开发过程中我们肯定遇到过,同一个View如果连续被add两次,会报出下边的错误:
The specified child already has a parent. You must call removeView() on the child's parent first.
错误信息告诉我们此时这个View已经有了parent,并提示我们应该这个view的父容器,在addView之前,应该先调用removeView()方法。
源码分析
1. ViewGroup.addView()
public void addView(View child) {
addView(child, -1);
}
public void addView(View child, int index) {
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
public void addView(View child, int width, int height) {
final LayoutParams params = generateDefaultLayoutParams();
params.width = width;
params.height = height;
addView(child, -1, params);
}
public void addView(View child, int index, LayoutParams params) {
if (DBG) {
System.out.println(this + " addView");
}
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
从上述源码可以看出,调用addView(View child)方法,其实最后调用的是addViewInner(View child, int index, LayoutParams params,boolean preventRequestLayout):
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
if (mTransition != null) {
// Don't prevent other add transitions from completing, but cancel remove
// transitions to let them complete the process before we add to the container
mTransition.cancel(LayoutTransition.DISAPPEARING);
}
//检查child的mParent是否为空
if (child.getParent() != null) {
throw new IllegalStateException("The specified child already has a parent. " +
"You must call removeView() on the child's parent first.");
}
if (mTransition != null) {
mTransition.addChild(this, child);
}
if (!checkLayoutParams(params)) {
params = generateLayoutParams(params);
}
if (preventRequestLayout) {
child.mLayoutParams = params;
} else {
child.setLayoutParams(params);
}
if (index < 0) {
index = mChildrenCount;
}
addInArray(child, index);
// tell our children
//在这里给view的mParent赋值的
if (preventRequestLayout) {
child.assignParent(this);
} else {
child.mParent = this;
}
if (child.hasFocus()) {
requestChildFocus(child, child.findFocus());
}
AttachInfo ai = mAttachInfo;
if (ai != null && (mGroupFlags & FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW) == 0) {
boolean lastKeepOn = ai.mKeepScreenOn;
ai.mKeepScreenOn = false;
child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags&VISIBILITY_MASK));
if (ai.mKeepScreenOn) {
needGlobalAttributesUpdate(true);
}
ai.mKeepScreenOn = lastKeepOn;
}
if (child.isLayoutDirectionInherited()) {
child.resetRtlProperties();
}
onViewAdded(child);
if ((child.mViewFlags & DUPLICATE_PARENT_STATE) == DUPLICATE_PARENT_STATE) {
mGroupFlags |= FLAG_NOTIFY_CHILDREN_ON_DRAWABLE_STATE_CHANGE;
}
if (child.hasTransientState()) {
childHasTransientStateChanged(child, true);
}
if (child.isImportantForAccessibility() && child.getVisibility() != View.GONE) {
notifySubtreeAccessibilityStateChangedIfNeeded();
}
}
这个方法的逻辑比较多,其他的我们先不作分析,从这里可以看到,如果child.getParent() != null,就会抛出这个异常。
2. View的getParent()
/**
* The parent this view is attached to.
* {@hide}
*
* @see #getParent()
*/
protected ViewParent mParent;
/**
* Gets the parent of this view. Note that the parent is a
* ViewParent and not necessarily a View.
*
* @return Parent of this view.
*/
public final ViewParent getParent() {
return mParent;
}
View的getParent()方法返回的是mParent对象,并不是一个View对象,而是ViewParent实现类的对象。ViewParent是一个接口,定义了一组子View与Parent交互的API。ViewGroup是ViewParent接口实现类。下面我们追踪下mParent对象是在怎么被赋值的。
/*
* Caller is responsible for calling requestLayout if necessary.
* (This allows addViewInLayout to not request a new layout.)
*/
void assignParent(ViewParent parent) {
if (mParent == null) {
mParent = parent;
} else if (parent == null) {
mParent = null;
} else {
throw new RuntimeException("view " + this + " being added, but"
+ " it already has a parent");
}
}
追踪可以发现,View的mParent是在assignParent()方法中被赋值的,在该方法中,如果mParent != null && parent != null的时候,会抛出"view XXX being added, but it already has a parent"异常,这个异常其实是跟ViewGroup抛出的那个异常是对应的,算是一种Double Check吧。
assignParent()方法是在ViewGroup的rivate void addViewInner(View child, int index, LayoutParams params,boolean preventRequestLayout)方法有被被调用,从上边给出的ViewGroup->addViewInner()的源码可知,如果preventRequestLayout为true的情况下,ViewGroup就会调用 child.assignParent(this),给child的mParent对象进行赋值。否则,就会直接把自己复制给child的mParent。
3. ViewGroup->removeView
public void removeView(View view) {
removeViewInternal(view);
requestLayout();
invalidate(true);
}
requestLayout()和invalidate(true)分别是请求对其重新测量和重绘,下边重点来看下 removeViewInternal(view)的逻辑。
private void removeViewInternal(View view) {
final int index = indexOfChild(view);
if (index >= 0) {
removeViewInternal(index, view);
}
}
该方法先通过indexOfChild获取View在ViewGroup中的索引index,如果在index>=0的情况下,会调用 removeViewInternal(index, view)方法。
private void removeViewInternal(int index, View view) {
if (mTransition != null) {
mTransition.removeChild(this, view);
}
boolean clearChildFocus = false;
if (view == mFocused) {
view.unFocus();
clearChildFocus = true;
}
if (view.isAccessibilityFocused()) {
view.clearAccessibilityFocus();
}
cancelTouchTarget(view);
cancelHoverTarget(view);
if (view.getAnimation() != null ||
(mTransitioningViews != null && mTransitioningViews.contains(view))) {
addDisappearingView(view);
} else if (view.mAttachInfo != null) {
view.dispatchDetachedFromWindow();
}
if (view.hasTransientState()) {
childHasTransientStateChanged(view, false);
}
needGlobalAttributesUpdate(false);
removeFromArray(index);
if (clearChildFocus) {
clearChildFocus(view);
if (!rootViewRequestFocus()) {
notifyGlobalFocusCleared(this);
}
}
onViewRemoved(view);
if (view.isImportantForAccessibility() && view.getVisibility() != View.GONE) {
notifySubtreeAccessibilityStateChangedIfNeeded();
}
}
通过追查,发现在removeFromArray(index)中,会将当前index的view的mParent置为null。
// 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;
}
final int count = mChildrenCount;
if (index == count - 1) {
children[--mChildrenCount] = null;
} else if (index >= 0 && index < count) {
System.arraycopy(children, index + 1, children, index, count - index - 1);
children[--mChildrenCount] = null;
} else {
throw new IndexOutOfBoundsException();
}
if (mLastTouchDownIndex == index) {
mLastTouchDownTime = 0;
mLastTouchDownIndex = -1;
} else if (mLastTouchDownIndex > index) {
mLastTouchDownIndex--;
}
}
这个方法主要是将ViewGroup的mChildren中相应index的子View给移除,同时也会把相应的子view的mParent置为null,这样这个子view就可以再次被ViewGroup添加了。这也就是为什么我们在报了"The specified child already has a parent. You must call removeView() on the child's parent first."这个异常以后,需要把调用view.getParent().removeAllViews()就可以了。当然,如果确定view在这个ViewGroup中,也可以调用removeView(View view),只把这个子view移除掉就行。
总结
- 在ViewGroup调用addView的时候,会先判断子View的mParent是否为空,不为空则会抛出异常;
- ViewGroup实现了ViewParent接口,在addView的过程中,会把自己复制给子View的mParent;
- 在ViewGroup中,调用removeView(),会清除子View的mParent对象;
本文只是从源码搞清楚了这个异常出现的原因,但是一般我们开发是不应该将一个子view连续add两次的,在特别复杂的布局中,如果真的出现了这个异常,可以尝试调用view.getParent().removew(view)来解决问题,但是这时候也说明你需要梳理你的逻辑是否出现了问题。