Android4.0 Ice Cream Sandwich (ICS) 提供了两种新的控件,也就是Space和GridLayout,是专门为大屏幕设备提供更丰富的用户交互体验而设计。
在这之前,Android中最常用的布局类是LinearLayout,它能将它的子元素们水平排列或垂直排列。当界面布局比较复杂的时候,也可以利用它嵌套一系列分割出来的LinearLayout子布局来实现,嵌套的层数通常不宜太深,只适合于许多简单布局的情形。
嵌套布局有很多显著的缺点(参考 Android Layout Tricks #1, Flattening The Stack 这些文章),总结起来有这三个方面:
以下就是关于上述第一个问题的简单的例子:
当文字字体和“Email address”标签文字本身改变的时候,我们会希望标签与它右边的组件的底部基线对齐,同时让它的右边缘与它下方的标签的右边缘对齐。但如果用嵌套的线性布局做这个会很困难,因为标签本身会去和其他组件在水平和竖直方向上自动对齐。
诸如此类的问题并不是第一次出现在Android或者更大范围的UI工具上,但我们需要用它们丰富的平台支持,来推动我们的其他工作。
为了提供更好的布局支持,我们已经在4.0框架中增加了一种新的布局类即GridLayout,它通过将容器自身的真实区域切割成行列单元来解决上述问题。
正如上图所示在使用GridLayout之后,“Email address”标签就可以同时属于那底部基线对齐的一行和那右边缘对齐的一列。
GridLayout用一组无限细的直线将它的绘图区域分割成行、列、单元。它支持行、列拼接合并,这就使得一个子元素控件能够排布在一系列连续单元格组成的矩形区域。在下文中,我们将直接使用“行”、“列”、“单元格”这些术语来分别代表“行集合”、“列集合”、“单元格集合”,这里集合的意思是指那些一个或多个连续元素。
GridLayout的所有XML API与LinearLayout有着一致的语法规则,所以如果你已经使用过LinearLayout的话,上手GridLayout也是应该很容易的。事实上,它们之间是非常相似的,相似到直接将XML文件中的标签名从LinearLayout改到GridLayout而无需做其他改变,就可以实现与LinearLayout中相似的UI布局。即使不是这样,你也仍然能借此开启使用这种二维布局之路。
在Android 4.0 SDK的程序示例中,有两个示例分别演示了如何在Java代码和XML中使用GridLayout:
samples/ApiDemos/src/com/example/android/apis/view/GridLayout0.java
samples/ApiDemos/res/layout/grid_layout_1.xml
这里有一个较上述XML布局中稍微简化版的XML代码:
<?xml version=”1.0″ encoding=”utf-8″?>
<GridLayout
xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:useDefaultMargins=”true”
android:alignmentMode=”alignBounds”
android:columnOrderPreserved=”false”
android:columnCount=”4″
><TextView
android:text=”Email setup”
android:textSize=”32dip”
android:layout_columnSpan=”4″
android:layout_gravity=”center_horizontal”
/><TextView
android:text=”You can configure email in just a few steps:”
android:textSize=”16dip”
android:layout_columnSpan=”4″
android:layout_gravity=”left”
/><TextView
android:text=”Email address:”
android:layout_gravity=”right”
/><EditText
android:ems=”10″
/><TextView
android:text=”Password:”
android:layout_column=”0″
android:layout_gravity=”right”
/><EditText
android:ems=”8″
/><Space
android:layout_row=”4″
android:layout_column=”0″
android:layout_columnSpan=”3″
android:layout_gravity=”fill”
/><Button
android:text=”Next”
android:layout_row=”5″
android:layout_column=”3″
/>
</GridLayout>
在这里你能注意到的第一个区别就是在以上例子中,子元素控件没有使用WRAP_CONTENT或MATCH_PARENT这种通常出现在Android布局资源中的属性常量。但在GridLayout中,你通常不需要使用这些属性,这样做的原因在GridLayout.LayoutParams的API文档中有所解释。
第二个你会注意到的事是在上述XML资源中,子元素控件并不总是明确指定它们自身摆放在哪些单元格内。事实上GridLayout中每个子元素控件的布局参数属性中有行与列的索引,它们一起定义了这个控件该摆放在哪个位置,但当其中一个参数或这两个参数都没有指定的时候,GridLayout会提供默认值而不抛出异常。
当子元素控件被加入一个GridLayout的时候,GridLayout会维护这一个位置指针,以及一个所谓的“高水位标志”,用来将控件摆放到那些还闲置着的单元格里去。
当GridLayout的方向属性orientation被设置成水平(horizontal),且列数属性columnCount 也设置过时(在上图例子中,该值为8),那么高水位标志(上图中红色标记的线)将会为每一列维护一个独立的高度值。当索引值需要创建时,GridLayout首先会通过检查新控件的rowSpan和columnSpan属性来计算所需单元格集的区域大小,然后从上图中位置指针所指的位置开始,以从左到右,从上往下的顺序遍历可用空间,找到的第一个可用空间的行、列索引值。
假如GridLayout的方向属性是竖直(vertical)的,所有的算法原则都与水平时一样,只是将水平轴和竖直轴所扮演的角色互换一下。
如果你希望将多个视图控件摆放在同一单元格内,你就必须要明确指定它的索引值了,因为默认的分配过程正如上文解释的那样,是将控件摆在独立的单元格内的。
在GridLayout中,定义控件大小、外边距的做法与在LinearLayout中是一样的。对齐、位置属性(gravity)的使用方式也与LinearLayout中一样,同样使用left, top, right, bottom, center_horizontal, center_vertical, center, fill_horizontal, fill_vertical, fill这些常量值。
与其他UI工具包着的网格布局不同的是,GridLayout不会将数据绑定到行列中。相反的,所有与对齐和布局自适应相关的东西都只跟组件自身相关。GridLayout将那些行为模式从中剥离开来,提供了一个更加通用的系统来让那些处于传统深层次嵌套布局中的但只有轻微联系的子元素们自适应到一个单一的布局配置文件中。
那些单元列的自适应性可以从该列中组件的gravity属性推断得到,如果每个组件都定义了一个gravity属性,那么这个列就可以认为是自适应的,否则只能认为是不自适应的。更多关于这方面的细节可参考GridLayout的API文档。
GridLayout并没有加入Android平台中其他布局的所有特性,但它也拥有一系列非常丰富的特性,使得其他布局中常用的特性也能被正常地在单一GridLayout中模拟出来。
尽管LinearLayout可以被当作是GridLayout的一个特例,但当一组视图控件的布局简单到就是单一行或单一列对齐时,选择用LinearLayout会更好,因为它自身就能说明容器中子元素的排版表现目的,而且还有一些性能上的优势(当然是非常小的)。
TableLayout布局的模拟也是非常直接的,因为GridLayout支持行、列合并。同时,TableRows在GridLayout中就不需要了。对于相同的UI,使用GridLayout完成起来往往更快而且消耗更少的内存空间。
简单的RelativeLayout布局通常就是将相互关联的视图控件组合成行与列,以此来实现网格。与标准的网格不同的是,GridLayout由系统来完成那些繁重的排版工作。通过设置GridLayout的rowOrderPreserved和columnOrderPreserved属性,就可以把GridLayout从传统的网格排版中解放出来,完成大部分RelativeLayout所能完成的布局——甚至是那种当子元素尺寸变化时要求网格线能相互交错的效果。
简单的FrameLayout排版也能够在GridLayout的单元格内实现,因为GridLayout的一个单元格可以放置多个视图。要在两个视图之间切换,只需要将它们放在同一单元格内,在代码中设置可见性属性的值来切换。与LinearLayout提到的情形一样,如果你所需要的功能就是如上描述的那样,选择使用FrameLayout会更好,因为那样会有少量性能上的优势。
GridLayout缺少的一个重要特性就是按指定比例在行、列给子元素中分配额外空间,这个特性在LinearLayout中是能通过weight这个属性实现的。此项遗漏以及实现它的可能性在GridLayout的API文档中也有所提及。
将上文中讨论的单元格索引分配阶段与视图控件布局阶段区分开是很有必要的。正常来讲,单元格索引的分配阶段只发生一次,也就是在UI初始化之后。索引分配只会在索引本身未指定时才发生,同时这个阶段要确保所有视图在布局阶段都已经有一系列已定义好的、将它摆放在哪里的单元格。
而视图控件布局阶段通常在索引分配阶段之后进行,并且每当视图的尺寸改变时都会重新计算其位置、尺寸。GridLayout会在布局过程中测量每个子元素控件的尺寸,这样就能计算它们在网格中所需要的宽和高值。当GridLayout使用gravity属性最终摆放每个组件在单元格中的位置后,布局阶段才算完成。
尽管索引的自动分配只发生一次,但从技术上来讲GridLayout是一个动态布局,这意味着如果你在组件布局完成之后改变了它的方向,或者增加或者删除某些子元素,GridLayout都会重复上述过程,重新分配索引,让布局重新以一合理的方式呈现。
站在性能的角度来看,了解到GridLayout的实现已经为一些通用场景比如初始化一次但经常更新布局这种场景做了优化是十分有必要的。因此,初始化步骤通常是设置一下内部数据结构,这样布局阶段会快速完成而且不消耗任何内存。换句话说,也就是无论是改变GridLayout的方向还是改变它子元素的数量,都比其他常用布局操作要更消耗性能。
GridLayout吸纳了很多Android框架中已有的通用目的的布局的功能:也就是LinearLayout、FrameLayout、TableLayout、RelativeLayout的综合功能。比如说它提供了将高度嵌套的视图结构替换成一个单一的高度优化过的布局的解决方案。
如果你刚开始使用Android UI,还不熟悉Android的布局类型,那么可以考虑使用GridLayout——它提供了其他布局所拥有的大部分特性,并且拥有比TableLayout或RelativeLayout更通用的API。
我们希望FrameLayout、LinearLayout以及GridLayout的结合能提供一系列足够丰富的特性来完成大解决布局工作而无需手写代码。当然花一些时间来决定优先使用哪一种布局是非常有必要的,一个最佳的选择可以减少中间容器的使用并且提供一个更快、消耗内存更少的用户界面。