Eine Medien-App, die auf einem Fernseher ausgeführt wird, muss es Nutzern ermöglichen, sich das Inhaltsangebot anzusehen, eine Auswahl zu treffen und Inhalte abzuspielen. Das Durchsuchen von Inhalten muss einfach und intuitiv sowie visuell ansprechend und ansprechend sein.
In diesem Leitfaden wird erläutert, wie du mit den von der androidx.leanback-Bibliothek bereitgestellten Klassen eine Benutzeroberfläche zum Durchsuchen von Musik oder Videos aus dem Medienkatalog deiner App implementierst.
Hinweis:In dem hier gezeigten Implementierungsbeispiel wird BrowseSupportFragment
anstelle der verworfenen Klasse BrowseFragment
verwendet. BrowseSupportFragment
erweitert die Fragment
-Klasse von AndroidX und sorgt für ein einheitliches Verhalten auf allen Geräten und Android-Versionen.
Layout zur Mediensuche erstellen
Mit der Klasse BrowseSupportFragment
im Leanback-UI-Toolkit kannst du mit minimalem Code ein primäres Layout zum Durchsuchen von Kategorien und Zeilen von Medienelementen erstellen. Das folgende Beispiel zeigt, wie ein Layout erstellt wird, 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>
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); } ...
Die BrowseSupportFragment
-Methoden füllen die Ansicht mit den Videodaten und UI-Elementen und legen Layoutparameter wie das Symbol und den Titel fest und legen fest, ob Kategorieheader aktiviert sind.
Die abgeleitete Klasse der Anwendung, die die BrowseSupportFragment
-Methoden implementiert, richtet auch Ereignis-Listener für Nutzeraktionen in 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 Stil des Medienkatalogbrowsers zu gestalten:
setBadgeDrawable()
platziert die angegebene Drawable-Ressource oben rechts im Fragment, wie in den Abbildungen 1 und 2 gezeigt. Diese Methode ersetzt den Titelstring durch die Drawable-Ressource, wenn auchsetTitle()
aufgerufen wird. Die Drawable-Ressource muss 52 dp hoch sein.setTitle()
legt den Titelstring in der oberen rechten Ecke des Fragments fest, 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 Headerbereichs mit dem angegebenen Farbwert. setSearchAffordanceColor()
legt die Farbe des Suchsymbols mit dem angegebenen Farbwert fest. Das Suchsymbol wird in der oberen linken Ecke des Suchfragments angezeigt, wie in den Abbildungen 1 und 2 dargestellt.
Headeransichten anpassen
Das in Abbildung 1 gezeigte Suchfragment zeigt die Namen der Videokategorien (die Zeilenüberschriften in der Videodatenbank) in Textansichten an. Sie können den Header auch so anpassen, dass er zusätzliche Ansichten in einem komplexeren Layout enthält. In den folgenden Abschnitten wird beschrieben, wie Sie eine Bildansicht einfügen, in der neben dem Kategorienamen ein Symbol angezeigt wird (siehe Abbildung 2).
Das Layout für den Zeilentitel ist wie folgt 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, um den Ansichtsinhaber zu erstellen, zu binden und die Bindung aufzuheben. Das folgende Beispiel zeigt, wie Sie den Viewholder mit zwei Ansichten binden: ImageView
und 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 } }
Ihre Überschriften müssen fokussierbar sein, damit Sie mit dem Steuerkreuz durch sie scrollen können. Dafür gibt es zwei Möglichkeiten:
- Stellen Sie Ihre Ansicht in
onBindViewHolder()
so ein, dass sie 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 BrowseSupportFragment
-Implementierung, die den Katalogbrowser anzeigt, die Methode setHeaderPresenterSelector()
, um den Presenter 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 sollen die Zeilenüberschriften nicht angezeigt werden, z. B. wenn nicht genügend Kategorien vorhanden sind, sodass eine scrollbare Liste erforderlich ist. Rufen Sie die Methode BrowseSupportFragment.setHeadersState()
während der Methode onActivityCreated()
des Fragments auf, um die Zeilenheader auszublenden oder zu deaktivieren. Die Methode setHeadersState()
legt den Anfangszustand der Header im Suchfragment fest. Dabei wird eine der folgenden Konstanten als Parameter angegeben:
HEADERS_ENABLED
: Wenn die Aktivität zum Durchsuchen des Fragments erstellt wird, sind Header standardmäßig aktiviert und angezeigt. Die Überschriften werden in den Abbildungen 1 und 2 auf dieser Seite dargestellt.HEADERS_HIDDEN
: Wenn die Aktivität zum Durchsuchen des Fragments erstellt wird, sind Header standardmäßig aktiviert und ausgeblendet. Der Headerbereich des Bildschirms ist minimiert, wie in eine Abbildung unter Kartenansicht bereitstellen zu sehen ist. Der Nutzer kann den minimierten Kopfzeilenbereich auswählen, um ihn zu maximieren.HEADERS_DISABLED
: Wenn die Aktivität zum Durchsuchen des Fragments 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 zu unterstützen, von einem ausgewählten Inhaltselement in der Zeile zum Zeilentitel zurückzukehren. Dies ist standardmäßig aktiviert, wenn Sie die Methode nicht aufrufen. Wenn du die Rückwärtsbewegung selbst ausführen möchtest, übergib false
an setHeadersTransitionOnBackEnabled()
und implementiere deine eigene Back-Stack-Handhabung.
Medienlisten anzeigen
Mit der Klasse BrowseSupportFragment
können Sie durchsuchbare Medieninhaltskategorien und Medienelemente aus einem Medienkatalog mithilfe von Adaptern und Vortragenden definieren und anzeigen lassen. Mit Adaptern können Sie eine Verbindung zu lokalen oder Online-Datenquellen herstellen, die Ihre Medienkataloginformationen enthalten.
Adapter verwenden Moderatoren, um Ansichten zu erstellen und Daten an diese Ansichten zu binden, damit ein Element auf dem Bildschirm angezeigt werden kann.
Der folgende Beispielcode zeigt eine Implementierung einer 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 mit dem BrowseSupportFragment
verknüpfen, um diese Elemente auf dem Bildschirm anzuzeigen, damit der Nutzer sie durchsuchen kann. Der folgende Beispielcode zeigt, wie Sie mithilfe der Klasse StringPresenter
aus dem vorherigen Codebeispiel einen Adapter zum Anzeigen von Kategorien und Elementen in diesen Kategorien erstellen:
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 zum Durchsuchen von Medien verwendet Daten aus einer Onlinedatenbank oder einem Webdienst. Ein Beispiel für eine Browseranwendung, die aus dem Web abgerufene Daten verwendet, finden Sie in der Leanback-Beispielanwendung.
Hintergrund aktualisieren
Um einer App zum Surfen in Medien auf Fernsehern visuelles Interesse zu verleihen, kannst du das Hintergrundbild aktualisieren, während Nutzer Inhalte durchsuchen. Mit dieser Technik können Sie die Interaktion mit Ihrer App ansprechender gestalten.
Das Leanback-UI-Toolkit 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 innerhalb deiner TV-App-Aktivitäten erstellst:
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 Surfen im Medienbereich aktualisieren den Hintergrund automatisch, während der Nutzer durch Medienlisten navigiert. Dazu können Sie einen Auswahl-Listener einrichten, der den Hintergrund automatisch anhand der aktuellen Auswahl des Nutzers aktualisiert. Das folgende Beispiel zeigt, 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 Aktion für die Hintergrundaktualisierung zur Leistungsverbesserung in einem separaten Thread aus. Wenn du den Hintergrund aktualisieren möchtest, sobald Nutzer durch die Elemente scrollen, musst du eine Zeit hinzufügen, um die Aktualisierung des Hintergrundbilds zu verzögern, bis der Nutzer sich für einen Artikel entschieden hat. So vermeiden Sie übermäßige Aktualisierungen der Hintergrundbilder.