安卓电视应用开发教程-全-
安卓电视应用开发教程(全)
一、入门指南
Electronic supplementary material The online version of this chapter (doi:10.1007/978-1-4842-1784-9_1) contains supplementary material, which is available to authorized users.
众所周知,科技在我们生活的方方面面都在不断进步。随着智能手机和平板电脑的爆炸式增长,电视作为下一个联网“智能”设备加入竞争只是时间问题。虽然交互式电视已经存在了几年,但主要竞争者只是最近才进入市场,Android TV 是在 2014 年 6 月推出的(尽管在谷歌电视的失败尝试和 Chromecast 的巨大成功之后),Apple TV 最终在 2015 年 9 月向应用开发者开放了他们的平台。有鉴于此,现在是通过使用 Android TV 扩展您的技能组合来为应用开发的下一个趋势做准备的最佳时机。
安卓电视到底是什么?
Android TV 是谷歌开发的互动电视平台,于 2014 年在其 I/O 大会上发布。利用谷歌从他们之前的一次进入客厅的尝试(称为谷歌电视)中吸取的经验,他们创建了这个操作系统,以便轻松嵌入电视机,或者通过使用独立的机顶盒,让传统的“哑”电视成为交互式电视。新平台是一个针对电视进行了优化的 Android 版本,可以访问开发人员已经熟悉的所有功能,以及通过向后倾斜支持库提供的一些附加组件。
除了能够为 Android TV 创建原生应用,该操作系统还提供了对 Google Cast 的支持。Google Cast 作为 Chromecast 背后的技术为大多数人所熟悉。这意味着,如果您现有的应用支持强制转换,那么用户仍然可以在 Android TV 上使用它,尽管没有原生 Android TV 应用提供的完整沉浸式体验。虽然了解如何开发支持 cast 的应用是有用的,但这本书将专注于为 Android TV 开发原生应用。
对这本书有什么期待
这本书旨在让你开始使用 Android 电视平台,以便你可以扩展现有的应用或创建自己的应用来改善用户的客厅体验。您应该对 Android 开发有一个基本的了解,因为您将使用适配器、片段、活动、视图和其他标准的 Android 组件。任何专门为 Android TV 介绍的内容都将在本书中讨论,因此不需要这些组件的先前知识。在阅读本书的过程中,您将通过编写每个组件来创建一个相对简单的 Android TV 媒体应用,以便充分理解该应用是如何运行的。您还将构建几个强调附加 API 的小示例程序,比如 LAN 通信和从游戏控制器读取输入,这样您就可以开始构建其他应用,比如游戏和工具。在此过程中,您将了解到与智能手机和平板电脑相比,电视用户体验有何不同的设计理念。
当你读完这本书时,你应该已经牢牢掌握了与 Android TV 相关的词汇。你不仅能够为该平台创建应用,还应该能够通过理解你在论坛和谷歌的大量精心编写的 Android 文档中搜索的内容,自信地找到更复杂问题的答案。
正在设置
为 Android 开发的一个好处是,开发工具可以在大多数现代计算机平台上使用,Android TV 开发也不例外。为了编写本书中的例子,你需要一台运行 Windows、Mac OS X 或 Linux 的计算机。这本书将重点介绍使用 Android Studio 作为开发环境,它本身目前需要 Java 运行时环境(JRE)和 Java 开发工具包(JDK)。如果您还没有 Android Studio,您可以通过访问 https://developer.android.com/sdk/index.html ,下载 Android Studio,并按照您的操作系统的安装说明进行操作,获得并找到 Android 开发的官方系统要求,包括最低操作系统版本。在撰写本文时,Android Studio 的最新版本是 1.4。在安装过程中,您需要安装至少适用于 Android 5.0 (Lollipop)的平台工具和 API。
创建新的 Android 电视项目
一旦您安装并设置了 Android Studio,您就可以使用 Google 提供的基本 Android TV 模板创建一个示例项目。打开 Android Studio,点击快速启动标题下的开始一个新的 Android Studio 项目。
当您到达配置您的新项目屏幕时,将应用名称设置为 Hello World,将公司域设置为apress.com,将您的项目位置设置为您想要保存源代码的位置(参见图 1-1 )。填写完所有必需的信息后,单击下一步,您将进入一个屏幕,选择您的应用将支持的外形。

图 1-1。
Configure your new project screen
对于 Hello World 应用,取消选中电话和平板电脑旁边的复选框,并激活电视旁边的复选框。虽然您的项目中可能有一个支持手机和平板电脑的模块,但在本书中,为了简单起见,我们将忽略这种情况。Android TV 需要的最低 API 版本是最早的 21 版(Lollipop ),因为 Android TV 是和 Lollipop 一起推出的。图 1-2 显示了在继续之前,您的目标 Android 设备屏幕应该是什么样子。

图 1-2。
Screen for selecting form factors supported by your app
当你点击“下一步”时,你将被带到一个屏幕,询问你是要创建一个空项目还是一个默认的 Android TV 活动,如图 1-3 所示。对于此示例,选择 Android TV 活动选项。

图 1-3。
Selecting an Android TV template
您遇到的下一个屏幕将为您提供重命名示例 Android TV 应用中的活动、片段和布局文件的选项(图 1-4 )。对于本例,您可以接受默认值并单击 Finish。

图 1-4。
Naming your files
Android Studio 将花一些时间为你的 Hello World 应用创建通用模板。如果您浏览这个应用的源代码,您会注意到十几个 Java 文件。您可能还会注意到,有些文件,比如VideoDetailsFragment.java,包含不推荐使用的代码,或者 Google 不再推荐使用的代码。现在,请忽略它们,因为在本书的后面部分,您将了解到为媒体应用推荐的不同组件。
运行您的 Android 电视应用
下一步你要做的是运行你的安卓电视应用。与移动开发一样,您可以使用仿真器,也可以将其安装在物理设备上。为了创建 Android TV 仿真器,请在 Android Studio 工具栏中单击 AVD 管理器按钮(它看起来像一个手机屏幕,图标的右下角有一个 Android 头,并且是图 1-5 中左起第八个按钮)。

图 1-5。
Android Studio toolbar with AVD Manager button
在出现的 AVD 管理器对话框的左下角选择创建虚拟设备…并从左栏中选择电视类别。应该有多个设备配置文件可供选择,如图 1-6 所示,所以选择任何一个并点击下一步。

图 1-6。
Virtual device options
下一个屏幕应该为您提供一个用于创建基本仿真器的系统映像列表。如果在下一个屏幕上没有必要的系统映像,请选中显示可下载的系统映像复选框。您应该会看到类似于图 1-7 的内容。根据屏幕右侧的建议框下载一个系统映像,以便构建一个在您的系统上运行良好的虚拟设备。

图 1-7。
Selection of system images for the Android TV emulator
最后一个屏幕(如图 1-8 所示)将为您提供自定义虚拟设备设置的选项。出于这些目的,您可以保留默认值,然后单击 Finish 创建您的模拟器。

图 1-8。
Configuring your new Android virtual device
虽然模拟器很方便,但最好还是在物理设备上测试。在 2014 年 I/O 期间,谷歌发布了一套开发设备,可以由开发人员请求,称为 ADT-1。棒棒糖一经正式发布,谷歌就公布了 Nexus Player,可供购买。其他设备,如 NVIDIA SHIELD,也可以从第三方制造商处获得。随着越来越多的原始设备制造商将 Android TV 集成到他们的电视机或创建机顶盒,测试设备的选择将继续增加。如果你有一个测试用的物理设备,你只需在它运行的时候把它插到你的电脑上就可以直接安装你的应用了。
现在您已经创建了一个 Hello World 示例应用和一个运行应用的环境,单击 Android Studio 工具栏中的绿色 run 箭头来安装您的应用,以确保一切正常。当您的应用在仿真器或实际的 Android 电视设备上启动时,您应该会看到类似图 1-9 的屏幕。

图 1-9。
Initial screen for the Android TV template application
摘要
在本章中,您已经迈出了学习 Android 电视平台的第一步。您学习了如何设置示例 Android TV 项目,并且创建了一个用于查看 Android TV 应用的模拟器。在下一章中,你将会学到一些如何设计你的应用,当你从房间的另一边观看时,它对你的用户是有用的,并且你将会被介绍到媒体应用的一些组件。
二、规划您的应用
就像任何电影或电视节目都必须有一个脚本一样,在开始编写应用之前,您应该有一个计划。虽然您可能熟悉手机和平板电脑的 Android 开发,但在为电视创建内容时,您需要考虑许多事情,这取决于您是在制作游戏、工具还是媒体应用。在这一章中,我们将探讨当你的用户在房间的另一边而不是在他们手中体验你的应用时,需要考虑的一些设计因素,以及在构建你的用户界面以支持基本的 Android 电视控制器时,你应该考虑什么。
Android 电视主屏幕
当用户打开他们的 Android 电视时,他们首先看到的是主屏幕,主屏幕通过提供各种方式来发现内容并与他们的应用进行交互,从而充当他们电视的网关。在适用的情况下,你会希望尽可能地利用这些功能,以使你的应用更容易被用户看到。至少,你会希望使用一个专门为 Android TV 制作的应用图标,以便于用户识别你的应用。
主屏幕以行的形式显示给用户,这与上一章的 Hello World 应用有些相似(见图 2-1 )。顶行上方的部分包含时间和搜索按钮,可以通过物理 Android 电视遥控器上的麦克风按钮或 Play Store 的官方 Android 电视移动遥控应用来选择或激活该按钮(参见图 2-2 )。第二行显示从已安装的应用生成的推荐内容列表。建议部分下面是两行—顶部显示所有未配置为可玩游戏的已安装应用,底部显示所有游戏。已安装应用列表下方是设置和系统信息行。

图 2-2。
The Android TV remote control Android app

图 2-1。
The Android TV home screen
启动器图标
用户访问您的应用最常见的方式是从他们电视上已安装的应用列表中选择它。这意味着一旦你的应用被安装到用户的设备上,使用一个大小和样式都合适的启动图标对于帮助用户找到它是非常重要的。由于 Android TV 不会在图标下方显示应用的名称,因此您必须在启动器图标中包含应用的名称。你的启动器图标的大小应该是 320 像素 x 180px 像素,以便在主屏幕上正确显示。一旦你为你的启动器图标创建了一个素材,你将需要把它应用到你的AndroidManifest.xml文件中的启动活动中,类似于一个标准的 Android 应用。需要注意的是,类别选项将使用LEANBACK_LAUNCHER而不是标准的LAUNCHER来显示电视启动器图标。如果您正在构建一个游戏,您还需要将isGame="true"属性添加到应用节点,以便主屏幕将您的启动器图标放在游戏行中。
<application
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme">
<activity
android:name="MainActivity"
android:label=“@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
</application>
建议行
推荐行是 Android TV 主屏幕上的第一行,如图 2-1 所示,是让用户对你的应用感兴趣的最简单的方法。开发人员可以推荐三类内容:延续、相关和新内容。当您从应用创建要在主屏幕推荐行上显示的推荐列表时,您应该根据与过去消费行为的相关性来确定要显示的内容。如果您的用户以前在您的应用中观看过某个节目,您可以提供继续推荐,显示他们没有看完的一集,或者推荐该节目的下一集。您还可以显示相关的推荐,介绍您的用户基于以前观看的媒体可能喜欢的新内容。可以推荐的最后一类内容属于新的类别。这是突出特色媒体和介绍用户可能喜欢的内容的绝佳位置。当显示新的内容推荐时,你应该小心不要意外泄露任何可能破坏兴趣的剧透。您还应该注意相关和延续建议,因为相关行在 Android TV 上对所有用户都可见,因此应该适合所有年龄层。
Android TV 在整个平台上使用简单易消化的卡片格式向用户显示信息,如图 2-3 所示。

图 2-3。
Recommendation card. The number 1 designates the large icon, 2 is the title, 3 is the content text, and 4 is the small icon
虽然已经为您设置了这种格式,但是您仍然可以定制卡片以适合您自己的应用。每张卡片都包含一张显示图片,该图片应能对推荐内容一目了然。该显示图像的高度应为 176dp 或更高,宽度应为高度的 2/3 到 4/3。当用户滚动推荐的内容时,突出显示的项目将展开以显示卡片的其余部分。你可以改变卡片的背景颜色,尽管它应该很好地补充白色文本。您还需要在卡片上添加一个代表您的应用的小图标。这个小图标在#EEEEEE里应该是一个 16dp x 16dp 的 PNG,背景和前景都是透明的。
您可以在推荐卡中显示两条文本信息:标题和内容文本。标题应该是内容的主要描述符,如歌曲或电影标题。在可选的内容文本中,您可以告诉用户一些关于内容的信息,例如为什么向他们显示内容。内容文本可能有用的一个例子是当显示来自体育赛事的视频馈送时。如果事件当前正在进行,或者事件已经结束,并且您可以回放录像,这些都是用户可能想要了解并引起足够兴趣来打开您的应用的优秀信息。
除了显示卡,您还可以根据突出显示的推荐更改主屏幕上的背景。这不仅可以让你给你的应用推荐一个时尚的风格,还提供了另一种方式来吸引你的用户,给他们更多关于内容的信息。使用大的背景图片为你的用户描绘一幅图画比用一段文字描述内容更能引起他们的兴趣。此图像应为 2016px x 1134px (1920 x 1080,留有 5%的页边距),并且不同于您的标准显示图像。应该注意的是,如果图像的大小不正确,系统将试图缩放它以适合,这可能会产生不期望的结果,例如降低图像质量。
一旦您的用户开始从您的应用中查看媒体,您就可以提供一个“正在播放”卡片,它看起来与标准推荐卡片相同,但还包括一个进度条。这张卡片不仅为您的用户提供有用的信息,而且它还作为推荐行中的第一张卡片出现,有助于保持用户参与度。如果使用得当,推荐行提供了一种强大的方式,让用户打开你的应用,因为他们知道有什么内容可供欣赏。在本书的后面,你将学习如何在一个工作媒体应用中实现一个简单的推荐服务。
全局搜索
当用户对他们正在寻找的东西有一些想法时,比如一部特定的电影,找到一个有该内容的应用可能会很麻烦。幸运的是,Android TV 提供了一个搜索选项,可以同时搜索多个应用,因此用户可以快速找到他们想要的东西。通过按下遥控应用上的麦克风按钮,或导航到主屏幕顶部的搜索球,用户可以进入搜索 UI,在那里他们可以说出或键入他们要找的内容(见图 2-4 )。当用户执行搜索时,Android TV 上所有可搜索的应用都将运行查询并返回相关内容(如果有)。一旦结果被显示给用户,他们可以被选择以直接链接到期望的媒体。通过使您的应用可搜索,您可以增加应用的可见性,从而提高用户参与度。

图 2-4。
Android TV home screen search box
用户体验指南
既然你已经知道如何使用主屏幕来吸引用户,你需要一个设计良好的应用,易于使用,视觉上有吸引力,以保持他们的参与。重要的是要认识到,电视比智能手机和平板电脑存在的时间要长得多,因此用户对他们的电视体验会有一个预定义的期望。谷歌建议你的应用遵循三个主要理念:允许随意消费,提供电影体验,保持简单。
休闲消费
智能手机或平板电脑与电视的主要区别在于,电视是专门用作娱乐设备的。为了适应电视的理想用例,您应该了解您的应用的总体目标,并帮助您的用户尽快实现该目标。如果你的应用是用来显示媒体的,那么你应该把你的应用设计成只需要点击几下就可以获得用户想看的内容并开始播放。如果你正在开发一个游戏,那么你应该给你的用户一个身临其境的体验,让他们玩你的游戏,而不会用许多与游戏不直接相关的内容来分散他们的注意力。适合在一个房间里与多人互动的应用,如聚会游戏或卡拉 ok 应用,应该为用户提供他们需要的信息,同时让他们专注于更重要的事情,房间里的其他人。
电影体验
你希望你的用户沉浸在你的应用中。如果可能的话,使用音频和视频提示来告诉你的用户你的应用正在发生什么,而不是通过文本来告诉他们。例如,如果你的用户到达了列表的末尾,Android 倾向于在最后一个项目上提供一个发光的效果。你也可以提供一个听得见的叮声来增加更多的体验。因为 Android 电视设备都运行最低限度的 Lollipop (SDK 21),所以当用户浏览你的应用时,你可以使用大量的动画和过渡来取悦他们。虽然动画可能很有趣,但是您应该尝试在每个屏幕上提供尽可能多的内容,并限制用户为了达到他们的目标而必须查看的屏幕数量。
保持简单
这是最重要的设计准则,也是 Android 电视设计的首要主题。当您的用户坐在电视机前时,他们希望能够快速找到一些东西来观看或开始玩游戏。为了帮助你的用户,在进入你的应用和欣赏内容之间保持最少的屏幕数量。尽量避免要求任何类型的文本输入,并且在必须输入数据的情况下总是提供语音输入选项。请记住,大多数用户将使用简单的 D-pad 控制器与他们的电视进行交互,该控制器带有一个选择按钮,该按钮来自他们的 Android 电视附带的遥控器或遥控器应用。UI 模式应该易于导航,只有几个可用的按钮。确保你不只是从触摸屏设备上复制 UI,而是尝试使用主屏幕上看到的行列表模式。在手机和平板电脑上有效的东西不一定能很好地移植到电视上。你的应用越简单,你的用户就越高兴。
设计您的布局
电视应用设计的好坏,最重要的决定因素之一是屏幕看起来有多杂乱。你如何分隔你的内容,屏幕上有多少项目,以及这些项目的大小都有助于你的用户界面的整洁。虽然电视不断变大并支持更高的分辨率,但坚持使用质量更高的较少可视项目总是比许多不那么吸引人的项目更好。你所有的布局都应该设计成横向模式,因为大多数家庭电视不支持纵向观看。同样,任何导航 UI 组件都应该占据屏幕的左边或右边,这样可以节省显示内容的垂直空间。最后,你应该始终确保你有足够的空白空间,使项目不反对或超出屏幕的边缘。
一般规则是在布局边缘增加 10%的边距,以考虑过扫描,过扫描是指可能位于屏幕可视边界之外的电视区域。虽然这可能看起来需要考虑很多,但如果你正在构建一个媒体播放应用,Android Leanback 支持库已经将布局设计准则考虑在内,并为你处理它们。
染色
虽然电脑显示器和移动设备在不同设备上显示颜色时往往相当一致,但电视不提供这种奢侈,在为应用选择颜色时应采取特殊预防措施。不同类型的电视,如等离子电视或液晶电视,由于技术的固有属性或应用的锐化和平滑滤镜,其显示颜色的方式可能会有所不同。最重要的是,亮度或色调的细微差异在一些设备上要么无法区分,要么被过度强调。避免在大面积屏幕上使用白色(#FFFFFF),因为在明亮的屏幕上显示会对眼睛造成刺激。你还应该对照各种电视和设置检查非常暗或高度饱和的颜色,以确保它们符合你的期望。谷歌建议使用比移动设备上使用的颜色暗两到三级的颜色。它还建议从 www.google.com/design/spec/style/color.html#color-color-palette 的谷歌调色板中选择 700-900 范围内的颜色。
使用文本
虽然通常应该避免使用文本来保持身临其境的体验,但也有一些地方需要使用文本。假设用户平均坐在离电视机大约 10 英尺(3 米)远的地方,设计易读的文本是很重要的。你需要将文本分成小块,以便于阅读。文本最好是深色背景上的浅色,你应该避免像 Roboto Light 这样的细字体,因为电视设置可能会使它们不可读。虽然您应该使用的最小尺寸是 12sp,但 Android TV 的推荐默认尺寸是 18sp。谷歌还为媒体应用的不同部分收集了一套推荐尺寸:
- 卡片上的标题应该用 Roboto 缩略为 16 便士
- 卡片上的字幕应该使用 12sp 的自动字幕
- 浏览屏幕上的标题应该使用 44sp 的 Roboto Regular
- 浏览屏幕上的类别标题应该使用 Roboto 来压缩为 20sp
- 媒体详细信息屏幕上的内容标题应使用 34sp 的 Roboto Regular
- 详细信息屏幕上的描述文本应使用 14sp
您应该注意到,sp 中列出了所有字体大小,这是一个专门针对文本的与密度无关的大小量词。这允许操作系统确定给定设备上的合适大小。虽然这看起来需要记住很多,但是向后倾斜支持库类包含了自己的风格,可以为您处理这种逻辑。
其他考虑因素
虽然理解设计准则很重要,但是在构建应用之前,您还需要考虑其他一些事情。
- 如果你正在为 Android TV 开发一款游戏,你可能想研究一下 Google Play 游戏服务,因为谷歌已经整合了一套令人印象深刻的工具来使游戏开发更快更容易:
developers.google.com/games/services/。 - 如果您正在构建一个媒体应用,您应该考虑您的媒体来自哪里。您是在自己的服务器上托管所有内容,还是聚合多个服务?你如何处理突发事件,比如没有互联网连接,或者服务器没有返回内容?您还需要考虑如何格式化您的内容,以确保它可以在 Android TV 设备上播放。你可以在谷歌官方文档页面找到 Android 支持的媒体格式列表:
developer.android.com/guide/appendix/media-formats.html。 - 如果您的内容是通过要求使用自己的媒体播放器软件的专有第三方提供的,您需要确保它支持使用 Android TV D-pad 控制器。
- 如果你需要支持数字版权管理(DRM),那么你可能会发现 Android DRM 文档很有用:
source.android.com/devices/drm.html。
一旦你意识到所有的需求,你就可以选择如何显示你的内容。虽然你的要求可能排除了这种可能性,但谷歌确实提供了一款出色的开源媒体播放器,名为 ExoPlayer,它支持目前在MediaPlayer类中不可用的功能: developer.android.com/guide/topics/media/exoplayer.html 。
虽然这些主题超出了本书的范围,但是回顾它们是很重要的,以确保在开发最终产品时你的时间是值得的。
摘要
在本章中,你学习了 Android TV 主屏幕的各个部分,以及如何抓住用户对你的应用的兴趣。您还了解了与 Android TV 相关的设计原则,以及 Google 为使您的应用具有视觉吸引力而提供的建议。在下一章中,你将开始构建一个非常基本的媒体播放应用,以了解向后倾斜支持库的一些关键部分,以及如何使用它们来构建一个 Android TV 应用。
三、构建媒体应用
毫无疑问,开发人员为电视开发的最常见的应用只是显示和播放媒体。了解到这一点,谷歌创建了 Leanback 支持库,它为创建完全符合 Android TV 设计准则的应用提供了通用组件。在这一章中,你将从头开始创建一个基本的媒体应用,以便了解一些可用于为用户创建简单而愉快的体验的组件。
项目设置
当您在第一章中创建 Hello World 应用时,您可能会注意到示例应用相当大,并且您很可能会看到一些关于在内容细节片段等区域使用不推荐使用的类的警告。为了更好地理解如何创建一个媒体应用,而不需要筛选杂乱的内容,您将从空白的石板上一点一点地创建一个。虽然还可以做更多的事情来真正让应用发光,但这本书专注于要点,以便在教授主题时不会陷入细节,这些细节最好作为一个有趣的练习。
创建 Android Studio 项目
首先打开 Android Studio,进入欢迎使用 Android Studio 屏幕。一般来说,这将是您打开程序时看到的第一个屏幕,但是如果您之前打开了一个项目(如 Hello World),那么您需要关闭它以返回到欢迎屏幕。点击右边面板中的 Start a New Android Studio Project,进入配置您的新项目屏幕。在“应用名称”字段中,输入 Media Player,并为公司域输入 apress.com 。选择项目位置的路径,然后单击下一步。
在下一个屏幕上,你需要选择你的应用支持的外形。对于媒体播放器,选择电视,将最低 SDK 设置为 API 21: Android 5.0(棒棒糖)。在同一屏幕上,取消选择电话和平板电脑项目,然后单击下一步。
在第一章中,您让 Android Studio 创建了一个 Android TV 活动,这反过来又创建了一个完整的 Android TV 演示应用。对于媒体播放器应用,单击不添加活动,然后单击完成。当 Android Studio 创建项目时,您将拥有应用的基础结构,但它将几乎没有源文件。
更新相关性
你要做的第一件事是打开build.gradle文件。虽然对 Gradle 构建系统的详细介绍超出了本书的范围,但是您只需要在这个项目中使用它来添加依赖项。在 Dependencies 节点中,您应该已经看到了导入 Leanback 支持库和 RecyclerView 库的行。您还需要导入 GSON 库和 Picasso 库,前者用于从 JSON 数据创建对象,后者用于从 Square 轻松显示应用中来自互联网的图像。在撰写本文时,以下代码清单中显示的每一项都是库的最新版本。
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.google.code.gson:gson:2.3'
compile 'com.squareup.picasso:picasso:2.5.2'
compile 'com.android.support:leanback-v17:23.0.1'
compile 'com.android.support:recyclerview-v7:23.0.1'
}
Tip
Square 提供了各种各样的开源库,在创建应用时会很有用。你可以在 http://square.github.io/ 找到他们发布的库列表。
构建项目框架
一旦你完成了build.gradle的更新,如果你在项目导航布局中,导航到左侧导航面板中的app/src/main/java(如果你在 Android 导航布局中,导航到app/java)。在 Android Studio 中,右击包名,转到 New 和 Java Class。将新的 Java 类命名为MainActivity,并单击 OK。您将希望MainActivity扩展Activity,然后您将覆盖onCreate来将活动与布局文件相关联。
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView( R.layout.activity_main );
}
}
MainActivity将是你的用户进入你的应用时看到的第一个活动。因此,您需要在AndroidManifest.xml中声明它,并将其标记为主活动和启动活动。
最后,您需要在manifest标签内的AndroidManifest.xml顶部添加三行。第一个声明应用需要有INTERNET权限。接下来的两项说明了使用该应用时设备需要哪些功能。在本例中,您将设置应用,使其不需要touchscreen,但需要Leanback功能。这将使您的应用可以安装在 Android 电视系统上。
<manifest xmlns:android="??http://schemas.android.com/apk/res/android
package="com.apress.mediaplayer">
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="true" />
<application android:allowBackup="true" android:label="@string/app_name"
android:icon="@mipmap/ic_launcher" android:theme="@style/AppTheme">
<activity
android:name="MainActivity"
android:label="@string/app_name"
android:logo="@mipmap/ic_launcher">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
现在您的清单已经设置好了,您需要为MainActivity创建一个布局文件。右键单击 Android Studio 左侧导航窗格中的app/src/main/res(同样,假设您在项目导航布局中)并创建一个名为layout的新 Android 资源目录。接下来,您需要右键单击layout并创建一个名为activity_?? 的新布局资源文件。该布局文件将由单个项目组成。
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="??http://schemas.android.com/apk/res/android
android:id="@+id/main_browse_fragment"
android:name="com.apress.mediaplayer.MainFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
您会注意到这个片段使用了name属性来指向com.apress.mediaplayer.MainFragment。MainFragment将是BrowseFragment类的扩展,该类由向后倾斜支持库提供。BrowseFragment将允许你显示代表你的应用内容、偏好和搜索选项的项目行。现在,通过右键单击app/src/main/java下的应用包名称并创建一个名为MainFragment的新 Java 类来创建MainFragment。一旦文件被创建,让它扩展BrowseFragment。
public class MainFragment extends BrowseFragment { }
此时,您应该可以毫无问题地运行您的应用了,尽管它只会显示一个黑屏,在左侧有一个蓝绿色面板(称为快速通道)。接下来,您将开始构建BrowseFragment类,该类将使用本地数据文件和模型通过 presenter 显示卡片。
构建 BrowseFragment 类
由向后倾斜支持库提供的BrowseFragment类构成了 Android 电视媒体应用的核心部分之一。虽然它在使用中充当单个片段,但实际上它由两个片段组成:一个RowsFragment和一个HeadersFragment。RowsFragment显示代表您的内容的定制卡片行,每行上面都有一个标题,通常用于显示类别名称。这些标题也用于填充HeadersFragment,它构成了您运行应用时看到的蓝绿色快速通道面板。
创建数据
在您开始在BrowseFragment中工作之前,您需要为您想要显示给用户的内容规划数据。在生产应用中,您很可能希望将这些数据存储在网上的某个地方,或者有一个 API 将这些数据传送到您的应用,以便您可以根据用户的使用习惯轻松传送新内容或调整用户看到的内容。为了降低复杂性并专注于 Android TV 平台,您正在构建的媒体播放器应用将简单地将内容数据存储在应用内的 JSON 文件中,并将其读入BrowseFragment。在Data文件夹中可以找到本书代码样本的样本数据文件。JSON 数组中的每一项都代表公共领域中的一部不同的电影。本教程使用的字段是“标题”、“描述”、“视频 Url”、“类别”和“海报”。如果您想使用自己的内容或将自己的内容添加到数据中,只需遵循以下项目格式:
{
"title": "Content title",
"description": "Some long text description",
"videoUrl": "Video URL",
"category": "Category",
"poster": "Image file URL"
}
Note
样本数据中使用的电影都是公共领域的,用于播放的视频文件由公共领域内容的非营利性互联网档案库archive.org托管。
一旦你有了你的数据文件,把它命名为videos.json,并把它放在一个名为raw的新目录下/app/src/main/res。
创建数据模型
接下来,您需要创建一个模型对象类来表示应用中使用的数据。这个模型需要实现Serializable,以便将数据转换成可以在应用组件之间传递的字符串表示。您还将使用 Google 的 GSON 库来获取 JSON 文件,并轻松地将其解析为一个对象列表,因此您将需要模型中每个属性的 setters 和 getters。
在app/src/main/java目录下的 app package 文件夹下创建一个新的 Java 文件,并将其命名为Video。一旦创建了Video.java,就为每个数据属性添加字符串、toString方法以及 getters 和 setters,如下所示。
public class Video implements Serializable {
private String title;
private String description;
private String videoUrl;
private String category;
private String poster;
@Override
public String toString() {
return "Video {" +
"title=\'" + title + "\'" +
", description=\'" + description + "\'" +
", videoUrl=\'" + videoUrl + "\'" +
", category=\'" + category + "\'" +
", poster=\'" + poster + "\'" +
"}";
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getVideoUrl() {
return videoUrl;
}
public void setVideoUrl(String videoUrl) {
this.videoUrl = videoUrl;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getPoster() {
return poster;
}
public void setPoster(String poster) {
this.poster = poster;
}
}
加载数据
现在您有了一个表示数据的模型,您可以在您的BrowseFragment中使用它。您需要做的第一件事是在MainFragment类的顶部创建一个Video对象的列表,以便存储您的数据。
private List<Video> mVideos = new ArrayList<Video>();
接下来您应该覆盖onActivityCreated方法,因为这将驱动片段中的大部分逻辑。在onActivityCreated中,您应该调用loadData,这是您将创建的一个帮助器方法,以便将数据加载到mVideos中。
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
loadData();
}
在定义loadData之前,您应该在您的应用包文件夹中创建一个新的 Java 类,名为Utils. Utils,它将包含一个名为loadJSONFromResource的静态方法,该方法接受本地 JSON 文件的上下文和资源 ID,在本例中为videos.json,这样它就可以被转换成一个字符串并返回。在Utils包中输入以下代码,然后返回MainFragment。
public class Utils {
private Utils() {}
public static String loadJSONFromResource( Context context, int resource ) {
if( resource <= 0 || context == null )
return null;
String json = null;
InputStream is = context.getResources().openRawResource( resource );
try {
if( is != null ) {
int size = is.available();
byte[] buffer = new byte[size];
is.read(buffer);
json = new String(buffer, "UTF-8");
}
} catch( IOException e ) {
return null;
} finally {
try {
if( is != null )
is.close();
} catch( IOException e ) {}
}
return json;
}
}
现在,您已经有了一个方法来读取之前创建的 JSON 文件,是时候使用它了。在MainFragment中创建一个名为loadData的新方法,并让它从Utils.?? 生成一个字符串。接下来你需要使用反射和 GSON 库来填充你的视频对象列表,这样它们就可以用来填充你的BrowseFragment UI。
private void loadData() {
String json = Utils.loadJSONFromResource( getActivity(), R.raw.videos );
Type collection = new TypeToken<ArrayList<Video>>(){}.getType();
Gson gson = new Gson();
mVideos = gson.fromJson( json, collection );
}
当您将数据加载到片段中之后,您就可以开始为您的 Android TV 媒体应用创建 UI 了。在下一节中,您将为BrowseFragment设置一些 UI 属性,然后使用您刚刚生成的视频列表来创建定制的卡片行,您可以单击这些卡片来访问视频详细活动。
自定义 BrowseFragment UI
如果你阅读 Google ( developer.android.com/reference/android/support/v17/leanback/app/BrowseFragment.html )的BrowseFragment文档页面,你会注意到有几种方法可以定制片段的一些 UI 方面。为了简单起见,我们在这里只涉及其中的一部分。在onActivityCreated中,在您对loadData的调用下面,添加以下三行代码
setTitle( "Apress Media Player" );
setHeadersState( HEADERS_ENABLED );
setHeadersTransitionOnBackEnabled( true );
这些方法中的每一个都控制着BrowseFragment UI 中不同的可定制部分:
-
setTitle方法将获取传递给它的字符串,并将其显示在BrowseFragment的右上角。 -
Using
setHeadersStateaccepts one of three predefined values inBrowseFragmentthat will allow you to control how theHeaderFragmentfastlane works.HEADERS_ENABLEDleaves the fastlane usable and expanded,HEADERS_DISABLEDwill hide and disable it (see Figure 3-1), andHEADERS_HIDDENwill enable theHeaderFragmentwhile mostly hiding it except for a sliver on the side of the screen.![A978-1-4842-1784-9_3_Fig1_HTML.jpg]()
图 3-1。
BrowseFragment with hidden fast lane
-
最后,虽然默认情况下启用了后台操作上的头转换,但是我在这里包含了它,因为需要指出这一点。当用户打开
HeadersFragment时,一个条目将被添加到后台堆栈中。这意味着当用户按下控制器上的 back 按钮时,headers 部分将会转换。如果你想覆盖 back 按钮操作,你需要调用值为false的setHeadersTransitionOnBackEnabled,然后覆盖BrowseTransitionListener来实现你自己的 back stack 处理。
现在您已经设置了一些BrowseFragment UI 属性,是时候为您的数据添加卡片行了。BrowseFragment显示内容行的方式是通过一个ObjectAdapter并在一个垂直列表中显示内容列表(行)。一行中的每一项都与一个Presenter对象相关联,该对象定义了每一项在 UI 中的外观,在本例中是一张带有图像和电影标题的卡片。每一行的上方都有一个标题,代表数据集中视频的类别。
在MainFragment的onActivityCreated方法中,添加一个对名为loadRows的新助手方法的方法调用,不带参数。接下来您将定义loadRows。新方法中的第一行将用包含每一行的ListRowPresenter初始化一个ArrayObjectAdapter。ListRowPresenter用于定义一行项目将如何在BrowseFragment中工作。在初始化ArrayObjectAdapter之后,您将想要创建一个名为CardPresenter. CardPresenter的新对象,这是您将在本节稍后创建的一个类,所以现在您可以忽略 Android Studio 给出的错误。
private void loadRows() {
ArrayObjectAdapter adapter =
new ArrayObjectAdapter( new ListRowPresenter() );
CardPresenter presenter = new CardPresenter();
接下来,您需要创建一个字符串对象列表,代表您的视频类别。这些类别中的每一个都将在一行之上使用,以帮助为用户组织内容。这里您将使用另一个名为getCategories的助手方法,它将遍历数据中的每个视频项,并将其类别添加到一个列表中。虽然这可能不是创建类别列表的最有效方式,但它适合这里的目的。
List<String> categories = getCategories();
if( categories == null || categories.isEmpty() )
return;
其中getCategories定义为
private List<String> getCategories() {
if( mVideos == null )
return null;
List<String> categories = new ArrayList<String>();
for( Video movie : mVideos ) {
if( !categories.contains( movie.getCategory() ) ) {
categories.add( movie.getCategory() );
}
}
return categories;
}
现在您已经有了一个类别列表,您可以使用CardPresenter类创建卡片,并根据每个数据项的类别将它们添加到BrowseFragment中的行。为简单起见,您将遍历类别列表,并检查每个数据项以查看类别是否匹配。如果有,你将把它添加到一个新的ArrayObjectAdapter。一旦一个类别的所有电影都被添加到新的适配器中,您将创建一个新的HeaderItem,并使用它和ArrayObjectAdapter来创建一个新的项目行。最后,您将调用setAdapter,一个内置于BrowseFragment的方法,来添加您首先在loadRows中创建的父ArrayObjectAdapter,作为片段的主适配器。
for( String category : categories ) {
ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter( presenter );
for( Video movie : mVideos ) {
if( category.equalsIgnoreCase( movie.getCategory() ) )
listRowAdapter.add( movie );
}
if( listRowAdapter.size() > 0 ) {
HeaderItem header = new HeaderItem( adapter.size() - 1, category );
adapter.add( new ListRow( header, listRowAdapter ) );
}
}
setAdapter(adapter);
创建演示者
即使为BrowseFragment创建项目行的所有逻辑都已完成,您仍然需要创建CardPresenter对象来编译您的应用并显示数据。CardPresenter将是Presenter的扩展,存在于向后倾斜支持库中。Presenter的目的是获取数据并将其绑定到视图,类似于 RecyclerView 中适配器的概念,但不依赖于位置。如果您以前使用过 RecyclerViews,这个类应该看起来有些熟悉。
public class CardPresenter extends Presenter {
static class ViewHolder extends Presenter.ViewHolder {
private ImageCardView mCardView;
public ViewHolder(View view) {
super(view);
mCardView = (ImageCardView) view;
}
public ImageCardView getCardView() {
return mCardView;
}
public void updateCardViewImage( Context context, String link ) {
Picasso.with(context).load(link)
.resize(210, 210).centerCrop()
.into(mCardView.getMainImageView());
}
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
ImageCardView cardView = new ImageCardView( parent.getContext() );
cardView.setFocusable( true );
return new ViewHolder(cardView);
}
@Override
public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
Video video = (Video) item;
if ( !TextUtils.isEmpty(video.getPoster()) ) {
((ViewHolder) viewHolder).mCardView
.setTitleText(video.getTitle());
((ViewHolder) viewHolder).mCardView
.setMainImageDimensions( 210, 210 );
( (ViewHolder) viewHolder )
.updateCardViewImage( ( (ViewHolder) viewHolder )
.getCardView().getContext(), video.getPoster() );
}
}
@Override
public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
}
@Override
public void onViewAttachedToWindow(Presenter.ViewHolder viewHolder) {
}
}
Note
RecyclerViews 仍然是 Android 开发中相对较新的内容。如果你以前没有和他们一起工作过,我强烈推荐戴夫·史密斯的《安卓秘籍,第四版》。也是由 Apress 出版的,它包含了许多 Android 开发的惊人例子,包括一个关于 RecyclerView 的强大部分。
虽然Presenter在幕后完成了大部分工作,但你可以看到仍有一些定制工作需要完成。Presenter类需要使用ViewHolder模式,以便在包含多个条目的行之间循环时重用视图。之前使用的ViewHolder包含一个单独的ImageCardView,这是一个专门为 Android TV 设计的视图,包含一个大图像和该图像下方的一个预先设计好的信息卡片。为了从存储为 URL 的数据中加载海报图像,您将使用 Picasso library by Square 来加载图像、调整图像大小和居中裁剪图像。
定义了您的ViewHolder之后,您需要在onCreateViewHolder中创建它,并将其设置为可聚焦的,这允许它被用户高亮显示。当ViewHolder被创建后,你可以将它归还给Presenter使用。您需要在Presenter中覆盖的最后一个方法是onBindViewHolder。顾名思义,这是将数据绑定到ViewHolder视图的地方,这样CardPresenter就能正确显示。
此时,您应该能够在 Android TV 设备或模拟器上编译和运行您的应用。你应该会看到代表videos.json的数据项的行,以及每组视频的类别标题,如图 3-2 所示。虽然还可以对BrowseFragment进行更多的定制,但我们将把它留到本书的下一章。

图 3-2。
BrowseFragment with cards for each piece of media
创建视频详细信息屏幕
虽然BrowseFragment旨在让你的用户快速了解他们可以享受什么,但DetailsFragment旨在专注于一个项目。该详细屏幕不仅为用户提供了更多的内容信息,还允许他们执行各种操作和查看相关内容。
设置视频详细信息
您可以通过在您的媒体播放器包中创建一个新的 Java 文件并将其命名为VideoDetailsActivity来开始构建您的详细信息屏幕。像MainActivity,VideoDetailsActivity将简单地为包含片段的内容视图设置一个布局。
public class VideoDetailsActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_video_details);
}
}
您还需要创建正在使用的布局文件。进入res/layout目录,创建一个名为activity_video_details的新 XML 文件。这个布局将只包含一个名为VideoDetailsFragment的片段。
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="??http://schemas.android.com/apk/res/android
xmlns:tools="??http://schemas.android.com/tools
android:id="@+id/video_detail_fragment"
android:name="com.apress.mediaplayer.VideoDetailsFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:deviceIds="tv"
tools:ignore="MergeRootFrame" />
现在您已经有了一个带有布局的活动,您将需要在您的 Java 应用包目录中创建VideoDetailsFragment。当文件被创建时,让它扩展DetailsFragment并实现OnItemViewClickedListener和OnActionClickedListener接口。OnItemViewClickedListener类似于一个标准的OnItemClickedListener,除了它是后倾支持库的一部分,并且被专门设计为当一个行视图控件中的项目被点击时的回调函数。OnActionClickedListener顾名思义,当一个动作项被点击时被调用。您需要覆盖接口的OnItemClicked和onActionClicked方法。
public class VideoDetailsFragment extends DetailsFragment
implements OnItemViewClickedListener, OnActionClickedListener {
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder,
Object item,
RowPresenter.ViewHolder rowViewHolder,
Row row) {
}
@Override
public void onActionClicked(Action action) {
}
}
连接视频细节
既然您已经构建了基类DetailsFragment,那么您将需要在类的顶部添加一个静态字符串,该字符串将用于将序列化的Video对象传递给片段。
public static final String EXTRA_VIDEO = "extra_video";
接下来你需要把BrowseFragment系到DetailsFragment上。回到MainFragment.java,更新类定义行,这样MainFragment也实现了OnItemViewClickedListener。当点击一个CardPresenter项时,会调用OnItemClicked。您将需要检查与视图相关联的数据项类型,以查看它是否是一个Video对象,如果是,则在开始新活动时将其作为额外的对象传递给VideoDetailsActivity。
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder,
Object item,
RowPresenter.ViewHolder rowViewHolder,
Row row) {
if( item instanceof Video ) {
Video video = (Video) item;
Intent intent = new Intent( getActivity(), VideoDetailActivity.class );
intent.putExtra( VideoDetailsFragment.EXTRA_VIDEO, video );
startActivity( intent );
}
}
当你的onItemClick方法被定义时,你需要将它与MainFragment关联起来,这样当一个项目被点击时,应用知道调用onItemClicked。您可以通过添加以下行作为onActivityCreated中的最后一项来完成此操作。
setOnItemViewClickedListener( this );
最后,你需要在AndroidManifest.xml中注册VideoDetailsActivity,这样就可以启动了。
<activity android:name=".VideoDetailsActivity" />
此时,您应该能够运行您的应用了。如果一切按预期进行,你将能够点击BrowseFragment中的一个项目来打开VideoDetailsActivity,尽管VideoDetailsFragment应该只显示一个空白屏幕。
显示内容详细信息
DetailsFragment的主要组件是DetailsOverviewRow. DetailsOverviewRow由一个主图像、一个显示描述的文本视图和一系列可选的动作按钮组成。在用信息填充您的DetailsOverviewRow之前,您应该在VideoDetailsFragment类的顶部声明四个成员变量。
public static final long ACTION_WATCH = 1;
private Video mVideo;
private DetailsOverviewRow mRow;
private Target target = new Target() {
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
mRow.setImageBitmap(getActivity(), bitmap);
}
@Override
public void onBitmapFailed(Drawable errorDrawable) {
}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
}
};
ACTION_WATCH是用户选择查看内容时将使用的标识符。mVideo是您将用于显示内容的数据对象。mRow是对DetailsOverviewRow的简单引用,这样就可以很容易地访问它。最后,Target是 Picasso 库的一部分,它允许你将远程图像加载到其中,这样你的程序就可以使用它们,而不用直接加载到ImageView。在DetailsFragment的情况下,图像将被加载到Target并被发送到onBitmapLoaded,然后onBitmapLoaded将调用DetailsOverviewRow上的setImageBitmap。
onCreate方法是处理VideoDetailsFragment背后的逻辑的地方。您要做的第一件事是从用于启动细节活动的意图中检索内容数据,然后您可以将mRow初始化为新的DetailsOverviewRow。
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mVideo = (Video) getActivity().getIntent().getSerializableExtra( EXTRA_VIDEO );
mRow = new DetailsOverviewRow( mVideo );
}
接下来,您可以创建可供用户选择的操作。如果你看了第一章中的 Hello World 示例,你会注意到示例使用了addAction方法,但是被 Android Studio 删除了,因为它被弃用了。目前公认的向DetailsOverviewRow添加动作的方式是通过setActionsAdapter方法传递一个SparseArrayObjectAdapter。SparseArrayObjectAdapter有两个您需要覆盖的方法:size和get. size返回您想要显示的动作按钮的数量,get 句柄返回要显示的新动作。对于这个例子,我们将使用大小为 3,虽然只有第一个项目将执行一个真正的动作。按照这里的定义创建一个名为initAction的新方法,并从onCreate调用它。
private void initActions() {
mRow.setActionsAdapter(new SparseArrayObjectAdapter() {
@Override
public int size() {
return 3;
}
@Override
public Object get(int position) {
if(position == 0) {
return new Action(ACTION_WATCH, "Watch", "");
} else if( position == 1 ) {
return new Action( 42, "Rent", "Line 2" );
} else if( position == 2 ) {
return new Action( 42, "Preview", "" );
}
else return null;
}
});
}
定义好您的操作后,就可以开始创建将用于细节片段的演示者了。在onCreate中对initActions的调用下面,您将创建一个ClassPresenterSelector和一个ArrayObjectAdapter,然后将DetailsOverviewRow添加到ArrayObjectAdapter。
ClassPresenterSelector presenterSelector = createDetailsPresenter();
ArrayObjectAdapter adapter = new ArrayObjectAdapter( presenterSelector );
adapter.add(mRow);
这里的createDetailsPresenter是返回一个ClassPresenterSelector的 helper 方法。您现在将创建这个助手方法。在 Hello World 示例应用中,使用了DetailsOverviewRowPresenter,但也不推荐使用。相反,您将使用一个FullWidthDetailsOverviewRowPresenter,它由左边的一个徽标、顶部的一行操作和右边的一个可定制的详细描述视图组成。这个演示器允许您定制片段和动作按钮的背景颜色,设置动画,关联动作监听器,以及添加其他定制来取悦您的用户。当您的演示者构建完成后,将其绑定到DetailsOverviewRow并添加到ClassPresenterSelector。您还需要向ClassPresenterSelector添加一个空的ListRowPresenter,用于在返回之前显示相关媒体。
private ClassPresenterSelector createDetailsPresenter() {
ClassPresenterSelector presenterSelector = new ClassPresenterSelector();
FullWidthDetailsOverviewRowPresenter presenter =
new FullWidthDetailsOverviewRowPresenter(
new DetailsDescriptionPresenter() );
presenter.setOnActionClickedListener(this);
presenterSelector.addClassPresenter(DetailsOverviewRow.class,
presenter);
presenterSelector.addClassPresenter(ListRow.class,
new ListRowPresenter());
return presenterSelector;
}
到现在为止,你应该注意到FullWidthDetailsOverviewRowPresenter使用的DetailsDescriptionPresenter不是由向后倾斜支持库提供的类。这是一个你需要创建的类,所以现在就在java目录下的应用包中创建。该类应该是AbstractDetailsDescriptionPresenter的扩展,并将覆盖onBindDescription方法,以便为片段的细节部分设置所有文本。
public class DetailsDescriptionPresenter extends
AbstractDetailsDescriptionPresenter {
@Override
protected void onBindDescription(
AbstractDetailsDescriptionPresenter.ViewHolder viewHolder, Object item) {
Video video = (Video) item;
if (video != null) {
viewHolder.getTitle().setText(video.getTitle());
viewHolder.getSubtitle().setText(video.getCategory());
viewHolder.getBody().setText(video.getDescription());
}
}
}
当您的演示者完成后,转到onCreate的末尾,添加以下两行
loadRelatedMedia(adapter);
setAdapter(adapter);
此时,您应该能够运行您的应用并进入视频详细信息屏幕,如图 3-3 所示。

图 3-3。
FullWidthDetailsOverviewRowPresenter with actions and data
loadRelatedMedia是您将创建的另一个助手方法,它将用于查找和显示与当前显示的详细信息屏幕相关的内容。setAdapter会用DetailsFragment把所有东西绑定在一起,这样就可以显示给你的用户了。
loadRelatedMedia方法是访问后端或为应用执行任何其他逻辑,以显示相关内容。在本例中,您只需加载来自videos.json的所有数据,并显示与所选视频属于同一类别的项目。您将以与BrowseFragment相同的方式访问videos.json,使用反射和 GSON 创建一个视频对象列表。一旦你有了数据,你将创建一个ArrayObjectAdapter,并添加相关的视频对象。最后,您将把带有标题的适配器作为ListRow添加到DetailsFragment中。
private void loadRelatedMedia( ArrayObjectAdapter adapter ) {
String json = Utils.loadJSONFromResource( getActivity(), R.raw.videos );
Gson gson = new Gson();
Type collection = new TypeToken<ArrayList<Video>>(){}.getType();
List<Video> videos = gson.fromJson( json, collection );
if( videos == null )
return;
ArrayObjectAdapter listRowAdapter =
new ArrayObjectAdapter( new CardPresenter() );
for( Video video : videos ) {
if( video.getCategory().equals( mVideo.getCategory() )
&& !video.getTitle().equals( mVideo.getTitle() ) ) {
listRowAdapter.add( video );
}
}
HeaderItem header = new HeaderItem( 0, "Related" );
adapter.add(new ListRow(header, listRowAdapter));
}
回到onCreate,在方法的末尾再添加两行。第一个是调用 Picasso 将当前选择的视频内容的海报加载到在类的顶部定义的具有设定大小的Target对象中。第二行将OnItemViewClickedListener界面与DetailsFragment关联起来,这将允许用户点击您的相关内容行。
Picasso.with(getActivity()).load(mVideo.getPoster())
.resize(274, 274).into(target);
setOnItemViewClickedListener(this);
此时,您应该能够运行您的应用,并在BrowseFragment上选择一个视频,带到一个功能DetailsFragment屏幕。您应该会看到电影海报、动作按钮和内容的文本描述。如果你向下滚动,也应该至少有一个相关项目是可选的(见图 3-4 )。您现在可以通过更新onItemClicked方法来解决这个问题,这样它就可以用相关视频的信息打开一个新的VideoDetailsFragment。

图 3-4。
Related media on the detail screen
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
RowPresenter.ViewHolder mRowViewHolder, Row mRow) {
if( item instanceof Video ) {
Video video = (Video) item;
Intent intent = new Intent( getActivity(),
VideoDetailActivity.class );
intent.putExtra( VideoDetailsFragment.EXTRA_VIDEO, video );
startActivity( intent );
}
}
最后,您需要处理片段顶部的动作按钮。如果点击其中一个按钮,将调用onActionClicked。当点击 Watch 按钮时,您将希望开始为您的用户播放内容。在这个示例中,另外两个动作用于强调,所以当单击一个动作时,您可以简单地显示一条 Toast 消息。
@Override
public void onActionClicked(Action action) {
if( action.getId() == ACTION_WATCH ) {
Intent intent = new Intent(getActivity(),
PlayerActivity.class);
intent.putExtra(VideoDetailsFragment.EXTRA_VIDEO,
mVideo);
startActivity(intent);
} else {
Toast.makeText( getActivity(), "Action",
Toast.LENGTH_SHORT ).show();
}
}
此时PlayerActivity不存在,所以项目不会编译。在下一节中,您将创建PlayerActivity来播放视频,并使用向后倾斜支持库向屏幕添加控件。
播放和控制内容
毫无疑问,播放内容是媒体应用最重要的部分。如何播放内容将取决于用户使用的媒体类型、是否使用专有内容播放器、媒体是本地媒体还是远程媒体,以及是否有 DRM 或广告等其他要求。为了使这个例子简单,您将创建一个带有VideoView的活动来回放视频。您还将使用来自向后倾斜支持库中的一个新的PlaybackOverlayFragment来为您的用户显示媒体控件。
创建媒体播放器
您应该首先在与其他 Java 源文件相同的位置创建一个名为PlayerActivity的新Activity。你也会想把PlayerActivity添加到AndroidManifest.xml,就像你对VideoDetailsActivity做的那样。一旦在清单中创建和注册了PlayerActivity,就可以开始充实它了。
在类的顶部有两个成员变量,一个代表将播放内容的VideoView,另一个代表存储媒体数据的Video对象。您还需要覆盖onCreate,并将一个布局与PlayerActivity相关联。
public class PlayerActivity extends Activity {
private VideoView mVideoView;
private Video mVideo;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_player);
}
}
接下来,您需要创建activity_player.xml布局文件。在xml/layouts下,创建一个新的 Android 布局文件,命名为activity_player。布局将由两个主要部分组成:一个是VideoView,另一个是您即将编写的叫做PlayerControlsFragment的新片段。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="??http://schemas.android.com/apk/res/android
android:layout_width="match_parent"
android:layout_height=“match_parent"
android:background="@android:color/black">
<VideoView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true" />
<fragment
android:id="@+id/player_controls"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="com.apress.mediaplayer.PlayerControlsFragment" />
</RelativeLayout>
回到PlayerActivity,在onCreate结束时,您将需要检索VideoView和数据。一旦初始化了两个成员变量,就可以调用VideoView上的setVideoPath来将Video对象的 URL 设置为媒体源。
mVideoView = (VideoView) findViewById( R.id.video_view );
mVideo = (Video) getIntent().getSerializableExtra( VideoDetailsFragment.EXTRA_VIDEO );
mVideoView.setVideoPath( mVideo.getVideoUrl() );
构建回放控制片段
为了编译你的应用,你需要制作PlayerControlsFragment。在名为PlayerControlsFragment的包目录下创建一个新的 Java 类,并对其进行扩展PlaybackOverlayFragment. PlaybackOverlayFragment包含一个ObjectAdapter,它可以垂直堆叠多行动作按钮,这使得它成为一种方便的方式来显示用户交互内容的控件。您还需要实现OnActionClickedListener,当一个控件动作被点击时,它将接收一个调用。
public class PlayerControlsFragment extends PlaybackOverlayFragment implements OnActionClickedListener {
public void onActionClicked(Action action) {
}
}
您还需要在PlayerControlsFragment中创建一个新的接口来链接片段和PlayerActivity。这是您从控件中操纵视频内容的方式。为了简单起见,您将只在接口中添加一个启动和停止方法。
public interface PlayerControlsListener {
void play();
void pause();
}
现在你需要在PlayerActivity中实现这个接口。返回到PlayerActivity并更新类定义以使用PlayerControlsListener。
public class PlayerActivity extends Activity implements
PlayerControlsFragment.PlayerControlsListener
然后,您需要在启动和暂停VideoView的接口中覆盖这两个方法。
@Override
public void play() {
mVideoView.start();
}
@Override
public void pause() {
mVideoView.pause();
}
在PlayerActivity上设置好界面,就可以关闭它,关注PlayerControlsFragment了。首先在顶部添加两个成员变量,一个用于Video对象,另一个用于接口。
private PlayerControlsListener mControlsCallback;
private Video mVideo;
然后覆盖onCreate并初始化两个成员变量。这也是您可以为PlaybackOverlayFragment设置一些基本属性的地方。对于这个项目,您将为控件设置一个浅色半透明的背景,并禁用淡入淡出,这样控件就会一直留在屏幕上,直到您重新启用淡入淡出。
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setBackgroundType(PlaybackOverlayFragment.BG_LIGHT);
setFadingEnabled(false);
mControlsCallback = (PlayerControlsListener) getActivity();
mVideo = (Video) getActivity().getIntent()
.getSerializableExtra(VideoDetailsFragment.EXTRA_VIDEO);
}
回放控件的剩余部分可以分成更小的方法。在类的顶部包含下列成员变量。
private ArrayObjectAdapter mRowsAdapter;
private ArrayObjectAdapter mPrimaryActionsAdapter;
private ArrayObjectAdapter mSecondaryActionsAdapter;
private PlaybackControlsRow mPlaybackControlsRow;
private PlaybackControlsRow.PlayPauseAction mPlayPauseAction;
private PlaybackControlsRow.RepeatAction mRepeatAction;
private PlaybackControlsRow.ShuffleAction mShuffleAction;
private PlaybackControlsRow.FastForwardAction mFastForwardAction;
private PlaybackControlsRow.RewindAction mRewindAction;
private PlaybackControlsRow.SkipNextAction mSkipNextAction;
private PlaybackControlsRow.SkipPreviousAction mSkipPreviousAction;
private PlaybackControlsRow.HighQualityAction mHighQualityAction;
private PlaybackControlsRow.ClosedCaptioningAction mClosedCaptionAction;
mRowsAdapter将用于垂直显示动作按钮的水平行和控件的其他细节。mPlaybackControlsRow、mPrimaryActionsAdapter和mSecondaryActionsAdapter包含用户可以采取的控制和动作的附加细节。其余项目分别代表不同的预定义动作,可在PlaybackOverlayFragment上显示。
接下来,您将在名为setupPlaybackControlsRow的onCreate末尾添加一个新的方法调用。这个方法将初始化三个ArrayObjectAdapter实例,并分配一个ControlButtonPresenterSelector来决定在任何给定的时间应该显示什么按钮。
private void setupPlaybackControlsRow() {
mPlaybackControlsRow = new PlaybackControlsRow( mVideo );
ControlButtonPresenterSelector presenterSelector =
new ControlButtonPresenterSelector();
mPrimaryActionsAdapter = new ArrayObjectAdapter(presenterSelector);
mSecondaryActionsAdapter = new ArrayObjectAdapter(presenterSelector);
mPlaybackControlsRow.setPrimaryActionsAdapter(mPrimaryActionsAdapter);
mPlaybackControlsRow.setSecondaryActionsAdapter(
mSecondaryActionsAdapter);
}
写完setupPlaybackControlsRow后,添加一个对setupPresenter的调用到onCreate. setupPresenter会将OnActionClickedListener与片段相关联,创建类表示器,并将PlaybackControlsRow添加到PlayerControlsFragment的主适配器。
private void setupPresenter() {
ClassPresenterSelector ps = new ClassPresenterSelector();
PlaybackControlsRowPresenter playbackControlsRowPresenter =
new PlaybackControlsRowPresenter( new DescriptionPresenter() );
playbackControlsRowPresenter.setOnActionClickedListener(this);
playbackControlsRowPresenter.setSecondaryActionsHidden(false);
ps.addClassPresenter(PlaybackControlsRow.class,
playbackControlsRowPresenter);
ps.addClassPresenter(ListRow.class, new ListRowPresenter());
mRowsAdapter = new ArrayObjectAdapter(ps);
mRowsAdapter.add(mPlaybackControlsRow);
}
你会注意到DescriptionPresenter被传递到了PlaybackControlsRowPresenter. DescriptionPresenter扩展AbstractDetailsDescriptionPresenter的构造函数中,并且是PlayerControlsFragment的内部类。它用于在控件上方添加细节。在这种情况下,它会在控件上方添加媒体的名称。如果您将 null 传递给PlaybackControlsRowPresenter的构造函数,您将只能看到覆盖图中的控件。
static class DescriptionPresenter extends AbstractDetailsDescriptionPresenter {
@Override
protected void onBindDescription(ViewHolder viewHolder, Object item) {
viewHolder.getTitle().setText(((Video) item).getTitle());
}
}
创建操作
现在您的行已经初始化,是时候向它们添加操作按钮了。将以下四个方法调用放在onCreate的末尾。
initActions();
setupPrimaryActionsRow();
setupSecondaryActionsRow();
setAdapter(mRowsAdapter);
initActions将每个成员变量动作初始化为新对象。每个动作都将作为一个按钮出现在控件叠层上。
private void initActions() {
mPlayPauseAction =
new PlaybackControlsRow.PlayPauseAction(getActivity());
mRepeatAction =
new PlaybackControlsRow.RepeatAction(getActivity());
mShuffleAction =
new PlaybackControlsRow.ShuffleAction(getActivity());
mSkipNextAction =
new PlaybackControlsRow.SkipNextAction(getActivity());
mSkipPreviousAction =
new PlaybackControlsRow.SkipPreviousAction(getActivity());
mFastForwardAction =
new PlaybackControlsRow.FastForwardAction(getActivity());
mRewindAction =
new PlaybackControlsRow.RewindAction(getActivity());
mHighQualityAction =
new PlaybackControlsRow.HighQualityAction(getActivity());
mClosedCaptionAction =
new PlaybackControlsRow.ClosedCaptioningAction(getActivity());
}
通过将每个动作分配给适当的适配器,可以指定每个动作进入哪一行。setupPrimaryActionsRow会将跳回、倒带、播放/暂停、快进和快进添加到顶部控制行。setupSecondaryActionsRow将在底部一行添加重复、无序播放、高质量切换和隐藏字幕选项。应该注意的是,向适配器分配操作的顺序与它们从左到右显示的顺序直接相关。
private void setupPrimaryActionsRow() {
mPrimaryActionsAdapter.add(mSkipPreviousAction);
mPrimaryActionsAdapter.add(mRewindAction);
mPrimaryActionsAdapter.add(mPlayPauseAction);
mPrimaryActionsAdapter.add(mFastForwardAction);
mPrimaryActionsAdapter.add(mSkipNextAction);
}
private void setupSecondaryActionsRow() {
mSecondaryActionsAdapter.add(mRepeatAction);
mSecondaryActionsAdapter.add(mShuffleAction);
mSecondaryActionsAdapter.add(mHighQualityAction);
mSecondaryActionsAdapter.add(mClosedCaptionAction);
}
此时,您可以运行应用并启动视频播放器活动来查看回放控件,如图 3-5 所示。

图 3-5。
Video playback controls
您会注意到视频不会自动开始,按钮也不会做任何事情。虽然onActionClicked方法已经与控件相关联,但是方法本身还没有做任何事情。你现在可以处理了。
@Override
public void onActionClicked(Action action) {
if(action.getId() == mPlayPauseAction.getId()) {
if(mPlayPauseAction.getIndex()
== PlaybackControlsRow.PlayPauseAction.PLAY) {
setFadingEnabled(true);
mControlsCallback.play();
mRowsAdapter.notifyArrayItemRangeChanged(0, 1);
} else {
setFadingEnabled( false );
mControlsCallback.pause();
}
((PlaybackControlsRow.MultiAction) action).nextIndex();
mPrimaryActionsAdapter.notifyArrayItemRangeChanged(
mPrimaryActionsAdapter.indexOf(action), 1);
} else {
Toast.makeText( getActivity(), "Other action”,
Toast.LENGTH_SHORT ).show();
}
}
如果单击的动作是播放/暂停按钮,您将检查按钮的状态。如果按钮处于播放状态,您将启用控件淡入淡出,使控件从屏幕上消失,并通知PlayerActivity应该开始播放视频。如果点击的按钮是暂停,控件淡入淡出将被禁用,以便控件保持可见,并且PlayerActivity将被通知媒体应该暂停。当按下播放/暂停按钮时,您必须处理的最后一件事是改变按钮的状态。由于PlayPauseAction是MultiAction类的扩展,你可以在主行动作上调用notifyArrayItemRangeChanged,它将处理按钮类型的改变。如果按下任何其他按钮,此示例将显示一条 Toast 消息。如果你想改变这一点,你可以通过检查预先定义的动作来控制应用中的任何按钮。
摘要
恭喜你!您已经为完整的工作媒体应用打下了基础。现在,您应该能够运行应用并查看BrowseFragment中的所有媒体,进入DetailsFragment以查看关于每条内容的更多信息,并使用向后倾斜支持库提供的控件来控制视频。虽然这是很好的第一步,但是还可以做更多的事情来定制你的应用。在下一章中,您将了解如何通过实现本地搜索组件、首选项以及向 Android TV 主屏幕添加搜索和推荐来增强您的应用。还将向您介绍对 Android TV 开发有用的附加组件,但不一定适合这个演示应用的范围。
四、丰富您的媒体应用
虽然您在上一章中构建的应用很好地演示了如何创建一个基本的媒体应用,但是您无疑会希望为您的用户添加更多的功能。幸运的是,Android TV 平台和 Leanback 支持库提供了多种工具,您可以使用它们来改进您的应用并与您的用户互动。在本章中,您将学习如何实现简单版本的全球和本地搜索,向您的应用添加偏好设置屏幕,并通过更新您在上一章中创建的演示应用,向 Android TV 主屏幕推荐行提供推荐。您还将了解适用于 Android TV 的其他功能,例如直播频道和 Now Playing card。
应用内搜索
当您的应用包含大量内容时,用户能够找到他们正在寻找的特定媒体项目就变得非常重要。幸运的是,BrowseFragment类支持一个名为 SearchOrbView 的新内置视图。当用户点击 SearchOrbView 时,他们应该会看到一个新的片段,便于在你的应用中进行搜索。在本节中,您将学习如何将 SearchOrbView 添加到MainFragment中,并在您在上一章中开始的演示应用中使用本地搜索。
添加 SearchOrbView 视图
由于BrowseFragment包含一个内置的 SearchOrbView,您可以用几行代码轻松地使它可访问。在MainFragment中,在onActivityCreated的末尾添加一个名为initSearchOrb的新方法调用,这样onActivityCreated看起来如下
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
loadData();
setTitle("Apress Media Player");
setHeadersState(HEADERS_HIDDEN);
setHeadersTransitionOnBackEnabled(true);
loadRows();
setOnItemViewClickedListener( this );
initSearchOrb();
}
在初始化搜索之前,进入app/src/res/values并创建一个名为colors.xml的新值资源文件。该文件将包含一些颜色,您将在一个易于访问的位置在整个应用中使用这些颜色。当文件创建后,添加一个名为search_button_color的新颜色,并赋予它任何适合你的应用主题的颜色十六进制值。这个例子使用了一个由#FFA500 定义的橙子。您的colors.xml文件应该如下所示:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="search_button_color">#FFA500</color>
</resources>
接下来,您需要在您的MainFragment类中定义initSearchOrb方法。您可以在此方法中设置 SearchOrbView 颜色,并确定单击时它会做什么。为了使 SearchOrbView 可见,您唯一需要做的就是通过setOnSearchClickedListener方法设置一个OnClickListener。您还可以通过调用setSearchAffordanceColor将SearchOrbView的颜色设置为colors.xml中search_button_color定义的颜色。
private void initSearchOrb() {
setSearchAffordanceColor(ContextCompat.getColor(getActivity(),
R.color.search_button_color));
setOnSearchClickedListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
}
});
}
Note
您可能会注意到,这里的颜色是通过调用ContextCompat .getColor 来检索的。从 SDK 23 开始,使用getResources().getColor(int colorResource)已被弃用,这是从资源中检索颜色的推荐方式。
当你运行你的应用时,你现在应该在你的BrowseFragment的左上角看到搜索球,如图 4-1 所示。您会注意到 SearchOrbView 已经内置了一些动画,当突出显示时,这些动画会导致球体的大小增加,并且还会产生涟漪效应,以便让用户知道它是可选的。

图 4-1。
BrowseFragment with a SearchOrbView visible
当您选择 SearchOrbView 时,您会注意到什么也没有发生。这是因为与 SearchOrbView 关联的OnClickListener是空的。在让 SearchOrbView 在被选中时执行某些操作之前,您需要定义一个新的活动和片段来处理搜索。
创建本地搜索活动和片段
在应用包文件夹中,您需要创建两个新的 Java 文件。将第一个命名为MediaSearchActivity.java,并将其扩展为Activity。这个活动现在唯一要做的事情是为一个布局文件设置一个内容视图,您将在本节的后面定义这个布局文件。
public class MediaSearchActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView( R.layout.activity_search );
}
}
第二个 Java 文件应该被命名为MediaSearchFragment.java,它将扩展后倾支持库类SearchFragment. SearchFragment,允许您实现自己的SearchResultProvider,并返回一个包含结果的ObjectAdapter,然后使用它以与BrowseFragment相同的方式呈现一个RowsFragment。
您还需要实现SpeechRecognitionCallback接口。SpeechRecognitionCallback是随着安卓棉花糖的发布而加入的,作为SpeechRecognizer的替代品。使用这个回调,用户在执行搜索时不需要显式地授予RECORD_AUDIO使用语音动作的权限。
最后,您需要实现OnItemViewClickedListener,这样您就可以确定当用户选择一个作为搜索结果返回的条目时采取什么动作。一旦创建了MediaSearchFragment并添加了带有方法存根的接口,您的类应该如下所示:
public class MediaSearchFragment extends SearchFragment implements
SpeechRecognitionCallback,
SearchFragment.SearchResultProvider,
OnItemViewClickedListener {
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder,
Object item, RowPresenter.ViewHolder rowViewHolder, Row row) {
}
@Override
public ObjectAdapter getResultsAdapter() {
return null;
}
@Override
public boolean onQueryTextChange(String newQuery) {
return false;
}
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
@Override
public void recognizeSpeech() {
}
}
现在已经实现了两个 Java 文件,您需要创建一个布局文件,MediaSearchActivity使用它来显示MediaSearchFragment。在app/src/main/res/layout下,创建一个名为activity_search.xml的新布局 XML 文件。这个文件看起来类似于activity_main.xml,除了它将包含一个对MediaSearchFragment类的引用。
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="??http://schemas.android.com/apk/res/android
xmlns:tools="??http://schemas.android.com/tools
android:id="@+id/main_browse_fragment"
android:name="com.apress.mediaplayer.MediaSearchFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
tools:deviceIds="tv"
tools:ignore="MergeRootFrame" />
接下来,您需要在清单文件中声明MediaSearchActivity,这样您就能够在应用不崩溃的情况下使用它。您可以将下面一行代码放在AndroidManifest.xml中VideoDetailsActivity和PlayerActivity的定义下面。
<activity android:name=".MediaSearchActivity" />
在深入研究使搜索成为可能的代码之前,您需要做的最后一件事是将 SearchOrbView 连接到您的新MediaSearchActivity。回到MainFragment,通过更新 SearchOrbView onClickListener中的onClick方法,创建一个在点击球体时启动新搜索屏幕的意图。
setOnSearchClickedListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(getActivity(),
MediaSearchActivity.class );
startActivity( intent );
}
});
从键盘实现本地搜索
此时,您应该能够运行您的应用并选择 SearchOrbView,但是当您这样做时,您会注意到应用崩溃了。如果你查看 Android Studio 中的应用日志,你会看到你收到了一个IllegalStateException,上面写着搜索需要RECORD_AUDIO权限。在上一节中,我提到您将使用SpeechRecognitionCallback来使该许可变得不必要。但是,为了告诉SearchFragment您想要使用自己的回调,您还需要做一些工作。首先覆盖MediaSearchFragment中的onCreate方法,并将SearchResultProvider、SpeechRecognitionCallback和OnItemClickListener接口指向MediaSearchFragment,因为该片段自己实现了这些接口。
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setSearchResultProvider(this);
setSpeechRecognitionCallback(this);
setOnItemViewClickedListener(this);
}
现在,您应该能够构建您的应用并选择 search orb 来查看应用搜索屏幕的开始。此时,屏幕将由左上角的语音搜索激活按钮和顶部中心的编辑文本组成,用于接受键入的输入,如图 4-2 所示。

图 4-2。
Initial App SearchFragment
当您尝试在搜索字段中键入内容时,您应该会看到另一个应用崩溃。这是因为大多数的MediaSearchFragment还没有被实现来处理搜索。虽然有效的搜索超出了本书的范围,但是您将创建一个搜索的基本版本,它检查本地 JSON 文件中的数据,以便返回结果。首先在这个类的顶部声明三个新的成员变量:
- 一个整数,将在下一个会话中用作语音输入的请求代码
- 用于存储和显示结果的
ArrayObjectAdapter - 用于跟踪所有可搜索数据的视频对象列表
public static final int SPEECH_REQUEST_CODE = 42;
private ArrayObjectAdapter mRowsAdapter;
private List<Video> mVideos;
在onCreate结束时,你将调用loadData()并用新的ListRowPresenter初始化mRowsAdapter。
loadData();
mRowsAdapter = new ArrayObjectAdapter( new ListRowPresenter() );
loadData将获取内容 JSON 数据并创建一个视频对象列表,您应该对此很熟悉,因为您之前已经在这个项目的其他类中实现了这个方法。
private void loadData() {
String json = Utils.loadJSONFromResource(getActivity(), R.raw.videos);
Type collection = new TypeToken<ArrayList<Video>>(){}.getType();
Gson gson = new Gson();
mVideos = gson.fromJson(json, collection);
}
现在你已经准备好了数据,是时候开始搜索了。在这个例子中,您将使用一个名为loadQuery的方法,该方法将接受来自用户的字符串,并从匹配查询的数据中查找内容。该方法将首先清除先前搜索的结果,并验证查询是否存在且不为空。接下来,您将遍历数据,查看媒体标题是否包含用户提供的查询。虽然这不是从数据中检索结果的最有效方法,但在了解 Android TV 如何显示搜索结果时,这是一种简单易懂的方法。匹配搜索查询的每个项目将被放入一个新的ArrayObjectAdapter中,以卡片的形式显示。一旦ArrayObjectAdapter被结果填充,一个新的Header和ListRow将被添加到顶层ArrayObjectAdapter来显示结果。
private void loadQuery(String query) {
if(mRowsAdapter != null)
mRowsAdapter.clear();
if(query == null || query.length() == 0)
return;
ArrayObjectAdapter listRowAdapter = new
ArrayObjectAdapter(new CardPresenter());
for(Video video : mVideos) {
if(video.getTitle() != null &&
video.getTitle().toLowerCase().contains(query.toLowerCase())) {
listRowAdapter.add(video);
}
}
if(listRowAdapter.size() == 0)
return;
HeaderItem header = new HeaderItem("Results");
mRowsAdapter.add(new ListRow(header, listRowAdapter));
}
当loadQuery完成时,您需要将它连接到MediaSearchFragment,以便应用知道调用该方法。您将需要使用来自SearchResultProvider接口的三个存根方法。getResultsAdapter将简单地返回mRowsAdapter,这样该片段将知道将显示什么。onQueryTextChange和onQueryTextSubmit将各自获取查询字符串参数,然后调用loadQuery。你会注意到这两个方法都返回一个布尔值。如果返回 true,则意味着结果由于查询而发生了变化。为了简单起见,对于这个简单的演示,我们将始终返回 true,即使结果可能与上次调用这些方法之一时没有什么不同。
@Override
public ObjectAdapter getResultsAdapter() {
return mRowsAdapter;
}
@Override
public boolean onQueryTextChange(String newQuery) {
loadQuery(newQuery);
return true;
}
@Override
public boolean onQueryTextSubmit(String query) {
loadQuery(query);
return true;
}
此时,您将能够运行您的应用并输入您的搜索查询以返回结果,如图 4-3 所示。

图 4-3。
In-app search screen implementation
要完成文本搜索,您最不想做的事情就是在选择一个返回的搜索结果时执行一个操作。您可以通过从OnItemViewClickedListener接口覆盖onItemClicked方法来做到这一点。当一个项目被选中时,确保它是一个视频对象的实例,然后通过一个意向传递给VideoDetailActivity。
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) {
if(item instanceof Video) {
Video video = (Video) item;
Intent intent = new Intent(getActivity(),
VideoDetailsActivity.class);
intent.putExtra(VideoDetailsFragment.EXTRA_VIDEO, video);
startActivity(intent);
}
}
现在,当您单击返回的结果时,将显示该视频的详细信息屏幕(图 4-4 ),您的用户将能够查看媒体内容。

图 4-4。
Detail screen shown when selecting an item from search
虽然能够输入搜索查询对你的用户来说是有益的,但是许多用户仍然希望能够说出他们正在寻找的东西,并让它出现在屏幕上。在下一节中,您将学习如何通过接受用户的语音输入来适应这一点,以便您的应用可以显示搜索结果。
使用语音输入进行本地搜索
有了搜索的框架,添加语音支持就相当简单了。当您在MediaSearchFragment中实现SpeechRecognitionCallback接口时,您为方法recognizeSpeech添加了一个存根。在这个方法中,您将使用从getRecognizerIntent接收的意图和您在MediaSearchFragment顶部的上一节中定义的请求代码来调用startActivityForResult。
@Override
public void recognizeSpeech() {
startActivityForResult(getRecognizerIntent(),
SPEECH_REQUEST_CODE);
}
这将触发onActivityResult在MediaSearchFragment中被调用。在这里,您将检查requestCode的值,以确保它与SPEECH_REQUEST_CODE匹配,并且resultCode与Activity.RESULT_OK匹配。如果这两个都为真,那么您将调用setSearchQuery。setSearchQuery方法将设置搜索栏中的文本,并接受两个参数:包含搜索查询的 intent 和一个布尔值。一旦接收到语音输入,将布尔值设置为真将自动调用onQueryTextSubmit。
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if(requestCode == SPEECH_REQUEST_CODE
&& resultCode == Activity.RESULT_OK) {
setSearchQuery(data, true);
}
}
此时,如果用户选择搜索屏幕上的语音按钮,MediaSearchFragment中的一切都应该正常工作。但是,如果您的用户选择遥控器上的语音输入按钮,将会显示 Android TV 全球搜索屏幕。您可以改变这种行为,以便通过覆盖您的MediaSearchActivity和MainActivity文件中的onSearchRequested方法来启动MediaSearchActivity。此方法还必须返回一个布尔值。如果返回 true,则意味着搜索活动已经开始。如果动作被阻止,则返回 false。
@Override
public boolean onSearchRequested() {
startActivity(new Intent(this, MediaSearchActivity.class));
return true;
}
经过最后几项更改,用户现在只要在你的应用中按下遥控器上的搜索按钮,就会被带到你的自定义搜索屏幕。你现在应该能够将你所学到的关于本地搜索的知识应用到你自己的 Android 电视应用中,以丰富你的用户体验。
实现首选项屏幕
当你想让你的用户能够在 Android 应用中定制他们的体验时,你通常使用PreferenceFragment类。PreferenceFragment允许您显示用户可以操作的项目列表,并自动保存对应用SharedPreferences的更改。考虑到这个组件的有用性,Google 提供了一个向后倾斜的偏好库,其中包含一个名为LeanbackPreferenceFragment的定制PreferenceFragment。这个新的类提供了与PreferenceFragment相同的功能,同时也符合电视设计模式。在本节中,您将学习如何在您的演示应用中实现它。
由于这个组件使用了一个尚未添加到您的项目中的库,您需要将它作为一个依赖项包含在您的build.gradle文件中。在撰写本文时,这个库的版本 23.1.1 是最新的。在build.gradle的 dependencies 节点下,添加下面一行,然后同步您的项目。
compile 'com.android.support:preference-leanback-v17:23.1.1'
显示偏好项入口点
如果你看过 Android TV 主屏幕,你会注意到最后一行包含两个设置项目(如图 4-5 所示)。您将在演示应用中遵循这一模式,在MainFragment底部提供一个新的设置项目,该项目将向您的LeanbackPreferenceFragment打开。

图 4-5。
Android home screen and location of settings
为了实现这一点,您将向包含MainFragment中的媒体卡行的ArrayObjectAdapter添加一个新的行和标题。您还将创建一个新的演示者,以便与您以前使用的媒体卡相比,设置项会更突出。开始创建一个名为PreferenceCardPresenter的新 Java 类,并让它扩展Presenter类。您需要在这个类中声明三个方法:onCreateViewHolder、onBindViewHolder和onUnbindViewHolder。
public class PreferenceCardPresenter extends Presenter {
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, Object item) {
}
@Override
public void onUnbindViewHolder(ViewHolder viewHolder) {
}
}
在onCreateViewHolder中,您将创建一个TextView,设置那个TextView的大小以匹配MainFragment中的媒体卡,并应用一些其他样式以使卡为灰色,白色文本居中。
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
TextView view = new TextView(parent.getContext());
view.setLayoutParams(new ViewGroup.LayoutParams(210, 210));
view.setFocusable(true);
view.setBackgroundColor(ContextCompat.getColor(parent.getContext(),
R.color.preference_card_background));
view.setTextColor(Color.WHITE);
view.setGravity(Gravity.CENTER);
return new ViewHolder(view);
}
接下来,您需要获取传递给该类的对象,将其转换为一个字符串,并使用该字符串来设置将为该项显示的文本。
@Override
public void onBindViewHolder(ViewHolder viewHolder, Object item) {
((TextView) viewHolder.view).setText((String) item);
}
在onCreateViewHolder方法中,您使用颜色资源值为设置卡设置背景颜色。为此,您需要在您的colors.xml文件中添加一个名为preference_card_background的颜色资源。打开app/src/main/res/values目录下的colors.xml文件,并在 resources 标签中添加下面一行
<color name=``preference_card_background">#AAAAAA</color>
当您完成PreferenceCardPresenter后,您可以关闭该文件并打开MainFragment。一旦你打开了MainFragment,将下面的代码添加到loadRows的末尾。
setupPreferences(adapter);
setupPreferences将接受用于显示媒体内容行的ArrayObjectAdapter,以便您可以将包含您的偏好项的行附加到您的BrowseFragment的底部。在这个方法中,您将创建一个新的HeaderItem和PreferenceCardPresenter,然后您将初始化一个新的ArrayObjectAdapter来包含您的设置项。最后,该方法将向带有设置项标题的行添加一个新字符串,并将首选项行添加到主BrowseFragment行集中。
private void setupPreferences(ArrayObjectAdapter adapter) {
HeaderItem gridHeader = new HeaderItem(adapter.size(), "Preferences");
PreferenceCardPresenter presenter = new PreferenceCardPresenter();
ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(presenter);
gridRowAdapter.add("Settings");
adapter.add(new ListRow(gridHeader, gridRowAdapter));
}
如果你现在运行你的应用,你会在MainFragment的底部看到一个灰色的卡片,里面有白色的文字,如图 4-6 所示。

图 4-6。
LeanbackPreferenceFragment entry point in MainFragment
为了打开您的首选项屏幕,修改OnItemClicked方法以检查所选项目是否等于“设置”。如果是,为SettingsActivity.class创建一个新的意图,然后用这个意图调用startActivity。
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) {
if(item instanceof Video) {
Video movie = (Video) item;
Intent intent = new Intent(getActivity(),
VideoDetailActivity.class);
intent.putExtra(VideoDetailsFragment.EXTRA_VIDEO, movie);
startActivity(intent);
} else if("Settings".equals(item)) {
Intent intent = new Intent(getActivity(), SettingsActivity.class);
startActivity(intent);
}
}
创建首选项屏幕
要编译您的应用,您需要创建几个新文件。在您的应用包下,创建一个新的 Java 类,并将其命名为SettingsActivity.java。这个类将扩展Activity,并且您将覆盖onCreate,以便您可以将布局文件设置为内容视图。
public class SettingsActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
}
}
接下来,您需要创建布局文件。在项目的布局资源文件夹中创建一个名为activity_settings.xml的新布局文件。从上几个活动布局文件来看,这应该很熟悉,尽管这一个将包含对您将要编写的名为SettingsFragment的新类的引用。
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="??http://schemas.android.com/apk/res/android
android:id="@+id/settings_fragment"
android:name="com.apress.mediaplayer.SettingsFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
创建好布局后,现在可以在应用包目录中创建SettingsFragment Java 文件。这个新类将扩展LeanbackPreferenceFragment并实现OnSharedPreferenceChangeListener接口。
public class SettingsFragment extends LeanbackPreferenceFragment
implements OnSharedPreferenceChangeListener {
@Override
public void onSharedPreferenceChanged(SharedPreferences
sharedPreferences, String key) {
}
@Override
public void onCreatePreferences(Bundle bundle, String s) {
}
}
接下来你需要覆盖onPause和onResume,这样onSharedPreferenceChangedListener就可以注册和注销了。
@Override
public void onResume() {
super.onResume();
getPreferenceManager().getSharedPreferences()
.registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onPause() {
getPreferenceManager().getSharedPreferences()
.unregisterOnSharedPreferenceChangeListener(this);
super.onPause();
}
每当用户修改首选项屏幕上的项目时,就会调用onSharedPreferenceChanged方法,允许您更改应用与用户的交互方式。
在SettingsFragment中,您需要做的最后一件事是覆盖onCreate,这样您就可以将定义您的偏好的 XML 文件与您的片段相关联。
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
}
现在您已经完成了SettingsFragment,您将需要制作preferences.xml文件。在res目录下创建一个名为xml的新资源文件夹。接下来,创建一个新的 XML 资源文件,并将其命名为preferences.xml。您的首选项文件将包含 Android TV 支持的各种首选项示例,包括选中的项目、可选项目列表、编辑文本对话框以及除非其父项目被选中否则无法选择的项目。应该注意的是,Android TV 不支持一些偏好设置功能,例如包含转到网页的意图。
<PreferenceScreen
xmlns:android="??http://schemas.android.com/apk/res/android
android:title="Preferences">
<PreferenceCategory
android:title="Inline Preference">
<CheckBoxPreference
android:key="checkbox_preference"
android:title="CheckboxPreference"
android:summary="Checkbox Preference Summary" />
</PreferenceCategory>
<PreferenceCategory
android:title="Dialog Preference">
<EditTextPreference
android:key="edittext_preference"
android:title="Edit Text Preference"
android:summary="Edit Text Preference Summary"
android:dialogTitle="Dialog Title Edit Text Preference" />
<ListPreference
android:key="list_preference"
android:title="List Preference"
android:summary="List Preference Summary"
android:entries="@array/entries_list_preference"
android:entryValues="@array/entries_list_preference"
android:dialogTitle="List Preference Dialog Title" />
</PreferenceCategory>
<PreferenceCategory
android:title="Attributes Title">
<CheckBoxPreference
android:key="parent_checkbox_preference"
android:title="Parent Preference Title"
android:summary="Parent Preference Summary" />
<CheckBoxPreference
android:key="child_checkbox_preference"
android:dependency="parent_checkbox_preference"
android:title="Child Preference Title"
android:summary="Child Preference Summary" />
</PreferenceCategory>
</PreferenceScreen>
在这个文件中,使用了一个名为entries_list_preference的字符串数组。在 res 文件夹下的 values 资源目录中,创建一个名为string-array.xml的新文件,并用一个项目列表填充它。在这个演示中,你只需列出太阳系中的前四颗行星;但是,您可以使用任何您喜欢的数据。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="entries_list_preference">
<item>Mercury</item>
<item>Venus</item>
<item>Earth</item>
<item>Mars</item>
</string-array>
</resources>
为了运行您的应用并进入新的首选项屏幕,您需要将应用主题中的preferenceTheme样式项目配置为PreferenceThemeOverlay样式。你可以从你的res/values目录中打开styles.xml,然后在AppTheme风格下添加新的项目。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="@style/Theme.Leanback">
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
</style>
</resources>
最后,打开AndroidManifest.xml并为SettingsActivity定义一个新的活动项,这样系统就知道该活动可以在您的应用中使用。
<activity android:name=".SettingsActivity" />
此时,您应该能够运行您的应用,并从MainFragment中选择设置项目,进入首选项屏幕。该页面将包含由preferences.xml定义的标题和项目列表,如图 4-7 所示。

图 4-7。
LeanbackPreferenceFragment screen
当你的用户选择这个片段中的任何一项时,一个SharedPreferences条目将被改变以反映这个选择,允许你决定你的应用应该如何为你的用户服务。复选框的值存储为布尔值,EditText首选项将编辑文本字段的内容保存为字符串,列表首选项将向用户显示一个对话框,其中包含可供选择的选项列表,这些选项也将存储为字符串,如图 4-8 所示。

图 4-8。
LeanbackPreferenceFragment list preference example
在本节中,您了解了 Android TV 支持的首选项,以及如何在您的应用中实现它们。在下一部分中,您将详细介绍您的演示程序,以便在用户不积极使用您的应用时,通过使用将显示在 Android TV 主屏幕上的建议来吸引他们。
使用建议
不管你的应用有多好,或者你构建了多少功能,你都必须让用户进入你的应用,让他们能够享受它。当用户坐在电视机前时,他们希望能够快速找到内容并开始观看。回到第二章中,您已经了解了 Android TV 主屏幕的推荐行,在这一部分,您将了解如何利用这一功能与您的用户互动。
建筑推荐卡
推荐只是从后台服务创建的通知卡,定期推送到 Android TV 主屏幕,以便向您的用户提供内容。现在,您可以通过在应用的包目录下创建一个名为RecommendationService的新 Java 类并让它扩展IntentService来开始推出推荐。
public class RecommendationService extends IntentService {
public RecommendationService() {
super("RecommendationService");
}
@Override
protected void onHandleIntent(Intent intent) {
}
}
在本演示中,您需要将两个成员变量放在服务的顶部:一个表示应用将向用户显示的最大推荐数量的整数,以及一个用于构建推荐的视频对象列表。
private int MAX_RECOMMENDATIONS = 3;
private List<Video> mVideos;
onHandleIntent方法是创建您的推荐的所有工作发生的地方。您可以通过调用loadData来开始设置该方法,它将像在其他类中一样工作,以填充mVideos结构。接下来,您将需要创建一个新的NotificationManager对象,一旦构建好,它将用于推出您的通知对象集。当您创建了NotificationManager后,您可以做一些简单的应急计划,检查mVideos是否为空,如果是则返回。如果它不为 null 或空,但是数据中的项目数少于您通常会推出的建议数(在本例中为 3),那么您可以更改建议数,以便显示您拥有的所有数据。
@Override
protected void onHandleIntent(Intent intent) {
loadData();
NotificationManager notificationManager = (NotificationManager)
getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
int numOfRecommendations = MAX_RECOMMENDATIONS;
if( mVideos == null ) {
return;
} else if(mVideos.size() < MAX_RECOMMENDATIONS){
numOfRecommendations = mVideos.size();
}
}
因为所有要构建的通知都将共享一些公共属性,所以您可以创建一个基本的NotificationCompat.Builder对象,它具有在每个建议中共享的公共属性。
NotificationCompat.Builder builder = new
NotificationCompat.Builder(getApplicationContext())
.setSmallIcon(R.mipmap.ic_launcher)
.setLocalOnly(true)
.setOngoing(true)
.setColor(ContextCompat.getColor(getApplicationContext(),
android.R.color.black))
.setCategory(Notification.CATEGORY_RECOMMENDATION);
该构建器将创建一个通知:
- 使用卡片右下角的应用图标
- 将只与本地设备相关
- 被标记为正在进行,这意味着它比非正在进行的通知具有更高的优先级
- 将有一个黑色的选择颜色(虽然你可以使用任何适合你的应用的主题的颜色)
- 会被安卓电视主屏幕归类为推荐
您可以通过创建一个循环来结束onHandleIntent方法,该循环遍历您的数据并创建您将显示的三个通知,然后将它们发送到NotificationManager。对于本例,您只需从数据集中抓取前三个视频进行显示,尽管您自己的应用将使用符合您目的的逻辑。
for(int i = 0; i < numOfRecommendations; i++ ) {
Video video = mVideos.get(i);
Bitmap bitmap;
try {
bitmap = Picasso.with(this)
.load(video.getPoster()).
resize(313, 176)
.get();
} catch( IOException e ) {
continue;
}
builder.setPriority(numOfRecommendations - i)
.setContentTitle(video.getTitle())
.setContentText(video.getCategory())
.setLargeIcon(bitmap)
.setContentIntent(buildPendingIntent(video, i + 1));
notificationManager.notify(i + 1, builder.build());
}
虽然你在上一章中使用 Picasso 在应用内的媒体卡上显示图像,但应该重申的是,Picasso 是来自 Square 的第三方开源库,而不是 Android 框架或官方库的一部分。对 Picasso image loader 类的调用将同步下载存储在video.getPoster中的 URL 处的图像,并将其调整为 313 像素宽、176 像素高。虽然这个宽度和高度看起来可能是任意的,但它们是谷歌自己的应用(如 YouTube 应用)使用的尺寸,以符合 16:9 的显示比例。该位图将被用作推荐卡的主图像。
一旦您的卡片图像被下载,您可以在NotificationCompat.Builder对象上设置额外的属性,例如推荐在推荐行中出现的顺序、卡片上的标题和描述,以及当推荐被选中时将被激发的意图。
此时,您可能会注意到您的应用无法编译。这是因为buildPendingIntent还没有被定义。这可以通过在您的RecommendationService类中添加以下方法来解决。
private PendingIntent buildPendingIntent(Video video, long id ) {
Intent detailsIntent = new Intent(this, VideoDetailActivity.class);
detailsIntent.putExtra(VideoDetailsFragment.EXTRA_VIDEO, video);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addParentStack(VideoDetailActivity.class);
stackBuilder.addNextIntent(detailsIntent);
detailsIntent.setAction(Long.toString(id));
PendingIntent intent = stackBuilder.getPendingIntent(0,
PendingIntent.FLAG_UPDATE_CURRENT);
return intent;
}
该方法将创建一个启动VideoDetailsActivity的意图,并将其与一个TaskStackBuilder相关联,以便使用 Android TV 遥控器上的后退按钮将用户正确地返回到 Android TV 主屏幕,而不是试图将他们放在您的应用中。你会注意到这个意向也有一个对setAction的调用。这允许您确保用于推荐的每个PendingIntent对于创建它的数据是唯一的。否则,你的每一个推荐可能最终会打开一个基于相同数据的屏幕,而不是用户想要看到的。此时,您的服务将能够基于您的数据创建通知,并将它们作为推荐显示在 Android TV 主屏幕上。在下一节中,您将了解如何启动这项服务,以便它自动开始推送内容供您的用户欣赏。
启动推荐服务
启动推荐服务最简单的方法是当用户打开应用时。你可以通过进入MainActivity并在onCreate的末尾添加下面一行来简单地做到这一点。
startService(new Intent(this, RecommendationService.class));
当你的用户重启他们的 Android 电视设备,但没有打开你的应用时,这种策略就会遇到麻烦。为了处理这种情况,最好的办法是创建一个BroadcastReceiver来监听 Android TV 设备何时完成启动。当 Android TV 启动时,这个BroadcastReceiver将启动RecommendationService。为了演示这一点,创建一个名为BootupReceiver的新 Java 类,并让它扩展BroadcastReceiver。
public class BootupReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
}
}
当onReceive被触发时,您将想要检查已经传递给它的意图,以查看它是否是一个Intent.ACTION_BOOT_COMPLETED动作。如果是,那么你可以开始RecommendationService。您还需要创建一个名为scheduleRecommendationUpdate的新方法,它创建一个AlarmManager,以便每 30 分钟重启或更新RecommendationService。这将为用户提供来自你的应用的新的或更新的内容,希望能引起他们的注意,让他们进入你的应用。
private static final long INITIAL_DELAY = 5000;
@Override
public void onReceive(Context context, Intent intent) {
if(intent.getAction().endsWith(Intent.ACTION_BOOT_COMPLETED)) {
context.startService(new Intent(context,
RecommendationService.class));
scheduleRecommendationUpdate(context);
}
}
private void scheduleRecommendationUpdate(Context context) {
AlarmManager alarmManager = (AlarmManager)
context.getSystemService(Context.ALARM_SERVICE);
Intent recommendationIntent = new Intent(context,
RecommendationService.class);
PendingIntent alarmIntent = PendingIntent.getService(context, 0,
recommendationIntent, 0);
alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
INITIAL_DELAY,
AlarmManager.INTERVAL_HALF_HOUR,
alarmIntent);
}
在您的BroadcastReceiver完成之后,您将需要在应用标签的AndroidManifest.xml中声明它和RecommendationService。BootupReceiver将需要一个意图过滤器,以便它可以监听来自操作系统的BOOT_COMPLETED意图。
<service android:name=".RecommendationService"
android:enabled="true" />
<receiver android:name=".BootupReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
您还需要在AndroidManifest.xml的顶部包含RECEIVE_BOOT_COMPLETE权限声明。
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
随着MainActivity、RecommendationService、BootupReceiver和AndroidManifest.xml的完成,您现在应该能够运行您的应用并在推荐行中看到推荐。如果你在模拟器上测试你的应用,那么你的 Android 电视主屏幕应该看起来类似于图 4-9 。如果您在物理设备上进行测试,那么您的建议通知将与来自其他已安装应用的建议混合在一起。

图 4-9。
Populated recommendations on the Android TV home screen
在本节中,您已经了解了 Android TV 主屏幕的推荐行,以及如何在其中放置您自己的内容。虽然您现在已经能够做到这一点,但您可能会发现还有其他一些特性有助于让您的应用脱颖而出。其中之一是能够改变 Android 电视主屏幕的背景,以匹配您的内容。这是通过用 ContentProvider 存储一个位图文件并在您的通知被选中时通过 URI 访问它来实现的。虽然这超出了本书的范围,但它确实给你的应用增加了一点你的用户会喜欢的东西。在下一节中,您将继续学习如何通过 Android TV 全局搜索功能搜索您的数据和内容,从而在用户不在您的应用中时与他们互动。
安卓电视全球搜索
您的用户无需直接访问您的应用即可访问您的内容的另一种方式是通过 Android TV 的全球搜索功能。当用户按下控制器上的麦克风按钮或选择 Android TV 主屏幕左上角的搜索图标时,系统将打开一个搜索屏幕,并查看哪些已安装的应用提供了可供搜索的数据。在本节中,您将学习如何使用一些基本的 SQLite 功能,以及如何通过使用 ContentProvider 使您的内容可搜索。
构建搜索数据库
为了使您的内容可用于 Android TV 全球搜索,您需要创建一个 SQLite 表来存储您的内容信息,并且可以从您的应用外部访问。首先,您将为 SQLite 数据库编写一个处理程序类。这个类的目的是使通过使用 ContentProvider 访问您将创建的数据库变得更加容易。现在在应用包目录下创建这个类,并将其命名为VideoDatabaseHandler.java。这个文件需要在类的顶部声明和初始化多个属性,如下所示。
public class VideoDatabaseHandler {
private static final String DATABASE_NAME = "video_database_leanback";
private static final int DATABASE_VERSION = 1;
private static final String FTS_VIRTUAL_TABLE = "Leanback_table";
public static final String KEY_NAME =
SearchManager.SUGGEST_COLUMN_TEXT_1;
public static final String KEY_DATA_TYPE =
SearchManager.SUGGEST_COLUMN_CONTENT_TYPE;
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;
}
这里,DATABASE_NAME和DATABASE_VERSION用于创建您的 SQLite 数据库,FTS_VIRTUAL_TABLE包含将存储您所有数据的表的名称,其余项是将用于标识数据库中数据的键。接下来,您需要为您的表中将要使用的所有列制作一个映射。您可以在一个名为buildColumnMap的新方法中做到这一点。
private static HashMap<String, String> buildColumnMap() {
HashMap<String, String> map = new HashMap<String, String>();
map.put(KEY_NAME, KEY_NAME);
map.put(KEY_DATA_TYPE, KEY_DATA_TYPE);
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;
}
回到VideoDatabaseHandler的顶部,您需要创建一个新的数据项,并使用buildColumnMap进行初始化。该项目将用于为 SQLite 查询创建投影地图。
private static final HashMap<String, String> COLUMN_MAP =
buildColumnMap();
在开始处理查询之前,您需要创建一个名为VideoDatabaseOpenHelper的新内部类,它扩展了SQLiteOpenHelper来包装您的数据库。这个内部类将数据加载到数据库中,并创建将被查询的表。当您创建VideoDatabaseOpenHelper时,您还需要包含一个表示用于创建您的表的 SQL 命令的字符串、一个 SQLite 数据库和一个包含Context的WeakReference。
private static class VideoDatabaseOpenHelper extends SQLiteOpenHelper {
private final WeakReference<Context> mHelperContext;
private SQLiteDatabase mDatabase;
private static final String FTS_TABLE_CREATE =
"CREATE VIRTUAL TABLE " + FTS_VIRTUAL_TABLE +
" USING fts3 (" +
KEY_NAME + ", " +
KEY_DATA_TYPE + "," +
KEY_ACTION + "," +
KEY_PRODUCTION_YEAR + "," +
KEY_COLUMN_DURATION + ");";
public VideoDatabaseOpenHelper(Context context){
super(context, DATABASE_NAME, null, DATABASE_VERSION);
mHelperContext = new WeakReference<Context>(context);
}
@Override
public void onCreate(SQLiteDatabase db){
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
}
}
Note
您会注意到这个内部类的上下文存储在一个WeakReference中。使用一个带有WeakReference到Context的静态内部类是一种避免内存泄漏的方法,正如这篇 2009 年 Android 开发者的博客文章所提到的: android-developers.blogspot.com/2009/01/avoiding-memory-leaks.html 。
接下来,您需要完成onCreate方法。该方法将存储对数据库的引用,创建内容表,并将所有可搜索的项目加载到数据库中。
@Override
public void onCreate(SQLiteDatabase db) {
mDatabase = db;
mDatabase.execSQL(FTS_TABLE_CREATE);
loadDatabase();
}
loadDatabase 方法会将应用中的数据 JSON 文件中的所有视频加载到新线程中的表中。
private void loadDatabase() {
new Thread(new Runnable() {
public void run() {
try {
loadVideos();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
}
private void loadVideos() throws IOException {
List<Video> videos;
String json = Utils.loadJSONFromResource(mHelperContext.get(),
R.raw.videos);
Type collection = new TypeToken<ArrayList<Video>>(){}.getType();
Gson gson = new Gson();
videos = gson.fromJson(json, collection);
for(Video video : videos) {
addVideoForDeepLink(video);
}
}
public void addVideoForDeepLink(Video video) {
ContentValues initialValues = new ContentValues();
initialValues.put(KEY_NAME, video.getTitle());
initialValues.put(KEY_DATA_TYPE, "video/mp4");
initialValues.put(KEY_PRODUCTION_YEAR, "2015");
initialValues.put(KEY_COLUMN_DURATION, 6400000);
mDatabase.insert(FTS_VIRTUAL_TABLE, null, initialValues);
}
在addVideoForDeepLink方法中,您将四个值添加到一个ContentValues对象中。这些是表中信息可搜索的唯一必需项。您可以通过向表中添加更多的列并包含来自SearchManager类的键来添加数据。因为这个项目的样本数据不包含媒体的生产年份或持续时间,所以您可以在这个演示应用中使用虚拟信息。一旦您的ContentValues对象被填充,您就可以使用它将给定视频的数据插入到您的表中。
至此,您的VideoDatabaseOpenHelper内部类完成了。您需要在VideoDatabaseHandler的顶部创建一个对它的引用,并在类构造函数中初始化它。
private final VideoDatabaseOpenHelper mDatabaseOpenHelper;
public VideoDatabaseHandler(Context context) {
mDatabaseOpenHelper = new VideoDatabaseOpenHelper(context);
}
在VideoDatabaseHandler中你需要做的最后一件事是处理查询。还有两个方法要添加到这个类中。第一个是 query,它接受传递给它的查询,并返回一个包含搜索结果的Cursor对象。第二个是getWordMatch,它将接受一个字符串,并通过创建一个 SQL 命令传递给query方法来查看它是否匹配您的媒体数据中的任何标题。
public Cursor getWordMatch(String query, String[] columns) {
String selection = KEY_NAME + " MATCH ?";
String[] selectionArgs = new String[]{query + "*"};
return query(selection, selectionArgs, columns);
}
private Cursor query(String selection, String[] selectionArgs, String[] columns) {
SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
builder.setTables(FTS_VIRTUAL_TABLE);
builder.setProjectionMap(COLUMN_MAP);
Cursor cursor = builder.query(mDatabaseOpenHelper.getReadableDatabase(),
columns, selection, selectionArgs, null, null, null);
if (cursor == null) {
return null;
} else if (!cursor.moveToFirst()) {
cursor.close();
return null;
}
return cursor;
}
如果您以前没有使用过 SQLite,那么这个类可能会让人望而生畏,但是没关系。虽然 SQLite 超出了本书的范围,但我强烈建议通读 Google 的相关文档。随着你对数据库越来越熟悉,在 Android 平台上处理数据和做一些有趣的事情变得越来越容易。如果您已经熟悉了 SQLite,那么您已经有了一个良好的开端!在下一节中,您将创建一个新的 ContentProvider 来访问刚刚创建的数据库,并使其可用于 Android TV 全局搜索。
创建全球搜索内容提供商
内容提供商是 Android 让一个进程中的数据在另一个进程中可用的方式。它们封装数据,并提供必要的工具来安全地定义哪些数据可以被其他进程使用。创建一个新的 Java 类并将其命名为VideoContentProvider.java。它将扩展ContentProvider类。
public class VideoContentProvider extends ContentProvider {
@Override
public boolean onCreate() {
return false;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
return null;
}
@Override
public String getType(Uri uri) {
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
}
接下来,在类的顶部创建一个新的VideoDatabaseHandler对象,并在onCreate中初始化它。
private VideoDatabaseHandler mVideoDatabase;
@Override
public boolean onCreate() {
mVideoDatabase = new VideoDatabaseHandler(getContext());
return true;
}
在VideoContentProvider中,您需要做的最后一件事是充实查询方法。幸运的是,所有困难的工作都由VideoDatabaseHandler来处理。您唯一需要做的就是从 arguments 数组中获取传递给内容提供者的查询,创建一个数组来呈现您想要返回的数据,然后从VideoDatabaseHandler中的getWordMatch方法返回Cursor。
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
String query = selectionArgs[0];
query = query.toLowerCase();
String[] columns = new String[]{
BaseColumns._ID,
VideoDatabaseHandler.KEY_NAME,
VideoDatabaseHandler.KEY_DATA_TYPE,
VideoDatabaseHandler.KEY_PRODUCTION_YEAR,
VideoDatabaseHandler.KEY_COLUMN_DURATION,
SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID
};
return mVideoDatabase.getWordMatch(query, columns);
}
在下一节中,您将学习如何向外部流程公开您的内容提供者。
公开内容提供者
完成 Java 文件后,您需要设置您的应用,以便其他进程可以找到您的内容提供者。在你的res文件夹中,在xml目录下创建一个名为searchable.xml的新文件。该配置文件将描述您的内容提供商,并包含 Android TV 的其他重要信息。
<searchable xmlns:android="??http://schemas.android.com/apk/res/android
android:label="@string/app_name"
android:hint="Search for videos"
android:searchSettingsDescription="Description"
android:searchSuggestAuthority="com.apress.mediaplayer"
android:searchSuggestIntentAction="android.intent.action.VIEW"
android:searchSuggestIntentData="content://com.apress.mediaplayer/video_database_leanback"
android:searchSuggestSelection=" ?"
android:searchSuggestThreshold="1"
android:includeInGlobalSearch="true"
/>
当 searchable.xml 文件完成后,打开AndroidManifest.xml并在应用节点中添加以下代码行,以在应用中声明您的内容提供者。
<provider android:name=".VideoContentProvider"
android:authorities="com.apress.mediaplayer"
android:exported="true" />
现在,您的内容提供商已经公开,您需要配置您的应用,以便搜索返回的任何项目都可以正确打开。
对搜索动作作出反应
要完成处理全局搜索操作,您必须让 Android TV 系统知道当您的一个项目匹配搜索查询时该做什么。在AndroidManifest.xml中,修改VideoDetailsActivity的声明,使其包含一个SEARCH动作的意图过滤器,并指向您的searchable.xml文件作为元数据。
<activity android:name=".VideoDetailsActivity">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
如果您现在运行您的应用,您将能够使用 Android TV 的全局搜索功能按标题搜索您的媒体项目之一,如图 4-10 所示。

图 4-10。
Content found from a global search by title
还有一个问题。如果选择该项,应用将会崩溃。这是因为VideoDetailsFragment不知道如何处理没有视频传递给它的情况。要解决这个问题,打开VideoDetailsFragment并进入onCreate方法。紧接在试图从 intent 中的序列化 extra 初始化mVideo的行之后,添加以下代码块:
if( mVideo == null ) {
Intent intent = getActivity().getIntent();
Uri data = intent.getData();
String json = Utils.loadJSONFromResource(getActivity(), R.raw.videos);
Type collection = new TypeToken<ArrayList<Video>>(){}.getType();
Gson gson = new Gson();
List<Video> videos = gson.fromJson(json, collection);
mVideo = videos.get(Integer.parseInt(data.getLastPathSegment())- 1 );
}
这将为您的应用加载数据,并尝试根据与媒体项目在数据中的位置相关的标识符来查找合适的项目。现在,如果您运行您的应用并使用全局搜索选项,您应该能够单击应用中的任何结果,并进入该项目的正确详细信息屏幕。
虽然您还可以做更多的工作来改进这一功能,例如为返回的结果添加图像或包括更多元数据,但您已经完成了演示应用与 Android TV 全球搜索功能的集成。您现在拥有了一个充实的 Android TV 媒体应用,了解了如何通过 Leanback 支持库的各种组件为您的用户显示内容,添加了一个允许用户自定义其体验的偏好屏幕,实施了本地和全局搜索,并将您的功能与 Android TV 推荐系统相集成。
更多媒体应用功能
虽然您已经了解了 Android 电视媒体应用的许多功能,但还有更多的功能有待开发。在这一节中,您将了解到包含在向后倾斜支持库中的一些附加工具,这些工具对于给您的用户带来真正愉快的体验非常有用。本节将不会详细介绍实现这些特性的全部细节,但是您应该学习足够的知识,以便能够找到并理解来自 Google 的文档。
现在打牌
如果你的应用播放任何类型的媒体,可以在用户返回主屏幕后继续播放,如音频,那么你必须提供一种方法让用户返回你的应用,以便暂停或更改正在播放的内容。幸运的是,Android TV 通过包含一个可以出现在主屏幕推荐行中的 Now Playing 卡来支持这一点。您可以通过从后台服务创建一个MediaSession对象来启用正在播放的卡片,该对象控制您的音频并将元数据与会话相关联,以显示插图和关于播放内容的信息。您还可以将一个未决内容与正在播放的卡相关联,以便在选择该卡时启动您的应用。
GuidedStepFragment
有时候你会想帮助你的用户浏览一系列的决定。为了帮助解决这个问题,Google 在向后倾斜支持库中提供了GuidedStepFragment,它由两部分组成,右边是指南视图,左边是可选项目列表。guidance 视图将允许您提示您的用户,以便他们能够确定他们想要从选项列表中选择什么。GuidedStepFragment的一个例子是 Android TV 主屏幕上的系统设置,它可以允许用户浏览每个类别的多个级别的选项。
直播频道
由于用户熟悉通过浏览频道来查找要观看的内容的体验,您可以通过使用电视输入框架以这种方式呈现您自己的数据。这将允许您创建频道,通过利用另一个内容提供商和服务,将您的内容与来自硬件来源和其他应用的内容一起传输给用户。用户将能够通过使用安装在他们设备上的 Android TV 直播频道应用来浏览所有频道。
摘要
在本章中,您了解了可以在自己的媒体应用中实现的附加功能,这些功能可以丰富用户体验,并为用户提供有价值的工具。您在应用中添加了本地搜索,并通过 Android TV 的全球搜索功能进行搜索。您还了解了应用的倾斜式偏好片段,以及当用户不在您的应用中时,如何通过 Android TV 推荐系统与他们互动。除了您在自己的演示中使用的主题之外,还向您介绍了三个特性,这三个特性为您创建令人惊叹的用户体验提供了更多可能性。在下一章中,你将了解一些工具,它们将帮助你为安卓电视创造更好的游戏体验。
五、用于游戏开发的 Android 电视平台
虽然媒体应用构成了 Android TV 应用生态系统的重要组成部分,但游戏占开发者从 Play Store 获得的收入的大约 90%。如果你是一名游戏开发者或爱好者,想要有一个额外的途径从你的作品中获利,那么了解如何让你的游戏工作并为 Android TV 做好准备是至关重要的。令人欣慰的是,由于 Android TV 是一个功能齐全的 Android 操作系统,将您的游戏迁移到新平台并不需要太多时间。本章将重点介绍 Android TV 和一些使用该平台构建游戏的工具,因为开发完整的游戏是一个广泛的话题,超出了本书的范围。这一章也将只讨论 Android 开发,尽管其他流行的游戏引擎也可以在 Android TV 上运行。
安卓电视游戏与手机游戏
虽然为 Android TV 开发游戏与为手机和平板电脑开发游戏非常相似,但有几件事你需要知道,这样你的游戏才能按预期运行,并让你的用户感到愉快。首先也是最重要的,你的应用应该在横向模式下工作。虽然手机和平板电脑倾向于在纵向模式下使用,也可能在横向模式下使用,但电视几乎总是会在横向模式下使用。因此,你需要确保你的游戏能按预期运行,并明智地使用屏幕空间。你应该考虑的第二件事是,电视是一个房间里玩家共享的中央屏幕。如果你有一个卡牌或策略游戏,你的玩家必须能够互相隐藏他们的行动。解决这个问题的最好方法之一是通过使用手机和平板电脑创建第二屏幕体验,允许玩家私下执行他们的操作,然后在电视上更新游戏屏幕。
清单设置
正如在第二章中所讨论的,Android 电视上有单独的几行用于显示媒体应用和游戏。为了让您的游戏显示在游戏行中,您必须在AndroidManifest.xml的应用节点中将您的应用声明为游戏。
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:isGame="true"
android:theme="@style/AppTheme">
如果你的游戏支持 Android TV 游戏手柄的使用,那么你也需要在你的应用中声明这个特性。您可以通过在清单顶部添加一个新的uses-feature节点来做到这一点。
<uses-feature android:name="android.hardware.gamepad"
android:required="false" />
您会注意到这个节点中的required字段被设置为 false。如果此字段设置为 true,则您的应用将无法安装在 Android TV 设备上。除了这些非常小的变化,清单的其余部分将完全像标准移动应用一样工作。
游戏手柄控制器输入
虽然您的媒体应用使用简化的导航来支持 D-pad,但您的游戏可以支持 gamepad 控制器,以实现更加复杂的交互。有两种主要类型的输入可以来自游戏手柄控制器:数字和模拟。数字输入包括按钮,有按下和未按下两种状态。模拟输入由控制器上的操纵杆或触发器组成,可以提供设定范围内的值。在本节中,您将学习如何从游戏手柄上读取输入,以便在 Android TV 上为您的用户提供更好的游戏体验。
设置控制器演示项目
您将通过使用一个新的示例应用来了解 gamepad 控制器,该应用接受来自游戏控制器的输入并在屏幕上显示控件的状态。这个应用的源代码可以在本书的网站上找到( www.apress.com/9781484217832 )。您也可以自己创建这个项目,方法是在 Android Studio 中创建新的空 Android TV 应用并设置AndroidManifest.xml,如上一节所述。布局文件,在本例中为activity_main.xml,将由多个TextView对象组成,这些对象或者提供一个标签,或者被改变以显示它们相关输入的值。虽然下面的示例代码只代表了一项,但是当您看到 Java 代码时,其余的部分应该很容易理解,因此为了节省空间,这里不包括它们。
...
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/label_a"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button A: "/>
<TextView
android:id="@+id/button_a"
android:layout_toRightOf="@+id/label_a"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/state_not_pressed"/>
</RelativeLayout>
...
该示例还将使用strings.xml中的两个附加值来表示按钮的按下和未按下状态:
<string name="state_pressed">is pressed.</string>
<string name="state_not_pressed">is not pressed.</string>
接下来你需要修改MainActivity.java,使其包含所有可从布局文件中改变的视图的引用,并将它们与正确的视图相关联。
private TextView mButtonStateA;
private TextView mButtonStateB;
private TextView mButtonStateX;
private TextView mButtonStateY;
private TextView mButtonStateR1;
private TextView mButtonStateL1;
private TextView mJoystickState1AxisX;
private TextView mJoystickState1AxisY;
private TextView mJoystickState2AxisX;
private TextView mJoystickState2AxisY;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButtonStateA = (TextView) findViewById(R.id.button_a);
mButtonStateB = (TextView) findViewById(R.id.button_b);
mButtonStateX = (TextView) findViewById(R.id.button_x);
mButtonStateY = (TextView) findViewById(R.id.button_y);
mButtonStateR1 = (TextView) findViewById(R.id.button_r1);
mButtonStateL1 = (TextView) findViewById(R.id.button_l1);
mJoystickState1AxisX = (TextView)findViewById(R.id.joystick_1_axis_x);
mJoystickState1AxisY = (TextView)findViewById(R.id.joystick_1_axis_y);
mJoystickState2AxisX = (TextView)findViewById(R.id.joystick_2_axis_x);
mJoystickState2AxisY = (TextView)findViewById(R.id.joystick_2_axis_y);
}
一旦你完成了布局并保存了对关键视图的引用,你应该能够运行你的应用并看到一个类似于图 5-1 的屏幕。应该注意的是,虽然您可以在模拟器上运行这个程序,但是您需要一个实际的游戏手柄和一个 Android TV 设备来测试本节剩余部分的代码。

图 5-1。
Gamepad controller demo layout
存储控制器输入
为了简化维护控制器状态的过程,您将为这个名为GameController.java的示例项目创建一个新的工具类。每当接收到模拟或数字控件输入事件时,该类将存储数据,并为您的应用提供帮助方法,以确定当前正在使用哪些控件。
一旦创建了GameController.java,您将需要创建新的数据项来表示控制器上的按钮和操纵杆,以及帮助这些数据项正确映射到控制器的值。当GameController对象被实例化时,您将初始化按钮和操纵杆状态对象,然后为它们提供默认值。
public static final int BUTTON_A = 0;
public static final int BUTTON_B = 1;
public static final int BUTTON_X = 2;
public static final int BUTTON_Y = 3;
public static final int BUTTON_R1 = 4;
public static final int BUTTON_R2 = 5;
public static final int BUTTON_L1 = 6;
public static final int BUTTON_L2 = 7;
public static final int BUTTON_COUNT = 8;
public static final int AXIS_X = 0;
public static final int AXIS_Y = 1;
public static final int AXIS_COUNT = 2;
public static final int JOYSTICK_1 = 0;
public static final int JOYSTICK_2 = 1;
public static final int JOYSTICK_COUNT = 2;
private final float mJoystickPositions[][];
private final boolean mButtonState[];
public GameController() {
mButtonState = new boolean[BUTTON_COUNT];
mJoystickPositions = new float[JOYSTICK_COUNT][AXIS_COUNT];
resetState();
}
private void resetState() {
for (int button = 0; button < BUTTON_COUNT; button++) {
mButtonState[button] = false;
}
for (int joystick = 0; joystick < JOYSTICK_COUNT; joystick++) {
for (int axis = 0; axis < AXIS_COUNT; axis++) {
mJoystickPositions[joystick][axis] = 0.0f;
}
}
}
随着数据项的创建和初始化,您将需要一种方法来更改它们的值。当应用接收到一个KeyEvent(用于按钮按压)或MotionEvent(用于模拟输入)时,它将被转发到这个工具类。然后,您可以确定使用了什么控件来保存其状态。对于一个按钮,您将确定该按钮当前是否被按住,然后将该信息保存在按钮状态数组中。
public void handleKeyEvent(KeyEvent keyEvent) {
boolean keyIsDown = keyEvent.getAction() == KeyEvent.ACTION_DOWN;
if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_BUTTON_A) {
mButtonState[BUTTON_A] = keyIsDown;
} else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_BUTTON_B) {
mButtonState[BUTTON_B] = keyIsDown;
} else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_BUTTON_X) {
mButtonState[BUTTON_X] = keyIsDown;
} else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_BUTTON_Y) {
mButtonState[BUTTON_Y] = keyIsDown;
} else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_BUTTON_R1 ) {
mButtonState[BUTTON_R1] = keyIsDown;
} else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_BUTTON_R2 ) {
mButtonState[BUTTON_R2] = keyIsDown;
} else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_BUTTON_L1 ) {
mButtonState[BUTTON_L1] = keyIsDown;
} else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_BUTTON_L2 ) {
mButtonState[BUTTON_L2] = keyIsDown;
}
}
类似地,当移动操纵杆时,您将保存它的 X 和 Y 轴位置。
public void handleMotionEvent(MotionEvent motionEvent) {
mJoystickPositions[JOYSTICK_1][AXIS_X] =
motionEvent.getAxisValue( MotionEvent.AXIS_X );
mJoystickPositions[JOYSTICK_1][AXIS_Y] =
motionEvent.getAxisValue( MotionEvent.AXIS_Y );
mJoystickPositions[JOYSTICK_2][AXIS_X] =
motionEvent.getAxisValue( MotionEvent.AXIS_Z );
mJoystickPositions[JOYSTICK_2][AXIS_Y] =
motionEvent.getAxisValue( MotionEvent.AXIS_RZ );
}
既然您的工具类可以存储游戏手柄控制器的状态信息,您将需要提供一种在您的应用中访问该信息的方法。您可以通过创建两个 getter 方法来实现这一点,这两个方法返回您在控制器上跟踪的任何按钮或操纵杆的状态。
public float getJoystickPosition(int joystickIndex, int axis) {
return mJoystickPositions[joystickIndex][axis];
}
public boolean isButtonDown(int buttonId) {
return mButtonState[buttonId];
}
当你的GameController工具类完成时,你将需要能够从物理游戏手柄控制器向它提供数据。检索该输入的方式因应用而异。如果您使用视图来侦听控制器事件,那么您将需要覆盖以下方法:
onGenericMotionEvent( MotionEvent event )
onKeyDown( int keyCode, KeyEvent event )
onKeyUp( int keyCode, KeyEvent event )
如果您使用一个活动来接收输入事件,那么您只需要重写两个方法:
dispatchGenericMotionEvent( MotionEvent event )
dispatchKeyEvent( KeyEvent event )
对于这个示例,您将使用MainActivity从游戏手柄控制器中检索事件,因此只需要覆盖上面提到的两个方法。在您开始从控制器接收输入之前,您需要在您的类的顶部创建一个新的GameController utility 对象实例。
private final GameController mController = new GameController();
要从控制器上的操纵杆或触发器检索模拟输入,您将使用dispatchGenericMotionEvent。在这个方法中,您将把MotionEvent传递给mController,然后您将通过检查操纵杆的位置来更新代表操纵杆值的视图。
@Override
public boolean dispatchGenericMotionEvent(MotionEvent ev) {
mController.handleMotionEvent(ev);
//R2 and L2 (triggers) are also analog and use this callback
mJoystickState1AxisX.setText(String.valueOf(
mController.getJoystickPosition(GameController.JOYSTICK_1,
GameController.AXIS_X)));
mJoystickState1AxisY.setText(String.valueOf(
mController.getJoystickPosition(GameController.JOYSTICK_1,
GameController.AXIS_Y)));
mJoystickState2AxisX.setText(String.valueOf(
mController.getJoystickPosition(GameController.JOYSTICK_2,
GameController.AXIS_X)));
mJoystickState2AxisY.setText(String.valueOf(
mController.getJoystickPosition(GameController.JOYSTICK_2,
GameController.AXIS_Y)));
return true;
}
无论何时按下或释放一个键,都会调用dispatchKeyEvent。在这个方法中,您将把KeyEvent传递给mController,然后更新适当的TextView来反映事件。你会注意到这个例子中 back 键有一个特例。当按钮被按下时,用户对它应该如何操作有一个预期,所以你应该让系统来处理它,而不是创建你自己的响应。
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if( event.getKeyCode() == KeyEvent.KEYCODE_BACK ) {
return super.dispatchKeyEvent(event);
}
mController.handleKeyEvent(event);
updateTextViewForButton( mButtonStateA,
mController.isButtonDown( GameController.BUTTON_A ) );
updateTextViewForButton( mButtonStateB,
mController.isButtonDown( GameController.BUTTON_B ) );
updateTextViewForButton( mButtonStateX,
mController.isButtonDown( GameController.BUTTON_X ) );
updateTextViewForButton( mButtonStateY,
mController.isButtonDown( GameController.BUTTON_Y ) );
updateTextViewForButton( mButtonStateR1,
mController.isButtonDown( GameController.BUTTON_R1 ) );
updateTextViewForButton( mButtonStateL1,
mController.isButtonDown( GameController.BUTTON_L1 ) );
return true;
}
private void updateTextViewForButton( TextView textView, boolean pressed ) {
if( pressed ) {
textView.setText( getString( R.string.state_pressed ) );
} else {
textView.setText( getString( R.string.state_not_pressed ) );
}
}
现在你应该能够在一个实际的 Android 电视设备上运行这个示例应用,并看到你的应用如何响应来自游戏手柄控制器的输入,如图 5-2 所示。

图 5-2。
Gamepad control input demonstration
控制器最佳实践
在本节的示例应用中,您已经学习了如何使用 Android TV 游戏手柄的一些功能。虽然了解如何使用控制器很重要,但您也应该遵循这些最佳实践,以确保您的用户在使用您的应用时拥有出色的体验。谷歌提供了一些要点来帮助你充分利用你的应用:
- 如果需要控制器,尽快让你的用户知道。这在谷歌 Play 商店的描述中做得最好。如果用户没有意识到他们需要一个控制器,并因此在使用你的应用时遇到困难,他们可能会给你的应用一个糟糕的评级。
- 用户希望某些按钮执行某些操作,比如 A 按钮触发接受操作,B 按钮取消操作。你越接近这些期望,你的用户就会越高兴。
- 验证控制器硬件要求。如果您的应用使用控制器陀螺仪或触发器来执行操作,但用户拥有的控制器缺乏所需的硬件,那么您的应用将不会像预期的那样工作。确保有一个备份计划来支持这些用户。
- 虽然这看起来很明显,但你需要确保你的应用能处理多人游戏的多个控制器。虽然本节的示例应用没有涉及这一点,但是您应该能够检测应用接收到的每个输入事件的设备 id,并做出相应的响应。
- 当游戏控制器由于任何原因在游戏过程中断开连接时,包括蓝牙掉线或控制器没电,您应该暂停游戏并通知用户他们已经断开连接。您还可以提供一个对话框来帮助他们解决问题。
- 如果可能,显示使用控制器的直观说明。谷歌在其文档页面上提供了一个 Android TV 游戏手柄模板,可用于此目的。
Note
在 http://developer.android.com/training/tv/games/index.html 的“构建电视游戏”文章中的“显示控制器说明”下提供了 Android 电视游戏手柄模板的下载链接。
使用局域网
正如本章前面提到的,一些游戏需要玩家之间一定程度的保密。促进这一点的最佳方式之一是使用手机或平板电脑作为第二屏幕,允许每个玩家秘密计划他或她的行动。为了帮助创建这种体验,谷歌创建了 Nearby Connections API,允许同一局域网(LAN)上的设备轻松通信。在 Android 电视游戏的环境中,电视将充当中央主机,而每个第二屏幕设备将充当客户端。
设置第二个屏幕项目
为了让第二屏幕体验发挥作用,您需要两个应用:一个用于电视,一个用于移动设备。当您为此示例创建项目时,您将为这两种设备类型创建一个模块,如图 5-3 所示。

图 5-3。
Android Studio module creation screen
移动模块可以使用默认设置的空活动选项,当您构建电视模块时,您可以选择不添加活动选项。一旦两个模块都创建好了,你需要为电视模块创建一个新的MainActivity.java文件,并将其添加到模块的AndroidManifest.xml中,类似于你在第三章中所做的。
当您创建了初始项目,并且可以在移动设备上安装移动模块,在 Android 电视上安装电视模块时,您可以继续下一步。因为附近的连接 API 是 Play Services 的一部分,所以您需要在dependencies节点下的每个模块的build.gradle文件中包含 Play Services 库。
compile 'com.google.android.gms:play-services:8.3.0'
在两个模块的AndroidManifest.xml文件中,您需要请求ACCESS_NETWORK_STATE和WAKE_LOCK权限。
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
接下来,您需要在应用节点中声明一段元数据,该元数据将定义服务 ID,附近的连接 API 将使用该服务 ID 通过 LAN 连接设备。
<meta-data android:name="com.google.android.gms.nearby.connection.SERVICE_ID"
android:value="@string/service_id" />
这里,service_id是两个模块的字符串,以便每个应用可以在网络上识别另一个。现在您可以将该字符串添加到strings.xml中。
<string name="service_id">Apress Service Id</string>
一旦您完成了对AndroidManifest.xml和strings.xml文件的操作,您就可以为每个模块关闭这两个文件。在MainActivity.java内部,你需要为两个模块创建一个新的GoogleApiClient并连接到它。首先在您的活动中实现ConnectionCallbacks和OnConnectionFailedListener接口。
public class MainActivity extends Activity implements
GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
@Override
public void onConnected(Bundle bundle) {
}
@Override
public void onConnectionSuspended(int i) {
}
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
}
}
接下来,在类的顶部定义你的GoogleApiClient,并在onCreate中初始化它。一旦您的GoogleApiClient设置好,您就可以在onStart中连接到它。在onStop中,您还会想要断开与GoogleApiClient的连接。
private GoogleApiClient mGoogleApiClient;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mGoogleApiClient = new GoogleApiClient.Builder( this )
.addConnectionCallbacks( this )
.addOnConnectionFailedListener( this )
.addApi( Nearby.CONNECTIONS_API )
.build();
}
@Override
protected void onStart() {
super.onStart();
mGoogleApiClient.connect();
}
@Override
protected void onStop() {
super.onStop();
if( mGoogleApiClient != null && mGoogleApiClient.isConnected() ) {
disconnect();
mGoogleApiClient.disconnect();
}
}
在onStop中,您会注意到有一个对名为disconnect的方法的额外调用。现在,在两个MainActivity文件中为该方法创建一个存根。
这两个应用使用的最后一段代码是一个助手方法,用于确定应用是通过无线电缆还是以太网电缆连接到 LAN。首先,您需要定义一个表示两种网络类型的整数数组。
private static int[] NETWORK_TYPES = {
ConnectivityManager.TYPE_WIFI,
ConnectivityManager.TYPE_ETHERNET
};
在您创建了NETWORK_TYPES之后,您可以添加一个名为isConnectedToNetwork的新方法来检查设备是否连接到任何一种网络类型。
private boolean isConnectedToNetwork() {
ConnectivityManager connectivityManager = (ConnectivityManager)
getSystemService( Context.CONNECTIVITY_SERVICE );
for( int networkType : NETWORK_TYPES ) {
NetworkInfo info = connectivityManager.getNetworkInfo(networkType);
if( info != null && info.isConnectedOrConnecting() ) {
return true;
}
}
return false;
}
此时,您的电视和移动模块应该看起来几乎相同。在下一节中,您将学习如何修改电视模块,使其能够通过局域网进行广告并正确响应连接请求。
局域网上的广告
当您为用户创建第二个屏幕体验时,您将使用电视应用作为传入连接的主机。为了让这些传入的连接(称为客户端或对等体)找到主机,它需要在网络上通告其可用性。主机还需要能够响应连接请求并通过局域网发送消息。在电视模块的MainActivity.java中,用存根实现ConnectionRequestListener和MessageListener所需的方法。
public class MainActivity extends Activity implements
GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener,
Connections.ConnectionRequestListener,
Connections.MessageListener
接下来,您需要在类的顶部添加两个新值:一个常量 long 值,用于定义应用应该广告的时间长度;一个 string 对象列表,用于存储连接到主机的每个对等体的 ID。
private static final long CONNECTION_TIME_OUT = 60000L;
private List<String> mRemotePeerEndpoints = new ArrayList<String>();
接下来你需要开始在网络上做广告。对于这个例子,你将在GoogleApiClient完成连接后立即开始广告,尽管在真正的应用中,直到你准备好将客户端连接到主机时才开始广告。
@Override
public void onConnected(Bundle bundle) {
advertise();
}
advertise方法将确保设备连接到网络,然后使用附近的连接 API 开始广告。这个 API 提供了一个回调函数,可以让您检查广告的结果,以便您的应用可以根据广告的成功与否做出适当的反应。
private void advertise() {
if( !isConnectedToNetwork() )
return;
String name = "Nearby Advertising";
Nearby.Connections.startAdvertising(mGoogleApiClient, name, null, CONNECTION_TIME_OUT, this).setResultCallback(
new ResultCallback<Connections.StartAdvertisingResult>() {
@Override
public void onResult(Connections.StartAdvertisingResult result) {
if (result.getStatus().isSuccess()) {
Log.v( "Apress", "Successfully advertising" );
}
}
});
}
现在,您的电视应用能够做广告了,它需要响应来自移动设备的连接请求。在onConnectionRequest方法中,您可以根据作为参数传递的信息来决定是否要连接到一个设备。对于这个示例,您只需连接到对您的服务 ID 广告做出响应的任何东西。连接后,您将把远程设备 ID 保存在一个列表中,这样您就可以在整个连接过程中与该设备通信。
@Override
public void onConnectionRequest(final String remoteEndpointId,
final String remoteDeviceId,
final String remoteEndpointName,
byte[] payload) {
Nearby.Connections.acceptConnectionRequest( mGoogleApiClient,
remoteEndpointId, payload, this ).setResultCallback(
new ResultCallback<Status>() {
@Override
public void onResult(Status status) {
if( status.isSuccess() ) {
getWindow().addFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
if( !mRemotePeerEndpoints.contains( remoteEndpointId ) ) {
mRemotePeerEndpoints.add( remoteEndpointId );
}
} else {
Log.e( "Apress", "onConnectionRequest failed: " +
status.getStatusMessage() );
}
}
});
}
您会注意到,当客户端成功连接后,设备会请求保持屏幕处于唤醒状态。这是因为从客户端接收消息不会被算作输入事件,因此这种方法可以防止电视在用户玩游戏时进入睡眠状态。将客户端连接到主机后,您将能够在设备之间来回接收和发送消息。每当客户端向主机发送消息负载时,就会调用onMessageReceived方法。对于这个示例,您只需将有效负载放入 Toast 消息中,然后将其回显给所有客户端。
@Override
public void onMessageReceived(String s, byte[] bytes, boolean b) {
Toast.makeText(this, new String(bytes), Toast.LENGTH_SHORT).show();
Nearby.Connections.sendReliableMessage( mGoogleApiClient,
mRemotePeerEndpoints, bytes );
}
当活动调用onStop时,您需要对宿主应用做的最后一件事是正确地断开连接。为此,您需要扩展您之前创建的disconnect存根方法,以便它停止应用的广告并切断所有对等连接。
private void disconnect() {
Nearby.Connections.stopAdvertising(mGoogleApiClient);
Nearby.Connections.stopAllEndpoints(mGoogleApiClient);
mRemotePeerEndpoints.clear();
}
如果你现在运行你的应用,你只会看到一个空白的屏幕,因为这个应用只自己做广告,但需要客户端来响应他们的连接请求或消息。
通过局域网发现
虽然设置主机应用很好,但您仍然需要创建一个客户端应用,以便能够在第二个屏幕和电视之间进行通信。您可以从打开移动模块中的MainActivity.java文件开始,并添加客户端所需的两个附加接口— MessageListener和EndpointDiscoveryListener。一旦为这两个接口创建了所需的方法,您将需要一些新的成员对象。第一个是一个字符串,它将存储您已经连接到的主机端点的 ID。其他对象是一个处理程序和 runnable,它将定期通过附近的连接 API 向主机设备发送消息。
private String mRemoteHostEndpoint;
private Handler mHandler = new Handler();
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
if ( !TextUtils.isEmpty( mRemoteHostEndpoint ) ) {
Nearby.Connections.sendReliableMessage(mGoogleApiClient,
mRemoteHostEndpoint, "Hello World".getBytes() );
mHandler.postDelayed(this, 5000);
}
}
};
虽然这个代码片段使用了sendReliableMessage方法,但是您也可以发送不保证能够送达的消息。就像软件开发中的任何事情一样,总有一个权衡。发送不可靠的消息开销较低,而如果可以找到接收者,可靠的消息总是会被传递。
接下来,一旦应用的 Google API 客户端完成连接,您就可以添加对名为discoverHost的新方法的调用。
@Override
public void onConnected(Bundle bundle) {
discoverHost();
}
discoverHost要做的第一件事是确保移动设备连接到本地网络。验证网络连接后,该方法将使用附近的连接 API 来尝试发现 LAN 上的广告主机。传递给startDiscovery方法的第三个参数将告诉 API 只尝试 60 秒的发现。一旦应用开始发现或失败,您将在结果回调中收到一个Status对象。
private void discoverHost() {
if( !isConnectedToNetwork() ) {
return;
}
String serviceId = getString( R.string.service_id );
Nearby.Connections.startDiscovery(mGoogleApiClient, serviceId,
60000L, this)
.setResultCallback( new ResultCallback<Status>() {
@Override
public void onResult(Status status) {
if (status.isSuccess()) {
Log.v("Apress", "Started discovering");
}
}
});
}
当发现主机时,附近的连接 API 将调用onEndpointFound方法。在这个方法中,您将向主机发送一个带有新回调的连接请求。如果主机接受您的连接请求,将会通知您,以便您可以停止发现。此时,您可以在适合您的应用的时候开始向主机发送消息。为了简单起见,这个示例应用将使用您在本节前面定义的Handler和Runnable每五秒钟向主机发送一次文本“Hello World”。
@Override
public void onEndpointFound(String endpointId, String deviceId,
final String serviceId, String endpointName) {
byte[] payload = null;
Nearby.Connections.sendConnectionRequest( mGoogleApiClient, deviceId,
endpointId, payload,
new Connections.ConnectionResponseCallback() {
@Override
public void onConnectionResponse(String s, Status status,
byte[] bytes) {
if( status.isSuccess() ) {
getWindow().addFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
Nearby.Connections.stopDiscovery(mGoogleApiClient,
serviceId);
mRemoteHostEndpoint = s;
mHandler.post( mRunnable );
} else {
Log.e( "Apress", "Connection to endpoint failed" );
}
}
}, this );
}
与主机应用一样,您已经在客户端应用中实现了MessageListener。当从另一个设备接收到消息时,onMessageReceived将被调用。在这个示例应用中,您将获取字节数组有效负载并将其转换为字符串,然后将其显示为 Toast 消息。
@Override
public void onMessageReceived(String s, byte[] bytes, boolean b) {
Toast.makeText( this, new String( bytes ), Toast.LENGTH_SHORT ).show();
}
在客户端应用中,您需要做的最后一件事是处理与主机的断开连接。在您的onStop方法中,您应该有一个对disconnect的调用。此方法将验证您的设备是否已连接到 LAN,然后停止尝试发现主机设备或断开任何已连接的主机设备。
private void disconnect() {
if( !isConnectedToNetwork() )
return;
if( TextUtils.isEmpty(mRemoteHostEndpoint) ) {
Nearby.Connections.stopDiscovery( mGoogleApiClient,
getString( R.string.service_id ) );
} else {
Nearby.Connections.disconnectFromEndpoint( mGoogleApiClient,
mRemoteHostEndpoint );
mRemoteHostEndpoint = null;
}
}
现在您已经创建了主机和客户端应用,您应该将电视模块安装到 Android 电视设备上,将移动模块安装到平板电脑或手机上。如果两台设备都连接到同一个 LAN,它们应该会自动找到并连接到彼此。一旦连接建立,数据将开始在两个设备之间定期发送,并显示在 Toast 消息中,如图 5-4 所示。

图 5-4。
Toast message on the Android TV from a received message payload
在本节中,您已经了解了 Nearby Connections API 以及如何创建主机和客户端应用。这将让您轻松地为您的应用设置第二屏幕体验,以便用户可以使用移动设备来计划他们的行动,并将信息发送到电视。在下一节中,将向您介绍 Google Play 游戏服务中的一些新 API。
Google Play 游戏服务
您很可能知道,Google 已经在 Google Play 服务库中为 Android 开发提供了一套优秀的 API 和工具。随之而来的是 Google Play 游戏服务,这是一组专注于帮助游戏开发者轻松创建用户喜欢的应用的类。虽然深入研究 Google Play 游戏服务以及如何使用它远远超出了本书的范围,但它本身值得一书,你至少应该知道它提供的一些功能,以便你可以增强你的 Android 电视游戏。
成就
成就是 Android 游戏的核心部分,也是奖励玩家享受你的游戏的一种简单方式。他们也可以用来刺激玩家之间的友好竞争,因为他们试图获得比他们的朋友更多的成就。当你在游戏中添加成就时,你有几个不同的选择,因为它们要么可以因为执行一个动作而立即获得奖励,要么迭代地让玩家必须多次完成一个特定的任务。成就也可以被隐藏,使得用户在被赢得之前不知道它们的存在。一个游戏至少需要五项成就才能发布。
排行榜
排行榜提供了一个很好的方式来显示玩家与其他玩家相比在游戏中的表现。对于你的铁杆玩家来说,这为他们提供了一个争夺第一的机会,而你的休闲玩家可以和他们的朋友比较他们的表现。如果你的游戏中有不同的地图(比如在一个策略游戏中有不同的地图),你可以使用多个排行榜,这样玩家就可以看到他们在不同的地图之间的比较。
保存的游戏
对于拥有多台设备的游戏玩家来说,最大的麻烦之一是为了完成游戏而被锁定到特定的设备。如果你有一个游戏既可以在电视上运行,也可以在手持设备上运行,你应该实现保存的游戏 API。这将允许您拍摄数据快照并在线保存,以便玩家可以在不同平台之间切换或升级到新设备时保持进度。
多光盘播放器
虽然你在本章前面已经学习了附近的连接 API,但是它只支持本地多人游戏。使用 Google Play 游戏服务,您可以支持实时和回合制游戏的在线多人游戏。Google 自动管理连接,提供玩家选择用户界面,并在游戏过程中存储玩家和房间的状态信息。
任务和事件
事件可以由游戏中玩家的动作触发,并发送到谷歌的游戏服务器供您分析。这有助于确定游戏的哪些区域太容易或太难,这样你就可以为你的用户调整它们。任务服务使用事件功能,这样你就可以通过限时挑战来吸引玩家。当玩家完成这些挑战,你就可以奖励他们。quest 系统最大的优势之一就是,只要你在从游戏中收集事件,你就可以为你的用户发布新的任务,而不必将你的应用的更新版本推送到 Play Store。
摘要
在本章中,你学习了一些可以用来创建游戏或将游戏移植到 Android TV 平台的工具。您了解了控制器以及如何从控制器读取输入,如何通过本地网络提供第二个屏幕体验,以及 Google Play 游戏服务的一些功能,这些功能可以丰富玩家的游戏体验。虽然游戏开发超出了本书的范围,但是您应该非常了解如何让您的游戏与 Android TV 一起工作。
六、安卓电视应用发布
在你构建了一个应用之后,你总是想要做一个最后的运行,以确保你的应用如预期的那样工作。对于 Android TV,这一点尤为重要,因为每个 Android TV 应用在谷歌 Play 商店上可用之前都要经过批准。在这一章中,你将了解谷歌在评估你的应用时会寻找的项目,以及一些将你的应用分发给用户的一般提示。
安卓电视应用清单
值得注意的是,批准过程不是为了审查,而是为了确保你的应用布局和控制对于 Android TV 用户来说能够正确工作。在您尝试将您的 APK 上传到 Play Store 之前,您应该验证您的应用是否符合 Google 的指导原则。
Support the Android TV OS
为了让用户从 Android TV 主屏幕访问您的应用,您需要确保通过在清单的 activity 节点中声明一个CATEGORY_LEANBACK_LAUNCHER过滤器来提供一个 Android TV 进入应用的入口点。如果不可用,那么您的应用将不会出现在主屏幕上的任何一个应用行中。
当您为 Android TV 声明一个活动时,您需要将一个横幅图标与其关联,该图标将显示在应用行中。启动横幅需要 320 像素乘以 180 像素,图像上的任何文本都需要针对您的应用支持的每种语言进行本地化。
如果您将一个应用从严格意义上的移动设备移植到 Android TV,那么您需要确保您的清单没有声明任何 Android TV 平台不支持的必需硬件。这包括摄像头、触摸屏和各种硬件传感器。如果这些项目中的任何一项被声明为必需,您的应用将无法被 Android TV 设备发现。
UI Design
在 Android Backstage 播客的一集中,前 Android TV 团队工程师蒂姆·基尔伯恩(Tim Kilbourn)提到了一款为谷歌电视平台发布的应用,该应用没有验证它是否如预期那样工作。这款应用没有以像样的方式显示,而是被锁定为纵向模式,并在电视上展开。像这样的经历就是为什么 UI 验证是 Android TV 审批流程的重要组成部分。不用说,你应该确保你的应用提供横向布局资源。
因为大多数用户将在平均 10 英尺远的地方体验他们的电视,所以您需要确保所有的文本和控件都足够大以至于可见,并且所有的位图和图标都是高分辨率的。由于电视的一些独特条件,你还需要确保你的布局处理过扫描和你的应用的配色方案工作良好。这些主题在第二章中有更详细的讨论。
如果您的应用使用广告,建议您使用 30 秒内全屏且可忽略的视频广告。值得注意的是,不应该使用依赖于向网页发送意图的广告,因为 Android TV 没有内置的网络浏览器。如果你启动一个网页的意图,如果用户没有安装他们自己的浏览器,你的应用将会崩溃。
您还必须确保您的应用正确响应 D-pad 或游戏控制器,以便您的用户可以浏览您的应用。这由向后倾斜支持库中的类来处理,但是您需要确保您自己的自定义类也相应地做出响应。
Searching and Discovery
虽然拥有一个播放内容的应用是一回事,但是通过帮助用户发现内容或提供推荐,您可以将它提升到一个全新的水平。这些项目在第四章中有详细介绍。简而言之,您应该确保全局搜索和推荐对您的应用有效,并且当用户找到他们感兴趣的内容时,应该直接将他们带到内容。
Games
在上一章中,你已经了解了 Android 电视游戏开发的一些关键点。当您为 Android TV 创建一个游戏时,您需要在清单中将它声明为一个游戏,以便它显示在主屏幕的游戏行中。
如果您支持在应用中使用游戏控制器,您的清单也应该更新。如果你支持使用游戏控制器,你需要确保你的应用有按钮应急使用开始,选择和菜单按钮,因为不是所有的控制都包括这些。你需要提供一个通用的游戏手柄控制器图形来告诉你的用户这些控件会如何影响你的游戏。您需要确保您的应用提供了方便退出应用的控件,以便用户可以返回到主屏幕。
虽然网络对 Android 来说不是一个新概念,但 Android TV 是首批支持以太网连接的设备之一。因此,您需要确保您拥有的任何网络代码都能够验证该设备是通过 WiFi 还是以太网电缆连接到网络。
分发您的应用
一旦你完成了你的应用,你检查了你的项目,以确保一切看起来都很棒,你需要让它可供用户下载。这里有两个主要渠道,谷歌 Play 商店和亚马逊应用商店。请注意,在用户可以访问您的应用之前,两家商店都有类似的审批流程,这一点很重要。
Google Play Store Distribution
与大多数涉及安卓电视的事情一样,谷歌的应用发布过程与使用标准手机或平板电脑应用非常相似。您需要创建一个 APK,并用发布认证对其进行签名,然后将其上传到 Google Play 开发人员控制台。但是,当你开始填写商店列表信息时,你需要进入 Android TV 部分,并提供 Play 商店可以使用的素材,如图 6-1 所示。

图 6-1。
Android TV Google Play Store listing assets
除了必须提供素材之外,Play Store 将自动知道您是否正在发布 Android TV 应用,因为在您的清单文件中声明了一个向后倾斜的启动器。
Amazon Fire TV Distribution
从 Fire OS 5 开始,你可以在亚马逊应用商店为 Fire TVs 分发使用 Leanback 支持库和 Lollipop 功能制作的 Android 应用。虽然让您的应用与 Amazon Fire OS 兼容超出了本书的范围,但您可以在 developer.amazon.com 上找到详细的文档,其中介绍了如何安装和设置 Amazon SDK 平台工具,以及如何在您的应用中使用 Amazon 的特定 SDK 和工具。这将允许你在不做太多修改的情况下将你的应用分发给更多的用户。
摘要
在这一章中,你回顾了贯穿全书的设计和体验指南。您还了解了为电视用户分发应用的一些方法。这本书讲述了如何从头开始创建一个应用来显示内容,并帮助您的用户享受媒体,以及一些应该可以帮助您为 Android TV 开发游戏的工具。当你继续学习 Android TV 开发时,你应该仔细阅读谷歌的开发者文档,在线观看谷歌开发者视频,并试验你的应用,找出最适合你和你的用户的方法。祝你好运,玩得开心!



浙公网安备 33010602011771号