在 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 版本中的行為一致。

創(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 中所示。

行標題的布局定義如下:
<?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_ENABLED 或 HEADERS_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è)置搜索、標題、背景更換等操作。