添加自定义搜索建议

使用 Android 搜索对话框或搜索微件时,您可以提供由应用数据创建的自定义搜索建议。例如,如果您的应用是词典,则可以在用户完成查询输入之前,根据词典中的词语来建议匹配搜索字段中所输入文本的词语。这些建议很有价值,因为它们可以有效地预测用户的意图并提供即时访问。图 1 显示了带有自定义建议的搜索对话框示例。

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

在添加自定义建议之前,请为应用内的搜索实现 Android 搜索对话框或搜索微件。请参阅创建搜索界面内容提供程序

基础知识

图 1. 带有自定义搜索建议的搜索对话框截图。

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

要提供自定义建议,请执行以下步骤

  • 实现一个基本的、可搜索的 Activity,如创建搜索界面中所述。
  • 修改可搜索配置,添加提供自定义建议的内容提供程序信息。
  • 构建一个表(例如在 SQLiteDatabase 中),用于存储您的建议,并使用必需的列格式化该表。
  • 创建一个可以访问您的建议表的内容提供程序,并在清单文件中声明该提供程序。
  • 声明用户选择建议时将发送的 Intent 类型,包括自定义操作和自定义数据。

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

当系统识别出您的 Activity 是可搜索的并提供搜索建议时,用户输入查询时将执行以下过程

  1. 系统获取搜索查询文本(即目前为止输入的内容),并向管理您的建议的内容提供程序执行查询。
  2. 您的内容提供程序返回一个 Cursor,该 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 类型以及您要如何向内容提供程序格式化查询。其他可选属性将在以下部分讨论。

创建内容提供程序

要为自定义建议创建内容提供程序,首先请参阅内容提供程序,了解如何创建内容提供程序。用于自定义建议的内容提供程序与任何其他内容提供程序类似。但是,对于您提供的每个建议,Cursor 中的相应行必须包含系统能够理解并用于格式化建议的特定列。

当用户在搜索对话框或搜索微件中输入文本时,系统会调用内容提供程序的 query() 方法,每次输入一个字母时都会查询您的内容提供程序以获取建议。在您的 query() 实现中,您的内容提供程序必须搜索您的建议数据,并返回一个 Cursor,指向它确定为良好建议的行。

有关为自定义建议创建内容提供程序的详细信息,将在以下两个部分中讨论

处理建议查询
系统如何向您的内容提供程序发送请求以及如何处理这些请求。
构建建议表
如何定义系统在每次查询返回的 Cursor 中期望的列。

处理建议查询

当系统请求您的内容提供程序提供建议时,它会调用内容提供程序的 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 时,才需要这样做。如果是这种情况,请消除建议查询来源的歧义。

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

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

在 Uri 中获取查询

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

Kotlin

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

Java

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

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

在 selection 参数中获取查询

除了使用 URI 之外,对于您的 query() 方法来说,接收执行查找所需的所有内容可能更有意义,而且您可能希望 selectionselectionArgs 参数携带适当的值。在这种情况下,请将 android:searchSuggestSelection 属性添加到您的可搜索配置中,并包含您的 SQLite selection 字符串。在 selection 字符串中,包含一个问号 (?) 作为实际搜索查询的占位符。系统将调用 query(),其中 selection 字符串作为 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 服务器上的数据库,还是设备或 Web 上的其他格式中,都应将建议格式化为表中的行,并使用 Cursor 来呈现它们。

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

_ID
每个建议的唯一整数行 ID。系统需要此 ID 才能在 ListView 中呈现建议。
SUGGEST_COLUMN_TEXT_1
用作建议的字符串。

以下列都是可选的。大多数将在以下部分进一步讨论。

SUGGEST_COLUMN_TEXT_2
一个字符串。如果您的 Cursor 包含此列,则所有建议都将以双行格式提供。此列中的字符串将显示为主建议文本下方的第二行较小的文本。它可以是 null 或空字符串,表示没有副文本。
SUGGEST_COLUMN_ICON_1
可绘制资源、内容或文件 URI 字符串。如果您的 Cursor 包含此列,则所有建议将以图标加文本格式提供,可绘制图标位于左侧。此值可以为 null 或零,表示此行没有图标。
SUGGEST_COLUMN_ICON_2
可绘制资源、内容或文件 URI 字符串。如果您的 Cursor 包含此列,则所有建议将以图标加文本格式提供,图标位于右侧。此值可以为 null 或零,表示此行没有图标。
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。此列允许建议提供额外数据,这些数据包含在 Intent 的 extra Bundle 的 EXTRA_DATA_KEY 键中。
SUGGEST_COLUMN_QUERY
如果此列存在且此元素在给定行存在,则这是在形成建议的查询时使用的数据,作为额外数据包含在 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

当用户从搜索对话框或微件下方显示的列表中选择建议时,系统会向您的可搜索 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 列中定义每个建议的数据。

    通过包含 SUGGEST_COLUMN_INTENT_DATA 列,然后在其中填充每行的唯一数据,在建议表中为每个 Intent 提供所有必要的数据信息。此列中的数据会完全按照您在此列中定义的方式附加到 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。这与您的可搜索 Activity 已经处理的 ACTION_SEARCH Intent 不同。以下是一个示例,说明您如何在 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,该 URI 由 android:searchSuggestIntentData 字符串和 SUGGEST_COLUMN_INTENT_DATA_ID 列合成。然后,该 URI 将传递给本地 showResult() 方法,该方法查询内容提供程序以获取由 URI 指定的项。

重写查询文本

默认情况下,如果用户使用方向控制(例如轨迹球或方向键)浏览建议列表,查询文本不会更新。但是,您可以使用与焦点中的建议匹配的查询来暂时重写用户在文本框中显示的查询文本。这让用户可以看到建议的查询,并且可以在将其作为搜索分派之前选择搜索框并编辑查询。

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

  • android:searchMode 属性添加到您的可搜索配置中,值为 "queryRewriteFromText"。在这种情况下,使用建议的 SUGGEST_COLUMN_TEXT_1 列中的内容重写查询文本。
  • android:searchMode 属性添加到您的可搜索配置中,值为 "queryRewriteFromData"。在这种情况下,使用建议的 SUGGEST_COLUMN_INTENT_DATA 列中的内容重写查询文本。仅用于 intended 对用户可见的 URI 或其他数据格式,例如 HTTP URL。不要以这种方式使用内部 URI 方案重写查询。
  • 在建议表的 SUGGEST_COLUMN_QUERY 列中提供唯一的查询文本字符串。如果此列存在且包含当前建议的值,则它将用于重写查询文本并覆盖前两种实现中的任何一种。

向快速搜索框公开搜索建议

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

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

<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" 路径前缀内内容的读取访问权限来修改此限制。这授予了快速搜索框的访问权限,以便它可以查询您的内容提供程序以获取建议。

如果您的内容提供程序未强制执行读取权限,则快速搜索框默认会读取它。

在设备上启用建议

默认情况下,应用即使配置了,也未启用在快速搜索框中提供建议。用户通过打开可搜索项(位于设置 > 搜索中)并启用您的应用作为可搜索项来选择是否在快速搜索框中包含您的应用的建议。

每个可用于快速搜索框的应用在可搜索项设置页面中都有一个条目。该条目包含应用的名称以及应用中可搜索并可用于快速搜索框建议内容的简短描述。要定义可搜索应用的描述文本,请将 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 时,务必包含此属性。

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

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

用户从快速搜索框中选择的建议可以自动转换为快捷方式。这些是系统从您的内容提供程序复制的建议,因此可以快速访问建议而无需再次查询您的内容提供程序。

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

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

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

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

    如果建议引用的数据刷新时间可能较长,例如基于网络的刷新,您还可以在建议表中添加 SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING 列,并将其值设置为 true,以便在刷新完成之前为右侧图标显示进度微调器。任何其他值都不会显示进度微调器。

  • 完全阻止建议被复制为快捷方式。

    SUGGEST_COLUMN_SHORTCUT_ID 列中提供 SUGGEST_NEVER_MAKE_SHORTCUT 值。在这种情况下,建议永远不会被复制到快捷方式中。仅当您绝对不希望先前复制的建议出现时才需要这样做。如果您为此列提供正常值,则建议快捷方式只会在刷新查询返回之前出现。

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

    对于每个不变化且可以保存为快捷方式的建议,将 SUGGEST_COLUMN_SHORTCUT_ID 留空。

如果您的建议从未改变,则您不需要 SUGGEST_COLUMN_SHORTCUT_ID 列。

关于快速搜索框建议排名

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