Android TV 主屏幕(简称“主屏幕”)提供了一个 UI,它以**频道**和**节目**的表格形式显示推荐内容。每行都是一个频道。频道包含该频道上每个节目的卡片。
本文档展示了如何在主屏幕上添加频道和节目、更新内容、处理用户操作以及为用户提供最佳体验。 (如果您想深入了解 API,请尝试主屏幕 codelab并观看I/O 2017 Android TV 会议。)
注意:推荐频道仅在 Android 8.0(API 级别 26)及更高版本中可用。您必须使用它们才能为在 Android 8.0(API 级别 26)及更高版本上运行的应用提供推荐。要为在早期版本的 Android 上运行的应用提供推荐,您的应用必须使用推荐行。
主屏幕 UI
应用可以创建新频道、添加、删除和更新频道中的节目,以及控制频道中节目的顺序。例如,应用可以创建一个名为“最新”的频道,并显示新发布节目的卡片。
应用无法控制频道在主屏幕上显示的顺序。当您的应用创建新频道时,主屏幕会将其添加到频道列表的底部。用户可以重新排序、隐藏和显示频道。
Watch Next 频道
Watch Next 频道是主屏幕上显示的第二行,位于应用行之后。系统会创建和维护此频道。您的应用可以将节目添加到 Watch Next 频道。有关更多信息,请参阅将节目添加到 Watch Next 频道。
应用频道
您应用创建的所有频道都遵循以下生命周期
- 用户在您的应用中发现一个频道并请求将其添加到主屏幕。
- 应用创建频道并将其添加到
TvProvider
中(此时该频道不可见)。 - 应用要求系统显示频道。
- 系统要求用户批准新频道。
- 新频道显示在主屏幕的最后一行。
默认频道
您的应用可以为用户提供任意数量的频道,供用户添加到主屏幕。 用户通常需要选择并批准每个频道,然后才能将其显示在主屏幕上。 每个应用都有创建单个 *默认* 频道的选项。 默认频道是特殊的,因为它会自动出现在主屏幕上;用户无需显式请求它。
先决条件
Android TV 主屏幕使用 Android 的 TvProvider
API 来管理您的应用创建的频道和节目。 要访问提供者的数据,请将以下权限添加到您的应用清单中
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
TvProvider
支持库使使用提供者变得更加容易。 将其添加到您的 build.gradle
文件中的依赖项中
Groovy
implementation 'androidx.tvprovider:tvprovider:1.0.0'
Kotlin
implementation("androidx.tvprovider:tvprovider:1.0.0")
要使用频道和节目,请确保在您的程序中包含这些支持库导入
Kotlin
import android.support.media.tv.Channel import android.support.media.tv.TvContractCompat import android.support.media.tv.ChannelLogoUtils import android.support.media.tv.PreviewProgram import android.support.media.tv.WatchNextProgram
Java
import android.support.media.tv.Channel; import android.support.media.tv.TvContractCompat; import android.support.media.tv.ChannelLogoUtils; import android.support.media.tv.PreviewProgram; import android.support.media.tv.WatchNextProgram;
频道
您的应用创建的第一个频道将成为其默认频道。 默认频道会自动出现在主屏幕上。 您创建的所有其他频道都必须由用户选择并接受,才能出现在主屏幕上。
创建频道
您的应用应该只在运行在前台时要求系统显示新添加的频道。 这可以防止您的应用在用户运行其他应用时显示对话框,要求批准添加您的频道。 如果你尝试在后台运行时添加频道,该活动的 onActivityResult()
方法将返回状态代码 RESULT_CANCELED
。
要创建频道,请执行以下步骤
创建一个频道生成器并设置其属性。 请注意,频道类型必须为
TYPE_PREVIEW
。 根据需要添加更多 属性 。Kotlin
val builder = Channel.Builder() // Every channel you create must have the type
TYPE_PREVIEW
builder.setType(TvContractCompat.Channels.TYPE_PREVIEW) .setDisplayName("Channel Name") .setAppLinkIntentUri(uri)Java
Channel.Builder builder = new Channel.Builder(); // Every channel you create must have the type
TYPE_PREVIEW
builder.setType(TvContractCompat.Channels.TYPE_PREVIEW) .setDisplayName("Channel Name") .setAppLinkIntentUri(uri);将频道插入提供者
Kotlin
var channelUri = context.contentResolver.insert( TvContractCompat.Channels.CONTENT_URI, builder.build().toContentValues())
Java
Uri channelUri = context.getContentResolver().insert( TvContractCompat.Channels.CONTENT_URI, builder.build().toContentValues());
-
您需要保存频道 ID,以便稍后将节目添加到该频道。 从返回的 URI 中提取频道 ID
Kotlin
var channelId = ContentUris.parseId(channelUri)
Java
long channelId = ContentUris.parseId(channelUri);
您必须为您的频道添加徽标。 使用
Uri
或Bitmap
。 徽标图标应为 80dp x 80dp,并且必须不透明。 它显示在圆形蒙版下Kotlin
// Choose one or the other storeChannelLogo(context: Context, channelId: Long, logoUri: Uri) // also works if logoUri is a URL storeChannelLogo(context: Context, channelId: Long, logo: Bitmap)
Java
// Choose one or the other storeChannelLogo(Context context, long channelId, Uri logoUri); // also works if logoUri is a URL storeChannelLogo(Context context, long channelId, Bitmap logo);
创建默认频道(可选):当您的应用创建其第一个频道时,您可以将其设置为 默认频道 ,以便它立即出现在主屏幕上,无需任何用户操作。 您创建的任何其他频道都不可见,除非用户显式地 选择 它们。
Kotlin
TvContractCompat.requestChannelBrowsable(context, channelId)
Java
TvContractCompat.requestChannelBrowsable(context, channelId);
- 在打开您的应用之前,让您的默认频道出现。 您可以通过添加一个
BroadcastReceiver
来实现此行为,该BroadcastReceiver
监听android.media.tv.action.INITIALIZE_PROGRAMS
操作,该操作是在安装应用后由主屏幕发送的<receiver android:name=".RunOnInstallReceiver" android:exported="true"> <intent-filter> <action android:name="android.media.tv.action.INITIALIZE_PROGRAMS" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </receiver>
在开发过程中侧载您的应用时,您可以通过 adb 触发意图来测试此步骤,其中 your.package.name/.YourReceiverName 是您的应用的BroadcastReceiver
adb shell am broadcast -a android.media.tv.action.INITIALIZE_PROGRAMS -n \ your.package.name/.YourReceiverName
在极少数情况下,您的应用可能会在用户启动您的应用的同时收到广播。 确保您的代码不会尝试多次添加默认频道。
更新频道
更新频道与创建频道非常相似。
使用另一个 Channel.Builder
来设置需要更改的属性。
使用 ContentResolver
来更新频道。 使用您在最初添加频道时保存的频道 ID
Kotlin
context.contentResolver.update( TvContractCompat.buildChannelUri(channelId), builder.build().toContentValues(), null, null )
Java
context.getContentResolver().update(TvContractCompat.buildChannelUri(channelId), builder.build().toContentValues(), null, null);
要更新频道的徽标,请使用 storeChannelLogo()
。
删除频道
Kotlin
context.contentResolver.delete(TvContractCompat.buildChannelUri(channelId), null, null)
Java
context.getContentResolver().delete(TvContractCompat.buildChannelUri(channelId), null, null);
节目
将节目添加到应用频道
创建一个 PreviewProgram.Builder
并设置其属性
Kotlin
val builder = PreviewProgram.Builder() builder.setChannelId(channelId) .setType(TvContractCompat.PreviewPrograms.TYPE_CLIP) .setTitle("Title") .setDescription("Program description") .setPosterArtUri(uri) .setIntentUri(uri) .setInternalProviderId(appProgramId)
Java
PreviewProgram.Builder builder = new PreviewProgram.Builder(); builder.setChannelId(channelId) .setType(TvContractCompat.PreviewPrograms.TYPE_CLIP) .setTitle("Title") .setDescription("Program description") .setPosterArtUri(uri) .setIntentUri(uri) .setInternalProviderId(appProgramId);
根据节目类型添加更多属性。(要查看每种节目类型可用的属性,请参考下面的 表格。)
将节目插入提供者
Kotlin
var programUri = context.contentResolver.insert(TvContractCompat.PreviewPrograms.CONTENT_URI, builder.build().toContentValues())
Java
Uri programUri = context.getContentResolver().insert(TvContractCompat.PreviewPrograms.CONTENT_URI, builder.build().toContentValues());
检索节目 ID 以供将来参考
Kotlin
val programId = ContentUris.parseId(programUri)
Java
long programId = ContentUris.parseId(programUri);
将节目添加到“继续观看”频道
要将节目插入“继续观看”频道,请参阅 将节目添加到“继续观看”频道 。
更新节目
您可以更改节目的信息。 例如,您可能想要更新电影的租赁价格,或更新进度条,显示用户观看过多少节目。
使用 PreviewProgram.Builder
来设置您需要更改的属性,然后调用 getContentResolver().update
来更新节目。 指定您在最初添加节目时保存的节目 ID
Kotlin
context.contentResolver.update( TvContractCompat.buildPreviewProgramUri(programId), builder.build().toContentValues(), null, null )
Java
context.getContentResolver().update(TvContractCompat.buildPreviewProgramUri(programId), builder.build().toContentValues(), null, null);
删除节目
Kotlin
context.contentResolver .delete(TvContractCompat.buildPreviewProgramUri(programId), null, null)
Java
context.getContentResolver().delete(TvContractCompat.buildPreviewProgramUri(programId), null, null);
处理用户操作
您的应用可以通过提供 UI 来显示和添加频道来帮助用户发现内容。 您的应用还应该在频道出现在主屏幕后处理与其的交互。
发现和添加频道
您的应用可以提供一个 UI 元素,让用户选择并添加其频道(例如,一个按钮,询问是否要添加频道)。
在用户请求特定频道后,执行此代码以获取用户将其添加到主屏幕 UI 的权限
Kotlin
val intent = Intent(TvContractCompat.ACTION_REQUEST_CHANNEL_BROWSABLE) intent.putExtra(TvContractCompat.EXTRA_CHANNEL_ID, channelId) try { activity.startActivityForResult(intent, 0) } catch (e: ActivityNotFoundException) { // handle error }
Java
Intent intent = new Intent(TvContractCompat.ACTION_REQUEST_CHANNEL_BROWSABLE); intent.putExtra(TvContractCompat.EXTRA_CHANNEL_ID, channelId); try { activity.startActivityForResult(intent, 0); } catch (ActivityNotFoundException e) { // handle error }
系统会显示一个对话框,要求用户批准该频道。 在活动的 onActivityResult
方法 (Activity.RESULT_CANCELED
或 Activity.RESULT_OK
) 中处理请求的结果。
Android TV 主屏幕事件
当用户与应用发布的节目/频道进行交互时,主屏幕会向应用发送意图
- 当用户选择频道的徽标时,主屏幕会将存储在频道 APP_LINK_INTENT_URI 属性中的
Uri
发送到应用。 应用应该只启动其主 UI 或与所选频道相关的视图。 - 当用户选择节目时,主屏幕会将存储在节目 INTENT_URI 属性中的
Uri
发送到应用。 应用应该播放所选内容。 - 用户可以表明他们不再对某个节目感兴趣,并希望将其从主屏幕的 UI 中删除。 系统会从 UI 中删除节目,并向拥有该节目的应用发送一个意图(android.media.tv.ACTION_PREVIEW_PROGRAM_BROWSABLE_DISABLED 或 android.media.tv.ACTION_WATCH_NEXT_PROGRAM_BROWSABLE_DISABLED),其中包含节目的 ID。 应用应该从提供者中删除该节目,并且不应该重新插入它。
确保为主屏幕发送的所有用于用户交互的 Uris
创建意图过滤器;例如
<receiver
android:name=".WatchNextProgramRemoved"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.media.tv.ACTION_WATCH_NEXT_PROGRAM_BROWSABLE_DISABLED" />
</intent-filter>
</receiver>
最佳实践
- 许多电视应用要求用户登录。 在这种情况下,
BroadcastReceiver
会监听android.media.tv.action.INITIALIZE_PROGRAMS
,它应该为未经身份验证的用户推荐频道内容。 例如,您的应用可以最初显示最佳内容或当前流行内容。 用户登录后,它可以显示个性化内容。 这是应用在用户登录前向上销售用户的绝佳机会。 - 当您的应用不在前台,并且您需要更新频道或节目时,请使用
JobScheduler
来安排工作(请参阅:JobScheduler 和 JobService)。 - 如果您的应用行为不当(例如:不断向提供者发送数据),系统可能会撤销您应用的提供者权限。 确保使用 try-catch 子句包装访问提供者的代码,以处理安全异常。
在更新节目和频道之前,查询提供者以获取您需要更新的数据并协调数据。 例如,没有必要更新用户想要从 UI 中删除的节目。 使用一个后台作业,在查询现有数据后将您的数据插入/更新到提供者,然后请求批准您的频道。 您可以启动此作业,当应用启动时,以及当应用需要更新其数据时。
Kotlin
context.contentResolver .query( TvContractCompat.buildChannelUri(channelId), null, null, null, null).use({ cursor-> if (cursor != null and cursor.moveToNext()) { val channel = Channel.fromCursor(cursor) if (channel.isBrowsable()) { //update channel's programs } } })
Java
try (Cursor cursor = context.getContentResolver() .query( TvContractCompat.buildChannelUri(channelId), null, null, null, null)) { if (cursor != null && cursor.moveToNext()) { Channel channel = Channel.fromCursor(cursor); if (channel.isBrowsable()) { //update channel's programs } } }
为所有图像(徽标、图标、内容图像)使用唯一的 Uris。 确保在更新图像时使用不同的 Uri。 所有图像都会被缓存。 如果您在更改图像时未更改 Uri,则旧图像将继续出现。
请记住,不允许使用 WHERE 子句,并且对使用 WHERE 子句的提供者的调用会抛出安全异常。
属性
本节分别描述频道和节目属性。
频道属性
您必须为每个频道指定这些属性
属性 | 备注 |
---|---|
TYPE | 设置为 TYPE_PREVIEW 。 |
DISPLAY_NAME | 设置为频道的名称。 |
APP_LINK_INTENT_URI | 当用户选择频道的徽标时,系统会发送一个意图,启动一个活动来呈现与该频道相关的内容。 将此属性设置为该活动意图过滤器中使用的 Uri。 |
此外,频道还有六个为内部应用使用而保留的字段。 这些字段可用于存储密钥或其他值,这些值可以帮助应用将频道映射到其内部数据结构
- INTERNAL_PROVIDER_ID
- INTERNAL_PROVIDER_DATA
- INTERNAL_PROVIDER_FLAG1
- INTERNAL_PROVIDER_FLAG2
- INTERNAL_PROVIDER_FLAG3
- INTERNAL_PROVIDER_FLAG4
节目属性
请参阅各个页面,了解每种节目类型的属性
示例代码
要详细了解构建与主屏幕交互并向 Android TV 主屏幕添加频道和节目的应用,请参阅我们的主屏幕 codelab 。