- Android开发权威指南(第二版)
- 李宁编著
- 1356字
- 2024-12-21 14:45:58
8.4 布局高级技术
本节将介绍一些与布局相关的技巧,通过这些技巧,可以更灵活地使用布局,并且也可以使布局更加有效率,占用更少的资源。
8.4.1 布局别名
布局别名本是资源本地化的一部分,不过由于本章的主题就是布局,所以在本节先提前向读者展示Android本地化的强大功能。
首先说明一下什么叫本地化资源目录。在res目录中的所有子目录都是资源目录,例如res/values、res/layout等。这些目录中存储的都是默认的资源。但在满足某些情况下系统需要使用另外的资源,例如,支持国际化的程序如果当前环境是中文,就要求所有的字符串资源从res/values-zh目录中的相应资源文件读取,如果当前环境是英文,就要从res/values-en目录中相应资源文件读取。其中values-zh、values-en就是本地化资源目录。所谓本地化目录也就是满足特定要求的资源文件存放的目录,例如,特殊的屏幕尺寸、不同的Android版本、不同的语言环境、不同的屏幕方向等。如果系统发现并没有满足当前特定要求的本地化资源目录,就会从默认的资源目录中寻找资源。例如,本地化资源目录只有res/values-zh和res/values-en,而当前的语言环境是法文,但并没有res/values-fr目录,所以系统会在默认的资源目录(res/values)寻找相应的字符串资源。
在了解了本地化资源目录后,就很容易理解布局别名了。所谓布局别名就是为不同布局文件指定同一个资源ID,以便在不同环境下系统可以使用同一个布局资源ID访问不同的布局资源。使用布局别名必须要注意布局文件的引用只能放到本地化资源目录中,而不能放到默认的资源目录(res/values)中。
下面是一个典型的布局别名的使用案例。
假设在res/layout目录中有两个布局文件:main_layout.xml和main_layout_en.xml,并且在主窗口中使用setContentView(R.layout.main_layout)将main_layout.xml与当前窗口关联。现在建立一个res/values-en目录(英文环境下使用该目录中的资源),然后在该目录下建立一个refs.xml文件(资源文件名可以任意命名),最后在refs.xml文件中输入如下的内容:
<resources>
<!-- 为main_layout_en.xml文件指定一个名为main_layout的别名 -->
<item name="main_layout" type="layout" >@layout/main_layout_en</item>
</resources>
如果当前环境正好是英文,系统就会使用res/values-en目录中的资源,所以会为main_layout_en.xml文件指定一个别名。如果别名正好与某一个布局文件的资源ID相同,那么就相当于修改该资源ID的指针,也就是说如果在英文环境下,R.layout.main_layout引用的不再是main_layout.xml,而是main_layout_en.xml。
扩展学习:本地化的方式
如果目录中的文件太乱,需要整理,通常会采用两种方式。第1种方式最容易想到,也是最常用的,就是建立若干个目录,然后分门别类地将目录的文件放到这些刚建立的目录中。但还有另外一种方法,就是并不移动源目录中的文件,而是为每一个文件建立一个索引(可以将这些索引数据存储在数据库或其他文件中),然后对这些索引进行分组和管理。这么做的好处是并不需要移动文件,而且同一个文件还可以属于不同的类别。如果要采用第1种方式,就需要将文件所属的每个类别对应的目录都复制一份同样的文件,这样不仅浪费存储空间,也不易于管理。
本地化的方式与管理目录中文件的方式类似,可以将满足不同环境的资源文件放到相应的本地化资源目录中,也可以将资源文件统一放到一起,而在不同的本地化资源目录中添加别名引用,布局别名就是采用了后一种方式。在后面的部分会有一章专门讨论Android中的本地化技术,到时读者的所有关于本地化的疑问都将解开。
8.4.2 重用布局
源代码目录:src/ch08/ReusableLayout
在一个复杂的应用程序中往往同样的布局要在多处使用。当然,最笨的方法是在所有需要的地方重复编写这些布局。这种方法固然可行,不过一旦需要修改布局,维护其一致性恐怕就要增加很大的工作量(有的布局可能会被重复使用上百次,甚至更多)。
在布局文件中可以使用<include>标签(注意,include首字母要小写)很好地解决上述问题,通过这个标签,可以在一个布局文件中引用另外一个布局文件。这样我们就可以将在多处使用的布局单独放在一个或多个布局文件中,然后在使用到这些布局文件时用<include>标签来引用它们。
例如,我们有一个叫workspace_screen.xml的布局文件,在另一个布局文件中需要使用3次,那么可以使用如下的布局代码。
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<!-- 引用三次workspace_screen -->
<include android:id="@+id/cell1" layout="@layout/workspace_screen" />
<include android:id="@+id/cell2" layout="@layout/workspace_screen" />
<include android:id="@+id/cell3" layout="@layout/workspace_screen" />
</LinearLayout>
<include>标签只有layout属性是必选的,该属性不需要android命名空间作为前缀。layout属性值是布局文件的资源ID,如“@layout/workspace_screen”。在上面的代码中<include>标签还使用了一个android:id属性,实际上,该属性指定的是workspace_screen.xml布局文件中根节点的android:id属性值。如果根节点已经设置了android:id属性值,那么<include>标签的android:id属性值将覆盖workspace_screen.xml布局文件中根节点的android:id属性值。<include>标签还可以覆盖被引用的布局文件根节点的所有与布局有关的属性,也就是以“android:layout_”开头的属性,例如,android:layout_width、android:layout_height等。通过覆盖属性值,可以使被引用的布局文件中的视图拥有不同的布局风格。例如,下面的布局代码引用了image_holder.xml文件两次,但只有第1个<include>标签覆盖了一些属性。
<!-- override the layout height and width -->
<include layout="@layout/image_holder"
android:layout_height="fill_parent"
android:layout_width="fill_parent" />
<!—下面的include标签没有覆盖任何属性 -->
<include layout="@layout/image_holder" />
注意
如果想覆盖布局的尺寸,必须同时覆盖android:layout_width和android:layout_ height。不能只覆盖其中一个属性,否则对这两个属性值的覆盖无效。
<include>标签在设计与设备相关的布局文件时非常有用,例如,当手机横屏(landscape)和竖屏(portrait)时可以使用不同的布局文件,但有可能很多视图的布局是相同的。这样就可以在res/layout目录中放置横屏和竖屏都需要用到的布局,而在res/layout-land和res/layout-port目录中的布局文件可以使用<include>标签引用这些布局。
<include>标签还可以简化布局文件的复杂度,例如,在8.1.2小节使用了一个较复杂的布局文件。我们可以使用<include>标签将其简化成6个小的布局文件,从而使布局文件更容易阅读和使用。现在我们来看一下这6个布局文件。
源代码文件:src/ch08/ReusableLayout/res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent">
<include layout="@layout/first" />
<include layout="@layout/second" />
</LinearLayout>
从main.xml文件可以看出,已经将原来复杂的布局代码简化成了几行代码,其中使用<include>标签引用了first.xml和second.xml文件,它们分别表示布局的第1部分和第2部分。
源代码文件:src/ch08/ReusableLayout/res/layout/first.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_weight="1">
<include layout="@layout/first_top" />
<include layout="@layout/first_middle" />
<include layout="@layout/first_bottom" />
</LinearLayout>
在first.xml文件中引用了first_top.xml、first_middle.xml和first_bottom.xml这3个布局文件,表示第1部分的上、中、下这3个小部分。这3个布局文件的内容如下:
源代码文件:src/ch08/ReusableLayout/res/layout/first_top.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_weight="1">
<LinearLayout android:orientation="vertical"
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:layout_weight="1">
<Button android:layout_width="wrap_content"
android:layout_height="wrap_content" android:text="左上按钮"
android:layout_gravity="left" />
</LinearLayout>
<LinearLayout android:orientation="vertical"
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:layout_weight="1">
<Button android:layout_width="wrap_content"
android:layout_height="wrap_content" android:text="右上按钮"
android:layout_gravity="right" />
</LinearLayout>
</LinearLayout>
源代码文件:src/ch08/ReusableLayout/res/layout/first_middle.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_weight="1"
android:gravity="center">
<Button android:layout_width="wrap_content"
android:layout_height="wrap_content" android:text="中心按钮" />
</LinearLayout>
源代码文件:src/ch08/ReusableLayout/res/layout/first_bottom.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_weight="1">
<LinearLayout android:orientation="vertical"
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:layout_weight="1" android:gravity="left bottom">
<Button android:layout_width="wrap_content"
android:layout_height="wrap_content" android:text="左下按钮" />
</LinearLayout>
<LinearLayout android:orientation="vertical"
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:layout_weight="1" android:gravity="right bottom">
<Button android:layout_width="wrap_content"
android:layout_height="wrap_content" android:text="右下按钮" />
</LinearLayout>
</LinearLayout>
下面来看看第2部分的布局文件。
源代码文件:src/ch08/ReusableLayout/res/layout/second.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_weight="1">
<ImageView android:layout_width="fill_parent"
android:layout_height="fill_parent" android:src="@drawable/background"
android:layout_weight="1" />
<EditText android:layout_width="fill_parent"
android:layout_height="wrap_content" android:hint="请在这里输入文本" />
</LinearLayout>
运行本节的例子后,效果与8.1.2小节的图8-5完全一样。
8.4.3 优化布局
源代码目录:src/ch08/Merge
让我们先使用hierarchyviewer(Android SDK自带的一个分析视图类层次的工具,运行<Android SDK安装目录>/tools/hierarchyviewer.bat可启动该工具)分析8.1.1小节的布局文件。启动hierarchyviewer,并运行8.1.1小节中的例子,然后单击hierarchyviewer界面上方的“Refresh Tree”按钮,会在界面的左侧显示模拟器中运行的程序的当前界面的视图类层次结构,如图8-13所示。
也许很多读者会感到奇怪,为什么会出现两个<FrameLayout>标签呢?明明在布局文件中只使用了一个<FrameLayout>标签。实际上,任何一个布局文件,无论根节点是<LinearLayout>、<RelativeLayout>还是<FrameLayout>,Android系统都会在上一层添加一个<FrameLayout>。也就是说,任何的布局文件都会被包含在<FrameLayout>标签中。了解了其中的原理,也就不奇怪为什么会出现两个<FrameLayout>标签了。
为了解决这个问题,可以使用<merge>标签代替<FrameLayout>标签,系统在遇到<merge>标签后,会自动忽略这个标签,这样就只剩下一个<FrameLayout>标签了,代码如下:
源代码文件:src/ch08/Merge/res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ImageView android:layout_width="fill_parent"
android:layout_height="wrap_content" android:background="@drawable/background"
android:layout_gravity="center" />
<ImageView android:layout_width="63dp"
android:layout_height="46dp" android:background="@drawable/bird"
android:layout_gravity="center" android:layout_marginTop="80dp"/>
<ImageView android:layout_width="85dp"
android:layout_height="85dp" android:background="@drawable/superman"
android:layout_gravity="center" android:layout_marginBottom="80dp"/>
</merge>
再次运行这个程序,会在hierarchyviewer中看到如图8-14所示的视图类的层次结构,看看<FrameLayout>标签是否少了一个呢!
▲图8-13 未优化的视图类层次结构
▲图8-14 优化后的视图类层次结构
注意
<merge>标签虽然能使视图类层次结构更简洁,但该标签只对<FrameLayout>标签有效,因为当<merge>标签被忽略时,上层还有一个<FrameLayout>标签。<merge>不能使用在其他的布局中,这是因为系统是不会将<merge>转换成<LinearLayout>、<RelativeLayout>标签的,再说系统也不知道如何转换。所以如果使用<merge>代替<LinearLayout>、<RelativeLayout>等布局标签,这些标签就消失了,这样就相当于仍然使用了<FrameLayout>标签。
8.4.4 动态装载布局
源代码目录:src/ch08/LoadLayout
在一些情况下需要动态装载布局,并生成具有弹性的界面。例如,图8-15共有10行,每一行由一个图像和一段文本组成。
▲图8-15 动态生成的界面
实际上,图8-15的界面的布局中并没有定义10个<ImageView>标签和10个<TextView>标签,而只是将一个<ImageView>和一个<TextView>标签单独在一个布局文件(item.xml)中定义,然后装载10次该布局文件即可。下面看一下具体的实现代码。
本例的主布局文件(activity_load_layout.xml)只包含一个<LinearLayout>标签,并未包含任何子标签,因为所有的视图都是动态生成的。
源代码文件:src/ch08/LoadLayout/res/layout/activity_load_layout.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000"
android:orientation="vertical" >
</LinearLayout>
item.xml是每一行的布局,具体代码如下:
源代码文件:src/ch08/LoadLayout/res/layout/item.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher" />
<TextView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="100dp"
android:textColor="#FFF"
android:textSize="25sp" />
</LinearLayout>
如果要动态装载、添加视图,通常在主窗口类(LoadLayoutActivity)中调用LayoutInflater.inflate方法创建一个新的视图对象,inflate方法的原型如下:
public View inflate(int resource, ViewGroup root);
resource参数表示要装载的视图资源ID,root参数表示要装载的视图的父视图。如果装载的视图没有父视图,该参数值为null。LoadLayoutActivity类的代码如下:
源代码文件:src/ch08/LoadLayout/src/mobile/android/load/layout/LoadLayoutActivity.java
public class LoadLayoutActivity extends Activity
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// 装载主窗口使用的布局
LinearLayout parent = (LinearLayout) getLayoutInflater().inflate(
R.layout.activity_load_layout, null);
// 在循环中会装载10次item.xml
for(int i = 1; i <= 10; i++)
{
// 装载item.xml,每一个view就是图8-15中的一行
// 由于view是动态添加的,所以inflate方法的第2个参数值为null
View view = getLayoutInflater().inflate(R.layout.item, null);
TextView textView = (TextView)view.findViewById(R.id.textview);
// 设置TextView控件的文本
textView.setText("text" + i);
// 动态添加每一行的视图对象
parent.addView(view);
}
// 将视图对象与当前窗口绑定
setContentView(parent);
}
}
8.4.5 动态设置布局属性
在上一节的例子中我们会发现如果将activity_load_layout.xml文件中<LinearLayout>标签的android:gravity属性值设为“center_horizontal”,或将item.xml文件中<LinearLayout>标签的android:layout_gravity属性值设为“center_horizontal”,或将这两个属性值设为“right”,动态添加的10个视图并没有居中或右对齐。原因是动态添加视图时并不会采用静态方式设置布局,而要想重新设置布局属性,就需要使用LayoutParams类。但要注意,由于主窗口布局使用的是LinearLayout布局,所以要使用android.widget.LinearLayout.LayoutParams类。修改后的LoadLayoutActivity类的代码如下:
源代码文件:src/ch08/LoadLayout/src/mobile/android/load/layout/LoadLayoutActivity.java
public class LoadLayoutActivity extends Activity
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
LinearLayout parent = (LinearLayout) getLayoutInflater().inflate(
R.layout.activity_load_layout, null);
for(int i = 1; i <= 10; i++)
{
View view = getLayoutInflater().inflate(R.layout.item, null);
TextView textView = (TextView)view.findViewById(R.id.textview);
textView.setText("text" + i);
// 创建LayoutParams对象
android.widget.LinearLayout.LayoutParams layoutParams = new
android.widget.LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
// 设置gravity字段的值(水平居中)
layoutParams.gravity = Gravity.CENTER_HORIZONTAL;
// 动态添加视图时指定父视图(LinearLayout)的布局参数
parent.addView(view, layoutParams);
}
setContentView(parent);
}
}
运行修改后的程序,所有的控件都会居中显示。
8.4.6 从右到左布局(RTL Layout)
从Android 4.2开始,Android SDK支持一种从右到左(RTL,Right-to-Left)UI布局的方式,尽管这种布局方式经常被使用在诸如阿拉伯语、希伯来语等环境中,中国用户很少使用。不过在某些特殊用途中还是很方便的。
所谓RTL,就是指按平常习惯在左的视图都会在右侧,在右侧的视图都会在左侧。例如,在线性布局中第1个子视图默认都是在左上角的,如果采用RTL布局,默认就在右上角了。
RTL布局默认是关闭的,如果想使用RTL布局,首先要在AndroidManifest.xml文件中将<application>标签的android:supportsRtl属性值设为“true”,然后需要将相应视图标签的android:layoutDirection属性值设为“rtl”。
如果要使用RTL布局,还应该注意一个重要的问题。假设一个水平线性布局中有两个<TextView>标签:TextView1和TextView2。TextView1位于窗口的左上角,而TextVew2在TextView1的右侧,到TextView1的距离是100dp。实际上就是TextView2的左边缘到TextView1的右边缘的距离。如果当前是默认布局方式(LTR,从左到右,Left-to-Right),只需要将TextView2的android:layout_marginLeft属性值设为“100dp”即可。不过这在RTL布局中却恰好相反。在RTL布局中,TextView1在窗口的右上角,而TextView2却跑到了TextView1的左侧,所以TextView2到TextView1的距离实际上变成了TextView2的右边缘到TextView1的左边缘的距离。因此应该设置TextView2的android:layout_marginRight属性,这样就会造成RTL和LTR两种布局模式中UI排列的混乱。为了解决这个问题,在Android 4.2中新加了如下两个布局属性。
android:layout_marginStart:如果在LTR布局模式下,该属性等同于android:layout_marginLeft。如果在RTL布局模式下,该属性等同于android:layout_marginRight。
android:layout_marginEnd:如果在LTR布局模式下,该属性等同于android:layout_marginRight。如果在RTL布局模式下,该属性等同于android:layout_marginLeft。
首先来看下面的布局。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000"
android:orientation="horizontal" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#F00"
android:text="TextView1"
android:textSize="30sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="100dp"
android:background="#F00"
android:text="TextView2"
android:textSize="30sp" />
</LinearLayout>
该布局是默认的LTR布局,所以两个TextView控件都是从左到右排列的,如图8-16所示。
▲图8-16 LTR 布局排列方式
现在将<LinearLayout>标签的android:layoutDirection属性值设为“rtl”,其他布局代码不动。再运行程序,就会看到如图8-17所示的效果。TextView1和TextView2都是从右侧开始排列的,在不改动其他代码的情况下达到这个效果,全靠TextView2使用了android:layout_marginStart属性设置了两个TextView控件的间距。
▲图8-17 RTL 布局排列方式