在开发中UI布局是我们都会遇到的问题,随着UI越来越多,布局的重复性、复杂度也会随之增长。Android官方给了几个优化的方法,但是网络上的资料基本上都是对官方资料的翻译,这些资料都特别的简单,经常会出现问题而不知其所以然。这篇文章就是对这些问题的更详细的说明,也欢迎大家多留言交流。
一、include
首先用得最多的应该是include,按照官方的意思,include就是为了解决重复定义相同布局的问题。例如你有五个界面,这五个界面的顶部都有布局一模一样的一个返回按钮和一个文本控件,在不使用include的情况下你在每个界面都需要重新在xml里面写同样的返回按钮和文本控件的顶部栏,这样的重复工作会相当的恶心。使用include标签,我们只需要把这个会被多次使用的顶部栏独立成一个xml文件,然后在需要使用的地方通过include标签引入即可。其实就相当于C语言、C++中的include头文件一样,我们把一些常用的、底层的API封装起来,然后复用,需要的时候引入它即可,而不必每次都自己写一遍。
使用include最常见的问题就是findViewById查找不到目标控件
:首先找到include的id, 例如这里include设置的id为“my_title_ly”,然后再对获取到的titleView.findViewById来查找目标布局中的子控件,例如title_tv就是my_title_layout.xml中定义的子控件。因此我们如果需要查找控件的话,可以设置include标签的id,通过这个id获取include对应的view以后,再通过对这个view进行findViewById才能正确查找。如果你设置了include标签的id,然后通过被include的布局的root view的id来查找子元素的话,则会报错,
这样会报空指针异常,因为titleView没有找到,会报空指针。那么这是怎么回事呢? 我们来分析它的源码看看吧。对于布局文件的解析,最终都会调用到LayoutInflater的inflate方法,该方法最终又会调用rInflate方法,我们看看这个方法。
这个方法其实就是遍历xml中的所有元素,然后挨个进行解析。例如解析到一个标签,那么就根据用户设置的一些layout_width、layout_height、id等属性来构造一个TextView对象,然后添加到父控件(ViewGroup类型)中。标签也是一样的,我们看到遇到include标签时,会调用parseInclude函数,这就是对标签的解析,我们看看吧。
整个过程就是根据不同的标签解析不同的元素,首先会解析include元素,然后再解析被include进来的布局的root view元素。在我们的例子中对应的root view就是id为my_title_parent_id的RelativeLayout,然后再解析root view下面的所有元素,这个过程是从上面注释的2~4的过程,然后是设置布局参数。我们注意看注释5处,这里就解释了为什么include标签和被引入的布局的根元素都设置了id的情况下,通过被引入的根元素的id来查找子控件会找不到的情况。我们看到,注释5处的会判断include标签的id如果不是View.NO_ID的话会把该id设置给被引入的布局根元素的id,即此时在我们的例子中被引入的id为my_title_parent_id的根元素RelativeLayout的id被设置成了include标签中的id,即RelativeLayout的id被动态修改成了”my_title_ly”。因此此时我们再通过“my_title_parent_id”这个id来查找根元素就会找不到了!
所以结论就是: 如果include中设置了id,那么就通过include的id来查找被include布局根元素的View;如果include中没有设置Id, 而被include的布局的根元素设置了id,那么通过该根元素的id来查找该view即可。拿到根元素后查找其子控件都是一样的。
Android 布局优化
Android 布局优化
在开发过程中我们经常说性能优化,但性能优化是一个比较宽泛的概念。
在Android开发中性能优化可能包括:
Java代码优化,
算法优化,
SQLite优化,
布局优化等。
那么这篇博客就来总结并分享下Android开发中的布局优化。
布局原则
在Android UI布局过程中,通过遵守一些惯用、有效的布局原则,我们可以制作出高效且复用性高的UI,概括来说包括如下几点:
-
尽量多使用RelativeLayout和LinearLayout, 不要使用绝对布局AbsoluteLayout,在布局层次一样的情况下, 建议使用LinearLayout代替RelativeLayout, 因为LinearLayout性能要稍高一点,但往往RelativeLayout可以简单实现LinearLayout嵌套才能实现的布局。
-
将可复用的组件抽取出来并通过include标签使用;
-
使用ViewStub标签来加载一些不常用的布局;
-
使用merge标签减少布局的嵌套层次;
RelativeLayout VS LinearLayout
第一条原则说了布局层次一样的情况下LinearLayout比RelativeLayout要好, 但往往RelativeLayout可以简单实现LinearLayout嵌套才能实现的布局。假如需要实现如下布局:
<span style="font-family:Microsoft YaHei;font-size:14px;">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:padding="6dip">
<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_marginRight="6dip"
android:src="@drawable/icon" />
<LinearLayout
android:orientation="vertical"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="fill_parent">
<TextView
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="My Application" />
<TextView
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:singleLine="true"
android:ellipsize="marquee"
android:text="Simple application that shows how to use RelativeLayout" />
</LinearLayout>
</LinearLayout></span>
而用RelativeLayout实现代码如下:
<span style="font-family:Microsoft YaHei;font-size:14px;"><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:padding="6dip">
<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_alignParentTop="true"
android:layout_alignParentBottom="true"
android:layout_marginRight="6dip"
android:src="@drawable/icon" />
<TextView
android:id="@+id/secondLine"
android:layout_width="fill_parent"
android:layout_height="26dip"
android:layout_toRightOf="@id/icon"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:singleLine="true"
android:ellipsize="marquee"
android:text="Simple application that shows how to use RelativeLayout" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/icon"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_above="@id/secondLine"
android:layout_alignWithParentIfMissing="true"
android:gravity="center_vertical"
android:text="My Application" />
</RelativeLayout></span>
可以看到用RelativeLayout实现,布局层次明显少了,所以大多数时候优先推荐使用RelativeLayout。
查看布局层次
如何查看布局层次呢?有两种办法:一是通过手机的开发者选项,4.0及以上Android版本可通过设置->开发者选项->显示布局边界打开页面布局显示,看看是否有不必要的节点和嵌套。第二种就是利用SDK自带的UI性能检测工具HierarchyViewer。 进入sdk目录下的tools文件夹下,找到HierarchyViewer并运行(此时保持你的模拟器或真机正在运行需要进行分析的App),双击我们正在显示的这个App所代表的进程。接下来便会进入hierarchyviewer的界面,我们可以在这里很清晰看到正在运行的UI的布局层次结构以及它们之间的关系。大概的显示如下图:
通过布局图我们可以看到根节点DecorView下包含一个LinearLayout, 这个LinearLayout就是包含Activity布局和状态栏的整个屏幕显示的布局父节点,这个LinearLayout有两个子节点, 一个是FrameLayout, FrameLayout就是Activity布局中默认的父布局节点, 这个节点下面就包含了我们自己写的xml布局, 还有一个子节点就是ViewStub,关于这个节点我们在后面会详细介绍。
< include />的使用
在实际开发中,我们经常会遇到一些共用的UI组件,比如带返回按钮的导航栏,如果为每一个xml文件都设置这部分布局,一是重复的工作量大,二是如果有变更,那么每一个xml文件都得修改。还好,Android为我们提供了include标签,顾名思义,通过它,我们可以将这些共用的组件抽取出来单独放到一个xml文件中,然后使用include标签导入共用布局,这样,前面提到的两个问题都解决了。下面以在一个布局main.xml中用include引入另一个布局header.xml为例。
header.xml文件
<span style="font-family:Microsoft YaHei;font-size:14px;"><?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/button"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_40"
android:layout_above="@id/text"/>
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_40"
android:layout_alignParentBottom="true"
android:text="@string/app_name" />
</RelativeLayout></span>
然后我们在需要引入footer的布局xml中通过include导入这个共用布局。
main.xml文件
<span style="font-family:Microsoft YaHei;font-size:14px;"><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="hello world" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center" >
<include layout="@layout/header" />
</RelativeLayout>
</FrameLayout></span>
通过这种方式,我们既能提高UI的制作和复用效率,也能保证制作的UI布局更加规整和易维护。
< merge />的使用
merge标签的作用是合并UI布局,使用该标签能降低UI布局的嵌套层次。merge标签可用于两种典型情况:
-
布局根结点是FrameLayout且不需要设置background或padding等属性,可以用merge代替,因为Activity内容布局的parent view就是个FrameLayout,所以可以用merge消除只剩一个,这一点可以从上图中看到。
-
某布局作为子布局被其他布局include时,使用merge当作该布局的顶节点,这样在被引入时顶结点会自动被忽略,而将其子节点全部合并到主布局中。
以第一种情况为例,main.xml布局就可以优化如下:
<span style="font-family:Microsoft YaHei;font-size:14px;"><merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="hello world" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center" >
<include layout="@layout/header" />
</RelativeLayout>
</FrameLayout>
</merge>
</span>
以第二种情况为例,header.xml布局可以优化如下:
<span style="font-family:Microsoft YaHei;font-size:14px;"><?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_40"
android:layout_above="@id/text"/>
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_40"
android:layout_alignParentBottom="true"
android:text="@string/app_name" />
</merge></span>
这样就不会有多余的FrameLayout和RelativeLayout节点了。
ViewStub标签
viewstub标签同include标签一样可以用来引入一个外部布局,不同的是,viewstub引入的布局默认不会扩张,即既不会占用显示也不会占用位置,从而在解析layout时节省cpu和内存。 viewstub常用来引入那些默认不会显示,只在特殊情况下显示的布局,如进度布局、网络失败显示的刷新布局、信息出错出现的提示布局等。
我们新建一个xml文件用来显示一个网络错误时提示信息error.xml:
<span style="font-family:Microsoft YaHei;font-size:14px;"><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="@android:color/white"
android:padding="10dip"
android:text="Message"
android:textColor="@android:color/black" />
</RelativeLayout></span>
然后在main.xml里面加入ViewStub的标签引入上面的布局:
<span style="font-family:Microsoft YaHei;font-size:14px;"><merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:background="@android:color/darker_gray"
android:layout_height="match_parent" >
...
<ViewStub
android:id="@+id/error_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout="@layout/error" />
</merge></span>
在java中通过(ViewStub)findViewById(id)找到ViewStub,通过stub.inflate()展开ViewStub,然后得到子View,如下:<span style="font-family:Microsoft YaHei;font-size:14px;">private View errorView;
private void showError() {
// not repeated infalte
if (errorView != null) {
errorView.setVisibility(View.VISIBLE);
return;
}
ViewStub stub = (ViewStub)findViewById(R.id.error_layout);
errorView = stub.inflate();
}
private void showContent() {
if (errorView != null) {
errorView.setVisibility(View.GONE);
}
}</span>
在上面showError()中展开了ViewStub,同时我们对errorView进行了保存,这样下次不用继续inflate。
总结
这篇Blog没有详细介绍HierarchyViewer工具的使用,相信如果对布局原则比较熟练之后,对工具的依赖大大减少,开发效率也会大大的提升。除这些布局原则之外,还需要大家对Android各个组件的属性很熟悉,比如如果要做这么一个布局, 一个图片和一个文本的布局,新手们往往会用一个Layout嵌套ImageView和TextView来做, 但是当我们知道TextView有drawableLeft, drawableRight等属性时,那么实现这样的一个布局是非常快速高效的。总之,且学且实践!