在此 Codelab 中,您将学习如何使用 Kotlin 和 AndroidX 库构建向 Android TV 主屏幕中添加频道和节目的应用。此 Codelab 并未涵盖主屏幕中的所有功能。请阅读相关文档,了解主屏幕的所有特性和功能。
概念
Android TV 主屏幕(或简称“主屏幕”)提供了一个以频道和节目表格的形式显示推荐内容的界面。每行对应一个频道。每个频道中包含一些卡片,每个卡片对应着该频道上提供的一个节目。您的应用可以提供任意数量的频道供用户添加到主屏幕。通常,在用户选择并批准频道后,频道才会显示在主屏幕上。
每个应用都可以创建一个默认频道。默认频道比较特殊,因为它会自动显示在主屏幕中;用户无需明确要求添加该频道。
概览
此 Codelab 演示如何在主屏幕上创建、添加和更新频道和节目。它使用包含合集和影片的模拟数据库。为简单起见,对于所有订阅都使用同一个影片列表。
克隆入门级项目代码库
本 Codelab 使用 Android Studio,后者是用于开发 Android 应用的 IDE。
如果您尚未安装该软件,请下载并安装它。
您可以从 GitHub 代码库下载源代码:
git clone https://github.com/googlecodelabs/tv-recommendations-kotlin.git
也可以下载它的 ZIP 文件。
打开 Android Studio,从菜单栏中依次点击 File > Open,或者从启动画面中点击 Open an Existing Android Studio Project,然后选择最近克隆的文件夹。
了解入门级项目
该项目中有四个步骤。在每个步骤中,您都需要向应用中添加更多代码,在完成每个部分中的所有说明后,可以将结果与下一步中的代码进行比较。
下面是该应用中的主要组件:
MainActivity
是项目的入口 Activity。model/TvMediaBackground
是一个对象,表示浏览影片时的背景图片。model
/TvMediaCollection
是一个表示影片合集的对象。model/TvMediaMetadata
是一个用于存储影片信息的对象。model/TvMediaDatabase
表示数据库持有者,并作为底层影片数据的主要访问点。fragments/NowPlayingFragment
用于播放影片。fragments
/MediaBrowserFragment
是媒体浏览器 Fragment。workers/TvMediaSynchronizer
是一个数据同步管理程序类,其中包含用于提取 Feed、构建对象和更新频道的代码。utils/TvLauncherUtils
是一个帮助程序类,用于使用 AndroidX 库和 TV Provider 管理频道和预览节目。
运行入门级项目
尝试运行该项目。如果您遇到问题,请参阅关于使用入门的文档。
- 连接 Android TV 或启动模拟器。
- 选择 step_1 配置,选择您的 Android 设备,然后按菜单栏中的 run 按钮。
- 您应该会看到一个简单的 TV 应用轮廓,其中包含三个视频合集。
您学到的内容
在此简介中,您学习了以下内容:
- TV 主屏幕及其频道
- 此 Codelab 中的项目代码结构和主要类
接下来做什么?
在主屏幕上添加频道
首先在主屏幕上添加频道。添加完频道后,您就可以在频道中插入节目了。用户可在频道配置面板中发现您的频道,并选择在主屏幕界面中显示哪些频道。此 Codelab 会为每个媒体合集创建频道:
- Historical Feature Films
- 1910's Feature Films
- Charlie Chaplin Collection
以下部分将介绍如何加载数据并将其用于频道。
TvMediaSynchronizer
中的 synchronize()
方法会执行以下操作:
- 提取媒体 Feed,其中包含背景图片、媒体合集和视频元数据。此信息在
assets/media-feed.json
中定义 - 更新
TvMediaDatabase
实例,该实例用于将背景图片、媒体合集和视频数据存储到相应的对象中 - 使用
TvLauncherUtils
创建或更新频道和节目
不用担心此 Codelab 中的数据加载。此 Codelab 的目标是帮助您了解如何使用 AndroidX 库创建频道。为此,您需向 TvLauncherUtils
类中的若干个方法添加代码。
创建频道
提取媒体数据并将其保存到本地数据库后,项目代码会将媒体 Collection
转换为频道。该代码会在 TvLauncherUtils
类的 upsertChannel()
方法中创建和更新频道。
- 创建
PreviewChannel.Builder()
的实例。为避免频道出现重复,此 Codelab 会检查频道是否存在,如果频道存在,则只会对其进行更新。每个视频合集都有一个关联的 ID,您可以将其用作频道的internalProviderId
。如需标识现有的频道,请将其internalProviderId
与合集 ID 进行比较。复制下方代码并将其粘贴到upsertChannel()
中的如下代码注释处://TODO: Step 1 create or find an existing channel.
val channelBuilder = if (existingChannel == null) {
PreviewChannel.Builder()
} else {
PreviewChannel.Builder(existingChannel)
}
- 在频道的
Builder
中设置属性(例如,频道名称和徽标/图标)。显示名显示在主屏幕上频道图标的正下方。当用户点击某个频道图标后,Android TV 会使用appLinkIntentUri
将用户导航到相应位置。此 Codelab 使用该 URI 将用户引导至应用中的相应合集。复制下方代码并将其粘贴到如下代码注释处:// TODO: Step 2 add collection metadata and build channel object.
val updatedChannel = channelBuilder
.setInternalProviderId(collection.id)
.setLogo(channelLogoUri)
.setAppLinkIntentUri(appUri)
.setDisplayName(collection.title)
.setDescription(collection.description)
.build()
- 调用
PreviewChannelHelper
类中的函数,将该频道插入到 TV Provider 中或更新该频道。调用publishChannel()
会将该频道的内容值插入到 TV Provider 中。updatePreviewChannel
会更新现有的频道。在代码注释“// TODO: Step 3.1 update an existing channel.”处插入以下代码。
PreviewChannelHelper(context)
.updatePreviewChannel(existingChannel.id, updatedChannel)
Log.d(TAG, "Updated channel ${existingChannel.id}")
在代码注释“// TODO: Step 3.2 publish a channel.”处插入以下代码,以创建一个新频道。
val channelId = PreviewChannelHelper(context).publishChannel(updatedChannel)
Log.d(TAG, "Published channel $channelId")
channelId
- 查看
upsertChannel()
方法,了解如何创建或更新频道。
将默认频道设为可见
当您向 TV Provider 添加频道时,它们是不可见的。除非用户要求,否则频道不会显示在主屏幕上。通常,在用户选择并批准频道后,频道才会显示在主屏幕上。每个应用都可以创建一个默认频道。默认频道比较特殊,因为它会自动显示在主屏幕上;用户无需明确批准添加该频道。
将以下代码添加到 upsertChannel()
方法中(在“TODO: step 4 make default channel visible”注释处):
if(allChannels.none { it.isBrowsable }) {
TvContractCompat.requestChannelBrowsable(context, channelId)
}
如果您针对非默认频道调用 requestChannelBrowsable()
,系统将会显示一个要求用户表示同意的对话框。
调度频道更新
添加频道创建/更新代码后,开发者需要调用 synchronize()
方法来创建频道或更新频道。
最好在用户安装您的应用后立即创建该应用的频道。您可以创建一个广播接收器来监听 android.media.tv.action.INITIALIZE_PROGRAMS
广播消息。系统会在用户安装完 TV 应用后发送该广播,并且开发者可以在其中进行一些节目初始化。
查看示例代码中的 AndroidManifest.xml
文件并找到广播接收器部分。尝试找到广播接收器的正确类名称(将在后面介绍)。
<action
android:name="android.media.tv.action.INITIALIZE_PROGRAMS" />
打开 TvLauncherReceiver
类并查看以下代码块,了解示例应用如何创建主屏幕频道。
TvContractCompat.ACTION_INITIALIZE_PROGRAMS -> {
Log.d(TAG, "Handling INITIALIZE_PROGRAMS broadcast")
// Synchronizes all program and channel data
WorkManager.getInstance(context).enqueue(
OneTimeWorkRequestBuilder<TvMediaSynchronizer>().build())
}
您应该定期更新频道。此 Codelab 使用 WorkManager 库创建后台任务。MainActivity
类中的 TvMediaSynchronizer
用于调度定期频道更新。
// Syncs the home screen channels hourly
// NOTE: It's very important to keep our content fresh in the user's home screen
WorkManager.getInstance(baseContext).enqueue(
PeriodicWorkRequestBuilder<TvMediaSynchronizer>(1, TimeUnit.HOURS)
.setInitialDelay(1, TimeUnit.HOURS)
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.build())
运行应用
运行应用。转到主屏幕。系统会显示默认 (My TV App Default) 频道,但该频道中没有任何节目。如果您在实际设备(而不是模拟器)上运行代码,该频道可能不会显示。
添加更多频道
Feed 包含三个合集。在 TvMediaSynchronizer
类中,为这些合集添加其他频道(在“TODO:第 5 步‘添加更多频道’”处)。
feed.collections.subList(1, feed.collections.size).forEach {
TvLauncherUtils.upsertChannel(
context, it, database.metadata().findByCollection(it.id))
}
再次运行应用
验证是否所有三个频道均已创建完毕。点击 Customize Channels 按钮,然后点击 TV Classics。在频道面板中切换隐藏/显示按钮,以便在主屏幕上隐藏/显示这些频道。
删除频道
如果应用不再维护某个频道,您可以将其从主屏幕中移除。
搜索第 6 步并找到 removeChannel
函数。将下面这段代码添加到该函数中(在“TODO:第 6 步‘移除频道’”处)。如需了解这段代码的运作方式,请移除 media-feed.json
中名为“Charlie Chaplin Collection”的合集(请务必移除整个合集)。再次运行该应用,几秒钟后,您将会看到该频道已被移除。
// First, get all the channels added to the home screen
val allChannels = PreviewChannelHelper(context).allChannels
// Now find the channel with the matching content ID for our collection
val foundChannel = allChannels.find { it.internalProviderId == collection.id }
if (foundChannel == null) Log.e(TAG, "No channel with ID ${collection.id}")
// Use the found channel's ID to delete it from the content resolver
return foundChannel?.let {
PreviewChannelHelper(context).deletePreviewChannel(it.id)
Log.d(TAG, "Channel successfully removed from home screen")
// Remove all of the channel programs as well
val channelPrograms =
TvContractCompat.buildPreviewProgramsUriForChannel(it.id)
context.contentResolver.delete(channelPrograms, null, null)
// Return the ID of the channel removed
it.id
}
完成上述所有说明后,您可以将应用代码与 step_2 进行比较。
您学到的内容
- 如何查询频道。
- 如何在主屏幕中添加或删除频道。
- 如何在频道上设置徽标或名称。
- 如何将默认频道设为可见。
- 如何调度
WorkManager
来更新频道。
接下来做什么?
下一部分介绍如何向频道添加节目。
向频道添加节目的过程与创建频道类似,不过,应使用 PreviewProgram.Builder
代替 PreviewChannel.Builder
。
您仍要使用 TvLauncherUtils
类中的 upsertChannel()
方法。
创建预览节目
在以下部分中,我们将向 step_2 添加代码。请务必在 Android Studio 项目中更改该模块的源文件。
得知频道可见后,请使用带有 PreviewProgram.Builder
的 Metadata
对象创建一个 PreviewProgram
对象。同样,您不希望将同一节目插入某个频道中两次,因此该示例会将 metadata.id
分配给 PreviewProgram
的 contentId
以进行去重。在“TODO:第 7 步‘创建或查找现有的预览节目’”处添加以下代码。
val existingProgram = existingProgramList.find { it.contentId == metadata.id }
val programBuilder = if (existingProgram == null) {
PreviewProgram.Builder()
} else {
PreviewProgram.Builder(existingProgram)
}
使用媒体元数据构建构建器,然后在频道中发布/更新该构建器。(在“TODO:第 8 步‘构建预览节目并发布’”处。)
val updatedProgram = programBuilder.also { metadata.copyToBuilder(it) }
// Set the same channel ID in all programs
.setChannelId(channelId)
// This must match the desired intent filter in the manifest for VIEW action
.setIntentUri(Uri.parse("https://$host/program/${metadata.id}"))
// Build the program at once
.build()
以下是几点注意事项:
- 示例代码会通过预览节目的
contentId
将元数据与该节目相关联。 - 可通过对
PreviewProgram.Builder()
调用setChannelId()
将预览节目插入到频道中。 - 当用户从频道中选择某个节目后,Android TV 系统会启动该节目的
intentUri
。Uri
应包含节目 ID,以便应用可以在用户选择该节目时从数据库中查找并播放媒体。
添加节目
在这一部分,此 Codelab 使用 AndroidX 库中的 PreviewChannelHelper
将节目插入到频道中。
使用 PreviewChannelHelper.publishPreviewProgram()
或 PreviewChannelHelper.updatePreviewProgram()
将相应节目保存到频道中(在“TODO:第 9 步‘向频道添加预览节目’”处)。
try {
if (existingProgram == null) {
PreviewChannelHelper(context).publishPreviewProgram(updatedProgram)
Log.d(TAG, "Inserted program into channel: $updatedProgram")
} else {
PreviewChannelHelper(context)
.updatePreviewProgram(existingProgram.id, updatedProgram)
Log.d(TAG, "Updated program in channel: $updatedProgram")
}
} catch (exc: IllegalArgumentException) {
Log.e(TAG, "Unable to add program: $updatedProgram", exc)
}
非常棒!应用现在已将节目添加到频道中。您可以将应用代码与 step_3 进行比较。
运行应用
在配置中选择 step_2,然后运行应用。
当应用运行时,点击主屏幕底部的 Customize Channels 按钮,然后查找我们的应用“TV Classics”。切换这三个频道,并查看日志了解发生了什么。创建频道和节目的操作在后台进行,因此您可以随意添加额外的日志语句,以帮助您跟踪所触发的事件。
您学到的内容
- 如何向频道中添加节目。
- 如何更新节目的属性。
接下来做什么?
向“接下来观看”频道添加节目。
“接下来观看”频道位于主屏幕顶部附近;它显示在“应用”下方,所有其他频道的上方。
概念
“接下来观看”频道为您的应用提供了一种提高用户兴趣度的方法。您的应用可以向“接下来观看”频道添加以下节目:用户标记为感兴趣的节目,用户观看到中间停止的节目,或者与用户正在观看的内容相关的节目(例如电视连续剧的下一集或节目的下一季)。“接下来观看”频道有 4 种使用情形:
- 继续观看用户尚未看完的视频。
- 推荐下一个要观看的视频。例如,如果用户看完了第 1 集,那么您可以推荐第 2 集。
- 显示新内容来提高用户的兴趣度。
- 维护用户添加的感兴趣视频的观看列表。
本课程将介绍如何使用“接下来观看”频道继续观看视频,具体而言,就是如何在用户暂停观看某个视频时将该视频加入到“接下来观看”频道中。当该视频播放结束后,它应该从“接下来观看”频道中移除。
更新播放位置
可以通过多种方法跟踪播放内容的播放位置。此 Codelab 使用一个线程定期将最新的播放位置保存到数据库中,并更新“接下来观看”节目的元数据。请打开 step_3,然后按照以下说明添加相应代码。
在 NowPlayingFragment
中,将以下代码添加到 updateMetadataTask.
的 run()
方法中(在“TODO:第 10 步‘更新进度’”处):
val contentDuration = player.duration
val contentPosition = player.currentPosition
// Updates metadata state
val metadata = args.metadata.apply {
playbackPositionMillis = contentPosition
}
仅当播放进度小于总时长的 95% 时,该代码才会保存元数据。
添加以下代码(在“TODO:第 11 步‘更新数据库中的元数据’”处)。
val programUri = TvLauncherUtils.upsertWatchNext(requireContext(), metadata)
lifecycleScope.launch(Dispatchers.IO) {
database.metadata().update(
metadata.apply { if (programUri != null) watchNext = true })
}
如果播放进度已超过视频的 95%,系统将移除该“接下来观看”节目,以便优先播放其他内容。
在 NowPlayingFragment
中添加以下代码,以便从“接下来观看”行中移除已看完的视频(在“TODO:第 12 步 [移除‘接下来观看’]”处)。
val programUri = TvLauncherUtils.removeFromWatchNext(requireContext(), metadata)
if (programUri != null) lifecycleScope.launch(Dispatchers.IO) {
database.metadata().update(metadata.apply { watchNext = false })
}
updateMetadataTask
每隔 10 秒调度一次,以确保跟踪到最新的播放位置。此方法在 onResume()
中调度并会在 NowPlayingFragment
的 onPause()
中停止,因此,相关数据仅在用户观看视频时才会更新。
添加/更新“接下来观看”节目
TvLauncherUtils
会与 TV Provider 进行互动。在上一步中,调用了 TvLauncherUtils
中的 removeFromWatchNext
和 upsertWatchNext
。现在,您需要实现这两个方法。AndroidX 库提供了 PreviewChannelHelper
类,可以让此任务变得很简单。
首先,创建或查找 WatchNextProgram.Builder
的现有实例,然后使用最新的播放 metadata
更新该对象。在 upsertWatchNext()
方法中添加以下代码(在“TODO:第 13 步 [构建‘接下来观看’节目]”处)。
programBuilder.setLastEngagementTimeUtcMillis(System.currentTimeMillis())
programBuilder.setWatchNextType(metadata.playbackPositionMillis?.let { position ->
if (position > 0 && metadata.playbackDurationMillis?.let { it > 0 } == true) {
Log.d(TAG, "Inferred watch next type: CONTINUE")
TvContractCompat.WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE
} else {
Log.d(TAG, "Inferred watch next type: UNKNOWN")
WatchNextProgram.WATCH_NEXT_TYPE_UNKNOWN
}
} ?: TvContractCompat.WatchNextPrograms.WATCH_NEXT_TYPE_NEXT)
// This must match the desired intent filter in the manifest for VIEW intent action
programBuilder.setIntentUri(Uri.parse(
"https://${context.getString(R.string.host_name)}/program/${metadata.id}"))
// Build the program with all the metadata
val updatedProgram = programBuilder.build()
在对 WatchNextProgram.Builder
调用 build(
) 方法后,系统将创建 WatchNextProgam
。您可以使用 PreviewChannelHelper
将它发布到“接下来观看”行。
添加以下代码(在“TODO:第 14.1 步 [创建‘接下来观看’节目]”处):
val programId = PreviewChannelHelper(context)
.publishWatchNextProgram(updatedProgram)
Log.d(TAG, "Added program to watch next row: $updatedProgram")
programId
或者,如果该节目已存在,则更新它(在“TODO:第 14.2 步 [更新‘接下来观看’节目]”处)
PreviewChannelHelper(context)
.updateWatchNextProgram(updatedProgram, existingProgram.id)
Log.d(TAG, "Updated program in watch next row: $updatedProgram")
existingProgram.id
移除“接下来观看”节目
当用户播放完视频时,您应该清理“接下来观看”频道。这与移除 PreviewProgram
几乎相同。
使用 buildWatchNextProgramUri()
创建一个执行删除操作的 Uri
。(我们无法在 PreviewChannelHelper
中使用 API 来移除“接下来观看”节目。)
将 TvLauncherUtils
类的 removeFromWatchNext()
方法中的现有代码替换为以下语句(在“TODO:第 15 步‘移除节目’”处):
val programUri = TvContractCompat.buildWatchNextProgramUri(it.id)
val deleteCount = context.contentResolver.delete(
programUri, null, null)
运行应用
在配置中选择 step_3,然后运行应用。
观看您的任一合集中的视频几秒钟,然后暂停播放器(如果您使用的是模拟器,请按空格键)。返回到主屏幕后,您应该会看到该影片已添加至“接下来观看”频道。从“接下来观看”频道中选择同一部影片,它应该会从您上次暂停的位置继续播放。在您看完整部影片后,它应该从“接下来观看”频道中移除。在不同的用户场景中试用“接下来观看”频道。
您学到的内容
- 如何向“接下来观看”频道中添加节目以提高用户兴趣度。
- 如何更新“接下来观看”频道中的节目。
- 如何从“接下来观看”频道中移除节目。
接下来做什么?
完成 Codelab 后,将该应用变成您自己的应用。将媒体 Feed 和数据模型替换为您自己的数据,并将其转换为 TV Provider 的频道和节目。
如需了解详情,请访问相关文档!