Katalogbrowser erstellen

Besser lernen mit der Funktion „Schreiben“
Mit Jetpack Compose für Android TV OS lassen sich mit nur minimalem Code ansprechende UIs erstellen.

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.

Hauptbildschirm der App

Abbildung 1: Das Suchfragment der Leanback-Beispielanwendung zeigt Videokatalogdaten an.

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.

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

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 auch setTitle() aufgerufen wird. Die Drawable-Ressource muss 52 dp hoch sein.
  • setTitle() legt den Titelstring in der oberen rechten Ecke des Fragments fest, sofern nicht setBadgeDrawable() aufgerufen wird.
  • Mit setHeadersState() und setHeadersTransitionOnBackEnabled() 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).

Hauptbildschirm der App

Abbildung 2: Die Zeilenüberschriften im Suchfragment haben sowohl ein Symbol als auch ein Textlabel.

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.