Android5 高级教程(二)
四、适配器和列表控件
在第三章中,我们介绍了一系列基本的用户界面控件,你可以用它们来构建 Android 应用。如果您还记得 TextView 上的例子,我们探索的控件类型之一是 AutoCompleteTextView,当它与数据源(适配器)结合时,能够提示用户一系列预定的值。在这一章中,我们将进一步探索适配器,以及更广泛的列表控件的主题,这些控件支持构建更精细和复杂的屏幕设计。
了解适配器
在我们进入 Android 的列表控件的细节之前,我们需要谈论一下适配器。列表控件用于显示数据集合。但是,Android 没有使用单一类型的控件来管理显示和数据,而是将这两项职责分为列表控件和适配器。列表控件是扩展 Android . widget . adapter view 的类,包括 ListView 、 GridView 、 Spinner 和 Gallery (见图 4-1 )。
图 4-1 。 AdapterView 类层次
AdapterView 本身扩展了 android.widget.ViewGroup ,也就是说 ListView 、 GridView 等等都是容器控件。换句话说,列表控件包含子视图的集合。适配器的目的是管理适配器视图的数据,并为其提供子视图。让我们通过检查 SimpleCursorAdapter 来看看这是如何工作的。
了解 SimpleCursorAdapter
简单光标适配器如图图 4-2 所示。
图 4-2 。简单光标适配器
这是一张非常重要的理解图。左边是适配器视图;在这个例子中,它是一个由文本视图子视图组成的列表视图。右边是数据;在本例中,它被表示为来自针对内容提供者的查询的数据行的结果集。
为了将数据行映射到 ListView,SimpleCursorAdapter 需要一个子布局资源 ID。子布局必须描述应该显示在左侧的右侧的每个数据元素的布局。这种情况下的布局就像我们为活动所做的布局一样,但是它只需要指定我们的列表视图的单行布局。例如,如果您有一个来自 Contacts 内容提供者的信息结果集,并且您只想在您的 ListView 中显示每个联系人的名字,那么您需要提供一个布局来描述 name 字段应该是什么样子。如果您想在 ListView 的每一行显示结果集中的名称和图像,您的布局必须说明如何显示名称和图像。
这并不意味着您必须为结果集中的每个字段提供一个布局规范,也不意味着您必须在结果集中为您想要包含在 ListView 的每一行中的所有内容提供一段数据。例如,我们将向您展示如何在您的 ListView 中设置复选框来选择行,并且这些复选框不需要从结果集中的数据中设置。我们还将向您展示如何获取结果集中不属于列表视图的数据。尽管我们刚刚讨论了列表视图、文本视图和结果集,但是请记住,适配器的概念比这更普遍。左侧可以是一个画廊,右侧可以是一个简单的图像数组。但是现在让我们保持事情相当简单,更详细地看一下 SimpleCursorAdapter 。
SimpleCursorAdapter 最简单的构造函数如下:
SimpleCursorAdapter(Context context, int childLayout, Cursor c, String[] from, int[] to)
此适配器将光标中的行转换为容器控件的子视图。子视图的定义是在 XML 资源中定义的( childLayout 参数)。注意,因为光标中的一行可能有许多列,所以通过指定一组列名(使用来自参数的,告诉 SimpleCursorAdapter 您想要从该行中选择哪些列。
类似地,因为您选择的每个列必须映射到布局中的一个视图,所以您必须在到参数中指定 id。您选择的列和显示列中数据的视图之间存在一对一的映射,因此从和到的参数数组必须具有相同数量的元素。正如我们之前提到的,子视图可以包含其他类型的视图;它们不一定是文本视图,例如,你可以使用图像视图。
在 ListView 和我们的适配器之间有一个谨慎的合作。当 ListView 想要显示一行数据时,它调用适配器的 getView() 方法,传入位置来指定要显示的数据行。适配器通过使用在适配器的构造函数中设置的布局构建适当的子视图,并从结果集中的适当记录中提取数据来做出响应。因此, ListView 不需要处理数据如何存在于适配器端;它只需要根据需要调用子视图。这是一个关键点,因为这意味着我们的 ListView 不一定需要为每个数据行创建每个子视图。它实际上只需要有显示窗口中可见的所需数量的子视图。如果只显示 10 行,从技术上讲,ListView 只需要实例化 10 个子布局,即使我们的结果集中有数百条记录。实际上,有超过 10 个子布局被实例化,因为 Android 通常会保留额外的子布局,以便更快地将新行显示出来。您应该得出的结论是,由 ListView 管理的子视图可以被回收。我们稍后会详细讨论这一点。
图 4-2 揭示了使用适配器的一些灵活性。因为列表控件使用适配器,所以可以根据数据和子视图替换各种类型的适配器。例如,如果您不打算从内容供应器或数据库填充一个 AdapterView ,您就不必使用 SimpleCursorAdapter 。您可以选择更“简单”的适配器——array adapter。
了解 ArrayAdapter
ArrayAdapter 是 Android 中最简单的适配器。它专门针对列表控件,并假设 TextView 控件代表列表项(子视图)。创建一个新的数组适配器看起来就像这样简单:
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1,
new String[]{"Dave","Satya","Dylan"});
我们仍然传递上下文( this )和一个 childLayout 资源 ID。但是我们不是从数据字段规范的数组中传递一个,而是传递一个字符串数组作为实际数据。我们不会将光标或传递给视图资源 id 的数组。这里的假设是,我们的子布局由一个单独的 TextView 组成,这就是 ArrayAdapter 将用作我们的数据数组中的字符串的目的地。
现在我们将为子布局资源 ID 引入一个很好的快捷方式。我们可以利用 Android 中预定义的布局,而不是为列表项创建自己的布局文件。注意,子布局资源 ID 的资源前缀是 android。Android 在自己的目录中查找,而不是在我们本地的 /res 目录中查找。您可以通过导航到 Android SDK 文件夹并在平台/<Android-版本>/数据/资源/布局下查找来浏览该文件夹。在那里你会找到 simple_list_item_1.xml ,并且可以看到它定义了一个简单的 TextView 。那个 TextView 是我们的 ArrayAdapter 将用来创建一个视图(在它的 getView() 方法中)以提供给 ListView 。请随意浏览这些文件夹,找到各种用途的预定义布局。我们以后会用到更多这样的东西。
ArrayAdapter 有其他的构造函数。如果 childLayout 不是简单的 TextView ,可以传入行布局资源 ID 加上 TextView 的资源 ID 来接收数据。当没有现成的字符串数组可以传入时,可以使用 createFromResource() 方法。清单 4-1 、 4-2 和 4-3 展示了一个例子,其中我们为一个旋转器创建了一个 ArrayAdapter 。
清单 4-1 。 清单片段,用于从字符串资源文件创建一个 ArrayAdapter
<Spinner android:id="@+id/spinner"
android:layout_width="wrap_content" android:layout_height="wrap_content" />
清单 4-2 。 用于从字符串资源文件创建 ArrayAdapter 的代码片段
Spinner spinner = (Spinner) findViewById(R.id.spinner);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
R.array.planets, android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
清单 4-3 。 实际字符串-资源文件
<?xml version="1.0" encoding="utf-8"?>
<!-- This file is /res/values/planets.xml -->
<resources>
<string-array name="planets">
<item>Mercury</item>
<item>Venus</item>
<item>Earth</item>
<item>Mars</item>
<item>Jupiter</item>
<item>Saturn</item>
<item>Uranus</item>
<item>Neptune</item>
</string-array>
</resources>
第一个清单是 spinner 的 XML 布局。中的第二个 Java 清单展示了如何创建一个 ArrayAdapter ,它的数据源是在一个字符串资源文件中定义的。使用这种方法,您不仅可以将列表内容具体化为 XML 文件,还可以使用本地化版本。稍后我们将讨论微调器,但是现在,我们知道微调器有一个视图来显示当前选择的值,还有一个列表视图来显示可以从中选择的值。基本上就是下拉菜单。清单 4-3 是名为 /res/values/planets.xml 的 XML 资源文件,读入该文件是为了初始化 ArrayAdapter 。
值得一提的是, ArrayAdapter 允许对底层数据进行动态修改。例如, add() 方法会在数组末尾追加一个新值。方法将在数组中的指定位置添加一个新值。并且 remove() 从数组中取出一个对象。也可以调用 sort() 对数组重新排序。当然,一旦你这样做了,数据数组就与 ListView 不同步了,所以这时你调用适配器的 notifyDataSetChanged() 方法。该方法将使列表视图与适配器重新同步。
以下列表总结了 Android 提供的适配器:
- ArrayAdapter < T > :这是一个位于任意对象的通用数组之上的适配器。这意味着与一个列表视图一起使用。
- CursorAdapter :这个适配器也打算在 ListView 中使用,通过光标向列表提供数据。
- SimpleAdapter :顾名思义,这个适配器是一个简单的适配器。它通常用于用静态数据填充列表(可能来自参考资料)。
- ResourceCursorAdapter :这个适配器扩展了 CursorAdapter ,并且知道如何从资源中创建视图。
- SimpleCursorAdapter :这个适配器扩展了 ResourceCursorAdapter 并从光标中的列创建了 TextView/ImageView 视图。视图在参考资料中定义。
我们已经介绍了足够多的适配器,现在开始向您展示一些使用适配器和列表控件的真实例子(也称为 AdapterView s)。我们开始吧。
将适配器与 AdapterViews 一起使用
既然已经向您介绍了适配器,是时候让它们为我们工作了,为列表控件提供数据。在这一节中,我们将首先介绍基本的列表控件,即列表视图。然后,我们将描述如何创建您自己的定制适配器,最后,我们将描述其他类型的列表控件: GridView s,spinners 和 gallery。
基本列表控件:ListView
ListView 控件垂直显示项目列表。也就是说,如果我们有一个要查看的项目列表,并且项目的数量超出了我们当前在显示中可以看到的范围,我们可以滚动来查看其余的项目。你一般通过编写一个扩展 android.app.ListActivity 的新活动来使用 ListView ,ListActivity 包含一个 ListView ,你通过调用 setListAdapter() 方法来为 ListView 设置数据。
如前所述,适配器将列表控件链接到数据,并帮助准备列表控件的子视图。在列表视图中的项目可以被点击以立即采取行动,或者被选择以稍后对所选择的项目集采取行动。我们将从非常简单的开始,然后逐步增加功能。
在列表视图中显示值
图 4-3 显示了一个最简单形式的列表视图控件。
图 4-3 。使用列表视图控件
在这个练习中,我们将把一个 ListView 放入一个默认的 Android 布局中,没有任何特殊的调整或更改,因此您可以看到它们如何适合一个典型的主布局 XML 文件。清单 4-4 显示了我们活动的 Java 代码。
清单 4-4 。 添加项目到列表视图
public class MainActivity extends Activity {
private ListView listView1;
private ArrayAdapter<String> listAdapter1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView1 = (ListView) findViewById(R.id.listView1);
String[] someColors = new String[] { "Red", "Orange", "Yellow",
"Green", "Blue", "Indigo", "Violet", "Black", "White"};
ArrayList<String> colorArrayList = new ArrayList<String>();
colorArrayList.addAll( Arrays.asList(someColors) );
listAdapter1 = new ArrayAdapter<String>(this, android.R.id.text1,
colorArrayList);
listView1.setAdapter( listAdapter1 );
}
...
}
清单 4-2 创建了一个 ListView 控件,其中填充了我们在数组中指定的颜色列表, someColors 。在我们的例子中,我们获取数组的内容并将字符串颜色名称映射到一个 TextView 控件( android)。R.id.text1 )。之后,我们创建一个数组适配器并设置列表的适配器。适配器类很聪明,可以从您提供的任何数据源中获取行来填充 UI。
我们可以利用非常基本的 ListActivity 来提供主布局,因为没有其他 UI 元素或复杂性需要考虑。然而,我们选择在一个典型的新项目中部署 ListView,并利用基本活动。我们还为我们的子视图使用了 Android 提供的布局(资源 ID 。R.layout.simple_list_item_1 ,其中包含一个 Android 提供的 TextView (资源 ID android。R.id.text1 )。总而言之,设置非常简单。
我们可以通过展示如何用我们自己的设计替换 Android 为子视图提供的布局来扩展这个例子和您的理解。在项目的 res/layout 文件夹中创建一个新的空文件,并将其命名为 simple_list_row.xml 。清单 4-5 显示了一个简单的文本视图的 XML 布局,用来表示我们的列表视图中要呈现的每一行(或者引用这个简单列表行布局的任何其他布局)。
清单 4-5 。 为列表渲染创建自定义的 TextView 子视图
<TextView xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:id="@+id/rowTextView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:textSize="24sp" >
</TextView>
在我们的代码中,我们只需要更改绑定 ListView 中使用的所选布局的引用,以使用新的 simple_list_row 布局,如下所示:
listAdapter1 = new ArrayAdapter<String>(this, R.layout.simple_list_row,
colorArrayList);
请注意,当我们以这种方式引用我们自己的自定义布局时,我们去掉了前面的“android”引用。我们现在可以运行这个例子来看看完整的效果,如图 4-4 所示。
图 4-4 。ListView 示例的运行
列表视图中可点击的项目
当然,当您运行这个示例时,您会看到您可以上下滚动列表来查看所有的颜色名称,但仅此而已。如果我们想在这个例子中做一些更有趣的事情,比如让应用在用户点击我们的列表视图中的一个项目时做出响应,该怎么办?清单 4-6 显示了我们的例子接受用户输入的修改。
清单 4-6 。?? 在列表视图上接受用户输入
public class MainActivity extends Activity {
private ListView listView1;
private ArrayAdapter<String> listAdapter1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView1 = (ListView) findViewById(R.id.listView1);
String[] someColors = new String[] { "Red", "Orange", "Yellow",
"Green", "Blue", "Indigo", "Violet", "Black", "White"};
ArrayList<String> colorArrayList = new ArrayList<String>();
colorArrayList.addAll( Arrays.asList(someColors) );
listAdapter1 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,
colorArrayList);
listView1.setAdapter( listAdapter1 );
listView1.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position
, long id) {
String itemValue = (String) listView1.getItemAtPosition(position);
Toast.makeText(getApplicationContext(), itemValue,
Toast.LENGTH_LONG).show();
}
});
}
...
}
我们的活动现在正在实现 OnItemClickListener 接口,这意味着当用户点击我们的 ListView 中的某些内容时,我们将收到一个回调。正如您可以通过我们的 onItemClick() 方法看到的,我们获得了许多关于被点击内容的信息,包括接收点击的视图、被点击项目在 ListView 中的位置,以及根据我们的适配器的项目 ID。在调用 makeText() 方法来处理颜色名称之前,我们进行了相应的转换。位置值表示该项在列表视图中相对于整个项目列表的位置,并且是从零开始的。因此,列表中的第一项位于位置 0。
ID 值完全取决于适配器和数据源。在我们的例子中,我们碰巧在一个数组中查询带有颜色名称的字符串,所以根据这个适配器的 ID 是来自内容提供者的条目在数组中的位置。但是在其他情况下,您的数据源可能不像这样简单,所以您不应该认为您可以像我们在这个示例中所做的那样,总是能够提前知道诸如订购之类的事情。如果我们使用一个从系统的联系人数据库中读取其值的 SimpleCursorAdapter ,那么提供给我们的 ID 将是记录的底层 _ID ,它可以是任何值,这取决于系统中联系人的年龄。
当我们之前讨论过 ArrayAdapter s 时,我们提到了 notifydatascethanged()方法让适配器在数据改变时更新 ListView 。有些适配器,如 SimpleCursorAdapter ,能够感知底层数据源(如 Contacts content provider)发生的更新,并将根据变化动态地为您更新 ListView 内容。然而,使用 ArrayAdapter s,您将需要自己调用 notifyDataSetChanged() 方法。
这很容易做到。我们生成了自己的颜色名称列表视图,通过点击一种颜色,我们向用户显示了一条消息。但是如果我们想先选择一堆名字,然后对这些人的子集做些什么呢?对于下一个示例应用,我们将修改列表项的布局以包含复选框,并且我们将向 UI 添加一个按钮,然后对选定项的子集进行操作。
用 ListView 添加其他控件
如果您想在主布局中添加更多的控件,您可以提供自己的布局 XML 文件,放入一个 ListView ,并添加其他想要的控件。例如,你可以在 UI 中的 ListView 下面添加一个按钮,提交对所选项目的操作,如图图 4-5 所示。
图 4-5 。一个额外的按钮,让用户提交选择的项目
这个例子的主布局在清单 4-7 中,它包含了活动的 UI 定义—列表视图和按钮。
清单 4-7 。 覆盖 ListView 被引用我们的 活动
<LinearLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
xmlns:tools="[`schemas.android.com/tools`](http://schemas.android.com/tools)"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.artifexdigital.android.listviewdemo3.MainActivity" >
<ListView
android:id="@+id/listView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1" />
<Button
android:id="@+id/button1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="doClick"
android:text="Submit selection" />
</LinearLayout>
注意我们必须在 LinearLayout 中指定 ListView 的高度和重量。我们希望我们的按钮一直出现在屏幕上,不管我们的列表视图中有多少项目,我们不希望一直滚动到页面底部才找到按钮。为了实现这一点,我们将 layout_height 设置为 wrap_content ,然后使用 layout_weight 来说明该控件应该占用父容器的所有可用空间。这个技巧为按钮留出了空间,并保留了我们滚动列表视图的能力。我们将在本章的后面更多地讨论布局和权重。
活动的实现看起来就像清单 4-8 中的。
清单 4-8 。 从 ListActivity 读取用户输入
public class MainActivity extends Activity {
private ListView listView1;
private ArrayAdapter<String> listAdapter1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView1 = (ListView) findViewById(R.id.listView1);
String[] someColors = new String[] { "Red", "Orange", "Yellow",
"Green", "Blue", "Indigo", "Violet", "Black", "White"};
ArrayList<String> colorArrayList = new ArrayList<String>();
colorArrayList.addAll( Arrays.asList(someColors) );
listAdapter1 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_checked,
colorArrayList);
listView1.setAdapter( listAdapter1 );
listView1.setChoiceMode(listView1.CHOICE_MODE_MULTIPLE);
listView1.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position
, long id) {
String itemValue = (String) listView1.getItemAtPosition(position);
Toast.makeText(getApplicationContext(), itemValue,
Toast.LENGTH_LONG).show();
}
});
}
public void doClick(View view) {
int count=listView1.getCount();
SparseBooleanArray viewItems = listView1.getCheckedItemPositions();
for(int i=0; i<count; i++) {
if(viewItems.get(i)) {
String selectedColor = (String) listView1.getItemAtPosition(i);
Log.v("ListViewDemo", selectedColor + " is checked at position " + i);
}
}
}
}
在适配器的设置中,我们为一个 ListView 行项目( android)传递另一个 Android 提供的视图。r . layout . simple _ list _ item _ checked,这导致每一行都有一个文本视图和一个复选框。如果你查看这个布局文件,你会看到 TextView 的另一个子类,这个叫做 CheckedTextView 。这种特殊类型的文本视图旨在与列表视图一起使用。看,我们告诉过你在 Android 布局文件夹中有一些有趣的东西!您将看到 CheckedTextView 的 ID 是 text1 ,这是我们需要在视图数组中传递给 SimpleCursorAdapter 的构造函数的内容。
因为我们希望用户能够选择我们的行,所以我们将选择模式设置为 CHOICE_MODE_MULTIPLE 。默认情况下,选择模式为选择模式无。另一个可能的值是选择 _ 模式 _ 单一。如果你想在这个例子中使用选择模式,你会想使用不同的布局,最有可能是 android。r . layout . simple _ list _ item _ single _ choice。
在这个例子中,我们实现了一个基本按钮,它调用活动的 doClick() 方法。为了简单起见,我们只想写出用户检查过的条目的名称。好消息是解决方案非常简单;坏消息是 Android 已经进化了,所以最好的解决方案取决于你的目标 Android 版本。我们在这里展示的 ListView 解决方案从 Android 1 就开始工作了(尽管我们在按钮回调上采用了 Android 1.6 的快捷方式)。也就是说,getCheckedItemPositions()方法是旧的,但是仍然有效。返回值是一个数组,它可以告诉你一个项是否被检查过。所以,我们遍历数组。如果我们的 ListView 中的相应行被选中,viewItems.get(i) 将返回 true。使用列表视图的 getItemAtPosition() 方法,可以直接从列表视图中访问我们的数据。在我们的例子中,从 getItemAtPosition() 返回的对象是一个字符串对象。正如我们之前所说的,在其他情况下,当与一些特定的内容提供者(如本书后面讨论的联系人提供者)一起工作时,我们可能会得到一些其他类型的对象,如光标和包装器。您必须了解您的数据源和适配器,才能知道会发生什么。
如果我们点击图 4-5 中的提交选择按钮,我们可以在 Eclipse 或 Android Studio 中看到 log cat 窗口,它从我们的选择中发出数据,如在 doClick() 方法中实现的。如图 4-6 中的所示。
图 4-6 。使用列表视图中的用户输入进行进一步处理
从列表视图中读取选择的另一种方法
Android 1.6 引入了另一种从列表视图 : getCheckItemIds() 中检索选中行列表的方法。然后在 Android 2.2 中,这个方法被弃用,取而代之的是 getCheckedItemIds() 。这是一个微妙的名称变化,但你使用方法的方式基本上是一样的。清单 4-9 显示了我们对 Java 代码所做的修改,以反映处理列表中选中条目的这种演变。对于 list.xml 的 XML 布局,我们可以继续使用清单 4-7 中的文件。
清单 4-9 。 从 ListActivity 中读取用户输入的另一种方式
public class MainActivity extends Activity {
private ListView listView1;
private ArrayAdapter<String> listAdapter1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView1 = (ListView) findViewById(R.id.listView1);
String[] someColors = new String[] { "Red", "Orange", "Yellow",
"Green", "Blue", "Indigo", "Violet", "Black", "White"};
ArrayList<String> colorArrayList = new ArrayList<String>();
colorArrayList.addAll( Arrays.asList(someColors) );
listAdapter1 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_checked,
colorArrayList);
listView1.setAdapter( listAdapter1 );
listView1.setChoiceMode(listView1.CHOICE_MODE_MULTIPLE);
listView1.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position
, long id) {
String itemValue = (String) listView1.getItemAtPosition(position);
Toast.makeText(getApplicationContext(), itemValue,
Toast.LENGTH_LONG).show();
}
});
}
...
public void doClick(View view) {
if(!listAdapter1.hasStableIds()) {
Log.v(TAG, "Data is not stable");
return;
}
long[] viewItems = listView1.getCheckedItemIds();
for(int i=0; i<viewItems.length; i++) {
String selectedColor = (String) listView1.getItemAtPosition(i);
Log.v("ListViewDemo", selectedColor + " is checked at position " + i);
}
}
}
}
在这个示例应用中,当我们单击按钮时,我们的回调调用方法 getCheckedItemIds() 。在我们的上一个例子中,我们在 ListView 中获得了被检查条目的位置数组,而这次我们从适配器中获得了在 ListView 中被检查的记录的 id 数组。我们现在可以绕过 ListView 和光标,因为 id 可以用来驱动我们想要的任何动作。
我们已经向您展示了如何在各种场景中使用 ListView s。我们已经展示了适配器做了很多工作来支持一个列表视图。接下来,我们将讨论其他类型的列表控件,从 GridView 开始。
GridView 控件
大多数小部件工具包都提供一个或多个基于网格的控件。Android 有一个 GridView 控件,可以以网格的形式显示数据。注意,虽然我们在这里使用术语数据,但是网格的内容可以是文本、图像等等。
GridView 控件在网格中显示信息。 GridView 的使用模式是在 XML 布局中定义网格(参见清单 4-10 和 4-11 ),然后使用 Android . widget . list adapter 将数据绑定到网格。
清单 4-10 。XML 布局中 GridView 的定义
<RelativeLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
xmlns:tools="[`schemas.android.com/tools`](http://schemas.android.com/tools)"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.artifexdigital.android.gridviewdemo.MainActivity" >
<GridView
android:id="@+id/gridView1"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:padding="10dp"
android:verticalSpacing="10dp"
android:horizontalSpacing="10dp"
android:numColumns="auto_fit"
android:columnWidth="100dp"
android:stretchMode="columnWidth"
android:gravity="center" />
</RelativeLayout>
清单 4-11 。 Java 实现为 GridView
public class MainActivity extends Activity {
private GridView gridView1;
private ArrayAdapter<String> listAdapter1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
gridView1 = (GridView) findViewById(R.id.gridView1);
String[] someColors = new String[] { "Red", "Orange", "Yellow",
"Green", "Blue", "Indigo", "Violet", "Black", "White"};
ArrayList<String> colorArrayList = new ArrayList<String>();
colorArrayList.addAll( Arrays.asList(someColors) );
listAdapter1 = new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1, colorArrayList);
gridView1.setAdapter( listAdapter1 );
}
}
清单 4-10 在 XML 布局中定义了一个简单的 GridView 。然后,网格被加载到活动的内容视图中。生成的界面如图图 4-7 所示。
图 4-7 。一个用颜色填充的 GridView
图 4-7 中的网格显示了我们数组中颜色的名称。我们已经决定显示一个带有颜色名称的文本视图,但是你可以很容易地生成一个充满图像或其他控件的网格。我们再次利用了 Android 中预定义的布局。事实上,这个例子看起来非常像的清单 4-7 ,除了一些重要的区别。我们必须调用 setContentView() 来为我们的 GridView 设置布局;没有默认视图可以依赖。为了设置适配器,我们在 GridView 对象上调用 setAdapter() ,而不是在活动上调用 setListAdapter() 。
毫无疑问,您已经注意到网格使用的适配器是一个 ListAdapter 。列表通常是一维的,而网格是二维的。我们可以得出结论,网格实际上显示的是面向列表的数据。结果是列表是按行显示的。也就是说,列表遍历第一行,然后遍历第二行,依此类推。
和以前一样,我们有一个列表控件,它和一个适配器一起处理数据管理和子视图的生成。我们之前使用的相同技术应该可以很好地处理 GridView s。一个例外与选择有关:没有办法在 GridView 中指定多个选择,正如我们在清单 4-7 中所做的那样。
微调控制按钮
微调器控件就像一个下拉菜单。它通常用于从相对较短的选项列表中进行选择。如果选择列表太长而无法显示,则会自动为您添加滚动条。您可以通过 XML 布局实例化一个微调器,就像下面这样简单:
<Spinner
android:id="@+id/spinner" android:prompt="@string/spinnerprompt"
android:layout_width="wrap_content" android:layout_height="wrap_content" />
尽管 spinner 在技术上是一个列表控件,但它看起来更像一个简单的 TextView 控件。换句话说,当微调器静止时,只显示一个值。微调器的目的是允许用户从一组预先确定的值中进行选择:当用户单击小箭头时,会显示一个列表,用户需要选择一个新值。填充该列表的方式与填充其他列表控件的方式相同:使用适配器。
因为微调按钮经常像下拉菜单一样使用,所以经常可以看到适配器从资源文件中获取列表选项。清单 4-12 中显示了一个使用资源文件设置旋转器的例子。请注意名为 android:prompt 的新属性,用于在列表顶部设置一个提示以供选择。微调器提示的实际文本在我们的 /res/values/strings.xml 文件中。正如您所料, Spinner 类也有一个在代码中设置提示的方法。
清单 4-12 。 代码从资源文件中创建一个微调器
public class SpinnerActivity extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.spinner);
Spinner spinner = (Spinner)findViewById(R.id.spinner);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
R.array.planets, android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
}
}
您可能还记得在清单 4-1 中看到的 planets.xml 文件。我们在这个例子中展示了如何创建一个微调控件;设置适配器,然后将其关联到微调器。参见图 4-8 中的了解这在实际操作中的效果。
图 4-8 。选择行星的旋转器
与我们以前的列表控件的一个不同之处是,当使用微调器时,我们有一个额外的布局要处理。图 4-8 的左侧显示了微调器的正常模式,其中显示了当前的选择。在这种情况下,当前选择是土星。单词旁边有一个向下的箭头,表示该控件是一个微调器,可用于弹出一个列表来选择不同的值。第一个布局作为参数提供给 array adapter . createfromresource()方法,定义了微调器在正常模式下的外观。在图 4-8 的右侧,我们以弹出列表的方式显示微调器,等待用户选择新的值。使用 setDropDownViewResource()方法设置该列表的布局。同样在这个例子中,我们使用 Android 提供的布局来满足这两个需求,所以如果你想检查这些布局的定义,你可以访问 Android res/layout 文件夹。当然,您可以为其中任何一个指定您自己的布局定义,以获得您想要的效果。
图库控制
Gallery 控件是一个可水平滚动的列表控件,总是聚焦在列表的中心。该控件通常在触摸模式下用作照片库。您可以通过 XML 布局或代码实例化一个图库:
<Gallery
android:id="@+id/gallery"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
/>
Gallery 控件通常用于显示图像,所以您的适配器很可能是图像专用的。我们将在下一节自定义适配器中向您展示自定义图像适配器。视觉上,一个画廊看起来像图 4-9 。
图 4-9 。展示海牛图像的画廊
摘要
在本章中,我们通过以下方式扩展了您对 UI 组件的理解和熟练程度:
- Android 中可用的主要列表控件
- 如何使用适配器填充列表控件中的数据
五、构建更高级的用户界面布局
在前几章中,我们回顾了 Android 提供的许多标准布局,涵盖了各种可能的 UI 方法。当 Android 提供的股票布局不完全符合你的要求时,你会转向哪里?在这一章中,我们将快速探索 Android 如何为您提供构建自己的定制布局和管理相关适配器以填充有用数据的能力。
创建自定义适配器
Android 中的标准适配器很容易使用,但有一些限制。为了解决这个问题,Android 提供了一个名为 BaseAdapter 的抽象类,如果你需要一个定制的适配器,你可以扩展它。如果您有特殊的数据管理需求,或者如果您希望对如何显示子视图有更多的控制,您可以使用自定义适配器。您还可以使用自定义适配器,通过使用缓存技术来提高性能。接下来,我们将向您展示如何构建自定义适配器。
清单 5-1 显示了定制适配器的 XML 布局和 Java 代码。对于下一个例子,我们的适配器将处理海牛的图像,所以我们称它为 ManateeAdapter 。我们也将在活动中创建它。
清单 5-1 。 本店 自定义适配器: ManateeAdapter
<?xml version="1.0" encoding="utf-8"?>
<!-- This file is at /res/layout/gridviewcustom.xml -->
<GridView xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:id="@+id/gridview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:padding="10dip"
android:verticalSpacing="10dip"
android:horizontalSpacing="10dip"
android:numColumns="auto_fit"
android:gravity="center"
/>
Java 实现
public class GridViewCustomAdapter extends Activity
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.gridviewcustom);
GridView gv = (GridView)findViewById(R.id.gridview);
ManateeAdapter adapter = new ManateeAdapter(this);
gv.setAdapter(adapter);
}
public static class ManateeAdapter extends BaseAdapter {
private static final String TAG = "ManateeAdapter";
private static int convertViewCounter = 0;
private Context mContext;
private LayoutInflater mInflater;
static class ViewHolder {
ImageView image;
}
private int[] manatees = {
R.drawable.manatee00, R.drawable.manatee01, R.drawable.manatee02,
// ... many more manatees here - see the sample code folder
R.drawable.manatee32, R.drawable.manatee33 };
private Bitmap[] manateeImages = new Bitmap[manatees.length];
private Bitmap[] manateeThumbs = new Bitmap[manatees.length];
public ManateeAdapter(Context context) {
Log.v(TAG, "Constructing ManateeAdapter");
this.mContext = context;
mInflater = LayoutInflater.from(context);
for(int i=0; i<manatees.length; i++) {
manateeImages[i] = BitmapFactory.decodeResource(
context.getResources(), manatees[i]);
manateeThumbs[i] = Bitmap.createScaledBitmap(manateeImages[i],
100, 100, false);
}
}
@Override
public int getCount() {
Log.v(TAG, "in getCount()");
return manatees.length;
}
public int getViewTypeCount() {
Log.v(TAG, "in getViewTypeCount()");
return 1;
}
public int getItemViewType(int position) {
Log.v(TAG, "in getItemViewType() for position " + position);
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
Log.v(TAG, "in getView for position " + position +
", convertView is " +
((convertView == null)?"null":"being recycled"));
if (convertView == null) {
convertView = mInflater.inflate(R.layout.gridimage, null);
convertViewCounter++;
Log.v(TAG, convertViewCounter + " convertViews have been created");
holder = new ViewHolder();
holder.image = (ImageView) convertView.findViewById(R.id.gridImageView);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.image.setImageBitmap( manateeThumbs[position] );
return convertView;
}
@Override
public Object getItem(int position) {
Log.v(TAG, "in getItem() for position " + position);
return manateeImages[position];
}
@Override
public long getItemId(int position) {
Log.v(TAG, "in getItemId() for position " + position);
return position;
}
}
}
当您运行这个应用时,您应该会看到类似于图 5-1 的显示。
图 5-1 。一个带有海牛图像的网格视图
在这个例子中有很多东西需要解释,尽管它看起来相对简单。我们将从我们的 Activity 类开始,它看起来很像我们在本章的这一节一直在使用的那些。有一个来自 gridviewcustom.xml 的主布局,它只包含一个 GridView 定义。我们需要从布局内部获取对 GridView 的引用,因此我们定义并设置 gv 。我们实例化我们的 ManateeAdapter ,将我们的上下文传递给它,并在我们的 GridView 上设置适配器。到目前为止,这是相当标准的东西,尽管您无疑已经注意到,我们的定制适配器在创建时使用的参数并不像预定义适配器那么多。这主要是因为我们完全控制了这个特定的适配器,并且我们只在这个应用中使用它。如果我们让这个适配器更通用,我们很可能会设置更多的参数。但是让我们继续。
我们在适配器中的工作是管理数据向 Android 视图对象的传递。列表控件将使用视图对象(在本例中是一个 GridView )。数据来自某个数据源。在前面的例子中,数据是通过传递给适配器的游标对象获得的。在我们的定制案例中,我们的适配器知道所有的数据以及数据来自哪里。列表控件会询问一些事情,这样它就知道如何构建 UI。当它有一个不再需要的视图时,它也会好心地传递视图以供回收。认为我们的适配器必须知道如何构造视图似乎有点奇怪,但最终,这一切都是有意义的。
当我们实例化我们的定制适配器 ManateeAdapter 时,习惯上是传入上下文并让适配器持有它。在需要的时候让它可用通常是非常有用的。我们想在适配器中做的第二件事是挂在充气机上。当我们需要创建一个新视图来返回列表控件时,这将有助于提高性能。适配器中第三件典型的事情是创建一个视图持有者对象,包含我们管理的数据的视图对象。采用这种方法也起到了性能优化的作用,使我们不必重复查找视图。对于这个例子,我们只是存储了一个 ImageView ,但是如果我们有额外的字段要处理,我们会将它们添加到 ViewHolder 的定义中。例如,如果我们有一个 ListView ,其中每行包含一个 ImageView 和两个 TextView ,我们的 ViewHolder 将有一个 ImageView 和两个 TextView
因为我们在这个适配器中处理的是海牛的图像,所以我们设置了一个它们的资源 id 数组,以便在创建位图的过程中使用。我们还定义了一个位图数组作为我们的数据列表。
正如您可以从我们的 ManateeAdapter 构造函数中看到的,我们保存上下文,创建并挂起一个 inflater,然后我们遍历图像资源 id 并构建一个位图数组。这个位图数组将是我们的数据。
正如您之前了解到的,设置适配器将导致我们的 GridView 调用适配器上的方法来设置自身以显示数据。例如,我们的 GridView gv 将调用适配器的 getCount() 方法来确定有多少对象要显示。它还将调用 getViewTypeCount() 方法来确定在 GridView 中可以显示多少种不同类型的视图。出于本例的目的,我们将其设置为 1。然而,如果我们有一个 ListView 并且想要在常规数据行之间放置分隔符,我们将有两种类型的视图,并且需要从 getViewTypeCount() 返回 2。您可以拥有任意多的不同视图类型,只要您适当地从该方法返回正确的计数。与此方法相关的是 getItemViewType() 。我们刚刚说过,我们可以从适配器返回多种类型的视图,但是为了简单起见, getItemViewType() 只需要返回一个整数值来指示哪种视图类型位于数据中的特定位置。因此,如果我们有两种类型的视图要返回, getItemViewType() 将需要返回 0 或 1 来指示哪种类型。如果我们有三种类型的视图,这个方法需要返回 0、1 或 2。
如果我们的适配器正在处理 ListView 中的分隔符,它必须将分隔符视为数据。这意味着数据中有一个位置被分隔符占用。当列表控件调用 getView() 来检索该位置的适当视图时, getView() 将需要返回一个分隔符作为视图,而不是常规数据作为视图。当在 getItemViewType() 中询问该位置的视图类型时,我们需要返回我们认为匹配该视图类型的适当整数值。如果使用分隔符,您应该做的另一件事是实现 isEnabled() 方法。这对于列表项应该返回 true,对于分隔符应该返回 false,因为分隔符不应该是可选择或可点击的。
ManateeAdapter 中最有趣的方法是 getView() 方法调用。一旦 GridView 确定了有多少项可用,它就开始请求数据。现在,我们可以谈谈循环利用的观点。列表控件只能在显示屏上显示尽可能多的子视图。这意味着没有必要为适配器中的每条数据调用 get view();调用 getView() 来显示尽可能多的项目是有意义的。当 gv 从适配器获取子视图时,它将决定有多少子视图适合显示。当显示全是子视图时, gv 可以停止调用 getView() 。
如果您在启动这个示例应用后查看 LogCat,您将会看到各种调用,但是您还会看到在请求所有图像之前, getView() 停止被调用。如果你开始上下滚动 GridView ,你会在 LogCat 中看到更多对 getView() 的调用,你会注意到,一旦我们创建了一定数量的子视图, getView() 被调用,而 convertView 被设置为某个值,而不是 null。这意味着我们现在正在回收子视图,这对性能非常有利。
如果我们从 getView() 中的 gv 得到一个非空的 convertView 值,这意味着 gv 正在回收那个视图。通过重用传入的视图,我们避免了必须膨胀 XML 布局,并且我们避免了必须找到 ImageView 。通过将一个 ViewHolder 对象链接到我们返回的视图,我们可以在下次视图返回时更快地回收视图。我们在 getView() 中所要做的就是重新获取视图持有者并将正确的数据分配到视图中。
在这个例子中,我们想展示放入视图中的数据不一定就是数据中存在的内容。 createScaledBitmap() 方法创建一个较小版本的数据用于显示。重点是我们的列表控件没有调用 getItem() 方法。如果用户操作列表控件,这个方法将被我们的其他代码调用,这些代码想要对数据做一些事情。同样,对于任何适配器,理解它在做什么是非常重要的。您不一定想要依赖来自列表控件的视图中的数据,正如适配器中的 getView() 所创建的那样。有时,您需要调用适配器的 getItem() 方法来获取要操作的实际数据。有时,就像我们在前面的 ListView 例子中所做的那样,你会想要找到数据的光标。这完全取决于适配器和数据最终来自哪里。尽管我们在示例中使用了 createScaledBitmap() 方法,但 Android 2.2 引入了另一个可能会有所帮助的类: ThumbnailUtils 。这个类有一些从位图和视频生成缩略图的静态方法。
这个例子最后要指出的是对 getItemId() 方法的调用。在我们之前的例子中,列表视图和联系人的条目 ID 是来自内容供应器的 _ID 值。对于这个例子,除了 position 之外,我们不需要为商品 ID 使用任何东西。项目 id 的目的是提供一种机制来独立于其位置引用数据。当数据离开这个适配器时尤其如此,我们的联系人就是这种情况。当我们对数据有了这种直接控制,就像我们对海牛图像的控制一样,并且我们知道如何在应用中获得实际数据时,简单地使用位置作为项目 ID 是一种常见的捷径。在我们的例子中尤其如此,因为我们甚至不允许添加或删除数据。
Android 中的其他控件
Android 中有很多很多控件可以使用。到目前为止,我们已经讨论了相当多,更多将在后面的章节中讨论(例如第十九章中的 MapView 和第二十章中的 VideoView 和 MediaController )。你会发现其他控件,因为它们都是从视图派生出来的,与我们在这里讨论的有很多共同点。现在,我们将只提到几个您可能想自己进一步探索的控件。
ScrollView 是一个设置带有垂直滚动条的视图容器的控件。当你在一个屏幕上显示太多内容时,这很有用。
进度条 和分级条 控件类似于滑块。第一个图标直观地显示了某项操作的进度(可能是文件下载或音乐播放),第二个图标显示了星级评定。
计时器 控制是一个计时的计时器。如果你想帮助你显示一个倒计时器,有一个 CountDownTimer 类,但它不是一个 View 类。
在 Android 4.0 中引入了 Switch 控件,其功能类似于 ToggleButton ,但在视觉上有一个侧到侧的呈现,以及 Space 视图,这是一个轻量级视图,可以在布局中使用,以便更容易地在其他视图之间创建空间。
WebView 是一个非常特殊的显示 HTML 的视图。它能做的远不止这些,包括处理 cookies 和 JavaScript,以及链接到应用中的 Java 代码。但是在您开始在应用中实现 web 浏览器之前,您应该仔细考虑调用设备上的 web 浏览器来完成所有这些繁重的工作。
这就完成了本章中对控件的介绍。我们现在将继续讨论修改控件外观和感觉的样式和主题,然后讨论在屏幕上排列控件的布局。
样式和主题
Android 提供了几种方法来改变应用中的视图风格。我们将首先介绍在字符串中使用标记标签,然后介绍如何使用 Spannables 来改变文本的特定视觉属性。但是,如果您想对几个视图或整个活动或应用使用一个通用的规范来控制事物的外观,该怎么办呢?我们将讨论 Android 风格和主题来告诉你怎么做。
使用样式
有时,您想要突出显示视图内容的一部分或设置其样式。您可以静态或动态地做到这一点。静态地,您可以将标记直接应用于字符串资源中的字符串,如下所示:
<string name="styledText"><i>Static</i> style in a <b>TextView</b>.</string>
然后,您可以在 XML 或代码中引用它。注意,可以对字符串资源使用以下 HTML 标记: < i > 、 < b > 、 < u > 分别用于斜体、粗体和下划线,以及 < sup > (上标)、 < sub > (下标)、 < strike > 【删除线】、你甚至可以嵌套它们来得到,例如,小的上标。这不仅适用于文本视图,也适用于其他视图,比如按钮。图 5-2 展示了样式化和主题化的文本,使用了本节中的许多例子。
图 5-2 。风格和主题示例
以编程方式设计一个 TextView 控件的内容需要一点额外的工作,但是允许更多的灵活性(见清单 5-2 ),因为你可以在运行时设计它。不过,这种灵活性只能应用于 Spannable ,这就是 EditText 通常管理内部文本的方式,而 TextView 通常不使用 Spannable 。span able 基本上是一个字符串,您可以对其应用样式。要让 TextView 将文本存储为 span able,可以这样调用 setText() :
tv.setText("This text is stored in a Spannable", TextView.BufferType.SPANNABLE);
然后,当你调用 tv.getText() 时,你会得到一个 Spannable 。
如清单 5-2 中的所示,您可以获取 EditText 的内容(作为一个 Spannable 对象),然后为部分文本设置样式。清单中的代码将文本样式设置为粗体和斜体,并将背景设置为红色。您可以使用所有的样式选项,就像我们之前描述的 HTML 标签一样,还可以使用其他选项。
***清单 5-2 。***编辑文本 的内容动态应用样式
EditText et =(EditText)this.findViewById(R.id.et);
et.setText("Styling the content of an EditText dynamically");
Spannable spn = (Spannable) et.getText();
spn.setSpan(new BackgroundColorSpan(Color.RED), 0, 7,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spn.setSpan(new StyleSpan(android.graphics.Typeface.BOLD_ITALIC),
0, 7, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
这两种样式化技术只对它们所应用的一个视图有效。Android 提供了一种样式机制来定义跨视图重用的通用样式,还提供了一种主题机制,它基本上将一种样式应用于整个活动或整个应用。首先,我们需要谈谈风格。
一个样式 是一个视图属性的集合,它有一个名称,所以你可以通过它的名称来引用这个集合,并通过名称将那个样式分配给视图。例如,清单 5-3 显示了一个资源 XML 文件,保存在 /res/values 中,我们可以将它用于所有的错误消息。
清单 5-3 。 定义在多个视图中使用的样式
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="ErrorText">
<item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textColor">#FF0000</item>
<item name="android:typeface">monospace</item>
</style>
</resources>
定义了视图的大小以及字体颜色(红色)和字样。注意项目标记的 name 属性是我们在布局 XML 文件中使用的 XML 属性名,并且项目标记的值不再需要双引号。我们现在可以对一个错误使用这种风格文本视图,如清单 5-4 所示。
清单 5-4 。 在视图中使用样式
<TextView android:id="@+id/errorText"
style="@style/ErrorText"
android:text="No errors at this time"
/>
需要注意的是,在这个视图定义中,样式的属性名称不是以 android: 开头的。注意这一点,因为除了风格之外,所有东西似乎都使用 android: 。当您的应用中有许多共享一种样式的视图时,在一个地方改变该样式就简单多了;您只需要在一个资源文件中修改样式的属性。当然,您可以为各种控件创建许多不同的样式。按钮可以共享一个共同的样式,例如,不同于菜单中文本的共同样式。
样式的一个非常好的方面是你可以设置它们的层次结构。我们可以为非常糟糕的错误消息定义一种新的样式,并以 ErrorText 的样式为基础。清单 5-5 展示了这可能是什么样子。
清单 5-5 。 从 父样式 中定义样式
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="ErrorText.Danger" >
<item name="android:textStyle">bold</item>
</style>
</resources>
这个例子表明,我们可以简单地使用父样式作为新样式名的前缀来命名我们的子样式。因此, ErrorText。危险是错误文本的子元素,继承了父元素的样式属性。然后,它为文本样式添加了一个新属性。这可以一次又一次地重复,以创建一个完整的风格树。
和适配器布局一样,Android 提供了大量我们可以使用的样式。要指定 Android 提供的样式,请使用如下语法:
style="@android:style/TextAppearance"
这个样式设置了 Android 中文本的默认样式。要找到主 Android styles.xml 文件,请访问 Android SDK/platforms//data/RES/values/文件夹。在这个文件中,您会发现许多现成的样式供您使用或扩展。这里有一个关于扩展 Android 提供的样式的警告:以前使用前缀的方法不适用于 Android 提供的样式。相反,您必须使用样式标签的父属性,如下所示:
<style name="CustomTextAppearance" parent="@android:style/TextAppearance">
<item ... your extensions go here ... />
</style>
你不必总是在你的视图中引入一个完整的样式。你可以选择借用这种风格的一部分。例如,如果您想将文本视图中的文本颜色设置为系统样式颜色,您可以执行以下操作:
<EditText android:id="@+id/et2"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:textColor="?android:textColorSecondary"
android:text="@string/hello_world" />
注意,在这个例子中, textColor 属性值的名称以开始。字符代替了 @ 字符。?使用了字符,所以 Android 知道在当前主题中寻找一个样式值。因为我们看到?android ,我们在 android 系统主题中寻找这个样式值。
使用主题
样式的一个问题是,您需要添加一个属性规范 style="@style/... "应用到您希望它应用到的每个视图定义。如果你想在整个活动或者整个应用中应用一些样式元素,你应该使用主题来代替。一个主题实际上只是一种被广泛应用的风格;但就定义主题而言,它就像一种风格。事实上,主题和样式是可以互换的:你可以将主题扩展成样式,也可以将样式称为主题。通常,只有名称给出了一个提示,说明一个样式是用作样式还是主题。
要为活动或应用指定主题,请为项目的 AndroidManifest.xml 文件中的 <活动> 或 <应用> 标签添加一个属性。代码可能如下所示:
<activity android:theme="@style/MyActivityTheme">
<application android:theme="@style/MyApplicationTheme">
<application android:theme="@android:style/Theme.NoTitleBar">
您可以在 Android 提供的样式所在的文件夹中找到 Android 提供的主题,主题位于一个名为 themes.xml 的文件中。当您查看主题文件时,您会看到一大组定义的样式,它们的名称都以主题开头。你还会注意到,在 Android 提供的主题和风格中,有很多扩展,这就是为什么你最终使用了名为主题的风格。比如 Dialog.AppError 。
我们对 Android 控件集的讨论到此结束。正如我们在本章开始时提到的,在 Android 中构建 ui 需要你掌握两件事:控件集和布局管理器。在下一部分,我们将讨论 Android 布局管理器。
了解布局管理器
Android 提供了一组视图类,作为视图的容器。这些容器类被称为布局(或布局管理器),每个容器类都实现一个特定的策略来管理其子容器的大小和位置。例如, LinearLayout 类一个接一个地水平或垂直布局其子元素。所有的布局管理器都是从视图类中派生出来的,因此你可以将布局管理器相互嵌套。
Android SDK 附带的布局管理器包括表 5-1 中定义的常用管理器。
表 5-1 。 Android 布局管理器
|
布局管理器
|
描述
| | --- | --- | | 线性布局 | 水平或垂直组织其子节点 | | 表格布局 | 以表格形式组织其子节点 | | 相对布局 | 相对于彼此或相对于父节点组织其子节点 | | 帧布局 | 允许您动态更改布局中的控件 | | 网格布局 | 在网格排列中组织其子节点 |
我们将在接下来的章节中讨论这些布局管理器。名为 AbsoluteLayout 的布局管理器已被弃用,不在本书讨论范围内。
线性布局布局管理器
LinearLayout 布局管理器是最基本的。该布局管理器根据方向属性的值水平或垂直组织其子节点。到目前为止,我们已经在几个例子中使用了线性布局。清单 5-6 显示了水平配置的线性布局。
清单 5-6 。 线形布局带 横向配置
<LinearLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:orientation="horizontal"
android:layout_width="fill_parent" android:layout_height="wrap_content">
<!-- add children here-->
</LinearLayout>
通过将方向设置为垂直,可以创建一个垂直方向的线性布局。因为布局管理器可以嵌套,所以您可以构建一个包含水平布局管理器的垂直布局管理器来创建一个填充表单,其中每一行在一个 EditText 控件旁边都有一个标签。每一行都有自己的水平布局,但是集合中的行是垂直组织的。
了解重量和重力
方向属性是线性布局布局管理器识别的第一个重要属性。影响子控件大小和位置的其他重要属性是重量和重力。
您使用 weight 来指定一个控件相对于容器中其他控件的大小重要性。假设一个容器有三个控件:一个权重为 1,而其他的权重为 0。在这种情况下,权重等于 1 的控件将占用容器中的空白空间。引力本质上是对齐的。例如,如果您想将标签的文本向右对齐,您可以将其重力设置为右。重力的可能值有很多,包括左、中、右、上、下、中 _ 垂直、夹 _ 水平等。参见 developer.android.com 了解这些和其他重力值的细节。
注意布局管理器扩展了 android.widget.ViewGroup ,许多基于控件的容器类也是如此,比如 ListView 。尽管布局管理器和基于控件的容器扩展了相同的类,但按照惯例(如果不是严格的要求),布局管理器类处理控件的大小和位置,而不是用户与子控件的交互。
现在让我们看一个涉及重量和重力属性的例子(见图 5-3 )。
图 5-3 。使用线性布局布局管理器
图 5-3 显示了三个使用线性布局的用户界面,具有不同的重量和重力设置。左边的用户界面使用重量和重力的默认设置。第一个用户界面的 XML 布局如清单 5-7 所示。
清单 5-7 。 三个 文本字段 以线性布局垂直排列,使用默认的重量和重力值
<LinearLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent">
<EditText android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="one"/>
<EditText android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="two"/>
<EditText android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="three"/>
</LinearLayout>
图 5-3 中间的 UI 使用默认的 weight 值,但是将容器中控件的 android:gravity 分别设置为左侧、中间和右侧。最后一个示例将中心组件的 android:layout_weight 属性设置为 1.0,并将其他属性保留为默认值 0.0(参见清单 5-8 )。通过将中间组件的权重属性设置为 1.0,并将其他两个组件的权重属性设置为 0.0,我们指定中间组件应该占据容器中所有剩余的空白,而其他两个组件应该保持其理想大小。
类似地,如果您希望容器中的三个控件中的两个共享它们之间剩余的空白,您可以将这两个控件的权重设置为 1.0,将第三个控件的权重设置为 0.0。最后,如果希望三个组件平均共享空间,可以将它们的权重值都设置为 1.0。这样做可以均等地扩展每个文本字段。
清单 5-8 。 线型布局搭配 权重配置
<LinearLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent">
<EditText android:layout_width="fill_parent" android:layout_weight="0.0"
android:layout_height="wrap_content" android:text="one"
android:gravity="left"/>
<EditText android:layout_width="fill_parent" android:layout_weight="1.0"
android:layout_height="wrap_content" android:text="two"
android:gravity="center"/>
<EditText android:layout_width="fill_parent" android:layout_weight="0.0"
android:layout_height="wrap_content" android:text="three"
android:gravity="right"
/>
</LinearLayout>
安卓:重力 vs 安卓:布局 _ 重力
注意,Android 定义了两个相似的重力属性: android:gravity 和 android:layout_gravity 。区别在于: android:gravity 是视图使用的设置,而 android:layout_gravity 是容器( android.view.ViewGroup )使用的设置。例如,您可以将 android:gravity 设置为 center 以使 EditText 中的文本在控件内居中。同样,你可以通过设置 Android:layout _ gravity = " right "将 EditText 对齐到 LinearLayout (容器)的最右边。参见图 5-4 和清单 5-9 。
图 5-4 。应用重力设置
清单 5-9 。 了解安卓:重力和安卓:布局 _ 重力 的区别
<LinearLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent">
<EditText android:layout_width="wrap_content" android:gravity="center"
android:layout_height="wrap_content" android:text="one"
android:layout_gravity="right"/>
</LinearLayout>
如图 5-4 所示,文本在编辑文本中居中,与线型布局右侧对齐。
表布局布局管理器
表格布局布局管理器是线性布局的扩展。该布局管理器将其子控件组织成行和列。清单 5-10 显示了一个例子。
清单 5-10 。一个简单的表格布局
<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:layout_width="fill_parent" android:layout_height="fill_parent">
<TableRow>
<TextView android:text="First Name:"
android:layout_width="wrap_content" android:layout_height="wrap_content" />
<EditText android:text="Edgar"
android:layout_width="wrap_content" android:layout_height="wrap_content" />
</TableRow>
<TableRow>
<TextView android:text="Last Name:"
android:layout_width="wrap_content" android:layout_height="wrap_content" />
<EditText android:text="Poe"
android:layout_width="wrap_content" android:layout_height="wrap_content" />
</TableRow>
</TableLayout>
要使用这个布局管理器,您需要创建一个 TableLayout 的实例,并在其中放置 TableRow 元素。这些 TableRow 元素包含了表格的控件。清单 5-10 的用户界面如图 5-5 所示。
图 5-5 。表格布局 布局管理器
使用 TableLayout 可以实现许多更复杂的布局,包括嵌套、不对称的行和列等等。我们在图书网站上有一个关于桌面布局、【www.androidbook.com】的更多选项的奖励部分。
相对布局布局管理器
另一个有趣的布局管理器是 RelativeLayout 。顾名思义,这个布局管理器实现了一个策略,其中容器中的控件相对于容器或容器中的另一个控件进行布局。清单 5-11 和图 5-6 显示了一个例子。
清单 5-11 。?? 使用相对布局布局管理器
<RelativeLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView android:id="@+id/userNameLbl"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:text="Username: "
android:layout_alignParentTop="true" />
<EditText android:id="@+id/userNameText"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:layout_toRightOf="@id/userNameLbl" />
<TextView android:id="@+id/pwdLbl"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/userNameText"
android:text="Password: " />
<EditText android:id="@+id/pwdText"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:layout_toRightOf="@id/pwdLbl"
android:layout_below="@id/userNameText" />
<TextView android:id="@+id/pwdCriteria"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:layout_below="@id/pwdText"
android:text="Password Criteria... " />
<TextView android:id="@+id/disclaimerLbl"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="Use at your own risk... " />
</RelativeLayout>
图 5-6 。使用相对布局 布局管理器布局的用户界面
如图所示,UI 看起来像一个简单的登录表单。用户名标签被固定在容器的顶部,因为我们将 Android:layout _ alignParentTop 设置为 true 。类似地,用户名输入字段位于用户名标签下方,因为我们设置了 android:layout_below 。密码标签出现在用户名标签下方,密码输入字段出现在密码标签下方。免责声明标签被固定在容器的底部,因为我们将 Android:layout _ alignParentBottom 设置为 true 。
除了这三个布局属性,你还可以指定 layout_above 、 layout_toRightOf 、 layout_toLeftOf 、 layout_centerInParent 等等。使用 RelativeLayout 很有趣,因为它很简单。事实上,一旦你开始使用它,它将成为你最喜欢的布局管理器——你会发现自己一遍又一遍地回到它身边。
框架布局布局管理器
到目前为止,我们讨论的布局管理器实现了各种布局策略。换句话说,每一个都有特定的方式在屏幕上定位和定向它的孩子。有了这些布局管理器,你可以同时在屏幕上有许多控件,每个控件占据屏幕的一部分。Android 还提供了一个布局管理器,主要用来显示单个项目: FrameLayout 。您主要使用这个工具布局类来动态显示单个视图,但是您可以用许多项目填充它,将一个项目设置为可见,而将其他项目设置为不可见。清单 5-12 演示了如何使用框架布局。
清单 5-12 。 填充框架布局
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:id="@+id/frmLayout"
android:layout_width="fill_parent" android:layout_height="fill_parent">
<ImageView
android:id="@+id/oneImgView" android:src="@drawable/one"
android:scaleType="fitCenter"
android:layout_width="fill_parent" android:layout_height="fill_parent"/>
<ImageView
android:id="@+id/twoImgView" android:src="@drawable/two"
android:scaleType="fitCenter"
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:visibility="gone" />
</FrameLayout>
public class FrameLayoutActivity extends Activity{
private ImageView one = null;
private ImageView two = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.listing6_48);
one = (ImageView)findViewById(R.id.oneImgView);
two = (ImageView)findViewById(R.id.twoImgView);
one.setOnClickListener(new OnClickListener(){
public void onClick(View view) {
two.setVisibility(View.VISIBLE);
view.setVisibility(View.GONE);
}});
two.setOnClickListener(new OnClickListener(){
public void onClick(View view) {
one.setVisibility(View.VISIBLE);
view.setVisibility(View.GONE);
}});
}
}
清单 5-12 显示了布局文件以及活动的 onCreate() 方法。演示的想法是在 FrameLayout 中加载两个 ImageView 对象,一次只能看到 ImageView 对象中的一个。在 UI 中,当用户单击可见图像时,我们隐藏一个图像并显示另一个图像。
现在仔细看看清单 5-12 ,从布局开始。你可以看到我们用两个 ImageView 对象定义了一个 FrameLayout (一个 ImageView 是一个知道如何显示图像的控件)。注意,第二个 ImageView 的可见性被设置为消失,使得控件不可见。现在,看看 onCreate() 方法。在 onCreate() 方法中,我们注册侦听器来点击 ImageView 对象上的事件。在点击处理器中,我们隐藏一个 ImageView 并显示另一个。
如前所述,当您需要动态地将视图的内容设置为单个控件时,通常使用 FrameLayout 。虽然这是一般的做法,但控件将接受许多子控件,正如我们所演示的那样。清单 5-12 在布局中添加了两个控件,但每次只能看到其中一个。然而,FrameLayout 并不强迫你一次只能看到一个控件。如果你在布局中添加了很多控件, FrameLayout 会简单地堆叠控件,一个在另一个上面,最后一个在上面。这可以创建一个有趣的 UI。例如,图 5-7 显示了一个 FrameLayout 控件,带有两个可见的 ImageView 对象。您可以看到控件堆叠在一起,顶部的控件部分覆盖了它后面的图像。
图 5-7 。 框架布局 带有两个 ImageView 对象
FrameLayout 的另一个有趣的方面是,如果你向布局中添加多个控件,布局的大小将被计算为容器中最大项目的大小。在图 5-7 中,顶部的图像实际上比它后面的图像小得多,但是因为布局的尺寸是根据最大的控件计算的,所以顶部的图像被拉伸了。
还要注意的是,如果你在一个 FrameLayout 中放置了许多控件,其中一个或多个是不可见的,你可能要考虑在你的 FrameLayout 中使用 setMeasureAllChildren(true)。因为最大的子元素决定了布局的大小,如果最大的子元素一开始是不可见的,那么就会有问题:当它变得可见时,它只是部分可见。为了确保所有项目都能正确呈现,调用 setMeasureAllChildren()并向其传递值 true 。 FrameLayout 的等价 XML 属性是 Android:measure all children = " true "。
GridLayout 布局管理器
Android 4.0 带来了一个新的布局管理器,叫做 GridLayout 。正如您所料,它以行和列的网格模式布局视图,有点像 TableLayout 。不过比 TableLayout 好用。使用 GridLayout ,您可以为视图指定行和列值,这就是它在网格中的位置。这意味着您不需要为每个单元格指定一个视图,只需要为那些您想要保存视图的单元格指定一个视图。视图可以跨越多个网格单元。您甚至可以在同一个网格单元中放置多个视图。
布局视图时,不能使用 weight 属性,因为它在 GridLayout 的子视图中不起作用。你可以使用布局 _ 重力属性来代替。您可以在 GridLayout 子视图中使用的其他有趣属性包括 layout_column 和 layout_columnSpan 来分别指定视图最左侧的列和列数。同样,还有 layout_row 和 layout_rowSpan 属性。有趣的是,您不需要为 GridLayout 子视图指定 layout_height 和 layout _ width;它们默认为 WRAP_CONTENT 。
为各种设备配置定制布局
到目前为止,您已经非常了解 Android 提供了大量的布局管理器来帮助您构建 ui。如果您使用过我们讨论过的布局管理器,您会知道您可以用各种方式组合布局管理器来获得您想要的外观和感觉。但是,即使有了所有的布局管理器,构建 ui——并使它们正确——也可能是一个挑战。对于移动设备来说尤其如此。移动设备的用户和制造商变得越来越复杂,这使得开发人员的工作更具挑战性。
挑战之一是为应用构建一个可以在各种屏幕配置中显示的 UI。例如,如果你的应用以纵向和横向模式显示,你的用户界面会是什么样子?如果你还没有遇到这种情况,你现在可能正在思考如何处理这种常见的情况。有趣且幸运的是,Android 为这个用例提供了一些支持。
它是这样工作的:当建立一个布局时,Android 会根据设备的配置从特定的文件夹中找到并加载布局。设备可以有三种配置:纵向、横向或方形(方形很少见)。要为各种配置提供不同的布局,您必须为每个配置创建特定的文件夹,Android 将从这些文件夹中加载适当的布局。如你所知,默认布局文件夹位于 res/layout 。为了支持纵向显示,创建一个名为 res/layout-port 的文件夹。对于风景,创建一个名为 res/layout-land 的文件夹。对于正方形,创建一个名为 res/layout-square 的正方形。
此时一个很好的问题是,“有了这三个文件夹,我还需要默认的布局文件夹( res/layout )吗?”一般来说,是的。Android 的资源解析逻辑首先查看特定于配置的目录。如果 Android 在那里找不到资源,它会转到默认的布局目录。因此,您应该将默认布局定义放在 res/layout 中,并将定制版本放在特定于配置的文件夹中。
另一个技巧是在布局文件中使用 < include / > 标签。这允许您创建布局代码的公共块(例如,在默认的布局目录中)并将它们包含在布局端口和布局端口中定义的布局中。一个包含标签可能看起来像这样:
<include layout="@layout/common_chunk1" />
如果你对包含的概念感兴趣,你还应该看看 Android API 中的 < merge / > 标签和 ViewStub 类。这些给你更多的灵活性,当组织布局,没有重复的观点。
请注意,Android SDK 没有提供任何 API 让您以编程方式指定加载哪个配置——系统只是根据设备的配置选择文件夹。但是,您可以在代码中设置设备的方向,例如,使用以下代码:
import android.content.pm.ActivityInfo;
...
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
这将强制您的应用以横向模式出现在设备上。请在您的早期项目中尝试一下。将代码添加到 activity 的 onCreate() 方法中,在模拟器中运行它,然后查看您的应用。
摘要
让我们通过快速列举你所学到的关于构建用户界面的知识来结束这一章:
- 布局的主要类型以及何时使用每种布局
- Android 支持的视图以及如何用 XML 和代码定义它们
- 您可以使用样式和主题从一组公共资源中管理应用的外观和感觉
六、使用菜单和操作栏
Android SDK 支持常规菜单、子菜单、上下文菜单、图标菜单和二级菜单。Android 3.0 引入了动作栏,与菜单很好的融合在一起。我们将在本章中讨论菜单和动作栏。
像本书中的许多其他章节一样,我们将展示一些基本的代码片段,您可以用它们来处理菜单和动作栏。这些代码片段的完整代码上下文可以在专门为本章开发的可下载应用中找到。这些可下载项目的链接位于本章末尾的“参考资料”部分。
通过 XML 文件使用菜单
在 Android 中,使用菜单最简单的方法是通过 XML 菜单资源文件。这种创建菜单的 XML 方法有几个优点,比如命名菜单、自动排序菜单和分配 id 的能力。由于 XML 菜单是资源,您还可以获得菜单文本和图标的本地化支持。
创建 XML 菜单资源文件
清单 6-1 给出了一个示例菜单 XML 文件。在这个清单中,您可以看到一系列的菜单项组合在一个组 XML 节点下。您可以使用 @+id 资源引用方法为组指定一个 ID。您可以在 java 代码中使用这个 ID 来访问菜单组,并在需要时管理它。分组是可选的,可以省略 group XML 节点。
每个菜单 XML 文件都有一系列菜单项,它们的菜单项 id 与符号名相关联。标题表示菜单标题, orderInCategory 表示菜单项在菜单中出现的顺序。您可以参考 Android SDK 文档,了解这些 XML 标签的所有可能属性。本章的“参考资料”部分提供了参考 URL。
清单 6-1 。带有菜单定义的菜单 XML 资源文件
<menu xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)">
<group android:id="@+id/menuGroup_Main">
<item android:id="@+id/menu_item1"
android:orderInCategory="1"
android:title="item1 text" />
<item android:id="@+id/menu_item2"
android:orderInCategory="2"
android:enabled="true"
android:icon="@drawable/some-file"
android:title="item2 text" />
<item android:id="@+id/menu_item3"
android:orderInCategory="3"
android:title="item3 text" />
</group>
</menu>
清单 6-1 中的所有子菜单项都根据它们在 XML 文件中的名称(例如: menu_item1 )分配了菜单项 id。现在让我们看看如何获取这个菜单 XML 文件并将其与一个活动相关联。
从菜单 XML 文件填充活动菜单
假设菜单 XML 文件的名称是 my_menu.xml 。你需要把这个文件放在 /res/menu 子目录中。将文件放在 /res/menu 中会自动生成一个名为 R.menu.my_menu 的资源 ID。
Android 菜单支持中的关键类是 android.view.Menu 。Android 中的每个活动都与一个这种类型的菜单对象相关联。在活动的生命周期中,Android 调用一个名为 onCreateOptionsMenu()的方法来填充这个菜单对象。在这个方法中,我们将 XML 菜单文件加载到菜单对象中。这显示在清单 6-2 中。
清单 6-2 。使用菜单充气器
//This callback method is available on every activity class
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater(); //from activity
inflater.inflate(R.menu.my_menu, menu);
//It is important to return true to see the menu
return true;
}
一旦菜单项被填充,代码应该返回 true 以使菜单可见。如果这个方法返回假,菜单是不可见的。
响应基于 XML 的菜单项
您在 onOptionsItemSelected()回调方法中响应菜单项。Android 不仅为 XML 菜单文件生成一个资源 id(如在清单 6-2 中使用的),而且还生成必要的菜单项 ID 来帮助你区分菜单项。清单 6-3 中的代码说明了如何响应菜单项。
清单 6-3 。响应 XML 菜单资源文件中的菜单项
@Override
public void onOptionsItemSelected (MenuItem item){
if (item.getItemId() == R.id.menu_item1){
//do something
//for items handled
return true;
}
else if (item.getItemId() == R.id.menu_item2){
//do something
return true;
}
//for the rest
...return super.onOptionsItemSelected(item);
}
注意 XML 菜单资源文件中的菜单项名称是如何在 R.id 空间中自动生成菜单项 id 的。
从 SDK 3.0 开始,您还可以使用菜单项的 android:onClick 属性来直接指示附加到该菜单的活动中的方法名称。然后使用菜单项对象作为唯一的输入来调用这个活动方法。该功能仅在 3.0 及以上版本中可用。清单 6-4 显示了一个例子。
清单 6-4 。在 XML 菜单资源文件中指定菜单回调方法
<item android:id="... "
android:onClick="a-method-name-in-your-activity"
...
</item>
在 Android 中使用菜单项就是这么简单。现在让我们探索一下菜单的 Java API。
在 Java 代码中使用菜单
如前所述,Android 菜单支持中的关键类是 android.view.Menu 。Android 中的每个活动都与一个这种类型的菜单对象相关联。然后,菜单对象包含许多菜单项和子菜单。菜单项由 android.view.MenuItem 表示。子菜单以 android.view.SubMenu 为代表。
在 SDK 3.0 之前, onCreateOptionsMenu() 在第一次访问活动的选项菜单时被调用。从 3.0 开始,该方法作为活动创建的一部分被调用。另请注意,此方法在活动的生命周期中只调用一次。如果你想动态添加菜单,你需要使用 onPrepareOptionsMenu()方法,稍后会讲到。清单 6-5 中的代码显示了如何使用一个单独的组 ID 以及递增的菜单项 ID 和订单 ID 来添加三个菜单项。
清单 6-5 。添加菜单项
@Override
public boolean onCreateOptionsMenu(Menu menu){
super.onCreateOptionsMenu(menu);
menu.add(0 // Group
,1 // item id
,0 //order
,"item1"); // title
menu.add(0,2,1,"item2");
menu.add(0,3,2,"item3");
//It is important to return true to see the menu
return true;
}
还应该调用该方法的基类实现,让系统有机会用系统菜单项填充菜单(到目前为止还没有定义系统菜单项)。
清单 6-5 中的解释了创建菜单项的参数。最后一个参数是菜单项的名称或标题。代替自由文本,你可以通过 R.java 的常量文件使用一个字符串资源。组、菜单项和订单 id 都是可选的;你可以使用菜单。无如果你不想指定它们中的任何一个。如果菜单。没有为组指定,则项目不属于任何组。如果菜单。没有为项目指定,那么这可能是一个子菜单或分隔符。如果菜单。没有为顺序指定,Android 将选择一些机制来排序它们。
使用菜单组
现在,让我们看看如何使用菜单组。清单 6-6 展示了如何添加两组菜单:第一组和第二组。
清单 6-6 。使用组 id 创建菜单组
@Override
public boolean onCreateOptionsMenu(Menu menu) {
//Group 1
int group1 = 1;
menu.add(group1,1,1,"g1.item1");
menu.add(group1,2,2,"g1.item2");
//Group 2
int group2 = 2;
menu.add(group2,3,3,"g2.item1");
menu.add(group2,4,4,"g2.item2");
return true; // it is important to return true
}
Android 在 android.view.Menu 类上提供了一组基于组 id 的方法。您可以使用清单 6-7 中所示的方法操作一个组的菜单项:
清单 6-7 。菜单组相关方法
removeGroup(id)
setGroupCheckable(id, checkable, exclusive)
setGroupEnabled(id,enabled)
setGroupVisible(id,visible)
removeGroup() 给定组 ID,从该组中删除所有菜单项。您可以使用 setGroupEnabled 方法 () 来启用或禁用给定组中的菜单项。同样,您可以使用 setgroup visible()来控制一组菜单项的可见性。
setGroupCheckable()有意思。当菜单项被选中时,可以使用此方法在该菜单项上显示复选标记。当应用于一个组时,它为该组中的所有菜单项启用此功能。如果设置了该方法的独占标志,则该组中只有一个菜单项被允许进入选中状态。其他菜单项保持未选中状态。
现在您知道了如何用一组菜单项填充活动的主菜单,并根据它们的性质对它们进行分组。除了菜单项 id 由程序员显式控制之外,您对这些菜单项的响应方式与您对它们的 XML 对应项的响应方式是相同的。
通过监听器响应菜单项
你通常通过覆盖 onOptionsItemSelected() 来响应菜单;菜单项还允许您注册一个可以用作回调的侦听器。这种方法分两步走。第一步,实现菜单项。OnMenuItemClickListener 接口。然后,获取该实现的一个实例,并将其传递给菜单项。当菜单项被点击时,菜单项调用 MenuItem 的 onMenuItemClick() 方法。OnMenuItemClickListener 接口(参见清单 6-8 )。
清单 6-8 。使用监听器作为菜单项点击的回调
//Step 1
public class MyResponse implements MenuItem.OnMenuItemClickListener{
public MyResponse(...someargs...){} //a constructor
@override
public boolean OnMenuItemClick(MenuItem item) {
//do your thing
return true;
}
}
//Step 2
MyResponse myResponse = new MyResponse(..your args..);//supply your args
menuItem.setOnMenuItemClickListener(myResponse);
...
当菜单项被调用时,调用 onMenuItemClick() 方法。菜单项一被单击,甚至在调用 onOptionsItemSelected() 方法之前,这段代码就会执行。如果 onMenuItemClick() 返回 true ,则不执行其他回调,包括 onOptionsItemSelected() 回调方法。这意味着监听器代码优先于 onOptionsItemSelected()方法。
使用意图来响应菜单项
您也可以通过使用 MenuItem 的方法 setIntent(intent) 将菜单项与意图相关联。当一个意图与一个菜单项相关联,并且没有其他东西处理该菜单项时,那么默认行为是使用 startActivity(intent) 调用该意图。为此,所有处理器——尤其是 onOptionsItemSelected() 方法——都应该为那些未被处理的菜单项调用父类的 onOptionsItemSelected() 方法。
了解扩展菜单
如果一个应用的菜单项比它在主屏幕上显示的要多,Android 会多显示个菜单项,让用户可以看到剩下的部分。这个菜单被称为扩展菜单,当有限的空间内显示太多菜单项时,它会自动出现。
使用图标菜单
Android 不仅支持文本,还支持图像或图标作为其菜单的一部分。创建图标菜单项很简单。像以前一样创建一个常规的基于文本的菜单项,然后使用 MenuItem 类上的 setIcon() 方法来设置图像。您需要使用图像的资源 ID,所以您必须首先通过将图像或图标放在 /res/drawable 目录中来生成它。例如,如果图标的文件名是气球,那么资源 ID 就是 R.drawable .气球。清单 6-9 演示了如何给一个菜单项添加一个图标。
清单 6-9 。将图标附加到菜单项
//add a menu item and remember it so that you can use it
//subsequently to set the icon on it.
MenuItem item = menu.add(...);//supply the menu item details
item.setIcon(R.drawable.balloons);
只要菜单项显示在主应用屏幕上,图标就会显示。如果它显示为扩展菜单的一部分,则不显示图标,只显示文本。在 XML 菜单资源文件中,还有一个图标标签可以用来指示图标。在某些情况下,Android 可能会选择不显示图标,并建议始终提供文本。
使用子菜单
一个菜单对象可以有多个子菜单对象。通过调用 Menu.addSubMenu() 方法,将每个子菜单对象添加到菜单对象中(参见清单 6-10 )。向子菜单添加菜单项的方式与向菜单添加菜单项的方式相同。这是因为子菜单 也是从菜单对象中派生出来的。但是,您不能向子菜单添加额外的子菜单。
清单 6-10 。添加子菜单
private void addSubMenu(Menu menu){
//Secondary items are shown just like everything else
int base=Menu.FIRST + 100;
SubMenu sm = menu.addSubMenu(base,base+1,Menu.NONE,"submenu");
sm.add(base,base+2,base+2,"sub item1");
sm.add(base,base+3,base+3,"sub item2");
sm.add(base,base+4,base+4,"sub item3");
//the following is ok
sm.setIcon(R.drawable.icon48x48_1);
//This will result in runtime exception
//sm.addSubMenu("try this");
}
注意 子菜单,作为菜单对象的子类,继续携带 addSubMenu() 方法。如果你把一个子菜单添加到另一个子菜单,编译器不会报错,但是如果你试图这样做,你会得到一个运行时异常。
Android SDK 文档也建议子菜单不支持图标菜单项。当您将图标添加到菜单项,然后将该菜单项添加到子菜单时,菜单项会忽略该图标,即使您没有看到编译时或运行时错误。但是,子菜单本身可以有图标。
使用上下文菜单 s
Android 通过一个叫做长点击的动作来支持上下文菜单的想法。在任何 Android 视图中,长点击是指长时间按住鼠标,时间比平时稍长。活动拥有常规的选项菜单,而视图拥有上下文菜单。这是意料之中的,因为激活上下文菜单的长时间点击适用于被点击的视图。因此,一个活动只能有一个选项菜单,但可以有许多上下文菜单。
注册上下文菜单的视图
实现上下文菜单的第一步是在活动的 onCreate() 方法中注册上下文菜单的视图。您可以使用清单 6-11 中的代码为上下文菜单注册一个文本视图。首先找到文本视图,然后使用文本视图作为参数,对活动调用 registerForContextMenu() 。这为上下文菜单设置了文本视图。
清单 6-11 。为上下文菜单注册一个文本视图
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
TextView tv = (TextView)this.findViewById(R.id.textViewId);
registerForContextMenu(tv);
}
填充上下文菜单
一旦像本例中的 TextView 这样的视图被注册为上下文菜单,Android 就调用 onCreateContextMenu() 方法,将该视图作为参数。这是您可以填充该上下文菜单的上下文菜单项的地方。 onCreateContextMenu() 回调方法提供了三个参数。清单 6-12 演示了 onCreateContextMenu() 方法。
清单 6-12 。onCreateContextMenu()方法
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo){
menu.setHeaderTitle("Sample Context Menu");
menu.add(200, 200, 200, "item1");
}
第一个参数是预构造的 ContextMenu 对象,第二个是生成回调的视图(如 TextView ),第三个是 ContextMenuInfo 类。对于许多简单的情况,您可以忽略 ContextMenuInfo 对象。但是,某些视图可能会通过该对象传递额外的信息。在这些情况下,您需要将 ContextMenuInfo 类转换为一个子类,然后使用附加方法来检索附加信息。
从 ContextMenuInfo 派生的类的一些例子包括 AdapterContextMenuInfo 和 ExpandableListContextMenuInfo。作为 AdapterViews 的视图,比如 Android 中的 ListView 使用 AdapterContextMenuInfo 类来传递显示上下文菜单的视图中的行 id。从某种意义上说,您可以使用这个类来进一步澄清触摸或点击下面的对象,甚至是在给定的复合视图中。
响应上下文菜单项
Android 提供了一个类似于 onOptionsItemSelected() 的回调方法叫做 onContextItemSelected()。清单 6-13 演示了 onContextItemSelected()。
清单 6-13 。响应上下文菜单
//This method is available for all activities @Override
public boolean onContextItemSelected(MenuItem item) {
if (item.getItemId() == some-menu-item-id) {
//handle this menu item
return true;
}
... other exception processing
}
整合动态菜单
到目前为止,我们已经讨论了静态菜单——你设置了一次,它们不会根据屏幕上的内容动态变化。如果你想创建动态菜单,使用 Android 在活动类上提供的 onPrepareOptionsMenu() 方法。这个方法类似于 onCreateOptionsMenu() ,除了它在每次显示菜单之前被调用。如果你的菜单有动态菜单选项,你应该使用 onPrepareOptionsMenu() 和 onCreateOptionsMenu() 来有效地管理你的菜单。 onPrepareOptionMenu() 是根据您正在显示的内容启用或禁用某些菜单项或菜单组的地方。对于 3.0 及更高版本,当您想要更改菜单时,因为像操作栏这样的菜单相关组件总是显示,所以您必须显式调用一个名为 activity . invalidateoptions menu()的新预配方法,该方法又调用 onCreateOptionsMenu() 并重新绘制菜单,从而也导致在显示之前调用 onPrepareOptionsMenu() 。只要应用状态发生变化,需要更改菜单,就可以调用此方法。
使用弹出式菜单
Android 3.0 引入了另一种类型的菜单,称为弹出菜单。SDK 4.0 通过在 PopupMenu 类中添加几个实用方法(例如, PopupMenu.inflate )稍微增强了这一点。(参见弹出菜单 API 文档以了解这些方法。清单 6-14 也引起了对这种差异的注意。)
可以针对任何视图调用弹出菜单来响应任何 UI 事件。UI 事件的一个例子是按钮点击或图像视图上的点击。图 6-1 显示了一个针对视图调用的弹出菜单。
图 6-1 。附加到文本视图的弹出菜单
要创建一个类似于图 6-1 中的弹出菜单,从一个常规的 XML 菜单文件开始,使用清单 6-14 中的 Java 代码加载这个菜单 XML 作为弹出菜单。如果您想查看完整的实现,请参阅本章的可下载项目。
清单 6-14 。使用弹出式菜单
//Other activity code goes here...
//Invoke the following method to show a popup menu
private void showPopupMenu() {
//Get hold of a view to anchor the popup
TextView tv = findViewById(R.id.SOME_TEXT_VIEW_ID);
//instantiate a popup menu. var "this" stands for activity
PopupMenu popup = new PopupMenu(this, tv);
//the following code for 3.0 sdk
//popup.getMenuInflater().inflate(R.menu.popup_menu, popup.getMenu());
//Or in sdk 4.0 and above
popup.inflate(R.menu.popup_menu);
popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
//do something here
return true; } } );
popup.show();
}
如您所见,弹出菜单的行为很像选项菜单。主要区别如下:
- 弹出菜单按需使用,而选项菜单总是可用的。
- 弹出菜单锚定到一个视图,而选项菜单属于整个活动。
- 弹出菜单使用自己的菜单项回调,而选项菜单使用活动上的 onOptionsItemSelected() 回调。
探索动作栏
在 Android 3.0 中引入并在 Android 4.0 中扩展的 ActionBar 将菜单的范围扩展到了活动的标题栏。这允许用户容易地获得频繁使用的动作,而无需搜索选项菜单或上下文菜单。除了图标和菜单项,动作栏可以容纳其他视图,如标签、列表或搜索框,以帮助导航。图 6-2 显示了选项卡导航模式下的动作栏。
图 6-2 。带有选项卡式操作栏的活动
您可以在这里看到动作栏的各个部分。操作栏左上角的图标称为主页图标。点击这个 Home 图标会向菜单 ID 为 android 的选项菜单发送一个回调。R.id.home 主页图标后面是此活动的标题区域。然后您会看到一组选项卡(或者一个下拉列表,如果这是一个基于列表的动作栏的话)。在中间,你可以看到搜索视图。快结束时,你会看到一组动作图标。该操作栏的最后一部分是一条垂直虚线,代表该活动的菜单。当你点击该图标时,会出现一个标准的下拉菜单(参见图 6-3 )。
你在图 6-1 中看到的动作条是一个选项卡式动作条。动作栏的另外两种模式是标准模式和列表模式。在列表操作栏中,选项卡由下拉列表代替。在标准操作栏中,没有为列表或选项卡留出区域。现在,让我们向您展示如何实现一个简单的标准动作栏。
实现标准动作栏
清单 6-15 展示了为一个活动实现一个标准导航动作栏的示例源代码。
清单 6-15 。标准导航操作栏活动
public class StandardNavigationActionBarActivity extends Activity {
// ..... other code
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActionBar bar = this.getActionBar();
bar.setTitle("Some title of your choosing");
bar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
}
public boolean onCreateOptionsMenu(Menu mainMenu) {
//load the menu xml file into the mainMenu object as usual here
return true;
}
}
从清单 6-15 中可以看出,使用动作栏很容易。注意在清单中我们如何使用 getActionBar() 来访问动作栏对象,然后设置它的标题和导航模式。您在 onCreateOptionsMenu() 中设置的任何菜单都可以直接从操作栏中调用,如图图 6-3 所示。(然而,当菜单以这种方式从动作栏呈现时,由于空间限制,系统可能不显示图标和菜单文本。)
图 6-3 。带有操作栏和扩展菜单的活动
随着动作栏的引入,菜单 XML 文件通过新的属性得到了增强,以指示一些菜单项在动作栏中直接显示为图标。(您可以在图 6-3 中展开菜单上方的操作栏中看到这些图标)。清单 6-16 中的 XML 菜单文件示例演示了如何将一个菜单项直接指定为动作栏上的一个图标。
清单 6-16 。此项目的菜单 XML 文件
<!-- /res/menu/menu.xml -->
<menu
xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)">
<!-- This group uses the default category. -->
<group android:id="@+id/menuGroup_Main">
<!-- a regular menu item -->
<item android:id="@+id/menu_da_clear"
android:title="clear" />
<!--item to be shown directly on the action bar-->
<item android:id="@+id/menu_action_icon1"
android:title="Action Icon1"
android:icon="@drawable/creep001"
android:showAsAction="ifRoom"/>
<!-- ..other menu items-->
</group>
</menu>
要在动作栏上显示的菜单项用标签 showAsAction 表示。在前面的代码中,该属性被设置为“ ifRoom ”。这个 XML 标签的其他可能值如下:总是,从不,带文本, collapseActionView 。您也可以使用在 MenuItem 类上可用的 Java API 来实现相同的效果。选项始终表示“在动作栏中将此项显示为按钮”选项从不表示“从不显示该项目”选项 withText 表示“显示此项目及其文本标签和图标”选项 collapseActionView 的意思是“当没有被选中时,折叠这个动作菜单项的动作视图所占用的空间。”因为这些动作仅仅是菜单项,所以它们的行为也是如此,并调用 activity 类的 onOptionsItemSelected()回调方法。
实现选项卡式操作栏
清单 6-17 展示了如何设置一个选项卡式动作栏。
清单 6-17 。选项卡-启用导航的操作栏活动
//Activity Source code
public class TabNavigationActionBarActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
workwithTabbedActionBar();
}
public void workwithTabbedActionBar() {
ActionBar bar = this.getActionBar();
bar.setTitle(tag);
bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
TestTabListener tl = new TestTabListener();
Tab tab1 = bar.newTab();
tab1.setText("Tab1"); tab1.setTabListener(tl); bar.addTab(tab1);
Tab tab2 = bar.newTab();
tab2.setText("Tab2"); tab2.setTabListener(tl); bar.addTab(tab2);
}
}//eof-class
选项卡式操作栏,顾名思义,有多个选项卡。在清单 6-17 中,你可以看到有一些额外的方法和类被用来处理标签动作栏。与标准动作栏不同,选项卡式动作栏需要每个选项卡都有一个选项卡监听器。这个监听器需要实现 TabListener 接口。在清单 6-18 中,类 TestTabListener 实现了 TabListener 接口。如果您忘记在添加到操作栏的选项卡上调用 setta listener()方法,您会得到一个运行时错误,指示需要一个侦听器。清单 6-18 显示了测试列表器类的代码。
清单 6-18 。响应选项卡操作的选项卡监听器
public class TestTabListener implements ActionBar.TabListener {
// constructor code
public TestTabListener(){}
// callbacks
public void onTabReselected(Tab tab, FragmentTransaction ft) {
//apply necessary logic here
}
public void onTabSelected(Tab tab, FragmentTransaction ft) {
//apply necessary logic here
}
public void onTabUnselected(Tab tab, FragmentTransaction ft) {
//apply necessary logic here
}
}
当标签被选中和取消选中时,清单 6-18 中的回调方法将被调用。动作栏是活动的属性,不跨越活动边界。换句话说,我们不能使用一个动作栏来控制或影响多个活动。每个活动必须提供自己的动作栏。动作栏之间动作的任何共性都留给程序员来编排。
在清单 6-17 中,一旦我们获得了活动的动作栏,我们就将其导航模式设置为动作栏。导航 _ 模式 _ 标签页。另外两种可能的动作栏导航模式是导航 _ 模式 _ 列表和导航 _ 模式 _ 标准。现在让我们看看如何实现一个基于列表的动作栏。
实现基于列表的动作栏
为了能够用列表导航模式初始化操作栏,您需要以下两样东西:
- 可用于填充导航选项下拉列表的微调器适配器。
- 一个列表导航监听器,当一个列表项被选中时,你可以得到一个回调。
清单 6-19 展示了实现 SpinnerAdapter 接口的 simplespinnerrayadapter。如前所述,这个类的目标是给出要显示的项目列表。
清单 6-19 。为列表导航创建微调器适配器
public class SimpleSpinnerArrayAdapter extends ArrayAdapter<String>
implements SpinnerAdapter {
public SimpleSpinnerArrayAdapter(Context ctx) {
super(ctx,
android.R.layout.simple_spinner_item,
new String[]{"one","two"});
this.setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item);
}
public View getDropDownView(int position, View convertView, ViewGroup parent) {
return super.getDropDownView(
position, convertView, parent);
}
}
没有直接实现列表导航所需的 SpinnerAdapter 接口的 SDK 类。因此,您从一个 ArrayAdapter 中派生出这个类,并为 SpinnerAdapter 提供一个简单的实现。在本章的最后是一个关于 spinner 适配器的参考 URL,供进一步阅读。现在让我们转到列表导航监听器。这是一个实现 ActionBar 的简单类。OnNavigationListener 。清单 6-20 显示了这个类的代码。
清单 6-20 。为列表导航创建列表侦听器
public class ListListener implements ActionBar.OnNavigationListener {
//simple constructor...
public ListListener(){}
//needed callback to respond to actions
public boolean onNavigationItemSelected(int itemPosition, long itemId) {
//respond and return true
return true;
}
}
现在,您已经具备了设置列表导航操作栏所需的条件。使用基于列表的动作栏所需的源代码如清单 6-21 所示。
清单 6-21 。列出导航操作栏活动
//Activity Source code
public class TabNavigationActionBarActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
workwithTabbedActionBar();
}
public void workwithListActionBar() {
ActionBar bar = this.getActionBar();
bar.setTitle("title");
bar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
bar.setListNavigationCallbacks(
new SimpleSpinnerArrayAdapter(this),
new ListListener());
}
}//eof-class
图 6-4 显示了一个列表条动作条展开后的样子。
图 6-4 。打开导航列表的活动
这就是我们如何将动作栏用于常规菜单、选项卡式导航和基于列表的导航的结论。现在让我们看看如何嵌入一个搜索视图,如图 6-2 所示。
浏览操作栏和搜索视图
本节展示了如何在操作栏中使用搜索小部件。要在操作栏中使用搜索,您需要具备以下条件:
- 在指向 SDK 提供的搜索视图类的菜单 XML 文件中定义菜单项。您还需要一个活动来加载这个菜单。这通常称为搜索调用者活动。
- 创建另一个活动,该活动可以从步骤 1 中的搜索视图中获取查询并提供结果。这通常称为搜索结果活动。
- 创建一个 XML 文件,允许您在操作栏中自定义搜索视图。这个文件通常被称为 searchable.xml ,位于 res/xml 子目录中。
- 在清单文件中声明搜索结果活动。这个定义需要指向步骤 3 中定义的 XML 文件。
- 在搜索调用者活动的菜单设置中,指出搜索视图需要以步骤 2 中的搜索结果活动为目标。
让我们从搜索视图小部件开始。
将搜索视图小部件定义为菜单项
要定义一个出现在活动操作栏中的搜索视图,您需要在一个菜单 XML 文件中定义一个菜单项,如清单 6-22 中的所示。
清单 6-22 。搜索视图菜单项定义
<item android:id="@+id/menu_search"
android:title="Search"
android:showAsAction="ifRoom"
android:actionViewClass="android.widget.SearchView"
/>
清单 6-22 中的关键元素是指向 Android . widget . search view 的 actionViewClass 属性。在本章前面,当您声明您的普通菜单项在动作栏中显示为动作图标时,您已经看到了其他属性。
创建搜索结果活动
要在应用中启用搜索,您需要一个能够响应搜索查询的活动。这可以像任何其他活动。清单 6-23 中显示了一个例子。
清单 6-23 。搜索结果活动
public class SearchResultsActivity extends Activity {
public static String tag = "SearchResultsActivity ";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Intent queryIntent = getIntent();
doSearchQuery(queryIntent);
}
@Override
public void onNewIntent(final Intent newIntent) {
super.onNewIntent(newIntent);
final Intent queryIntent = getIntent();
doSearchQuery(queryIntent);
}
private void doSearchQuery(final Intent queryIntent) {
final String queryAction = queryIntent.getAction();
if (!(Intent.ACTION_SEARCH.equals(queryAction))) {
Log.d(tag,"intent NOT for search");
return;
}
final String queryString = queryIntent.getStringExtra(SearchManager.QUERY);
Log.d(tag, queryString);
}
}//eof-class
在清单 6-23 中,活动检查调用它的动作是否是由搜索发起的。或者,这个活动可能是新创建的,或者只是放在顶部,在这种情况下,它需要做一些与它的 onNewIntent() 方法中的 onCreate() 方法相同的事情。另一方面,如果这个活动由 search 调用,它将使用一个名为 SearchManager 的额外参数来检索查询字符串。查询 。然后,活动记录该字符串是什么。在真实的场景中,您将使用该字符串来绘制匹配的结果。
指定可搜索的 XML 文件
正如前面的步骤所指出的,让我们看看定制搜索小部件所需的 XML 文件;参见清单 6-24 。
清单 6-24 。可搜索的 XML 文件
<!-- /res/xml/searchable.xml -->
<searchable xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:label="@string/search_label"
android:hint="@string/search_hint"
/>
提示属性将作为提示出现在搜索视图小部件上,当您开始键入时,该提示将消失。标签在动作栏中并不起重要作用。但是,当您在搜索对话框中使用相同的搜索结果活动时,该对话框会在此处定义标签。您可以通过以下 URL 了解有关可搜索 XML 属性的更多信息:
[`developer.android.com/guide/topics/search/searchable-config.html`](http://developer.android.com/guide/topics/search/searchable-config.html)
在清单文件中定义搜索结果活动
现在让我们看看如何将这个 XML 文件与搜索结果活动联系起来。这是在清单文件中完成的,作为定义搜索结果活动的一部分:参见清单 6-25 。注意指向可搜索的 XML 文件资源的元数据定义。
清单 6-25 。将活动绑定到它的 Searchable.xml
<activity android:name=".SearchResultsActivity"
android:label="Search Results">
<intent-filter>
<action android:name="android.intent.action.SEARCH"/>
</intent-filter>
<meta-data android:name="android.app.searchable"
android:resource="@xml/searchable"/>
</activity>
标识搜索视图小部件的搜索目标
到目前为止,您的操作栏中有 search 视图,并且您有可以响应 search 的活动。您需要使用 Java 代码将这两部分结合在一起。作为设置菜单的一部分,您可以在搜索调用活动的 onCreateOptions() 回调中这样做。清单 6-26 中的函数可以从 onCreateOptions() 中调用,以链接搜索视图小部件和搜索结果活动。
清单 6-26 。将搜索视图小部件绑定到搜索结果活动
private void setupSearchView(Menu menu) {
//Step1: Locate the search view widget
SearchView searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
//report error and return if searchView is null
//Step2: get SearchManager and searchableInfo
SearchManager
searchManager = (SearchManager)getSystemService(Context.SEARCH_SERVICE);
ComponentName cn = new ComponentName(this,SearchResultsActivity.class);
SearchableInfo info = searchManager.getSearchableInfo(cn);
//report error and return if searchable info is null
//Step3: set searchableInfo on the searchview widget
searchView.setSearchableInfo(info);
// Do not iconify the widget; expand it by default
searchView.setIconifiedByDefault(false);
}
让我们看看清单 6-26 中发生了什么。这段代码的目标是告诉搜索视图在哪里可以找到定义搜索行为的 searchable.xml 。为此,第一步是获取对 SearchView 的引用。这是通过菜单对象完成的。第二步是询问系统范围的搜索管理器什么可搜索的 XML 文件与活动 SearchResultsActivity 相关联。这是通过调用 SearchManager 系统服务上的方法 getSearchableInfo来完成的。一旦我们有了代表 XML 文件的 SearchableInfo 对象,我们就将该信息传递给 SearchView 对象。有了所有这些,现在如果您在搜索框中键入一些内容,这些信息将被传递给搜索结果活动,它将显示结果。
Android Search API 是一个很大的 API,有很多细微差别,由于篇幅原因,我们没有包括在本书中。有三点建议。我们在“参考资料”部分提供了一个 URL,指向一系列关于 Google search API 的文章和注释。我们也有一个关于搜索的大章节,可以在网上找到。这个链接也在“参考资料”一节中。我们还更新了上一版本的搜索资料,并将这些内容添加到了来自 Apress 的专家 Android 版本中。
资源
当您了解并使用 Android 菜单和操作栏时,您可能希望将以下 URL 放在手边:
- :谷歌描述如何使用菜单的主要文档。
developer . Android . com/guide/topics/resources/menu-resource . html:关于菜单资源中可以使用的各种 XML 标签的信息。developer . Android . com/reference/Android/app/ActionBar . html:用于 ActionBar 类的 API URL。- :我们对动作栏的研究,包括进一步的参考资料列表、样本代码、例子的链接,以及代表各种动作栏模式的 UI 图。
- :要设置列表导航模式,你需要了解下拉列表和微调器是如何工作的。这篇简短的文章展示了一些关于如何在 Android 中使用 spinners 的示例和参考链接。
- :解释搜索如何工作,帮助你最大限度地利用动作栏。
- 【http://www.androidicons.com】:本章中用到的几个图标都是从这个网站借来的。这些图标受知识共享许可 3.0 的保护。
www.androidbook.com/item/3302:“讨好安卓布局。”简单布局的一些注释和示例代码。- :你可以在这里找到上一版搜索章节的免费拷贝。这为 Android 搜索提供了广泛的覆盖面。
- 【http://androidbook.com/proandroid5/projects】:本书项目下载网址。本章可下载的项目 ZIP 文件是 pro Android 5 _ ch06 _ test menus . ZIP 和 pro Android 5 _ ch06 _ testactionbar . ZIP。
摘要
菜单和动作栏是编写移动应用不可或缺的一部分。本章包括常规菜单、上下文菜单、弹出菜单、标准动作栏、选项卡式动作栏和基于列表的动作栏。本章还介绍了如何在操作栏中嵌入搜索视图小部件的基础知识。
七、样式和主题
到目前为止,我们已经介绍了 Android 用户界面(UI)的一些基础知识。在这一章中,我们将讨论样式和主题,它们有助于封装控件外观属性,以便于设置和维护。Android 提供了几种改变应用中视图风格的方法,包括 XML 和代码。我们将首先介绍在字符串中使用标记标签,然后介绍如何使用 spannables 来改变文本的特定视觉属性。但是,如果您想对几个视图或整个活动或应用使用一个通用的规范来控制事物的外观,该怎么办呢?我们将讨论 Android 风格和主题,向您展示如何操作。
使用样式
有时,您想要突出显示视图内容的一部分或设置其样式。您可以静态或动态地做到这一点。静态地,你可以将标记直接应用到你的字符串资源中的字符串,如下所示:
<string name="styledText"><i>Static</i> style in a <b>TextView</b>.</string>
然后,您可以在 XML 或代码中引用它。注意,可以对字符串资源使用以下 HTML 标记: < i > 、 < b > 和 < u > ,分别用于斜体、粗体和下划线,以及 < sup > (上标)、 < sub > (下标)、 < strike > (删除线)、你甚至可以嵌套它们来得到,例如,小的上标。这不仅适用于文本视图,也适用于其他视图,比如按钮。图 7-1 展示了样式化和主题化的文本,使用了本节中的许多例子。
图 7-1 。风格和主题示例
以编程方式设计一个 TextView 控件的内容需要一点额外的工作,但是允许更多的灵活性(见清单 7-1 ),因为你可以在运行时设计它。不过,这种灵活性只能应用于 spannable,这就是 EditText 通常管理内部文本的方式,而 TextView 通常不使用 Spannable 。span able 基本上是一个可以应用样式的字符串。要让 TextView 将文本存储为 spannable,可以这样调用 setText :
tv.setText("This text is stored in a Spannable", TextView.BufferType.SPANNABLE);
然后,当你调用 tv.getText 时,你会得到一个 spannable。
如清单 7-1 中的所示,您可以获取 EditText 的内容(作为一个 Spannable 对象),然后为部分文本设置样式。清单中的代码将文本样式设置为粗体和斜体,并将背景设置为红色。您可以使用所有的样式选项,就像我们之前描述的 HTML 标签一样,还可以使用其他选项。
清单 7-1 。将样式动态应用于编辑文本的内容
EditText et =(EditText)this.findViewById(R.id.et);
et.setText("Styling the content of an EditText dynamically");
Spannable spn = (Spannable) et.getText();
spn.setSpan(new BackgroundColorSpan(Color.RED), 0, 7,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spn.setSpan(new StyleSpan(android.graphics.Typeface.BOLD_ITALIC),
0, 7, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
这两种样式化技术只对它们所应用的一个视图有效。Android 提供了一种样式机制来定义跨视图重用的通用样式,还提供了一种主题机制,它基本上将一种样式应用于整个活动或整个应用。首先,我们需要谈谈风格。
一个样式是一个有名称的视图 属性的集合,所以你可以通过它的名称来引用这个集合,并通过名称将那个样式分配给视图。例如,清单 7-2 显示了一个资源 XML 文件,保存在 /res/values 中,我们可以将它用于所有的错误消息。
清单 7-2 。定义在多个视图中使用的样式
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="ErrorText">
<item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textColor">#FF0000</item>
<item name="android:typeface">monospace</item>
</style>
</resources>
定义了视图的大小以及字体颜色(红色)和字样。注意项目标签的名称属性(例如 android:layout_width )是我们在前面章节的布局 XML 文件中使用的 XML 属性名称,并且项目标签的值不再需要双引号。我们现在可以对一个错误使用这种样式 TextView ,如清单 7-3 所示。
清单 7-3 。在视图中使用样式
<TextView android:id="@+id/errorText"
style="@style/ErrorText"
android:text="No errors at this time"
/>
需要注意的是,在这个视图定义中,样式的属性名不是以 android: 开头的。注意这一点,因为除了风格,所有东西似乎都用 android: 。当您的应用中有许多共享一种样式的视图时,在一个地方改变该样式就简单多了;您只需要在一个资源文件中修改样式的属性。
当然,您可以为各种控件创建许多不同的样式。例如,按钮可以共享不同于菜单中文本的通用样式的通用样式。常见的是用样式管理文本属性,包括 android:textColor、android:textStyle、android:textSize。样式使用的其他常见属性包括填充值、android:background 和颜色。
样式的一个非常好的方面是你可以设置它们的层次结构。我们可以为非常糟糕的错误消息定义一种新的样式,并以 ErrorText 的样式为基础。清单 7-4 展示了这可能是什么样子。
清单 7-4 。从父样式定义样式
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="ErrorText.Danger" >
<item name="android:textStyle">bold</item>
</style>
</resources>
这个例子表明,我们可以简单地使用父样式作为新样式名的前缀来命名我们的子样式。因此, ErrorText。危险 是 ErrorText 的子节点,继承了父节点的样式属性。然后为 textStyle 添加一个新属性。这可以一次又一次地重复,以创建一个完整的风格树。
和适配器布局一样,Android 提供了大量我们可以使用的样式。要指定 Android 提供的样式,使用如下语法:
style="@android:style/TextAppearance"
这个样式设置了 Android 中文本的默认样式。要找到主 Android styles.xml 文件,请访问安装 Android SDK 的 Android SDK/platforms//data/RES/values/文件夹;是您想要查看样式的 Android 的特定版本。在这个文件中,您会发现许多现成的样式供您使用或扩展。关于@ Android:style/text appearance 的一个快速说明:这种样式没有设置 android:layout_height 或 android:layout_width ,因此一个视图规范需要比这种样式更多的样式才能正确编译。
这里有一个关于扩展 Android 提供的样式的警告:以前使用前缀的方法不适用于 Android 提供的样式。相反,您必须使用样式标签的父属性,如下所示:
<style name="CustomTextAppearance" parent="@android:style/TextAppearance">
<item ... your extensions go here ... />
</style>
你不必总是在你的视图中引入一个完整的样式。你可以选择借用这种风格的一部分。例如,如果您想将文本视图中的文本颜色设置为系统样式颜色,您可以执行以下操作:
<TextView android:id="@+id/tv2"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:textColor="?android:textColorSecondary"
android:text="@string/hello_world" />
注意,在这个例子中, textColor 属性值的名称以开始。字符代替了 @ 字符。?使用了字符,所以 Android 知道在当前主题中寻找一个样式值。因为我们看到?android ,我们在 android 系统主题中寻找这个样式值。
使用主题
样式的一个问题是,您需要添加一个属性规范 style="@style/... "应用到您希望它应用到的每个视图定义。如果您希望在整个活动或整个应用中应用一些样式元素,您应该使用主题。一个主题实际上只是一种被广泛应用的风格;但就定义主题而言,它就像一种风格。事实上,主题和样式是可以互换的:你可以将主题扩展成样式,也可以将样式称为主题。通常,只有名称给出了一个提示,说明一个样式是用作样式还是主题。
要为活动或应用指定一个主题,您需要为项目的 AndroidManifest.xml 文件中的 <活动> 或 <应用> 标签添加一个属性。代码可能类似于以下代码之一:
<activity android:theme="@style/MyActivityTheme">
<application android:theme="@style/MyApplicationTheme">
<application android:theme="@android:style/Theme.NoTitleBar">
您可以在 Android 提供的样式所在的文件夹中找到 Android 提供的主题,主题位于一个名为 themes.xml 的文件中。当您查看主题文件时,您会看到一大组定义的样式,它们的名称都以主题开头。把最后一句读几遍可能会有好处。换句话说,所有的样式和主题都是类型样式,即使样式名称中有“主题”。你还会注意到,在 Android 提供的主题和样式中,有很多扩展,这就是为什么你最终会有被称为主题的样式。比如 Dialog.AppError 。
参考
以下是一些对您可能希望进一步探索的主题有帮助的参考:
- :与本书相关的可下载项目列表。对于这一章,寻找一个名为 pro Android 5 _ Ch07 _ styles . ZIP 的 ZIP 文件。这个 ZIP 文件包含本章中的所有项目,列在单独的根目录中。还有一个自述。TXT 文件,它准确地描述了如何将项目从这些 ZIP 文件之一导入到您的 IDE 中。
- :Android 风格和主题指南。
摘要
让我们通过快速列举你所学到的风格和主题来结束这一章:
- 样式只是视图属性的集合,便于在视图、活动和应用之间重用。
- 您可以创建自己的样式、使用预定义的样式或扩展现有样式。
- 当主题被应用到一个活动或应用时,你称之为风格。