1. 前言

最近在做动态添加 View 的效果,给动态添加的 View 设置了 margin 属性,但却总是被忽略,不起作用。原因是出在 LayoutInflater 上。
LayoutInflater 有三种加载方式,但是想要使动态加载的view的属性生效,实际上取决于我们使用的 LayoutInflate r的方法。

2. 获得 LayoutInflater 的三种方式

获得 LayoutInflater 有三种方式
方法一:

//调用Activity的getLayoutInflater() 
LayoutInflater inflater = getLayoutInflater();

方法二:

LayoutInflater inflater = LayoutInflater.from(context);

方法三:

LayoutInflater inflater =  (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

3. inflate 方法

inflate 方法从大范围来看,分两种,三个参数的构造方法和两个参数的构造方法。

3.1 三个参数的 inflate 方法

方法头如下:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)

这里分三种情况

3.1.1 root 不为 null,attachToRoot 为 true

当 root 不为 null,attachToRoot 为 true时,表示将 resource 指定的布局添加到 root 中,添加的过程中 resource 所指定的布局的根节点的各个属性都是有效的。
比如下面一个案例,我的Activity的布局如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"   
    android:id="@+id/ll"   
    tools:context="org.sang.layoutinflater.MainActivity">
</LinearLayout>

还有一个布局linearlayout.xml如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
       android:id="@+id/ll"
       android:layout_width="200dp"
       android:layout_height="200dp"
       android:background="@color/colorPrimary"
       android:gravity="center"
       android:orientation="vertical">
       
       <Button
             android:layout_width="wrap_content"
             android:layout_height="wrap_content" />
</LinearLayout>

现在想把这个 linearlayout.xml 布局文件添加到 activity 布局中,可以这样做:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        LinearLayout ll = (LinearLayout) findViewById(R.id.ll);
        LayoutInflater inflater = LayoutInflater.from(this);
        inflater.inflate(R.layout.linearlayout, ll,true);
    }

伙伴们注意到,这里没写将 inflate 出来的 View 添加到 ll 中的代码,但是 linearlayout 布局文件就已经添加进来了,这就是因为我第三个参数设置为了true,表示将第一个参数所指定的布局添加到第二个参数的View中。最终显示效果如下:

Android 动态设置ViewHolder margin_xml


如果作死多写这么一行代码,如下:

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        LinearLayout ll = (LinearLayout) findViewById(R.id.ll);
        LayoutInflater inflater = LayoutInflater.from(this);
        View view = inflater.inflate(R.layout.linearlayout, ll, true);
        ll.addView(view);
    }

这个时候再运行,系统会抛如下异常:

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

原因就是因为当第三个参数为 true 时,会自动将第一个参数所指定的 View 添加到第二个参数所指定的 View 中。

3.1.2 root 不为 null,attachToRoot 为 false

如果 root 不为 null,而 attachToRoot 为 false 的话,表示不将第一个参数所指定的 View 添加到 root 中。
那么问题来了,既然不添加到 root 中,那我还写这么多干嘛?第二个参数直接给null不就可以了?
其实不然,这里涉及到另外一个问题:我们在开发的过程中给控件所指定的 layout_width 和 layout_height 到底是什么意思?该属性表示一个控件在容器中的大小,就是说这个控件必须在容器中,这个属性才有意义,否则无意义。这就意味着如果我直接将 linearlayout 加载进来而不给它指定一个父布局,则 inflate 布局的根节点的 layout_width 和 layout_height 属性将会失效(因为这个时候 linearlayout 将不处于任何容器中,那么它的根节点的宽高自然会失效)。如果我想让 linearlayout 的根节点有效,又不想让其处于某一个容器中,那我就可以设置 root 不为 null,而 attachToRoot 为 false。这样,指定root的目的也就很明确了,即root会协助linearlayout的根节点生成布局参数,只有这一个作用。

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        LinearLayout ll = (LinearLayout) findViewById(R.id.ll);
        LayoutInflater inflater = LayoutInflater.from(this);
        View view = inflater.inflate(R.layout.linearlayout, ll, false);
        ll.addView(view);
    }

注意,这个时候需要手动的将 inflate 加载进来的 view 添加到 ll 容器中,因为 inflate 的最后一个参数 false 表示不将 linealayout 添加到 ll 中。显示效果和上文一样,不再贴图。
采用这种方式,可设置子 View 的 margin,不会使属性失效。

/**
 * 1、设置外边距必须在addView之后进行,不然获取不到父类的布局属性
 * 2、LayoutInflater需要这样调用,才能让子控件的 margin 属性生效
 * LayoutInflater.from(context).inflate(R.layout.lab_title_view, parentLayout, false);
*/
 if(!TextUtils.isEmpty(components.margins)){
        String[] margins = components.margins.split("&&");
         setMargins(windowsManagerUtil.dip2px(Integer.parseInt(margins[0])),
                    windowsManagerUtil.dip2px(Integer.parseInt(margins[1])),
                    windowsManagerUtil.dip2px(Integer.parseInt(margins[2])),
                    windowsManagerUtil.dip2px(Integer.parseInt(margins[3])));
 }


    /**
     * 设置LabTitleView的外边距
     * @param left
     * @param top
     * @param right
     * @param bottom
     */
    private void setMargins(int left, int top, int right, int bottom){
        ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
        params.setMargins(left, top, right, bottom);
        view.setLayoutParams(params);
    }
3.1.3 root 为 null

当 root 为 null 时,不论 attachToRoot 为 true 还是为 false,显示效果都是一样的。当 root 为 null 表示我不需要将第一个参数所指定的布局添加到任何容器中,同时也表示没有任何容器来来协助第一个参数所指定布局的根节点生成布局参数。
还是使用上文提到的linearlayout,我们来看下面一段代码:

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        LinearLayout ll = (LinearLayout) findViewById(R.id.ll);
        LayoutInflater inflater = LayoutInflater.from(this);
        View view = inflater.inflate(R.layout.linearlayout, null, false);
        ll.addView(view);
    }

当第二个参数为 null,第三个参数为 false 时(即使为 true 显示效果也是一样的,这里以 false 为例),由于在 inflate 方法中没有将 linearlayout 添加到某一个容器中,所以需要手动添加,另外由于 linearlayout 并没有处于某一个容器中,所以它的根节点的宽高属性会失效,显示效果如下:

Android 动态设置ViewHolder margin_宽高_02


注意,这个时候不管给 linearlayout 的根节点的宽高设置什么,都是没有效果的,它都是包裹 button,如果我修改 button,则 button 会立即有变化,因为 button 是处于某一个容器中的。

3.2 两个参数的 inflate 方法

两个参数的 inflate 方法就很简单了,来稍微看一点点源码:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
        return inflate(parser, root, root != null);
}

这是两个参数的 inflate 方法,注意两个参数实际上最终也是调用了三个参数。
两个参数的 inflate 方法分为如下两种情况:

  • root 为 null,等同于 3.1.3 所述情况。
  • root 不为 null,等同于 3.1.1 所述情况。

4. 为什么Activity布局的根节点的宽高属性会生效?

inflate 方法我们已经说完了,小伙伴们可能有另外一个疑问,那为什么 Activity 布局的根节点的宽高属性会生效?
其实原因很简单,大部分情况下我们一个 Activity 页面由两部分组成(Android 的版本号和应用主题会影响到 Activity 页面组成,这里以常见页面为例),我们的页面中有一个顶级 View 叫做 DecorView,DecorView 中包含一个竖直方向的 LinearLayout,LinearLayout 由两部分组成,第一部分是标题栏,第二部分是内容栏,内容栏是一个 FrameLayout,我们在 Activity 中调用 setContentView 就是将View添加到这个 FrameLayout 中,所以给大家一种错觉仿佛 Activity 的根布局很特殊,其实不然。