(翻译)使用正确的方式绘制布局
文章目录
原文 : 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()* 方法有两种可用的方式。
inflate(int resource, ViewGroup root)
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* 传给第二个参数来省略它。