安卓 4 入门指南(二)
十二、使用选择小部件
在第十一章的中,您看到了如何对字段进行约束以限制可能的输入,比如只能输入数字或只能输入电话号码。这些约束帮助用户在输入信息时“正确无误”,尤其是在键盘狭窄的移动设备上。
当然,约束输入的最终目的是只允许从一组项目中进行选择,比如一组单选按钮。经典的 UI 工具包有列表框、组合框、下拉列表等等,就是为了这个目的。Android 提供了许多相同种类的小工具,加上其他移动设备特别感兴趣的小工具(例如,用于检查保存的照片的Gallery)。
此外,Android 提供了一个灵活的框架来确定这些小部件中哪些选项可用。具体来说,Android 提供了一个数据适配器框架,为选择列表提供了一个公共接口,范围从静态数组到数据库内容。选择视图——用于呈现选项列表的小部件——有一个适配器来提供实际的选项。
适应环境
抽象地说,适配器为多个不同的 API 提供了一个公共接口。更具体地说,在 Android 的情况下,适配器为选择样式的小部件(如列表框)背后的数据模型提供了一个公共接口。Java 接口的这种使用相当普遍(例如,Java/Swing 的模型适配器用于JTable),Java 远不是唯一提供这种抽象的环境(例如,Flex 的 XML 数据绑定框架接受 XML 作为静态数据内联或从互联网检索)。
Android 的适配器不仅负责为选择小部件提供数据列表,还负责将单个数据元素转换成特定的视图,在选择小部件中显示。适配器系统的后一个方面听起来可能有点奇怪,但实际上,它与其他 GUI 工具包覆盖默认显示行为的方式没有什么不同。例如,在 Java/Swing 中,如果您希望一个由JList支持的列表框实际上是一个检查列表(其中每一行都是一个复选框加标签,单击调整复选框的状态),那么您不可避免地要调用 setCellRenderer()来提供您自己的ListCellRenderer,这又会将列表的字符串转换成JCheckBox -plus- JLabel复合小部件。
最容易使用的适配器是ArrayAdapter。您只需将其中一个封装在 Java 数组或java.util.List实例中,您就有了一个全功能的适配器:
String[] items={"this", "is", "a", "really", "silly", "list"}; new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, items);
一种风格的ArrayAdapter构造函数有三个参数:
- 要使用的
Context(通常这将是您的活动实例) - 要使用的视图的资源 ID(如前面示例中所示的内置系统资源 ID)
- 要显示的实际项目数组或列表
默认情况下,ArrayAdapter将对列表中的对象调用toString(),并将这些字符串包装在由提供的资源指定的视图中。android.R.layout.simple_list_item_1只是将这些字符串转换成TextView对象。这些TextView小部件将依次显示在列表、微调器或任何使用这个ArrayAdapter的小部件中。如果你想看看android.R.layout.simple_list_item_1是什么样子,你可以在你的 SDK 安装中找到它的副本——只需搜索simple_list_item_1.xml。
在第十三章中,你将看到如何子类化一个适配器并覆盖行创建,给你更多的控制行如何出现。
淘气和乖孩子的名单
Android 中经典的列表框小部件被称为ListView。在您的布局中包含其中一个,调用setAdapter()提供您的数据和子视图,并通过setOnItemSelectedListener()附加一个监听器来发现选择何时改变。这样,你就有了一个功能齐全的列表框。
但是,如果您的活动由一个列表控制,您可以考虑将您的活动创建为ListActivity的子类,而不是常规的Activity基类。如果你的主视图只是列表,你甚至不需要提供布局— ListActivity会为你构建一个全屏列表。如果您确实想定制布局,您可以,只要您将您的ListView标识为@android:id/list,这样ListActivity就知道哪个小部件是活动的主列表。
例如,这里有一个来自Selection/List示例项目的布局,一个简单的列表,顶部有一个标签显示当前的选择:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:id="@+id/selection" android:layout_width="fill_parent" android:layout_height="wrap_content"/> <ListView android:id="@android:id/list" android:layout_width="fill_parent" android:layout_height="fill_parent" android:drawSelectorOnTop="false" /> </LinearLayout>
配置列表并将列表与标签连接起来的 Java 代码如下:
`public class ListViewDemo extends ListActivity { private TextView selection; private static final String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"};
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); setListAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_1, items)); selection=(TextView)findViewById(R.id.selection); }
public void onListItemClick(ListView parent, View v, int position, long id) { selection.setText(items[position]); } }`
使用ListActivity,您可以通过setListAdapter()设置列表适配器——在这种情况下,提供一个ArrayAdapter来包装一组无意义的字符串。为了找出列表选择何时改变,覆盖onListItemClick()并根据提供的子视图和位置采取适当的步骤——在本例中,用该位置的文本更新标签。结果如图 12–1 所示。
图 12–1。??【listview demo】示例应用
我们的ArrayAdapter、android.R.layout.simple_list_item_1的第二个参数控制行的外观。上例中使用的值提供了标准的 Android 列表行:大字体、大量填充和白色文本。
选择模式
默认情况下,ListView被设置为简单地收集列表条目的点击量。如果您想要一个跟踪用户选择的列表,或者可能是多个选择的列表,ListView也可以处理,但是需要做一些修改。
首先,您需要在 Java 代码中调用ListView上的setChoiceMode()来设置选择模式,提供CHOICE_MODE_SINGLE或CHOICE_MODE_MULTIPLE作为值。你可以通过getListView()从ListActivity得到你的ListView。您也可以通过布局 XML 中的android:choiceMode属性来声明这一点。
然后,不使用android.R.layout.simple_list_item_1作为ArrayAdapter构造函数中列表行的布局,而是需要使用android.R.layout.simple_list_item_single_choice或android.R.layout.simple_list_item_multiple_choice分别用于单选或多选列表。
例如,下面是来自Selection/Checklist示例项目的活动布局:
<?xml version="1.0" encoding="utf-8"?> <ListView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/list" android:layout_width="fill_parent" android:layout_height="fill_parent" android:drawSelectorOnTop="false" android:choiceMode="multipleChoice" />
它是一个全屏的ListView,用android:choiceMode="multipleChoice"属性表示我们想要多选支持。
我们的活动只是在我们的无意义单词列表中使用一个标准的ArrayAdapter,但是使用android.R.layout.simple_list_item_multiple_choice作为行布局:
`package com.commonsware.android.checklist;
import android.os.Bundle; import android.app.ListActivity; import android.widget.ArrayAdapter; import android.widget.ListView;
public class ChecklistDemo extends ListActivity { private static final String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"};
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); setListAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_multiple_choice, items)); } }`
用户在左边看到单词列表,右边有复选框,如图 Figure 12–2 所示。
图 12–2。 多选模式
如果我们愿意,我们可以在我们的ListView上调用getCheckedItemPositions()来找出用户检查了哪些条目,或者我们自己调用setItemChecked()来检查(或取消检查)一个特定的条目。
旋转控制
在 Android 中,Spinner相当于其他工具包中的下拉选择器(例如,Java/Swing 中的JComboBox)。按下键盘上的中央按钮会弹出一个选择对话框,用户可以从中选择一个项目。Spinner基本上提供列表选择功能,而不会占用ListView的所有屏幕空间,代价是额外的点击或屏幕点击来做出改变。
与ListView一样,您通过setAdapter()为数据和子视图提供适配器,并通过setOnItemSelectedListener()为选择挂接一个监听器对象。
如果您想定制显示下拉透视图时使用的视图,您需要配置适配器,而不是Spinner小部件。使用setDropDownViewResource()方法提供要使用的视图的资源 ID。
例如,从Selection/Spinner示例项目中挑选出来的,下面是一个带有Spinner的简单视图的 XML 布局:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:id="@+id/selection" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <Spinner android:id="@+id/spinner" android:layout_width="fill_parent" android:layout_height="wrap_content" android:drawSelectorOnTop="true" /> </LinearLayout>
这是与上一节所示相同的视图,但是用Spinner代替了ListView。Spinner属性android:drawSelectorOnTop控制是否在Spinner UI 右侧的选择器按钮上绘制箭头。
为了填充和使用Spinner,我们需要一些 Java 代码:
`public class SpinnerDemo extends Activity implements AdapterView.OnItemSelectedListener { private TextView selection; private static final String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"};
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); selection=(TextView)findViewById(R.id.selection);
Spinner spin=(Spinner)findViewById(R.id.spinner); spin**.setOnItemSelectedListener**(this);
ArrayAdapter aa=new ArrayAdapter(this, android.R.layout.simple_spinner_item, items);
aa.setDropDownViewResource( android.R.layout.simple_spinner_dropdown_item); spin**.setAdapter**(aa); }
public void onItemSelected(AdapterView<?> parent, View v, int position, long id) { selection**.setText**(items[position]); }
public void onNothingSelected(AdapterView<?> parent) {
selection**.setText**(""); }
}`
这里,我们将活动本身附加为选择监听器(spin.setOnItemSelectedListener(this))。这是因为活动实现了OnItemSelectedListener接口。我们不仅用假词列表来配置适配器,还用特定的资源来用于下拉视图(通过aa.setDropDownViewResource())。还要注意使用android.R.layout.simple_spinner_item作为内置的View来显示微调器中的项目。
最后,我们实现了OnItemSelectedListener所需的回调,以根据用户输入调整选择标签。图 12–3 和 12–4 显示了结果。
图 12–3。 最初启动的 SpinnerDemo 示例应用
图 12–4。 同样的应用,用微调器下拉列表显示
给你的狮子网格(或者类似的东西...)
顾名思义,GridView为您提供了一个可供选择的二维项目网格。您可以适度控制列的数量和大小;行数是根据提供的适配器所说的可供查看的项目数动态确定的。
有几个属性组合在一起决定了列的数量及其大小:
android:numColumns:表示有多少列,或者,如果您提供一个值auto_fit,Android 将根据可用空间和列表中的以下属性计算列数。android:verticalSpacing和android:horizontalSpacing:指示网格中的项目之间应该存在多少空白。android:columnWidth:表示每列应该有多少像素宽。android:stretchMode:对于auto_fit代表android:numColumns的网格,表示任何未被列或间距占据的可用空间应该发生的情况。这可以是columnWidth,让列占据可用空间,或者是spacingWidth,让列之间的空白空间吸收额外的空间。
否则,GridView的工作方式与任何其他选择小部件非常相似——使用setAdapter()来提供数据和子视图,调用setOnItemSelectedListener()来注册选择监听器,等等。
例如,下面是来自Selection/Grid示例项目的 XML 布局,显示了一个GridView配置:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:id="@+id/selection" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <GridView android:id="@+id/grid" android:layout_width="fill_parent" android:layout_height="fill_parent" android:verticalSpacing="40dip" android:horizontalSpacing="5dip" android:numColumns="auto_fit" android:columnWidth="100dip" android:stretchMode="columnWidth" android:gravity="center" /> </LinearLayout>
对于这个网格,除了我们的选择标签所要求的以外,我们占据了整个屏幕。Android ( android:numColumns = "auto_fit")根据我们的水平间距(android:horizontalSpacing = "5dip")和列宽(android:columnWidth = "100dip")计算列数,列吸收任何剩余的“倾斜”宽度(android:stretchMode = "columnWidth")。
配置GridView的 Java 代码如下:
`package com.commonsware.android.grid;
import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.GridView; import android.widget.TextView;
public class GridDemo extends Activity
implements AdapterView.OnItemSelectedListener {
private TextView selection;
private static final String[] items={"lorem", "ipsum", "dolor",
"sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel",
"ligula", "vitae", "arcu", "aliquet", "mollis",
"etiam", "vel", "erat", "placerat", "ante",
"porttitor", "sodales", "pellentesque", "augue", "purus"};
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); selection=(TextView)findViewById(R.id.selection);
GridView g=(GridView) findViewById(R.id.grid); g**.setAdapter**(new ArrayAdapter(this, R.layout.cell, items)); g**.setOnItemSelectedListener**(this); }
public void onItemSelected(AdapterView<?> parent, View v, int position, long id) { selection**.setText**(items[position]); }
public void onNothingSelected(AdapterView<?> parent) { selection**.setText**(""); } }`
网格单元由一个单独的res/layout/cell.xml文件定义,在我们的ArrayAdapter中称为R.layout.cell:
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="14dip" />
通过与 XML 布局(android:verticalSpacing = "40dip")的垂直间距,网格溢出模拟器屏幕的边界,如图图 12–5 和 12–6 所示。
**图 12–5。**GridDemo 示例应用,最初启动时为
图 12–6。 同一个应用,滚动到网格底部
字段:现在减少了 35%的打字量!
AutoCompleteTextView是EditText(场)和Spinner的混合体。使用自动完成功能,当用户键入时,文本被视为前缀过滤器,将输入的文本作为前缀与候选列表进行比较。匹配项显示在从字段下拉的选择列表中(与Spinner一样)。用户可以键入完整的条目(例如,列表中没有的内容),或者从列表中选择一个项目作为字段的值。
AutoCompleteTextView子类EditText,这样你就可以配置所有的标准外观,比如字体和颜色。此外,AutoCompleteTextView有一个android:completionThreshold属性,用来指示用户在列表过滤开始之前必须输入的最少字符数。
您可以通过setAdapter()给AutoCompleteTextView一个包含候选值列表的适配器。然而,由于用户可以输入列表中没有的内容,AutoCompleteTextView不支持选择监听器。相反,您可以注册一个TextWatcher,就像您可以注册任何一个EditText小部件一样,当文本改变时,您会得到通知。这些事件会因为手动键入或从下拉列表中选择而发生。
下面是一个熟悉的 XML 布局,这次包含一个AutoCompleteTextView(来自Selection/AutoComplete示例应用):
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:id="@+id/selection" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <AutoCompleteTextView android:id="@+id/edit" android:layout_width="fill_parent" android:layout_height="wrap_content" android:completionThreshold="3"/> </LinearLayout>
相应的 Java 代码如下:
`package com.commonsware.android.auto;
import android.app.Activity;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView; import android.widget.TextView;
public class AutoCompleteDemo extends Activity implements TextWatcher { private TextView selection; private AutoCompleteTextView edit; private static final String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"};
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); selection=(TextView)findViewById(R.id.selection); edit=(AutoCompleteTextView)findViewById(R.id.edit); edit**.addTextChangedListener**(this);
edit**.setAdapter**(new ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, items)); }
public void onTextChanged(CharSequence s, int start, int before, int count) { selection.setText(edit.getText()); }
public void beforeTextChanged(CharSequence s, int start, int count, int after) { // needed for interface, but not used }
public void afterTextChanged(Editable s) { // needed for interface, but not used } }`
这次,我们的活动实现了TextWatcher,这意味着我们的回调是onTextChanged()、beforeTextChanged()和afterTextChanged()。在这种情况下,我们只对onTextChanged()感兴趣,我们更新选择标签以匹配AutoCompleteTextView的当前内容。图 12–7、12–8 和 12–9 显示了结果。
**图 12–7。**autocompleted emo 示例应用,如同最初启动的
图 12–8。 同一个应用,输入几个匹配的字母后,显示自动完成下拉框
图 12–9。 同样的应用,在自动完成值被选中后
画廊,给予或接受艺术
Gallery小部件在 GUI 工具包中并不常见。实际上,它是一个水平布局的列表框。在水平面上,一个选项接着一个选项,当前选定的项目高亮显示。在 Android 设备上,用户通过左右方向键在选项间旋转。
与ListView相比,Gallery占用更少的屏幕空间,同时仍然可以一次显示多个选项(假设它们足够短)。与Spinner相比,Gallery总是一次显示多个选择。
用于Gallery的典型例子是图像预览。给定一组照片或图标,Gallery让人们在选择一个的过程中预览图片。
就代码而言,Gallery的工作方式很像Spinner或GridView。在 XML 布局中,有几个属性供您使用:
android:spacing:控制列表中条目之间的像素数。android:spinnerSelector:控制用来表示选择的内容。这既可以是对一个Drawable(参见参考资料章节)的引用,也可以是一个用#AARRGGBB或类似符号表示的 RGB 值。
android:drawSelectorOnTop:表示选择条(或Drawable)是画在false之前还是true之后。如果你选择true,确保你的选择器有足够的透明度,通过选择器显示子;否则,用户将无法阅读选择。
十三、喜欢上列表
不起眼的ListView是所有安卓系统中最重要的部件之一,因为它被频繁使用。无论是选择要呼叫的联系人、要转发的电子邮件还是要阅读的电子书,ListView widgets 被广泛应用于各种活动中。当然,如果它们不仅仅是纯文本就好了。
好消息是,安卓列表可以随心所欲,当然是在移动设备屏幕的限制范围内。然而,让它们变得有趣需要一些工作,需要本章中提到的 Android 的特性。
到达一垒
经典的 Android ListView是一个简单的文本列表——坚实但缺乏灵感。基本上,我们把一串单词放在一个数组中交给ListView,并告诉 Android 使用一个简单的内置布局将这些单词放入一个列表中。
然而,我们可以有一个列表,它的行由图标、图标和文本、复选框和文本或者我们想要的任何东西组成。它仅仅是向适配器提供足够的数据,并帮助适配器为每一行创建一组更丰富的View对象。
例如,假设我们想要一个ListView,它的条目由一个图标和一些文本组成。我们可以为行构建一个布局,如下所示,在FancyLists/Static示例项目的res/layout/row.xml中可以找到:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:id="@+id/selection" android:layout_width="fill_parent" android:layout_height="wrap_content"/> <ListView android:id="@android:id/list" android:layout_width="fill_parent" android:layout_height="fill_parent" android:drawSelectorOnTop="false" /> </LinearLayout>
这个布局使用一个LinearLayout来设置一行,图标在左边,文本(用漂亮的大字体)在右边。
然而,在默认情况下,Android 并不知道我们想要将这种布局用于我们的ListView。为了建立连接,我们需要向我们的Adapter提供前面显示的定制布局的资源 ID:
`public class StaticDemo extends ListActivity { private TextView selection; private static final String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"};
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); setListAdapter(new ArrayAdapter(this, R.layout.row, R.id.label, items)); selection=(TextView)findViewById(R.id.selection); }
public void onListItemClick(ListView parent, View v, int position, long id) { selection**.setText**(items[position]); } }`
这遵循前一个ListView样本的一般结构。这里的关键区别是,我们已经告诉ArrayAdapter我们想要使用我们的自定义布局(R.layout.row),而单词应该出现的TextView在自定义布局中被称为R.id.label。
**注意:**记住,要引用一个布局(row.xml),使用R.layout作为布局 XML 文件(R.layout.row)的基本名称的前缀。
结果是一个左侧带有图标的ListView;在这个例子中,所有的图标都是一样的,如图 Figure 13–1 所示。
**图 13–1。**static demo 应用
动态演示
如前一节所示,提供用于行的替代布局的技术非常好地处理了简单的情况。但是,如果我们想让图标根据行数据改变呢?例如,假设我们想对小单词使用一个图标,对大单词使用不同的图标。在ArrayAdapter的例子中,我们需要扩展它,创建我们自己的定制子类(例如,IconicAdapter),合并我们的业务逻辑。特别是,它需要超越getView()。
Adapter的getView()方法是AdapterView(像ListView或Spinner)在需要与Adapter管理的给定数据相关联的View时调用的方法。在使用ArrayAdapter的情况下,根据需要为数组中的每个位置调用getView()——“为第一行获取View,“为第二行获取View”,依此类推。
例如,让我们重新编写上一节中的代码以使用getView(),这样我们可以为不同的行显示不同的图标——在本例中,一个图标代表短词,一个代表长词(来自FancyLists/Dynamic示例项目):
public class DynamicDemo extends ListActivity { TextView selection; private static final String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", ` "ligula", "vitae", "arcu", "aliquet", "mollis",
"etiam", "vel", "erat", "placerat", "ante",
"porttitor", "sodales", "pellentesque", "augue", "purus"};
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); setListAdapter(new IconicAdapter()); selection=(TextView)findViewById(R.id.selection); }
public void onListItemClick(ListView parent, View v, int position, long id) { selection**.setText**(items[position]); }
class IconicAdapter extends ArrayAdapter { IconicAdapter() { super(DynamicDemo.this, R.layout.row, R.id.label, items); }
public View getView(int position, View convertView, ViewGroup parent) { View row=super**.getView**(position, convertView, parent); ImageView icon=(ImageView)row**.findViewById**(R.id.icon);
if (items[position].length()>4) { icon**.setImageResource**(R.drawable.delete); } else { icon**.setImageResource**(R.drawable.ok); }
return(row); } } }`
我们的IconicAdapter——活动的内部类——有两个方法。首先,它有一个构造函数,简单地将我们在StaticDemo的ArrayAdapter构造函数中使用的相同数据传递给ArrayAdapter。第二,它有我们的getView()实现,它做两件事:
- 它链接到超类的实现
getView(),它返回给我们一个由ArrayAdapter准备的行View的实例。特别是,我们的字已经放入了TextView,因为ArrayAdapter通常会这样做。 - 它找到我们的
ImageView,并应用业务规则来设置应该使用哪个图标,引用两个可绘制资源(R.drawable.ok和R.drawable.delete)中的一个。
我们修改后的示例结果如图图 13–2 所示。
图 13–2。??【dynamic demo】应用
给我们自己充气
先前版本的DynamicDemo应用运行良好。然而,有时ArrayAdapter甚至不能用于设置我们行的基础。例如,有可能有一个ListView,其中的行实际上是不同的,比如分类标题散布在常规的行中。在这种情况下,我们可能需要自己做所有的工作,从膨胀我们的行开始。我们将在简要介绍通货膨胀之后再做那件事。
关于通货膨胀的补充报道
“膨胀”指的是将 XML 布局规范转换成 XML 表示的实际的View对象树的行为。这无疑是一段乏味的代码:获取一个元素,创建一个指定的View类的实例,遍历属性,将这些属性转换成属性设置器调用,遍历所有子元素,生成、清洗并重复。
好消息是,Android 团队的优秀人员将所有这些打包成了一个名为LayoutInflater的类,我们可以自己使用它。例如,当涉及到漂亮的列表时,我们希望为列表中显示的每一行增加一个View,这样我们就可以使用 XML 布局的简便简写来描述这些行应该是什么样子。
例如,让我们看一下FancyLists/DynamicEx项目中DynamicDemo类的一个稍微不同的实现:
`public class DynamicDemo extends ListActivity { TextView selection; private static final String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"};
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); setListAdapter(new IconicAdapter()); selection=(TextView)findViewById(R.id.selection); }
public void onListItemClick(ListView parent, View v, int position, long id) { selection**.setText**(items[position]); }
class IconicAdapter extends ArrayAdapter { IconicAdapter() { super(DynamicDemo.this, R.layout.row, items); }
public View getView(int position, View convertView, ViewGroup parent) { LayoutInflater inflater=getLayoutInflater(); View row=inflater**.inflate**(R.layout.row, parent, false); TextView label=(TextView)row**.findViewById**(R.id.label);
label**.setText**(items[position]);
ImageView icon=(ImageView)row**.findViewById**(R.id.icon);
if (items[position].length()>4) { icon**.setImageResource**(R.drawable.delete); } else { icon**.setImageResource**(R.drawable.ok); }
return(row); } } }`
在这里,我们通过使用一个通过getLayoutInflater()从我们的Activity获得的LayoutInflater对象来扩展我们的R.layout.row布局。这给了我们一个View对象,实际上,它是我们的LinearLayout,带有一个ImageView和一个TextView,正如R.layout.row 所指定的。然而,XML 和LayoutInflater为我们处理“重担”,而不是必须自己创建所有这些对象并将它们连接在一起。
现在,回到我们的故事
所以我们用LayoutInflater给我们一个代表行的View。这一行是“空的”,因为静态布局文件不知道实际有什么数据进入这一行。我们的工作是在返回行之前,按照我们认为合适的方式定制和填充行,如下所示:
- 在提供的位置使用单词,为我们的标签小部件填充文本标签
- 查看单词是否超过四个字符,如果是,找到我们的
ImageView图标小部件,用一个不同的替换股票资源
用户看不到任何不同——我们只是改变了这些行的创建方式。显然,这是一个相当不自然的例子,但是您可以看到这种技术可以用于基于任何类型的标准定制行。
更好。更强。更快。
在FancyLists/DynamicEx项目中显示的getView()实现可以工作,但是效率很低。每当用户滚动时,我们必须创建一堆新的View对象来容纳新显示的行。这在开销和感知性能方面都很糟糕。
如果列表看起来很慢,可能会影响用户的即时体验。然而,更有可能的是,由于电池的使用,它会变坏 CPU 的每一点使用都会耗尽电池。垃圾收集器需要做额外的工作来清除我们创建的所有额外的对象,这就更复杂了。所以我们的代码效率越低,手机的电池消耗得越快,用户就越不高兴。我们想要快乐的用户,对吗?
所以,让我们来看看一些技巧,让我们的花哨的ListView小部件更有效率。
使用 convertView
按照惯例,getView()方法接收一个名为convertView的View作为其参数之一。有时候,convertView会是null。在这些情况下,我们需要从头开始创建一个新行View(例如,通过膨胀),就像我们在前面的例子中所做的那样。但是,如果convertView不是null,那么它其实就是我们之前创建的View对象之一!这主要发生在用户滚动ListView的时候。随着新行的出现,Android 将尝试回收滚动到列表另一端的行的视图,以使我们不必从头开始重建它们。
假设我们的每一行都有相同的基本结构,我们可以使用findViewById()来获取组成我们的行的各个小部件并改变它们的内容,然后从getView()返回convertView,而不是创建一个全新的行。例如,下面是前面例子中的getView()实现,现在通过convertView(来自FancyLists/Recycling项目)进行了优化:
`public class RecyclingDemo extends ListActivity { private TextView selection; private static final String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"};
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); setListAdapter(new IconicAdapter()); selection=(TextView)findViewById(R.id.selection); }
public void onListItemClick(ListView parent, View v, int position, long id) { selection**.setText**(items[position]); }
class IconicAdapter extends ArrayAdapter { IconicAdapter() { super(RecyclingDemo.this, R.layout.row, items); }
public View getView(int position, View convertView, ViewGroup parent) { View row=convertView;
if (row==null) { LayoutInflater inflater=getLayoutInflater();
row=inflater**.inflate**(R.layout.row, parent, false); }
TextView label=(TextView)row**.findViewById**(R.id.label);
label**.setText**(items[position]);
ImageView icon=(ImageView)row**.findViewById**(R.id.icon);
if (items[position].length()>4) { icon**.setImageResource**(R.drawable.delete); } else { icon**.setImageResource**(R.drawable.ok); }
return(row); } } }`
在这里,我们检查一下convertView是否是null。如果是这样,我们膨胀我们的行;否则,我们只是重复使用它。在这两种情况下,填充内容(图标图像和文本)的工作是相同的。好处是我们避免了潜在的昂贵的通货膨胀步骤。事实上,根据谷歌在 2010 年谷歌 I|O 大会上引用的统计数据,使用回收的ListView比不使用回收的ListAdapter运行速度快 150%。对于复杂的行,这甚至可能低估了好处。
这不仅更快,而且使用的内存也更少。每个小部件或容器——换句话说,View的每个子类——保存高达 2kB 的数据,这还不包括ImageView小部件中的图像。因此,我们的每一行可能有 6kB 那么大。对于我们的 25 个无意义单词的列表,消耗 150kB 的非循环列表(25 行,每行 6kB)将是低效的,但不是一个大问题。然而,一个包含 1000 个无意义单词的列表会消耗多达 6MB 的内存,这将是一个更大的问题。请记住,您的应用可能只有 16MB 的 Java 堆内存可供使用,特别是当您的目标是资源有限的旧设备时。回收允许我们处理任意长度的列表,只消耗屏幕上可见行所需的View内存。
请注意,只有当我们自己创建行时,行回收才是一个问题。如果我们让ArrayAdapter通过利用getView()的实现来创建行,如FancyLists/Dynamic项目所示,那么它处理回收。
使用固定器模式
另一个有点昂贵的操作是调用findViewById()。这将深入到我们展开的行中,并根据分配给它们的标识符提取小部件,这样我们就可以定制小部件的内容(例如,更改TextView的文本或ImageView中的图标)。由于findViewById()可以在该行的根View的子树中的任何地方找到小部件,这可能需要相当数量的指令来执行,特别是如果我们需要重复找到相同的小部件。
在一些 GUI 工具包中,这个问题是通过让复合的View对象(比如行)完全在程序代码中声明(在本例中是 Java)来避免的。然后,访问单个小部件仅仅是调用一个 getter 或访问一个字段的问题。我们当然可以用 Android 做到这一点,但代码变得相当冗长。最好是一种方法,使我们仍然能够使用布局 XML,同时缓存我们的行的关键子部件,这样我们只需要找到它们一次。这就是 holder 模式发挥作用的地方,在一个我们称为ViewHolder的类中。
所有的View对象都有getTag()和setTag()方法。这些允许我们将任意对象与小部件相关联。holder 模式使用那个“标签”来保存一个对象,该对象又保存每个感兴趣的子部件。通过将该容器附加到行View,每次我们使用该行时,我们已经可以访问我们关心的子部件,而不必再次调用findViewById()。
因此,让我们来看看其中一个 holder 类(取自FancyLists/ViewHolder示例项目):
`package com.commonsware.android.fancylists.five;
import android.view.View; import android.widget.ImageView;
class ViewHolder { ImageView icon=null;
ViewHolder(View base) { this.icon=(ImageView)base**.findViewById**(R.id.icon); } }`
ViewHolder持有子部件,通过其构造函数中的findViewById()初始化。小部件只是受包保护的数据成员,可以从这个项目中的其他类访问,比如一个ViewHolderDemo活动。在这种情况下,我们只持有一个小部件——图标——因为我们将让ArrayAdapter为我们处理标签。
使用ViewHolder就是每当我们膨胀一行时创建一个实例,并通过setTag()将所述实例附加到行View,如在ViewHolderDemo中找到的getView()的重写所示:
`public View getView(int position, View convertView, ViewGroup parent) { View row=super**.getView**(position, convertView, parent); ViewHolder holder=(ViewHolder)row.getTag();
if (holder==null) { holder=new ViewHolder(row); row**.setTag**(holder); }
if (getModel(position).length()>4) { holder.icon**.setImageResource**(R.drawable.delete); } else { holder.icon**.setImageResource**(R.drawable.ok); }
return(row); }`
在这里,我们回到让ArrayAdapter为我们处理我们的行膨胀和回收。如果对行上的getTag()的调用返回null,我们知道我们需要创建一个新的ViewHolder,然后通过setTag()将它附加到行上,供以后重用。然后,访问子部件仅仅是访问容器上的数据成员。第一次显示ListView时,所有新行都需要膨胀,我们最终为每个行创建了一个ViewHolder。当用户滚动时,行被回收,我们可以重用它们对应的ViewHolder小部件缓存。
使用固定器有助于提高性能,但效果并不明显。虽然回收可以让你的性能提高 150%,但增加一个支架可以让性能提高 175%。因此,虽然您可能希望在创建适配器时预先实现回收,但是添加一个容器可能是您以后要处理的事情,当您专门从事性能调优工作时。
在这种特殊情况下,我们当然可以通过跳过ViewHolder并直接使用getTag()和setTag()以及ImageView来简化这一切。这个例子是为了演示如何处理一个更复杂的场景,在这个场景中,您可能有几个小部件需要通过 holder 模式进行缓存。
交互式行
旁边有漂亮图标的列表都很好。但是,我们可以创建行中包含交互式子部件的ListView部件,而不仅仅是像TextView和ImageView这样的被动部件吗?例如,有一个RatingBar小部件,允许用户通过点击一组星形图标来分配评级。我们能不能将RatingBar和文本结合起来,让人们滚动一个列表,比如说,歌曲列表,并在列表中对它们进行评分?有好消息也有坏消息。
好消息是,成排的交互式小部件工作得很好。坏消息是这有点棘手,特别是当交互式小部件的状态改变时(例如,在字段中键入一个值),需要采取行动。我们需要将该状态存储在某个地方,因为当滚动ListView时,我们的RatingBar小部件将被回收。当RatingBar被回收时,我们需要能够基于被查看的实际单词来设置RatingBar状态,并且我们需要在它改变时保存状态,以便当这个特定的行被滚动回视图时它可以被恢复。
有趣的是,默认情况下,RatingBar完全不知道它代表的是ArrayAdapter中的哪一项。毕竟,RatingBar只是一个小部件,在一排ListView中使用。我们需要告诉行它们当前显示的是ArrayAdapter中的哪个项目,这样当它们的RatingBar被选中时,它们就知道要修改哪个项目的状态。
因此,让我们使用FancyLists/RateList示例项目中的活动来看看这是如何完成的。我们将使用与上一个例子中相同的基本类。我们正在显示一个无意义单词的列表,然后可以对其进行评级。此外,获得最高评级的单词全部大写。
`package com.commonsware.android.fancylists.six;
import android.app.Activity;
import android.os.Bundle;
import android.app.ListActivity;
import android.view.View;
import android.view.ViewGroup;
import android.view.LayoutInflater;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.RatingBar;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import java.util.ArrayList; public class RateListDemo extends ListActivity {
private static final String[] items={"lorem", "ipsum", "dolor",
"sit", "amet",
"consectetuer", "adipiscing", "elit", "morbi", "vel",
"ligula", "vitae", "arcu", "aliquet", "mollis",
"etiam", "vel", "erat", "placerat", "ante",
"porttitor", "sodales", "pellentesque", "augue", "purus"};
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle);
ArrayList list=new ArrayList();
for (String s : items) { list.add(new RowModel(s)); }
setListAdapter(new RatingAdapter(list)); }
private RowModel getModel(int position) { return(((RatingAdapter)getListAdapter()).getItem(position)); }
class RatingAdapter extends ArrayAdapter { RatingAdapter(ArrayList list) { super(RateListDemo.this, R.layout.row, R.id.label, list); }
public View getView(int position, View convertView, ViewGroup parent) { View row=super.getView(position, convertView, parent); ViewHolder holder=(ViewHolder)row.getTag();
if (holder==null) { holder=new ViewHolder(row); row.setTag(holder);
RatingBar.OnRatingBarChangeListener l= new RatingBar.OnRatingBarChangeListener() { public void onRatingChanged(RatingBar ratingBar, float rating, boolean fromTouch) { Integer myPosition=(Integer)ratingBar.getTag(); RowModel model=getModel(myPosition);
model.rating=rating;
LinearLayout parent=(LinearLayout)ratingBar.getParent(); TextView label=(TextView)parent.findViewById(R.id.label);
label.setText(model.toString());
}
}; holder.rate.setOnRatingBarChangeListener(l);
}
RowModel model=getModel(position);
holder.rate.setTag(new Integer(position)); holder.rate.setRating(model.rating);
return(row); } }
class RowModel { String label; float rating=2.0f;
RowModel(String label) { this.label=label; }
public String toString() { if (rating>=3.0) { return(label.toUpperCase()); }
return(label); } } }`
以下列表解释了本活动和getView()实施与之前的不同之处:
- 我们仍然使用
String[]项作为无意义单词的列表,但是我们没有将那个String数组直接注入一个ArrayAdapter,而是将它转化为一个RowModel对象的列表。RowModel是可变模型:它保存无意义单词和当前选中状态。在真实的系统中,这些可能是从数据库中填充的对象,并且这些属性将具有更多的业务意义。 - 我们更新了效用方法,比如
onListItemClick(),以反映从纯String模型到使用RowModel的变化。 - 在
getView()中的ArrayAdapter子类(RatingAdapter,让ArrayAdapter膨胀并回收行,然后检查行的标签中是否有ViewHolder。如果没有,我们创建一个新的ViewHolder并将它与该行相关联。对于该行的RatingBar,我们添加了一个匿名的onRatingChanged()监听器,它查看该行的标签(getTag())并将其转换成一个Integer,表示该行在ArrayAdapter中显示的位置。使用它,评级栏可以获得该行的实际RowModel,并根据评级栏的新状态更新模型。选中时,它还会更新与RatingBar相邻的文本,以匹配评级栏状态。 - 我们总是确保
RatingBar有正确的内容,并且有一个标签(通过setTag())指向适配器中行显示的位置。
行布局很简单,一个LinearLayout里面就一个RatingBar和一个TextView:
` <LinearLayout xmlns:android="schemas.android.com/apk/res/and…" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="horizontal"
<RatingBar android:id="@+id/rate" android:layout_width="wrap_content" android:layout_height="wrap_content" android:numStars="3" android:stepSize="1" android:rating="2" /> <TextView android:id="@+id/label" android:padding="2dip" android:textSize="18sp" android:layout_gravity="left|center_vertical" android:layout_width="fill_parent" android:layout_height="wrap_content"/> `
ViewHolder同样简单,只是从行View中提取出RatingBar用于缓存:
`package com.commonsware.android.fancylists.six;
import android.view.View; import android.widget.RatingBar;
class ViewHolder { RatingBar rate=null;
ViewHolder(View base) { this.rate=(RatingBar)base.findViewById(R.id.rate); } }`
从视觉上看,结果就是你所期望的,如图 Figure 13–3 所示。
**图 13–3。**RateListDemo 应用,如同最初启动的
Figure 13–4 显示了一个切换的分级栏,它将单词全部转换成大写字母。
图 13–4。 同一个应用,显示一个置顶词
十四、更多的小部件和容器
到目前为止,这本书已经介绍了许多小部件和容器。本章是最后一章,专门讨论小部件和容器,涵盖了许多流行的选项,从日期和时间小部件到选项卡。后续章节偶尔会介绍新的小部件,但是是在一些其他主题的背景下,比如在第二十章中介绍ProgressBar(涵盖线程)。
挑选
对于像手机这样输入受限的设备,拥有能够感知用户应该输入的内容类型的小工具和对话框是非常有帮助的。它们最大限度地减少了击键和屏幕点击,并减少了用户犯某种错误的机会(例如,在只需要数字的地方输入字母)。
如第九章中的所示,EditText具有输入数字和文本的内容感知功能。Android 还支持小工具(DatePicker和TimePicker)和对话框(DatePickerDialog和TimePickerDialog)来帮助用户输入日期和时间。
DatePicker和DatePickerDialog允许您设置选择的开始日期,以年、月和月中的某一天值的形式。请注意,月份是从一月的0到十二月的11。最重要的是,DatePicker和DatePickerDialog都允许您提供一个回调对象(OnDateChangedListener或OnDateSetListener),以便在用户选择了新日期时通知您。由您决定是否将该日期存储在某个地方,尤其是在使用对话框的情况下,因为您没有其他方法可以在以后访问所选择的日期。
类似地,TimePicker和TimePickerDialog让您执行以下操作:
- 设置用户可以调整的初始时间,以小时(
0到23)和分钟(0到59)的形式 - 指出选择应该是 12 小时模式(带 AM/PM 切换)还是 24 小时模式(在美国被认为是“军事时间”,在世界其他地方被认为是“正常时间”)
- 提供一个回调对象(
OnTimeChangedListener或OnTimeSetListener),当用户选择了一个新的时间时,它会被通知,以小时和分钟的形式提供给你
作为使用日期和时间选取器的一个例子,来自Fancy/Chrono示例项目,这里有一个包含一个标签和两个按钮的简单布局,它将弹出日期和时间选取器风格的对话框:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:id="@+id/dateAndTime" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <Button android:id="@+id/dateBtn" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Set the Date" android:onClick="chooseDate" /> <Button android:id="@+id/timeBtn" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Set the Time" android:onClick="chooseTime" /> </LinearLayout>
更有趣的东西来自 Java 源代码:
`package com.commonsware.android.chrono;
import android.app.Activity; import android.os.Bundle; import android.app.DatePickerDialog; import android.app.TimePickerDialog; import android.view.View; import android.widget.DatePicker; import android.widget.TimePicker; import android.widget.TextView; import java.text.DateFormat; import java.util.Calendar;
public class ChronoDemo extends Activity {
DateFormat fmtDateAndTime=DateFormat.getDateTimeInstance();
TextView dateAndTimeLabel;
Calendar dateAndTime=Calendar.getInstance(); @Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
dateAndTimeLabel=(TextView)findViewById(R.id.dateAndTime);
updateLabel(); }
public void chooseDate(View v) { new DatePickerDialog(ChronoDemo.this, d, dateAndTime.get(Calendar.YEAR), dateAndTime.get(Calendar.MONTH), dateAndTime.get(Calendar.DAY_OF_MONTH)) .show(); }
public void chooseTime(View v) { new TimePickerDialog(ChronoDemo.this, t, dateAndTime.get(Calendar.HOUR_OF_DAY), dateAndTime.get(Calendar.MINUTE), true) .show(); }
private void updateLabel() { dateAndTimeLabel.setText(fmtDateAndTime .format(dateAndTime.getTime())); }
DatePickerDialog.OnDateSetListener d=new DatePickerDialog.OnDateSetListener() { public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) { dateAndTime.set(Calendar.YEAR, year); dateAndTime.set(Calendar.MONTH, monthOfYear); dateAndTime.set(Calendar.DAY_OF_MONTH, dayOfMonth); updateLabel(); } };
TimePickerDialog.OnTimeSetListener t=new TimePickerDialog.OnTimeSetListener() { public void onTimeSet(TimePicker view, int hourOfDay, int minute) { dateAndTime.set(Calendar.HOUR_OF_DAY, hourOfDay); dateAndTime.set(Calendar.MINUTE, minute); updateLabel(); } }; }`
这个活动的模型只是一个Calendar实例,最初设置为当前日期和时间。我们通过一个DateFormat格式化程序将其注入视图。在updateLabel()方法中,我们获取当前的Calendar,对其进行格式化,并将其放入TextView。
每个按钮都有一个对应的方法,当用户点击它时会得到控制(chooseDate()和chooseTime())。点击按钮时,会显示DatePickerDialog或TimePickerDialog。对于DatePickerDialog,我们给它一个OnDateSetListener回调,用新的日期(年、月、日)更新Calendar。我们还给对话框最后选择的日期,从Calendar中获取值。在TimePickerDialog的情况下,它得到一个OnTimeSetListener回调来更新Calendar的时间部分、最后选择的时间和一个值true,该值指示我们想要时间选择器上的 24 小时模式。
将所有这些连接在一起,最终的活动如图 14–1、14–2 和 14–3 所示。
**图 14–1。**chrono demo 示例应用,最初启动时
图 14–2。 同样的应用,显示日期选择器对话框
图 14–3。 同样的应用,显示时间选择器对话框
时间像河流一样不停地流淌
如果您想显示时间,而不是让用户输入时间,您可能希望使用DigitalClock小部件或AnalogClock小部件。这些小部件非常容易使用,因为它们会随着时间的推移自动更新。你所需要做的就是把它们放到你的布局中,让它们做自己的事情。
例如,在Fancy/Clocks示例应用中,有一个包含DigitalClock和AnalogClock的 XML 布局:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" > <AnalogClock android:id="@+id/analog" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_alignParentTop="true" /> <DigitalClock android:id="@+id/digital" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_below="@id/analog" /> </RelativeLayout>
除了生成的存根之外,没有任何 Java 代码,我们可以构建这个项目并获得如图 Figure 14–4 所示的活动。
**图 14–4。**clocks demo 示例应用
如果你正在寻找更多的计时器,Chronometer可能会感兴趣。使用Chronometer,您可以从一个起点开始跟踪经过的时间,如图 14–5 中的示例所示。您只需告诉它何时执行start()和stop(),并可能覆盖显示文本的格式字符串。
图 14–5。 来自 Android SDK 的 Views/Chronometer API 演示
求解析
SeekBar是一个输入小部件,允许用户从一系列可能的值中选择一个值。图 14–6 显示了一个例子。
图 14–6。 来自 Android SDK 的 Views/SeekBar API 演示
用户可以拖动拇指或单击拇指的任一侧来重新定位它。然后拇指指向一个范围内的特定值。这个范围将会是0到某个最大值,默认情况下是100,你可以通过调用setMax()来控制它。你可以通过getProgress()找到当前位置,或者通过setOnSeekBarChangeListener()注册一个监听器找到用户何时改变了拇指的位置。
我们在第十三章的中看到了这个主题的一个变化。
记在我的账上
一般的 Android 哲学是保持活动简短和甜蜜。如果有更多的信息超出了一个屏幕的合理容纳范围,尽管可能需要滚动,那么它可能属于通过Intent开始的另一个活动,正如将在第二十二章中描述的。然而,这可能是复杂的设置。此外,有时确实需要收集大量信息,以作为原子操作进行处理。
在传统的 UI 中,您可能会使用选项卡来收集和显示信息,例如 Java/Swing 中的JTabbedPane。在 Android 中,你现在可以选择以同样的方式使用一个TabHost 容器。活动屏幕的一部分被标签占据,当点击标签时,会换出视图的一部分并用其他内容替换。例如,您可能有一个活动,其中一个选项卡用于输入位置,另一个选项卡用于显示该位置的地图。
一些 GUI 工具包称“标签”为用户点击从一个视图切换到另一个视图的东西。其他 GUI 工具包将“选项卡”称为可点击的按钮状元素和选择该元素时出现的内容的组合。Android 将选项卡按钮和内容视为离散的实体,因此在本节中它们被称为“选项卡按钮”和“选项卡内容”。
片段
您可以使用以下小部件和容器来设置视图的选项卡部分:
TabHost:标签按钮和标签内容的总体容器。TabWidget:实现标签按钮行,包含文本标签和图标(可选)。FrameLayout:标签内容的容器。每个选项卡内容都是FrameLayout的子级。
这类似于 Mozilla 的 XUL 采取的方法。在 XUL 的例子中,tabbox元素对应安卓的TabHost,tabs元素对应TabWidget,tabpanels对应FrameLayout。
例如,下面是一个选项卡式活动的布局定义,来自Fancy/Tab:
<?xml version="1.0" encoding="utf-8"?> <TabHost xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/tabhost" android:layout_width="fill_parent" android:layout_height="fill_parent"> <LinearLayout android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TabWidget android:id="@android:id/tabs" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <FrameLayout android:id="@android:id/tabcontent" android:layout_width="fill_parent" android:layout_height="fill_parent"> <AnalogClock android:id="@+id/tab1" android:layout_width="fill_parent" android:layout_height="fill_parent" /> <Button android:id="@+id/tab2" android:layout_width="fill_parent" android:layout_height="fill_parent" android:text="A semi-random button" /> </FrameLayout> </LinearLayout> </TabHost>
注意,TabWidget和FrameLayout是TabHost的间接子节点,FrameLayout本身有代表不同选项卡的子节点。在本例中,有两个选项卡:一个时钟和一个按钮。在一个更复杂的场景中,选项卡可以是某种形式的容器(例如,LinearLayout),有自己的内容。
将它们连接在一起
您可以将这些小部件放在常规的Activity或TabActivity中。TabActivity和ListActivity一样,将一个通用的 UI 模式(一个完全由选项卡组成的活动)包装成一个模式感知的活动子类。如果你想使用TabActivity,你必须给TabHost一个@android:id/tabhost的android:id。相反,如果您不希望使用TabActivity,您需要通过findViewById()获得TabHost,然后在进行其他操作之前,在TabHost上调用setup()。
其余的 Java 代码需要告诉TabHost哪些视图代表了选项卡内容,以及选项卡按钮应该是什么样子。这些都被包装在TabSpec对象中。您通过newTabSpec()从主机获得一个TabSpec实例,填充它,然后按照正确的顺序将其添加到主机。
TabSpec有两个关键方法:
setContent():表示该标签页的标签内容,通常是选择该标签页时希望显示的视图的android:idsetIndicator():设置标签按钮的标题,在这种方法的某些风格中,提供一个Drawable来表示标签的图标
请注意,如果您需要比简单的标签和可选图标更多的控制,选项卡“指示器”实际上可以是它们自己的视图。
还要注意,在配置任何这些TabSpec对象之前,您必须调用TabHost上的setup()。如果您的活动使用的是TabActivity基类,那么就不需要调用setup()。
例如,下面是将前面布局示例中的选项卡连接在一起的 Java 代码:
`package com.commonsware.android.fancy;
import android.app.Activity; import android.os.Bundle; import android.widget.TabHost;
public class TabDemo extends Activity {
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main); TabHost tabs=(TabHost)findViewById(R.id.tabhost);
tabs.setup();
TabHost.TabSpec spec=tabs.newTabSpec("tag1");
spec.setContent(R.id.tab1); spec.setIndicator("Clock"); tabs.addTab(spec);
spec=tabs.newTabSpec("tag2"); spec.setContent(R.id.tab2); spec.setIndicator("Button"); tabs.addTab(spec); } }`
我们通过熟悉的findViewById()方法找到我们的TabHost,然后通过setup()设置它。之后,我们通过newTabSpec()得到一个TabSpec,提供一个标签,这个标签的目的现在还不知道。给定规范,我们调用setContent()和setIndicator(),然后调用TabHost上的addTab()来注册标签为可用。最后,我们可以通过setCurrentTab()选择显示哪个选项卡,提供基于0的选项卡索引。
结果如图图 14–7 和图 14–8 所示。
**图 14–7。**tab demo 示例应用,显示第一个选项卡
图 14–8。 同样的应用,显示第二个标签页
请注意,如果您的应用运行在较旧的 SDK 级别下,在 Honeycomb 和 Ice Cream Sandwich 发布之前,那么您的菜单将以老式的“按钮”样式出现,如图 Figure 14–9 所示。通过在您的AndroidManifest.xml中指定android:targetSdkVersion和android:minSdkVersion,您可以控制是使用旧行为还是新行为。第二十九章有一个有用的 SDK 版本列表。
**图 14–9。**tab demo 示例应用,显示了第一个带有旧式 UI 的选项卡
相加
TabWidget的设置是为了让你在编译时轻松定义制表符。但是,有时您可能希望在运行时向活动添加选项卡。例如,设想一个电子邮件客户端,它在自己的选项卡中打开每个单独的电子邮件,以便在邮件之间轻松切换。在这种情况下,直到运行时,当用户选择打开一条消息时,您才知道需要多少选项卡或者它们的内容是什么。幸运的是,Android 还支持在运行时动态添加标签。
在运行时动态添加选项卡的工作方式与前面描述的编译时选项卡非常相似,只是您使用了另一种风格的setContent(),它采用了一个TabHost.TabContentFactory实例。这只是一个将被调用的回调。您提供了一个createTabContent()的实现,并使用它来构建和返回成为选项卡内容的View。
我们来看一个例子(Fancy/DynamicTab)。首先,下面是一个活动的布局 XML,它设置了选项卡并定义了一个选项卡,包含一个按钮:
<?xml version="1.0" encoding="utf-8"?> <TabHost xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/tabhost" android:layout_width="fill_parent" android:layout_height="fill_parent"> <LinearLayout android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TabWidget android:id="@android:id/tabs" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <FrameLayout android:id="@android:id/tabcontent" android:layout_width="fill_parent" android:layout_height="fill_parent"> <Button android:id="@+id/buttontab" android:layout_width="fill_parent" android:layout_height="fill_parent" android:text="A semi-random button" android:onClick="addTab" /> </FrameLayout> </LinearLayout> </TabHost>
每当单击按钮时,我们都希望添加新的选项卡,这可以通过下面的代码来实现:
`package com.commonsware.android.dynamictab;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.AnalogClock;
import android.widget.TabHost; public class DynamicTabDemo extends Activity {
private TabHost tabs=null;
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main);
tabs=(TabHost)findViewById(R.id.tabhost); tabs.setup();
TabHost.TabSpec spec=tabs.newTabSpec("buttontab");
spec.setContent(R.id.buttontab); spec.setIndicator("Button"); tabs.addTab(spec); }
public void addTab(View v) { TabHost.TabSpec spec=tabs.newTabSpec("tag1");
spec.setContent(new TabHost.TabContentFactory() { public View createTabContent(String tag) { return(new AnalogClock(DynamicTabDemo.this)); } });
spec.setIndicator("Clock"); tabs.addTab(spec); } }`
在我们的按钮的addTab()回调中,我们创建了一个TabHost.TabSpec对象,并给它一个匿名的TabHost.TabContentFactory。工厂依次返回用于选项卡的View——在本例中,只是一个AnalogClock。构建选项卡的View的逻辑可以更加复杂,比如使用LayoutInflater从布局 XML 构建一个视图。
最初,当活动启动时,我们只有一个选项卡,如图 Figure 14–10 所示。图 14–11 显示了三个动态创建的选项卡。
图 14–10。??【dynamic tab】应用,带单个初始标签
**图 14–11。**dynamic tab 应用,有三个动态创建的选项卡
表格处理是真正动态的,适应你的屏幕大小。Android 将表格格式化,以适应平板电脑甚至电视等更大尺寸的屏幕。图 14–12 在一个更大的平板电脑大小的屏幕上显示了四个动态创建的选项卡。
**图 14–12。**dynamic tab 应用,在平板电脑大小的屏幕上展示适应性
把它们翻过来
有时,您想要选项卡的整体效果(一次只有一些View可见),而不是选项卡的实际 UI 实现。也许标签占据了太多的屏幕空间。也许你想根据一个手势或一个设备摇动来切换视角。或者你只是喜欢与众不同。Android 4.0 冰淇淋三明治提供了在空间允许的情况下,将标签“推”到动作栏的空白空间的能力,例如当你旋转到横向时,但这并不能满足你可能会有的疯狂的“摇晃、拨浪鼓和滚动”想法。
好消息是选项卡的视图翻转逻辑的核心可以在ViewFlipper容器中找到,它可以以传统选项卡之外的其他方式使用。
ViewFlipper继承自FrameLayout,就像我们用它来描述TabWidget的内部一样。然而,最初,ViewFlipper只是显示第一个子视图。您可以通过用户交互手动或通过计时器自动安排视图的翻转。
例如,下面是一个使用Button和ViewFlipper的简单活动(Fancy/Flipper1)的布局:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <Button android:id="@+id/flip_me" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Flip Me!" android:onClick="flip" /> <ViewFlipper android:id="@+id/details" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:textStyle="bold" android:textColor="#FF00FF00" android:text="This is the first panel" /> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:textStyle="bold" android:textColor="#FFFF0000" android:text="This is the second panel" /> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:textStyle="bold" android:textColor="#FFFFFF00" android:text="This is the third panel" /> </ViewFlipper> </LinearLayout>
注意,布局为ViewFlipper定义了三个子视图,每个视图都是一条简单的消息。当然,如果你愿意,你可以有非常复杂的孩子视图。
要手动翻转视图,我们需要挂入Button并在点击按钮时自己翻转它们:
`package com.commonsware.android.flipper1;
import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.ViewFlipper;
public class FlipperDemo extends Activity { ViewFlipper flipper;
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main);
flipper=(ViewFlipper)findViewById(R.id.details); }
public void flip(View v) { flipper.showNext(); } }`
这只是在ViewFlipper上调用showNext()的问题,就像在任何ViewAnimator类上一样。结果是一个简单的活动:单击按钮,显示序列中的下一个TextView,在查看完最后一个后返回到第一个,如图图 14–13 和 14–14 所示。
**图 14–13。**flipper demo 应用,显示第一个面板
图 14–14。 同样的应用,切换到第二个面板后
当然,这可以通过使用一个TextView并在每次点击时改变文本和颜色来更简单地处理。然而,您可以想象一下,ViewFlipper的内容可能更复杂,比如您可能放入TabView的内容。
与TabWidget一样,有时在编译时可能不知道ViewFlipper的内容。和TabWidget一样,你可以轻松地随时添加新内容。
例如,让我们看看另一个示例活动(Fancy/Flipper2),使用以下布局:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <ViewFlipper android:id="@+id/details" android:layout_width="fill_parent" android:layout_height="fill_parent" > </ViewFlipper> </LinearLayout>
注意,ViewFlipper在编译时没有内容。还要注意,没有用于在内容之间翻转的Button——稍后会详细介绍。
对于ViewFlipper内容,我们将创建大的Button小部件,每个小部件包含本书许多章节中使用的一个随机单词。并且,我们将设置ViewFlipper在Button部件之间自动旋转。
`package com.commonsware.android.flipper2;
import android.app.Activity; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ViewFlipper;
public class FlipperDemo2 extends Activity { static String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"}; ViewFlipper flipper;
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main);
flipper=(ViewFlipper)findViewById(R.id.details);
for (String item : items) { Button btn=new Button(this);
btn.setText(item);
flipper.addView(btn, new ViewGroup.LayoutParams( ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); }
flipper.setFlipInterval(2000); flipper.startFlipping(); } }`
在迭代完时髦的单词后,将每个单词变成一个Button,并将Button添加为ViewFlipper的子单词,我们设置 flipper 在子单词(flipper.setFlipInterval(2000);)之间自动翻转,并开始翻转(flipper.startFlipping( )。
结果是一系列无止境的按钮,每个按钮都会出现,如图图 14–15 所示,然后在 2 秒钟后被下一个按钮依次替换,在最后一个按钮出现后返回到第一个按钮。
**图 14–15。**flipper demo 2 应用
自动滑动ViewFlipper对于状态面板或其他有大量信息要显示但没有足够空间显示的情况很有用。然而,由于它自动在视图之间切换,期望用户与单个视图交互是冒险的,因为视图可能会在交互过程中中途切换。
进入某人的抽屉
很长一段时间以来,Android 开发人员渴望有一个滑动抽屉容器,像主屏幕上那样工作,包含启动应用的图标。官方实现在开源代码中,但不是 SDK 的一部分,直到 Android 1.5,开发者发布了SlidingDrawer供其他人使用。
与大多数其他 Android 容器不同,SlidingDrawer可以移动,从关闭位置切换到打开位置。这就对哪个容器可以容纳SlidingDrawer提出了一些限制。它需要放在一个容器中,允许多个小部件相互叠加。RelativeLayout和FrameLayout满足这个要求。FrameLayout是一个纯粹的容器,用来堆叠小部件。另一方面,LinearLayout不允许小部件堆叠(它们在一行或一列中一个接一个地落下),所以你不应该将SlidingDrawer作为LinearLayout的直接子元素。
这里是一个布局,显示了来自Fancy/DrawerDemo项目的FrameLayout中的SlidingDrawer:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#FF4444CC" > <SlidingDrawer android:id="@+id/drawer" android:layout_width="fill_parent" android:layout_height="fill_parent" android:handle="@+id/handle" android:content="@+id/content"> <ImageView android:id="@id/handle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/tray_handle_normal" /> <Button android:id="@id/content" android:layout_width="fill_parent" android:layout_height="fill_parent" android:text="I'm in here!" /> </SlidingDrawer> </FrameLayout>
SlidingDrawer应该包含两件事:
- 一个句柄,通常是一个
ImageView或类似的东西,比如这里使用的,来自 Android 开源项目 - 抽屉本身的内容,通常是某种容器,但在本例中是一个
Button
此外,SlidingDrawer需要知道句柄和内容的android:id值,分别通过android:handle和android:content属性。这些告诉抽屉如何在滑动打开和关闭时显示动画。
图 14–16 显示了SlidingDrawer关闭时的样子,使用提供的手柄,图 14–17 显示了打开时的样子。
图 14–16。 抽屉滑动,关闭
图 14–17。 一个滑动抽屉,打开
正如您所料,您可以通过 Java 代码以及用户触摸事件来打开和关闭抽屉。然而,你有两组这样的方法:一组是即时发生的(open()、close()和toggle()),另一组是使用动画的(animateOpen()、animateClose()和animateToggle())。你也可以lock()和unlock()抽屉;锁定时,抽屉不会响应触摸事件。
如果愿意,您还可以注册三种类型的回调:
- 抽屉打开时要调用的侦听器
- 抽屉关闭时要调用的侦听器
- 当抽屉“滚动”(即用户拖动或投掷手柄)时调用的侦听器
例如,Android launcher 的SlidingDrawer将手柄上的图标从打开切换到关闭再切换到“删除”(如果你长按桌面上的某个东西)。它实现这一点,部分是通过像这样的回调。
SlidingDrawer可以是垂直的也可以是水平的。不过,请注意,不管屏幕方向如何,它都会保持自己的方向。换句话说,如果你旋转运行DrawerDemo的 Android 设备或模拟器,抽屉总是从底部打开——它并不总是“粘”在它打开的原始一侧。这意味着如果你希望抽屉总是从同一边打开,就像启动器一样,你将需要单独的纵向和横向布局,这是在第二十三章中讨论的话题。
其他好东西
Android 提供AbsoluteLayout,内容根据具体坐标位置进行布局。你告诉AbsoluteLayout在精确的 x 和 y 坐标上把一个孩子放在哪里,Android 就放在那里,不问任何问题。从好的方面来说,这给了你精确的定位。不利的一面是,这意味着您的视图只能在特定尺寸的屏幕上看起来合适,或者您需要编写一堆代码来根据屏幕大小调整坐标。由于 Android 屏幕可能会有各种尺寸,新尺寸会定期出现,使用AbsoluteLayout可能会变得相当烦人。另外,请注意AbsoluteLayout已被正式否决,这意味着尽管您可以使用它,但不鼓励使用它。
安卓也有ExpandableListView。这提供了简化的树表示,支持两个深度级别:组和孩子。群组包含孩子;孩子是树的“叶子”。这需要一组新的适配器,因为ListAdapter系列没有为列表中的项目提供任何类型的组信息。
除了本书中提到的,Android 中还有一些其他可用的小部件:
CheckedTextView:一个TextView,旁边可以有一个复选框或单选按钮,用于单选和多选列表Chronometer:秒表式倒计时器Gallery:水平滚动选择小工具,设计用于图像的缩略图预览(例如,相机照片和相册封面)MultiAutoCompleteTextView:类似于AutoCompleteTextView,除了用户可以从下拉列表中进行多项选择,而不是只有一项QuickContactBadge:给定来自用户联系人数据库的联系人的身份,显示代表将对该联系人执行的动作(拨打电话、发送文本消息、发送电子邮件等)的图标列表。)ToggleButton:两种状态的按钮,用“灯”和散文(“开”、“关”)来表示状态,而不是复选标记ViewSwitcher(以及ImageSwitcher和TextSwitcher子类):像一个简化的ViewFlipper,用于在两个视图之间切换
十五、嵌入 WebKit 浏览器
其他 GUI 工具包允许您使用 HTML 来呈现信息,从有限的 HTML 呈现器(例如 Java/Swing 和 wxWidgets)到将 Internet Explorer 嵌入。NET 应用。Android 也是如此,你可以将内置的网络浏览器作为一个小部件嵌入到你自己的活动中,用于显示 HTML 或全面的浏览。Android 浏览器基于 WebKit,与苹果的 Safari 和谷歌的 Chrome 等网络浏览器使用的引擎相同。
Android 浏览器足够复杂,它有自己的 Java 包(android.webkit)。根据您的需求,使用WebView widgetitself 可以是简单的,也可以是强大的。
浏览器,小字体
对于简单的东西来说,WebView与 Android 中的任何其他小部件没有明显的不同——将它弹出到一个布局中,通过 Java 代码告诉它要导航到哪个 URL,然后就完成了。
例如,下面是一个带有WebView(来自WebKit/Browser1)的简单布局:
<?xml version="1.0" encoding="utf-8"?> <WebViewxmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/webkit" android:layout_width="fill_parent" android:layout_height="fill_parent" />
与任何其他小部件一样,您需要告诉它应该如何填充布局中的空间(在这种情况下,它填充所有剩余的空间)。
Java 代码同样简单:
`package com.commonsware.android.browser1;
importandroid.app.Activity; importandroid.os.Bundle;
importandroid.webkit.WebView;
public class BrowserDemo1 extends Activity { WebView browser;
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); browser=(WebView)findViewById(R.id.webkit);
browser.loadUrl("commonsware.com"); } }`
这个版本的onCreate()唯一不同寻常的地方是我们调用了WebView小部件上的loadUrl(),告诉它加载一个网页(在这个例子中,是某个随机公司的主页)。
然而,我们还需要对AndroidManifest.xml做一个修改,请求访问互联网的许可:
<?xml version="1.0"?> <manifestxmlns:android="http://schemas.android.com/apk/res/android" package="com.commonsware.android.browser1"> <uses-permission android:name="android.permission.INTERNET"/> <applicationandroid:icon="@drawable/cw"> <activityandroid:name=".BrowserDemo1" android:label="BrowserDemo1"> <intent-filter> <actionandroid:name="android.intent.action.MAIN"/> <categoryandroid:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> <supports-screens android:largeScreens="true" android:normalScreens="true" android:smallScreens="true" android:anyDensity="true"/> </manifest>
如果我们未能添加此权限,浏览器将拒绝加载页面。权限将在第三十八章中详细介绍。
最终的活动看起来像一个网络浏览器,但是带有隐藏的滚动条,如图 Figure 15–1 所示。
图 15–1。??【browser demo 1 示例应用】??
与常规的 Android 浏览器一样,你可以通过拖动它来浏览页面,而 D-pad 可以让你浏览页面上所有可聚焦的元素。缺少的是组成网络浏览器的所有额外的东西,比如导航工具栏。
现在,您可能想用依赖于 JavaScript 的东西来替换源代码中的 URL,比如 Google 的主页。默认情况下,JavaScript 在WebView小部件中是关闭的。如果您想启用 JavaScript,在WebView实例上调用getSettings().setJavaScriptEnabled(true);。本章稍后会更详细地介绍这个选项。
装载完毕
将内容放入WebView有两种主要方式。一种方法是为浏览器提供一个 URL,并让浏览器通过loadUrl()显示该页面。浏览器将通过特定设备当前可用的任何方式(Wi-Fi、2G、3G、4G、WiMAX、EDGE、HSDPA、HSPA、训练有素的小信鸽等)访问互联网。).
另一种方法是使用loadData()。在这里,您提供 HTML 供浏览器查看。您可以使用它来执行以下操作:
- 显示作为应用包文件安装的手册
- 显示作为其他处理的一部分检索到的 HTML 片段,例如 Atom 提要中的条目描述
- 使用 HTML 生成一个完整的用户界面,而不是使用 Android widget 集
loadData()有两种口味。更简单的方法允许您以字符串的形式提供内容、MIME 类型和编码。通常,对于普通的 HTML,你的 MIME 类型是text/html,你的编码是UTF-8。
例如,您可以用以下代码替换前面示例中的loadUrl()调用:
browser.loadData("<html><body>Hello, world!</body></html>", "text/html", "UTF-8");
您将得到如图图 15–2 所示的结果。
**图 15–2。**browser demo 2 示例应用
这也是一个完全可构建的示例,如WebKit/Browser2。
在水中航行
如前所述,WebView小部件没有导航工具栏。这允许你在这样的工具栏毫无意义和浪费屏幕空间的地方使用它。也就是说,如果你想提供导航功能,你可以,但你必须提供用户界面。
WebView提供了执行普通浏览器导航的方法,包括以下方法:
reload():刷新当前查看的网页goBack():在浏览器历史中后退一步canGoBack():确定是否有任何历史可以返回goForward():在浏览器历史中前进一步canGoForward():确定是否有任何要前进的历史goBackOrForward():在浏览器历史中后退或前进,负数作为参数表示后退多少步,正数表示前进多少步canGoBackOrForward():确定浏览器是否可以后退或前进指定的步数(遵循与goBackOrForward()相同的正/负约定)clearCache():清除浏览器资源缓存clearHistory():清除浏览历史
招待客户
如果你打算将WebView用作本地 UI(而不是浏览网页),你将希望能够在关键时刻获得控制权,尤其是当用户点击链接时。你需要确保这些链接得到正确的处理,要么将你自己的内容加载回WebView,要么向 Android 提交一个Intent以在一个完整的浏览器中打开 URL,要么通过其他方式(见第二十二章)。
您对WebView活动的挂钩是通过setWebViewClient()实现的,它将一个WebViewClient实现的实例作为参数。所提供的回调对象将被通知各种各样的事件,从页面的部分被检索时开始(onPageStarted()等等)。)到当您作为主机应用需要处理某些用户或环境发起的事件时,例如onTooManyRedirects()或onReceivedHttpAuthRequest()。
一个常见的钩子是shouldOverrideUrlLoading(),在这里你的回调被传递一个 URL(加上WebView本身),如果你要处理请求,你返回true,或者如果你想要默认处理,你返回false(例如,实际获取 URL 引用的网页)。例如,对于提要阅读器应用,您可能没有内置导航功能的完整浏览器。在这种情况下,如果用户点击一个 URL,你可能想使用一个Intent来请求 Android 在一个完整的浏览器中加载那个页面。但是如果你在 HTML 中插入了一个“假”的 URL,代表了一些活动提供的内容的链接,你可以自己更新WebView。
举个例子,让我们修改第一个浏览器演示,使它成为一个点击后显示当前时间的应用。从WebKit/Browser3开始,这里是修改后的 Java:
`public class BrowserDemo3 extends Activity { WebView browser;
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); browser=(WebView)findViewById(R.id.webkit); browser.setWebViewClient(new Callback());
loadTime(); }
void loadTime() { String page="<a href="clock">" +new Date().toString() +"";
browser.loadData(page, "text/html", "UTF-8"); }
private class Callback extends WebViewClient { public boolean shouldOverrideUrlLoading(WebView view, String url) { loadTime();
return(true); } } }`
这里,我们在浏览器(loadTime())中加载一个简单的网页,其中包含当前时间,并制作成一个到/clock URL 的超链接。我们还附加了一个WebViewClient子类的实例,提供了我们的shouldOverrideUrlLoading()实现。在这种情况下,不管 URL 是什么,我们只想通过loadTime()重新加载WebView。
运行该活动会产生如图 Figure 15–3 所示的结果。
图 15–3。??【browser demo 3 示例应用】??
选择该链接并单击 D-pad 中心按钮将“单击”该链接,从而使用新时间重建页面。
设置、首选项和选项(哦,天哪!)
有了你最喜欢的桌面网络浏览器,你就有了某种设置、偏好或选项窗口。在这个和工具栏控件之间,你可以调整和旋转浏览器的行为,从偏好的字体到 JavaScript 的行为。类似地,您可以通过调用小部件的getSettings()方法返回的WebSettings实例,调整您的WebView小部件的设置。
在WebSettings上有很多选项可以玩。大多数看起来相当深奥(例如setFantasyFontFamily())。然而,这里有一些你可能会发现更有用的:
- 通过
setDefaultFontSize()(使用磅值)或setTextZoom()(使用常量表示相对大小,如LARGER和SMALLEST)控制字体大小 - 通过
setJavaScriptEnabled()(彻底禁用它)和setJavaScriptCanOpenWindowsAutomatically()(仅仅阻止它打开弹出窗口)控制 JavaScript - 通过
setUserAgent()控制 web 站点的呈现,这样您就可以提供自己的用户代理字符串,让 web 服务器认为您是一个桌面浏览器、另一个移动设备(例如,iPhone)或者其他什么 - 你改变的设置不是持久的,所以如果你允许你的用户决定设置,你应该把它们存储在某个地方(比如通过 Android 偏好引擎),而不是硬连接到你的应用中。
十六、应用菜单
像桌面应用和一些移动操作系统一样,Android 支持应用菜单的活动。大部分安卓手机都有弹出菜单的专用菜单键;其他设备提供了触发菜单出现的替代方法,如 Archos 5 Android 平板电脑使用的屏幕按钮。
此外,与许多 GUI 工具包一样,您可以为 Android 应用创建上下文菜单。在传统的 GUI 上,用户点击鼠标右键可能会触发上下文菜单。在移动设备上,当用户点击并按住特定的小部件时,通常会出现上下文菜单。例如,如果一个TextView有一个上下文菜单,并且该设备是为基于手指的触摸输入而设计的,那么你可以用手指按下TextView,按住一两秒钟,就会出现一个弹出菜单。
菜单的口味
Android 将前一节描述的两种类型的菜单称为选项菜单和上下文菜单。选项菜单是通过按下设备上的硬件菜单按钮来触发的,而上下文菜单是通过点击并按住显示菜单的小工具来启动的。
此外,选项菜单以两种模式之一运行:图标或展开。当用户第一次按下菜单按钮时,将出现图标模式,在屏幕底部的网格中以手指友好的大按钮形式显示前六个菜单选项。如果菜单有六个以上的选项,第六个按钮将标记为“更多”。点击更多选项将调出扩展模式,显示常规菜单中不可见的其余选项。该菜单是可滚动的,因此用户可以滚动到任何菜单选项。
菜单的选项
你需要实现onCreateOptionsMenu(),而不是在onCreate()期间构建活动的选项菜单,这是你连接 UI 其余部分的方式。这个回调接收一个Menu的实例。
你应该做的第一件事是链接到超类(super.onCreateOptionsMenu(menu)),这样 Android 框架就可以添加任何它认为必要的菜单选项。然后,您可以着手添加您自己的选项,如本节所述。
如果您需要在活动的使用过程中调整菜单(例如,禁用现在无效的菜单选项),只需保留在onCreateOptionsMenu()中收到的Menu实例。或者,您可以实现onPrepareOptionsMenu(),它在每次请求显示菜单之前被调用。
假设您已经通过onCreateOptionsMenu()收到了一个Menu对象,您可以通过调用add()来添加菜单选项。这种方法有多种形式,需要以下参数的某种组合:
- 一个组标识符(
int),它应该是NONE,除非你正在创建一个特定的菜单选项组,用于setGroupCheckable()(稍后描述) - 一个选项标识符(也是一个
int),用于在选择菜单选项时在onOptionsItemSelected()回调中标识该选项 - 一个订单标识符(另一个
int),用于指示如果菜单中有 Android 提供的选项和你自己的选项,那么这个菜单选项应该放在哪里;现在,就用NONE - 菜单选项的文本,如
String或资源 ID
add()系列方法都返回一个MenuItem的实例,您可以在其中调整已经设置的任何菜单项设置(例如,菜单选项的文本)。
您还可以设置菜单选项的快捷键,这些快捷键是单字符助记符,当菜单可见时,它们会选择菜单项。Android 支持一组字母快捷键和一组数字快捷键。这些分别通过调用setAlphabeticShortcut()和setNumericShortcut()来单独设置。通过使用true参数调用菜单上的setQwertyMode(),菜单进入字母快捷方式。
选项和组标识符是用于解锁附加菜单功能的按键,如下所示:
- 使用选项标识符调用
MenuItem#setCheckable(),以控制菜单选项是否在标题旁边有一个双态复选框,当用户选择该菜单项时,复选框值被切换 - 用组标识调用
Menu#setGroupCheckable(),将一组菜单选项变成相互排斥的单选按钮,这样在任何时候组中只能有一项处于选中状态
您可以通过调用addSubMenu(),提供与addMenu()相同的参数来创建弹出子菜单。Android 最终会调用onCreatePanelMenu(),传递给它子菜单的选择标识符,以及另一个代表子菜单本身的Menu实例。与onCreateOptionsMenu()一样,您应该向上链接到超类,然后将菜单选项添加到子菜单中。一个限制是不能无限嵌套子菜单,一个菜单可以有子菜单,但是子菜单不能有子菜单。
最后,您甚至可以将菜单项推到操作栏中,这使您的用户更容易发现您的选项,更重要的是,更好地利用平板电脑和更大设备上的所有可用屏幕空间。当我们关注动作栏本身时,我们将在第二十七章中更深入地探讨这个功能。
如果用户选择了一个菜单,那么您的活动将会通过onOptionsItemSelected()回调得到一个菜单被选中的通知。您将获得与所选菜单选项相对应的MenuItem对象。一个典型的模式是对菜单 ID ( item.getItemId())进行switch(),并采取适当的行为。请注意,无论选择的菜单项是在基本菜单还是子菜单中,都会使用onOptionsItemSelected()。
上下文中的菜单
总的来说,上下文菜单和选项菜单使用相同的元素。两个主要的区别是你如何填充菜单和你如何被告知菜单选择。
首先,您需要指出活动中的哪个或哪些小部件有上下文菜单。为此,从活动中调用registerForContextMenu(),提供需要上下文菜单的小部件View。
接下来,您需要实现onCreateContextMenu(),它通过您在registerForContextMenu()中提供的View传递。假设您的活动有多个菜单,您可以使用它来决定构建哪个菜单。
onCreateContextMenu()方法获得了ContextMenu本身、与上下文菜单相关联的View以及一个ContextMenu.ContextMenuInfo,它告诉您用户点击并按住了列表中的哪个项目,以防您想要基于该信息定制上下文菜单。例如,您可以根据项目的当前状态切换可检查的菜单选项。
值得注意的是,每次请求上下文菜单时都会调用onCreateContextMenu()。与选项菜单(每个活动只构建一次)不同,上下文菜单在使用或取消后会被丢弃。因此,您不想保留提供的ContextMenu对象;您只需根据用户的操作,根据需求重新构建菜单,以满足您的活动需求。
要找出何时选择了上下文菜单选项,请在活动上实现onContextItemSelected()。注意,您只获得在这个回调中选择的MenuItem实例。因此,如果您的活动有两个或更多的上下文菜单,您可能希望确保它们的所有选择都有唯一的菜单项标识符,这样您就可以在这个回调中区分它们。还有,你可以在MenuItem上呼叫getMenuInfo()来获得你在onCreateContextMenu()收到的ContextMenu.ContextMenuInfo。否则,这个回调的行为与前面部分描述的onOptionsItemSelected()相同。
偷看一眼
在示例项目Menus/Menus中,您会发现带有相关菜单的ListView示例(List)的修改版本。由于菜单不影响布局,XML 布局文件不需要更改,因此在此不再重印。然而,Java 代码有一些新的行为:
`packagecom.commonsware.android.menus;
importandroid.app.AlertDialog; importandroid.app.ListActivity; importandroid.content.DialogInterface; importandroid.os.Bundle; importandroid.view.ContextMenu; importandroid.view.Menu; importandroid.view.MenuItem; importandroid.view.View; importandroid.widget.AdapterView; importandroid.widget.ArrayAdapter; importandroid.widget.EditText; importandroid.widget.ListView; importandroid.widget.TextView; importjava.util.ArrayList;
public class MenuDemo extends ListActivity { private static final String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"}; public static final int MENU_ADD = Menu.FIRST+1; public static final int MENU_RESET = Menu.FIRST+2; public static final int MENU_CAP = Menu.FIRST+3; public static final int MENU_REMOVE = Menu.FIRST+4 ; private ArrayList words=null;
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle);
initAdapter();
registerForContextMenu(getListView());
} @Override
public boolean onCreateOptionsMenu(Menu menu) {
menu
.add(Menu.NONE, MENU_ADD, Menu.NONE, "Add")
.setIcon(R.drawable.ic_menu_add);
menu
.add(Menu.NONE, MENU_RESET, Menu.NONE, "Reset")
.setIcon(R.drawable.ic_menu_refresh);
return(super.onCreateOptionsMenu(menu)); }
@Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { menu.add(Menu.NONE, MENU_CAP, Menu.NONE, "Capitalize"); menu.add(Menu.NONE, MENU_REMOVE, Menu.NONE, "Remove"); }
@Override public booleanon OptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case MENU_ADD: add(); return(true);
case MENU_RESET: initAdapter(); return(true); }
return(super.onOptionsItemSelected(item)); }
@Override public boolean onContextItemSelected(MenuItem item) { AdapterView.AdapterContextMenuInfo info= (AdapterView.AdapterContextMenuInfo)item.getMenuInfo(); ArrayAdapter adapter=(ArrayAdapter)getListAdapter();
switch (item.getItemId()) { case MENU_CAP: String word=words.get(info.position);
word=word.toUpperCase();
adapter.remove(words.get(info.position)); adapter.insert(word, info.position);
return(true);
case MENU_REMOVE: adapter.remove(words.get(info.position));
return(true);
} return(super.onContextItemSelected(item));
}
private void initAdapter() { words=new ArrayList();
for (String s : items) { words.add(s); }
setListAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_1, words)); }
private void add() { final View addView=getLayoutInflater().inflate(R.layout.add, null);
newAlertDialog.Builder(this) .setTitle("Add a Word") .setView(addView) .setPositiveButton("OK", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { ArrayAdapter adapter=(ArrayAdapter)getListAdapter(); EditText title=(EditText)addView.findViewById(R.id.title);
adapter.add(title.getText().toString()); } }) .setNegativeButton("Cancel", null) .show(); } }`
在onCreate()中,我们将ListView小部件注册为具有上下文菜单。我们还将加载适配器委托给一个initAdapter()私有方法,该方法将数据从我们的静态String数组中复制出来,并将其倒入一个ArrayList,对ArrayAdapter使用ArrayList。我们这样做的原因是我们希望能够动态地改变列表的内容,如果我们使用一个ArrayList而不是一个普通的String数组,这就容易多了。
对于选项菜单,我们覆盖了onCreateOptionsMenu()并添加了两个菜单项,一个向列表中添加新单词,另一个将单词重置为初始状态。这些菜单项的 id 在本地被定义为静态数据成员(MENU_ADD和MENU_RESET),它们还带有从 Android 开源项目中复制的图标。如果用户显示菜单,看起来如图图 16–1 所示。
**图 16–1。**MenuDemo 示例应用及其选项菜单
我们还覆盖了onOptionsItemSelected(),如果用户从菜单中做出选择,就会调用它。提供的MenuItem有一个getItemId()方法,应该映射到MENU_ADD或MENU_RESET。在MENU_ADD的情况下,我们调用一个私有的add()方法,该方法显示一个AlertDialog,它的内容是一个自定义的View,从res/layout/add.xml开始膨胀:
<?xml version="1.0" encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="wrap_content" > <TextView android:text="Word:" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <EditText android:id="@+id/title" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="4dip" /> </LinearLayout>
这将产生一个类似于图 16–2 所示的对话框。
图 16–2。 同样的应用,显示出的添加单词对话框
如果用户点击 OK 按钮,我们得到我们的ArrayAdapter并在上面调用add(),将输入的单词添加到列表的末尾。
如果用户选择了MENU_RESET,我们再次调用initAdapter(),建立一个新的ArrayAdapter,并将其附加到我们的ListActivity。
对于上下文菜单,我们覆盖了onCreateContextMenu()。我们再一次用本地 id 定义了一对菜单项,MENU_CAP(大写长点击的单词)和MENU_REMOVE(删除单词)。因为上下文菜单没有图标,我们可以跳过这一部分。如果用户长按一个单词,就会得到如图图 16–3 所示的上下文菜单。
图 16–3。 同样的应用,显示出的快捷菜单
我们也覆盖了onContextMenuSelected()。由于这是一个ListView的上下文菜单,我们的MenuItem为我们提供了一些额外的信息——特别是,列表中哪个项目被长时间点击了。为此,我们在MenuItem上调用getMenuInfo(),并将结果转换为AdapterView.AdapterContextMenuInfo。该对象又有一个位置数据成员,它是用户选择的单词在数组中的索引。从那里,我们按照要求,用我们的ArrayAdapter来大写或删除这个单词。
更多通货膨胀
第十三章解释了如何通过 XML 文件描述View并在运行时将它们“膨胀”成实际的View对象。Android 还允许你通过 XML 文件描述菜单,并在需要菜单时放大菜单。这有助于将菜单结构从菜单处理逻辑的实现中分离出来,并为开发菜单创作工具提供了更简单的方法。
菜单 XML 结构
菜单 XML 放在项目树的res/menu/中,与项目可能使用的其他类型的资源放在一起。与布局一样,项目中可以有几个菜单 XML 文件,每个文件都有自己的文件名和扩展名.xml。
例如,在Menus/Inflation示例项目中,这里有一个名为option.xml的菜单:
`
`请注意以下几点:
- 您必须以一个
menu根元素开始。 - 在一个
menu元素中有item元素和group元素,后者代表可以作为一个组操作的菜单项的集合。 - 通过添加一个
menu元素作为item元素的子元素来指定子菜单,使用这个新的menu元素来描述子菜单的内容。 - 如果您想检测一个项目何时被选择,或者从 Java 代码中引用一个项目或组,请确保应用一个
android:id,就像您使用View布局 XML 一样。
菜单选项和 XML
在item和group元素中,可以指定各种选项,与Menu或MenuItem上的相应方法相匹配,如下所示:
- 标题:菜单项的标题是通过
item元素上的android:title属性提供的。这可以是文字字符串,也可以是对字符串资源的引用(例如,@string/foo)。 - 图标:菜单项可选有图标。要以引用可绘制资源的形式提供图标(例如,
@drawable/eject),请使用item元素上的android:icon属性。 - 顺序:默认情况下,菜单项在菜单中的顺序由它们在菜单 XML 中出现的顺序决定。您可以通过在
item元素上指定android:orderInCategory属性来改变这个顺序。这是一个基于0的与当前类别相关的商品订单索引。有一个隐含的默认类别;组可以提供一个android:menuCategory属性来为该组中的项目指定一个不同的类别。不过,一般来说,最简单的方法是将 XML 中的项目按照您希望它们出现的顺序排列。 - Enabled :可以启用或禁用项目和组,在 XML 中通过
item或group元素上的android:enabled属性来控制。默认情况下,项目和组处于启用状态。禁用的项目和组出现在菜单中,但不能被选择。你可以通过MenuItem上的setEnabled()方法在运行时改变一个项目的状态,或者通过Menu上的setGroupEnabled()改变一个组的状态。 - Visible :条目和组可以是可见的或不可见的,在 XML 中通过
item或group元素上的android:visible属性来控制。默认情况下,项目和组是可见的。不可见的项目和群组不会出现在菜单中。你可以通过MenuItem上的setVisible()方法在运行时改变一个项目的状态,或者通过Menu上的setGroupVisible()改变一个组的状态。 - 快捷方式:项目可以有快捷方式——单个字母(
android:alphabeticShortcut)或数字(android:numericShortcut),可以按下这些快捷方式来选择项目,而不必使用触摸屏、D-pad 或轨迹球来导航整个菜单。
膨胀菜单
实际上,一旦用 XML 定义了菜单,使用它就很容易了。只需创建一个MenuInflater并告诉它膨胀你的菜单。
项目Menus/Inflation是项目Menus/Menus的克隆,菜单创建转换为使用菜单 XML 资源和MenuInflater。选项菜单已转换为本节前面显示的 XML 以下是上下文菜单:
`
item android:id="@+id/remove" android:title="Remove" /> `Java 代码几乎是相同的,主要变化在于onCreateOptionsMenu()和onCreateContextMenu()的实现:
`@Override public boolean onCreateOptionsMenu(Menu menu) { new MenuInflater(this).inflate(R.menu.option, menu);
return(super.onCreateOptionsMenu(menu)); }
@Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfomenuInfo) { new MenuInflater(this).inflate(R.menu.context, menu); }`
在这里,我们看到MenuInflater如何将菜单资源中指定的菜单项(如R.menu.option)注入到提供的Menu或ContextMenu对象中。
我们还需要更改onOptionsItemSelected()和onContextItemSelected()以使用 XML 中指定的android:id值:
`@Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.add: add(); return(true);
case R.id.reset: initAdapter(); return(true); }
return(super.onOptionsItemSelected(item)); }
@Override public boolean onContextItemSelected(MenuItem item) { AdapterView.AdapterContextMenuInfo info= (AdapterView.AdapterContextMenuInfo)item.getMenuInfo(); ArrayAdapter adapter=(ArrayAdapter)getListAdapter();
switch (item.getItemId()) { caseR.id.cap: String word=words.get(info.position);
word=word.toUpperCase();
adapter.remove(words.get(info.position)); adapter.insert(word, info.position);
return(true);
case R.id.remove: adapter.remove(words.get(info.position));
return(true); }
return(super.onContextItemSelected(item)); }`
当巨型菜单在地球上行走
随着 Android 3.x 和 4.0 的推出,处理平板电脑和大显示器的新方法被引入并融入到平台的核心。特别是选项菜单,从菜单按钮触发变成了动作栏的下拉菜单。幸运的是,这是向后兼容的,所以您现有的菜单不需要改变来采用这种新的外观。我们将在第二十六章中讲述使用更大设备的整体含义,动作栏本身将在第二十七章的中讲述。