Android TV开发之创建目录浏览器

在 TV 上运行的媒体应用需要允许用户浏览其提供的内容、选择要播放的内容,以及开始播放内容。此类应用的内容浏览体验应简单直观,并且视觉上要赏心悦目。
本节课介绍如何利用 Leanback androidx 库提供的类来实现一个用户界面,该界面可让用户浏览应用的媒体目录中包含的音乐或视频。
注意:此处显示的实现示例使用的是 BrowseSupportFragmentBrowseSupportFragment 扩展了 AndroidX Fragment 类,这可确保各种设备和 Android 版本中的行为一致。

图 1. Leanback 示例应用浏览 Fragment 中显示视频目录数据。

创建媒体浏览布局

通过 Leanback 库中的 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 用于设置此视图,如下例所示:

    public class MainActivity extends Activity {
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
        }
    ...

BrowseSupportFragment 方法用于在视图中填入视频数据和界面元素,并设置图标、标题以及是否启用类别标题等布局参数。
实现 BrowseSupportFragment 方法的应用子类还会设置事件监听器来监听界面元素上的用户操作,并准备后台管理器,如下例所示:

    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 headers 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(),此方法会将标题字符串替换为可绘制资源。可绘制资源的高度应为 52dp。
  • 如果未调用 setBadgeDrawable()setTitle() 可用于设置浏览 Fragment 右上角的标题字符串。
  • 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 与两个视图(一个 ImageView 和一个 TextView)绑定在一起。

    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() 中将您的视图设为可聚焦:
    @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,如下例所示。

    setHeaderPresenterSelector(new PresenterSelector() {
        @Override
        public Presenter getPresenter(Object o) {
            return new IconHeaderItemPresenter();
        }
    });    

如需查看完整示例,请参阅 Android TV GitHub 代码库中的 Android Leanback 示例应用。

隐藏或停用标题

有时,您可能不希望显示行标题。例如:当类别数量不是很多,无需使用可滚动列表时。在 Fragment 的onActivityCreated()方法期间调用 BrowseSupportFragment.setHeadersState() 方法可隐藏或停用行标题。setHeadersState() 方法用于设置浏览 Fragment 中标题的初始状态,前提是提供以下某个常量作为参数:

  • HEADERS_ENABLED - 创建浏览 Fragment Activity 后,默认情况下会启用并显示标题。标题外观如本页图 1 和图 2 中所示。
  • HEADERS_HIDDEN - 创建浏览 Fragment Activity 后,默认情况下会启用并隐藏标题。
  • HEADERS_DISABLED - 创建浏览 Fragment Activity 后,默认情况下会停用标题,且一律不显示标题。

如果设置了 HEADERS_ENABLEDHEADERS_HIDDEN,您可以调用 setHeadersTransitionOnBackEnabled(),以支持从行中所选内容项返回到行标题。它默认处于启用状态(如果您未调用该方法),但如果您希望自行处理返回操作,应将值 false 传递给 setHeadersTransitionOnBackEnabled(),并实现您自己的返回堆栈处理。

显示媒体列表

通过 BrowseSupportFragment 类,您可以使用 Adapter 和 Presenter 定义和显示来自媒体目录的可浏览媒体内容类别和媒体内容。您可以通过 Adapter 连接到包含媒体目录信息的本地或在线数据源。Adapter 使用 Presenter 来创建视图并将数据绑定到这些视图,以便在屏幕上显示内容。

以下示例代码展示了一个用于显示字符串数据的 Presenter 实现:

    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 类后,您可以构建 Adapter 并将其附加到 BrowseSupportFragment,以将这些内容显示在屏幕上供用户浏览。以下示例代码展示了如何构建 Adapter 来利用上一代码示例中所示的 StringPresenter 类来显示类别以及这些类别的内容:

    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 的一个静态实现。典型的媒体浏览应用会使用来自在线数据库或网络服务的数据。如需查看使用从网络检索的数据的浏览应用示例,请参阅 Android TV GitHub 代码库中的 Android Leanback 示例应用。

更新背景

为了增强 TV 上媒体浏览应用的视觉吸引力,您可以在用户浏览内容时更新背景图片。此方法可令用户与应用的交互更加赏心悦目。
Leanback 支持库提供了一个 BackgroundManager 类,以用于更改 TV 应用 Activity 的背景。以下示例展示了如何创建一个简单的方法来更新 TV 应用 Activity 内的背景:

    protected void updateBackground(Drawable drawable) {
        BackgroundManager.getInstance(this).setDrawable(drawable);
    }

许多现有的媒体浏览应用都会在用户浏览媒体列表时自动更新背景。为了实现此项更新,您可以设置一个选择监听器,以根据用户的当前选择自动更新背景。以下示例展示了如何设置一个 OnItemViewSelectedListener 类来捕获选择事件并更新背景:

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

注意:以上实现是出于说明目的而展示的一个简单示例。在您自己的应用中创建此功能时,您应考虑在一个单独的线程内运行背景更新操作,以便获得更好的性能。此外,如果您计划在用户滚动浏览内容时更新背景,不妨考虑添加一个延迟时间,使背景图片更新延迟到用户选择某个内容后再执行。此方法可以避免背景图片更新过于频繁。

本篇文章主要是讲解如何利用BrowseSupportFragment实现结构为(分组+节目列表)结构的界面,其中包括设置搜索、标题、背景更换等操作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351