原文地址:
Android最佳性能实践(四)——布局优化技巧 - 郭霖的专栏 - 博客频道 - CSDN.NET
http://blog.csdn.net/guolin_blog/article/details/43376527
一、<include>
<include>标签可以允许在一个布局当中引入另外一个布局,那么比如说我们程序的所有界面都有一个公共的部分,这个时候最好的做法就是将这个公共的部分提取到一个独立的布局文件当中,然后在每个界面的布局文件当中来引用这个公共的布局。
目前几乎所有的软件都会有一个头布局,头布局中可以包含界面的标题、返回按钮等。有些软件是使用ActionBar来实现的,但是ActionBar的灵活性不太好。如果自己去实现的话,由于这个头布局是在所有界面都要使用的,显然不能在每个界面当中都去写一遍头布局的代码,因此这种情况下使用<include>标签就非常合适了。在res/layout中新建titlebar.xml作为头布局:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <Button android:id="@+id/back" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_centerVertical="true" android:text="Back" /> <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="Title" android:textSize="20sp" /> <Button android:id="@+id/done" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:text="Done" /> </RelativeLayout>
titlebar作为一个独立的布局已经编写完了,接下来,只需要在其他布局文件中引入titlebar.xml就可以了。比如说我们的程序当中有一个activity_main.xml文件,现在想要引入titlebar只需要这样写:
<?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="match_parent" android:orientation="vertical" > <include layout="@layout/titlebar" /> </LinearLayout>
等等!现在如果你运行一下程序会发现出大问题了,虽然titlebar是成功引入了,但是我们activity_main.xml中本来的界面全部都不见了!出现这个问题是原因是因为titlebar的最外层布局是一个宽高都是match_parent的RelativeLayout,它会将整个布局都填充满,因而我们原本的布局也就看不见了。那既然问题的原因清楚了,相信你立刻就想到应该怎么修改了,将RelativeLayout的layout_height属性修改成wrap_content不就可以了嘛。没错,这样修改当然是没问题的,不过这种修改方式会让所有引用titlebar的界面都受到影响,而如何你只希望让activity_main.xml这一个界面受影响的话,那么可以使用覆写<include>属性的方式。
在<include>标签当中,我们是可以覆写所有layout属性的,即include中指定的layout属性将会覆盖掉titlebar中指定的layout属性。因此,这里我们希望将titlebar的高度设置成wrap_content,就可以这样写:
<?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="match_parent" android:orientation="vertical" > <include android:layout_width="match_parent" android:layout_height="wrap_content" layout="@layout/titlebar" /> </LinearLayout>
除了layout_height之外,我们还可以覆写titlebar中的任何一个layout属性,如layout_gravity、layout_margin等,而非layout属性则无法在<include>标签当中进行覆写。另外需要注意的是,如果我们想要在<include>标签当中覆写layout属性,必须要将layout_width和layout_height这两个属性也进行覆写,否则覆写效果将不会生效。
二、<merge>
<merge>标签是作为<include>标签的一种辅助扩展来使用的,它的主要作用是为了防止在引用布局文件时产生多余的布局嵌套。大家都知道,Android去解析和展示一个布局是需要消耗时间的,布局嵌套的越多,那么解析起来就越耗时,性能也就越差,因此我们在编写布局文件时应该让嵌套的层数越少越好。
在上面我们讲解<include>标签的用法时主要介绍了它优点,但是它也存在着一个不好的地方,就是可能会导致产生多余的布局嵌套。这里还是通过举例的方式跟大家说明一下,比如说我们需要编写一个确定取消按钮的公共布局,这样任何一个界面需要确定和取消功能时就不用再单独编写了,新建ok_cancel_layout.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="wrap_content" android:orientation="vertical" > <Button android:id="@+id/ok" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:text="OK" /> <Button android:id="@+id/cancel" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:layout_marginTop="10dp" android:text="Cancel" /> </LinearLayout>
然后我们有一个profile.xml的界面需要编辑一些内容,那么这里就可以将ok_cancel_layout这个布局引入到profile.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="match_parent" android:orientation="vertical" > <EditText android:id="@+id/edit" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="10dp" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:layout_marginTop="10dp" android:hint="Edit something here" /> <include layout="@layout/ok_cancel_layout"/> </LinearLayout>
可是在你毫无察觉的情况下,目前profile.xml这个界面当中其实已经存在着多余的布局嵌套了!我们可以通过View Hierarchy工具来查看一下,如下图所示:
可以看到,最外层首先是一个FrameLayout,这个无可厚非,然后FrameLayout中包含的是一个LinearLayout,这个就是我们在profile.xml中定义的最外层布局。接下来的部分就有问题了,在最外层的LinearLayout当中包含了两个元素,一个是EditText,另一个又是一个LinearLayout,然后在这个内部的LinearLayout当中才包含了确定和取消这两个按钮。
这个内部的LinearLayout就是一个多余的布局嵌套,实际上并不需要这样一层,让两个按钮直接包含在外部的LinearLayout当中就可以了。而这个多余的布局嵌套其实就是由于布局引入所导致的,因为我们在ok_cancel_layout.xml中也定义了一个LinearLayout。那么应该怎样优化掉这个问题呢?当然就是使用<merge>标签来完成了,修改ok_cancel_layout.xml中的代码:
<?xml version="1.0" encoding="utf-8"?> <merge xmlns:android="http://schemas.android.com/apk/res/android"> <Button android:id="@+id/ok" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:text="OK" /> <Button android:id="@+id/cancel" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:layout_marginTop="10dp" android:text="Cancel" /> </merge>
这里我们将ok_cancel_layout最外层的LinearLayout布局删除掉,换用了<merge>标签,这就表示当有任何一个地方去include这个布局时,会将<merge>标签内包含的内容直接填充到include的位置,不会再添加任何额外的布局结构。好的,<merge>的用法就是这么简单,重新运行一下程序,你会看到界面没有任何改变,然后我们再通过View Hierarchy工具来查看一下当前的View结构:
OK,可以看到,现在EditText和两个按钮都直接包含在了LinearLayout下面,我们的profile.xml当中也就不存在多余的布局嵌套了。
三、<ViewStub>
ViewStub:仅在需要时才加载布局。有时某个布局当中那些不常用的元素只有在用户进行特定操作的情况下才会显示出来。比如,在添加联系人时最常用的姓名和电话显示在界面上。然后给用户提供一个添加更多字段的选项,当用户真的有需要去添加其它信息(Email 地址等)的时候,才将另外的元素显示到界面上。
说到实现这样一个功能,我相信大多数人的第一反应就是将不常用的元素使用INVISIBLE或者GONE进行隐藏,然后当用户需要使用这些元素的时候再把它们置成VISIBLE显示出来。使用这种方式肯定可以实现功能的,但是性能方面就表现得一般了,因为即使是将元素进行隐藏,它们其实还是在布局当中的,每个元素还拥有着自己的宽、高、背景等等属性,解析布局的时候也会将这些隐藏的元素一一解析出来。
其实,Android为此提供了一种非常轻量级的控件,ViewStub。ViewStub虽说也是View的一种,但是它没有大小,没有绘制功能,也不参与布局,资源消耗非常低,将它放置在布局当中基本可以认为是完全不会影响性能的。
目前profile.xml中只有一个EditText用于编辑信息,那么另外三个不太常用的EditText,就可以将它们定义在另外一个布局文件当中。新建profile_extra.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="match_parent" android:orientation="vertical" > <EditText android:id="@+id/edit_extra1" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:hint="Extra field 1" /> <EditText android:id="@+id/edit_extra2" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:layout_marginTop="10dp" android:hint="Extra field 2" /> <EditText android:id="@+id/edit_extra3" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:layout_marginTop="10dp" android:hint="Extra field 3" /> </LinearLayout>
目前profile_extra.xml是一个独立的布局,和profile.xml这个布局文件是完全没有关系的。接下来我们修改profile.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="match_parent" android:orientation="vertical" > <EditText android:id="@+id/edit" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="10dp" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:layout_marginTop="10dp" android:hint="@string/edit_something_here" /> <Button android:id="@+id/more" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" android:layout_marginRight="20dp" android:layout_marginBottom="10dp" android:text="More" /> <ViewStub android:id="@+id/view_stub" android:layout="@layout/profile_extra" android:layout_width="match_parent" android:layout_height="wrap_content"/> <include layout="@layout/ok_cancel_layout" /> </LinearLayout>
这里我们新增了一个More Button,这个按钮就是用于去加载那些不常用的元素的,然后在Button的下面定义了一个ViewStub。在ViewStub控件中,我们先是通过id属性给它指定了一个唯一标识,又通过layout属性将profile_extra布局传入进来,接着给ViewStub指定了一个宽高。注意,虽然ViewStub是不占用任何空间的,但是每个布局都必须要指定layout_width和layout_height属性,否则运行就会报错。
接着修改ProfileActivity中的代码,在Activity中添加More Button的点击事件,并在点击事件中进行如下逻辑处理:
private EditText editExtra1; private EditText editExtra2; private EditText editExtra3; public void onMoreClick() { ViewStub viewStub = (ViewStub) findViewById(R.id.view_stub); if (viewStub != null) { View inflatedView = viewStub.inflate(); editExtra1 = (EditText) inflatedView.findViewById(R.id.edit_extra1); editExtra2 = (EditText) inflatedView.findViewById(R.id.edit_extra2); editExtra3 = (EditText) inflatedView.findViewById(R.id.edit_extra3); } }
当点击More Button之后我们首先会调用findViewById()方法将ViewStub的实例获取到,拿到ViewStub的实例之后就很简单了,调用inflate()方法或者setVisibility(View.VISIBLE)都可以将隐藏的布局给加载出来,而加载的这个布局就是刚才在XML当中配置的profile_extra布局。
调用inflate()方法之后会将加载出来的布局进行返回,之后我们就可以对这个布局进行任意的操作了,再次隐藏显示,或者获取子元素的实例等。注意这里我对ViewStub的实例进行了一个非空判断,这是因为ViewStub在XML中定义的id只在一开始有效,一旦ViewStub中指定的布局加载之后,这个id也就失败了,那么此时findViewById()得到的值也会是空。
现在我们重新运行一下程序,界面如下图所示:
可以看到,界面上只有一个More按钮,ViewStub是完全不占用任何空间的。然后点击一下More按钮,新的界面如下所示:
另外需要提醒大家一点,ViewStub所加载的布局是不可以使用<merge>标签的,因此这有可能导致加载出来的布局存在着多余的嵌套结构,具体如何去取舍就要根据各自的实际情况来决定了,对于那些隐藏的布局文件结构相当复杂的情况,使用ViewStub还是一种相当不错的选择的,即使增加了一层无用的布局结构,仍然还是利大于弊。