二、为视图展示数据

在第一章中,我们介绍了项目的基本创建,以及如何构建一个简单的用户界面。我们用足够的代码来支持我们的第一个Activity,以动态生成一些按钮,用户可以使用这些按钮来回答我们的选择题。

那么现在我们可以捕捉一些数据,但是显示数据呢?软件的一大优势是它能够以易于阅读的格式快速呈现和过滤大量数据。在这一章中,我们将看到一系列专为呈现数据而设计的安卓小部件。

大多数安卓以数据为中心的类都建立在Adapter对象之上,从而扩展了AdapterView 。一个Adapter可以被认为是一个摇摆模型类和一个渲染器(或演示者)的交叉。一个Adapter对象用于为你的软件需要显示给用户的数据对象创建View对象。这种模式允许软件维护和使用数据模型,并且只在实际需要时为每个数据对象创建一个图形View。这不仅有助于节省内存,而且从开发的角度来看也更符合逻辑。作为开发人员,您使用自己的数据对象,而不是试图将数据保存在图形小部件中(图形小部件通常不是最健壮的结构)。

最常见的AdapterView类有:ListView``Spinner``GridView。在这一章中,我们将介绍ListView类和GridView,并探索它们的各种使用方式和风格。

列出并选择数据

ListView类可能是显示数据列表最常见的方式。它由一个 ListAdapter对象支持,该对象负责保存数据并在View中呈现数据对象。一个ListView包含内置滚动,所以不需要用 ScrollView包装。

列表视图选择模式

ListView类允许三种基本的项目选择模式,由它的常量定义:CHOICE_MODE_NONECHOICE_MODE_SINGLECHOICE_MODE_MULTIPLE。可以使用布局 XML 文件中的 android:choiceMode属性或 Java 中的 ListView.setChoiceMode方法来设置ListView的模式。

选择模式和项目

ListView的选择模式会改变ListView结构的行为方式,但不会改变它的外观。ListView的外观主要由ListAdapter定义,它为应该出现在ListView中的每个项目提供View对象

无选择模式–选择 _ 模式 _ 无

在桌面系统上,这毫无意义——一个不允许用户选择任何东西的列表?然而,这是安卓系统的默认模式。原因是,当用户通过触摸进行导航时,这是有意义的。ListView的默认模式允许用户点击其中一个元素,并触发一个动作。这种行为的结果是,不需要“下一步”按钮或任何类似的东西。所以ListView的默认模式是像菜单一样。下面的截图是一个默认的ListView对象,显示来自String数组 Java 对象的不同字符串列表,取自 Android SDK 中的一个默认ApiDemos示例。

No selection mode – CHOICE_MODE_NONE

单一选择模式–选择 _ 模式 _ 单一

在此模式下,ListView更像是一个桌面List小部件。它有当前选择的概念,点击列表项无非是选择它。这种行为对于配置或设置之类的事情来说很好,用户希望应用记住他或她的当前选择。单个选择列表变得有用的另一个地方是当屏幕上有其他交互式小部件时。不过,注意不要在一个Activity里放太多信息。ListView占据几乎整个屏幕是很常见的。

单项选择:它不会直接改变列表项目的显示方式。列表项目的外观完全由ListAdapter对象定义。

然而,安卓确实在系统资源中提供了一组合理的默认值。在android包中你会发现一个R类。这是一种访问系统默认资源的编程方式。如果你想创建一个单一的选择ListView其中有一个<string-array>的颜色,你可以使用以下代码:

list.setAdapter(new ArrayAdapter(
        this,
        android.R.layout.simple_list_item_single_choice,
        getResources().getStringArray(R.array.colors)));

在这种情况下,我们使用从android.widget包中提供的 ArrayAdapter类。在第二个参数中,我们引用了名为simple_list_item_single_choice的安卓布局资源。安卓系统将该资源定义为在带有CHOICE_MODE_SINGLEListView中显示项目的默认方式。最典型的 这是一个标签,对于ListAdapter中的每个对象都有一个RadioButton

Single selection mode – CHOICE_MODE_SINGLE

多选模式–选择 _ 模式 _ 多选

在 多选模式下,ListView用普通复选框代替了单选模式的单选按钮。这种设计结构也常用于桌面和基于网络的系统。复选框很容易被用户识别,并且很容易返回并再次关闭选项。如果你想使用一个标准的ListAdapter,安卓系统会为你提供android.R.layout.simple_list_item_multiple_choice资源作为一个有用的默认资源:一个标签,里面有一个ListAdapter中每个对象的CheckBox

Multiple selection mode – CHOICE_MODE_MULTIPLE

添加页眉和页脚小部件

a ListView中的 Hea 页面和页脚允许您在List的顶部和底部放置额外的小部件。默认情况下,页眉和页脚小部件被视为列表中的项目(好像它们来自您的ListAdapter)。这意味着您可以选择它们,就像它们是List结构中的数据元素一样。标题项的一个非常简单的例子可能是:

TextView header = new TextView(this);
header.setText("Header View");
list.addHeaderView(header);

通常,您不希望页眉和页脚是ListView中的项目,而是一个标签或一组标签来标识ListView的部分,或者提供其他信息。在这种情况下,您需要告诉ListView您的页眉或页脚视图是不可选择的列表项。这可以通过使用addHeaderViewaddFooterView的扩展实现来实现:

TextView footer = new TextView(this);
footer.setText("Footer View");
list.addFooterView(footer, null, false);

ListView类将页眉和页脚如此紧密地集成到列表结构中,以至于您还可以提供一个Object,它将从Ad apterView.getItemAtPosition(index)方法返回。在前面的例子中,我们提供了null。每个标题项将使后续视图的索引偏移一个(就像您在ListView中添加新项一样)。第三个参数告诉ListView页眉或页脚是否应该被算作可选列表项(在我们前面的例子中不应该)。

如果你习惯了桌面小部件,安卓ListView上的页眉和页脚小部件会给你带来一点惊喜。它们将与列表项目的其余部分一起滚动,不会停留在ListView对象的顶部和底部。

创建一个简单的列表视图

为了介绍ListView类,我们将开始一个新的例子,这将在本章后面的各节中得到加强。我们将创建的第一个Activity将使用从<string-array>资源填充的简单ListView

行动时间——创建快餐菜单

到继续食物和饮食主题,让我们构建一个简单的应用,允许我们订购各种类型的快餐,并获得它的交付!用户将首先选择他们想要从哪里点菜,然后选择他们想要吃的各种食物。

  1. 使用安卓命令行工具创建一个新的android项目:

    java android create project -n DeliveryDroid -p DeliveryDroid -k com.packtpub.deliverydroid -a SelectRestaurantActivity -t 3

  2. 在自己喜欢的编辑器或 IDE 中打开/res/values/strings.xml文件。

  3. 创建一个字符串数组结构,列出我们的用户可以从

    java <string-array name="restaurants"> <item>The Burger Place</item> <item>Mick's Pizza</item> <item>Four Buckets \'o Fruit</item> <item>Sam\'s Sushi</item> </string-array>

    点的各种快餐店 4. 在自己喜欢的编辑器或 IDE 中打开/res/layout/main.xml文件。 5. 移除默认LinearLayout内的任何小部件。 6. 添加新的<ListView>元素。 7. 给<ListView>元素分配一个restaurant的标识:

    java <ListView android:id="@+id/restaurant"/>

  4. ListView的宽度和高度指定给fill_parent:T2

  5. 因为我们有一个字符串数组资源,我们想用它来填充ListView,我们可以直接在布局 XML 文件中引用它:

    java android:entries="@array/restaurants"

  6. 完成指定步骤后,您应该有一个如下所示的main.xml布局文件:

    ```java <?xml version="1.0" encoding="UTF-8"?>

    ```

刚刚发生的事情

如果将应用安装到模拟器中并运行它,您将看到一个屏幕,您可以从字符串数组资源中指定的餐馆列表中进行选择。请注意,ListView上的choiceMode被保留为CHOICE_MODE_NONE,使其成为一个更直接的菜单,用户可以在其中选择他们的餐厅,并立即进入其菜单。

What just happened

在本例中,我们使用布局 XML 文件中的android:entries属性来指定对字符串数组资源的引用,其中包含我们想要的列表项。通常,使用AdapterView需要创建一个Adapter对象来为每个数据对象创建View对象。

使用android:entries属性允许您从布局资源中指定ListView的数据内容,而不是要求您编写与AdapterView相关联的普通 Java 代码。然而,它有两个缺点需要注意:

  • 由生成的ListAdapter创建的View对象将始终是系统指定的默认值,因此无法轻松设置主题。
  • 您不能定义将在ListView中表示的数据对象。因为字符串数组很容易本地化,所以您的应用将依赖于项的索引位置来确定它们所指示的内容。

你可能会注意到截图顶部的标签Where should we order from?不是应用默认。Activity的标签在AndroidManifest.xml文件中定义如下:

<activity
    android:name=".SelectRestaurantActivity"
    android:label="Where should we order from?">

设置标准列表适配器的样式

标准的ListAdapter实现要求每个项目都用一个TextView项目来表示。默认的单项选择和多项选择是使用CheckedTextView构建的,虽然安卓系统中有很多其他TextView实现,但它确实有点限制了我们的选择。然而,标准的ListAdapter实现非常方便,并且为最常见的清单需求提供了可靠的实现。

既然一个带有CHOICE_MODE_NONEListView很像菜单,那把物品换成Button物件而不是普通的TextView物品不是很好吗?从技术上讲,ListView可以包含任何扩展TextView的小部件。然而,一些实现比其他实现更适合(例如,当用户触摸某个ToggleButtonView时,它不会保持指定的文本值)。

定义标准尺寸

在 这个例子中,我们将为应用创建各种菜单。为了保持一致的外观和感觉,我们应该定义一组标准尺寸,用于我们的每个布局文件。这样我们就可以为不同类型的屏幕重新定义尺寸。对于用户来说,没有什么比只能看到部分项目更令人沮丧的了,因为它的尺寸比他们的屏幕还要大。

创建一个包含维度的新资源文件。文件应命名为res/values/dimens.xml。将以下代码复制到新的 XML 文件中:

<?xml version="1.0" encoding="UTF-8"?>

<resources>
    <dimen name="item_outer_height">48sp</dimen>
    <dimen name="menu_item_height">52sp</dimen>
    <dimen name="item_inner_height">45sp</dimen>
    <dimen name="item_text_size">24sp</dimen>
    <dimen name="padding">15dp</dimen>
</resources>

我们 为列表项声明了两个高度维度:item_outer_heightitem_inner_heightitem_outer_height将是列表项的高度,而item_inner_height是列表项中包含的任何View对象的高度。

文件末尾的 padding维度用于定义两个视觉元素之间的标准空白量。这被定义为dp,因此它将根据屏幕的 DPI 保持不变(而不是根据用户的字体大小偏好进行缩放)。

类型

交互式项目的大小

在 t 他的造型中,你会注意到item_outer_heightmenu_item_height48sp52sp,这使得ListView中的物品相当大。安卓中列表视图项目的标准大小是48sp。列表项的高度至关重要。如果你的用户手指很大,如果你把他们做得太小,他们将很难点击他们的目标列表项。

这是安卓用户界面设计的通用“好做法”。如果用户需要触摸,就把它做大。

行动时间-改善餐厅列表

我们之前整理的 餐厅列表很不错,但这是菜单。为了进一步强调菜单,文本应该更加突出。为了用标准的ListAdapter实现来设计ListView的样式,您需要在您的 Java 代码中指定ListAdapter对象。

  1. 在名为menu_item.xmlres/layout目录中创建新文件。
  2. 将根 XML 元素创建为TextView :

    java <?xml version="1.0" encoding="UTF-8"?> <TextView />

  3. 导入安卓资源 XML 命名空间:

    java xmlns:android="http://schemas.android.com/apk/res/android"

  4. 通过设置其重力

    java android:gravity="center|center_vertical"

    ,使文本在TextView部件中居中 5. 我们将TextViewtextSize分配给我们的标准item_text_size :

    java android:textSize="@dimen/item_text_size"

  5. TextView文本默认颜色有点灰,我们希望是白色:

    java android:textColor="#ffffff"

  6. 我们希望TextView的宽度与包含它的ListView相同。因为这是我们的主菜单,所以它的高度是menu_item_height :

    java android:layout_width="fill_parent" android:layout_height="@dimen/menu_item_height"

  7. 现在我们有了一个风格化的TextView资源,我们可以将它合并到我们的菜单中。打开SelectRestaurantActivity.java文件。

  8. onCreate方法中,在你使用setContentView之后,我们需要一个对我们之前在main.xml中创建的ListView的引用:

    java ListView restaurants = (ListView)findViewById(R.id.restaurant);

  9. 将餐厅ListAdapter设置为新的ArrayAdapter,包含我们在values.xml文件中创建的餐厅字符串数组:

    java restaurants.setAdapter(new ArrayAdapter<String>( this, R.layout.menu_item, getResources().getStringArray(R.array.restaurants)));

刚刚发生的事情

我们 首先创建了一个新的布局 XML 资源,其中包含我们希望用于我们餐厅的ListView中每个列表项的样式TextView。您编写的menu_item.xml文件应该包含以下代码:

<?xml version="1.0" encoding="UTF-8"?>

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
              android:gravity="center|center_vertical"
              android:textSize="@dimen/item_text_size"
              android:textColor="#ffffff"
              android:layout_width="fill_parent"
              android:layout_height="@dimen/menu_item_height" />

与我们之前的布局资源不同,menu_item.xml不包含ViewGroup(如LinearLayout)。这是因为ArrayAdapter将尝试将menu_item.xml文件的根View铸造为TextView。因此,如果我们将TextView嵌套在某种ViewGroup中,我们会得到一个ClassCastException

我们还创建了一个ArrayAdapter实例来引用我们的menu_item XML 资源,以及我们之前创建的餐馆的字符串数组。此操作取消了在main.xml布局 XML 资源的ListView上使用android:entries属性。如果需要,可以删除该属性。您在SelectRestaurantActivity中的onCreate方法现在应该如下所示:

public void onCreate(final Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.main);

        final ListView restaurants = (ListView)
                findViewById(R.id.restaurant);

 restaurants.setAdapter(new ArrayAdapter<String>(
 this,
 R.layout.menu_item,
 getResources().getStringArray(R.array.restaurants)));
    }

试试 用 Apache Ant 将应用重新安装到模拟器中,你现在会看到一个看起来更像菜单的屏幕:

What just happened

玩一把围棋英雄——开发选择题应用

试着回到我们在第 1 章中写的选择题应用,开发一个简单的活动。它使用LinearLayoutButton对象来显示问题的可能答案,但是它也使用字符串数组来表示答案。尝试将应用修改为:

  • ListView代替LinearLayout
  • Button对象设计ListView,就像我们用TextView对象设计餐厅菜单一样
  • 确保Button列表项之间有一定的间隔,这样它们就不会靠得太近

创建自定义适配器

当我们想点食物时,我们经常想点不止一个同样的东西。ListView实现和标准ListAdapter实现允许我们选择奶酪汉堡项目,但不允许我们请求 3 奶酪汉堡。为了显示用户可以多次订购的不同食物的菜单,我们需要一个定制的ListAdapter实现。

为汉堡店创建菜单

对于我们主菜单中的每个餐厅,我们将建立一个单独的Activity类。实际上,这不是一个好主意,但它允许我们研究组织和显示菜单数据的不同方式。我们的第一站是汉堡店,我们为用户提供一份汉堡清单,让他们在屏幕上点击他们想要的汉堡。每次他们点击一个列表项,就会点另一个汉堡。我们将在汉堡名称的左侧以粗体显示他们订购的汉堡数量。在他们没有点的汉堡旁边,不应该有数字(这样用户可以快速看到他们点的是什么)。

汉堡班

为了显示菜单,我们需要一个简单的Burger数据对象。Burger类会有一个名字显示在菜单中,用户点的是Burger号。使用以下代码在项目的根包中创建一个Burger.java文件:

class Burger {
    final String name;
    int count = 0;

    public Burger(String name) {
        this.name = name;
    }
}

您会注意到在前面的代码中没有 getters 和 setters,并且namecount字段都被声明为受包保护的。在 2.2 之前的安卓版本中,与直接的字段查找相比,方法花费了大量的费用。由于这个类将是渲染过程的一小部分(我们将从中提取数据进行显示),我们应该确保花费尽可能少。

行动时间-创建汉堡项目布局

为了给汉堡店创建一个好看的菜单,首先要做的是设计菜单项。这与使用布局 XML 资源对餐厅列表进行样式化的方式非常相似。但是,由于这次我们将自己构建ListAdapter,所以我们不会被迫使用单个TextView,而是可以构建更复杂的布局。

  1. 在名为burger_item.xmlres/layout目录中创建新的 XML 文件。该文件将用于ListView的每个汉堡。
  2. 将布局的根声明为horizontal LinearLayout(注意高度,这将是ListView中每个项目的高度):

    java <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="@dimen/item_outer_height">

  3. 接下来,声明一个TextView,我们将使用它作为counter来表示正在订购的汉堡数量。我们稍后将通过它的 ID

    java <TextView android:id="@+id/counter" />

    访问它 4. counter文本大小与应用中的所有其他列表项完全相同。不过应该是bold,所以很容易识别,读出来:

    ```java android:textSize="@dimen/item_text_size" android:textStyle="bold"

    ```

  4. 我们也希望counter是正方形的,所以设置宽度和高度完全相同:

    java android:layout_width="@dimen/item_inner_height" android:layout_height="@dimen/item_inner_height"

  5. 我们还希望将文本居中放在counter :

    java android:gravity="center|center_vertical"

    中 7. 我们还需要一个文本空间来显示汉堡的名称:

    java <TextView android:id="@+id/text" />

  6. 文字大小标准:

    java android:textSize="@dimen/item_text_size"

  7. 我们希望countertext标签之间有一点空间:

    java android:layout_marginLeft="@dimen/padding"

  8. 标签的宽度应该填满ListView,但是我们希望两个TextView对象的大小相同:

    java android:layout_width="fill_parent" android:layout_height="@dimen/item_inner_height"

  9. 标签的文字应垂直居中,以匹配counter的位置。但是,标签应该左对齐:

    java android:gravity="left|center_vertical"

刚刚发生了什么?

您刚刚构建了一个非常好的LinearLayout ViewGroup,将为我们从汉堡店出售的每个汉堡渲染。由于counter TextView是一个独立于标签的对象,因此可以独立设置样式和管理。如果我们想独立地应用额外的风格,这将使事情变得更加灵活。您完整的burger_item.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="@dimen/item_outer_height">

 <TextView android:id="@+id/counter"
 android:textSize="@dimen/item_text_size"
 android:textStyle="bold"
 android:layout_width="@dimen/item_inner_height"
 android:layout_height="@dimen/item_inner_height"
 android:gravity="center|center_vertical" />

 <TextView android:id="@+id/text"
 android:textSize="@dimen/item_text_size"
 android:layout_marginLeft="@dimen/padding"
 android:layout_width="fill_parent"
 android:layout_height="@dimen/item_inner_height"
 android:gravity="left|center_vertical" />
</LinearLayout>

行动时间——展示汉堡物件

如果您的数据对象是字符串或者很容易表示为字符串,那么标准的ListAdapter类可以很好地工作。为了在屏幕上很好地显示我们的Burger对象,我们需要编写一个自定义的ListAdapter类。幸运的是,安卓为我们提供了一个很好的名为BaseAdapterListAdapter实现框架类。

  1. 创建一个名为BurgerAdapter的新类,并让它从android.widget.BaseAdapter类扩展:

    java class BurgerAdapter extends BaseAdapter {

  2. 一个Adapter是表示层的一部分,但也是ListView的底层模型。在BurgerAdapter中,我们存储了一组Burger对象,这些对象是我们在构造器中分配的:

    java private final Burger[] burgers; BurgerAdapter(Burger... burgers) { this.burgers = burders; }

  3. 直接在Burger对象的数组上实现Adapter.getCount()Adapter.getItem(int)方法:

    ```java public int getCount() { return burgers.length; }

    public Object getItem(int index) { return burgers[index]; } ```

  4. 一个Adapter也要提供各种物品的标识符,我们只需要返回它们的索引:

    java public long getItemId(int index) { return index; }

  5. 当一个Adapter被请求一个列表项的View时,它可能被给予一个可重用的现有View对象。我们将实现一个简单的方法来处理这种情况,如果需要的话,使用android.view包中的LayoutInflator类对我们之前编写的burger_item.xml文件进行膨胀:

    java private ViewGroup getViewGroup(View reuse, ViewGroup parent) { if(reuse instanceof ViewGroup) { return (ViewGroup)reuse; } Context context = parent.getContext(); LayoutInflater inflater = LayoutInflater.from(context); ViewGroup item = (ViewGroup)inflater.inflate( R.layout.burger_item, null); return item; }

  6. BurgerAdapter中对我们来说最重要的方法就是getView法。这就是ListView要求我们提供一个View对象来表示它需要显示的每个列表项的地方:

    java public View getView(int index, View reuse, ViewGroup parent) {

  7. 为了获取给定项目的正确View,您首先需要使用getViewGroup方法来确保您有burger_item.xml ViewGroup在:

    java ViewGroup item = getViewGroup(reuse, parent); TextView counter = (TextView)item.findViewById(R.id.counter); TextView label = (TextView)item.findViewById(R.id.text);

    中显示Burger项目 8. 我们将用来自请求的index处的Burger对象的数据填充这两个TextView对象。如果当前count为零:

    java Burger burger = burgers[index]; counter.setVisibility( burger.count == 0 ? View.INVISIBLE : View.VISIBLE); counter.setText(Integer.toString(burger.count)); label.setText(burger.name); return item;

    ,则counter小部件需要对用户隐藏

刚刚发生了什么?

我们刚刚编写了一个定制的Adapter类,在一个ListView中向用户呈现一组Burger对象。当ListView调用Adapter.getView方法时,它将尝试传入从先前对Adapter.getView的调用返回的View对象。将为ListView中的每个项目创建一个View对象。但是当ListView显示的数据发生变化时,ListView会要求ListAdapter重新使用它第一次生成的每个View对象。尝试并尊重这种行为很重要,因为它会直接影响应用的响应能力。在我们前面的例子中,我们实现了 getViewGroup方法,以便它将这个需求考虑在内。

getViewGroup方法也被用来对我们写的burger_item.xml文件进行膨胀。我们使用一个LayoutInflator对象来实现,这正是Activity.setContentView(int)方法加载 XML 布局资源的方式。我们从parent ViewGroup获取的Context对象(通常是ListView)定义了我们将从哪里加载布局资源。如果用户没有选择Burger,我们使用View.setVisibility方法隐藏计数器TextView。在 AWT 和 Swing 中,setVisible方法采用Boolean参数,而在 Android 中,setVisibility采用int值。原因是安卓将可见性视为布局过程的一部分。在我们的例子中,我们希望counter消失,但仍然占据布局中的空间,这将保持text标签彼此左对齐。如果我们希望计数器消失并且不占用空间,我们可以使用:

counter.setVisibility(burger.count == 0
        ? View.GONE
        : View.VISIBLE);

ListView对象将自动处理选中项目的高亮显示。这包括当用户将手指放在项目上时,以及当他们使用跟踪板或方向按钮导航ListView时。根据标准的用户界面约定,当一个项目被突出显示时,它的背景通常会改变颜色。

然而,在以某种方式直接捕获用户输入的ListView中使用小部件(即ButtonEditText)将导致ListView停止显示该小部件的选择高亮。事实上,这将完全阻止ListView注册OnItemClick事件。

类型

列表视图中的自定义分隔符

如果你覆盖了ListAdapterisEnabled(int index)方法,你可以策略性的禁用ListView中的指定项目。这种方法的一个常见用途是将某些项目变成逻辑分隔符。例如,按字母顺序排序的列表中的一个节分隔符,包含下一个“节”中所有项目的第一个字母。

创建博客空间活动类

为了将Burger菜单放在屏幕上,并允许用户订购项目,我们需要一个新的Activity类。我们需要知道用户何时触摸列表中的项目,为此我们需要实现OnItemClickListener界面。当特定事件发生时(在这种情况下,用户触摸ListView中的特定项目),注册为侦听器的对象将调用与发生的事件的细节相关的方法。安卓提供了一个简单的ListActivity类来为这个场景提供一些默认的布局和实用方法。

行动时间——实施紧急行动

为了用BurgerAdapter类呈现Burger对象的ListView,我们需要为汉堡店创建一个Activity实现。新的Activity还将负责收听ListView中项目的“触摸”或“点击”事件。当用户触摸其中一个项目时,我们需要更新型号和ListView来反映用户已经订购了另一个Burger

  1. 在项目的根包中创建一个名为TheBurgerPlaceActivity的新类,并确保它扩展了ListActivity :

    java public class TheBurgerPlaceActivity extends ListActivity {

  2. 覆盖Activity.onCreate方法。

  3. 调用super.onCreate允许正常安卓启动。
  4. 用一些Burger对象创建一个BurgerAdapter的实例,并将其设置为ListActivity代码使用的ListAdapter:

    java setListAdapter(new BurgerAdapter( new Burger("Plain old Burger"), new Burger("Cheese Burger"), new Burger("Chicken Burger"), new Burger("Breakfast Burger"), new Burger("Hawaiian Burger"), new Burger("Fish Burger"), new Burger("Vegatarian Burger"), new Burger("Lamb Burger"), new Burger("Rare Tuna Steak Burger")));

  5. 最后,执行onListItemClicked方法,代码如下:

    java protected void onListItemClick( ListView parent, View item, int index, long id) { BurgerAdapter burgers = (BurgerAdapter) parent.getAdapter(); Burger burger = (Burger)burgers.getItem(index); burger.count++; burgers.notifyDataSetInvalidated(); }

刚刚发生了什么?

TheBurgerPlaceActivity的这个实现有一个简单的硬编码列表Burger对象显示给用户,并创建一个BurgerAdapter将这些对象转换成我们之前创建的burger_item View对象。

当用户点击列表项时,我们在onItemClick方法中增加相关Burger对象的count。然后我们在BurgerAdapter上呼叫notifyDataSetInvalidated()。该方法将通知ListView底层数据已经改变。当数据发生变化时,ListView将对ListView中的每一项重新调用Adapter.getView方法。

一个ListView中的项目由有效静态的View对象表示。这意味着当数据模型更新时,必须允许Adapter更新或重新创建该View。一个常见的替代方法是获取代表您的更新数据的View,并直接更新它。

注册并启动紧急位置活动

为了从我们的餐厅菜单开始新的Activity课程,您需要在AndroidManifest.xml文件中注册。首先,在编辑器或 IDE 中打开AndroidManifest.xml文件,将下面的<activity>代码复制到<application>...</application>块中:

<activity android:name=".TheBurgerPlaceActivity"
          android:label="The Burger Place\'s Menu">

    <intent-filter>
        <action android:name=
                "com.packtpub.deliverydroid.TheBurgerPlaceActivity"/>
    </intent-filter>
</activity>

要启动Activity,你需要回到SelectRestaurantActivity并实现OnItemClickListener界面。在restaurants ListView上设置Adapter后,将SelectRestaurantActivity设置为restaurants ListViewOnItemClickListener。您可以使用onItemClick方法中的Intent对象启动TheBurgerPlaceActivity。您的SelectRestaurantActivity类现在应该看起来像下面的代码片段:

public class SelectRestaurantActivity extends Activity
 implements OnItemClickListener {

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.main);

        ListView restaurants = (ListView)
                findViewById(R.id.restaurant);

        restaurants.setAdapter(new ArrayAdapter<String>(
                this,
                R.layout.menu_item,
                getResources().getStringArray(R.array.restaurants)));

 restaurants.setOnItemClickListener(this);
    }

 public void onItemClick(
 AdapterView<?> parent,
 View item,
 int index,
 long id) {

 switch(index) {
 case 0:
 startActivity(new Intent(
 this,
 TheBurgerPlaceActivity.class));
 break;
 }
 }
}

当您重新安装应用并在模拟器中启动它时,您将能够导航到汉堡店并订购汉堡。按下汉堡店菜单中的硬件“返回”按钮,将带您返回餐厅菜单。

Registering and starting TheBurgerPlaceActivity

突击测验

  1. ListView对象的选择模式设置为CHOICE_MODE_SINGLE将会:
    1. 每个项目加一个RadioButton
    2. 什么都不做(这是默认设置)。
    3. 使ListView跟踪一个“选定”的项目。
  2. 一个ListAdapter定义一个ListView如何显示它的项目。什么时候会被要求为物品对象重用一个View
    1. 当数据模型失效或改变时。
    2. 每一件物品上都有橡皮图章。
    3. ListView重绘自身时。
  3. ListView可滚动时,页眉和页脚对象将被定位:
    1. 滚动项目的上方和下方。
    2. 水平并排,在滚动项目的上方和下方。
    3. 与其他项目一起滚动。

使用可扩展的列表视图类

T he ListView类非常适合显示小到中等数量的数据,但是有时它会向您的用户提供过多的信息。想想一个电子邮件应用。如果你的用户是一个重度电子邮件用户,或者订阅了一些邮件列表,他们很可能在一个文件夹中有几百封电子邮件。即使他们可能不需要滚动超过前几个,看到滚动条缩小到几个像素的大小对你的用户没有很好的心理效果。

I n 桌面邮件客户端,你会经常把邮件列表按时间分组:今天、昨天、本周、本月、永远(或者类似的)。安卓包括这种分组的ExpandableListView。每个项目都嵌套在一个组中,用户可以显示或隐藏一个组。它有点像树形视图,但总是嵌套在一个级别上(您不能在组外显示项目)。

类型

大规模可扩展列表视图组

有时,即使一个ExpandableListView也不足以将数据量保持在合理的长度。在这些情况下,考虑给你的用户组中的前几个项目,并在最后添加一个特殊的查看更多项目。或者,对组使用ListView,对嵌套项使用单独的Activity

创建可扩展的适配器实现

S 由于ExpandableList类包含两个细节层次,它不能与只处理一个层次的普通ListAdapter对抗。相反,它包括使用两组方法的ExpandableListAdapter:一组用于组级别,另一组用于项目级别。当实现一个定制的ExpandableListAdapter时,通常最容易让你的ExpandableListAdapter实现从BaseExpandableListAdapter继承,因为它提供了事件注册和触发的实现。

T 他ExpandableListAdapter会在每个组项目的左侧放置一个箭头指针,指示该组是打开还是关闭(很像下拉/组合框)。箭头被渲染在由ExpandableListAdapter返回的组的View对象的顶部。为了防止您的群组标签被这个箭头部分遮挡,您需要在列表项目View结构中添加填充。列表项的默认填充可用作主题参数expandableListPreferredItemPaddingLeft,您可以使用它:

android:paddingLeft=
    "?android:attr/expandableListPreferredItemPaddingLeft"

为了保持你的ExpandableListView看起来一致,最好给ExpandableListView的正常(子)项目添加相同数量的填充(以保持它们的文本与其父组的文本对齐),除非你把一个项目放在左边,比如一个图标或复选框。

玩得开心点定制披萨

举个Mick's Pizza的例子,我们将创建一个分类披萨配料的菜单。每个浇头都有一个名字,无论是披萨上的“开”还是“关”,或者是“额外的”(例如,额外的奶酪)。每个项目使用两个水平排列的TextView对象。右边的TextView可以保存浇头的名称。左边的TextView可以是不加配料的空的,On可以是加配料的空的,Extra可以是用户想要的比平时多的配料。

ToppingCatagory对象创建一个对象模型,包含一个名称和一组PizzaTopping对象。你会想要存储一些状态,每个浇头是否被订购,数量是多少。

您还想实现一个PizzaToppingAdapter类,扩展BaseExpandableListAdapter类。为组标签使用默认的安卓simple_expandable_list_item_1布局资源,并为项目标签使用新的定制布局资源。

当用户点击披萨浇头时,它会在三个值之间改变状态:关闭开启额外

使用ListView.getAdapter()方法不会返回您的ExpandableListAdapter实现,而是返回一个包装器。要获取原件ExpandableListAdapter,您需要使用getExpandableListAdapter()方法。您还想利用ExpandableListView. OnChildClickListener界面接收点击事件。

当您的新Activity完成时,您应该有一个类似如下的屏幕:

Have a go hero - ordering customized pizzas

使用 GridView 类

A GridView是一个ListView,列数固定,从左到右,从上到下排列。标准(非主题)安卓应用菜单像GridView一样排列。GridView类使用与ListView格式完全相同的ListAdapter。然而,由于其固定的列数,a GridView非常适合图标列表。

类型

有效使用网格视图

一个 GridView可以在单个屏幕上显示比一个ListView多得多的信息,代价是不能显示同样多的文本信息。从可用性的角度来看,图标通常比文本更容易使用。由于图标的颜色,它们比文本更容易被识别。当你有可以用图标表示的信息时,这样显示是个好主意。但是,请记住,图标需要在单个屏幕中是唯一的,最好是在整个应用中。

F 或者我们的下一个例子,我们将使用GridView构建四桶水果菜单。GridView菜单上的每个项目都有一个图标,图标下方有项目的名称。因此,当完成时,它将看起来很像标准的安卓应用菜单。下一个例子将不再关注ListAdapter的实现,因为它与我们为汉堡店建造的ListAdapter基本相同。

类型

触摸屏设备上的图标

思考触摸屏设备上的图标很重要。它们需要比平时更加不言自明,或者附有一些文字。有了触摸屏,很难提供任何类型的上下文帮助,比如工具提示。如果用户正在触摸对象,它通常会被他们的手指和/或手遮住,使得图标和工具提示不可见。

行动时间-创建水果图标

为了将各种类型的水果显示为图标,我们需要创建一个布局 XML 文件。GridView中的每个图标将被表示为该布局的一个实例,其方式与列表项在ListView中的表示方式完全相同。我们为图标创建了一个ImageView项目,在它下面有一个TextView作为标签。

  1. res/layout目录下创建一个名为fruit_item.xml的文件。
  2. 将图标的根元素声明为垂直的LinearLayout :

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

  3. 创建ImageView元素作为我们的图标:

    java <ImageView android:id="@+id/icon" android:layout_width="fill_parent" android:layout_height="wrap_content"/>

  4. 接下来,创建TextView元素作为标签:

    java <TextView android:id="@+id/text" android:textSize="@dimen/item_description_size" android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center|center_vertical" />

刚刚发生了什么?

文件对于我们的菜单图标来说是一个非常简单的布局,并且可以用于许多其他类型的图标,表示为一个网格。ImageView默认情况下,对象会尝试根据其大小缩放其内容。在我们前面的例子中,根LinearLayout的宽度和高度定义为fill_parent。当作为单个项目放置在GridView中时,使用fill_parent作为尺寸将导致LinearLayout填充为该网格项目提供的空间(而不是整个GridView)。

在网格视图中显示图标

我们需要一个对象模型和ListAdapter,以便在GridView中向用户显示水果。适配器在这一点上相当简单。这是一个普通的ListAdapter实现,建立在一个项目类和我们为图标定义的布局 XML 之上。

对于每一种水果,我们需要一个同时包含水果名称和图标的对象。用下面的代码在根包中创建一个FruitItem类:

class FruitItem {
    final String name;
    final int image;

    FruitItem(String name, int image) {
        this.name = name;
        this.image = image;
    }
}

在前面的代码中,我们将水果的图标图像引用为一个整数。当我们在安卓中引用应用资源和 id 时,总是用整数。在这个例子中,我们假设所有不同类型的水果都有一个图标作为应用资源。另一种选择是在每个FruitItem中保存对Bitmap对象的引用。然而,这意味着当FruitItem可能不在屏幕上时,在内存中保存完整的图像。

为了让安卓资产打包工具识别和存储图标,您需要将它们放在res/drawable目录中。

类型

安卓影像资源

一般来说,在安卓系统中,将位图图像存储为 PNG 文件被认为是一种很好的做法。因为您将从代码中访问这些文件,所以请确保它们具有 Java 友好的文件名。PNG 格式(不像 JPG)是无损的,可以有各种不同的颜色深度,并正确处理透明度。总的来说,这是一个很好的图像格式。

行动时间到——建立水果菜单

对于或水果四桶菜单,我们需要一个ListAdapter实现来将FruitItem对象渲染到fruit_item.xml布局资源中。我们还需要GridView的布局资源,我们将在新的Activity类中加载。

  1. 在项目的根包中创建一个名为FruitAdapter的扩展BaseAdapter的新类。
  2. FruitAdapter需要持有并表示一个FruitItem对象的数组。使用与BurgerAdapter相同的结构实现类。
  3. ListAdapter.getView方法中,按照fruit_item.xml布局资源中的定义设置标签和图标:

    java FruitItem item = items[index]; TextView text = ((TextView)view.findViewById(R.id.text)); ImageView image = ((ImageView)view.findViewById(R.id.icon)); text.setText(item.name); image.setImageResource(item.image);

  4. 创建一个新的布局资源来保存我们将用于水果四桶菜单的GridView,并将其命名为res/layout/four_buckets.xml

  5. 用三列GridView :

    java <GridView xmlns:android="http://schemas.android.com/apk/res/android" android:numColumns="3" android:horizontalSpacing="5dip" android:verticalSpacing="5dip" android:layout_width="fill_parent" android:layout_height="fill_parent"/>

    填充新布局资源

刚刚发生了什么?

新的four_buckets.xml布局资源只有一个GridView。这与我们到目前为止所写的其他布局资源不同,尤其是因为GridView没有 ID。例如,水果菜单Activity将只包含GridView,因此不需要 ID 引用或布局结构。我们还规定了5dip的水平和垂直间距。一个GridView对象的默认是其单元格之间没有间距,这使得内容相当拥挤。为了把事情隔开一点,我们要求每个单元格之间有一些空白。

行动时间——创建四个桶活动

因为我们使用的布局资源只有一个GridView,没有 ID 引用,所以我们将逐步完成Activity的创建。与之前的Activity实现不同,我们需要直接引用在four_buckets.xml中定义的GridView,这意味着手动加载它。

  1. 从在项目的根包中创建一个新类开始:

    java public class FourBucketsActivity extends Activity {

  2. 覆盖onCreate方法,调用超级实现:

    java protected void onCreate(final Bundle istate) { super.onCreate(istate);

  3. 获取您的Activity对象的LayoutInflator实例:

    java LayoutInflater inflater = getLayoutInflater();

  4. 膨胀four_buckets.xml资源并将其内容直接投射到GridView对象:

    java GridView view = (GridView)inflater.inflate( R.layout.four_buckets, null);

  5. view对象的ListAdapter设置为FruitAdapter类的新实例,并用一些FruitItem对象填充新的【T3:

    java view.setAdapter(new FruitAdapter( new FruitItem("Apple", R.drawable.apple), new FruitItem("Banana", R.drawable.banana), new FruitItem("Black Berries", R.drawable.blackberry), // and so on

  6. 使用setContentView使GridView成为你的根View对象:

    java setContentView(view);

  7. 在您的AndroidManifest.xml中注册您的FourBucketsActivity课程。

  8. SelectRestaurantActivity上增加一个案例,当用户选择时,开始新的FourBucketsActivity

刚刚发生了什么?

您刚刚完成了四桶水果菜单。如果你将应用重新安装到模拟器中,你现在就可以去订购水果了(只是要小心准备好 16 吨重的水果,以防送货员攻击你)。

如果你浏览一下Activity文档,你会注意到虽然有setContentView方法,但是没有相应的getContentView方法。仔细看看,你会注意到addContentView法。一个Activity对象可以附加任意数量的View对象作为“内容”。这排除了任何有用的getContentView方法的实现。

I n 为了绕开这个限制,我们自己对布局进行了膨胀。使用的getLayoutInflator()方法只是LayoutInflator.from(this)的一个捷径。我们不使用 ID 和findViewById,而是直接将View转换成GridView,因为这就是我们的four_buckets.xml文件包含的全部内容(与ArrayAdapter类处理TextView对象的方式非常相似)。如果我们想让事情更抽象一点,我们可以将其转换为AdapterView<ListAdapter>,在这种情况下,我们可以用ListView替换文件中的实现。然而,这对于这个例子来说不是很有用。

如果您现在重新安装并运行该应用,您的新FourBucketsActivity将显示一个类似于下面的屏幕:

What just happened?

加油英雄——山姆寿司

菜单上最后一家餐厅是Sam's Sushi。尝试使用Spinner类和GridView来创建一个复合寿司菜单。将旋转器放在屏幕顶部,可以选择不同类型的寿司:

  • 生鱼片
  • 牧卷
  • Nigiri
  • Oshi
  • 加州卷
  • 时尚三明治
  • 手卷

Spinner下方,使用GridView显示用户可以订购的每种不同类型鱼的图标。以下是一些建议:

  • 金枪鱼
  • 黄色尾巴
  • 鲷鱼
  • 海胆
  • 鱿鱼

这个类使用了SpinnerAdapter而不是ListAdapterSpinnerAdapter包括一个代表下拉菜单的附加View对象。这通常是对android.R.layout.simple_dropdown_item_1line资源的引用。但是,对于这个例子,您可能可以利用Spinner XML 元素上的android:entries属性。

总结

数据显示是移动应用最常见的要求之一,安卓有许多不同的选项。ListView可能是标准安卓套件中最常用的小部件之一,它的样式允许它用于显示不同数量的数据,从单行菜单项到多行待办事项。

GridView实际上是ListView的表格版本,非常适合向用户呈现图标视图。图标比文本有着巨大的优势,因为用户可以更快地识别它们。图标占用的空间也可以大大减少,在 T2,你可以很容易地在一个纵向屏幕上显示四到六个图标,而不会使用户界面变得混乱或更难操作。这也为其他项目的显示释放了宝贵的屏幕空间。

构建定制的Adapter类不仅可以让你完全控制ListView的样式,还可以决定数据来自哪里,以及如何加载。例如,您可以通过使用Adapter直接从 web 服务中加载数据,它会生成虚拟的View对象,直到 web 服务用实际数据进行响应。好好看看默认的Adapter实现,它们通常会满足您的需求,尤其是当与自定义布局资源相结合时。

在下一章中,我们将看看安卓提供的一些不太通用、更专业的View类。就像安卓系统中的几乎所有东西一样,默认值可能是特定的,但它们可以以任何方式定制,以适应一些非常不寻常的目的。