视图中的布局

尝试 Compose 方式
Jetpack Compose 是 Android 推荐的 UI 工具包。了解如何在 Compose 中处理布局。

布局定义了您应用中用户界面的结构,例如在 activity 中。布局中的所有元素都使用 ViewViewGroup 对象的层次结构构建。一个 View 通常会绘制用户可以看到和交互的内容。一个 ViewGroup 是一个不可见的容器,它定义了 View 和其他 ViewGroup 对象的布局结构,如图 1 所示。

图 1. 视图层次结构的图示,它定义了 UI 布局。

View 对象通常被称为微件,可以是许多子类中的一种,例如 ButtonTextViewViewGroup 对象通常被称为布局,可以是提供不同布局结构的多种类型之一,例如 LinearLayoutConstraintLayout

您可以通过两种方式声明布局

  • 在 XML 中声明 UI 元素。 Android 提供了一种直接的 XML 词汇表,与 View 类及其子类(例如微件和布局)相对应。您还可以使用 Android Studio 的 布局编辑器,通过拖放界面构建您的 XML 布局。

  • 在运行时实例化布局元素。您的应用可以创建 ViewViewGroup 对象,并通过编程方式操作它们的属性。

在 XML 中声明 UI 可以将应用展示与控制其行为的代码分离。使用 XML 文件还可以更轻松地为不同的屏幕尺寸和方向提供不同的布局。这将在支持不同屏幕尺寸中进一步讨论。

Android 框架为您提供了灵活性,可以使用这两种方法中的一种或两种来构建您的应用 UI。例如,您可以在 XML 中声明应用的默认布局,然后在运行时修改布局。

编写 XML

使用 Android 的 XML 词汇表,您可以快速设计 UI 布局及其包含的屏幕元素,就像您在 HTML 中创建带有嵌套元素系列的网页一样。

每个布局文件必须恰好包含一个根元素,该元素必须是 ViewViewGroup 对象。定义根元素后,您可以添加其他布局对象或微件作为子元素,以逐渐构建定义布局的 View 层次结构。例如,这是一个使用垂直 LinearLayout 包含 TextViewButton 的 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" >
    <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 资源

编译应用时,每个 XML 布局文件都会编译成一个 View 资源。在应用的 Activity.onCreate() 回调实现中加载布局资源。为此,请调用 setContentView(),并以 R.layout.layout_file_name 的形式传递对布局资源的引用。例如,如果您的 XML 布局保存为 main_layout.xml,则可以按如下方式为您的 Activity 加载它:

Kotlin

fun onCreate(savedInstanceState: Bundle) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.main_layout)
}

Java

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

Activity 启动时,Android 框架会调用 Activity 中的 onCreate() 回调方法。有关 Activity 生命周期的更多信息,请参阅Activity 简介

属性

每个 ViewViewGroup 对象都支持其自己的各种 XML 属性。某些属性是 View 对象特有的。例如,TextView 支持 textSize 属性。但是,这些属性也会被扩展此类的任何 View 对象继承。有些属性对所有 View 对象都通用,因为它们是从根 View 类继承的,例如 id 属性。其他属性被认为是布局参数,这些属性描述了 View 对象的某些布局方向,由该对象的父 ViewGroup 对象定义。

ID

任何 View 对象都可以拥有与其关联的整数 ID,以在树中唯一标识该 View。当应用程序编译时,此 ID 被引用为整数,但该 ID 通常在布局 XML 文件中作为字符串分配给 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,而不是本地资源类中的 ID。

要创建视图并从您的应用中引用它们,您可以使用如下常见模式:

  1. 在布局文件中定义一个视图并为其分配一个唯一 ID,如以下示例所示:
    <Button android:id="@+id/my_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/my_button_text"/>
  2. 创建视图对象的实例并从布局中捕获它,通常在 onCreate() 方法中,如以下示例所示:

    Kotlin

    val myButton: Button = findViewById(R.id.my_button)

    Java

    Button myButton = (Button) findViewById(R.id.my_button);

在创建 RelativeLayout 时,为视图对象定义 ID 非常重要。在相对布局中,同级视图可以相对于另一个同级视图定义其布局,该视图通过唯一 ID 引用。

一个 ID 不需要在整个树中都是唯一的,但它必须在您搜索的树部分中是唯一的。它通常可能是整个树,所以最好尽可能使其唯一。

布局参数

名为 layout_something 的 XML 布局属性定义了 View 的布局参数,这些参数适用于它所在的 ViewGroup

每个 ViewGroup 类都实现了一个嵌套类,该类扩展了 ViewGroup.LayoutParams。此子类包含定义每个子视图大小和位置的属性类型,以适用于视图组。如图 2 所示,父视图组为每个子视图(包括子视图组)定义布局参数。

图 2. 视图层次结构的可视化,每个视图都关联有布局参数。

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

所有视图组都包含宽度和高度,使用 layout_widthlayout_height,并且每个视图都必须定义它们。许多 LayoutParams 包含可选的边距和边框。

您可以精确测量宽度和高度,但您可能不希望经常这样做。更常见的是,您使用以下常量之一来设置宽度或高度:

  • wrap_content:告诉您的视图将其自身大小调整为其内容所需的尺寸。
  • match_parent:告诉您的视图变得与其父视图组允许的大小一样大。

通常,不建议使用像素等绝对单位指定布局宽度和高度。更好的方法是使用相对测量,例如密度无关像素单位 (dp)、wrap_contentmatch_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() 查询内边距。

虽然视图可以定义内边距,但它不支持外边距。然而,视图组确实支持外边距。有关更多信息,请参阅 ViewGroupViewGroup.MarginLayoutParams

有关尺寸的更多信息,请参阅尺寸

除了通过编程方式设置外边距和内边距之外,您还可以在 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" >
      <TextView android:id="@+id/text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="16dp"
                android:padding="8dp"
                android:text="Hello, I am a TextView" />
      <Button android:id="@+id/button"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_marginTop="16dp"
              android:paddingBottom="4dp"
              android:paddingEnd="8dp"
              android:paddingStart="8dp"
              android:paddingTop="4dp"
              android:text="Hello, I am a Button" />
  </LinearLayout>
  

上述示例显示了外边距和内边距的应用。TextView 的四周应用了统一的外边距和内边距,而 Button 则展示了如何将它们独立应用于不同的边缘。

常见布局

每个 ViewGroup 类的子类都提供了独特的显示嵌套视图的方式。最灵活的布局类型,以及为保持布局层次结构扁平化提供最佳工具的类型,是 ConstraintLayout

以下是 Android 平台内置的一些常见布局类型。

创建线性布局

将其子项组织成单行水平或垂直排列,如果窗口长度超过屏幕长度,则创建滚动条。

构建动态列表

当布局内容是动态的或未预先确定时,可以使用 RecyclerViewAdapterView 的子类。RecyclerView 通常是更好的选择,因为它比 AdapterView 更有效地利用内存。

使用 RecyclerViewAdapterView 可以实现常见的布局,包括:

列表

显示可滚动的单列列表。

网格

显示可滚动的列和行网格。

RecyclerView 提供了更多可能性和创建自定义布局管理器的选项。

使用数据填充适配器视图

您可以通过将 AdapterView 实例绑定到 Adapter 来填充 AdapterView,例如 ListViewGridView,该适配器从外部源检索数据并创建表示每个数据条目的 View

Android 提供了几个 Adapter 子类,它们对于检索不同类型的数据和为 AdapterView 构建视图很有用。两个最常见的适配器是:

ArrayAdapter
当您的数据源是一个数组时,请使用此适配器。默认情况下,ArrayAdapter 通过调用每个项上的 toString() 并将内容放置在 TextView 中来为每个数组项创建一个视图。

例如,如果您有一个要显示在 ListView 中的字符串数组,请使用构造函数初始化一个新的 ArrayAdapter,以指定每个字符串的布局和字符串数组:

Kotlin

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

Java

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

此构造函数的参数如下:

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

然后在您的 ListView 上调用 setAdapter()

Kotlin

    val listView: ListView = findViewById(R.id.listview)
    listView.adapter = adapter
    

Java

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

要自定义每个项的外观,您可以覆盖数组中对象的 toString() 方法。或者,要为每个项创建非 TextView 的视图(例如,如果您希望每个数组项都有一个 ImageView),请扩展 ArrayAdapter 类并覆盖 getView() 以返回您为每个项想要的视图类型。

SimpleCursorAdapter
当您的数据来自 Cursor 时,请使用此适配器。使用 SimpleCursorAdapter 时,请指定要用于 Cursor 中每行的布局,以及 Cursor 中您要插入到布局视图中的列。例如,如果您想创建一个包含人名和电话号码的列表,您可以执行一个查询,该查询返回一个 Cursor,其中包含每个人的行以及姓名和号码的列。然后,您创建一个字符串数组,指定您希望在每个结果的布局中显示 Cursor 中的哪些列,以及一个整数数组,指定每个列需要放置的相应视图:

Kotlin

    val fromColumns = arrayOf(ContactsContract.Data.DISPLAY_NAME,
                              ContactsContract.CommonDataKinds.Phone.NUMBER)
    val toViews = intArrayOf(R.id.display_name, R.id.phone_number)
    

Java

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

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

Kotlin

    val adapter = SimpleCursorAdapter(this,
            R.layout.person_name_and_number, cursor, fromColumns, toViews, 0)
    val listView = getListView()
    listView.adapter = adapter
    

Java

    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 中每个项的点击事件。例如:

Kotlin

listView.onItemClickListener = AdapterView.OnItemClickListener { parent, view, position, id ->
    // Do something in response to the click.
}

Java

// 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);

其他资源

在 GitHub 上的 Sunflower 演示应用中查看布局是如何使用的。