源码分析同一个View为什么不能被ViewGroup连续addV

2018-06-23  本文已影响0人  CyanStone

引言

在日常开发过程中我们肯定遇到过,同一个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移除掉就行。

总结

本文只是从源码搞清楚了这个异常出现的原因,但是一般我们开发是不应该将一个子view连续add两次的,在特别复杂的布局中,如果真的出现了这个异常,可以尝试调用view.getParent().removew(view)来解决问题,但是这时候也说明你需要梳理你的逻辑是否出现了问题。

上一篇下一篇

猜你喜欢

热点阅读