建立目錄瀏覽器

在電視上執行的媒體應用程式,必須讓使用者瀏覽影視內容、選取項目並開始播放內容。內容瀏覽體驗必須簡單明瞭,並以賞心悅目的視覺效果呈現。

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

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

應用程式主畫面

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

建立媒體瀏覽版面配置

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>

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

KotlinJava
class MainActivity : Activity() {

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

BrowseSupportFragment 方法會將影片資料和 UI 元素填入檢視畫面,並設定圖示和標題等版面配置參數,以及是否啟用類別標頭。

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

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

KotlinJava
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()
   
}
   
...
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 並實作抽象方法,以建立、繫結及解除檢視容器。以下範例說明如何繫結 View Holder 和 ImageViewTextView 這兩個檢視畫面。

KotlinJava
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
   
}
}
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() 中將檢視畫面設為可聚焦:
    KotlinJava
    override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, o: Any) {
       
    val headerItem = (o as ListRow).headerItem
       
    val rootView = viewHolder.view

        rootView
    .focusable = View.FOCUSABLE
       
    // ...
    }
    @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() 方法來設定資料列標頭的呈現器,如以下範例所示。

KotlinJava
setHeaderPresenterSelector(object : PresenterSelector() {
   
override fun getPresenter(o: Any): Presenter {
       
return IconHeaderItemPresenter()
   
}
})
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 實作以顯示字串資料:

KotlinJava
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
   
}
}
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 類別,建構轉接器顯示這些類別中的類別和項目:

KotlinJava
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
}
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 支援資料庫提供 BackgroundManager 類別,可用於變更電視應用程式活動的背景。以下範例說明如何建立在電視應用程式活動內更新背景的簡易方法:

KotlinJava
protected fun updateBackground(drawable: Drawable) {
   
BackgroundManager.getInstance(this).drawable = drawable
}
protected void updateBackground(Drawable drawable) {
   
BackgroundManager.getInstance(this).setDrawable(drawable);
}

許多媒體瀏覽應用程式會在使用者瀏覽媒體清單時,自動更新背景。如要這麼做,您可以設定選取事件監聽器,根據使用者目前選取的項目自動更新背景。以下範例說明如何設定 OnItemViewSelectedListener 類別來擷取選取事件並更新背景:

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

注意:先前的實作方式為簡易說明範例。在應用程式中建立這個函式時,請在另一個執行緒中執行背景更新動作以提升效能。此外,如果您打算更新背景,以因應使用者捲動瀏覽項目的動作,請新增時間,將背景圖片更新到使用者停留在項目之前。這項技術可避免背景圖片更新過多。