添加自定义搜索建议

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

提供自定义建议后,您还可以使它们在系统范围内的快速搜索框中可用,从而可以在您的应用之外访问您的内容。

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

基础知识

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

当用户选择自定义建议时,系统会向您的可搜索活动发送一个 Intent。与发送具有 ACTION_SEARCH 操作的意图的普通搜索查询不同,您可以定义自定义建议来使用 ACTION_VIEW(或任何其他意图操作),还可以包括与所选建议相关的數據。在字典示例中,当用户选择建议时,应用可以立即打开该词的定义,而不是在字典中搜索匹配项。

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

  • 创建搜索界面 中所述,实现一个基本的可搜索活动。
  • 使用有关提供自定义建议的内容提供者的信息修改可搜索配置。
  • SQLiteDatabase 中构建一个表格,用于存放您的建议,并使用必需的列对该表格进行格式化。
  • 创建一个 内容提供者,该提供者可以访问您的建议表格,并在您的清单中声明该提供者。
  • 声明当用户选择建议时要发送的 Intent 类型,包括自定义操作和自定义数据。

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

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

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

一旦显示了自定义建议,可能会发生以下情况

  • 如果用户输入了另一个字母或以任何方式更改了查询,则上述步骤会重复,并且建议列表会相应更新。
  • 如果用户执行搜索,则建议将被忽略,并且搜索将使用正常的 ACTION_SEARCH 意图传递到您的可搜索活动。
  • 如果用户选择了一个建议,则一个意图将被发送到您的可搜索活动,其中包含一个自定义操作和自定义数据,以便您的应用可以打开建议的内容。

修改可搜索配置

要添加对自定义建议的支持,请将 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>

您可能需要其他属性,具体取决于您附加到每个建议的意图类型以及您希望如何格式化对内容提供者的查询。其他可选属性将在以下各节中讨论。

创建内容提供者

要为自定义建议创建内容提供者,请首先参阅 内容提供者,了解如何创建内容提供者。用于自定义建议的内容提供者与任何其他内容提供者类似。但是,对于您提供的每个建议,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 编码规则会对末尾的查询文本进行编码,因此您可能需要在执行搜索之前对其进行解码。

    optional.suggest.path 部分仅在您在可搜索配置文件的 android:searchSuggestPath 属性中设置了此路径时才包含在 URI 中。只有在您对多个可搜索活动使用同一个内容提供者时才需要它。如果是这种情况,请区分建议查询的来源。

  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 参数承载相应的值。在这种情况下,将 android:searchSuggestSelection 属性添加到您的可搜索配置中,并添加您的 SQLite 选择字符串。在选择字符串中,包括一个问号(?)作为实际搜索查询的占位符。系统将使用选择字符串作为 selection 参数调用 query(),并将搜索查询作为 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,它定义了用户选择建议时随每个意图发送的意图操作。这将在 为建议声明意图 部分中进一步讨论。

构建建议表

当您使用 Cursor 向系统返回建议时,系统期望每一行中都有特定的列。无论您是将建议数据存储在设备上的 SQLite 数据库、Web 服务器上的数据库,还是设备或 Web 上的另一种格式,请将建议格式化为表格中的行,并使用 Cursor 显示它们。

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

_ID
每个建议的唯一整数行 ID。系统需要此 ID 在 ListView 中显示建议。
SUGGEST_COLUMN_TEXT_1
作为建议显示的字符串。

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

SUGGEST_COLUMN_TEXT_2
一个字符串。如果您的 Cursor 包含此列,那么所有建议都将以两行格式提供。此列中的字符串将显示为主要建议文本下方较小的第二行文本。它可以为空或空字符串以表示没有辅助文本。
SUGGEST_COLUMN_ICON_1
一个可绘制资源、内容或文件 URI 字符串。如果您的 Cursor 包含此列,那么所有建议都将以图标加文本的格式提供,可绘制图标位于左侧。这可以为空或为零以表示此行没有图标。
SUGGEST_COLUMN_ICON_2
一个可绘制资源、内容或文件 URI 字符串。如果您的 Cursor 包含此列,那么所有建议都将以图标加文本的格式提供,图标位于右侧。这可以为空或为零以表示此行没有图标。
SUGGEST_COLUMN_INTENT_ACTION
一个意图操作字符串。如果此列存在并且在给定行包含一个值,那么此处定义的操作将用于形成建议的意图。如果元素未提供,则操作将从您可搜索配置中的 android:searchSuggestIntentAction 字段中获取。如果您的操作对所有建议都是相同的,那么使用 android:searchSuggestIntentAction 指定操作并省略此列将更有效。
SUGGEST_COLUMN_INTENT_DATA
一个数据 URI 字符串。如果此列存在并且在给定行包含一个值,那么此数据将用于形成建议的意图。如果元素未提供,则数据将从您可搜索配置中的 android:searchSuggestIntentData 字段中获取。如果两个来源都没有提供,则意图的数据字段将为空。如果您的数据对所有建议都相同,或者可以使用常量部分和特定 ID 描述,那么使用 android:searchSuggestIntentData 指定数据并省略此列将更有效。
SUGGEST_COLUMN_INTENT_DATA_ID
一个 URI 路径字符串。如果此列存在并且在给定行包含一个值,那么“/”和此值将附加到意图中的数据字段。仅当您可搜索配置中的 android:searchSuggestIntentData 属性指定的数据字段已设置为适当的基字符串时才使用此列。
SUGGEST_COLUMN_INTENT_EXTRA_DATA
任意数据。如果此列存在并且在给定行包含一个值,那么这是用于形成建议意图的额外数据。如果未提供,则意图的额外数据字段将为空。此列允许建议提供作为意图的 EXTRA_DATA_KEY 键中的额外数据包含的额外数据。
SUGGEST_COLUMN_QUERY
如果此列存在,并且此元素存在于给定行,那么这是用于形成建议查询的数据,包含在作为意图的 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。您必须定义意图的操作和数据。

声明意图操作

自定义建议最常见的意图操作是 ACTION_VIEW,它在您想要打开某些东西时很适合,例如单词的定义、人员的联系信息或网页。但是,意图操作可以是任何其他操作,并且可以对每个建议不同。

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

  • 使用您可搜索配置文件的 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 属性中提供的意图。

声明意图数据

当用户选择一个建议时,您的可搜索活动会接收到您在上一节中讨论的具有您定义的操作的意图,但是意图还必须携带数据,以便您的活动能够识别选择的是哪个建议。具体来说,数据必须是每个建议的唯一内容,例如您 SQLite 表格中建议的行 ID。当收到意图时,您可以使用 getData()getDataString() 检索附加的数据。

您可以通过两种方式定义随意图包含的数据。

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

    通过包含 SUGGEST_COLUMN_INTENT_DATA 列,并在其中填充每行的唯一数据,为建议表中的每个意图提供所有必要的数据信息。此列中的数据将完全按照您在此列中定义的方式附加到意图。然后,您可以使用 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 列的相应值,从而形成一个完整的 Content URI。然后,您可以使用 getData() 来检索 Uri

添加更多数据

如果您需要用您的意图表达更多信息,可以添加另一个表格列,例如 SUGGEST_COLUMN_INTENT_EXTRA_DATA,它可以存储有关建议的附加信息。保存在此列中的数据将放在意图的额外捆绑包的 EXTRA_DATA_KEY 中。

处理意图

在您使用自定义意图提供自定义搜索建议后,您需要您的可搜索活动在用户选择建议时处理这些意图。这除了处理 ACTION_SEARCH 意图(您的可搜索活动已经执行此操作)之外。以下是如何在活动 onCreate() 回调期间处理意图的示例

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);
}

在此示例中,意图操作为 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 列中的内容将用于重写查询文本。仅当 URI 或其他数据格式旨在对用户可见时(例如 HTTP URL)才使用此方法。不要以这种方式使用内部 URI 方案来重写查询。
  • 在建议表的 SUGGEST_COLUMN_QUERY 列中提供唯一的查询文本字符串。如果此列存在并且包含当前建议的值,则将使用它来重写查询文本并覆盖之前的任何实现。

将搜索建议公开到快速搜索框

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

唯一需要额外工作的情况是,当您的内容提供者要求读取权限时。在这种情况下,您需要为提供者添加一个 <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>

在此示例中,提供者限制了对内容的读写权限。 <path-permission> 元素通过授予对 "/search_suggest_query" 路径前缀内的内容的读取权限来修补该限制,前提是存在 "android.permission.GLOBAL_SEARCH" 权限。这允许快速搜索框访问您的内容提供者,以便它可以查询您的内容提供者以获取建议。

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

在设备上启用建议

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

每个可用于快速搜索框的应用在可搜索项目设置页面中都有一个条目。该条目包括应用的名称以及有关从应用中搜索的内容的简短说明,以及在快速搜索框中提供建议。要为您的可搜索应用定义说明文本,请将 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 为真时,请始终包含此属性。

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

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

用户从快速搜索框中选择的建议可以自动变成快捷方式。这些是系统从您的内容提供者复制的建议,以便它可以快速访问建议,而无需重新查询您的内容提供者。

默认情况下,这适用于由快速搜索框检索的所有建议,但如果您的建议数据会随着时间的推移而发生变化,那么您可以请求刷新快捷方式。例如,如果您的建议引用的是动态数据(例如联系人的存在状态),则请求在向用户显示时刷新建议快捷方式。为此,请在建议表中包含 SUGGEST_COLUMN_SHORTCUT_ID。您可以使用此列以以下方式之一配置每个建议的快捷方式行为

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

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

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

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

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

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

  • 让默认快捷方式行为生效。

    SUGGEST_COLUMN_SHORTCUT_ID 留空,以表示不会更改且可以保存为快捷方式的每个建议。

如果您的建议永远不会改变,那么您不需要 SUGGEST_COLUMN_SHORTCUT_ID 列。

关于快速搜索框建议排名

将您的应用的搜索建议提供给快速搜索框后,快速搜索框排名将决定如何为特定查询向用户显示建议。这可能取决于有多少其他应用具有该查询的结果,以及用户选择您的结果的频率与选择其他应用的结果的频率相比。关于您的建议如何排名或您的应用的建议是否为给定查询显示,没有任何保证。一般而言,提供高质量的结果会增加您的应用的建议出现在显眼位置的可能性,而提供低质量建议的应用更有可能排名较低或根本不显示。