In einer Media-App, die auf einem Fernseher ausgeführt wird, müssen Nutzer die Inhalte durchsuchen, eine Auswahl treffen und die Wiedergabe starten können. Das Durchsuchen von Inhalten muss einfach und intuitiv sein und visuell ansprechend und interessant gestaltet sein.
In diesem Leitfaden wird beschrieben, wie Sie mit den Klassen der eingestellten androidx.leanback-Bibliothek eine Benutzeroberfläche zum Durchsuchen von Musik oder Videos aus dem Medienkatalog Ihrer App implementieren.
Hinweis:Im hier gezeigten Implementierungsbeispiel wird BrowseSupportFragment
anstelle der eingestellten Klasse BrowseFragment
verwendet. BrowseSupportFragment
erweitert die AndroidX-Klasse Fragment
und trägt so zu einem einheitlichen Verhalten auf verschiedenen Geräten und Android-Versionen bei.

Abbildung 1: Im Browse-Fragment der Leanback-Beispiel-App werden Videokatalogdaten angezeigt.
Medienbrowser-Layout erstellen
Mit der Klasse BrowseSupportFragment
im Leanback-UI-Toolkit können Sie mit einem Minimum an Code ein primäres Layout zum Durchsuchen von Kategorien und Zeilen mit Media-Elementen erstellen. Im folgenden Beispiel wird gezeigt, wie Sie ein Layout erstellen, das ein BrowseSupportFragment
-Objekt enthält:
<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>
Die Hauptaktivität der Anwendung legt diese Ansicht fest, wie im folgenden Beispiel gezeigt:
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); } ...
Mit den BrowseSupportFragment
-Methoden werden die Ansicht mit den Videodaten und UI-Elementen gefüllt und Layoutparameter wie das Symbol und der Titel sowie die Aktivierung von Kategorieüberschriften festgelegt.
Die Unterklasse der Anwendung, die die BrowseSupportFragment
-Methoden implementiert, richtet auch Event-Listener für Nutzeraktionen auf den UI-Elementen ein und bereitet den Hintergrundmanager vor, wie im folgenden Beispiel gezeigt:
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-Elemente festlegen
Im vorherigen Beispiel ruft die private Methode setupUIElements()
mehrere BrowseSupportFragment
-Methoden auf, um den Media-Katalogbrowser zu gestalten:
setBadgeDrawable()
platziert die angegebene zeichnungsfähige Ressource in der oberen rechten Ecke des Browse-Fragments, wie in Abbildung 1 und 2 dargestellt. Mit dieser Methode wird der Titelstring durch die Drawable-Ressource ersetzt, wenn auchsetTitle()
aufgerufen wird. Die Drawable-Ressource muss 52 dp hoch sein.setTitle()
Legt den Titelstring oben rechts im Browse-Fragment fest, sofern nichtsetBadgeDrawable()
aufgerufen wird.- Mit
setHeadersState()
undsetHeadersTransitionOnBackEnabled()
werden die Überschriften ausgeblendet oder deaktiviert. Weitere Informationen finden Sie im Abschnitt Überschriften ausblenden oder deaktivieren. setBrandColor()
legt die Hintergrundfarbe für UI-Elemente im Browse-Fragment fest, insbesondere die Hintergrundfarbe des Header-Bereichs, mit dem angegebenen Farbwert.setSearchAffordanceColor()
Legt die Farbe des Suchsymbols mit dem angegebenen Farbwert fest. Das Suchsymbol wird oben links im Browse-Fragment angezeigt, wie in Abbildung 1 und 2 zu sehen ist.
Headeransichten anpassen
Im in Abbildung 1 gezeigten Browse-Fragment werden die Namen der Videokategorien, die die Zeilenüberschriften in der Videodatenbank sind, in Textansichten angezeigt. Sie können auch die Kopfzeile anpassen, um zusätzliche Ansichten in einem komplexeren Layout einzufügen. In den folgenden Abschnitten wird gezeigt, wie Sie eine Bildansicht einfügen, in der ein Symbol neben dem Kategorienamen angezeigt wird (siehe Abbildung 2).

Abbildung 2: Die Zeilenüberschriften im Browse-Fragment mit einem Symbol und einem Textlabel.
Das Layout für den Zeilenheader ist so definiert:
<?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>
Verwenden Sie ein Presenter
und implementieren Sie die abstrakten Methoden zum Erstellen, Binden und Aufheben der Bindung des Viewholders. Im folgenden Beispiel wird gezeigt, wie der ViewHolder mit zwei Ansichten, einem ImageView
und einem TextView
, verknüpft wird.
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 } }
Ihre Überschriften müssen fokussierbar sein, damit mit dem Steuerkreuz durch sie gescrollt werden kann. Dafür gibt es zwei Möglichkeiten:
- Legen Sie fest, dass die Ansicht in
onBindViewHolder()
fokussierbar sein soll: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 // ... }
- Legen Sie fest, dass Ihr Layout fokussierbar ist:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" ... android:focusable="true">
Verwenden Sie schließlich in der BrowseSupportFragment
-Implementierung, in der der Katalogbrowser angezeigt wird, die Methode setHeaderPresenterSelector()
, um den Presenter für die Zeilenüberschrift festzulegen, wie im folgenden Beispiel gezeigt.
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(); } });
Ein vollständiges Beispiel finden Sie in der Leanback-Beispiel-App.
Überschriften ausblenden oder deaktivieren
Manchmal sollen die Zeilenüberschriften nicht angezeigt werden, z. B. wenn nicht genügend Kategorien vorhanden sind, um eine scrollbare Liste zu benötigen. Rufen Sie die Methode BrowseSupportFragment.setHeadersState()
während der Methode onActivityCreated()
des Fragments auf, um die Zeilenüberschriften auszublenden oder zu deaktivieren. Mit der Methode setHeadersState()
wird der Ausgangszustand der Header im Browse-Fragment festgelegt. Als Parameter wird eine der folgenden Konstanten verwendet:
HEADERS_ENABLED
: Wenn die Aktivität des Browse-Fragments erstellt wird, sind Header standardmäßig aktiviert und werden angezeigt. Die Header werden wie in Abbildung 1 und Abbildung 2 auf dieser Seite dargestellt angezeigt.HEADERS_HIDDEN
: Wenn die Aktivität des Browser-Fragments erstellt wird, sind Header standardmäßig aktiviert und ausgeblendet. Der Kopfbereich des Bildschirms ist minimiert, wie in einer Abbildung unter Kartenansicht bereitstellen dargestellt. Der Nutzer kann den minimierten Headerbereich auswählen, um ihn zu maximieren.HEADERS_DISABLED
: Wenn die Aktivität des Browse-Fragments erstellt wird, sind Header standardmäßig deaktiviert und werden nie angezeigt.
Wenn HEADERS_ENABLED
oder HEADERS_HIDDEN
festgelegt ist, können Sie setHeadersTransitionOnBackEnabled()
aufrufen, um das Zurückkehren zur Zeilenüberschrift von einem ausgewählten Inhaltselement in der Zeile aus zu unterstützen. Dies ist standardmäßig aktiviert, wenn Sie die Methode nicht aufrufen. Wenn Sie die Rückwärtsbewegung selbst verarbeiten möchten, übergeben Sie false
an setHeadersTransitionOnBackEnabled()
und implementieren Sie Ihre eigene Verarbeitung des Backstacks.
Medienlisten anzeigen
Mit der Klasse BrowseSupportFragment
können Sie durchsuchbare Kategorien für Medieninhalte und Media-Elemente aus einem Medienkatalog mithilfe von Adaptern und Presentern definieren und anzeigen. Mit Adaptern können Sie Verbindungen zu lokalen oder Online-Datenquellen herstellen, die Informationen zu Ihrem Media-Katalog enthalten.
Mit Adaptern werden Ansichten erstellt und Daten an diese Ansichten gebunden, um ein Element auf dem Bildschirm darzustellen.
Der folgende Beispielcode zeigt die Implementierung von Presenter
zum Anzeigen von Stringdaten:
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 } }
Nachdem Sie eine Presenter-Klasse für Ihre Media-Elemente erstellt haben, können Sie einen Adapter erstellen und ihn an BrowseSupportFragment
anhängen, um diese Elemente auf dem Bildschirm anzuzeigen, damit der Nutzer sie durchsuchen kann. Das folgende Beispiel zeigt, wie Sie einen Adapter erstellen, um Kategorien und Elemente in diesen Kategorien mit der Klasse StringPresenter
aus dem vorherigen Codebeispiel darzustellen:
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); }
In diesem Beispiel wird eine statische Implementierung der Adapter gezeigt. Eine typische Media-Browsing-Anwendung verwendet Daten aus einer Online-Datenbank oder einem Webdienst. Ein Beispiel für eine Browsing-Anwendung, die aus dem Web abgerufene Daten verwendet, finden Sie in der Leanback-Beispiel-App.
Hintergrund aktualisieren
Um einer Media-Browsing-App auf dem Fernseher mehr visuelle Attraktivität zu verleihen, können Sie das Hintergrundbild aktualisieren, während Nutzer Inhalte durchsuchen. Diese Technik kann die Interaktion mit Ihrer App kinoreifer und angenehmer gestalten.
Das Leanback-UI-Toolkit bietet eine BackgroundManager
-Klasse zum Ändern des Hintergrunds der Aktivität Ihrer TV-App. Das folgende Beispiel zeigt, wie Sie eine einfache Methode zum Aktualisieren des Hintergrunds in der Aktivität Ihrer TV-App erstellen:
Kotlin
protected fun updateBackground(drawable: Drawable) { BackgroundManager.getInstance(this).drawable = drawable }
Java
protected void updateBackground(Drawable drawable) { BackgroundManager.getInstance(this).setDrawable(drawable); }
Viele Apps zum Durchsuchen von Medien aktualisieren den Hintergrund automatisch, wenn der Nutzer durch Medienlisten navigiert. Dazu können Sie einen Auswahl-Listener einrichten, um den Hintergrund automatisch auf Grundlage der aktuellen Auswahl des Nutzers zu aktualisieren. Im folgenden Beispiel wird gezeigt, wie Sie eine OnItemViewSelectedListener
-Klasse einrichten, um Auswahlereignisse abzufangen und den Hintergrund zu aktualisieren:
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(); } } }; }
Hinweis:Die vorherige Implementierung ist ein einfaches Beispiel zur Veranschaulichung. Wenn Sie diese Funktion in Ihrer eigenen App erstellen, führen Sie die Hintergrundaktualisierungsaktion in einem separaten Thread aus, um die Leistung zu verbessern. Wenn Sie den Hintergrund aktualisieren möchten, wenn Nutzer durch Elemente scrollen, fügen Sie eine Zeit hinzu, um die Aktualisierung des Hintergrundbilds zu verzögern, bis der Nutzer ein Element ausgewählt hat. So werden unnötige Aktualisierungen des Hintergrundbilds vermieden.