Android TV 使用 Android 搜索界面从已安装的应用中检索内容数据,并将搜索结果提供给用户。您的应用的内容数据可以包含在这些结果中,以便用户即时访问您应用中的内容。
您的应用必须向 Android TV 提供数据字段,Android TV 可以根据这些字段在用户在搜索对话框中输入字符时生成建议的搜索结果。为此,您的应用必须实现一个 内容提供器,用于提供建议,以及一个 searchable.xml
配置文件,该文件描述了内容提供器和 Android TV 的其他重要信息。您还需要一个活动来处理当用户选择建议的搜索结果时触发的 Intent。有关更多详细信息,请参阅添加自定义搜索建议。本指南涵盖了 Android TV 应用的特定要点。
在阅读本指南之前,请确保您熟悉 搜索 API 指南中解释的概念。此外,请查看添加搜索功能。
本指南中的示例代码来自 Leanback 示例应用。
识别列
SearchManager
通过将其表示为本地数据库的列来描述其期望的数据字段。无论您的数据格式如何,您都必须将您的数据字段映射到这些列,通常在访问您内容数据类中进行。有关构建将现有数据映射到所需字段的类的详细信息,请参阅构建建议表。
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 |
您的媒体的时长(毫秒)(必需) |
搜索框架需要以下列
当您内容的这些列的值与 Google 服务器找到的其他提供商的相同内容的值匹配时,系统会在内容详细信息视图中提供指向您应用的深层链接,以及指向其他提供商应用的链接。这将在深层链接到详细信息屏幕中的应用部分中进一步讨论。
您的应用程序的数据库类可以定义如下列
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(): Map<String, 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 部分——URI 的最后一部分,描述内容存储在哪里。URI 的第一部分(当它对表中的所有行都通用时)在 searchable.xml
文件中设置为 android:searchSuggestIntentData
属性,如处理搜索建议部分所述。
如果 URI 的第一部分对于表中的每一行都不同,请使用 SUGGEST_COLUMN_INTENT_DATA
字段映射该值。当用户选择此内容时,触发的 Intent 将提供来自 SUGGEST_COLUMN_INTENT_DATA_ID
和 android:searchSuggestIntentData
属性或 SUGGEST_COLUMN_INTENT_DATA
字段值组合的 Intent 数据。
提供搜索建议数据
实现一个 内容提供器,以向 Android TV 搜索对话框返回搜索词建议。系统通过在每次输入一个字母时调用 query()
方法来查询您的内容提供器以获取建议。在您对 query()
的实现中,您的内容提供器搜索您的建议数据并返回一个指向您已指定为建议的行的 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); } ...
在您的清单文件中,内容提供器会受到特殊处理。它不是被标记为活动,而是被描述为 <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
文件中,包含 android:searchSuggestAuthority
属性,以告知系统您的内容提供器的命名空间。这必须与您在 AndroidManifest.xml
文件中 <provider>
元素的 android:authorities
属性中指定的字符串值匹配。
还要包含一个标签,即应用程序的名称。系统搜索设置在枚举可搜索应用时会使用此标签。
searchable.xml
文件还必须包含 android:searchSuggestIntentAction
,其值为 "android.intent.action.VIEW"
,用于定义提供自定义建议的 Intent 操作。这与提供搜索词的 Intent 操作不同,如以下部分所述。有关声明建议 Intent 操作的其他方式,请参阅声明 Intent 操作。
除了 Intent 操作之外,您的应用还必须提供 Intent 数据,您可以使用 android:searchSuggestIntentData
属性指定该数据。这是指向内容 URI 的第一部分,它描述了内容映射表中所有行共用的 URI 部分。URI 中每行特有的部分是通过 SUGGEST_COLUMN_INTENT_DATA_ID
字段建立的,如识别列部分所述。有关声明建议 Intent 数据的其他方式,请参阅声明 Intent 数据。
android:searchSuggestSelection=" ?"
属性指定作为 query()
方法的 selection
参数传递的值。问号 (?
) 值将替换为查询文本。
最后,您还必须包含 android:includeInGlobalSearch
属性,其值为 "true"
。以下是一个示例 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。您的应用中处理该 Intent 的活动会搜索存储库中包含给定词语的列,并返回包含这些列的内容项列表。在您的 AndroidManifest.xml
文件中,您将处理 ACTION_SEARCH
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" /> ...
该活动还必须通过引用 searchable.xml
文件来描述可搜索配置。要使用全局搜索对话框,清单必须描述哪个活动应该接收搜索查询。清单还必须描述 <provider>
元素,其描述方式与 searchable.xml
文件中的描述完全相同。
深层链接到详细信息屏幕中的应用
如果您已按照处理搜索建议部分所述设置搜索配置,并按照识别列部分所述映射了 SUGGEST_COLUMN_TEXT_1
、SUGGEST_COLUMN_PRODUCTION_YEAR
和 SUGGEST_COLUMN_DURATION
字段,则当用户选择搜索结果时,将启动的详细信息屏幕中将显示指向您的内容的观看操作的深层链接

当用户选择您的应用的链接(由详细信息屏幕中的“**在以下位置可用**”按钮标识)时,系统将启动处理设置为 searchable.xml
文件中 android:searchSuggestIntentAction
且值为 "android.intent.action.VIEW"
的 ACTION_VIEW
的活动。
您还可以设置自定义 Intent 以启动您的活动。这在 Leanback 示例应用中有所演示。请注意,该示例应用会启动其自己的 LeanbackDetailsFragment
来显示所选媒体的详细信息;在您的应用中,请立即启动播放媒体的活动,以节省用户一两次点击。
搜索行为
搜索功能在 Android TV 中可从主屏幕和应用内部使用。这两种情况下的搜索结果有所不同。
从主屏幕搜索
当用户从主屏幕搜索时,第一个结果会显示在一个实体卡片中。如果有可以播放内容的应用程序,则每个应用程序的链接会显示在卡片底部

您无法以编程方式将应用放置到实体卡片中。要作为播放选项包含在内,应用的搜索结果必须与搜索内容的标题、年份和时长匹配。
卡片下方可能提供更多搜索结果。要查看它们,用户必须按下遥控器并向下滚动。每个应用的结果显示在单独的一行中。您无法控制行顺序。支持观看操作的应用会首先列出。

从您的应用内搜索
用户还可以通过从遥控器或游戏手柄控制器启动麦克风,从您的应用内部开始搜索。搜索结果以单行显示在应用内容上方。您的应用使用自己的全局搜索提供程序生成搜索结果。

了解更多
要了解有关搜索电视应用的更多信息,请阅读将 Android 搜索功能集成到您的应用中和添加搜索功能。
有关如何使用 SearchFragment
自定义应用内搜索体验的更多信息,请阅读在电视应用内搜索。