布局

qiang.zhang大约 17 分钟

布局

概览

布局定义了应用中的界面结构(例如 Activity 的界面结构)。布局中的所有元素均使用 View 和 ViewGroup 对象的层次结构进行构建。View 通常用于绘制用户可看到并与之交互的内容。ViewGroup 则是不可见的容器,用于定义 View 和其他 ViewGroup 对象的布局结构,如图 1 所示。

图 1. 视图层次结构的图示,它定义了一个界面布局
图 1. 视图层次结构的图示,它定义了一个界面布局

View 对象通常称为“微件”,可以是多个子类之一,例如 Button 或 TextView。ViewGroup 对象通常称为“布局”,可以是提供不同布局结构的众多类型之一,例如 LinearLayout 或 ConstraintLayout。

可通过两种方式声明布局:

在 XML 中声明界面元素。Android 提供对应 View 类及其子类的简明 XML 词汇,如用于微件和布局的词汇。也可使用 Android Studio 的 Layout Editor,并采用拖放界面来构建 XML 布局。

在运行时实例化布局元素。应用能以程序化方式创建 View 对象和 ViewGroup 对象(并操纵其属性)。 通过在 XML 中声明界面,可以将应用外观代码与控制其行为的代码分开。使用 XML 文件还有助于为不同屏幕尺寸和屏幕方向提供不同布局。

借助 Android 框架,可以灵活选择使用两种或其中一种方法来构建应用界面。

例如,可以在 XML 中声明应用的默认布局,然后在运行时修改布局。

  • tip:如需在运行时调试布局,可以使用布局检查器工具。

编写 XML

利用 Android 的 XML 词汇,按照在 HTML 中创建包含一系列嵌套元素的网页的相同方式快速设计界面布局及其包含的屏幕元素。

每个布局文件都必须只包含一个根元素,并且该元素必须是视图对象或 ViewGroup 对象。定义根元素后,以子元素的形式添加其他布局对象或微件,从而逐步构建定义布局的视图层次结构。例如,以下 XML 布局使用垂直 LinearLayout 来储存 TextView 和 Button:


<?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" >
    <TextView android:id="@+id/text"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="Hello, I am a TextView" />
    <Button android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello, I am a Button" />
</LinearLayout>

在 XML 中声明布局后,以 .xml 扩展名将文件保存在Android 项目的 res/layout/ 目录中,以便该文件能正确编译。

加载 XML 资源

当编译应用时,系统会将每个 XML 布局文件编译成 View 资源。在 Activity.onCreate() 回调实现内加载应用代码中的布局资源。通过调用 setContentView(),并以 R.layout.layout_file_name 形式向应用代码传递对布局资源的引用,即可执行此操作。例如,如果 XML 布局保存为 main_layout.xml,应通过如下方式为 Activity 加载布局资源:


public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main_layout);
}

启动 Activity 时,Android 框架会调用 Activity 中的 onCreate() 回调方法(参阅 Activity 文档中有关生命周期的阐述)。

属性

每个 View 对象和 ViewGroup 对象均支持自己的各种 XML 属性。某些属性是 View 对象的特有属性(例如,TextView 支持 textSize 属性),但可扩展此类的任一 View 对象也会继承这些属性。某些属性是所有 View 对象的共有属性,因为它们继承自 View 根类(例如 id 属性)。此外,其他属性被视为“布局参数”,即描述 View 对象特定布局方向的属性,如该对象的父 ViewGroup 对象所定义的属性。

ID

任何 View 对象均可拥有与之关联的整型 ID,用于在结构树中对 View 对象进行唯一标识。编译应用后,系统会以整型形式引用此 ID,但在布局 XML 文件中,系统通常会以字符串的形式在 id 属性中指定该 ID。这是所有 View 对象共有的 XML 属性(由 View 类定义)。XML 标记内部的 ID 语法是:

android:id="@+id/my_button"

字符串开头处的 @ 符号指示 XML 解析器应解析并展开 ID 字符串的其余部分,并将其标识为 ID 资源。加号 (+) 表示这是一个新的资源名称,必须创建该名称并将其添加到我们的资源(在 R.java 文件中)内。Android 框架还提供许多其他 ID 资源。引用 Android 资源 ID 时,不需要加号,但必须添加 android 软件包命名空间,如下所示:

android:id="@android:id/empty"

添加 android 软件包命名空间后,可以从 android.R 资源类而非本地资源类引用 ID。

为了创建视图并从应用中引用它们,常见的模式是:

  1. 在布局文件中定义视图/微件,并为其分配唯一 ID: ``xml
```
  1. 然后创建视图对象的实例,并从布局中捕获它(通常使用 onCreate() 方法):
Button myButton = (Button) findViewById(R.id.my_button);

创建 RelativeLayout 时,请务必为视图对象定义 ID。在相对布局中,同级视图可定义其相对于其他通过唯一 ID 引用的同级视图的布局。

ID 无需在整个结构树中具有唯一性,但在要搜索的结构树部分中应具有唯一性(要搜索的部分往往是整个结构树,因此最好尽可能具有全局唯一性)。

  • tip:在 Android Studio 3.6 及更高版本中,视图绑定功能可以替换 findViewById() 调用,并为与视图互动的代码提供编译时类型安全。考虑使用视图绑定,而非 findViewById()。

布局参数

名为 layout_something 的 XML 布局属性可以为视图定义适合其所在 ViewGroup 的布局参数。

每个 ViewGroup 类都会实现一个扩展 ViewGroup.LayoutParams 的嵌套类。此子类包含的属性类型会根据需要为视图组的每个子视图定义尺寸和位置。如图 2 所示,父视图组会为每个子视图(包括子视图组)定义布局参数。

图 2. 视图层次结构的图示,其中包含与每个视图关联的布局参数
图 2. 视图层次结构的图示,其中包含与每个视图关联的布局参数

每个 LayoutParams 子类都有自己的值设置语法。每个子元素都必须定义适合其父元素的 LayoutParams,但父元素也可为其子元素定义不同的 LayoutParams。

所有视图组均包含宽度和高度(layout_width 和 layout_height),并且每个视图都必须定义它们。许多 LayoutParams 还包括可选的外边距和边框。

可以指定具有确切尺寸的宽度和高度,也可以用以下某种常量来设置宽度或高度:

  • wrap_content 指示视图将其大小调整为内容所需的尺寸。
  • match_parent 指示视图尽可能采用其父视图组所允许的最大尺寸。

一般而言,建议不要使用绝对单位(如像素)来指定布局宽度和高度。更好的方法是使用相对测量单位(如与密度无关的像素单位 dp、wrap_content 或 match_parent),因为这样有助于确保应用在各类尺寸的设备屏幕上正确显示。可用资源文档中定义了可接受的测量单位类型。

布局位置

视图的几何形状就是矩形的几何形状。视图拥有一个位置(以一对“水平向左”和“垂直向上”的坐标表示)和两个尺寸(以宽度和高度表示)。位置和尺寸的单位是像素。

可以通过调用 getLeft() 方法和 getTop() 方法来检索视图的位置。前者会返回表示视图的矩形的水平向左(或称 X 轴)坐标。后者会返回表示视图的矩形的垂直向上(或称 Y 轴)坐标。这些方法都会返回视图相对于其父项的位置。例如,如果 getLeft() 返回 20,则表示视图位于其直接父项左边缘向右 20 个像素处。

此外,系统还提供了几种便捷方法来避免不必要的计算,即 getRight() 和 getBottom()。这些方法会返回表示视图的矩形的右边缘和下边缘的坐标。例如,调用 getRight() 类似于进行以下计算:getLeft() + getWidth()。

尺寸、内边距和外边距

视图尺寸通过宽度和高度表示。实际上,视图拥有两对宽度和高度值。

第一对称为“测量宽度”和“测量高度”。这些尺寸定义视图希望在其父项内具有的大小。可通过调用 getMeasuredWidth() 和 getMeasuredHeight() 来获得这些测量尺寸。

第二对简称为“宽度”和“高度”,有时称为“绘制宽度”和“绘制高度”。这些尺寸定义绘制时和布局后,视图在屏幕上的实际尺寸。这些值可以(但不必)与测量宽度和测量高度不同。可通过调用 getWidth() 和 getHeight() 来获得宽度和高度。

为了测量尺寸,视图需将其内边距考虑在内。内边距以视图左侧、顶部、右侧和底部各部分的像素数表示。内边距可用于以特定数量的像素弥补视图内容。例如,若左侧内边距为 2,则会将视图内容从左边缘向右推 2 个像素。

可以使用 setPadding(int, int, int, int) 方法设置内边距,并通过调用 getPaddingLeft()、getPaddingTop()、getPaddingRight() 和 getPaddingBottom() 查询内边距。

尽管视图可以定义内边距,但它并不支持外边距。不过,视图组可以提供此类支持。

常见布局

ViewGroup 类的每个子类都会提供一种独特的方式,以显示在其中嵌套的视图。以下是 Android 平台中一些较为常见的内置布局类型。

注意:虽然可以通过将一个或多个布局嵌套在另一个布局内来实现界面设计,但我们应尽量使布局层次结构保持简洁。嵌套的布局越少,布局的绘制速度便会越快(扁平的视图层次结构优于深层的视图层次结构)。

线性布局

线性布局示意图
线性布局示意图

LinearLayout 是一个视图组,用于使所有子视图在单个方向(垂直或水平)保持对齐。可以使用 android:orientation 属性指定布局方向。 它一种使用单个水平行或垂直行来组织子项的布局。此布局会在窗口长度超出屏幕长度时创建滚动条。

LinearLayout 的所有子视图依次堆叠,因此无论子视图有多宽,垂直列表每行均只有一个子视图,水平列表将只有一行高(最高子视图的高度加上内边距)。LinearLayout 会考虑子视图之间的边距以及每个子视图的对齐方式(右对齐、居中对齐或左对齐)。

布局权重

LinearLayout 还支持使用 android:layout_weight 属性为各个子视图分配权重。此属性会根据视图应在屏幕上占据的空间大小,向视图分配“重要性”值。如果拥有更大的权重值,视图便可展开,填充父视图中的任何剩余空间。子视图可指定权重值,然后系统会按照子视图所声明的权重值比例,为其分配视图组中的任何剩余空间。默认权重为零。

均等分布

如要创建线性布局,让每个子视图使用大小相同的屏幕空间,可以将每个视图的 android:layout_height 设置为 "0dp"(针对垂直布局),或将每个视图的 android:layout_width 设置为 "0dp"(针对水平布局)。然后,请将每个视图的 android:layout_weight 设置为 "1"。

不等分布

也可创建线性布局,让子元素使用大小不同的屏幕空间:

如果有三个文本字段,其中两个声明权重为 1,另一个未赋予权重,那么没有权重的第三个文本字段就不会展开,而仅占据其内容所需的区域。另一方面,另外两个文本字段将以同等幅度展开,填充测量三个字段后仍剩余的空间。 如果有三个文本字段,其中两个字段声明权重为 1,而为第三个字段赋予权重 2(而非 0),那么现在相当于声明第三个字段比另外两个字段更为重要,因此,该字段将获得总剩余空间的一半,而其他两个字段均享余下的空间。

例子如下:

<?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:paddingLeft="16dp"
    android:paddingRight="16dp"
    android:orientation="vertical" >
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/to" />
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/subject" />
    <EditText
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:gravity="top"
        android:hint="@string/message" />
    <Button
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:text="@string/send" />
</LinearLayout>

此XML对应的示意图为:

相对布局

RelativeLayout 是一个以相对位置显示子视图的视图组。每个视图的位置可以指定为相对于同级元素的位置(例如,在另一个视图的左侧或下方)或相对于父级 RelativeLayout 区域的位置(例如在底部、左侧或中心对齐)。 RelativeLayout 是一个功能非常强大的界面设计实用工具,因为它可以消除嵌套视图组并使布局层次结构保持扁平化,从而提高性能。如果发现自己使用了多个嵌套的 LinearLayout 组,只需用一个 RelativeLayout 就可以替换它们。

它能指定子对象彼此之间的相对位置(子对象 A 在子对象 B 左侧)或子对象与父对象的相对位置(与父对象顶部对齐)。

相对布局示意图
相对布局示意图

放置视图

RelativeLayout 可以指定子视图相对于父视图或彼此(通过 ID 指定)的位置。因此,可以按照右边框对齐两个元素,或者使它们一上一下,屏幕居中,左侧居中,等等。默认情况下,所有子视图均绘制在布局的左上角,因此必须使用 RelativeLayout.LayoutParams 中提供的各种布局属性定义每个视图的位置。

有很多布局属性可用于 RelativeLayout 中的视图,部分示例包括:

android:layout_alignParentTop
如果为 "true",会将此视图的上边缘与父视图的上边缘对齐。
android:layout_centerVertical
如果为 "true",会将此子级在父级内垂直居中。
android:layout_below
将此视图的上边缘放置在使用资源 ID 指定的视图下方。
android:layout_toRightOf
将此视图的左边缘放置在使用资源 ID 指定的视图右侧。

以上只是少数几个示例。所有布局属性都记录在 RelativeLayout.LayoutParams。

每个布局属性的值要么是一个布尔值,用于启用相对于父级 RelativeLayout 的布局位置,要么是一个 ID,用于引用布局中作为视图放置依据的另一个视图。

在 XML 布局中,可以按照任何顺序声明对布局中其他视图的依赖关系。例如,即使“view2”是在层次结构中声明的最后一个视图,也可以声明“view1”位于“view2”之下。下面给出列子演示这种情况。

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"
    android:paddingLeft="16dp"
    android:paddingRight="16dp" >
    <EditText
        android:id="@+id/name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/reminder" />
    <Spinner
        android:id="@+id/dates"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_below="@id/name"
        android:layout_alignParentLeft="true"
        android:layout_toLeftOf="@+id/times" />
    <Spinner
        android:id="@id/times"
        android:layout_width="96dp"
        android:layout_height="wrap_content"
        android:layout_below="@id/name"
        android:layout_alignParentRight="true" />
    <Button
        android:layout_width="96dp"
        android:layout_height="wrap_content"
        android:layout_below="@id/times"
        android:layout_alignParentRight="true"
        android:text="@string/done" />
</RelativeLayout>

XML对应的效果图为:

网页视图

网页视图主要用于显示网页。这里不做介绍,将会单独开一篇文章介绍此视图的使用。

使用适配器构建布局

如果布局的内容是动态内容或未预先确定的内容,可以使用继承 AdapterView 的布局,在运行时用视图填充布局。AdapterView 类的子类会使用 Adapter 将数据与其布局绑定。Adapter 充当数据源与 AdapterView 布局之间的中间方:Adapter 会(从数组或数据库查询等来源)检索数据,并将每个条目转换为可添加到 AdapterView 布局中的视图。

适配器支持的常见布局包括:

列表视图

显示滚动的单列列表
显示滚动的单列列表

网格视图

显示滚动的行列网格
显示滚动的行列网格

可以参考博客中的列表篇,该篇章将会详细讲述列表与适配器

使用数据填充适配器视图

可以通过将 AdapterView 实例与 Adapter 绑定来填充 AdapterView(如 ListView 或 GridView),此操作会从外部来源检索数据,并创建表示每个数据条目的 View。

Android 提供几个 Adapter 子类,用于检索不同种类的数据和构建 AdapterView 的视图。两种最常见的适配器是:

ArrayAdapter

在数据源为数组时使用此适配器。默认情况下,ArrayAdapter 会通过对每个数组项调用 toString() 并将内容放入 TextView,为每个项创建视图。 例如,如果想在 ListView 中显示某个字符串数组,请使用构造函数初始化一个新的 ArrayAdapter,为每个字符串和字符串数组指定布局:

ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
        android.R.layout.simple_list_item_1, myStringArray);

此构造函数的参数是:

  • Context
  • 包含数组中每个字符串的 TextView 的布局
  • 字符串数组

然后,只需对 ListView 调用 setAdapter():


ListView listView = (ListView) findViewById(R.id.listview);
listView.setAdapter(adapter);

如需自定义每个项的外观,可以重写数组中各个对象的 toString() 方法。或者,如需为 TextView 之外的每个项创建视图(例如,如果想为每个数组项创建 ImageView),扩展 ArrayAdapter 类并替换 getView(),以返回我们想要为每个项获取的视图类型。

SimpleCursorAdapter

在数据来自 Cursor 时使用此适配器。使用 SimpleCursorAdapter 时,必须指定要为 Cursor 中的每个行使用的布局,以及应在哪些布局视图中插入 Cursor 中的哪些列。例如,如果想创建人员姓名和电话号码列表,则可以执行返回 Cursor(包含对应每个人的行,以及对应姓名和号码的列)的查询。然后,可以创建一个字符串数组,指定想要在每个结果的布局中包含 Cursor 中的哪些列,并创建一个整型数组,指定应放入每个列的对应视图:

String[] fromColumns = {ContactsContract.Data.DISPLAY_NAME,
                        ContactsContract.CommonDataKinds.Phone.NUMBER};
int[] toViews = {R.id.display_name, R.id.phone_number};

当实例化 SimpleCursorAdapter 时,传递要用于每个结果的布局、包含结果的 Cursor 以及以下两个数组:

SimpleCursorAdapter adapter = new SimpleCursorAdapter(this,
        R.layout.person_name_and_number, cursor, fromColumns, toViews, 0);
ListView listView = getListView();
listView.setAdapter(adapter);

然后,SimpleCursorAdapter 会使用提供的布局,将每个 fromColumns 项插入对应的 toViews 视图,从而为 Cursor 中的每个行创建视图。

如果在应用的生命周期中更改适配器读取的底层数据,则应调用 notifyDataSetChanged()。这将通知附加的视图数据已被更改,它应该自行进行刷新。

处理点击事件

可以实现 AdapterView.OnItemClickListener 接口,从而响应 AdapterView 中每一项上的点击事件。例如:

// Create a message handling object as an anonymous class.
private OnItemClickListener messageClickedHandler = new OnItemClickListener() {
    public void onItemClick(AdapterView parent, View v, int position, long id) {
        // Do something in response to the click
    }
};

listView.setOnItemClickListener(messageClickedHandler);

Loading...