在電視上運作的媒體應用程式必須讓使用者瀏覽內容服務, 即可開始播放內容內容瀏覽體驗 必須簡單、直覺,還有賞心悅目且吸引力十足的內容。
本指南討論如何使用 androidx.leanback 程式庫提供的類別 實作使用者介面,方便瀏覽應用程式媒體目錄中的音樂或影片。
注意事項:這裡顯示的實作範例使用的是
BrowseSupportFragment
而非已淘汰的 BrowseFragment
類別BrowseSupportFragment
會擴充 AndroidX
Fragment
類別,
有助於在不同裝置和 Android 版本上保持一致的行為。
![應用程式主畫面](https://developer.android.com/static/images/tv/app-browse.png?authuser=4&hl=zh-tw)
圖 1:Leanback 範例應用程式的瀏覽片段會顯示影片目錄資料。
建立媒體瀏覽版面配置
BrowseSupportFragment
Leanback UI 工具包的類別
可讓你建立瀏覽類別和媒體項目列的主要版面配置,
最基本的程式碼以下範例說明如何建立包含
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 元素,並設定圖示、標題和
是否已啟用類別標頭。
實作 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()); } ...
設定 UI 元素
在前一個範例中,私人方法 setupUIElements()
呼叫了多個
BrowseSupportFragment
方法來設定媒體目錄瀏覽器的樣式:
setBadgeDrawable()
會將指定的可繪製資源放在瀏覽片段的右上角,如 如圖 1 和圖 2 所示這個方法會將標題字串替換成 可繪製資源 (如果也呼叫了setTitle()
)。可繪製資源的大小必須是 52 dp 身高setTitle()
會在瀏覽片段的右上角設定標題字串,除非 系統會呼叫setBadgeDrawable()
。setHeadersState()
以及setHeadersTransitionOnBackEnabled()
隱藏或停用標頭。 詳情請參閱「隱藏或停用標頭」一節。setBrandColor()
設定瀏覽片段 (特別是標頭) 中 UI 元素的背景顏色 區段背景顏色,具有指定色彩值。setSearchAffordanceColor()
以指定色彩值設定搜尋圖示的顏色。搜尋圖示 出現在瀏覽片段的左上角,如圖 1 和圖 2 所示。
自訂標頭檢視畫面
圖 1 所示的瀏覽片段顯示了影片類別名稱。 也就是文字檢視中影片資料庫的列標題您還可以自訂 標題,以更複雜的版面配置納入額外的檢視畫面。下列各節將說明如何 包含一張圖片檢視模式,在類別名稱旁邊顯示圖示,如圖 2 所示。
![應用程式主畫面](https://developer.android.com/static/images/tv/custom-head.png?authuser=4&hl=zh-tw)
圖 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
並實作
建立、繫結和解除繫結檢視畫面容器的抽象方法。下列
範例說明如何將兩個檢視區塊繫結至檢視容器
ImageView
和 TextView
。
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 範例應用程式 ,直接在 Google Cloud 控制台實際操作。
隱藏或停用標頭
有時不希望顯示列標題,例如空間不足時
要求捲動清單呼叫 BrowseSupportFragment.setHeadersState()
在片段的 onActivityCreated()
期間
隱藏或停用列標題的方法。setHeadersState()
方法會在下列情況下,設定瀏覽片段中標頭的初始狀態
常數做為參數:
HEADERS_ENABLED
: 建立瀏覽片段活動時,標頭會啟用並由 預設值。標題如本頁圖 1 和圖 2 所示。HEADERS_HIDDEN
: 建立瀏覽片段活動時,標頭會預設為啟用並隱藏。 畫面的標題部分已收合,如 所示 提供資訊卡檢視模式中的數字。 使用者只要選取收合標題部分,即可展開標題。HEADERS_DISABLED
: 建立瀏覽片段活動時,標頭會預設為停用,而且 一律不會顯示。
如果已設定 HEADERS_ENABLED
或 HEADERS_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 範例應用程式 ,直接在 Google Cloud 控制台實際操作。
更新背景
如要為電視上的媒體瀏覽應用程式新增視覺興趣,你可以更新背景 圖片。這項技巧可讓使用者更常與應用程式互動 才能拍出電影般、充滿樂趣
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(); } } }; }
注意:先前的實作範例為以下目的的簡易範例: 插圖在應用程式中建立這個函式時,請執行 為提升效能,系統會在個別執行緒中執行背景更新動作。此外, 打算更新背景,以回應使用者捲動瀏覽項目的行為,並加入 延遲更新背景圖片,直到使用者完成某個項目。這種技巧可避免 過度更新背景圖片。