创建目录浏览器

使用 Compose 构建更出色的应用
使用适用于 Android TV OS 的 Jetpack Compose,只需极少的代码即可创建精美的界面。
<ph type="x-smartling-placeholder"></ph> Compose for TV →

在电视上运行的媒体应用需要允许用户浏览其提供的内容, 然后开始播放内容。内容浏览体验 不仅简单直观,而且具有视觉吸引力。

本指南介绍了如何使用 androidx.leanback 库提供的类。 实现一个界面,用于浏览应用的媒体目录中的音乐或视频。

注意:此处显示的实现示例使用 BrowseSupportFragment 而不是已废弃的 BrowseFragment 类。BrowseSupportFragment 扩展了 AndroidX Fragment 类、 有助于确保在设备和 Android 版本之间保持一致的行为。

应用主屏幕

图 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 所示。

应用主屏幕

图 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 与两个视图绑定, ImageViewTextView

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_ENABLEDHEADERS_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();
            }
        }
    };
}

注意:前面的实现是一个简单的示例, 图示。在您自己的应用中创建此函数时,请运行 在单独的线程中执行后台更新操作,以获得更好的性能。此外,如果您 计划在用户滚动浏览各项内容时更新背景,添加 延迟背景图片更新,直到用户选定某个项为止。这种方法可避免 过多的背景图片更新。