Katalogbrowser erstellen

Bessere Apps mit Compose entwickeln
Mit Jetpack Compose für Android TV OS können Sie mit minimalem Code schöne Benutzeroberflächen erstellen.

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.

Hauptbildschirm der App

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.

Weitere Informationen zum Einrichten von UI-Elementen finden Sie im Abschnitt UI-Elemente festlegen. Weitere Informationen zum Ausblenden von Headern finden Sie im Abschnitt Header ausblenden oder deaktivieren.

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 auch setTitle() aufgerufen wird. Die Drawable-Ressource muss 52 dp hoch sein.
  • setTitle() Legt den Titelstring oben rechts im Browse-Fragment fest, sofern nicht setBadgeDrawable() aufgerufen wird.
  • Mit setHeadersState() und setHeadersTransitionOnBackEnabled() 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).

Hauptbildschirm der App

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.