6、详情页设计:DetailsFragment的使用、动作按钮与元数据展示
详情页,说白了就是用户点进一部电影或一个节目后看到的那个页面。在Android TV上,这个页面通常由Leanback库中的DetailsFragment来承载。我最早接触它的时候,觉得这东西有点重,但用熟了才发现——它把很多电视端特有的交互细节都帮你处理好了。
为什么用DetailsFragment?
你想想看,手机上的详情页可以随便滑动、点击、缩放。但电视上不一样,用户手里只有遥控器,焦点移动是核心交互。DetailsFragment天然支持:
- 左侧海报区 + 右侧元数据区的标准布局
- 动作按钮行(播放、收藏、分享等)
- 上下左右焦点自动管理
- 与BrowseFragment风格统一
我在项目中遇到过直接用普通Fragment硬撸详情页的情况,结果焦点跳转、按键处理写了一堆bug。后来全换成DetailsFragment,省了一半代码量。
核心结构:DetailsFragment + DetailsPresenter
DetailsFragment本身不直接渲染UI,它依赖一个Presenter来生成视图。这个模式跟BrowseFragment如出一辙。我们来看一个典型的实现:
class MovieDetailsFragment : DetailsFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val movie = arguments?.getParcelable<Movie>("movie")
val adapter = ArrayObjectAdapter(MovieDetailsPresenter())
adapter.add(movie)
// 设置详情页的适配器
setAdapter(adapter)
}
}
嗯,这里要注意:setAdapter只能调用一次,而且传入的Adapter必须包含且仅包含一个对象。DetailsFragment的设计理念就是——一次只展示一个详情项。
Presenter里画什么?
Presenter负责生成详情页的视图。通常包含三块内容:
- 海报/背景图:占据左侧或全屏背景
- 元数据区:标题、评分、年份、时长、简介
- 动作按钮行:播放、加入收藏、分享等
我习惯用FullWidthDetailsOverviewRowPresenter来做这个,它自带了一个标准布局模板:
class MovieDetailsPresenter : FullWidthDetailsOverviewRowPresenter(
MovieDetailsDescriptionPresenter()
) {
override fun onCreateRowView(): ViewGroup {
val root = super.onCreateRowView() as ViewGroup
// 可以在这里自定义背景、添加额外元素
return root
}
}
但说实话,这个模板有时候不够灵活。如果你需要完全自定义布局,可以直接继承DetailsOverviewRowPresenter,自己写XML。
元数据展示:DescriptionPresenter
元数据那块,Leanback提供了一个AbstractDetailsDescriptionPresenter。你只需要重写onBindDescription方法:
class MovieDetailsDescriptionPresenter : AbstractDetailsDescriptionPresenter() {
override fun onBindDescription(
viewHolder: ViewHolder,
item: Any
) {
val movie = item as Movie
viewHolder.title.text = movie.title
viewHolder.subtitle.text = "${movie.year} · ${movie.duration}分钟"
viewHolder.body.text = movie.description
}
}
这里有个坑——subtitle默认只显示一行,如果内容长了会被截断。我曾经因为这个被测试提了bug,后来在XML里加了android:maxLines="2"才搞定。
动作按钮:ActionsRow
动作按钮是详情页的灵魂。用户拿着遥控器,最想干的就是点「播放」。DetailsFragment通过ActionsRow来管理这些按钮:
val actionsAdapter = ArrayObjectAdapter()
actionsAdapter.add(Action(PLAY_ACTION_ID, "播放"))
actionsAdapter.add(Action(FAV_ACTION_ID, "收藏"))
actionsAdapter.add(Action(SHARE_ACTION_ID, "分享"))
// 绑定到详情行
val detailsRow = DetailsOverviewRow(movie)
detailsRow.actionsAdapter = actionsAdapter
按钮的点击事件在Fragment中处理:
override fun onActionClicked(action: Action) {
when (action.id) {
PLAY_ACTION_ID -> startPlayback(movie)
FAV_ACTION_ID -> toggleFavorite(movie)
SHARE_ACTION_ID -> shareMovie(movie)
}
}
我个人建议动作按钮不要超过5个。遥控器左右切换太多按钮,用户会烦躁。3个是黄金数量。
避坑指南:焦点与背景
我曾经在详情页上踩过一个坑——背景图加载太慢,导致用户按遥控器时焦点还没渲染出来,界面卡死。后来我做了两件事:
- 背景图用低分辨率占位图先顶上
- 在
onBindDescription里异步加载高清图
另外,动作按钮的焦点样式一定要明显。电视上不像手机可以触摸,用户全靠焦点反馈来判断「我选中了哪个」。我一般会在按钮的focused状态加一个缩放动画:
<scale
android:fromXScale="1.0"
android:toXScale="1.1"
android:fromYScale="1.0"
android:toYScale="1.1"
android:pivotX="50%"
android:pivotY="50%"
android:duration="150" />
知识体系总览
下面这张图帮你理清DetailsFragment的核心组件关系:
完整示例:一个可用的详情页
把上面这些拼起来,就是一个完整的详情页Fragment:
class MovieDetailsFragment : DetailsFragment() {
private lateinit var movie: Movie
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
movie = arguments?.getParcelable("movie") ?: return
val presenter = FullWidthDetailsOverviewRowPresenter(
MovieDetailsDescriptionPresenter()
).apply {
// 设置动作按钮对齐方式
actionsRowStyle = DetailsOverviewRow.STYLE_ACTIONS_ROW_END
}
val adapter = ArrayObjectAdapter(presenter)
val row = DetailsOverviewRow(movie).apply {
actionsAdapter = ArrayObjectAdapter().apply {
add(Action(1, "播放"))
add(Action(2, "收藏"))
}
}
adapter.add(row)
setAdapter(adapter)
// 监听动作点击
onItemViewClickedListener = ItemViewClickedListener()
onItemViewSelectedListener = ItemViewSelectedListener()
}
private inner class ItemViewClickedListener : OnItemViewClickedListener {
override fun onItemClicked(
itemViewHolder: Presenter.ViewHolder?,
item: Any?,
rowViewHolder: RowPresenter.ViewHolder?,
row: Row?
) {
if (item is Action) {
when (item.id) {
1L -> startPlayback(movie)
2L -> toggleFavorite(movie)
}
}
}
}
}
onCreateRowView中拿到根布局,然后设置背景。记得用Glide或Coil异步加载,别在主线程干这事。
DPAD_CENTER和ENTER键来触发点击事件。如果你自定义了按键逻辑,记得调用super方法,否则动作按钮可能点不动。
元数据展示的细节
元数据不只是标题加简介。电视屏幕大,信息密度可以更高。我一般会展示:
| 字段 | 展示方式 | 说明 |
|---|---|---|
| 标题 | 大号加粗字体 | 主标题,建议28sp以上 |
| 副标题 | 年份 · 时长 · 分级 | 用圆点分隔,灰色字体 |
| 评分 | 星级或数字 | 我习惯用数字+星标组合 |
| 简介 | 多行文本 | 最多显示4行,超出用省略号 |
| 标签 | 圆角标签行 | 动作、喜剧、科幻等 |
这里有个经验——电视上文字不要太小。我见过很多应用把手机端的14sp直接搬过来,结果在55寸电视上根本看不清。详情页的正文至少20sp起步。
好了,DetailsFragment的核心用法就这些。说白了它就是帮你搭好了详情页的骨架,你只需要往里面填肉——元数据、动作按钮、背景图。下次做TV端详情页,别自己从头写了,直接用这个方案,省心不少。