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, effettuare una selezione e iniziare a riprodurre i contenuti. L'esperienza di navigazione dei contenuti deve essere semplice e intuitiva, oltre che visivamente piacevole e coinvolgente.

Questa guida illustra come utilizzare i corsi forniti dalla raccolta 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é la classe BrowseFragment deprecata. BrowseSupportFragment estende la classe AndroidX Fragment, 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

La classe BrowseSupportFragment nel toolkit Leanback UI ti consente di creare un layout principale per sfogliare le categorie e le righe di elementi multimediali con un minimo di codice. L'esempio seguente mostra come creare un layout che contiene 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 popolano la visualizzazione con i dati video e gli elementi UI, oltre a impostare parametri di layout, come l'icona e il titolo, e se le intestazioni delle categorie sono attivate.

Per ulteriori informazioni sulla configurazione degli elementi UI, consulta la sezione Impostare gli elementi UI. Per ulteriori informazioni su come nascondere le intestazioni, consulta la sezione Nascondere o disabilitare le intestazioni.

La sottoclasse dell'applicazione che implementa i metodi BrowseSupportFragment imposta anche listener di eventi per le azioni utente sugli elementi UI e prepara il 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 diversi metodi BrowseSupportFragment per applicare uno stile al browser del catalogo multimediale:

  • setBadgeDrawable() posiziona la risorsa disegnabile 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 la risorsa drawable, se viene chiamato anche setTitle(). La risorsa drawable deve avere un'altezza di 52 dp.
  • setTitle() imposta la stringa del titolo nell'angolo in alto a destra del frammento di navigazione, a meno che non venga chiamato setBadgeDrawable().
  • 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 di navigazione, in particolare il colore di sfondo della sezione dell'intestazione, con il valore di colore specificato.
  • setSearchAffordanceColor() imposta il colore dell'icona di ricerca con il valore colore specificato. L'icona di ricerca viene visualizzata nell'angolo in alto a sinistra 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 dei video, nelle visualizzazioni testo. Puoi anche personalizzare l'intestazione per includere altre visualizzazioni in un layout più complesso. Le seguenti sezioni spiegano come includere 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 un Presenter e implementa i metodi astratti per creare, associare e slegare il titolare della vista. L'esempio seguente mostra come associare il visualizzatore a due viste, una ImageView e una 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 utilizzare il D-pad per 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 il browser del catalogo, usa il metodo 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 App di esempio Leanback .

Nascondi o disattiva le intestazioni

A volte potresti non volere che le intestazioni di riga vengano visualizzate, ad esempio quando non ci sono abbastanza categorie per richiedere un elenco scorrevole. Chiama il metodo BrowseSupportFragment.setHeadersState() durante il metodo onActivityCreated() del frammento per nascondere o disabilitare le intestazioni di riga. Il metodo setHeadersState() imposta lo stato iniziale delle intestazioni nel frammento di navigazione, in base a una delle seguenti costanti come parametro:

  • HEADERS_ENABLED: quando viene creata l'attività dei frammenti di navigazione, le intestazioni sono abilitate e mostrate per impostazione predefinita. 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'immagine in Fornire una visualizzazione schede. 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 e non vengono mai visualizzate.

Se è impostata l'opzione HEADERS_ENABLED o HEADERS_HIDDEN, puoi richiamare setHeadersTransitionOnBackEnabled() per supportare il ritorno all'intestazione della riga da un contenuto selezionato nella riga. Questa opzione è abilitata per impostazione predefinita se non chiami il metodo. Per gestire autonomamente il movimento indietro, passa false a setHeadersTransitionOnBackEnabled() e implementa la gestione personalizzata dello stack posteriore.

Elenchi di contenuti multimediali display

La classe BrowseSupportFragment consente di definire e visualizzare categorie di contenuti multimediali sfogliabili ed elementi multimediali di un catalogo multimediale utilizzando adattatori e presentatori. Gli adattatori ti consentono di connetterti a origini dati locali o online contenenti le informazioni del catalogo multimediale. Gli adattatori utilizzano i presentatori per creare visualizzazioni e associare i dati a queste viste per la visualizzazione di 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 presentatore per gli elementi multimediali, puoi creare un adattatore e collegarlo a BrowseSupportFragment per visualizzare questi elementi sullo schermo e consentire all'utente di consultarli. Il codice di esempio seguente mostra come costruire un adattatore per visualizzare categorie ed elementi in queste categorie utilizzando la classe StringPresenter mostrata nell'esempio di codice precedente:

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. Per un esempio di applicazione di navigazione che utilizza dati recuperati dal web, vedi l'app di esempio Leanback.

Aggiorna lo sfondo

Per suscitare interesse visivo per un'app di navigazione multimediale sulla TV, puoi aggiornare l'immagine di sfondo mentre gli utenti sfogliano i contenuti. Questa tecnica può rendere l'interazione con la tua app più cinematografica e piacevole.

Il toolkit per la UI di Leanback fornisce una classe BackgroundManager per modificare lo sfondo dell'attività nelle app TV. L'esempio seguente mostra come creare 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 nei contenuti multimediali aggiornano automaticamente lo sfondo mentre l'utente naviga tra gli elenchi dei contenuti multimediali. A tale scopo, puoi configurare un listener di selezione che aggiorni automaticamente lo sfondo in base alla selezione corrente dell'utente. L'esempio seguente mostra come configurare una classe OnItemViewSelectedListener per rilevare gli eventi di selezione e aggiornare 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 a scopo illustrativo. Quando crei questa funzione nella tua app, esegui l'azione di aggiornamento in background in un thread separato per migliorare le prestazioni. Inoltre, se prevedi di aggiornare lo sfondo in risposta agli utenti che scorrono gli elementi, aggiungi un tempo per ritardare l'aggiornamento dell'immagine di sfondo finché l'utente non sceglie un elemento. Questa tecnica evita eccessivi aggiornamenti dell'immagine di sfondo.