如何实现动态添加布局文件(避免 The specified child already has a parent的问题)

首先扯点别的:我应经连续上了两个星期的班了,今天星期一。是第三个周。这个班上的也是没谁了。最近老是腰疼。估计是累了。最近也没跑步。今天下班继续跑起。

这篇文章讲一讲如何在一个布局文件中动态加在一个布局文件。避免出现The specified child already has a parent. You must call removeView() on the child’s parent first.的问题。先看一看效果再说。
这里写图片描述

接下来是实现过程 首先是 activity_add_view.xml文件

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="com.example.administrator.learnaddview.AddViewActivity">

<!--我们要在LinearLayout里面动态添加布局 现在这个LinearLayout里面只有三个textView-->
    <LinearLayout
        android:id="@+id/linearLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal"
        android:layout_marginTop="30dp"
        android:orientation="vertical">

        <TextView
            android:id="@+id/textView1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="textView1"
            android:textSize="30dp" />

        <TextView
            android:id="@+id/textView2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="textView2"
            android:textSize="30dp" />

        <TextView
            android:id="@+id/textView3"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="textView3"
            android:textSize="30dp" />
    </LinearLayout>

<!--一个按钮用来添加布局-->
    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        android:src="@android:drawable/ic_input_add" />

</android.support.design.widget.CoordinatorLayout>

然后是AddViewActivity.java代码

package com.example.administrator.learnaddview;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;

public class AddViewActivity extends AppCompatActivity {

    private ViewGroup parentViewGroup;//父布局
    /**
     * A static list of country names.
     */
    private static final String[] COUNTRIES = new String[]{
            "Belgium", "France", "Italy", "Germany", "Spain",
            "Austria", "Russia", "Poland", "Croatia", "Greece",
            "Ukraine",
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_add_view);
        //找到想动态添加子view的布局容器就是上面布局中的LinearLayout
        parentViewGroup = (ViewGroup) findViewById(R.id.linearLayout);

        //找到浮动按钮并添加监听事件
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        if (fab != null) {
            fab.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    //要被添加的子布局
 final ViewGroup childViewGroup = (ViewGroup)
 LayoutInflater.from(AddViewActivity.this).inflate(R.layout.beaddlayout, parentViewGroup, false);
                    //子布局中的TextView控件
                TextView textView = (TextView) childViewGroup.findViewById(R.id.text1);
                    //给textview随机设置一个文本
                textView.setText(COUNTRIES[(int) (Math.random() * COUNTRIES.length)]);
                    //子布局中的ImageButton控件
 ImageButton imageButton= (ImageButton) childViewGroup.findViewById(R.id.delete_button);
                    //给imageButton设置监听事件,当点击的时候就把这个刚添加的子布局从其父布局中删除掉
                    imageButton.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            parentViewGroup.removeView(childViewGroup);
                        }
                    });
                    parentViewGroup.addView(childViewGroup);

                }
            });
        }
    }

}

上面的beaddlayout.xml文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="?android:listPreferredItemHeightSmall"
    android:divider="?android:dividerVertical"
    android:dividerPadding="8dp"
    android:gravity="center"
    android:orientation="horizontal"
    android:showDividers="middle">

    <!-- 随机显示一个字符串 -->
    <TextView
        android:id="@+id/text1"
        style="?android:textAppearanceMedium"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:paddingLeft="?android:listPreferredItemPaddingLeft" />

    <!-- 当按钮点击的时候会把这个布局文件从其父布局中移除-->
    <ImageButton
        android:id="@+id/delete_button"
        android:layout_width="48dp"
        android:layout_height="match_parent"
        android:background="?android:selectableItemBackground"
        android:contentDescription="remove"
        android:src="@drawable/ic_list_remove" />
</LinearLayout>

下面来仔细讲解一下实现步骤
1,找到想添加布局的父布局

//找到想动态添加子view的布局容器就是上面布局中的LinearLayout
 parentViewGroup = (ViewGroup) findViewById(R.id.linearLayout);

2过滤将要被添加的布局文件到父布局中,父布局就是第一步骤中的parentViewGroup

                  //要被添加的子布局

 /*inflate方法有三个参数
 第一个参数:R.layout.beaddlayout 要被加载的布局
 第二个参数:parentViewGroup 要被加载到那里
 第三个参数:取值有true和false两种,等会我们试一试取值为true的情况*/
 final ViewGroup childViewGroup = (ViewGroup)
 LayoutInflater.from(AddViewActivity.this).inflate(R.layout.beaddlayout, parentViewGroup, false);

3:这一步是可选的。通过childViewGroup找到其中的iew,并添加监听事件等等操作。

/*子布局中的TextView控件*/
                    TextView textView = (TextView) childViewGroup.findViewById(R.id.text1);
                    textView.setText(COUNTRIES[(int) (Math.random() * COUNTRIES.length)]);

                    //子布局中的ImageButton控件
  ImageButton imageButton= (ImageButton)childViewGroup.findViewById(R.id.delete_button);
                 //给imageButton设置监听事件,当点击的时候就把这个刚添加的子布局从其父布局中删除掉
                    imageButton.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            //parentViewGroup.removeView(childViewGroup);
                            parentViewGroup.removeView(childViewGroup);
                        }
                    });

4:把子布局添加到父布局中。大功告成。

 parentViewGroup.addView(childViewGroup);

在上面的第二步中

/*如果把最后一个参数取值为true,调用addView()的时候就会出现 the specified child already has a parent ,you must call the removeView() ....的问题*/
final ViewGroup childViewGroup = (ViewGroup)
//第三个参数取值为true
 LayoutInflater.from(AddViewActivity.this).inflate(R.layout.beaddlayout, parentViewGroup, true);

我们进入addView()方法,经过辗转反侧,我们会进入一个方法叫 addViewInner(child, index, params, false);异常就是在这里抛出的。但是这个child.getParent() != null我是真的不是很理解。

我的一个尝试性的理解是这样的

  1. 当LayoutInflater.from(AddViewActivity.this).inflate(R.layout.beaddlayout, parentViewGroup, true);//第三个参数取值为true的时候,这个方法返回childViewGroup 是在我们的R.layout.beaddlayout外层套上一层布局(就是我们本来打算加入的布局parentViewGroup)的布局。
  2. 取值为false的时候就直接返回我们的R.layout.beaddlayout。
  3. 所以当我们调用addView()方法方法的时候,由于我们的子布局已经套在parentViewGroup里面了。我们调用child.getParent() 得到的就是一个LinaerLayout(我们的父布局parentViewGroup).不为空,所以这时候,就会抛出一个异常。
private void addViewInner(View child, int index, 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.");
        }
        ....//省略后面的代码

关于这个取值为true或者false的问题解释,我在网上找了很多说明,也没整明白,看了看这个方法的源码也还是难以理解,所以我把这个方法的源码贴出来,大家自己推敲一下。

  /**
     * Inflate a new view hierarchy from the specified xml resource. Throws
     * {@link InflateException} if there is an error.
     * 
     * @param resource ID for an XML layout resource to load (e.g.,
     *        <code>R.layout.main_page</code>)
     * @param root Optional view to be the parent of the generated hierarchy (if
     *        <em>attachToRoot</em> is true), or else simply an object that
     *        provides a set of LayoutParams values for root of the returned
     *        hierarchy (if <em>attachToRoot</em> is false.)
     * @param attachToRoot Whether the inflated hierarchy should be attached to
     *        the root parameter? If false, root is only used to create the
     *        correct subclass of LayoutParams for the root view in the XML.
     * @return The root View of the inflated hierarchy. If root was supplied and
     *         attachToRoot is true, this is root; otherwise it is the root of
     *         the inflated XML file.
     */
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
        }

        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

在第四步中,我们使用的是 parentViewGroup.addView(childViewGroup);来添加布局。这个方法还有其他几个重载的方法。如下

/*index 参数用来指出在父布局中的什么位置添加这个子view,取值是有讲究的。*/
 addView(View child, int index);
 /* 这个方法可以用来明确指出子view的宽高*/
 addView(View child, int width, int height);
 /*给子布局明确提供一个布局参数*/
 addView(View child, LayoutParams params);
 /*给子view提供位置信息,和布局参数*/
 addView(View child, int index, LayoutParams params)

接下来说一说addView(View child, int index);这个方法余下的几个方法就不说了。

在代码中我们把 parentViewGroup.addView(childViewGroup);改成 parentViewGroup.addView(childViewGroup,0);看一看有什么效果。

这里写图片描述

我们发现子布局被动态添加到了父布局的上面。

我们再改成 parentViewGroup.addView(childViewGroup,1);看一看有什么效果。

这里写图片描述

我们再改成 parentViewGroup.addView(childViewGroup,3);看一看有什么效果。
这里写图片描述

我们再改成 parentViewGroup.addView(childViewGroup,4);看一看有什么效果。
不用看了,当我们点击按钮的时候,程序直接崩溃了。我们看一看Log
java.lang.IndexOutOfBoundsException: index=4 count=3
at android.view.ViewGroup.addInArray(ViewGroup.java:3653)
at android.view.ViewGroup.addViewInner(ViewGroup.java:3584)
at android.view.ViewGroup.addView(ViewGroup.java:3415)
at android.view.ViewGroup.addView(ViewGroup.java:3360)

看到这里我想大家也许明白了点什么。我们的父布局LinearLayout中,原本只有三个TextView控件。我们index从0到2.当绘制子view的时候从0到index。而因为我们直接把index设为了4。我们这时父布局中只有4个view,从0到3,所以当遍历到4的时候就出现数组越界。

贴一下ViewGroup.java 3653的代码

  /**
     * By default, children are clipped to their bounds before drawing. This
     * allows view groups to override this behavior for animations, etc.
     *
     * @param clipChildren true to clip children to their bounds,
     *        false otherwise
     * @attr ref android.R.styleable#ViewGroup_clipChildren
     */
    public void setClipChildren(boolean clipChildren) {
        boolean previousValue = (mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN;
        if (clipChildren != previousValue) {
            setBooleanFlag(FLAG_CLIP_CHILDREN, clipChildren);
            for (int i = 0; i < mChildrenCount; ++i) {
            //这是3653行,我怀疑数组越界肯定和mChildrenCount有必然的联系。继续寻找原因。
                View child = getChildAt(i);
                if (child.mRenderNode != null) {
                    child.mRenderNode.setClipToBounds(clipChildren);
                }
            }
            invalidate(true);
        }
    }

1:我们进入addView(View child, int index)的源码找一找这个mChildrenCount在哪里。

 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");
            }
        }
        //看着一行,调用这个方法添加view,并使用一个默认的布局params参数
        addView(child, index, params);
    }

2 我们接着跳进addView(child, index, params);源码

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

3进入addViewInner(child, index, params, false);方法,这个方法有点长,其中有一行代码太显眼了

addInArray(child, index);

4.我们继续跳入这个方法

private void addInArray(View child, int index) {
        View[] children = mChildren;
        final int count = mChildrenCount;//把mChildrenCount赋给count
        final int size = children.length;
        //如果index==count
        if (index == count) {
            if (size == count) {
                mChildren = new View[size + ARRAY_CAPACITY_INCREMENT];
                System.arraycopy(children, 0, mChildren, 0, size);
                children = mChildren;
            }
            children[mChildrenCount++] = child;
        } //如果index<count
        else if (index < count) {
            if (size == count) {
                mChildren = new View[size + ARRAY_CAPACITY_INCREMENT];
                System.arraycopy(children, 0, mChildren, 0, index);
                System.arraycopy(children, index, mChildren, index + 1, count - index);
                children = mChildren;
            } else {
                System.arraycopy(children, index, children, index + 1, count - index);
            }
            children[index] = child;
            mChildrenCount++;
            if (mLastTouchDownIndex >= index) {
                mLastTouchDownIndex++;
            }
        } 
        //这个else也是没谁了,就是这里了。
        else {
            throw new IndexOutOfBoundsException("index=" + index + " count=" + count);
        }
    }

源码中定义的mChildrenCount是代表mChildren数组的长度,也就是当前布局中view的个数

 // Child views of this ViewGroup
    private View[] mChildren;
    // Number of valid children in the mChildren array, the rest should be null or not
    // considered as children
    private int mChildrenCount;

这个追踪也是欠妥,但是大家应该大致明白了是怎么回事,有兴趣的可以自己看看ViewGroup的源码自己找一找。

结尾:我的文章写得比较菜,欢迎大家提出疑问和指出错误。行,歇一歇,喝杯水。

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页