30、项目实战:从零构建一个完整的TV视频点播应用
终于到了最后一章。说实话,写到这里我还有点舍不得。
前面29章,我们把Android TV开发的边边角角都捋了一遍。从焦点控制到Leanback组件,从大屏适配到性能优化。但说实话,光看知识点是不够的。你得亲手搭一个完整的项目,才能真正理解这些技术是怎么串起来的。
这一章,我们就从零开始,构建一个完整的TV视频点播应用。我会带着你走一遍完整的流程——从项目结构设计,到核心功能实现,再到最后的调试与发布。
本章目标:完成一个可运行的TV视频点播应用,包含首页推荐、分类浏览、视频播放、搜索功能。
30.1 项目整体架构
先说说我的习惯。每次开始一个新项目,我不会急着写代码。我会先画一张架构图,把模块之间的关系理清楚。你想想看,TV应用和手机应用最大的区别是什么?
是操作方式。手机靠触摸,TV靠遥控器。所以我们的架构设计,必须围绕「焦点驱动」这个核心来展开。
这张图说白了就是三层架构:UI层只管展示和焦点,业务层处理逻辑,数据层管数据来源。每一层各司其职,互不干扰。
30.2 项目初始化与依赖配置
新建一个Android项目,包名我用的是 com.example.tvvideoclub。嗯,这里要注意一点:TV应用的minSdkVersion建议设为21或以上,因为Leanback组件在API 21以上才完整支持。
打开 build.gradle(模块级别),添加以下依赖:
dependencies {
// Leanback 核心库
implementation 'androidx.leanback:leanback:1.2.0-alpha02'
// ExoPlayer 播放器
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.19.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1'
// 网络请求
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// 图片加载
implementation 'com.github.bumptech.glide:glide:4.15.1'
// 本地数据库
implementation 'androidx.room:room-runtime:2.5.1'
implementation 'androidx.room:room-ktx:2.5.1'
// 生命周期组件
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
}
我的经验:ExoPlayer版本不要追最新,选一个稳定版就好。我曾经追新版本踩过坑,播放HLS流时出现音画不同步,回退一个版本就解决了。
30.3 首页推荐模块
首页是用户打开应用后看到的第一个界面。TV应用的首屏,说白了就是「大图+焦点」。用户拿着遥控器,左右移动焦点,看到感兴趣的内容就点进去。
我们用Leanback的 BrowseFragment 来实现。它自带标题栏和内容区,内容区支持横向滚动的卡片列表。
class MainFragment : BrowseFragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// 设置标题
title = "视频点播"
// 设置UI风格
brandColor = ContextCompat.getColor(requireContext(), R.color.primary)
searchAffordanceColor = ContextCompat.getColor(requireContext(), R.color.accent)
// 加载数据
setupAdapter()
}
private fun setupAdapter() {
val rowsAdapter = ArrayObjectAdapter(ListRowPresenter())
// 创建推荐行
val recommendRow = createRecommendRow()
rowsAdapter.add(recommendRow)
// 创建分类行
val categories = listOf("电影", "电视剧", "综艺", "动漫")
categories.forEach { category ->
val row = createCategoryRow(category)
rowsAdapter.add(row)
}
adapter = rowsAdapter
}
private fun createRecommendRow(): ListRow {
val cardPresenter = CardPresenter()
val cardAdapter = ArrayObjectAdapter(cardPresenter)
// 添加推荐视频数据
val recommendVideos = getRecommendVideos()
recommendVideos.forEach { video ->
cardAdapter.add(video)
}
val header = HeaderItem(0, "推荐内容")
return ListRow(header, cardAdapter)
}
}
这里有个关键点:CardPresenter 负责渲染每一张卡片。TV的卡片比手机大得多,我建议图片分辨率至少是 320x240,否则在大屏上会糊。
30.4 视频播放模块
播放器是点播应用的核心。我用ExoPlayer来实现,因为它对TV的支持最好,而且支持HLS、DASH等主流流媒体协议。
先创建一个播放界面:
class PlaybackActivity : Activity() {
private lateinit var player: ExoPlayer
private lateinit var playerView: PlayerView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_playback)
playerView = findViewById(R.id.player_view)
// 初始化播放器
player = ExoPlayer.Builder(this)
.setMediaSourceFactory(DefaultMediaSourceFactory(this))
.build()
playerView.player = player
// 准备播放
val videoUrl = intent.getStringExtra("video_url") ?: return
val mediaItem = MediaItem.fromUri(videoUrl)
player.setMediaItem(mediaItem)
player.prepare()
player.playWhenReady = true
}
override fun onDestroy() {
super.onDestroy()
player.release()
}
}
注意:TV应用播放视频时,一定要处理遥控器的返回键。用户按返回键时,应该暂停播放并释放资源,而不是直接退出Activity。我曾经见过一个应用,按返回键后播放器还在后台跑,结果把用户的电视盒子卡死了。
30.5 搜索功能实现
TV上的搜索和手机不一样。手机可以打字,TV只能用遥控器。所以TV搜索的核心是「语音输入」和「键盘导航」。
Leanback提供了 SearchFragment,我们直接继承它:
class VideoSearchFragment : SearchFragment(), SearchFragment.SearchResultProvider {
private val mAdapter = ArrayObjectAdapter(CardPresenter())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 设置搜索提供者
setSearchResultProvider(this)
// 设置语音搜索提示
setBadgeDrawable(
ContextCompat.getDrawable(requireContext(), R.drawable.ic_search)!!
)
}
override fun onQueryTextChange(query: String): Boolean {
// 实时搜索
loadSearchResults(query)
return true
}
override fun onQueryTextSubmit(query: String): Boolean {
// 提交搜索
loadSearchResults(query)
return true
}
private fun loadSearchResults(query: String) {
// 调用搜索API
viewModel.searchVideos(query).observe(this) { results ->
mAdapter.clear()
results.forEach { video ->
mAdapter.add(video)
}
}
}
}
说实话,TV搜索的体验很难做好。我建议你加上「热门搜索」和「搜索历史」两个功能,这样用户不用每次都打字。
30.6 焦点与遥控器适配
TV应用最头疼的问题是什么?焦点丢失。用户按着按着,焦点不知道跑到哪里去了。
我的解决方案是:给每个可聚焦的View设置 nextFocusDown、nextFocusUp 等属性,明确指定焦点的移动路径。
<!-- 在布局中指定焦点顺序 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btn_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="播放"
android:nextFocusRight="@+id/btn_add_favorite"
android:nextFocusLeft="@+id/btn_back" />
<Button
android:id="@+id/btn_add_favorite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="收藏"
android:nextFocusLeft="@+id/btn_play"
android:nextFocusRight="@+id/btn_share" />
</LinearLayout>
避坑指南:我曾经在一个项目中,所有焦点都用代码动态设置。结果测试时发现,某些电视盒子的遥控器按键映射不一样,导致焦点乱跳。后来我改成在XML中静态指定,问题就解决了。所以我的建议是:能用XML就别用代码。
30.7 性能优化要点
TV设备的硬件性能普遍不如手机。所以性能优化是必须的。我总结了几个关键点:
| 优化项 | 具体做法 | 效果 |
|---|---|---|
| 图片加载 | 使用Glide,设置合适的图片尺寸,开启内存缓存 | 减少内存占用,避免OOM |
| 列表渲染 | 使用RecyclerView + 固定Item高度 | 减少布局测量次数 |
| 播放器 | 使用ExoPlayer的缓冲策略,设置合理的缓冲区大小 | 减少卡顿,提升播放流畅度 |
| 数据加载 | 使用Paging 3分页加载,避免一次性加载大量数据 | 减少首屏加载时间 |
你想想看,如果用户打开应用,等了5秒才看到内容,他可能直接就退出了。所以首屏加载速度是重中之重。
30.8 调试与发布
TV应用的调试比手机麻烦。你不能用模拟器,因为模拟器没有遥控器。我建议你买一台便宜的电视盒子,比如小米盒子或者当贝盒子,专门用来调试。
调试时要注意几个点:
- 焦点测试:用遥控器把所有可聚焦的View都走一遍,确保没有焦点死循环
- 分辨率测试:在不同分辨率的电视上测试,1920x1080和3840x2160都要覆盖
- 网络测试:模拟弱网环境,测试播放器的缓冲和重连机制
- 内存测试:长时间播放视频,检查内存是否泄漏
发布时,记得在AndroidManifest中声明TV应用:
<application>
<!-- 声明这是TV应用 -->
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="true" />
<!-- 启动Activity -->
<activity
android:name=".MainActivity"
android:banner="@drawable/tv_banner"
android:screenOrientation="landscape">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
</application>
最后说一句:TV开发的门槛其实不高,但坑很多。你只要把焦点控制、播放器适配、性能优化这三个点吃透了,大部分问题都能解决。好了,30章的内容到这里就结束了。希望这些实战经验能帮到你。
公众号:蓝海资料掘金营,微信deep3321