使 TV 应用可供搜索

Android TV 使用 Android 搜索界面 从已安装的应用中检索内容数据,并向用户提供搜索结果。您应用的 内容数据可以包含在这些结果中,以便用户即时访问 。

您的应用必须为 Android TV 提供数据字段,以便 Android TV 可以据此生成搜索建议 当用户在搜索对话框中输入字符时返回的结果。为此,您的应用必须实现 Content Provider, 以及 searchable.xml 配置文件,用于描述 提供商和其他重要信息您还需要一个用于处理 在用户选择建议的搜索结果时触发的 intent。对于 请参阅添加 自定义搜索建议。本指南介绍了针对 Android TV 应用的要点。

在阅读本指南之前,请确保您熟悉 Search API 指南。 此外,请参阅添加搜索功能

本指南中的示例代码来自 <ph type="x-smartling-placeholder"></ph> Leanback 示例应用 ,了解所有最新动态。

标识列

SearchManager 通过将预期数据字段表示为 本地数据库的列。无论数据格式如何,您都必须将数据字段映射到 这些列通常位于访问内容数据的类中。关于构建 一个将现有数据映射到所需字段的类,请参阅 <ph type="x-smartling-placeholder"></ph> 构建建议表格

SearchManager 类包含多个 Android TV 列。其中一些 下表介绍了更重要的列。

说明
SUGGEST_COLUMN_TEXT_1 内容的名称(必填)
SUGGEST_COLUMN_TEXT_2 内容的文本说明
SUGGEST_COLUMN_RESULT_CARD_IMAGE 内容的图片、海报或封面
SUGGEST_COLUMN_CONTENT_TYPE 媒体的 MIME 类型
SUGGEST_COLUMN_VIDEO_WIDTH 媒体的分辨率宽度
SUGGEST_COLUMN_VIDEO_HEIGHT 媒体的分辨率高度
SUGGEST_COLUMN_PRODUCTION_YEAR 内容的制作年份(必需)
SUGGEST_COLUMN_DURATION 媒体的时长(以毫秒为单位)(必填)

搜索框架需要以下列:

当您内容的这些列的值与其他同一内容中的值匹配时 提供商时,系统会提供 深层链接, 查看相应内容,并提供指向其他提供商的应用的链接。本专精课程 详情屏幕中指向应用的深层链接部分。

应用的数据库类可能会按如下所示定义列:

Kotlin

class VideoDatabase {
    companion object {
        // The columns we'll include in the video database table
        val KEY_NAME = SearchManager.SUGGEST_COLUMN_TEXT_1
        val KEY_DESCRIPTION = SearchManager.SUGGEST_COLUMN_TEXT_2
        val KEY_ICON = SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE
        val KEY_DATA_TYPE = SearchManager.SUGGEST_COLUMN_CONTENT_TYPE
        val KEY_IS_LIVE = SearchManager.SUGGEST_COLUMN_IS_LIVE
        val KEY_VIDEO_WIDTH = SearchManager.SUGGEST_COLUMN_VIDEO_WIDTH
        val KEY_VIDEO_HEIGHT = SearchManager.SUGGEST_COLUMN_VIDEO_HEIGHT
        val KEY_AUDIO_CHANNEL_CONFIG = SearchManager.SUGGEST_COLUMN_AUDIO_CHANNEL_CONFIG
        val KEY_PURCHASE_PRICE = SearchManager.SUGGEST_COLUMN_PURCHASE_PRICE
        val KEY_RENTAL_PRICE = SearchManager.SUGGEST_COLUMN_RENTAL_PRICE
        val KEY_RATING_STYLE = SearchManager.SUGGEST_COLUMN_RATING_STYLE
        val KEY_RATING_SCORE = SearchManager.SUGGEST_COLUMN_RATING_SCORE
        val KEY_PRODUCTION_YEAR = SearchManager.SUGGEST_COLUMN_PRODUCTION_YEAR
        val KEY_COLUMN_DURATION = SearchManager.SUGGEST_COLUMN_DURATION
        val KEY_ACTION = SearchManager.SUGGEST_COLUMN_INTENT_ACTION
        ...
    }
    ...
}

Java

public class VideoDatabase {
    // The columns we'll include in the video database table
    public static final String KEY_NAME = SearchManager.SUGGEST_COLUMN_TEXT_1;
    public static final String KEY_DESCRIPTION = SearchManager.SUGGEST_COLUMN_TEXT_2;
    public static final String KEY_ICON = SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE;
    public static final String KEY_DATA_TYPE = SearchManager.SUGGEST_COLUMN_CONTENT_TYPE;
    public static final String KEY_IS_LIVE = SearchManager.SUGGEST_COLUMN_IS_LIVE;
    public static final String KEY_VIDEO_WIDTH = SearchManager.SUGGEST_COLUMN_VIDEO_WIDTH;
    public static final String KEY_VIDEO_HEIGHT = SearchManager.SUGGEST_COLUMN_VIDEO_HEIGHT;
    public static final String KEY_AUDIO_CHANNEL_CONFIG =
            SearchManager.SUGGEST_COLUMN_AUDIO_CHANNEL_CONFIG;
    public static final String KEY_PURCHASE_PRICE = SearchManager.SUGGEST_COLUMN_PURCHASE_PRICE;
    public static final String KEY_RENTAL_PRICE = SearchManager.SUGGEST_COLUMN_RENTAL_PRICE;
    public static final String KEY_RATING_STYLE = SearchManager.SUGGEST_COLUMN_RATING_STYLE;
    public static final String KEY_RATING_SCORE = SearchManager.SUGGEST_COLUMN_RATING_SCORE;
    public static final String KEY_PRODUCTION_YEAR = SearchManager.SUGGEST_COLUMN_PRODUCTION_YEAR;
    public static final String KEY_COLUMN_DURATION = SearchManager.SUGGEST_COLUMN_DURATION;
    public static final String KEY_ACTION = SearchManager.SUGGEST_COLUMN_INTENT_ACTION;
...

构建从 SearchManager 列到数据字段的映射时,您需要 还必须指定 _ID,以便为每一行分配一个唯一 ID。

Kotlin

companion object {
    ....
    private fun buildColumnMap(): MapS<tring, String> {
        return mapOf(
          KEY_NAME to KEY_NAME,
          KEY_DESCRIPTION to KEY_DESCRIPTION,
          KEY_ICON to KEY_ICON,
          KEY_DATA_TYPE to KEY_DATA_TYPE,
          KEY_IS_LIVE to KEY_IS_LIVE,
          KEY_VIDEO_WIDTH to KEY_VIDEO_WIDTH,
          KEY_VIDEO_HEIGHT to KEY_VIDEO_HEIGHT,
          KEY_AUDIO_CHANNEL_CONFIG to KEY_AUDIO_CHANNEL_CONFIG,
          KEY_PURCHASE_PRICE to KEY_PURCHASE_PRICE,
          KEY_RENTAL_PRICE to KEY_RENTAL_PRICE,
          KEY_RATING_STYLE to KEY_RATING_STYLE,
          KEY_RATING_SCORE to KEY_RATING_SCORE,
          KEY_PRODUCTION_YEAR to KEY_PRODUCTION_YEAR,
          KEY_COLUMN_DURATION to KEY_COLUMN_DURATION,
          KEY_ACTION to KEY_ACTION,
          BaseColumns._ID to ("rowid AS " + BaseColumns._ID),
          SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID to ("rowid AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID),
          SearchManager.SUGGEST_COLUMN_SHORTCUT_ID to ("rowid AS " + SearchManager.SUGGEST_COLUMN_SHORTCUT_ID)
        )
    }
}

Java

...
  private static HashMap<String, String> buildColumnMap() {
    HashMap<String, String> map = new HashMap<String, String>();
    map.put(KEY_NAME, KEY_NAME);
    map.put(KEY_DESCRIPTION, KEY_DESCRIPTION);
    map.put(KEY_ICON, KEY_ICON);
    map.put(KEY_DATA_TYPE, KEY_DATA_TYPE);
    map.put(KEY_IS_LIVE, KEY_IS_LIVE);
    map.put(KEY_VIDEO_WIDTH, KEY_VIDEO_WIDTH);
    map.put(KEY_VIDEO_HEIGHT, KEY_VIDEO_HEIGHT);
    map.put(KEY_AUDIO_CHANNEL_CONFIG, KEY_AUDIO_CHANNEL_CONFIG);
    map.put(KEY_PURCHASE_PRICE, KEY_PURCHASE_PRICE);
    map.put(KEY_RENTAL_PRICE, KEY_RENTAL_PRICE);
    map.put(KEY_RATING_STYLE, KEY_RATING_STYLE);
    map.put(KEY_RATING_SCORE, KEY_RATING_SCORE);
    map.put(KEY_PRODUCTION_YEAR, KEY_PRODUCTION_YEAR);
    map.put(KEY_COLUMN_DURATION, KEY_COLUMN_DURATION);
    map.put(KEY_ACTION, KEY_ACTION);
    map.put(BaseColumns._ID, "rowid AS " +
            BaseColumns._ID);
    map.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, "rowid AS " +
            SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
    map.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, "rowid AS " +
            SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
    return map;
  }
...

在前面的示例中,请注意到 SUGGEST_COLUMN_INTENT_DATA_ID 的映射。 字段。这是 URI 中指向此 row — URI 的最后一部分,用于描述内容的存储位置。URI 的第一部分 表中的所有行都是通用的,在 searchable.xml 文件作为 android:searchSuggestIntentData 属性,如 处理搜索建议部分。

如果 URI 中每一行的 URI 第一部分都不同, 表中,将该值与 SUGGEST_COLUMN_INTENT_DATA 字段映射。 当用户选择此内容时,触发的 intent 会提供来自 SUGGEST_COLUMN_INTENT_DATA_ID 的组合 以及 android:searchSuggestIntentData 属性或 SUGGEST_COLUMN_INTENT_DATA 字段值。

提供搜索建议数据

实现 Content Provider 可将搜索字词建议返回 Android TV 搜索对话框。系统查询您的内容 提供程序,通过每次调用 query() 方法来获取建议 所输入的字母。在query()的实现代码中,您的内容 provider 搜索您的建议数据,并返回指向Cursor 指定用于提供建议的行

Kotlin

fun query(uri: Uri, projection: Array<String>, selection: String, selectionArgs: Array<String>,
        sortOrder: String): Cursor {
    // Use the UriMatcher to see what kind of query we have and format the db query accordingly
    when (URI_MATCHER.match(uri)) {
        SEARCH_SUGGEST -> {
            Log.d(TAG, "search suggest: ${selectionArgs[0]} URI: $uri")
            if (selectionArgs == null) {
                throw IllegalArgumentException(
                        "selectionArgs must be provided for the Uri: $uri")
            }
            return getSuggestions(selectionArgs[0])
        }
        else -> throw IllegalArgumentException("Unknown Uri: $uri")
    }
}

private fun getSuggestions(query: String): Cursor {
    val columns = arrayOf<String>(
            BaseColumns._ID,
            VideoDatabase.KEY_NAME,
            VideoDatabase.KEY_DESCRIPTION,
            VideoDatabase.KEY_ICON,
            VideoDatabase.KEY_DATA_TYPE,
            VideoDatabase.KEY_IS_LIVE,
            VideoDatabase.KEY_VIDEO_WIDTH,
            VideoDatabase.KEY_VIDEO_HEIGHT,
            VideoDatabase.KEY_AUDIO_CHANNEL_CONFIG,
            VideoDatabase.KEY_PURCHASE_PRICE,
            VideoDatabase.KEY_RENTAL_PRICE,
            VideoDatabase.KEY_RATING_STYLE,
            VideoDatabase.KEY_RATING_SCORE,
            VideoDatabase.KEY_PRODUCTION_YEAR,
            VideoDatabase.KEY_COLUMN_DURATION,
            VideoDatabase.KEY_ACTION,
            SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID
    )
    return videoDatabase.getWordMatch(query.toLowerCase(), columns)
}

Java

@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
        String sortOrder) {
    // Use the UriMatcher to see what kind of query we have and format the db query accordingly
    switch (URI_MATCHER.match(uri)) {
        case SEARCH_SUGGEST:
            Log.d(TAG, "search suggest: " + selectionArgs[0] + " URI: " + uri);
            if (selectionArgs == null) {
                throw new IllegalArgumentException(
                        "selectionArgs must be provided for the Uri: " + uri);
            }
            return getSuggestions(selectionArgs[0]);
        default:
            throw new IllegalArgumentException("Unknown Uri: " + uri);
    }
}

private Cursor getSuggestions(String query) {
    query = query.toLowerCase();
    String[] columns = new String[]{
        BaseColumns._ID,
        VideoDatabase.KEY_NAME,
        VideoDatabase.KEY_DESCRIPTION,
        VideoDatabase.KEY_ICON,
        VideoDatabase.KEY_DATA_TYPE,
        VideoDatabase.KEY_IS_LIVE,
        VideoDatabase.KEY_VIDEO_WIDTH,
        VideoDatabase.KEY_VIDEO_HEIGHT,
        VideoDatabase.KEY_AUDIO_CHANNEL_CONFIG,
        VideoDatabase.KEY_PURCHASE_PRICE,
        VideoDatabase.KEY_RENTAL_PRICE,
        VideoDatabase.KEY_RATING_STYLE,
        VideoDatabase.KEY_RATING_SCORE,
        VideoDatabase.KEY_PRODUCTION_YEAR,
        VideoDatabase.KEY_COLUMN_DURATION,
        VideoDatabase.KEY_ACTION,
        SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID
    };
    return videoDatabase.getWordMatch(query, columns);
}
...

在您的清单文件中,内容提供程序会受到特殊对待。不像 标记为 activity,则被描述为 <provider>。通过 provider 包含 android:authorities 属性,告知系统 内容提供程序的命名空间内此外,您还必须将其 android:exported 属性设置为 "true",以便 Android 全局搜索可以使用从中返回的结果。

<provider android:name="com.example.android.tvleanback.VideoContentProvider"
    android:authorities="com.example.android.tvleanback"
    android:exported="true" />

处理搜索建议

应用程序必须包含 res/xml/searchable.xml 文件,用于配置搜索建议设置。

res/xml/searchable.xml 文件中,添加 <ph type="x-smartling-placeholder"></ph> android:searchSuggestAuthority 属性告知系统 content provider。该值必须与您在 android:authorities <provider> 的属性 元素。AndroidManifest.xml

此外,还要包含标签, 这是应用的名称系统搜索设置在进行枚举时使用该标签 搜索应用。

searchable.xml 文件 必须包含 android:searchSuggestIntentAction,值为 "android.intent.action.VIEW" 定义用于提供自定义建议的 intent 操作。这不同于 操作以提供搜索字词,如下一部分所述。 如需了解声明 intent 操作以提供建议的其他方式, 请参阅声明 intent 操作

除了 intent 操作之外,您的应用还必须提供 intent 数据,您可以使用 <ph type="x-smartling-placeholder"></ph> android:searchSuggestIntentData 属性。这是 URI 的第一部分 内容,用于说明该内容的映射表中所有行共有的 URI 部分 内容。URI 中每行的唯一部分是通过 SUGGEST_COLUMN_INTENT_DATA_ID 字段建立的, ,具体说明请参阅标识列部分。 如需了解声明 intent 数据以提供建议的其他方式,请参阅 声明 intent 数据

android:searchSuggestSelection=" ?" 属性指定传递的值 作为 query()selection 参数 方法。问号 (?) 值会替换为查询文本。

最后,您还必须添加 值为 "true"android:includeInGlobalSearch 属性。下面是一个示例 searchable.xml 文件:

<searchable xmlns:android="http://schemas.android.com/apk/res/android"
    android:label="@string/search_label"
    android:hint="@string/search_hint"
    android:searchSettingsDescription="@string/settings_description"
    android:searchSuggestAuthority="com.example.android.tvleanback"
    android:searchSuggestIntentAction="android.intent.action.VIEW"
    android:searchSuggestIntentData="content://com.example.android.tvleanback/video_database_leanback"
    android:searchSuggestSelection=" ?"
    android:searchSuggestThreshold="1"
    android:includeInGlobalSearch="true">
</searchable>

处理搜索字词

只要搜索对话框中包含的字词与应用某个列中的值匹配,系统就会立即触发 标识列部分中所述,系统会触发 ACTION_SEARCH intent。 应用中负责处理该操作的 activity intent 会在代码库中搜索值中包含给定字词的列,并返回列表 与这些列相关联。在 AndroidManifest.xml 文件中,指定 负责处理 ACTION_SEARCH 的 activity intent,如以下示例所示:

...
  <activity
      android:name="com.example.android.tvleanback.DetailsActivity"
      android:exported="true">

      <!-- Receives the search request. -->
      <intent-filter>
          <action android:name="android.intent.action.SEARCH" />
          <!-- No category needed, because the Intent will specify this class component -->
      </intent-filter>

      <!-- Points to searchable meta data. -->
      <meta-data android:name="android.app.searchable"
          android:resource="@xml/searchable" />
  </activity>
...
  <!-- Provides search suggestions for keywords against video meta data. -->
  <provider android:name="com.example.android.tvleanback.VideoContentProvider"
      android:authorities="com.example.android.tvleanback"
      android:exported="true" />
...

该 activity 还必须描述可搜索配置,并引用 searchable.xml 文件。 如需使用全局搜索对话框,请执行以下操作: 清单必须说明哪个 activity 应接收搜索查询。清单还必须 描述<provider> 元素,这与 searchable.xml 文件中的说明完全一样。

详情屏幕中指向应用的深层链接

如果您已按照处理搜索 建议部分,并映射 SUGGEST_COLUMN_TEXT_1SUGGEST_COLUMN_PRODUCTION_YEARSUGGEST_COLUMN_DURATION 字段,如 标识列部分, 深层链接指向您内容的观看操作,会显示在 当用户选择搜索结果时:

详情屏幕中的深层链接

当用户选择您的应用链接时,该链接由 详情屏幕,系统会启动处理 ACTION_VIEW 的 activity 设为 值为 "android.intent.action.VIEW"android:searchSuggestIntentAction searchable.xml 文件。

您还可以设置自定义 intent 来启动您的 activity。如示例所示, <ph type="x-smartling-placeholder"></ph> Leanback 示例应用 ,了解所有最新动态。请注意,示例应用会启动自己的 LeanbackDetailsFragment,以 显示所选媒体的详细信息;启动用于播放媒体内容的 activity 以便让用户再点击一两次

搜索行为

在 Android TV 中,您可以从主屏幕和应用内部使用搜索功能。搜索结果 会有所不同

在主屏幕上搜索

当用户从主屏幕进行搜索时,第一条结果会显示在实体卡片中。如果有 的应用,卡片底部会显示每个应用的链接:

TV 搜索结果播放

您无法以编程方式将应用放入实体卡中。以 则应用的搜索结果必须与 搜索内容。

卡片下方可能会显示更多搜索结果。要想看到它们,用户必须按下 然后向下滚动。每个应用的结果会显示在单独的一行中。您无法控制 行排序。支持的应用 观看操作最先列出。

电视搜索结果

在应用中搜索

用户也可以通过遥控器或 游戏手柄控制器搜索结果会显示在应用内容顶部的一行中。 您的应用会使用自己的全局搜索提供程序生成搜索结果。

TV 应用内搜索结果

了解详情

要详细了解如何搜索电视应用,请阅读 将 Android 搜索功能集成到您的应用中 添加搜索功能

如需详细了解如何使用 SearchFragment 自定义应用内搜索体验,请阅读下文 在 TV 应用内搜索