Creare un browser del catalogo

Migliora la creazione con Compose
Crea splendide UI con un minimo codice utilizzando Jetpack Compose per il sistema operativo Android TV.

Un'app multimediale eseguita su una TV deve consentire agli utenti di sfogliare le sue offerte di contenuti, creare una selezione e avvia la riproduzione dei contenuti. Esperienza di navigazione dei contenuti deve essere semplice e intuitivo, nonché visivamente piacevole e coinvolgente.

Questa guida illustra come utilizzare i corsi forniti dalla libreria androidx.leanback per implementare un'interfaccia utente per sfogliare la musica o i video dal catalogo multimediale della tua app.

Nota: l'esempio di implementazione mostrato qui utilizza BrowseSupportFragment anziché BrowseFragment, deprecato . BrowseSupportFragment estende AndroidX Fragment corso, contribuendo a garantire un comportamento coerente su tutti i dispositivi e le versioni di Android.

Schermata principale dell'app

Figura 1. Il frammento di navigazione dell'app Leanback di esempio mostra i dati del catalogo video.

Creare un layout di esplorazione dei contenuti multimediali

BrowseSupportFragment nel toolkit Leanback UI consente di creare un layout principale per sfogliare le categorie e le righe di elementi multimediali con un una quantità minima di codice. L'esempio seguente mostra come creare un layout che contenga un Oggetto BrowseSupportFragment:

<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>

L'attività principale dell'applicazione imposta questa visualizzazione, come mostrato nell'esempio seguente:

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);
    }
...

I metodi BrowseSupportFragment completano la vista con dati video ed elementi UI, nonché impostare parametri di layout come l'icona e il titolo. se le intestazioni delle categorie sono abilitate.

Per ulteriori informazioni sulla configurazione degli elementi UI, consulta la sezione Impostazione della UI elementi. Per ulteriori informazioni su come nascondere le intestazioni, consulta Sezione Nascondi o disattiva le intestazioni.

La sottoclasse dell'applicazione che implementa l'BrowseSupportFragment configura anche listener di eventi per le azioni dell'utente sugli elementi dell'interfaccia utente e prepara gestore in background, come mostrato nell'esempio seguente:

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());
    }
...

Impostare gli elementi dell'interfaccia utente

Nell'esempio precedente, il metodo privato setupUIElements() chiama diverse BrowseSupportFragment per applicare uno stile al browser del catalogo multimediale:

  • setBadgeDrawable() posiziona la risorsa drawable specificata nell'angolo in alto a destra del frammento di navigazione, come mostrato nelle figure 1 e 2. Questo metodo sostituisce la stringa del titolo con il risorsa drawable, se viene chiamato anche setTitle(). La risorsa drawable deve avere una dimensione di 52 dp alto.
  • setTitle() imposta la stringa del titolo nell'angolo in alto a destra del frammento di navigazione, a meno che setBadgeDrawable() ha chiamato.
  • setHeadersState() e setHeadersTransitionOnBackEnabled() nascondono o disattivano le intestazioni. Per ulteriori informazioni, consulta la sezione Nascondi o disattiva le intestazioni.
  • setBrandColor() imposta il colore di sfondo per gli elementi dell'interfaccia utente nel frammento Sfoglia, in particolare l'intestazione il colore di sfondo della sezione, con il valore del colore specificato.
  • setSearchAffordanceColor() imposta il colore dell'icona di ricerca con il valore colore specificato. Icona di ricerca compare nell'angolo superiore sinistro del frammento di navigazione, come mostrato nelle figure 1 e 2.

Personalizzare le visualizzazioni delle intestazioni

Il frammento Sfoglia mostrato nella figura 1 mostra i nomi delle categorie dei video. che sono le intestazioni di riga nel database video, nelle visualizzazioni di testo. Puoi anche personalizzare per includere altre viste in un layout più complesso. Le sezioni seguenti spiegano come includi una visualizzazione immagine che mostri un'icona accanto al nome della categoria, come mostrato nella figura 2.

Schermata principale dell&#39;app

Figura 2. Le intestazioni di riga nel frammento di navigazione con un'icona e un'etichetta di testo.

Il layout per l'intestazione della riga è definito come segue:

<?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>

Utilizza Presenter e implementa metodi astratti per creare, associare e slegare il titolare della vista. Le seguenti un esempio mostra come associare il visualizzatore a due viste, una ImageView e 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
    }
}

Le intestazioni devono essere attivabili in modo che sia possibile usare il D-pad scorrerle. Puoi gestire questa operazione in due modi:

  • Imposta la visualizzazione per impostarla su onBindViewHolder():

    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
        // ...
    }
    
  • Imposta il layout in modo che sia attivabile:
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
       ...
       android:focusable="true">

Infine, nell'implementazione BrowseSupportFragment che mostra browser catalogo, usa setHeaderPresenterSelector() per impostare il presentatore per l'intestazione della riga, come mostrato nell'esempio seguente.

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();
    }
});

Per un esempio completo, vedi Esempio di app Leanback di Google.

Nascondi o disattiva le intestazioni

A volte potresti non volere che le intestazioni di riga vengano visualizzate, ad esempio quando non sono sufficienti per richiedere un elenco scorrevole. Chiama il BrowseSupportFragment.setHeadersState() durante lo spostamento onActivityCreated() del frammento per nascondere o disattivare le intestazioni di riga. setHeadersState() imposta lo stato iniziale delle intestazioni nel frammento sfoglia, in base a uno dei seguenti criteri come parametro:

  • HEADERS_ENABLED: quando viene creata l'attività dei frammenti di navigazione, le intestazioni vengono abilitate e mostrate predefinito. Le intestazioni vengono visualizzate come illustrato nelle figure 1 e 2 di questa pagina.
  • HEADERS_HIDDEN: Quando viene creata l'attività dei frammenti di navigazione, le intestazioni sono abilitate e nascoste per impostazione predefinita. La sezione dell'intestazione dello schermo è compressa, come mostrato in un dato in Fornire una visualizzazione schede. La l'utente può selezionare la sezione dell'intestazione compressa per espanderla.
  • HEADERS_DISABLED: quando viene creata l'attività dei frammenti di navigazione, le intestazioni sono disabilitate per impostazione predefinita mai visualizzato.

Se HEADERS_ENABLED o HEADERS_HIDDEN è impostata, puoi chiamare setHeadersTransitionOnBackEnabled() per consentire il ritorno all'intestazione della riga da un contenuto selezionato nella riga. Questa operazione è abilitata da predefinito se non chiami il metodo. Per gestire autonomamente il movimento all'indietro, passa false a setHeadersTransitionOnBackEnabled() e implementare la tua gestione del back stack.

Elenchi di contenuti multimediali display

BrowseSupportFragment ti consente di definire e visualizzare categorie di contenuti multimediali sfogliabili ed elementi multimediali un catalogo multimediale usando adattatori e presentatori. Gli adattatori ti permettono di connettere alle origini dati locali o online contenenti le informazioni del catalogo di contenuti multimediali. Gli adattatori utilizzano i presentatori per creare viste e associare dati a queste viste per che mostra un elemento sullo schermo.

Il seguente codice di esempio mostra l'implementazione di un'istruzione Presenter per la visualizzazione di dati stringa:

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
    }
}

Dopo aver creato una classe per presentatore per gli elementi multimediali, puoi creare un alimentatore e collegalo a BrowseSupportFragment per visualizzare questi elementi sullo schermo per consentirne la navigazione da parte dell'utente. Nell'esempio che segue il codice illustra come costruire un adattatore per visualizzare categorie ed elementi in queste categorie utilizzando la classe StringPresenter mostrata in precedente esempio di codice:

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);
}

Questo esempio mostra un'implementazione statica degli adattatori. Una tipica applicazione di navigazione tra contenuti multimediali utilizza i dati di un database online o di un servizio web. Ad esempio di un'applicazione di navigazione che utilizza dati recuperati dal web, consulta Esempio di app Leanback di Google.

Aggiorna lo sfondo

Per aggiungere interesse visivo a un'app di navigazione multimediale sulla TV, puoi aggiornare lo sfondo immagine mentre gli utenti sfogliano i contenuti. Questa tecnica può migliorare l'interazione con la tua app cinematografici e godibili.

Il toolkit Leanback UI offre un BackgroundManager per cambiare lo sfondo della tua attività nell'app TV. L'esempio seguente mostra come crea un metodo semplice per aggiornare lo sfondo nell'attività dell'app TV:

Kotlin

protected fun updateBackground(drawable: Drawable) {
    BackgroundManager.getInstance(this).drawable = drawable
}

Java

protected void updateBackground(Drawable drawable) {
    BackgroundManager.getInstance(this).setDrawable(drawable);
}

Molte app di navigazione sui contenuti multimediali aggiornano automaticamente lo sfondo durante la navigazione dell'utente. tramite annunci multimediali. A tale scopo, puoi impostare un listener di selezione che aggiorna lo sfondo in base alla selezione corrente dell'utente. L'esempio seguente mostra come per configurare un corso OnItemViewSelectedListener per rileva gli eventi di selezione e aggiorna lo sfondo:

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();
            }
        }
    };
}

Nota: l'implementazione precedente è un semplice esempio ai fini della illustrazione. Quando crei questa funzione nella tua app, esegui l'azione di aggiornamento dello sfondo in un thread separato per migliorare le prestazioni. Inoltre, se prevedi di aggiornare lo sfondo in risposta agli utenti che scorrono gli elementi, aggiungere un tempo per ritardare l'aggiornamento di un'immagine di sfondo finché l'utente non sceglie un articolo. Questa tecnica evita l'aggiornamento eccessivo dell'immagine di sfondo.