原文 : https://possiblemobile.com/2013/05/layout-inflation-as-intended/

绘制(inflation,翻译下同)布局(Layout)是在 Android 的 Context 中使用的术语,它的作用是解析一个 XML 布局资源并且把这个资源转换到视图对象的层级(hierarchy)中

这在 Android SDK 中很长用,但你会很惊奇的发现有一种错误的方式使用*LayoutInflater*,并且你的应用可能正在错误的使用它。如果你曾经在你的 Android 应用中,使用*LayoutInflater*写过类似下面的代码,

inflater.inflate(R.layout.my_layout, null);

那么请继续阅读,因为你用错了,我想告诉你为什么你是错的。

了解  LayoutInflater

首先,我们看一下*LayoutInflater*是如何工作,对于常见的应用,*inflate()*  方法有两种可用的方式。

  1. inflate(int resource, ViewGroup root)
  2. inflate(int resource, ViewGroup root, boolean attachToRoot)

第一个参数(resource)指出哪个布局资源想要被绘制。第二个参数是你绘制的资源将会添加到该根视图的层级中(翻译不好,附上原文:The second parameter is the root view of the hierarchy you are inflating the resource to attach to.)。当第三个参数出现的时候,它控制绘制好的视图是否附着到所提供的根视图上。

后两个参数可能会让人困惑。对于这个方法的两个参数版本,*LayoutInflater*将会自动尝试将绘制好的视图附加到提供的根视图上。然而,系统会做一个检查,防止你给根视图传  *null*  导致的应用崩溃。

许多开发者使用这种方式,意味着给根视图传*null*是禁止绘制的视图附着的正确方法;在大部分情况下,甚至没有意识到  *inflate()*  方法有三个参数的版本。这样做我们同时也禁止了根视图拥有的另外一种很重要的功能——但是我走的太快了,你们可能会跟不上(翻译不好,附上原文:but I’m getting ahead of myself.)。

Android 系统的例子

在系统希望开发者绘制视图的地方,让我们做一些实验。

Adapters是使用*LayoutInflater*最常用的地方,通常用在自定义Listview的*Adapter*中,重载  *getView()*方法,这个方法有如下的方法签名:

getView(int position, View convertView, ViewGroup parent)

Fragments在创建视图的时候也使用*LayoutInflater*,通过*onCreateView()*方法;注意它的方法签名:

onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)

你是否注意到,每次系统想让你绘制布局的时候,它都会传给你最终要附着到的父*ViewGroup*?同时在大部分情况下(包括以上两个例子),在*LayoutInflater*被允许将绘制好的视图附加到根视图的时候,它将会抛出一个异常。

当我们不应该附着到*ViewGroup*的时候,为什么我们会需要它?原来,在绘制过程中,父视图是很重要的部分,因为在  XML  文件被绘制的过程中,计算在根元素声明的*LayoutParams*需要它  。在这里传*null*相当于告诉系统“抱歉,我不知道需要附着到哪个父视图上”。

这样做的问题是*android:layout_xxx*  属性总是在父视图的*Context*中被计算。因此,如果没有父视图的信息,在XML树中,所有的根元素声明的*LayoutParams*将会被丢弃,然后,你将会问“为什么系统忽略我声明的*Layout*属性,我需要好好检查一下为什么,看看是不是哪里有 bug”。

没有了LayoutParams, 最终承载绘制布局的*ViewGroup*将会生成一个默认的设置给你。  如果你足够幸运(大多数情况下都是),这些默认的参数和你在  XML 中声明的一样——因此遮盖了这里有错误的事实。

应用例子

因此你说你从没在应用中见过它出现?请看下面的布局,我们用它来绘制  *ListView*  的行:

R.layout.item_row

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="?android:attr/listPreferredItemHeight"
    android:gravity="center_vertical"
    android:orientation="horizontal">
    <TextView
        android:id="@+id/text1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingRight="15dp"
        android:text="Text1" />
    <TextView
        android:id="@+id/text2"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="Text2" />
</LinearLayout>

我们希望把每行的高度设置为固定的高度,在这种情况下,首选项的高度和当前的主题相关——看起来很合理。

然后,当我们以错误的方式绘制布局的时候

public View getView(int position, View convertView, ViewGroup parent) {
    if (convertView == null) {
        convertView = inflate(R.layout.item_row, null);
    }
    return convertView;
}

我们得到的结果看起来像这样:

我们设置的固定高度出了什么情况?

如果我们替换绘制布局的方式:

public View getView(int position, View convertView, ViewGroup parent) {
    if (convertView == null) {
        convertView = inflate(R.layout.item_row, parent, false);
    }
    return convertView;
}

我们得到的结果就如我们希望的那样。

每个规则都有例外(文中的例外在现在的 Android 版本已经不存在了,所以这一节就不翻译了)

所以,下一次你想给  *inflate()*方法传  *null*的时候,你需要停下来并问一问你自己“我真的不知道这个视图的根视图是什么吗?(这句话我没有理解作者的意思,就按我觉得翻译了,do I really not know where this view will end up?)”

最后,当第三个参数为 true 时,你可以使用两个参数版本的*inflate()*  来省它。当第三个参数为 false 的时候,你不应该将  *null*  传给第二个参数来省略它。