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. Die Suche nach Inhalten muss einfach und intuitiv sein sowie optisch ansprechend und fesselnd.
In dieser Anleitung wird beschrieben, wie Sie mit den Klassen der eingestellten androidx.leanback-Bibliothek eine Benutzeroberfläche zum Durchsuchen von Musik oder Videos aus dem Media-Katalog Ihrer App implementieren.
Hinweis:Im hier gezeigten Implementierungsbeispiel wird BrowseSupportFragment anstelle der eingestellten Klasse BrowseFragment verwendet. BrowseSupportFragment erweitert die AndroidX
Fragment Klasse und sorgt so für ein einheitliches Verhalten auf allen Geräten und Android-Versionen.
Abbildung 1 : Das Browse-Fragment der Leanback-Beispiel-App zeigt Daten aus dem Videokatalog.
Layout für die Suche nach Medien erstellen
Die BrowseSupportFragment
Klasse im Leanback UI Toolkit
ermöglicht es Ihnen, mit
minimalem Code ein primäres Layout zum Durchsuchen von Kategorien und Zeilen mit Media-Elementen zu erstellen. Im folgenden Beispiel wird gezeigt, wie Sie ein Layout mit einem
BrowseSupportFragment Objekt erstellen:
<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); } ...
Die Methoden von BrowseSupportFragment füllen die Ansicht mit den Videodaten und UI-Elementen und legen Layoutparameter wie das Symbol und den Titel sowie fest, ob Kategorieüberschriften aktiviert sind.
Die Unterklasse der Anwendung, die die Methoden von BrowseSupportFragment implementiert, richtet auch Ereignis-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 zeichenfähige Ressource in der oberen rechten Ecke des Browse-Fragments, wie in Abbildung 1 und 2 gezeigt. Diese Methode ersetzt den Titelstring durch die Drawable-Ressource, wennsetTitle()auch aufgerufen wird. Die Drawable-Ressource muss 52 dp hoch sein.setTitle()legt den Titelstring in der oberen rechten Ecke des Browse-Fragments fest, es sei denn,setBadgeDrawable()wird aufgerufen.setHeadersState()undsetHeadersTransitionOnBackEnabled()blenden die Überschriften aus oder deaktivieren sie. 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 in der oberen linken Ecke des Browse-Fragments angezeigt, wie in Abbildung 1 und 2 gezeigt.
Header-Ansichten anpassen
Im Browse-Fragment in Abbildung 1 werden die Namen der Videokategorien, die die Zeilenüberschriften in der Videodatenbank sind, in Textansichten angezeigt. Sie können den Header auch 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 neben dem Kategorienamen ein Symbol angezeigt wird, wie in Abbildung 2 gezeigt.
Abbildung 2 : Die Zeilenüberschriften im Browse-Fragment mit einem Symbol und einem Textlabel.
Das Layout für die 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 einen Presenter und implementieren Sie die abstrakten Methoden, um den View-Holder zu erstellen, zu binden und zu lösen. Im folgenden Beispiel wird gezeigt, wie Sie den View-Holder mit zwei Ansichten binden, einer ImageView und einer 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. Dazu haben Sie zwei Möglichkeiten:
- Legen Sie in
onBindViewHolder()fest, dass Ihre Ansicht 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 sein soll:
<?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 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 BrowseSupportFragment.setHeadersState()
Methode während der onActivityCreated()
Methode des Fragments auf, um die Zeilenüberschriften auszublenden oder zu deaktivieren. Die Methode setHeadersState() legt den Anfangszustand der Überschriften im Browse-Fragment fest. Als Parameter wird eine der folgenden Konstanten angegeben:
HEADERS_ENABLED: Wenn die Aktivität des Browse-Fragments erstellt wird, sind die Überschriften standardmäßig aktiviert und werden angezeigt. Die Überschriften werden wie in Abbildung 1 und 2 auf dieser Seite angezeigt.HEADERS_HIDDEN: Wenn die Aktivität des Browse-Fragments erstellt wird, sind die Überschriften standardmäßig aktiviert und ausgeblendet. Der Header-Bereich des Bildschirms ist minimiert, wie in einer Abbildung unter Kartenansicht bereitstellen gezeigt. Der Nutzer kann den minimierten Header-Bereich auswählen, um ihn zu maximieren.HEADERS_DISABLED: Wenn die Aktivität des Browse-Fragments erstellt wird, sind die Überschriften standardmäßig deaktiviert und werden nie angezeigt.
Wenn entweder 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 zu unterstützen. Diese Option ist standardmäßig aktiviert, wenn Sie die Methode nicht aufrufen. Wenn Sie die Zurück-Bewegung selbst verarbeiten möchten, übergeben Sie false an setHeadersTransitionOnBackEnabled() und implementieren Sie Ihre eigene Back-Stack-Verarbeitung.
Medienlisten anzeigen
Mit der Klasse BrowseSupportFragment
können Sie
durchsuchbare Kategorien von Medieninhalten und Media-Elemente aus
einem Media-Katalog mithilfe von Adaptern und Presentern definieren und anzeigen. Mit Adaptern können Sie Verbindungen
zu lokalen oder Online-Datenquellen herstellen, die die Informationen Ihres Media-Katalogs enthalten.
Adapter verwenden Presenter, 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 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 Presenter-Klasse für Ihre Media-Elemente erstellt haben, können Sie
einen Adapter erstellen und ihn an das BrowseSupportFragment
anhängen, um diese Elemente auf dem Bildschirm anzuzeigen, damit der Nutzer sie durchsuchen kann. Der folgende Beispiel
code zeigt, wie Sie einen Adapter erstellen, um Kategorien und Elemente
in diesen Kategorien mit der Klasse StringPresenter anzuzeigen, die im
vorherigen Codebeispiel gezeigt wird:
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 Anwendung zum Durchsuchen von Medien verwendet Daten aus einer Online-Datenbank oder einem Webdienst. Ein Beispiel für eine Anwendung zum Durchsuchen von Medien, die aus dem Web abgerufene Daten verwendet, finden Sie in der Leanback-Beispiel-App .
Hintergrund aktualisieren
Um einer App zum Durchsuchen von Medien auf dem Fernseher mehr visuelles Interesse zu verleihen, können Sie das Hintergrund bild 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. Im folgenden Beispiel wird gezeigt, 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 die Medienlisten navigiert. Dazu können Sie einen Auswahl-Listener einrichten, um den Hintergrund automatisch
anhand der aktuellen Auswahl des Nutzers zu aktualisieren. Im folgenden Beispiel wird gezeigt, 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 App erstellen, führen Sie die Hintergrundaktualisierung 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 Verzögerung hinzu, damit das Hintergrundbild erst aktualisiert wird, wenn der Nutzer ein Element ausgewählt hat. So vermeiden Sie übermäßige Aktualisierungen des Hintergrundbilds.