建立目錄瀏覽器

使用 Compose 建構更優質的內容
使用 Jetpack Compose for Android TV OS 以最少的程式碼建立精美的 UI。

在電視上執行的媒體應用程式,必須讓使用者瀏覽其內容、進行選擇,然後開始播放內容。內容瀏覽體驗必須簡單、直覺,在賞心悅目且吸引人的內容中。

本指南說明如何使用 androidx.leanback 程式庫提供的類別,實作瀏覽應用程式媒體目錄中音樂或影片的使用者介面。

注意:此處顯示的實作範例使用的是 BrowseSupportFragment,而非已淘汰的 BrowseFragment 類別。BrowseSupportFragment 會擴充 AndroidX Fragment 類別,協助您確保不同裝置和 Android 版本的行為保持一致。

應用程式主畫面

圖 1:Leanback 範例應用程式的瀏覽片段會顯示影片目錄資料。

建立媒體瀏覽版面配置

Leanback UI 工具包中的 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>

應用程式的主要活動會設定這個檢視畫面,如以下範例所示:

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 方法會在檢視畫面中填入影片資料和 UI 元素,並設定圖示和標題等版面配置參數,以及是否啟用類別標頭。

如要進一步瞭解如何設定 UI 元素,請參閱「設定 UI 元素」一節。如要進一步瞭解如何隱藏標頭,請參閱「隱藏或停用標頭」一節。

實作 BrowseSupportFragment 方法的應用程式子類別也會為 UI 元素上的使用者動作設定事件監聽器,並準備背景管理員,如以下範例所示:

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

設定 UI 元素

在上一個範例中,私人方法 setupUIElements() 會呼叫多個 BrowseSupportFragment 方法,設定媒體目錄瀏覽器的樣式:

  • setBadgeDrawable() 會將指定的可繪製資源放在瀏覽片段的右上角,如圖 1 和圖 2 所示。如果呼叫 setTitle(),此方法會將標題字串取代為可繪製資源。可繪製資源高度必須為 52 dp。
  • setTitle() 會設定瀏覽片段右上角的標題字串,除非呼叫 setBadgeDrawable()
  • setHeadersState()setHeadersTransitionOnBackEnabled() 隱藏或停用標頭。詳情請參閱「隱藏或停用標頭」一節。
  • setBrandColor() 會使用指定色彩值,設定瀏覽片段中 UI 元素的背景顏色 (特別是標頭區段的背景顏色)。
  • setSearchAffordanceColor() 會使用指定顏色值設定搜尋圖示的顏色。搜尋圖示會顯示在瀏覽片段的左上角,如圖 1 和圖 2 所示。

自訂標頭檢視畫面

圖 1 所示的瀏覽片段會在文字檢視區塊中顯示影片類別名稱,也就是影片資料庫中的資料列標頭。您也可以自訂標頭,在更複雜的版面配置中加入其他檢視畫面。以下各節說明如何加入圖片檢視畫面,在類別名稱旁邊顯示圖示 (如圖 2 所示)。

應用程式主畫面

圖 2. 瀏覽片段中同時含有圖示和文字標籤的資料列標頭。

資料列標頭的版面配置定義如下:

<?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 並實作抽象方法,建立、繫結及解除繫結檢視畫面預留位置。以下範例說明如何繫結檢視容器與兩個檢視畫面: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
    }
}

您的標頭必須是可聚焦的,以便 D-Pad 可以捲動瀏覽這些標頭。方法有兩種:

  • 將檢視畫面設為 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() 方法來設定資料列標頭的顯示器,如以下範例所示。

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

如需完整範例,請參閱 Leanback 範例應用程式

隱藏或停用標頭

有時候,您可能不想顯示資料列標頭,例如類別不足以需要可捲動的清單時。在片段的 onActivityCreated() 方法期間呼叫 BrowseSupportFragment.setHeadersState() 方法,即可隱藏或停用資料列標頭。setHeadersState() 方法會使用下列其中一個常數做為參數,設定瀏覽片段中的標頭初始狀態:

  • HEADERS_ENABLED:建立瀏覽片段活動時,系統會預設啟用標頭並顯示標頭。標題如本頁圖 1 和圖 2 所示。
  • HEADERS_HIDDEN:建立瀏覽片段活動時,標頭會預設為啟用並隱藏。畫面的標題部分已收合,如「提供資訊卡檢視畫面」中的 圖表所示。使用者只要選取收合標題區段,即可展開標題。
  • HEADERS_DISABLED:建立瀏覽片段活動後,標頭會預設為停用,一律不會顯示。

如果已設定 HEADERS_ENABLEDHEADERS_HIDDEN,您可以呼叫 setHeadersTransitionOnBackEnabled() 以支援從資料列中選取的內容項目返回資料列標頭。如果未呼叫該方法,則預設會啟用這項功能。如要自行處理返回動作,請將 false 傳遞至 setHeadersTransitionOnBackEnabled(),並實作自己的返回堆疊處理程序。

多媒體媒體清單

BrowseSupportFragment 類別可讓您透過轉接器和簡報器,定義並顯示媒體目錄中可瀏覽的媒體內容類別和媒體項目。轉接程式可讓您連結內含媒體目錄資訊的本機或線上資料來源。轉接器會使用顯示器建立檢視畫面,並將資料繫結至這些檢視畫面,以在螢幕上顯示項目。

以下程式碼範例說明如何實作 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
    }
}

為媒體項目建構簡報者類別後,您可以建構轉接器並附加至 BrowseSupportFragment,以便在使用者瀏覽時顯示這些項目。以下程式碼範例示範如何使用上述程式碼範例所示的 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);
}

這個範例說明轉接器的靜態實作。一般的媒體瀏覽應用程式會使用線上資料庫或網路服務的資料。如需使用從網路擷取的資料的瀏覽應用程式範例,請參閱 Leanback 範例應用程式

更新背景

如要為電視上的媒體瀏覽應用程式添加視覺興趣,您可以在使用者瀏覽內容時更新背景圖片。這項技術可以讓使用者與應用程式互動時,感覺更有臨場感,而且更有趣。

Leanback UI 工具包提供 BackgroundManager 類別,可用於變更 TV 應用程式活動的背景。以下範例說明如何建立簡易方法,更新 TV 應用程式活動中的背景:

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

注意:先前的實作為用於說明的簡易範例。在應用程式中建立這個函式時,請在個別執行緒中執行背景更新動作,以便提升效能。此外,若您打算更新背景,以回應使用者捲動瀏覽項目的情況,請增加一些時間使背景圖片更新作業延遲,直到使用者達成某個項目為止。這項技術可以避免過度更新背景圖片。