添加自定义搜索建议

使用 Android 搜索对话框或搜索 widget 时,您可以提供根据应用中的数据创建的自定义搜索建议。例如,如果您的应用是字典,您可以在用户完成查询输入之前,提供字典中与搜索字段中输入的文本相匹配的建议。这些建议很有价值,因为它们可以有效预测用户想要的内容,并提供即时访问。图 1 显示了包含自定义建议的搜索对话框的示例。

提供自定义建议后,您还可以将其提供给系统级快速搜索框,以便用户从您的应用外部访问您的内容。

在添加自定义建议之前,请实现 Android 搜索对话框或搜索 widget,以便在您的应用中进行搜索。请参阅创建搜索界面内容提供程序

基础知识

图 1. 包含自定义搜索建议的搜索对话框的屏幕截图。

当用户选择自定义建议时,系统会向可搜索的 activity 发送 Intent。与发送包含 ACTION_SEARCH 操作的 intent 的常规搜索查询不同,您可以改为将自定义建议定义为使用 ACTION_VIEW(或任何其他 intent 操作),还可以包含与所选建议相关的数据。在字典示例中,当用户选择一条建议时,应用可以立即打开该字词的定义,而不是在字典中搜索匹配项。

如需提供自定义建议,请执行以下步骤:

  • 实现一个基本的可搜索 Activity,如创建搜索界面中所述。
  • 使用有关提供自定义建议的内容提供程序的信息来修改可搜索配置。
  • 为建议构建表格(例如在 SQLiteDatabase 中),并设置表格的格式,使其包含所需的列。
  • 创建一个可以访问建议表的 content provider,并在清单中声明该提供程序。
  • 声明当用户选择某条建议时要发送的 Intent 类型,包括自定义操作和自定义数据。

就像 Android 系统显示搜索对话框一样,它也会显示搜索建议。您需要一个 content provider,系统可以从中检索建议。如需了解如何创建 content provider,请参阅内容提供程序

如果系统识别出您的 activity 可搜索并提供搜索建议,在用户输入查询时,会发生以下过程:

  1. 系统获取搜索查询文本(即到目前为止输入的任何内容),并对管理建议的内容提供程序执行查询。
  2. content provider 会返回一个 Cursor,指向与搜索查询文本相关的所有建议。
  3. 系统会显示 Cursor 提供的建议列表。

显示自定义建议后,可能会发生以下情况:

  • 如果用户输入其他字母或以任何方式更改查询,系统会重复执行上述步骤,并相应地更新建议列表。
  • 如果用户执行搜索,系统会忽略建议,并使用常规 ACTION_SEARCH intent 将搜索传递给可搜索 Activity。
  • 如果用户选择某条建议,系统会向您的可搜索 activity 发送一个 intent,其中包含自定义操作和自定义数据,以便您的应用可以打开建议的内容。

修改可搜索配置

如需添加对自定义建议的支持,请将 android:searchSuggestAuthority 属性添加到可搜索配置文件中的 <searchable> 元素中,如以下示例所示:

<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
    android:label="@string/app_label"
    android:hint="@string/search_hint"
    android:searchSuggestAuthority="com.example.MyCustomSuggestionProvider">
</searchable>

您可能需要其他属性,具体取决于您附加到每条建议的 intent 类型,以及您希望如何设置针对 content provider 的查询的格式。以下部分介绍了其他可选属性。

创建 content provider

如需为自定义建议创建 content provider,请先参阅 Content Provider,了解如何创建 content provider。自定义建议的 content provider 与任何其他 content provider 类似。不过,对于您提供的每条建议,Cursor 中的相应行必须包含系统理解并用来设置建议格式的特定列。

当用户在搜索对话框或搜索 widget 中输入文本时,每当输入一个字母时,系统都会通过调用 query() 向您的 content provider 查询建议。在您的 query() 实现中,content provider 必须搜索建议数据并返回一个 Cursor,该 Cursor 指向它确定是优质建议的行。

以下两部分详细介绍了如何为自定义建议创建 content provider:

处理建议查询
系统如何向您的 content provider 发送请求以及如何处理请求。
构建建议表格
如何定义系统期望随每个查询返回的 Cursor 中包含的列。

处理建议查询

当系统从 content provider 请求建议时,它会调用 content provider 的 query() 方法。实现此方法以搜索您的建议数据,并返回指向您认为相关的建议的 Cursor

下面总结了系统传递给 query() 方法的参数,按顺序列出:

  1. uri

    始终是内容 Uri,格式如下:

    content://your.authority/optional.suggest.path/SUGGEST_URI_PATH_QUERY
    

    默认行为是系统传递此 URI 并将查询文本附加到其中:

    content://your.authority/optional.suggest.path/SUGGEST_URI_PATH_QUERY/puppies
    

    末尾的查询文本使用 URI 编码规则进行编码,因此您可能需要在执行搜索之前对其进行解码。

    只有当您使用 android:searchSuggestPath 属性在可搜索配置文件中设置此类路径时,URI 中才会包含 optional.suggest.path 部分。仅当您针对多个可搜索 Activity 使用同一 content provider 时,才需要这样做。在这种情况下,请消除建议查询的来源的歧义。

  2. projection
    始终为 null。
  3. selection
    在可搜索配置文件的 android:searchSuggestSelection 属性中提供的值;如果您未声明 android:searchSuggestSelection 属性,则为 null。以下部分将进一步讨论这一点。
  4. selectionArgs
    如果您在可搜索配置中声明 android:searchSuggestSelection 属性,则包含搜索查询作为数组的第一个也是唯一元素。如果您不声明 android:searchSuggestSelection,则此参数为 null。以下部分将进一步讨论这一点。
  5. sortOrder
    始终为 null。

系统可以通过两种方式向您发送搜索查询文本。默认方法是将查询文本添加为 uri 参数中传递的内容 URI 的最后一个路径。不过,如果您在可搜索配置的 android:searchSuggestSelection 属性中添加选择值,则查询文本会作为 selectionArgs 字符串数组的第一个元素传递。下面将介绍这两个选项。

在 Uri 中获取查询

默认情况下,查询会附加为 uri 参数(一个 Uri 对象)的最后一段。如需在这种情况下检索查询文本,请使用 getLastPathSegment(),如以下示例所示:

Kotlin

val query: String = uri.lastPathSegment.toLowerCase()

Java

String query = uri.getLastPathSegment().toLowerCase();

这会返回 Uri 的最后一段,即用户输入的查询文本。

在选择参数中获取查询

与使用 URI 相比,更合适的做法是让 query() 方法接收执行查询所需的全部信息,并且您可能希望 selectionselectionArgs 参数携带适当的值。在本例中,请使用 SQLite 选择字符串将 android:searchSuggestSelection 属性添加到可搜索的配置中。在选择字符串中,添加一个问号 (?),作为实际搜索查询的占位符。系统会调用 query(),并将选择字符串作为 selection 参数,将搜索查询作为 selectionArgs 数组中的第一个元素。

例如,您可以按如下方式构建 android:searchSuggestSelection 属性以创建全文搜索语句:

<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
    android:label="@string/app_label"
    android:hint="@string/search_hint"
    android:searchSuggestAuthority="com.example.MyCustomSuggestionProvider"
    android:searchSuggestIntentAction="android.intent.action.VIEW"
    android:searchSuggestSelection="word MATCH ?">
</searchable>

采用这种配置时,query() 方法会将 selection 参数作为 "word MATCH ?" 传递,并将 selectionArgs 参数作为搜索查询传递。当您将这些元素作为各自的参数传递给 SQLite query() 方法时,它们会合成在一起,也就是说,问号会替换为查询文本。如果您以这种方式收到建议查询,并且需要向查询文本添加通配符,请将其附加或添加到 selectionArgs 参数的前面,因为该值会用引号括起来并插入代替问号。

上述示例中的另一个属性是 android:searchSuggestIntentAction,它定义了当用户选择建议时随每个 intent 发送的 intent 操作。声明用于建议的 intent 部分对此进行了进一步讨论。

构建建议表格

当您使用 Cursor 向系统返回建议时,系统要求每行中包含特定的列。无论您是将建议数据存储在设备上的 SQLite 数据库、网络服务器上的数据库,还是设备或 Web 中的其他格式中,都需将建议的格式设置为表格中的行,并使用 Cursor 呈现建议数据。

系统理解多个列,但其中只有两列是必需的:

_ID
每条建议的唯一整数行 ID。系统需要此标识符才能在 ListView 中显示建议。
SUGGEST_COLUMN_TEXT_1
显示为建议的字符串。

以下各列均为可选列。以下各部分将进一步讨论其中大多数选项。

SUGGEST_COLUMN_TEXT_2
一个字符串。如果您的 Cursor 包含此列,则所有建议都会以两行格式提供。此列中的字符串显示为主要建议文本下方的第二行较小的文本。它可以为 null 或为空,表示没有次要文本。
SUGGEST_COLUMN_ICON_1
一个可绘制资源、内容或文件 URI 字符串。如果您的 Cursor 包含此列,则所有建议都会以图标加文本格式提供,并且可绘制图标位于左侧。此列可以为 null 或为 0,表示此行中没有图标。
SUGGEST_COLUMN_ICON_2
一个可绘制资源、内容或文件 URI 字符串。如果您的 Cursor 包含此列,则所有建议都会以图标加文本的格式提供,且图标在右侧。此列可以为 null 或为 0,表示此行中没有图标。
SUGGEST_COLUMN_INTENT_ACTION
一个 intent 操作字符串。如果此列存在且在给定行中包含值,则在形成建议的 intent 时使用此处定义的操作。如果未提供该元素,系统将从可搜索配置中的 android:searchSuggestIntentAction 字段获取操作。如果您的操作对于所有建议都是相同的,则使用 android:searchSuggestIntentAction 指定操作并省略此列会更高效。
SUGGEST_COLUMN_INTENT_DATA
一个数据 URI 字符串。如果此列存在且在给定行中包含值,则在形成建议的 intent 时使用这些数据。如果未提供该元素,系统将从可搜索配置中的 android:searchSuggestIntentData 字段获取数据。如果二者均未提供,则 intent 的数据字段为 null。如果您的数据对于所有建议都是相同的,或者可以使用常量部分和特定 ID 进行描述,则更高效的做法是使用 android:searchSuggestIntentData 指定数据而省略此列。
SUGGEST_COLUMN_INTENT_DATA_ID
一个 URI 路径字符串。如果此列存在且在给定行中包含值,则“/”和此值会附加到 intent 中的数据字段。 仅当可搜索配置中的 android:searchSuggestIntentData 属性指定的数据字段已设置为适当的基字符串时,才应使用此参数。
SUGGEST_COLUMN_INTENT_EXTRA_DATA
任意数据。如果此列存在且在给定行中包含值,则这是形成建议的 intent 时使用的额外数据。如果未提供,intent 的 extra 数据字段为 null。此列可让建议提供额外的数据,这些数据将作为 extra 包含在 intent 的 EXTRA_DATA_KEY 键中。
SUGGEST_COLUMN_QUERY
如果此列存在且给定行中存在此元素,则这是形成建议的查询时使用的数据,作为 extra 包含在 intent 的 QUERY 键中。如果建议的操作为 ACTION_SEARCH,该元素是必需的;否则,它是可选的。
SUGGEST_COLUMN_SHORTCUT_ID
仅当为快速搜索框提供建议时,才使用此列。此列指示是否必须将搜索建议存储为快捷方式,以及是否必须对其进行验证。快捷方式通常在用户点按快速搜索框中的建议时形成。如果缺失,结果将存储为快捷方式,并且永远不会刷新。如果设置为 SUGGEST_NEVER_MAKE_SHORTCUT,则结果不会存储为快捷方式。否则,系统会使用快捷方式 ID 通过 SUGGEST_URI_PATH_SHORTCUT 回来查看最新建议。
SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING
仅当为快速搜索框提供建议时,才使用此列。此列指定,当此建议的快捷方式在快速搜索框中刷新时,必须显示旋转图标,而不是来自 SUGGEST_COLUMN_ICON_2 的图标。

以下各部分将进一步讨论其中的大部分列。

声明用于建议的 intent

当用户从搜索对话框或 widget 下方的列表中选择某条建议时,系统会向可搜索 activity 发送一个自定义 Intent。您必须为 intent 定义操作和数据。

声明 intent 操作

自定义建议最常见的 intent 操作是 ACTION_VIEW,当您要打开某些内容(如某个字词的定义、某人的联系信息或网页)时,该操作十分合适。不过,intent 操作可以是任何其他操作,并且各项建议的操作可以有所不同。

根据您是否希望所有建议使用相同的 intent 操作,您可以通过两种方式定义操作:

  • 使用可搜索配置文件的 android:searchSuggestIntentAction 属性为所有建议定义操作,如以下示例所示:
    <?xml version="1.0" encoding="utf-8"?>
    <searchable xmlns:android="http://schemas.android.com/apk/res/android"
        android:label="@string/app_label"
        android:hint="@string/search_hint"
        android:searchSuggestAuthority="com.example.MyCustomSuggestionProvider"
        android:searchSuggestIntentAction="android.intent.action.VIEW" >
    </searchable>
    
  • 使用 SUGGEST_COLUMN_INTENT_ACTION 列定义各条建议的操作。为此,请在建议表格中添加 SUGGEST_COLUMN_INTENT_ACTION 列,并针对每条建议在其中放入要使用的操作,例如 "android.intent.action.VIEW"

您还可以结合使用这两种方法。例如,您可以为 android:searchSuggestIntentAction 属性添加默认与所有建议搭配使用的操作,然后通过在 SUGGEST_COLUMN_INTENT_ACTION 列中声明其他操作来为某些建议替换此操作。如果您未在 SUGGEST_COLUMN_INTENT_ACTION 列中添加值,则系统会使用 android:searchSuggestIntentAction 属性中提供的 intent。

声明 intent 数据

当用户选择某条建议时,您的可搜索 activity 会通过您定义的操作接收 intent(如上一部分中所述),但 intent 还必须携带您的 activity 的数据,以标识用户选择了哪条建议。具体而言,每条建议的数据必须是唯一的,比如 SQLite 表格中建议的行 ID。收到 intent 后,您可以使用 getData()getDataString() 检索附加的数据。

您可以通过两种方式来定义 intent 中包含的数据:

  • 在建议表格的 SUGGEST_COLUMN_INTENT_DATA 列中定义每条建议的数据。

    在建议表格中提供每个 intent 的所有必要数据信息,方法是添加 SUGGEST_COLUMN_INTENT_DATA 列,然后在其中填充每行的唯一数据。此列中的数据会完全按照您在此列中的定义附加到 intent。然后,您可以使用 getData()getDataString() 进行检索。

  • 将数据 URI 分为两部分:所有建议共用的部分,以及每条建议独有的部分。将这两个部分分别放入可搜索配置的 android:searchSuggestintentData 属性和建议表格的 SUGGEST_COLUMN_INTENT_DATA_ID 列中。

    以下示例展示了如何在可搜索配置的 android:searchSuggestIntentData 属性中声明所有建议通用的 URI 部分:

      <?xml version="1.0" encoding="utf-8"?>
      <searchable xmlns:android="http://schemas.android.com/apk/res/android"
          android:label="@string/app_label"
          android:hint="@string/search_hint"
          android:searchSuggestAuthority="com.example.MyCustomSuggestionProvider"
          android:searchSuggestIntentAction="android.intent.action.VIEW"
          android:searchSuggestIntentData="content://com.example/datatable" >
      </searchable>
      

    在建议表的 SUGGEST_COLUMN_INTENT_DATA_ID 列中添加每条建议的最终路径(唯一部分)。当用户选择某条建议时,系统会从 android:searchSuggestIntentData 中获取字符串,附加一个斜杠 (/),然后添加 SUGGEST_COLUMN_INTENT_DATA_ID 列中的相应值,以形成完整的内容 URI。然后,您可以使用 getData() 检索 Uri

添加更多数据

如果您需要通过 intent 来表达更多信息,可以再添加一个表列(例如 SUGGEST_COLUMN_INTENT_EXTRA_DATA),它可以存储有关建议的更多信息。保存在此列中的数据会放置在 intent 的 extra bundle 的 EXTRA_DATA_KEY 中。

处理 intent

提供包含自定义 intent 的自定义搜索建议后,您需要使用可搜索 activity 在用户选择建议时处理这些 intent。这是除了处理 ACTION_SEARCH intent(您的可搜索 activity 已经这样做)之外,以下示例说明了如何在 activity 的 onCreate() 回调期间处理 intent:

Kotlin

when(intent.action) {
    Intent.ACTION_SEARCH -> {
        // Handle the normal search query case.
        intent.getStringExtra(SearchManager.QUERY)?.also { query ->
            doSearch(query)
        }
    }
    Intent.ACTION_VIEW -> {
        // Handle a suggestions click, because the suggestions all use ACTION_VIEW.
        showResult(intent.data)
    }
}

Java

Intent intent = getIntent();
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
    // Handle the normal search query case.
    String query = intent.getStringExtra(SearchManager.QUERY);
    doSearch(query);
} else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
    // Handle a suggestions click, because the suggestions all use ACTION_VIEW.
    Uri data = intent.getData();
    showResult(data);
}

在此示例中,intent 操作为 ACTION_VIEW,并且数据包含指向推荐项目的完整 URI(由 android:searchSuggestIntentData 字符串和 SUGGEST_COLUMN_INTENT_DATA_ID 列合成)。然后,该 URI 会传递给本地 showResult() 方法,该方法会向 content provider 查询 URI 指定的项。

重写查询文本

默认情况下,如果用户使用方向控件(例如使用轨迹球或方向键)浏览建议列表,查询文本不会更新。但是,您可以使用与获得焦点的建议匹配的查询来暂时重写显示在文本框中的用户查询文本。这样,用户就可以看到建议的查询,他们可以选择搜索框并修改查询,然后再将其作为搜索进行调度。

您可以通过以下方式重新编写查询文本:

  • 使用 "queryRewriteFromText" 值将 android:searchMode 属性添加到您的可搜索配置。在这种情况下,建议的 SUGGEST_COLUMN_TEXT_1 列中的内容将用于重新编写查询文本。
  • 使用 "queryRewriteFromData" 值将 android:searchMode 属性添加到您的可搜索配置。在这种情况下,建议的 SUGGEST_COLUMN_INTENT_DATA 列中的内容将用于重新编写查询文本。请仅将此参数与 URI 或其他应让用户可见的数据格式(如 HTTP 网址)结合使用。请勿使用内部 URI 架构以这种方式重写查询。
  • 在建议表格的 SUGGEST_COLUMN_QUERY 列中提供唯一的查询文本字符串。如果此列存在且包含当前建议的值,则使用它来重写查询文本并替换上述任一实现。

向快速搜索框显示搜索建议

将应用配置为提供自定义搜索建议后,只需修改可搜索配置以添加 android:includeInGlobalSearch 并将其值设为 "true",即可将这些建议提供给全局可访问的快速搜索框。

唯一需要执行额外工作的情况是,当 content provider 要求读取权限时。在这种情况下,您需要为提供程序添加 <path-permission> 元素,以向快速搜索框授予对 content provider 的读取权限,如以下示例所示:

<provider android:name="MySuggestionProvider"
          android:authorities="com.example.MyCustomSuggestionProvider"
          android:readPermission="com.example.provider.READ_MY_DATA"
          android:writePermission="com.example.provider.WRITE_MY_DATA">
  <path-permission android:pathPrefix="/search_suggest_query"
                   android:readPermission="android.permission.GLOBAL_SEARCH" />
</provider>

在此示例中,提供程序限制了对内容的读写权限。如果存在 "android.permission.GLOBAL_SEARCH" 权限,<path-permission> 元素会授予对 "/search_suggest_query" 路径前缀内内容的读取权限,从而修正该限制。此操作会授予对快速搜索框的访问权限,以便它向您的 content provider 查询建议。

如果您的 content provider 未强制执行读取权限,则默认情况下,快速搜索框会读取该权限。

在设备上启用建议功能

默认情况下,应用不会启用在快速搜索框中提供建议功能,即使它们已配置为这样提供。用户可以通过打开可搜索项(位于设置 > 搜索中)并选择您的应用作为可搜索项来选择是否在快速搜索框中包含来自您的应用的建议。

可供快速搜索框使用的每个应用在可搜索项设置页面中都有一个条目。该条目包含应用名称,以及哪些内容可以从应用中搜索,并在快速搜索框中提供建议。如需为可搜索应用定义说明文本,请将 android:searchSettingsDescription 属性添加到您的可搜索配置,如以下示例所示:

<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
    android:label="@string/app_label"
    android:hint="@string/search_hint"
    android:searchSuggestAuthority="com.example.MyCustomSuggestionProvider"
    android:searchSuggestIntentAction="android.intent.action.VIEW"
    android:includeInGlobalSearch="true"
    android:searchSettingsDescription="@string/search_description" >
</searchable>

使 android:searchSettingsDescription 的字符串尽可能简洁,并说明可搜索的内容。例如,音乐应用的说明为“音乐人、专辑和曲目”,记事本应用的说明为“保存的记事”。提供此说明非常重要,这样用户就能知道应用会提供什么样的建议。当 android:includeInGlobalSearch 为 true 时,请务必添加此属性。

由于用户必须访问设置菜单才能为您的应用启用搜索建议,因此,如果搜索是应用的一个重要方面,请考虑如何向用户传达这一点。例如,您可以在用户首次启动该应用时提供一条备注,说明如何为快速搜索框启用搜索建议。

管理快速搜索框建议快捷方式

用户从快速搜索框中选择的建议可以自动变为快捷方式。这些是系统从 content provider 复制的建议,以便快速访问建议,而无需重新查询 content provider。

默认情况下,系统会为快速搜索框检索到的所有建议启用此功能,但如果建议数据随时间发生变化,您可以请求刷新快捷方式。例如,如果您的建议引用动态数据(如联系人的在线状态),则请求在向用户显示建议快捷方式时刷新。为此,请在建议表格中添加 SUGGEST_COLUMN_SHORTCUT_ID。您可以通过以下任一方式使用此列为每条建议配置快捷方式行为:

  • 让快速搜索框向您的内容提供程序重新查询建议快捷方式的最新版本。

    SUGGEST_COLUMN_SHORTCUT_ID 列中提供一个值,用于在每次显示快捷方式时重新查询建议,以获取新版本。该快捷方式会快速显示最近可用的数据,直到刷新查询返回,此时建议会使用新信息刷新。刷新查询会通过 URI 路径 SUGGEST_URI_PATH_SHORTCUT(而不是 SUGGEST_URI_PATH_QUERY)发送到您的内容提供程序。

    使返回的 Cursor 包含一个与原始建议使用同一列的建议或为空,这表示快捷方式不再有效,在这种情况下,建议会消失并移除该快捷方式。

    如果建议引用的数据可能需要更长时间才能刷新(例如基于网络的刷新),您还可以将值为 true 的 SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING 列添加到建议表中,以显示右侧图标的进度旋转图标,直到刷新完成。除 true 之外的任何值都不会显示进度旋转图标。

  • 完全阻止将建议复制到快捷方式。

    SUGGEST_COLUMN_SHORTCUT_ID 列中提供 SUGGEST_NEVER_MAKE_SHORTCUT 值。在这种情况下,系统绝不会将建议复制到快捷方式中。只有在您绝对不想显示之前复制的建议时,才需要执行此操作。如果您为列提供一个正常值,则只有在刷新查询返回之前,系统才会显示建议快捷方式。

  • 应用默认快捷方式行为。

    对于每个不会更改且可保存为快捷方式的建议,请将 SUGGEST_COLUMN_SHORTCUT_ID 留空。

如果您的所有建议都没有变化,则您不需要 SUGGEST_COLUMN_SHORTCUT_ID 列。

关于快速搜索框建议排名

将应用的搜索建议提供给快速搜索框后,快速搜索框排名就会确定如何针对特定查询向用户显示建议。这可能取决于有多少其他应用具有该查询的结果,以及用户选择您的结果(相较于其他应用中的结果)的频率。无法保证您的建议如何排名,也无法保证针对给定查询是否会显示您的应用的建议。一般来说,提供优质结果会增加应用在显眼位置提供建议的可能性,而提供低质量建议的应用更有可能排名较低或不显示。