Skip to content

Files

Latest commit

94f6a4e · Jan 8, 2022

History

History
1084 lines (769 loc) · 50 KB

04.md

File metadata and controls

1084 lines (769 loc) · 50 KB

四、用户界面布局

在每个Activity中布局 UI 元素是安卓应用开发最重要的方面之一。它定义了应用程序的外观,如何收集和向用户显示信息,以及用户如何在组成应用程序的各种活动之间导航。

Android 编程中的第一章简洁地提供了 XML 布局的简要介绍,但是这一章将更深入地研究 Android 的内置布局选项。在这一章中,我们将学习如何以线性、相对和网格布局来排列 UI 元素,这是 Android 维护人员推荐的常见导航模式,我们甚至会简单了解如何开发能够无缝伸缩到不同维度的设备无关布局。

这一章将与下一章齐头并进,在这一章中,我们将了解所有单独的用户界面元素。本章的运行示例使用按钮和文本字段来演示不同的布局功能,但是请记住,您可以用下一章中讨论的任何用户界面元素来替代相同的效果。

加载安卓项目

您可以从一个新的安卓项目中继续本章的示例,但是如果您想看到最终结果,您可以加载包含在本书示例代码中的用户界面用户界面项目。要打开一个现有的项目,启动 Eclipse (带有 ADT 插件),并在文件菜单中选择导入。这将打开一个如下图所示的对话框:

图 20:在 Eclipse 中打开一个项目

打开常规文件夹,选择现有项目进入工作区,然后点击下一步。在下一个对话框中,点击浏览...,找到这本书附带的资源文件夹,打开 UserInterfaceLayouts 文件夹。单击完成后,您应该会在包浏览器中看到该项目。

装载布局

在第一章中,我们让模板为我们加载 XML 布局文件,但是了解这是如何工作的很重要。幸运的是,这也是一个相对简单的过程。

当你编译一个项目时,安卓会自动从你的每个 XML 布局文件中生成一个View实例。像String资源一样,这些资源通过静态layout变量下的特殊R类来访问。例如,如果您想要访问从名为activity_main.xml的文件创建的View实例,您可以使用以下内容:

    R.layout.activity_main

要在Activity中显示这个View对象,你所要做的就是调用setContentView()方法。空白Activity模板的onCreate()方法总是包含对setContentView()的调用,以将关联视图加载到Activity中:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    }

请注意,您可以将任何 View实例传递给setContentView();使用生成的R.layout.activity_main对象只是使用基于 XML 的布局的一个有用的约定。如果你需要动态替换Activity的内容,在onCreate()之外调用setContentView()是完全合法的。

基本视图属性

在我们开始讨论安卓的内置布局方案之前,了解如何在特定布局中设置用户界面元素的大小和位置非常重要。接下来的三个部分将向您展示如何使用各种 XML 属性定义用户界面元素的维度、填充和边距。

大小

要设置特定用户界面元素的宽度,您只需要在 XML 布局文件中为该元素添加一个android:layout_width属性。同样,android:layout_height属性定义了元素的高度。这些参数的值必须是下列值之一:

  • wrap_content–这个常数使元素尽可能大,以包含其内容。
  • match_parent–该常量使元素与父元素的宽度和高度相匹配。
  • 显式尺寸–显式尺寸可以用像素(px)、与密度无关的像素(dp)、基于首选字体大小的缩放像素(sp)、英寸(in)或毫米(mm)来度量。例如,android:layout_width="120dp"将使元件 120 与设备无关的像素宽。
  • 对资源的引用——维度资源允许您将可重用的值抽象到资源文件中,就像我们在本书第一章中看到的strings.xml一样。可以使用@dimen/resource_id语法访问维度资源,其中resource_id是在dimens.xml中定义的资源的唯一标识。

让我们从探索wrap_content开始。将activity_main.xml更改为以下内容并编译项目(我们将在本章后面讨论LinearLayout元素):

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity"
    >

    <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Click me!" />

    </LinearLayout>

您应该会在屏幕顶部看到一个按钮,由于我们对两个维度都使用了wrap_content,所以它应该足够大以适合“Click me!”文本(带有一些默认填充)。如果您更改文本,按钮将展开或收缩以匹配。下图显示了您在 activity_main.xml 中创建的按钮。

图 21:两个维度都使用 wrap_content 常量的按钮元素

如果您将android:layout_widthandroid:layout_height更改为match_parent,按钮将充满整个屏幕,因为这就是父级LinearLayout的大小:

    <Button
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:text="Click me!" />

当您运行此程序时,它应该如下所示:

图 22:两个维度都使用 match_parent 常量的 Button 元素

如果您需要对 UI 元素的大小进行更多控制,您可以始终使用显式值手动定义它们的宽度和高度,如下所示:

    <Button
    android:layout_width="200dp"
    android:layout_height="60dp"
    android:text="Click me!" />

这使得按钮宽 200 个设备无关像素,高 60 个设备无关像素,如下图所示:

图 23:具有明确宽度和高度的按钮元素

最后一个选项是向dimens.xml添加一个显式尺寸,并从activity_main.xml引用该尺寸。如果您有许多元素需要共享同一个维度,这非常有用。维度资源引用看起来就像字符串资源引用,除了使用@dimen而不是@string:

    <Button
    android:layout_width="@dimen/button_width"
    android:layout_height="@dimen/button_height"
    android:text="Click me!" />

当然,在编译项目之前,您还必须将这些资源添加到dimens.xml中:

    <resources>

    <!-- Default screen margins, per the Android Design guidelines. -->
    <dimen name="activity_horizontal_margin">16dp</dimen>
    <dimen name="activity_vertical_margin">16dp</dimen>
    <dimen name="button_width">200dp</dimen>
    <dimen name="button_height">60dp</dimen>

    </resources>

这将具有与前面片段完全相同的效果,但是现在可以在其他布局中重用button_widthbutton_height值(或者用于同一布局中的其他元素)。

还值得注意的是,您可以混合匹配不同的宽度和高度值方法。比如宽度用200dp,高度用wrap_content,完全合法。

填充

填充是元素内容与其边框之间的空间。它可以通过以下任何属性来定义,所有这些属性都采用显式维度(例如120dp)或对资源的引用(例如@dimen/button_padding):

  • android:padding–为元素的所有边设置统一的值。
  • android:paddingTop–设置元素上边缘的填充。
  • android:paddingBottom–设置元素底边的填充。
  • android:paddingLeft–设置元素左边缘的填充。
  • android:paddingRight–设置元素右边缘的填充。
  • android:paddingStart–设置元素开始边缘的填充。
  • android:paddingEnd–设置元素结束边缘的填充。

图 24:填充属性

可以在ViewViewGroup元素中添加填充。对于前者,它定义了元素内容(例如,按钮的标题文本)与其边框之间的空间,对于后者,它定义了组的边缘与其所有子元素之间的空间。例如,以下按钮的标题文本周围将有 60 个独立于设备的像素:

    <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="60dp"
    android:text="Click me!" />

这将导致以下结果:

图 25:带有 60dp 填充的按钮

接下来,让我们尝试在包含ViewGroup(即LinearLayout元素)的顶部和底部添加一些填充,并使按钮与其父元素的大小相匹配,如下所示:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingTop="24dp"
    android:paddingBottom="24dp"
    tools:context=".MainActivity"
    >

    <Button
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:text="Click me!" />

    </LinearLayout>

这表明在子元素上使用match_parent考虑了父元素的填充。请注意以下截图顶部和底部的24dp填充:

图 26:一个按钮,其宽度和高度为 match_parent,在其父视图组中有 24dp 的顶部和底部填充

保证金

元素的边距是其边框与周围元素或父元素边缘之间的空间。可以使用android:layout_margin元素为任何ViewViewGroup指定。像android:padding一样,可以使用android:layout_marginTopandroid:layout_marginBottom等单独定义顶部、底部、左侧、右侧、开始和结束边距。

图 27:边距属性

例如,下面的代码通过在Button上定义顶部和底部边距,而不是在父级LinearLayout上定义顶部和底部填充,创建了与上一节中的示例相同的结果:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity"
    >

    <Button
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginTop="24dp"
    android:layout_marginBottom="24dp"
    android:text="Click me!" />

    </LinearLayout>

通用布局

安卓提供了三种主要的排列 UI 元素的方式:线性布局、相对布局和网格布局。前两种方法分别使用名为LinearLayoutRelativeLayout的类。这些是ViewGroup的子类,具有设置其子View对象的位置和大小的内置功能。网格布局使向用户显示一维和二维数据结构变得容易,并且它们比线性或相对布局稍微复杂一些。下图显示了可用的布局元素:

图 28:三种标准的安卓布局

线性布局

LinearLayout类水平或垂直排列所有包含的用户界面元素。它以在 XML 文件中出现的相同顺序显示每个元素,这使得创建由许多元素组成的复杂布局变得非常简单。

方位

一个LinearLayout元素需要知道它应该向哪个方向布局它的子元素。这是通过android:orientation属性指定的,该属性的值可以是horizontalvertical。水平布局将使每个子元素从左到右堆叠(与它们在源 XML 中出现的顺序相同)。例如,尝试将activity_main.xml更改为以下内容:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    tools:context=".MainActivity"
    >

    <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Click me!" />
    <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Click me!" />
    <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Click me!" />

    </LinearLayout>

如果您编译项目并在模拟器中运行它,您应该会看到一行三个按钮,如下所示:

图 29:水平方向的线性布局

但是如果你把方位改成vertical,就像这样:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity"
    >
    ...
    </LinearLayout>

三个按钮将出现在一列中:

图 30:垂直方向的线性布局

重量

您可以使用本章前面讨论的相同的android:layout_widthandroid:layout_height属性来定义每个子元素的大小,但是LinearLayout也启用了另一个称为android:layout_weight的大小选项。一个元素的权重决定了它相对于它的兄弟元素占据的空间。例如,如果您希望三个按钮均匀分布在父级LinearLayout的高度上,您可以使用以下选项:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity"
    >

    <Button
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    android:text="Click me!" />
    <Button
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    android:text="Click me!" />
    <Button
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    android:text="Click me!" />

    </LinearLayout>

请注意,对于垂直方向,您需要将每个元素的android:layout_height属性设置为0,以便使用指定的权重自动计算。对于水平方向,您需要将它们的android:layout_width设置为0。运行上面的代码应该会得到以下结果:

图 31:垂直线性布局中均匀加权的用户界面元素

您可以更改重量比,使按钮占据屏幕的不同比例。例如,将按钮权重更改为 2、1、1 将使第一个按钮占据半个屏幕,另外两个按钮占据四分之一屏幕:

图 32:垂直线性布局中不均匀加权的用户界面元素

为用户界面元素指定权重而不是显式尺寸是配置布局的一种灵活方式,因为它允许它们自动缩放以匹配父元素ViewGroup的大小。

相对布局

RelativeLayoutLinearLayout的替代项,允许您指定每个元素相对于界面中其他元素的位置。与LinearLayout不同的是,RelativeLayout的渲染界面中元素的顺序不一定要匹配底层的 XML 文件。相反,它们的位置是通过指定与另一个元素的关系来定义的(例如,“将按钮放在文本字段的左侧”或“将按钮放在其父元素的底部”)。位置是相对于其他元素定义的,这一事实使得这成为创建基于屏幕大小扩展和收缩的令人愉悦的用户界面的非常强大的方法。

相对于父代

RelativeLayout中,用户界面元素可以相对于其父元素或其同级元素进行定位。无论哪种情况,您都可以使用相对布局定义的布局属性之一来定义位置。。下面列出了相对于其父元素定位元素的属性,所有属性都采用布尔值:

  • android:layout_alignParentLeft–如果true,将元素的左侧与其父元素的左侧对齐。
  • android:layout_centerHorizontal–如果true,将元素在其父元素中水平居中。
  • android:layout_alignParentRight–如果true,将元素的右侧与其父元素的右侧对齐。
  • android:layout_alignParentTop–如果true,将元素的顶部与其父元素的顶部对齐。
  • android:layout_centerVertical–如果true,将元素垂直居中于其父元素。
  • android:layout_alignParentBottom–如果true,将元素的底部与其父元素的底部对齐。
  • android:layout_alignParentStart–如果true,将元素的起始边与其父元素的起始边对齐。
  • android:layout_alignParentEnd–如果true,将元素的端边与其父元素的端边对齐。
  • android:layout_centerInParent–如果true,将元素水平和垂直居中于其父元素。

例如,尝试将activity_main.xml更改为以下内容:

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    >

    <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentRight="true"
    android:layout_alignParentBottom="true"
    android:text="Top Button" />
    <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerHorizontal="true"
    android:layout_centerVertical="true"
    android:text="Middle Button" />
    <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentLeft="true"
    android:layout_alignParentTop="true"
    android:text="Bottom Button" />

    </RelativeLayout>

这将产生三个按钮的对角线行,如下所示:

图 33:相对于元素的父元素定位元素的相对布局

请注意,如果您在activity_main.xml中更改了 XML 元素的顺序,按钮仍然会出现在相同的位置。这与LinearLayout的行为不同,它相对于前一个元素放置每个元素。还要注意,如果您更改按钮尺寸或旋转模拟器(Ctrl+F12),它们仍然会出现在相对于屏幕的相应位置。

相对于兄弟姐妹

元素也可以相对于彼此定位。下面列出的属性都指定了与周围元素的图形关系,但是它们需要布局中另一个元素的标识,而不是布尔值:

  • android:layout_above–将元素的底边放置在具有指定标识的元素上方。
  • android:layout_below–将元素的上边缘定位在具有指定标识的元素下方。
  • android:layout_toLeftOf–将元素的右边缘定位到具有指定标识的元素的左侧。
  • android:layout_toRightOf–将元素的左边缘定位到具有指定标识的元素的右侧。
  • android:layout_alignBaseline–将元素的基线与具有指定标识的元素的基线对齐。
  • android:layout_alignTop–将元素的上边缘与具有指定标识的元素的顶部对齐。
  • android:layout_alignBottom–将元素的底边与具有指定标识的元素的底边对齐。
  • android:layout_alignLeft–将元素的左边缘与具有指定标识的元素的左边缘对齐。
  • android:layout_alignRight–将元素的右边缘与具有指定标识的元素的右边缘对齐。

例如,考虑以下使用相对于兄弟属性而不是相对于父属性的RelativeLayout。第二个和第三个按钮相对于第一个按钮定位,第一个按钮将位于默认的左上角。

    <Button
    android:id="@+id/topButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Top Button" />
    <Button
    android:id="@+id/middleButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@id/topButton"
    android:layout_toRightOf="@id/topButton"
    android:text="Middle Button" />
    <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@id/middleButton"
    android:layout_toRightOf="@id/middleButton"
    android:text="Bottom Button" />

该代码不是一条横跨整个屏幕的对角线按钮,而是一条对角线,所有的按钮角都接触:

图 34:一个相对布局,它相对于元素的兄弟来定位元素

因为我们是通过 ID 来引用它们的,所以我们需要为顶部和中间的按钮包含一个android:id属性。请记住,从第一章开始,第一次使用 XML 元素标识时,需要将其声明为"@+id/foo"。这个加号通常出现在android:id属性中,但不是必须出现——它应该总是出现在使用 ID 的第一个属性中。在下面的代码片段中,android:layout_toLeftOf属性是引用middleButtonbottomButton标识的第一个位置,因此它们需要以加号作为前缀:

    <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_toLeftOf="@+id/middleButton"
    android:layout_above="@id/middleButton"
    android:text="Top Button" />
    <Button
    android:id="@id/middleButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_toLeftOf="@+id/bottomButton"
    android:layout_above="@id/bottomButton"
    android:text="Middle Button" />
    <Button
    android:id="@id/bottomButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentRight="true"
    android:layout_alignParentBottom="true"
    android:text="Bottom Button" />

加号的使用有助于通过确保新的标识被明确标记为错误标识来减少错误标识的数量。例如,如果您无意中试图引用带有@id/bottumButton的元素,编译器会让您知道没有这样的元素。

上面的 XML 将顶部和中间的按钮相对于底部的按钮进行定位,然后将底部的按钮放在屏幕的右下角。这为您提供了以下布局:

图 35:一个相对布局,它相对于尚未声明的元素来定位按钮

还要注意,您可以将相对于同级的定位方法和相对于父级的定位方法结合起来。底部按钮相对于其父按钮(例如,android:layout_alignParentRight)定位,其他按钮相对于同级按钮(例如,android:layout_toLeftOf)定位。

列表和网格布局

到目前为止,我们已经学会了如何用LinearLayoutRelativeLayout快速创建用户界面;然而,这些接口的内容是完全静态的——它们的 UI 元素被硬编码到底层的 XML 文件中。当您想要使用动态数据创建用户界面时,事情会变得稍微复杂一些。数据驱动布局需要三个组件:

  • 数据集。
  • 适配器的一个子类,用于将数据项转换成View对象。
  • 适配器视图的一个子类,用于布局由Adapter创建的View对象。

在开始配置数据驱动布局之前,您需要一些数据。对于这一部分,我们的数据集将是一个简单的String对象数组,但是安卓也提供了许多内置的类,用于从文本文件、数据库或网络服务中获取数据。

接下来,需要一个Adapter将数据转换成View对象。例如,内置的 ArrayAdapter 获取一个对象数组,为每个对象创建一个TextView,并将其文本属性设置为数组中每个对象的toString()值。这些TextView实例然后被发送到AdapterView

AdapterView是一个ViewGroup子类,取代了前面章节中的LinearLayoutRelativeLayout类。其工作是整理Adapter提供的View物件。本节探讨两个最常见的AdapterView子类,列表视图网格视图,它们分别将这些视图定位到列表或网格中。

图 36:使用适配器在 GridView 中显示数据集

列表布局

让我们从简单的ListView开始运行。由于我们不再硬编码用户界面元素,我们的 XML 布局文件将非常简单——它只需要一个空的ListView元素。将activity_main.xml改为以下内容:

    <ListView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/listView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

这个ListView定义了我们的整个 XML 布局——它的所有用户界面元素都将被动态填充。然而,我们确实需要包含一个android:id属性,这样我们就可以从我们的活动中访问ListView实例。

接下来,我们需要更新我们的MainActivity类来定义一个数据集,将该数据集传递给一个Adapter,然后将该适配器传递给我们在activity_main.xml中定义的ListView。所以,将MainActivity.java改为如下:

    package com.example.userinterfacelayouts;

    import android.os.Bundle;
    import android.app.Activity;
    import android.widget.ListView;
    import android.widget.ArrayAdapter;

    public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    String[] data = new String[] { "Item 1", "Item 2", "Item 3",
    "Item 4" };

    ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data);
    ListView listView = (ListView) findViewById(R.id.listView);
    listView.setAdapter(adapter);
    }
    }

首先,我们创建一个String数组作为我们的数据集,并将其分配给本地data变量。然后,我们创建一个ArrayAdapter,它从数组中的每个String生成一个TextView。它的构造器接受一个Activity上下文,原型TextView的标识和数据数组。android.R.layout.simple_list_item_1片段是对安卓方便的内置布局之一的引用。您可以在布局文档中找到完整的列表。接下来,我们必须找到我们通过findViewById()添加到activity_main.xmlListView,然后我们需要将其适配器属性设置为我们刚刚配置的ArrayAdapter实例。

现在,您应该能够编译项目,并看到data数组中的四个字符串显示为TextView元素的列表:

图 37:由列表视图生成的动态布局

列表布局使处理大型数据集变得非常容易,而且您可以用任何视图来表示每个项目,这一事实使您可以显示具有多个属性的对象(例如,显示图像、姓名和首选电话号码的联系人列表)。

*#### 网格布局

网格布局使用与列表布局相同的数据/ Adapter / AdapterView模式,但是使用网格视图类代替ListViewGridView还定义了一些额外的配置选项,用于定义列数和每个网格项之间的间距,其中大部分包含在下面的代码片段中。尝试将activity_main.xml更改为:

    <GridView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/gridView"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:columnWidth="100dp"
    android:numColumns="auto_fit"
    android:verticalSpacing="5dp"
    android:horizontalSpacing="5dp"
    android:stretchMode="columnWidth" />

我们在MainActivity.java中唯一需要做的改变是更新ListViewGridView的引用:

    package com.example.userinterfacelayouts;

    import android.os.Bundle;
    import android.app.Activity;
    import android.widget.GridView;
    import android.widget.ArrayAdapter;

    public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    String[] data = new String[] { "Item 1", "Item 2", "Item 3",
    "Item 4", "Item 5", "Item 6",
    "Item 7", "Item 8", "Item 9"};

    ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data);
    GridView gridView = (GridView) findViewById(R.id.gridView);
    gridView.setAdapter(adapter);
    }
    }

这将为您提供 100 个独立于设备的像素宽的文本字段网格,每个字段之间有 5 个独立于设备的像素:

图 38:由 GridView 生成的动态布局

处理点击事件

当然,您可能会希望允许用户与ListViewGridView中的项目进行交互。但是,因为这两个类中的任何一个生成的接口都是动态的,所以当用户单击其中一个列表项时,我们不能使用android:onClick XML 属性来调用方法。相反,我们必须在MainActivity.java中定义一个通用的回调函数。这可以通过实现适配器视图来实现。onimcliclistener界面,像这样:

    package com.example.userinterfacelayouts;

    import android.os.Bundle;
    import android.app.Activity;
    import android.widget.GridView;
    import android.widget.ArrayAdapter;

    import android.util.Log;
    import android.view.View;
    import android.widget.TextView;
    import android.widget.AdapterView;
    import android.widget.AdapterView.OnItemClickListener;

    public class MainActivity extends Activity {

    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    String[] data = new String[] { "Item 1", "Item 2", "Item 3",
    "Item 4", "Item 5", "Item 6",
    "Item 7", "Item 8", "Item 9"};

    ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data);
    GridView gridView = (GridView) findViewById(R.id.gridView);
    gridView.setAdapter(adapter);

    gridView.setOnItemClickListener(new OnItemClickListener() {
    public void onItemClick(AdapterView<?> parent, View v,
    int position, long id) {
                      TextView selectedView = (TextView) v;
    Log.d(TAG, String.format("You clicked: %s",
    selectedView.getText()));
    }
    });
    }
    }

现在,每次点击GridView的一个项目都会调用onItemClick()方法。所有相关参数都作为参数传递给这个函数:父项AdapterView、被点击的View项、它在数据集中的位置以及它的row id。上面的回调只是将点击的View转换为TextView,并在 LogCat 中显示它包含的任何文本。

点击可以用与ListView布局完全相同的方式处理。

编辑数据集

当您想要更改运行时显示给用户的数据时,您所要做的就是编辑底层数据集,内置的 BaseAdapter 类负责相应地更新用户界面。在这一部分,我们将在布局中添加一个按钮,以便用户可以向网格中添加新项目,然后我们将重新实现onItemClick()功能,从列表中删除选定的项目。

首先,让我们将activity_main.xml更改为包含一个按钮。为此,我们将使LinearLayout成为根 XML 元素,并给它一个Button和一个GridView用于儿童:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical"
    tools:context=".MainActivity" >

    <Button
    android:layout_width="match_parent"
    android:layout_height="80dp"
    android:text="Add Item"
    android:onClick="addItem"/>

    <GridView android:id="@+id/gridView"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:columnWidth="100dp"
    android:numColumns="auto_fit"
    android:verticalSpacing="5dp"
    android:horizontalSpacing="5dp"
    android:stretchMode="columnWidth" />
    </LinearLayout>

请注意,将不同的布局方案嵌套在彼此内部是完全合法的(即,将一个GridView放在一个LinearLayout内部)。这使得用很少的努力创建复杂的动态用户界面成为可能。

接下来,我们需要改变相应的活动来处理添加项目按钮点击,并在GridView中选择项目时移除项目。这将需要对MainActivity.java进行多项更改:

    package com.example.userinterfacelayouts;

    import android.os.Bundle;
    import android.app.Activity;
    import android.widget.GridView;
    import android.widget.ArrayAdapter;

    import android.view.View;
    import android.widget.AdapterView;
    import android.widget.AdapterView.OnItemClickListener;
    import java.util.ArrayList;

    public class MainActivity extends Activity {

    private ArrayList<String> data;
    private ArrayAdapter<String> adapter;
    private int count;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    this.data = new ArrayList<String>();
    this.data.add("Item 1");
    this.data.add("Item 2");
    this.data.add("Item 3");
    this.count = 3;

    adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data);
    GridView gridView = (GridView) findViewById(R.id.gridView);
    gridView.setAdapter(adapter);

    gridView.setOnItemClickListener(new OnItemClickListener() {
    public void onItemClick(AdapterView<?> parent, View v,
    int position, long id) {
    data.remove(position);
    adapter.notifyDataSetChanged();
    }
    });
    }

    public void addItem(View view) {
    count++;
    String newItem = String.format("Item %d", count);
    this.data.add(newItem);
    this.adapter.notifyDataSetChanged();
    }
    }

首先,我们必须将表示数据集的静态String[]数组更改为可变的ArrayList。这将允许我们添加和删除项目。我们还必须将dataadapter局部变量更改为实例变量,以便我们可以在onCreate()之外访问它们。我们还添加了一个count变量来跟踪已经创建了多少个项目。

要从GridView中移除一个项目,我们所要做的就是用ArrayListremove()方法从数据集中移除它,然后调用adapter.notifyDataSetChanged()。后一种方法由BaseAdapter定义,它告诉Adapter它需要同步其关联的AdapterView项。只要基础数据集发生了变化,就应该调用它。

只要点击添加项目按钮,就会调用新的addItem()方法。首先递增count变量,然后使用它为新项目生成标题,将标题添加到数据集中,最后调用notifyDataSetChanged()刷新GridView

图 39:在数据集中添加和移除项目

现在,您应该能够编译项目并更改基础数据集。虽然此示例仅演示了在GridView中添加和移除项目,但编辑现有项目的方式完全相同。您所要做的就是更改ArrayList中的值,并调用适配器上的notifyDataSetChanged()

自定义列表视图(和网格视图)项目

到目前为止,我们一直使用的R.layout.simple_list_item_1 TextView在处理字符串数据时很方便,但是更复杂的数据项通常需要相应复杂的视图。幸运的是,实现自己的ListViewGridView项目相对容易。在本节中,我们将创建一个显示姓名和电话号码的自定义视图。

自定义ListView项需要三个组件:一个表示数据的类,一个为每个项定义View的 XML 布局文件,以及一个在视图中显示数据的自定义Adapter。这些组件将取代上例中的String数据、android.R.layout.simple_list_item_1视图和ArrayAdapter。我们的目标是这样的结果:

图 40:列表视图,每个项目都有自定义视图对象

首先,我们需要创建一个新的类来表示自定义数据项。我们称之为DataItem,它需要存储的只是两个属性,分别叫做namephoneNumber。您可以在 Eclipse 中创建一个新的类,方法是按下 Cmd+N (或者如果您在电脑上,按下 Ctrl+N),然后选择 Java ,然后选择。在名称字段输入**DataItem**,其他内容保留默认值。该表单应该如下所示:

图 41:创建数据项目类

点击完成后,在包浏览器中src/com.example.userinterfacelayouts下应该会找到一个名为**DataItem.java**的新文件。双击文件将其打开,并将其更改为以下内容:

    package com.example.userinterfacelayouts;

    public class DataItem {

    String name;
    String phoneNumber;

    public DataItem(String name, String phoneNumber) {
    this.name = name;
    this.phoneNumber = phoneNumber;
    }

    }

这为每个数据项定义了两个属性,以及一个方便的构造函数。这就是我们表示数据所需的全部内容,因此我们可以继续前进到定义与每个数据项相关联的View对象的 XML 布局文件。

要创建布局 XML 文件,请按 Cmd+N (如果您在电脑上,请按 Ctrl+N)并选择安卓,然后选择安卓 XML 布局文件文件字段使用**list_item.xml**,将根元素保留为默认值(我们将通过编辑 XML 进行更改)。单击完成会在res/layout文件夹中给你一个名为list_item.xml的新文件。我们希望每个项目如下所示:

图 42:由列表项创建的视图

我们将使用一个RelativeLayout和四个TextView元素来创建这个布局。将list_item.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:padding="12dp">

    <TextView
    android:id="@+id/nameLabel"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/nameLabel"/>

    <TextView
    android:id="@+id/nameValue"
    android:layout_toRightOf="@id/nameLabel"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

    <TextView
    android:id="@+id/phoneLabel"
    android:layout_below="@id/nameLabel"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/phoneLabel"/>

    <TextView
    android:id="@+id/phoneValue"
    android:layout_below="@id/nameLabel"
    android:layout_toRightOf="@id/phoneLabel"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

    </RelativeLayout>

id/nameValueid/phoneValue元素将由我们的自定义Adapter动态设置,因此它们不需要android:text属性,但是id/nameLabelid/phoneLabel元素是静态标签,因此它们可以从strings.xml填充。在strings.xml增加以下两行:

    <string name="nameLabel">Name:&#160;</string>
    <string name="phoneLabel">Phone:&#160;</string>

这里唯一新的部分是&#160;实体,这是一个不可打破的空间。这是让我们的标签正确显示所必需的。这处理了 XML 布局文件,所以我们剩下的就是定制的Adapter来将其与DataItem类连接起来。

创建另一个名为CustomAdapter的新类,并使用BaseAdapter作为子类。 BaseAdapterAdapter类的最小实现。它为Adapter的内部工作提供了一些基本的定义,这让我们能够专注于我们的CustomAdapter中更高级别的功能。这将是一个更长的类定义,所以让我们分步骤解决它。

让我们从将CustomAdapter.java更改为以下内容开始:

    package com.example.userinterfacelayouts;

    import java.util.ArrayList;

    import android.view.View;
    import android.view.ViewGroup;
    import android.view.LayoutInflater;
    import android.content.Context;
    import android.widget.BaseAdapter;
    import android.widget.TextView;

    public class CustomAdapter extends BaseAdapter {

    ArrayList<DataItem> data;
    Context context;
    private static LayoutInflater inflater = null;

    public CustomAdapter(Context context, ArrayList<DataItem> data) {
    this.context = context;
    this.data = data;
    inflater = (LayoutInflater) context
    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    }

这从导入我们稍后需要的类开始。然后,它定义了一些属性。data变量存储支持适配器的数据,适配器只是DataItem对象的一个ArrayListContext 类包含全局应用环境的信息,我们需要在context变量中存储对它的引用。最后,我们需要一个LayoutInflater将来自list_item.xml的 XML 转换成一个View层次结构。没有这个LayoutInflater,就不可能到达我们在list_item.xml中定义的文本字段。

接下来,我们定义一个构造函数,它以Context实例和数据集为参数。然后,它使用上下文获取一个LayoutInflater。一个Context实例的 getSystemService() 方法是实现这一点的标准方法。

现在我们已经准备好定义适配器的自定义行为。在CustomAdapter.java中增加以下方法:

    @Override
    public int getCount() {
    return data.size();
    }

    @Override
    public Object getItem(int position) {
    return data.get(position);
    }

    @Override
    public long getItemId(int position) {
    return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
    // See if the view needs to be inflated
    View view = convertView;
    if (view == null) {
    view = inflater.inflate(R.layout.list_item, null);
    }
    // Extract the desired views
    TextView nameText = (TextView) view.findViewById(R.id.nameValue);
    TextView phoneText = (TextView) view.findViewById(R.id.phoneValue);

    // Get the data item
    DataItem item = data.get(position);

    // Display the data item's properties
    nameText.setText(item.name);
    phoneText.setText(item.phoneNumber);

    return view;
    }

前三种方法提供了所有Adapter子类所需的信息。getCount()getItem()方法必须分别返回适配器代表的项目数和指定位置的项目数。这两者只需要从底层ArrayAdapter转发信息即可。getItemId()方法应该在指定位置返回项目的行标识。在这种情况下,我们可以只返回该项在数组中的位置。

我们的CustomAdapter类的核心是getView()方法,它必须返回一个表示指定位置数据项的View对象。这是list_view.xml文件转换成View的地方,它的TextView填充有来自相关DataItem的数据。

首先,我们需要查看适配器是否正在使用现有视图,该视图将通过convertView参数传入。如果不是,我们需要通过用我们在构造函数中记录的LayoutInflater实例膨胀XML 布局文件来创建一个新的View实例。这将解析 XML,将每个元素转换为其对应的视图对象,并将其添加到视图层次结构中。最后,我们需要找到我们在 XML 文件中定义的TextView元素,并使用它们来显示所请求的DataItem``namephoneNumber属性。下图显示了如何膨胀一个 XML 布局文件来访问包含的视图。

**

图 43:膨胀一个 XML 布局文件来访问包含的视图

这个过程应该能更好地理解Adapter的目的:它的getView()方法是将数据集调整为View层次显示。返回的View实例是父级ListView / GridView显示的内容。

我们已经设置了定制ListView的三个组件,但是我们仍然需要在主活动中把它们放在一起。activity_main.xml所需要的只是一个我们可以在MainActivity.java中参考的ListView:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal"
    tools:context=".MainActivity" >

    <ListView
    android:id="@+id/listView"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" />

    </LinearLayout>

然后,在MainActivity.java中,我们需要创建DataItem实例的数据集,并将我们的CustomAdapter附加到上面的ListView中:

    package com.example.userinterfacelayouts;

    import android.os.Bundle;
    import android.app.Activity;
    import android.widget.ListView;
    import java.util.ArrayList;

    public class MainActivity extends Activity {

    private ArrayList<DataItem> data;
    private CustomAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    this.data = new ArrayList<DataItem>();
    this.data.add(new DataItem("John Smith", "(555) 454-5545"));
    this.data.add(new DataItem("Mary Johnson", "(555) 665-5665"));
    this.data.add(new DataItem("Bill Kim", "(555) 446-4464"));

    adapter = new CustomAdapter(this, data);
    ListView listView = (ListView) findViewById(R.id.listView);
    listView.setAdapter(adapter);
    }
    }

您现在应该能够编译该应用程序,并看到我们的自定义list_item.xml显示为ListView中的每个项目。虽然这是一个只使用TextView小部件的简单例子,但是很容易扩展这个模式来创建任意复杂的ListViewGridView带有图像、按钮和其他用户界面小部件的项目。

要带走的概念是列表/网格、自定义视图、数据对象和适配器之间的交互。它们共同提供了一个可重用的模型-视图-控制器框架,使得开发人员可以用最少的努力向用户显示复杂的数据集。

嵌套布局

虽然布局优化是本书范围之外的高级主题,但值得注意的是,过深的视图层次结构可能是潜在的性能瓶颈。由于这个原因,你应该尽量保持你的视野组平坦和宽阔,而不是狭窄和深邃。请考虑以下布局:

图 44:由三个按钮组成的简单布局

这可以通过两种方式之一来创建。首先,让我们看看如何使用两个LinearLayouts来实现:一个水平的将左按钮与其他按钮分开,另一个嵌套的LinearLayout渲染顶部和底部按钮。

    <!-- Don't do this (it's not efficient) -->
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    tools:context=".MainActivity" >
    <Button
    android:layout_width="0dp"
    android:layout_height="120dp"
    android:layout_weight="1"
    android:text="Left" />
    <LinearLayout
    android:layout_width="0dp"
    android:layout_height="120dp"
    android:layout_weight="2"
    android:orientation="vertical">
    <Button
                    android:layout_width="match_parent"
                    android:layout_height="0dp"
                    android:layout_weight="1"
                    android:text="Top" />
    <Button
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    android:text="Bottom" />
    </LinearLayout>
    </LinearLayout>

然而,两个LinearLayout元素都包含带有layout_weight属性的小部件,这意味着它们的尺寸需要计算两次。不用说,这不是一个最佳布局。

创建该布局的另一种(也是首选的)方法是使用单个RelativeLayout。这样做的好处是将视图层次结构展平为一个层,避免了上面显示的嵌套LinearLayout的低效率。

    <!-- Do this instead (it's more efficient) -->
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >
    <Button
    android:id="@+id/leftButton"
    android:layout_width="120dp"
    android:layout_height="120dp"
    android:text="Left" />
    <Button
    android:id="@+id/topButton"
    android:layout_width="match_parent"
    android:layout_height="60dp"
    android:layout_toRightOf="@id/leftButton"
    android:text="Top" />
    <Button
    android:layout_width="match_parent"
    android:layout_height="60dp"
    android:layout_toRightOf="@id/leftButton"
    android:layout_below="@id/topButton"
    android:text="Bottom" />
    </RelativeLayout>

所以,一般的经验法则是尽可能避免嵌套LinearLayout s。对于定制的ListView / GridView项目来说尤其如此,因为视图会多次膨胀,因此任何低效率也会成倍增加。然而,这并不是说你应该永远不要使用LinearLayout s。它们非常容易配置,并且在不嵌套时效率很高。这使得它们适用于更简单的布局(例如,由连续文本字段组成的表单)。

有关创建高效布局的更多信息,请访问优化您的用户界面

总结

在这一章中,我们探讨了布局安卓用户界面的标准机制。首先,我们讨论了定义View元素的填充和边距的基本属性。然后,我们学习了如何使用内置的ViewGroup子类来定位元素:LinearLayoutRelativeLayoutListViewGridView。这些类使得用最少的编码将 UI 小部件组织成复杂的布局变得非常容易。

虽然知道如何在屏幕上定位元素很重要,但是如果你不知道有什么元素可用,这些知识就没有用了。下一章将通过调查最常见的用户界面小部件来充实您对安卓用户界面框架的理解。***