在电视上运行的媒体应用需要允许用户浏览其提供的内容, 然后开始播放内容。内容浏览体验 不仅简单直观,而且具有视觉吸引力。
本指南介绍了如何使用 androidx.leanback 库提供的类。 实现一个界面,用于浏览应用的媒体目录中的音乐或视频。
注意:此处显示的实现示例使用
BrowseSupportFragment
而不是已废弃的 BrowseFragment
类。BrowseSupportFragment
扩展了 AndroidX
Fragment
类、
有助于确保在设备和 Android 版本之间保持一致的行为。
![应用主屏幕](https://developer.android.google.cn/static/images/tv/app-browse.png?authuser=9&hl=da)
图 1. Leanback 示例应用的浏览 Fragment 中显示视频目录数据。
创建媒体浏览布局
BrowseSupportFragment
类。
可让您创建主要布局,以便使用
代码。以下示例展示了如何创建包含
BrowseSupportFragment
对象:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/main_frame" android:layout_width="match_parent" android:layout_height="match_parent"> <fragment android:name="com.example.android.tvleanback.ui.MainFragment" android:id="@+id/main_browse_fragment" android:layout_width="match_parent" android:layout_height="match_parent" /> </FrameLayout>
应用的主 Activity 用于设置此视图,如下例所示:
Kotlin
class MainActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.main) } ...
Java
public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } ...
BrowseSupportFragment
方法会使用
视频数据和界面元素,并设置图标和标题等布局参数,
是否启用类别标题。
实现 BrowseSupportFragment
的应用子类
方法还会针对界面元素上的用户操作设置事件监听器,并准备
后台管理器运行,如以下示例所示:
Kotlin
class MainFragment : BrowseSupportFragment(), LoaderManager.LoaderCallbacks<HashMap<String, List<Movie>>> { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) loadVideoData() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) prepareBackgroundManager() setupUIElements() setupEventListeners() } ... private fun prepareBackgroundManager() { backgroundManager = BackgroundManager.getInstance(activity).apply { attach(activity?.window) } defaultBackground = resources.getDrawable(R.drawable.default_background) metrics = DisplayMetrics() activity?.windowManager?.defaultDisplay?.getMetrics(metrics) } private fun setupUIElements() { badgeDrawable = resources.getDrawable(R.drawable.videos_by_google_banner) // Badge, when set, takes precedent over title title = getString(R.string.browse_title) headersState = BrowseSupportFragment.HEADERS_ENABLED isHeadersTransitionOnBackEnabled = true // Set header background color brandColor = ContextCompat.getColor(requireContext(), R.color.fastlane_background) // Set search icon color searchAffordanceColor = ContextCompat.getColor(requireContext(), R.color.search_opaque) } private fun loadVideoData() { VideoProvider.setContext(activity) videosUrl = getString(R.string.catalog_url) loaderManager.initLoader(0, null, this) } private fun setupEventListeners() { setOnSearchClickedListener { Intent(activity, SearchActivity::class.java).also { intent -> startActivity(intent) } } onItemViewClickedListener = ItemViewClickedListener() onItemViewSelectedListener = ItemViewSelectedListener() } ...
Java
public class MainFragment extends BrowseSupportFragment implements LoaderManager.LoaderCallbacks<HashMap<String, List<Movie>>> { } ... @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); loadVideoData(); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); prepareBackgroundManager(); setupUIElements(); setupEventListeners(); } ... private void prepareBackgroundManager() { backgroundManager = BackgroundManager.getInstance(getActivity()); backgroundManager.attach(getActivity().getWindow()); defaultBackground = getResources() .getDrawable(R.drawable.default_background); metrics = new DisplayMetrics(); getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics); } private void setupUIElements() { setBadgeDrawable(getActivity().getResources() .getDrawable(R.drawable.videos_by_google_banner)); // Badge, when set, takes precedent over title setTitle(getString(R.string.browse_title)); setHeadersState(HEADERS_ENABLED); setHeadersTransitionOnBackEnabled(true); // Set header background color setBrandColor(ContextCompat.getColor(requireContext(), R.color.fastlane_background)); // Set search icon color setSearchAffordanceColor(ContextCompat.getColor(requireContext(), R.color.search_opaque)); } private void loadVideoData() { VideoProvider.setContext(getActivity()); videosUrl = getString(R.string.catalog_url); getLoaderManager().initLoader(0, null, this); } private void setupEventListeners() { setOnSearchClickedListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(getActivity(), SearchActivity.class); startActivity(intent); } }); setOnItemViewClickedListener(new ItemViewClickedListener()); setOnItemViewSelectedListener(new ItemViewSelectedListener()); } ...
设置界面元素
在前面的示例中,私有方法 setupUIElements()
调用了
BrowseSupportFragment
设置媒体目录浏览器样式的方法:
setBadgeDrawable()
将指定的可绘制资源置于浏览 Fragment 的右上角,如 如图 1 和图 2 所示。此方法将标题字符串替换为 可绘制资源(如果还调用了setTitle()
)。可绘制资源的大小必须为 52 dp 高。setTitle()
设置浏览 Fragment 右上角的标题字符串,除非 调用setBadgeDrawable()
。setHeadersState()
和setHeadersTransitionOnBackEnabled()
可隐藏或停用标题。 如需了解详情,请参阅隐藏或停用标题部分。setBrandColor()
设置浏览 Fragment 中界面元素的背景颜色,具体来说就是标头 部分背景颜色,并采用指定的颜色值。setSearchAffordanceColor()
用于将搜索图标的颜色设置为指定的颜色值。“搜索”图标 显示在浏览 Fragment 的左上角,如图 1 和图 2 所示。
自定义标题视图
图 1 中显示的浏览 fragment 显示了视频类别名称, 文本视图下的行标题,即视频数据库中的行标题。您还可以自定义 标头,以便在更复杂的布局中添加其他视图。以下部分介绍了如何 添加一个在类别名称旁边显示图标的图片视图,如图 2 所示。
![应用主屏幕](https://developer.android.google.cn/static/images/tv/custom-head.png?authuser=9&hl=da)
图 2. 浏览 fragment 中的行标题同时带有一个图标 和文本标签。
行标题的布局定义如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/header_icon" android:layout_width="32dp" android:layout_height="32dp" /> <TextView android:id="@+id/header_label" android:layout_marginTop="6dp" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
使用 Presenter
并实现
抽象方法来创建、绑定和取消绑定 ViewHolder。以下
示例显示了如何将 ViewHolder 与两个视图绑定,
ImageView
和 TextView
。
Kotlin
class IconHeaderItemPresenter : Presenter() { override fun onCreateViewHolder(viewGroup: ViewGroup): Presenter.ViewHolder { val view = LayoutInflater.from(viewGroup.context).run { inflate(R.layout.icon_header_item, null) } return Presenter.ViewHolder(view) } override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, o: Any) { val headerItem = (o as ListRow).headerItem val rootView = viewHolder.view rootView.findViewById<ImageView>(R.id.header_icon).apply { rootView.resources.getDrawable(R.drawable.ic_action_video, null).also { icon -> setImageDrawable(icon) } } rootView.findViewById<TextView>(R.id.header_label).apply { text = headerItem.name } } override fun onUnbindViewHolder(viewHolder: Presenter.ViewHolder) { // no-op } }
Java
public class IconHeaderItemPresenter extends Presenter { @Override public ViewHolder onCreateViewHolder(ViewGroup viewGroup) { LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); View view = inflater.inflate(R.layout.icon_header_item, null); return new ViewHolder(view); } @Override public void onBindViewHolder(ViewHolder viewHolder, Object o) { HeaderItem headerItem = ((ListRow) o).getHeaderItem(); View rootView = viewHolder.view; ImageView iconView = (ImageView) rootView.findViewById(R.id.header_icon); Drawable icon = rootView.getResources().getDrawable(R.drawable.ic_action_video, null); iconView.setImageDrawable(icon); TextView label = (TextView) rootView.findViewById(R.id.header_label); label.setText(headerItem.getName()); } @Override public void onUnbindViewHolder(ViewHolder viewHolder) { // no-op } }
标题必须可聚焦,以便使用方向键执行下列操作 滚动浏览它们您可以通过以下两种方式管理此功能:
- 在
onBindViewHolder()
中将您的视图设置为可聚焦:Kotlin
override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, o: Any) { val headerItem = (o as ListRow).headerItem val rootView = viewHolder.view rootView.focusable = View.FOCUSABLE // ... }
Java
@Override public void onBindViewHolder(ViewHolder viewHolder, Object o) { HeaderItem headerItem = ((ListRow) o).getHeaderItem(); View rootView = viewHolder.view; rootView.setFocusable(View.FOCUSABLE) // Allows the D-Pad to navigate to this header item // ... }
- 将布局设置为可聚焦:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" ... android:focusable="true">
最后,在显示BrowseSupportFragment
目录浏览器,请使用 setHeaderPresenterSelector()
方法为行标题设置 Presenter,如下例所示。
Kotlin
setHeaderPresenterSelector(object : PresenterSelector() { override fun getPresenter(o: Any): Presenter { return IconHeaderItemPresenter() } })
Java
setHeaderPresenterSelector(new PresenterSelector() { @Override public Presenter getPresenter(Object o) { return new IconHeaderItemPresenter(); } });
有关完整示例,请参阅 <ph type="x-smartling-placeholder"></ph> Leanback 示例应用 ,了解所有最新动态。
隐藏或停用标题
有时您并不希望显示行标题
需要一个可滚动列表。调用 BrowseSupportFragment.setHeadersState()
onActivityCreated()
方法隐藏或停用行标题。setHeadersState()
方法用于设置浏览 Fragment 中标题的初始状态,给定以下某个条件
常量作为参数:
HEADERS_ENABLED
: 创建浏览 fragment activity 时,标头会启用并显示 默认值。标题外观如本页面上的图 1 和图 2 所示。HEADERS_HIDDEN
: 创建浏览 fragment activity 时,默认情况下会启用并隐藏标题。 屏幕的标题部分处于收起状态,如 提供卡片视图中的数字。通过 用户可以选择展开前的标题部分将其展开。HEADERS_DISABLED
: 创建浏览 fragment activity 后,标头默认处于停用状态, 从不显示。
如果设置了 HEADERS_ENABLED
或 HEADERS_HIDDEN
,则您可以调用
setHeadersTransitionOnBackEnabled()
以支持从行中所选内容项移回行标题。这是通过
。如要自行处理背部动作
将 false
传递给 setHeadersTransitionOnBackEnabled()
并实现您自己的返回堆栈处理。
显示媒体列表
BrowseSupportFragment
类可让你
定义并显示以下可浏览媒体内容类别和媒体项:
使用适配器和 Presenter 创建媒体目录。借助适配器
包含媒体目录信息的本地或在线数据源。
Adapter 使用 Presenter 来创建视图并为这些视图绑定数据
在屏幕上显示一个列表项。
以下示例代码展示了用于显示字符串数据的 Presenter
实现:
Kotlin
private const val TAG = "StringPresenter" class StringPresenter : Presenter() { override fun onCreateViewHolder(parent: ViewGroup): Presenter.ViewHolder { val textView = TextView(parent.context).apply { isFocusable = true isFocusableInTouchMode = true background = parent.resources.getDrawable(R.drawable.text_bg) } return Presenter.ViewHolder(textView) } override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, item: Any) { (viewHolder.view as TextView).text = item.toString() } override fun onUnbindViewHolder(viewHolder: Presenter.ViewHolder) { // no op } }
Java
public class StringPresenter extends Presenter { private static final String TAG = "StringPresenter"; public ViewHolder onCreateViewHolder(ViewGroup parent) { TextView textView = new TextView(parent.getContext()); textView.setFocusable(true); textView.setFocusableInTouchMode(true); textView.setBackground( parent.getResources().getDrawable(R.drawable.text_bg)); return new ViewHolder(textView); } public void onBindViewHolder(ViewHolder viewHolder, Object item) { ((TextView) viewHolder.view).setText(item.toString()); } public void onUnbindViewHolder(ViewHolder viewHolder) { // no op } }
为媒体项构建 Presenter 类后,您可以
然后将适配器连接到 BrowseSupportFragment
将这些内容显示在屏幕上,以供用户浏览。以下示例
代码演示了如何构建 Adapter 来显示类别和项目
StringPresenter
类(如
上一个代码示例:
Kotlin
private const val NUM_ROWS = 4 ... private lateinit var rowsAdapter: ArrayObjectAdapter override fun onCreate(savedInstanceState: Bundle?) { ... buildRowsAdapter() } private fun buildRowsAdapter() { rowsAdapter = ArrayObjectAdapter(ListRowPresenter()) for (i in 0 until NUM_ROWS) { val listRowAdapter = ArrayObjectAdapter(StringPresenter()).apply { add("Media Item 1") add("Media Item 2") add("Media Item 3") } HeaderItem(i.toLong(), "Category $i").also { header -> rowsAdapter.add(ListRow(header, listRowAdapter)) } } browseSupportFragment.adapter = rowsAdapter }
Java
private ArrayObjectAdapter rowsAdapter; private static final int NUM_ROWS = 4; @Override protected void onCreate(Bundle savedInstanceState) { ... buildRowsAdapter(); } private void buildRowsAdapter() { rowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); for (int i = 0; i < NUM_ROWS; ++i) { ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter( new StringPresenter()); listRowAdapter.add("Media Item 1"); listRowAdapter.add("Media Item 2"); listRowAdapter.add("Media Item 3"); HeaderItem header = new HeaderItem(i, "Category " + i); rowsAdapter.add(new ListRow(header, listRowAdapter)); } browseSupportFragment.setAdapter(rowsAdapter); }
此示例展示了 Adapter 的一个静态实现。典型的媒体浏览应用 使用来自在线数据库或网络服务的数据。例如,一个浏览应用 使用从网络检索到的数据,请参阅 <ph type="x-smartling-placeholder"></ph> Leanback 示例应用 ,了解所有最新动态。
更新背景
若要为电视上的媒体浏览应用增添视觉吸引力,您可以更新背景 在用户浏览内容时看到的图片。此方法可促进用户与应用的互动更多 电影般的观赏体验
Leanback 界面工具包提供了一个 BackgroundManager
类,用于更改 TV 应用 activity 的背景。以下示例展示了如何
创建一个简单的方法来更新 TV 应用 activity 中的背景:
Kotlin
protected fun updateBackground(drawable: Drawable) { BackgroundManager.getInstance(this).drawable = drawable }
Java
protected void updateBackground(Drawable drawable) { BackgroundManager.getInstance(this).setDrawable(drawable); }
许多媒体浏览应用会在用户浏览时自动更新背景
。为此,您可以设置一个选择监听器
根据用户当前的选择更新背景。以下示例展示了如何
设置一个 OnItemViewSelectedListener
类,
捕获选择事件并更新背景:
Kotlin
protected fun clearBackground() { BackgroundManager.getInstance(this).drawable = defaultBackground } protected fun getDefaultItemViewSelectedListener(): OnItemViewSelectedListener = OnItemViewSelectedListener { _, item, _, _ -> if (item is Movie) { item.getBackdropDrawable().also { background -> updateBackground(background) } } else { clearBackground() } }
Java
protected void clearBackground() { BackgroundManager.getInstance(this).setDrawable(defaultBackground); } protected OnItemViewSelectedListener getDefaultItemViewSelectedListener() { return new OnItemViewSelectedListener() { @Override public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) { if (item instanceof Movie ) { Drawable background = ((Movie)item).getBackdropDrawable(); updateBackground(background); } else { clearBackground(); } } }; }
注意:前面的实现是一个简单的示例, 图示。在您自己的应用中创建此函数时,请运行 在单独的线程中执行后台更新操作,以获得更好的性能。此外,如果您 计划在用户滚动浏览各项内容时更新背景,添加 延迟背景图片更新,直到用户选定某个项为止。这种方法可避免 过多的背景图片更新。