Eine Medien-App, die auf einem Fernseher ausgeführt wird, muss Nutzern die Möglichkeit bieten, sich das Inhaltsangebot anzusehen, eine Auswahl zu treffen und Inhalte abzuspielen. Die Suche nach Inhalten muss einfach und intuitiv sowie visuell ansprechend und ansprechend sein.
In diesem Leitfaden wird erläutert, wie Sie mit den von der Leanback-Androidx-Bibliothek bereitgestellten Klassen eine Benutzeroberfläche zum Durchsuchen von Musik oder Videos aus dem Medienkatalog Ihrer App implementieren.
Hinweis:Im hier gezeigten Implementierungsbeispiel wird BrowseSupportFragment
anstelle der verworfenen Klasse BrowseFragment
verwendet. BrowseSupportFragment
erweitert die AndroidX-Klasse
Fragment
und sorgt so auf allen Geräten und Android-Versionen für ein einheitliches Verhalten.
![Hauptbildschirm der App](https://developer.android.com/static/images/tv/app-browse.png?authuser=6&hl=de)
Abbildung 1: Das Suchfragment der Leanback-Beispielanwendung zeigt Videokatalogdaten an.
Layout für das Durchsuchen von Medien erstellen
Mit der Klasse BrowseSupportFragment
in der Leanback-Bibliothek können Sie mit minimalem Code ein primäres Layout erstellen, um Kategorien und Zeilen von Medienelementen zu durchsuchen. Das folgende Beispiel zeigt, wie du ein Layout mit einem BrowseSupportFragment
-Objekt erstellst:
<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>
Diese Ansicht wird durch die Hauptaktivität der Anwendung festgelegt, 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); } ...
Über die BrowseSupportFragment
-Methoden werden die Videodaten und UI-Elemente in die Ansicht aufgenommen. Außerdem legen sie Layoutparameter wie Symbol und Titel fest und legen fest, ob Kategorieheader aktiviert werden sollen.
Die Unterklasse der Anwendung, die die BrowseSupportFragment
-Methoden implementiert, richtet auch Ereignis-Listener für Nutzeraktionen an 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 Medienkatalogbrowser zu gestalten:
setBadgeDrawable()
platziert die angegebene Drawable-Ressource oben rechts im Suchfragment, wie in den Abbildungen 1 und 2 dargestellt. Bei dieser Methode wird der Titelstring durch die Drawable-Ressource ersetzt, wenn auchsetTitle()
aufgerufen wird. Die Drawable-Ressource muss 52 dp hoch sein.- Mit
setTitle()
wird der Titelstring oben rechts im Suchfragment festgelegt, sofern nichtsetBadgeDrawable()
aufgerufen wird. - Mit
setHeadersState()
undsetHeadersTransitionOnBackEnabled()
werden die Header ausgeblendet oder deaktiviert. Weitere Informationen finden Sie im Abschnitt Header ausblenden oder deaktivieren. - Mit
setBrandColor()
wird die Hintergrundfarbe für UI-Elemente im Suchfragment festgelegt, insbesondere die Hintergrundfarbe des Header-Bereichs, mit dem angegebenen Farbwert. - Mit
setSearchAffordanceColor()
wird die Farbe des Suchsymbols mit dem angegebenen Farbwert festgelegt. Das Suchsymbol wird in der oberen linken Ecke des Suchfragments angezeigt, wie in den Abbildungen 1 und 2 dargestellt.
Kopfzeilenansichten anpassen
Das in Abbildung 1 gezeigte Suchfragment zeigt die Namen der Videokategorien, also die Zeilenüberschriften in der Videodatenbank, in Textansichten an. Sie können die Kopfzeile auch anpassen, um zusätzliche Ansichten in einem komplexeren Layout aufzunehmen. In den folgenden Abschnitten wird gezeigt, wie Sie eine Bildansicht einbinden, bei der neben dem Kategorienamen ein Symbol angezeigt wird (siehe Abbildung 2).
![Hauptbildschirm der App](https://developer.android.com/static/images/tv/custom-head.png?authuser=6&hl=de)
Abbildung 2: Die Zeilenüberschriften im Suchfragment enthalten sowohl ein Symbol als auch ein Textlabel.
Das Layout der Zeilenüberschrift 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 Presenter
und implementieren Sie die abstrakten Methoden, um den Ansichtsinhaber zu erstellen, zu binden und seine Bindung aufzuheben. Das folgende Beispiel zeigt, wie der Ansichtsinhaber 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 Header müssen fokussierbar sein, damit das Steuerkreuz zum Scrollen verwendet werden kann. Dafür gibt es zwei Möglichkeiten:
- Legen Sie fest, dass die Ansicht in
onBindViewHolder()
fokussierbar ist: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 das Layout so fest, dass es 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 Implementierung BrowseSupportFragment
, die den Katalogbrowser anzeigt, die Methode setHeaderPresenterSelector()
, um den Vortragenden für den Zeilenheader 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.
Header ausblenden oder deaktivieren
Manchmal möchten Sie nicht, dass die Zeilenüberschriften angezeigt werden, z. B. wenn nicht genügend Kategorien vorhanden sind, um eine scrollbare Liste erforderlich zu machen. Rufen Sie während der onActivityCreated()
-Methode des Fragments die Methode BrowseSupportFragment.setHeadersState()
auf, um die Zeilenüberschriften auszublenden oder zu deaktivieren. Die Methode setHeadersState()
legt den Anfangszustand der Header im Suchfragment fest, wobei eine der folgenden Konstanten als Parameter verwendet wird:
HEADERS_ENABLED
: Wenn die Aktivität zum Durchsuchen von Fragmenten erstellt wird, sind Header aktiviert und standardmäßig angezeigt. Die Überschriften werden wie in den Abbildungen 1 und 2 auf dieser Seite dargestellt.HEADERS_HIDDEN
: Wenn die Aktivität zum Durchsuchen von Fragmenten erstellt wird, werden Header standardmäßig aktiviert und ausgeblendet. Der Kopfzeilenbereich des Bildschirms ist minimiert, wie in einer Abbildung unter Kartenansicht bereitstellen dargestellt. Der Nutzer kann den minimierten Header-Abschnitt auswählen, um ihn zu maximieren.HEADERS_DISABLED
: Wenn die Aktivität zum Durchsuchen von Fragmenten erstellt wird, sind Header standardmäßig deaktiviert und werden nie angezeigt.
Wenn entweder HEADERS_ENABLED
oder HEADERS_HIDDEN
festgelegt ist, können Sie setHeadersTransitionOnBackEnabled()
aufrufen, um das Verschieben von einem ausgewählten Inhaltselement in der Zeile zum Zeilenheader zu unterstützen. Dies ist standardmäßig aktiviert, wenn Sie die Methode nicht aufrufen. Wenn Sie die Zurückbewegung selbst ausführen möchten, übergeben Sie false
an setHeadersTransitionOnBackEnabled()
und implementieren Sie Ihre eigene Back-Stack-Handhabung.
Medienlisten anzeigen
Mit der Klasse BrowseSupportFragment
können Sie mithilfe von Adaptern und Moderatoren durchsuchbare Medieninhaltskategorien und Medienelemente aus einem Medienkatalog definieren und anzeigen. Mit Adaptern können Sie eine Verbindung zu lokalen oder Online-Datenquellen herstellen, die Informationen aus Ihrem Medienkatalog enthalten.
Adapter verwenden Vortragende, um Ansichten zu erstellen und Daten mit diesen Ansichten zu verknüpfen, um ein Element auf dem Bildschirm anzuzeigen.
Der folgende Beispielcode zeigt eine Implementierung eines 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 Präsentationsklasse für Ihre Medienelemente erstellt haben, können Sie einen Adapter erstellen und an das BrowseSupportFragment
anhängen, damit die Elemente auf dem Bildschirm angezeigt werden können. Der folgende Beispielcode zeigt, wie Sie mithilfe der StringPresenter
-Klasse aus dem vorherigen Codebeispiel einen Adapter erstellen, um Kategorien und Elemente in diesen Kategorien anzuzeigen:
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); }
Dieses Beispiel zeigt eine statische Implementierung der Adapter. Eine typische Anwendung für das Surfen in Medien nutzt Daten aus einer Onlinedatenbank oder einem Webdienst. Ein Beispiel für eine Browseranwendung, die aus dem Web abgerufene Daten verwendet, findest du in der Leanback-Beispiel-App.
Hintergrund aktualisieren
Wenn Sie einer App zum Ansehen von Medien auf dem Fernseher visuelles Interesse hinzufügen möchten, können Sie das Hintergrundbild aktualisieren, während Nutzer Inhalte durchsuchen. Mit dieser Technik lassen sich Interaktionen mit Ihrer App filmreifer und angenehmer gestalten.
Die Leanback-Supportbibliothek bietet eine BackgroundManager
-Klasse zum Ändern des Hintergrunds deiner TV-App-Aktivitäten. Das folgende Beispiel zeigt, wie du eine einfache Methode zum Aktualisieren des Hintergrunds in deinen TV-App-Aktivitäten erstellen kannst:
Kotlin
protected fun updateBackground(drawable: Drawable) { BackgroundManager.getInstance(this).drawable = drawable }
Java
protected void updateBackground(Drawable drawable) { BackgroundManager.getInstance(this).setDrawable(drawable); }
In vielen Apps zum Suchen nach Medien wird der Hintergrund automatisch aktualisiert, während der Nutzer durch die Medieneinträge navigiert. Dazu können Sie einen Auswahl-Listener einrichten, der den Hintergrund basierend auf der aktuellen Auswahl des Nutzers automatisch aktualisiert. Das folgende Beispiel zeigt, wie Sie eine OnItemViewSelectedListener
-Klasse einrichten, um Auswahlereignisse zu erfassen 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 Anwendung erstellen, führen Sie die Hintergrundaktualisierungsaktion in einem separaten Thread aus, um eine bessere Leistung zu erzielen. Wenn Sie den Hintergrund aktualisieren möchten, wenn Nutzer durch die Elemente scrollen, sollten Sie die Aktualisierung des Hintergrundbilds so lange verzögern, bis sich der Nutzer auf ein Element festgelegt hat. Mit dieser Technik werden übermäßig viele Updates von Hintergrundbildern vermieden.