[关闭]
@flyouting 2014-06-26T15:32:09.000000Z 字数 3127 阅读 4753

按照预期初始化布局--layout inflation as intended

android layout inflation


Layout inflation是一个android中的术语,用于表达一个XML布局源文件被解析转变成一个View的层次结构对象。

这在AndroidSDK中是常见的做法,但你也许会惊讶地发现,有一个错误的方式去使用LayoutInflater,你的应用也许就是其中之一。如果你在你的Android应用程序曾经写过类似下面的代码:

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

请继续读下去,因为你做错了,我想向你解释为什么。

了解LayoutInflater

首先我们来看一看 LayoutInflater 是怎么工作的。这有两种inflate()方法可用的方式:

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

第一个参数指向你想要的图形化的布局资源。第二个参数是你图形化资源后所要依附的根view。第三个参数是否直接将图形化后的view添加进根view。

后边这两个参数,可能会导致一些混乱。在两个参数的方法中,LayoutInflater 将自动尝试将view附加到根view上。然而,Android框架在这有个检查,如果root参数你传的null,那么它就会绕过这个操作以防止应用崩溃。

很多开发者采用这种方式意思是这是种正确的方式,即root参数传null,去禁止在图形化过程中自动附加上view。很多情况下,他们都没有意识到有三个参数的inflate()方法存在,这种方式也导致了我们禁掉了根view的一些重要功能。

Framework中的例子

我们来检查一下在Android中,框架希望我们作为开发者inflate部分视图的情况:

适配器中使用LayoutInflater的最常见的方式就是自定义Adapter,重写getView方法:

  1. getView(int position, View convertView, ViewGroup parent)

Fragment也经常通过onCreateView()方法使用LayoutInflater来创建view:

  1. onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)

注意到没,每当系统想让你添加一个布局,都会传给你一个父viewgroup,而且,在大多数情况下(包括以上两个例子),如果允许LayoutInflater自动将view添加进父视图,就会抛出一个异常。

那么,如果我们不想直接添加上去,为什么又要我们给出这个viewgroup呢?原来,父view是inflate过程中很重要的一部分,因为我们需要取得XML中根元素的LayoutParams。这里传null意思就好比我也不知道这个view要添加到哪。

这么做的问题是 android:layout_xxx这些属性是在父view的context中获取到。因此,如果没有具体的父view,在XML根元素声明的LayoutParams都会被抛弃。然后你会困惑为啥系统忽略了我设置的这些属性。

没有 LayoutParams,生成view添加进viewgroup时就会依照默认属性。掩盖了你所设置的真正属性。

程序实例

你要说你从来没见过这种情况,那看如下代码:

R.layout.item_row

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:layout_width="match_parent"
  3. android:layout_height="?android:attr/listPreferredItemHeight"
  4. android:gravity="center_vertical"
  5. android:orientation="horizontal">
  6. <TextView
  7. android:id="@+id/text1"
  8. android:layout_width="wrap_content"
  9. android:layout_height="wrap_content"
  10. android:paddingRight="15dp"
  11. android:text="Text1" />
  12. <TextView
  13. android:id="@+id/text2"
  14. android:layout_width="0dp"
  15. android:layout_height="wrap_content"
  16. android:layout_weight="1"
  17. android:text="Text2" />
  18. </LinearLayout>

我们想把item的高度设置成一个特定值,在这种情况下,优先选定为当前主题,看起来是合理的。

然而当我们以错误的方式生成时:

  1. public View getView(int position, View convertView, ViewGroup parent) {
  2. if (convertView == null) {
  3. convertView = inflate(R.layout.item_row, null);
  4. }
  5. return convertView;
  6. }

我们得到的结果如此:
此处输入图片的描述

我们设置的行高呢?这就是常见的我们设置了高度,却最终是自适应的情况。

如果我们换一种实现方式:

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

我们就会得到我们希望的结果:
此处输入图片的描述

每个规则都有例外

当然有些情况下你可以发现parent确实是个null,但是这种情况很少,其中一例是,初始化一个自定义的布局,然后添加进一个AlertDialog,看一下如下代码:

  1. AlertDialog.Builder builder = new AlertDialog.Builder(context);
  2. View content = LayoutInflater.from(context).inflate(R.layout.item_row, null);
  3. builder.setTitle("My Dialog");
  4. builder.setView(content);
  5. builder.setPositiveButton("OK", null);
  6. builder.show();

这里的问题是,AlertDialog.Builder支持一个自定义view,但是没有提供一个支持传入布局资源ID的setView()方法,所以,我们需要先手动初始化我们的布局XML,然后,因为初始化后的对象是要被添加进Dialog,而不是其他的什么根view(事实上也不存在),我们没有进入父一级布局,所以实例化过程中我们不能使用它,但这无关紧要,因为AlertDialog会清除布局的任何LayoutParams,且用match_parent取代。

所以,当你下次又想直接在inflate()方法中传null时,先问下自己,你真的知道这个view最后如何展示么。

最后,你需要记住两个参数的inflate()方法省略的参数代表true,而不是你所认为的代表的false。

原文地址
翻译:flyouting

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注