六、验证和处理输入数据

不幸的是,应用中输入的验证和处理通常是设计过程中的事后想法。在用户界面的第二轮草稿中,这些应该是你思考的重点。一个触摸屏设备提供了更多的机会来简化从用户那里获取数据,在许多情况下消除了对卫生或验证的需求,同时大大改善了用户对应用的体验。

安卓提供了一个优秀的工具集来从用户那里捕获许多不同类型的数据,同时也以Intent结构的形式在你的应用组件之间提供了松散的耦合。通过使用几个较小的 Activity类来捕获数据,同时抽象功能来捕获不同类型的输入,您将能够更容易地重用输入捕获Activity类,不仅在应用中,而且在其他应用中也是如此。此外,通过正确注册Activity,您将允许其他应用覆盖或使用您的Activity实现,允许用户选择他们喜欢的捕获机制。

处理不良输入

应用通常需要用户提供特定类型的输入。应用捕获用户的输入,以便用户告诉它一些关于世界的事情。这可以是任何东西,从用户正在寻找的东西(即搜索词),到关于用户本身的东西(即他们的年龄)。在大多数情况下,用户可以通过使用机制(如自动完成框)进行输入。然而,如果一个用户可以给你“不希望的”输入,那么他们中的某一个会。

不需要的输入可以是任何东西,从文本到搜索结果。在这两种情况下,您需要做三件事:

  1. 告知用户您希望数据采用的格式
  2. 让他们知道他们输入了不需要的数据
  3. 让他们重新输入数据

正确标注输入

您对来自用户的不良输入的第一个防御是正确标记输入小部件。这不仅仅意味着,有一个标签,内容如下:

Date of Birth (dd/mm/yy):

这意味着使用正确的小部件来捕获数据。您的输入小部件是标签的一种形式,它们向用户指示您期望它们输入什么类型的数据。在许多情况下,它们可以用来阻止用户输入无效数据,或者至少降低输入的可能性。

请记住,用户期望事情的工作方式,以及他们期望能够快速选择事情的方式。如果你需要他们给你的申请一个国家的名字,不要用Spinner强迫他们滚动看似无穷无尽的名字列表。

发出不良输入信号

如果用户确实输入了不想要的或无用的东西,你需要告诉他们,而且要快!你越早让用户知道他们给了你一些没用的东西,他们就能越早纠正它并重新使用你的应用。

一个常见的错误是当用户按下保存提交按钮时,只需Toast用户。虽然这是可以的,如果你只能确定他们的错误在这一点上,但你几乎总是可以事先弄清楚。

请记住,在触摸屏设备上,虽然您有一个“集中”的小部件,但它并不像在桌面系统上那样发挥相同的作用,用户也不会“关闭”小部件。这意味着,您的用户界面应该尽可能对用户的动作做出实时响应,而不是等待他们做其他事情(即选择另一个小部件),然后再给他们一些反馈。如果他们做了一些使另一个表单元素无法使用的事情,请禁用它。如果他们做了一些使一组小部件无效的事情,对他们隐藏整个组,或者把它放在不同的屏幕上。

Signaling undesirable input

颜色和图标都是快速告诉用户他们有问题的好方法。当您意识到某些用户输入错误时,您可以采取额外的步骤禁用任何种类的保存下一步提交按钮。但是,如果您确实禁用了这样的按钮,请确保清楚哪个表单元素上有不需要的数据,并确保它在他们的屏幕上。一个很好的选择是Toast用户选择一个下一个按钮,并滚动到无效元素。

如果您需要针对某个远程服务检查用户的输入,请使用后台(或异步)消息。这将允许您在用户使用应用时验证他们的内容。它还允许你发出有问题的信号,而不会阻止他们使用表单的其余部分。他们总是可以回到无效字段并纠正它。

从不良输入中恢复

始终确保修复错误对用户来说是尽可能无痛的。他们必须做的纠正拼写错误的单词(或类似单词)的工作越多,他们就越有可能停止使用该应用。从不需要的输入中恢复的最简单的方法(恰好与上面的注释非常吻合)是在用户有机会进入流程的另一部分之前告诉他们。然而,这并不总是可能的。

在验证用户输入的过程中,有时需要弹出请等待对话框(通常作为副作用)。在这些情况下,明智的做法是使用 ProgressDialog,这样在这个阶段你就不会让用户离开你当前的Activity。这将有两个重要的副作用:

  • 您不会向活动堆栈添加不必要的层
  • 当您关闭ProgressDialog时,用户给出的输入仍然可用

给用户直接反馈

当接受用户的文本或其他键盘输入时,最好在用户还在输入的时候就向他们表明它的有效性。一种常见的方法是使用 EditText小部件右侧的和ImageView,并更改图像内容以表明用户输入了有效还是无效的内容。根据输入当前是否有效,可以设置 ImageView中显示的图像。这为用户提供了验证过程的实时视图。这种机制也适用于发信号通知可变级别的验证(也就是说,当输入不是严格有效或无效,而是质量好或不理想的质量时),例如在密码输入的情况下。

您可以使用图像图标,或者简单地使用安卓可绘制的 XML 资源来表示有效性(即绿色表示有效,红色表示无效)。这也意味着您的图标将缩放到您在布局 XML 文件中指定的任何大小。

类型

颜色和图标

使用非彩色指示器来区分图标通常是个好主意。色盲的人可能会发现很难或不可能区分两个图标,除非你改变形状和颜色。让你的“有效”图标变成绿色圆圈,让你的“无效”图标变成红色六边形,会让你的应用更有用。

为了避免屏幕上出现混乱的图标,您可能希望只在用户当前使用的字段旁边显示验证图标。然而,使用 INVISIBLE View状态代替GONE是一个好主意,以避免当用户改变用户界面的焦点时改变布局。同时,请确保验证图标大小相同。

完全避免无效输入

请记住,对于移动设备,时间通常是用户的一个限制。出于这个原因(以及简单的可用性原因),您通常应该努力完全避免来自用户的无效输入。安卓为你提供了几种实现这一点的机制,利用每一个机会都是明智的。通常,您会希望使用避免验证需求的小部件。这在 Android 中几乎总是一个选项,即使当您的需求比简单的类型信息更复杂时,您通常也可以定制小部件来阻止用户违反您的验证规则。

捕捉日期和时间

正如我们已经讨论过的,在输入日期和时间时,您应该使用 DatePickerTimePicker小部件,或者DatePickerDialogTimePickerDialog小部件,以避免原始小部件引入的布局问题。

避免创建自己的日历小部件,除非这是应用的硬性要求。你可能不喜欢一个 DatePickerDialog的样子,但是用户在其他安卓应用中见过,知道怎么用。这些标准小部件也有可能在未来的安卓版本中得到改进,让您的应用得到改进,而不需要您做任何工作。

Capturing date and time

您可能会发现需要对日期和时间输入进行额外的验证,尤其是在捕获日期或时间范围时。例如,如果您向用户询问出生日期,该用户将不能输入一个表示晚于“今天”的任何时间的字段(除非是预期的出生日期)。虽然DatePicker类有一个事件侦听器,允许您侦听其数据的更改(并且DatePickerDialog实现了这个事件侦听器),但是您不能使用这个事件侦听器来取消更改事件。

因此,为了取消事件,您需要在事件执行时将输入更改回有效的内容。这是安卓系统中一个惊人的简单技巧。由于事件是在进行绘制的同一线程上执行的,因此它允许您在屏幕上呈现无效数据之前更改值。下面是一个简单的 ValidatingDatePickerDialog的例子,你可以用它来在你的应用中实现一个简单级别的日期验证。如果你需要的话,可以很容易地为 TimePickerDialog编写另一个这样的类。

public class ValidatingDatePickerDialog extends DatePickerDialog {

    private int lastValidYear;
    private int lastValidMonth;
    private int lastValidDay;
    private ValidationCallback callback = null;

    public ValidatingDatePickerDialog(
            final Context context,
            final OnDateSetListener callBack,
            final int year,
            final int monthOfYear,
            final int dayOfMonth) {

        super(context, callBack, year, monthOfYear, dayOfMonth);
        setValidData(year, monthOfYear, dayOfMonth);
    }

 protected void setValidData(
 final int year,
 final int monthOfYear,
 final int dayOfMonth) {

 lastValidYear = year;
 lastValidMonth = monthOfYear;
 lastValidDay = dayOfMonth;
 }

    @Override
    public void onDateChanged(
            final DatePicker view,
            final int year,
            final int month,
            final int day) {

        if(callback != null && !callback.isValid(year, month, day)) {
 view.updateDate(
 lastValidYear,
 lastValidMonth,
 lastValidDay);
        } else {
            super.onDateChanged(view, year, month, day);
            setValidData(year, month, day);
        }
    }

    public void setValidationCallback(
            final ValidationCallback callback) {
        this.callback = callback;
    }

    public ValidationCallback getValidationCallback() {
        return callback;
    }

    public interface ValidationCallback {
        boolean isValid(int year, int monthOfYear, int dayOfMonth);
    }
}

这种处理验证的方法可以用在大多数不提供事件隐式验证的安卓小部件中,它提供了比给用户一个带有文本Toast更好的用户体验。请输入有效的出生日期。它还避免了在应用中需要额外的验证层。

使用微调器和列表视图进行选择

很多时候,用户需要从应用的可能值列表中选择一些东西。我们已经在第 2 章中讨论了SpinnerListView小部件,为视图呈现数据。然而,它们提供了几个在验证时非常有用的特性。它们是隐式验证的小部件,也就是说,用户不可能输入不正确的数据,因为输入的可能值是由应用定义的。然而,当有效项目集根据其他用户输入或一些外部信息来源发生变化时,该怎么办?在这些情况下,您可以选择几个选项。

改变数据集

阻止用户选择不再有效的值的最简单方法是将其从数据集中移除。我们已经在BurgerAdapter第 2 章中做了类似的事情,在中我们修改了当用户触摸某些项目时的数据集。修改AdapterView的数据集是一个好主意,因为它“取消了菜单选项”。然而,它在Spinner类中并不能很好地工作,因为,如果项目从屏幕上移除,用户将会疑惑刚刚在那里的项目发生了什么(并且可能担心他们会发疯)。

为了不混淆或挫败您的用户,您应该只从SpinnerListView数据集中删除项目,如果该项目可能不会被添加回数据集中。这种要求的一个很好的例子是可用的无线网络列表,或范围内的蓝牙设备。在这两种情况下,可用项目的列表由环境定义。用户将接受显示的选项不总是对他们可用,并且新项目可能会不时出现。

禁用选择

阻止某些项目被选中的另一种通常更为用户友好的方法是禁用它们。您可以通过覆盖ListAdapter类中的 isEnabled(int)方法来使ListViewSpinner忽略项目。但是,此方法将仅在事件级别禁用该项,该项仍将显示为已启用(其主要目的是定义分隔符视图)。

为了在视觉上禁用某个项目,您需要禁用显示该项目的View。这是一种非常有效的方式来告诉用户,“您已经更改了使该项目不可用的内容”。以图形方式禁用一个项目也让用户知道它可能在未来变得可用。

捕捉文本输入

最难处理的输入是各种形式的文本输入。我发现使用软键盘可能不如使用硬件键盘快,但从开发的角度来看,它提供了硬件键盘所没有的东西——灵活性。当我想在一个字段中输入文本时,软键盘的状态将指示对该字段有效的输入类型。如果我应该输入一个电话号码,键盘只能显示数字,甚至可以变成拨号盘。这不仅向我指明了我应该做什么,还阻止我输入任何会导致验证错误的内容。

安卓TextView(也就是EditText)小部件为您提供了许多不同的选项和方法,您可以通过它们来定义复杂的文本输入验证规则。各种软键盘也能理解其中的许多选项,允许它们根据 TextView小部件的配置显示完整键盘的子集。即使软键盘不完全理解(或使用硬件键盘),也必须遵守指定选项的规则。告诉EditText您希望它捕获什么类型的数据的最简单方法是使用 inputType XML 属性。

正如您将从inputType文档中看到的,其所有可能的值都是 android.view.inputmethod.InputType界面中可用的位掩码的不同组合。作为inputType属性的值可用的选项将涵盖大多数需要捕获特定类型输入的情况。您也可以使用 TextView.setRawInputTextView.setKeyboardListener方法创建自己的更复杂的输入类型。

类型

键盘监听器

您应该尽可能使用输入类型或标准的KeyListener来处理您的文本验证。写一个KeyListener是一个不平凡的任务,在某些情况下可能会看到你实现了一个自定义软键盘。如果存在软键盘,安卓系统中定义除TYPE_NULL之外的输入类型的KeyListener可能根本不会调用其监听器事件(onKeyDownonKeyUponKeyOther)。KeyListener的按键事件仅用于接受或拒绝来自硬件键盘的事件。软件键盘使用TextView的输入类型属性来决定它们应该向用户提供什么功能。

自动完成文本输入

SpinnerListView小部件是让用户从预定义的选项列表中进行选择的好方法。然而,两者都有一个主要缺陷,即它们不能很好地扩展到很长的列表。虽然实现和性能都很好,但用户只是不喜欢浏览大量的数据列表。解决这个问题的标准方法是提供一个自动完成的文本输入小部件。

Autocompleting text input

自动完成的输入小部件也经常与用户给出的过去选项的历史一起使用,或者建议用户可能想要“完成”他们的输入的可能方式。安卓AutoCompleteTextView小部件是一个具有自动完成功能的EditText。它使用ListAdapter(也必须实现Filterable界面)来查找并向用户显示可能的建议列表。

然而,一个AutoCompleteTextView有两个主要缺陷:

  • 还是一个TextView,用户没有被强制选择其中一个建议项,这意味着它的内容必须单独验证。
  • 建议列表显示在小部件的正下方,占用了相当多的屏幕空间。结合用于输入的软键盘,用户界面在小屏幕上可能变得混乱或几乎不可用

这两个问题都可以通过小心谨慎地使用AutoCompleteTextView类来解决。当您需要搜索框、网址输入或类似的东西时,它们非常有用,但它们通常不适合放在屏幕的中间(它们最好放在顶部,因为它们有足够的空间放置建议列表)。

突击测验

  1. KeyboardListener中的onKeyDown事件何时被调用?
    1. 当广播系统范围的按键事件时
    2. 取决于系统是否有硬件键盘
    3. 当按下硬件键盘键时
    4. 当按下硬件接口控制按钮之一时
  2. 什么时候使用Toast通知用户验证错误?
    1. 当他们出错时(也就是说,勾选一个不该勾选的复选框)
    2. 在他们关闭无效的小部件后
    3. 从外部服务收到验证错误后
  3. 在 IM(即时通讯)应用中,如果用户的某个联系人离线,如何更新联系人的ListView来反映这种变化?
    1. 图形化禁用ListView中的用户图标,并将其移到ListView的底部
    2. 将用户从ListView中移除
    3. 禁用ListView中的用户图标

为成果而建设活动

有时候,安卓系统中的默认小部件都不能独立满足您的输入需求,您需要某种复合输入结构。在这些情况下,您可以创建一个Dialog小部件,或者构建一个新的ActivityDialog小部件在内容保持较小(最多两行或三行小部件)时非常有用,因为它们在视觉上保持在当前Activity的顶部。然而,这意味着他们消耗额外的资源(因为他们的呼叫Activity不能被交换到后台),并且因为他们有自己的装饰,他们没有像Activity一样多的可用屏幕空间来工作。

第 4 章利用活动和意图中,我们讨论了Activity类的概念,它将数据返回给调用者。当您需要一些额外的验证形式或者您想要隔离一个特定的输入小部件(或一组小部件)时,这是一个很好的技术。您可以在Activity.setResult方法中指定一些结果数据。一般来说,Acitivity只会指定一个成功或失败的结果(使用RESULT_OKRESULT_CANCELLED常量)。也可以通过填充Intent来交还数据,目的是:

Intent result = new Intent();
result.putExtra("paymentDetails", paymentDetails);
setResult(RESULT_OK, result);

当您调用 finish()方法时,Intent数据将与结果代码一起传递到父Activity对象的onActivityResult方法中。

通用过滤搜索活动

正如本章前面所讨论的,有时您有一个预定义的对象列表,您希望用户选择其中一个。该列表太大,用户无法滚动浏览(例如,世界上所有国家的列表),但它也是一个已定义的列表,因此您不希望他们能够选择自由文本。

在这种情况下,可过滤的ListView通常是最合适的选择。虽然ListView类具有过滤功能,但它在没有硬件键盘的设备上运行不太好(如果有的话)。因此,明智的做法是使用一个EditText小部件来允许用户过滤ListView的内容。

这种需求是非常常见的,因此在这一节中,我们将考虑构建一个几乎完全通用的过滤和选择数据的功能。该示例将提供两种向用户显示数据的机制。一个通过Cursor,另一个通过简单的Object阵。在这两种情况下,过滤ListView的任务都留给了ListAdapter实现,使实现相对简单。

行动时间–创建列表项选择活动

这是一个相当大且有点复杂的例子,所以我将把它分成几个小块,每个都有一个目标。我们首先想要的是一个布局好看的Acitivity类。我们将构建的布局是一个位于 T2 上方的 T1,每个 T1 都有一个可以被 T3 使用的标识。

  1. 创建一个新项目来包含你的ListItemSelectionActivity类:

    java android create project -n Selector -p Selector -k com.packtpub.selector -a ListItemSelectionActivity -t 3

  2. 在编辑器或 IDE 中打开res/layout/main.xml文件。

  3. 移除任何默认布局代码。
  4. 确保根元素是消耗Activity :

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

    中可用屏幕空间的LinearLayout 5. 在根元素中,声明一个 ID 为inputEditText和一个 ID 为textFilterinputType,表示它将过滤另一个小部件的内容:

    java <EditText android:id="@+id/input" android:inputType="textFilter" android:layout_width="fill_parent" android:layout_height="wrap_content"/>

  5. EditText之后,我们声明一个ListView,消耗剩余空间:

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

  6. 在编辑器或 IDE 中打开ListItemSelectionActivity Java 源文件。

  7. 在类的顶部声明一个ListAdapter字段:

    java private ListAdapter adapter;

  8. ListAdapter字段后,声明一个Filter字段:

    java private Filter filter;

  9. onCreate方法中,确保您正在加载main.xml作为ListItemSelectionActivity :

    java setContentView(R.layout.main);

    的内容视图 11. 然后获取 XML 文件中声明的ListView供我们以后使用:

    java ListView list = (ListView)findViewById(R.id.list);

  10. 最后,获取 XML 文件中声明的EditText供我们以后使用:

    java EditText input = (EditText)findViewById(R.id.input);

刚刚发生了什么?

你现在已经有了ListItemSelectionActivity类的骨架。此时应用将能够运行,为您呈现一个空的ListView和一个EditText。在类顶部声明的ListAdapterFilter字段将在后面的阶段用于保存列表信息,并过滤屏幕上可见的内容。

行动时间——创建数组适配器

ListItemSelectionActivity类将接受来自两个不同来源的列表内容。您可以指定一个数据库查询Uri,用于从外部源中选择两列,也可以指定一个Object数组作为Intent对象中的额外数据。对于下一个任务,我们将编写一个私有的实用程序方法来从Intent对象创建一个ArrayAdapter

  1. 在编辑器或 IDE 中打开ListItemSelectionActivity Java 源文件。
  2. 声明一个新的实用方法,为Intent创建一个ListAdapter:

    java private ListAdapter createArrayAdapter(Intent intent) {

  3. Intent :

    java Object[] data = (Object[])intent.getSerializableExtra("data");

    的额外数据中获取一个Object数组 4. 如果数组不是null,也不是空的,返回一个新的ArrayAdapter对象,该对象将在安卓定义的标准列表项资源中显示数组的内容:

    java if(data != null && data.length > 0) { return new ArrayAdapter<Object>( this, android.R.layout.simple_list_item_1, data);

  4. 如果数组为空,抛出IllegalArgumentException :

    java else { throw new IllegalArgumentException( "no list data specified in Intent: " + intent); }

刚刚发生了什么?

你刚刚写了一个非常基本的实用方法,从一个Intent中提取一个Object数组,然后返回。如果数组不存在或为空,该方法将抛出IllegalArgumentException。这是一个有效的响应,因为我们将在寻找数据库查询之后寻找数组。如果没有外界给我们任何数据,那么这个Activity就不能执行。要求用户从空白列表中选择一个项目是没有用的。

请记住,该Activity旨在由另一个Activity启动,而不是由用户通过应用菜单直接启动。因此,当Activity没有按照预期的方式使用时,我们希望给自己或其他开发者提供有用的反馈。

行动时间–创建光标适配器

设置CursorAdapterArrayAdapter复杂得多。首先,与 T3 相比,T2 提供了更多的选择。我们的CursorAdapter可以根据指定的是一列还是两列来显示一行还是两行列表项。虽然ArrayAdapter包含一些默认的过滤逻辑,但我们需要为CursorAdapter提供更多一点的支持。

  1. 首先,我们允许使用两种不同的列命名约定,以及一些默认值。声明一个实用方法,从Intent :

    java private String getColumnName( final Intent intent, String primary, String secondary, String def) {

    中找到期望的列名 2. 首先,尝试使用primary属性名获得一个列名:

    java String col = intent.getStringExtra(primary);

  2. 如果列名为null,则尝试secondary属性名:

    java if(col == null) { col = intent.getStringExtra(secondary); }

  3. 如果列名仍然是null,使用默认值:

    java if(col == null) { col = def; }

  4. return列名:

    java return col;

  5. 现在,声明另一个实用方法,它将创建实际的CursorAdapter用于ListView :

    java private ListAdapter createCursorAdapter(Intent intent) {

  6. 找到要显示的第一列的名称:

    java final String line1 = getColumnName(intent, "name", "line1", "name");

  7. 找到要显示的可选第二列的名称:

    java String line2 = getColumnName( intent, "description", "line2", null);

  8. 我们现在有两种可能的路径——单行列表项或双线列表项。它们的构造非常相似,所以我们声明一些变量来保存两条路径之间不同的值:

    java int listItemResource; final String[] columns; String[] displayColumns; int[] textIds;

  9. 如果指定了line2列名,我们使用以下代码:

    java if(line2 != null) {

  10. 我们将使用两行列表项资源:

    java listItemResource = android.R.layout.two_line_list_item;

  11. 数据库查询需要选择_id列,以及在Intent中指定的两列:

    java columns = new String[]{"_id", line1, line2};

  12. 但是,列表项将只显示指定的两列:

    java displayColumns = new String[]{line1, line2};

  13. CursorAdapter需要知道在two_line_list_item资源中声明的TextView小部件的资源标识:

    java textIds = new int[]{android.R.id.text1, android.R.id.text2};

  14. 如果第二列名称没有在Intent中指定,ListView应该有单行项目:

    java else { listItemResource = android.R.layout.simple_list_item_1;

  15. 我们只需要请求_id列,单列名称:

    java columns = new String[]{"_id", line1};

  16. 列表中的项目应该包含请求列的内容:

    java displayColumns = new String[]{line1};

  17. 我们不需要告诉CursorAdapter在单行列表项资源中要查找哪个小部件 ID:

    java textIds = null;

  18. else子句之后,我们将填充所需的变量。我们可以运行我们的初始数据库查询,并获得完整的数据列表,以将其呈现给用户:

    java Cursor cursor = managedQuery( intent.getData(), columns, null, null, line1);

  19. 我们现在可以创建CursorAdapter来包装ListView的数据库Cursor对象。我们使用SimpleCursorAdapter实现:

    java CursorAdapter cursorAdapter = new SimpleCursorAdapter( this, listItemResource, cursor, displayColumns, textIds);

  20. 为了让用户过滤列表,我们需要给CursorAdapter一个FilterQueryProvider。将FilterQueryProvider声明为匿名内部类:

    java cursorAdapter.setFilterQueryProvider( new FilterQueryProvider() {

  21. 在匿名FilterQueryProvider中,声明runQuery方法,该方法将在用户每次键入密钥时调用:

    java public Cursor runQuery(CharSequence constraint) {

  22. 我们可以返回一个managedQuery,它只是对我们在ListView :

    java return managedQuery( intent.getData(), columns, line1 + " LIKE ?", new String[] {constraint.toString() + '%'}, line1);

    中渲染的第一列执行一个 SQL LIKE 24. 最后createCursorAdapter方法可以返回CursorAdapter :

    java return cursorAdapter;

刚刚发生了什么?

当在我们的Intent中指定查询Uri时,该实用程序方法处理CursorAdapter的创建。这种结构允许过滤非常大的数据集,因为它(通常)建立在 SQL Lite 数据库之上。它的性能与它将查询的数据库表的结构直接相关。

由于数据库查询的潜在巨大规模,CursorAdapter类本身不做任何数据集过滤。相反,您需要实现FilterQueryProvider接口来为过滤器的每次更改创建和运行新的查询。在前面的例子中,我们创建了一个与默认的Cursor完全相同的Cursor,但是我们在查询中添加了selectionselectionArgs。这个LIKE子句将告诉 SQL Lite 只返回以用户键入的过滤器开始的行。

行动时间-设置列表视图

我们现在有实现来创建两种类型的ListAdapter,这个Activity可以过滤。现在我们需要一个实用的方法来计算使用哪一个,并返回它;然后我们想用新的实用方法在ListView小部件上设置ListAdapter

  1. 声明一个新方法来创建所需的ListAdapter对象:

    java protected ListAdapter createListAdapter() {

  2. 获取用于启动ActivityIntent对象:

    java Intent intent = getIntent();

  3. 如果Intent中的数据Uri不是null,则给定的Intent返回一个CursorAdapter。否则,返回给定的Intent :

    ```java if(intent.getData() != null) { return createCursorAdapter(intent);

    else { return createArrayAdapter(intent); } ```

    ArrayAdapter 4. 在onCreate方法中,从布局中找到两个View对象后,使用新的实用方法

    java adapter = createListAdapter();

    创建所需的ListAdapter 5. 将Filter字段分配给ListAdapter给出的Filter:

    java filter = ((Filterable)adapter).getFilter();

  4. ListAdapter设置在ListView上:

    java list.setAdapter(adapter);

刚刚发生了什么?

这段代码现在引用了创建的ListAdapter对象和它使用的Filter。您会注意到,如果您现在运行应用,当您打开它时,您会看到一个强制关闭对话框。这是因为代码现在需要某种数据来填充ListView。虽然对于正常的应用来说并不理想,但这确实是一个可重用的组件,可以在各种情况下使用。

行动时间–过滤列表

虽然代码都是为了显示列表而设置的,甚至是为了过滤列表,但是我们还没有将EditText框附加到ListView上,所以输入EditText在此刻是绝对没有作用的。我们需要监听EditText框的变化,并请求根据输入的内容过滤ListView。这将涉及ListItemSelectionActivity类监听EditText上的事件,然后要求Filter对象缩小可用项目集。

  1. 应使ListItemSelectionActivity实现TextWatcher界面:

    java public class ListItemSelectionActivity extends Activity implements TextWatcher

  2. onCreate方法的ListView上设置ListAdapter后,在EditText小部件

    java input.addTextChangedListener(this);

    上添加ListItemSelectionActivity作为TextWatcher 3. 您需要声明beforeTextChangedonTextChanged方法的空实现,因为我们对这些事件不太感兴趣:

    ```java public void beforeTextChanged( CharSequence s, int start, int count, int after) { }

    public void onTextChanged( CharSequence s, int start, int count, int after) { } ```

  3. 然后声明我们感兴趣的afterTextChanged方法:

    java public void afterTextChanged(Editable s) {

  4. afterTextChanged方法中,我们简单的要求当前ListAdapterFilter过滤ListView :

    java filter.filter(s);

刚刚发生了什么?

TextWatcher界面用于跟踪对TextView小部件的更改。实现将能够监听TextView的实际内容的任何变化,而不管变化的来源是什么。虽然OnKeyListenerKeyboardListener接口主要用于处理硬件键盘事件,但TextWatcher处理从硬件键盘、软键盘甚至内部调用到TextView.setText的变化。

行动时间–返回选择

ListItemSelectionActivity现在可以用来显示一个可能项目的列表,并通过在ListView上方输入EditText来过滤它们。然而,我们无法让用户从ListView中实际选择一个选项,以便将其传递回我们的父母Activity。这只需要简单实现OnItemClickListener接口。

  1. ListItemSelectionActivity类现在需要实现OnItemClickListener接口:

    java public class ListItemSelectionActivity extends Activity implements TextWatcher, OnItemClickListener {

  2. onCreate方法中注册为TextWatcher后,在ListView上注册为OnItemClickListener:

    java list.setOnItemClickListener(this);

  3. 覆盖onItemClick方法,监听用户选择:

    java public void onItemClick( AdapterView<?> parent, View clicked, int position, long id) {

  4. 创建一个空的Intent对象传递回我们的父级Activity :

    java Intent data = new Intent();

  5. 如果ListAdapter是一个CursorAdapter,传递到onItemClickid将是数据库_id列的值供选择。将该值添加到Intent :

    java if(adapter instanceof CursorAdapter) { data.putExtra("selection", id);

  6. 如果ListAdapter不是CursorAdapter,我们将实际选择的Object添加到Intent :

    java else { data.putExtra( "selection", (Serializable)parent.getItemAtPosition(position)); }

  7. 将结果代码设置为RESULT_OK,并将Intent传回到:

    java setResult(RESULT_OK, data);

  8. 用户已经做出了选择,所以我们现在已经完成了这个部分:

    java finish();

刚刚发生了什么?

ListItemSelectionActivity现在已经完成,可以使用了。它提供了与AutoCompleteTextView几乎相同的功能,除了作为一个独立的Activity,它为用户提供了一个更大的建议列表,用户必须从ListView中选择一个项目,而不是能够简单地键入他们的输入数据。

使用列表项选择活动

您需要指定您希望用户从哪些数据中进行选择,作为启动ListItemSelectionActivityIntent的一部分。如前所述,实际上有两条路径:

  • 传入某种数组(非常适合在您自己的应用中使用)
  • 给它一个数据库查询Uri和你想要显示的列名(如果你想在另一个应用中使用它,这很好)

由于ListItemSelectionActivity返回了它的选择(如果没有也没什么用),你需要用startActivityForResult方法而不是正常的startActivity方法开始。如果你想给它传递一个String对象的数组来选择,你可以使用类似于下面的意图=新的意图(这个,ListItemSelectionActivity.class):

intent.putExtra("data", new String[] {
    "Blue",
    "Green",
    "Red",// more colors    
});
startActivityForResult(intent, 101);

在上面的data数组中给定足够的颜色,你会看到一个ListItemSelectionActivity屏幕,可以根据用户想要的颜色进行过滤。下面是结果屏幕的截图:

Using the ListItemSelectionActivity

为了接收来自ListItemSelectionActivity的结果,您需要在onActivityResult方法中收听结果(如第 4 章利用活动和意图中所述)。例如,如果您只是想Toast确认选择的结果,您可以使用以下代码:

@Override
protected void onActivityResult(
        int requestCode,
        int resultCode,
        Intent data) {

    super.onActivityResult(requestCode, resultCode, data);

    if(requestCode == 101 && resultCode == RESULT_OK) {
        Object obj = data.getSerializableExtra("selection");
        Toast.makeText(
                this,
                String.valueOf(obj),
                Toast.LENGTH_LONG).show();
    }
}

最后,如何使用ListItemSelectionActivity进行数据库查询?这非常容易展示,可能是ListItemSelectionActivity最令人兴奋的特点。下面的代码片段将让用户从他们的电话簿中选择一个联系人:

Intent intent = new Intent(
        this,
        ListItemSelectionActivity.class);

intent.setData(People.CONTENT_URI);
intent.putExtra("line1", People.NAME);
intent.putExtra("line2", People.NUMBER);

startActivityForResult(intent, 202);

有围棋英雄!

ListItemSelectionActivity可以过滤和选择几乎任何东西。试着建立一个世界上所有国家的列表(网上有很多这样的列表),然后创建一个Activity,让你用ListItemSelectionActivity选择一个。

总结

您如何接受用户的输入,以及如何验证输入在用户对您的应用的整体体验中起着至关重要的作用。软件应该帮助用户,并告诉他们每一步的期望。这不仅使应用更容易使用,而且工作起来也更快。

使用ListItemSelectionActivity,往往会帮助你的用户在大数据集中搜罗,同时保护他们不会做出他们不想做的选择,或者是无效的选择。这是一种非常常用的小部件类型,可以在许多不同的应用中看到(以各种形式)。目前,安卓还没有一个通用类来轻松完成这项工作。

在下一章中,我们将开始研究一种相当现代的用户反馈形式:动画。安卓有很好的支持,不仅仅是为你的用户界面制作动画,还可以合成复杂的自定义动画。动画在用户享受应用的过程中起着至关重要的作用。这不仅是因为它看起来很棒,还因为它给出了应用当前正在做什么以及它们的动作有什么影响的可视化队列。