Aplikacja multimedialna działająca na telewizorze musi umożliwiać użytkownikom przeglądanie oferty treści, wybieranie ich i odtwarzanie. Przeglądanie treści musi być proste i intuicyjne, a także atrakcyjne wizualnie i angażujące.
W tym przewodniku omawiamy, jak za pomocą klas udostępnianych przez wycofaną bibliotekę androidx.leanback zaimplementować interfejs użytkownika do przeglądania muzyki lub filmów z katalogu multimediów aplikacji.
Uwaga: w pokazanym tutaj przykładzie implementacji użyto klasy BrowseSupportFragment
zamiast wycofanej klasy BrowseFragment
. BrowseSupportFragment
rozszerza klasę AndroidX
Fragment
,
co pomaga zapewnić spójne działanie na różnych urządzeniach i wersjach Androida.

Rysunek 1. Fragment przeglądania w przykładowej aplikacji Leanback wyświetla dane katalogu filmów.
Tworzenie układu przeglądania multimediów
Klasa BrowseSupportFragment
w zestawie narzędzi interfejsu Leanback
umożliwia utworzenie podstawowego układu do przeglądania kategorii i wierszy elementów multimedialnych przy użyciu minimalnej ilości kodu. Poniższy przykład pokazuje, jak utworzyć układ zawierający obiekt 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>
Ten widok jest ustawiany przez główne działanie aplikacji, jak pokazano w tym przykładzie:
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); } ...
Metody BrowseSupportFragment
wypełniają widok danymi filmu i elementami interfejsu oraz ustawiają parametry układu, takie jak ikona i tytuł, a także to, czy nagłówki kategorii są włączone.
Podklasa aplikacji, która implementuje metody BrowseSupportFragment
, konfiguruje też odbiorniki zdarzeń dla działań użytkownika w elementach interfejsu i przygotowuje menedżera tła, jak pokazano w tym przykładzie:
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()); } ...
Ustawianie elementów interfejsu
W poprzednim przykładzie metoda prywatna setupUIElements()
wywołuje kilka metod BrowseSupportFragment
, aby nadać styl przeglądarce katalogu multimediów:
setBadgeDrawable()
umieszcza określony zasób rysowalny w prawym górnym rogu fragmentu przeglądania, jak pokazano na rysunkach 1 i 2. Ta metoda zastępuje ciąg tytułu zasobem rysowalnym, jeśli wywoływana jest też metodasetTitle()
. Zasób rysowalny musi mieć wysokość 52 dp.setTitle()
ustawia ciąg znaków tytułu w prawym górnym rogu fragmentu przeglądania, chyba że wywołana jest funkcjasetBadgeDrawable()
.setHeadersState()
isetHeadersTransitionOnBackEnabled()
ukrywają lub wyłączają nagłówki. Więcej informacji znajdziesz w sekcji Ukrywanie lub wyłączanie nagłówków.setBrandColor()
ustawia kolor tła elementów interfejsu w fragmencie przeglądania, w szczególności kolor tła sekcji nagłówka, na podaną wartość koloru.setSearchAffordanceColor()
ustawia kolor ikony wyszukiwania na podaną wartość. Ikona wyszukiwania jest widoczna w lewym górnym rogu fragmentu przeglądania, jak pokazano na rysunkach 1 i 2.
Dostosowywanie widoków nagłówka
Fragment przeglądania pokazany na rysunku 1 wyświetla nazwy kategorii filmów, które są nagłówkami wierszy w bazie danych filmów, w widokach tekstu. Możesz też dostosować nagłówek, aby uwzględnić dodatkowe widoki w bardziej złożonym układzie. W sekcjach poniżej dowiesz się, jak dodać widok obrazu, który wyświetla ikonę obok nazwy kategorii, jak pokazano na rysunku 2.

Rysunek 2. Nagłówki wierszy w fragmencie przeglądania z ikoną i etykietą tekstową.
Układ nagłówka wiersza jest zdefiniowany w ten sposób:
<?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>
Użyj Presenter
i zastosuj metody abstrakcyjne, aby utworzyć, powiązać i odłączyć uchwyt widoku. Poniższy przykład pokazuje, jak powiązać element Viewholder z 2 widokami: ImageView
i 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 } }
Nagłówki muszą być możliwe do zaznaczenia, aby można było przewijać je za pomocą pada kierunkowego. Możesz to zrobić na 2 sposoby:
- Ustaw widok jako fokusowalny w
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 // ... }
- Ustaw układ tak, aby można było na nim skupić uwagę:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" ... android:focusable="true">
Na koniec w implementacji BrowseSupportFragment
, która wyświetla przeglądarkę katalogu, użyj metody setHeaderPresenterSelector()
, aby ustawić prezenter nagłówka wiersza, jak pokazano w tym przykładzie.
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(); } });
Pełny przykład znajdziesz w przykładowej aplikacji Leanback.
Ukrywanie i wyłączanie nagłówków
Czasami nie chcesz, aby nagłówki wierszy się wyświetlały, np. gdy nie ma wystarczającej liczby kategorii, aby wymagać listy z możliwością przewijania. Wywołaj metodę BrowseSupportFragment.setHeadersState()
w trakcie metody onActivityCreated()
fragmentu, aby ukryć lub wyłączyć nagłówki wierszy. Metoda setHeadersState()
ustawia stan początkowy nagłówków we fragmencie przeglądania. Jako parametr przyjmuje jedną z tych stałych:
HEADERS_ENABLED
: gdy tworzony jest fragment przeglądania, nagłówki są domyślnie włączone i wyświetlane. Nagłówki będą wyglądać tak jak na ilustracjach 1 i 2 na tej stronie.HEADERS_HIDDEN
: gdy tworzona jest aktywność fragmentu przeglądania, nagłówki są domyślnie włączone i ukryte. Sekcja nagłówka ekranu jest zwinięta, jak pokazano na rysunku w artykule Wyświetlanie karty. Użytkownik może kliknąć zwiniętą sekcję nagłówka, aby ją rozwinąć.HEADERS_DISABLED
: gdy tworzona jest aktywność fragmentu przeglądania, nagłówki są domyślnie wyłączone i nigdy nie są wyświetlane.
Jeśli ustawiona jest wartość HEADERS_ENABLED
lub HEADERS_HIDDEN
, możesz wywołać
setHeadersTransitionOnBackEnabled()
, aby umożliwić powrót do nagłówka wiersza z wybranego elementu treści w wierszu. Ta opcja jest domyślnie włączona, jeśli nie wywołasz metody. Aby samodzielnie obsługiwać ruch wsteczny, przekaż wartość false
do setHeadersTransitionOnBackEnabled()
i zastosuj własną obsługę stosu wstecznego.
Wyświetlanie list multimediów
Klasa BrowseSupportFragment
umożliwia definiowanie i wyświetlanie kategorii treści multimedialnych, które można przeglądać, oraz elementów multimedialnych z katalogu multimediów za pomocą adapterów i prezenterów. Adaptery umożliwiają łączenie się z lokalnymi lub internetowymi źródłami danych zawierającymi informacje o katalogu multimediów.
Adaptery używają prezenterów do tworzenia widoków i powiązywania z nimi danych w celu wyświetlania elementu na ekranie.
Poniższy przykładowy kod pokazuje implementację Presenter
do wyświetlania danych w postaci ciągu znaków:
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 } }
Po utworzeniu klasy prezentera dla elementów multimedialnych możesz utworzyć adapter i dołączyć go do BrowseSupportFragment
, aby wyświetlać te elementy na ekranie i umożliwiać użytkownikowi ich przeglądanie. Poniższy przykładowy kod pokazuje, jak utworzyć adapter do wyświetlania kategorii i elementów w tych kategoriach za pomocą klasy StringPresenter
pokazanej w poprzednim przykładzie kodu:
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); }
Ten przykład pokazuje statyczną implementację adapterów. Typowa aplikacja do przeglądania multimediów korzysta z danych z bazy danych online lub usługi internetowej. Przykład aplikacji do przeglądania, która korzysta z danych pobranych z internetu, znajdziesz w przykładowej aplikacji Leanback .
Aktualizowanie tła
Aby uatrakcyjnić wizualnie aplikację do przeglądania multimediów na telewizorze, możesz aktualizować obraz tła, gdy użytkownicy przeglądają treści. Ta technika może sprawić, że korzystanie z aplikacji będzie bardziej filmowe i przyjemne.
Zestaw narzędzi interfejsu Leanback udostępnia klasę BackgroundManager
do zmiany tła aktywności aplikacji na telewizor. Poniższy przykład pokazuje, jak utworzyć prostą metodę aktualizowania tła w aktywności aplikacji na telewizor:
Kotlin
protected fun updateBackground(drawable: Drawable) { BackgroundManager.getInstance(this).drawable = drawable }
Java
protected void updateBackground(Drawable drawable) { BackgroundManager.getInstance(this).setDrawable(drawable); }
Wiele aplikacji do przeglądania multimediów automatycznie aktualizuje tło, gdy użytkownik przegląda listy multimediów. Aby to zrobić, możesz skonfigurować odbiornik wyboru, który będzie automatycznie aktualizować tło na podstawie bieżącego wyboru użytkownika. W tym przykładzie pokazujemy, jak skonfigurować klasę OnItemViewSelectedListener
, aby przechwytywać zdarzenia wyboru i aktualizować tło:
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(); } } }; }
Uwaga: poprzednia implementacja to prosty przykład służący do celów ilustracyjnych. Podczas tworzenia tej funkcji we własnej aplikacji uruchamiaj działanie aktualizacji w tle w osobnym wątku, aby uzyskać lepszą wydajność. Jeśli planujesz aktualizować tło w odpowiedzi na przewijanie elementów przez użytkowników, dodaj czas opóźnienia aktualizacji obrazu tła, aż użytkownik zatrzyma się na elemencie. Ta technika pozwala uniknąć nadmiernych aktualizacji obrazu tła.