Katalogbrowser erstellen

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

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.

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 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 auch setTitle() aufgerufen wird. Die Drawable-Ressource muss 52 dp hoch sein.
  • Mit setTitle() wird der Titelstring oben rechts im Suchfragment festgelegt, 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 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

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.