Android TV開發(fā)之創(chuàng)建目錄瀏覽器

在 TV 上運行的媒體應(yīng)用需要允許用戶瀏覽其提供的內(nèi)容、選擇要播放的內(nèi)容,以及開始播放內(nèi)容。此類應(yīng)用的內(nèi)容瀏覽體驗應(yīng)簡單直觀,并且視覺上要賞心悅目。
本節(jié)課介紹如何利用 Leanback androidx 庫提供的類來實現(xiàn)一個用戶界面,該界面可讓用戶瀏覽應(yīng)用的媒體目錄中包含的音樂或視頻。
注意:此處顯示的實現(xiàn)示例使用的是 BrowseSupportFragment,BrowseSupportFragment 擴展了 AndroidX Fragment 類,這可確保各種設(shè)備和 Android 版本中的行為一致。

圖 1. Leanback 示例應(yīng)用瀏覽 Fragment 中顯示視頻目錄數(shù)據(jù)。

創(chuàng)建媒體瀏覽布局

通過 Leanback 庫中的 BrowseSupportFragment 類,您只需很少的代碼,即可創(chuàng)建用于瀏覽媒體項目類別和行的主要布局。以下示例展示了如何創(chuàng)建一個包含 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>

應(yīng)用的主 Activity 用于設(shè)置此視圖,如下例所示:

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

BrowseSupportFragment 方法用于在視圖中填入視頻數(shù)據(jù)和界面元素,并設(shè)置圖標、標題以及是否啟用類別標題等布局參數(shù)。
實現(xiàn) BrowseSupportFragment 方法的應(yīng)用子類還會設(shè)置事件監(jiān)聽器來監(jiān)聽界面元素上的用戶操作,并準備后臺管理器,如下例所示:

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

設(shè)置界面元素

在上例中,私有方法 setupUIElements() 調(diào)用了幾個 BrowseSupportFragment 方法來設(shè)置媒體目錄瀏覽器的樣式:

  • setBadgeDrawable() 用于將指定的可繪制資源置于瀏覽 Fragment 的右上角,如圖 1 和圖 2 中所示。如果還調(diào)用了 setTitle(),此方法會將標題字符串替換為可繪制資源。可繪制資源的高度應(yīng)為 52dp。
  • 如果未調(diào)用 setBadgeDrawable(),setTitle() 可用于設(shè)置瀏覽 Fragment 右上角的標題字符串。
  • setHeadersState()setHeadersTransitionOnBackEnabled() 用于隱藏或停用標題。
  • setBrandColor() 用于將瀏覽 Fragment 中界面元素的背景顏色(具體來說就是標題部分的背景顏色)設(shè)為指定的顏色值。
  • setSearchAffordanceColor() 用于將搜索圖標的顏色設(shè)為指定的顏色值。搜索圖標顯示在瀏覽 Fragment 的左上角,如圖 1 和圖 2 中所示。

自定義標題視圖

圖 1 中所示的瀏覽 Fragment 在左側(cè)窗格中列出了視頻類別名稱(行標題)。文本視圖顯示的這些類別名稱來自視頻數(shù)據(jù)庫。您可以自定義標題,以便在更復(fù)雜的布局中添加其他視圖。下面幾部分展示了如何添加圖片視圖,以在類別名稱旁邊顯示一個圖標,如圖 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 并實現(xiàn)抽象方法來創(chuàng)建、綁定和取消綁定 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() 中將您的視圖設(shè)為可聚焦:
    @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
        //...
    }
  • 將您的布局設(shè)為可聚焦
<?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
       ...
       android:focusable="true">

最后,在顯示目錄瀏覽器的 BrowseSupportFragment 實現(xiàn)中,使用 setHeaderPresenterSelector() 方法為行標題設(shè)置 Presenter,如下例所示。

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

如需查看完整示例,請參閱 Android TV GitHub 代碼庫中的 Android Leanback 示例應(yīng)用。

隱藏或停用標題

有時,您可能不希望顯示行標題。例如:當類別數(shù)量不是很多,無需使用可滾動列表時。在 Fragment 的onActivityCreated()方法期間調(diào)用 BrowseSupportFragment.setHeadersState() 方法可隱藏或停用行標題。setHeadersState() 方法用于設(shè)置瀏覽 Fragment 中標題的初始狀態(tài),前提是提供以下某個常量作為參數(shù):

  • HEADERS_ENABLED - 創(chuàng)建瀏覽 Fragment Activity 后,默認情況下會啟用并顯示標題。標題外觀如本頁圖 1 和圖 2 中所示。
  • HEADERS_HIDDEN - 創(chuàng)建瀏覽 Fragment Activity 后,默認情況下會啟用并隱藏標題。
  • HEADERS_DISABLED - 創(chuàng)建瀏覽 Fragment Activity 后,默認情況下會停用標題,且一律不顯示標題。

如果設(shè)置了 HEADERS_ENABLEDHEADERS_HIDDEN,您可以調(diào)用 setHeadersTransitionOnBackEnabled(),以支持從行中所選內(nèi)容項返回到行標題。它默認處于啟用狀態(tài)(如果您未調(diào)用該方法),但如果您希望自行處理返回操作,應(yīng)將值 false 傳遞給 setHeadersTransitionOnBackEnabled(),并實現(xiàn)您自己的返回堆棧處理。

顯示媒體列表

通過 BrowseSupportFragment 類,您可以使用 Adapter 和 Presenter 定義和顯示來自媒體目錄的可瀏覽媒體內(nèi)容類別和媒體內(nèi)容。您可以通過 Adapter 連接到包含媒體目錄信息的本地或在線數(shù)據(jù)源。Adapter 使用 Presenter 來創(chuàng)建視圖并將數(shù)據(jù)綁定到這些視圖,以便在屏幕上顯示內(nèi)容。

以下示例代碼展示了一個用于顯示字符串數(shù)據(jù)的 Presenter 實現(xiàn):

    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
        }
    }

為媒體內(nèi)容構(gòu)建 Presenter 類后,您可以構(gòu)建 Adapter 并將其附加到 BrowseSupportFragment,以將這些內(nèi)容顯示在屏幕上供用戶瀏覽。以下示例代碼展示了如何構(gòu)建 Adapter 來利用上一代碼示例中所示的 StringPresenter 類來顯示類別以及這些類別的內(nèi)容:

    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 的一個靜態(tài)實現(xiàn)。典型的媒體瀏覽應(yīng)用會使用來自在線數(shù)據(jù)庫或網(wǎng)絡(luò)服務(wù)的數(shù)據(jù)。如需查看使用從網(wǎng)絡(luò)檢索的數(shù)據(jù)的瀏覽應(yīng)用示例,請參閱 Android TV GitHub 代碼庫中的 Android Leanback 示例應(yīng)用。

更新背景

為了增強 TV 上媒體瀏覽應(yīng)用的視覺吸引力,您可以在用戶瀏覽內(nèi)容時更新背景圖片。此方法可令用戶與應(yīng)用的交互更加賞心悅目。
Leanback 支持庫提供了一個 BackgroundManager 類,以用于更改 TV 應(yīng)用 Activity 的背景。以下示例展示了如何創(chuàng)建一個簡單的方法來更新 TV 應(yīng)用 Activity 內(nèi)的背景:

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

許多現(xiàn)有的媒體瀏覽應(yīng)用都會在用戶瀏覽媒體列表時自動更新背景。為了實現(xiàn)此項更新,您可以設(shè)置一個選擇監(jiān)聽器,以根據(jù)用戶的當前選擇自動更新背景。以下示例展示了如何設(shè)置一個 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();
                }
            }
        };
    }    

注意:以上實現(xiàn)是出于說明目的而展示的一個簡單示例。在您自己的應(yīng)用中創(chuàng)建此功能時,您應(yīng)考慮在一個單獨的線程內(nèi)運行背景更新操作,以便獲得更好的性能。此外,如果您計劃在用戶滾動瀏覽內(nèi)容時更新背景,不妨考慮添加一個延遲時間,使背景圖片更新延遲到用戶選擇某個內(nèi)容后再執(zhí)行。此方法可以避免背景圖片更新過于頻繁。

本篇文章主要是講解如何利用BrowseSupportFragment實現(xiàn)結(jié)構(gòu)為(分組+節(jié)目列表)結(jié)構(gòu)的界面,其中包括設(shè)置搜索、標題、背景更換等操作。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容