addView引发的崩溃问题及思考分析

开发过程中经常会通过addView()方法动态添加子控件,如果不注意的话会出现异常:
Caused by: java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child’s parent first.

一、实际案例

下面我们通过一个demo来演示分析:

1、创建自定义控件并提供addTag方法来添加子view
  • 示例代码
class CustomLinearLayout @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet,
    defaultStyle: Int? = 0
) : LinearLayout(context, attributeSet) {

    fun addTag(list: List<String>) {
        if (list.isEmpty()) return
        val tagView = createTagView()
        for (index in list.indices) {
            tagView.text = list[index]
            addView(tagView)
        }

    }

    private fun createTagView(): TextView {
        return LayoutInflater.from(context).inflate(R.layout.item_text, null) as TextView
    }
}
2、直接运行报异常:

Caused by: java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child’s parent first.

3、分析

代码分析:

通过上述代码我们通过createTagView()方法创建一个textView实例,并通过addView的方式将该实例添加到CustomLinearLayout中

运行结果分析:

通过运行,得到的结果是java.lang.IllegalStateException

原因分析:

通过异常提示可知,是因为该child实例已经拥有了一个parent,导致异常出现,也就意味着每一个child实例只能拥有一个parent

4、解决方法

结合上述分析是因为一个实例,已经了parent导致,那么我们可以从两个方面来解决:

  • 方法一:添加前调用removeView

在添加childview之前先进行parent判断,如果不为空就调用removeView

  fun addTag(list: List<String>) {
        if (list.isEmpty()) return
        val tagView = createTagView()
        for (index in list.indices) {
            tagView.text = list[index]
            val parentView = tagView.parent as ViewGroup
            if (parentView != null) {
         parentView.removeView(tagView)
                addView(tagView)
            }
        }

    }
  • 运行效果图
    在这里插入图片描述

但是通过运行发现,当使用同一个child实例时,添加多个子view时只显示了最后一个child,因为child是同一个实例子,与预期不符,该解决方法不起作用

  • 方法二 使用多个实例

每次都创建一个新的child实例来添加到CustomLinearLayout中

class CustomLinearLayout @JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet,
defaultStyle: Int? = 0
) : LinearLayout(context, attributeSet) {

fun addTag(list: List<String>) {
    if (list.isEmpty()) return
    for (index in list.indices) {
        val tagView = createTagView()
        tagView.text = list[index]
        addView(tagView)
    }

}

private fun createTagView(): TextView {
    val contentView =
        LayoutInflater.from(context).inflate(R.layout.item_text, null) as LinearLayout
    return contentView.findViewById<TextView>(R.id.tv_text)
}

}

  • 运行效果图
    在这里插入图片描述

运行结果和预期相符,正常显示多个子控件

5、全部示例代码
  • AddViewDemoActivity
class AddViewDemoActivity :AppCompatActivity(){
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_add_view_demo)
        custom_lin.addTag(listOf("Activity","Fragment","Viewpager"))
    }
}
  • 布局文件 activity_add_view_demo
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.petergee.myapplication.view.customView.CustomLinearLayout
        android:id="@+id/custom_lin"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
  • CustomLinearLayout
class CustomLinearLayout @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet,
    defaultStyle: Int? = 0
) : LinearLayout(context, attributeSet) {

    fun addTag(list: List<String>) {
        if (list.isEmpty()) return
        for (index in list.indices) {
            val tagView = createTagView()
            tagView.text = list[index]
            addView(tagView)
        }

    }

    private fun createTagView(): TextView {
        val contentView =
            LayoutInflater.from(context).inflate(R.layout.item_text, null) as LinearLayout
        return contentView.findViewById<TextView>(R.id.tv_text)
    }
}
  • 布局文件item_text
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="2dp"
        android:textColor="@android:color/black"
        android:textSize="16sp"
        android:id="@+id/tv_text">

    </TextView>
</LinearLayout>
二、源码查看
1、addView(View child)
  /**
     * <p>Adds a child view. If no layout parameters are already set on the child, the
     * default parameters for this ViewGroup are set on the child.</p>
     *
     * <p><strong>Note:</strong> do not invoke this method from
     * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
     * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
     *
     * @param child the child view to add
     *
     * @see #generateDefaultLayoutParams()
     */
    public void addView(View child) {
        addView(child, -1);
    }
2、addView(View child, int index)
    /**
     * Adds a child view. If no layout parameters are already set on the child, the
     * default parameters for this ViewGroup are set on the child.
     *
     * <p><strong>Note:</strong> do not invoke this method from
     * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
     * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
     *
     * @param child the child view to add
     * @param index the position at which to add the child
     *
     * @see #generateDefaultLayoutParams()
     */
    public void addView(View child, int index) {
        if (child == null) {
            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
        }
        LayoutParams params = child.getLayoutParams();
        if (params == null) {
            params = generateDefaultLayoutParams();
            if (params == null) {
                throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
            }
        }
        addView(child, index, params);
    }

if (child == null) {
throw new IllegalArgumentException(“Cannot add a null child view to a ViewGroup”);
}

  • child为空,同样会报异常
  • layoutParams为空,同样会报异常
3、 addView(child, index, params)
  /**
     * Adds a child view with the specified layout parameters.
     *
     * <p><strong>Note:</strong> do not invoke this method from
     * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
     * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
     *
     * @param child the child view to add
     * @param index the position at which to add the child or -1 to add last
     * @param params the layout parameters to set on the child
     */
    public void addView(View child, int index, LayoutParams params) {
        if (DBG) {
            System.out.println(this + " addView");
        }

        if (child == null) {
            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
        }

        // 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);
    }

关键方法addViewInner(child, index, params, false);

4、addViewInner(child, index, params, false)
 /**
     * Prevents the specified child to be laid out during the next layout pass.
     *
     * @param child the child on which to perform the cleanup
     */
    protected void cleanupLayoutState(View child) {
        child.mPrivateFlags &= ~View.PFLAG_FORCE_LAYOUT;
    }

    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);
        }

        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
        if (preventRequestLayout) {
            child.assignParent(this);
        } else {
            child.mParent = this;
        }
        if (child.hasUnhandledKeyListener()) {
            incrementChildUnhandledKeyListeners();
        }

        final boolean childHasFocus = child.hasFocus();
        if (childHasFocus) {
            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();
        }

        dispatchViewAdded(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.getVisibility() != View.GONE) {
            notifySubtreeAccessibilityStateChangedIfNeeded();
        }

        if (mTransientIndices != null) {
            final int transientCount = mTransientIndices.size();
            for (int i = 0; i < transientCount; ++i) {
                final int oldIndex = mTransientIndices.get(i);
                if (index <= oldIndex) {
                    mTransientIndices.set(i, oldIndex + 1);
                }
            }
        }

        if (mCurrentDragStartEvent != null && child.getVisibility() == VISIBLE) {
            notifyChildOfDragStart(child);
        }

        if (child.hasDefaultFocus()) {
            // When adding a child that contains default focus, either during inflation or while
            // manually assembling the hierarchy, update the ancestor default-focus chain.
            setDefaultFocus(child);
        }

        touchAccessibilityNodeProviderIfNeeded(child);
    }

if (child.getParent() != null) {
throw new IllegalStateException("The specified child already has a parent. " +
“You must call removeView() on the child’s parent first.”);
}

至此,我们找到了报错的出处,是因为通过child.getParent() != null来进行判断的,如果child.getParent() != null,则报异常。

三、小结

通过一步步分析,问题就解决了,工作中遇到问题在所难免,遇到问题要善于分析问题出现的原因,时间允许的情况下尽量通过查看源码找到问题出现的点,并做好笔记,这样的话才能有所进步和提高。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值