Un'app multimediale eseguita su una TV deve consentire agli utenti di sfogliare le offerte di contenuti, effettuare una selezione e iniziare a riprodurre i contenuti. L'esperienza di navigazione dei contenuti deve essere semplice e intuitiva, nonché visivamente piacevole e coinvolgente.
Questa guida illustra come utilizzare i corsi forniti dalla libreria Androidx di Leanback per implementare un'interfaccia utente per sfogliare musica o 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 tra i dispositivi e le versioni di Android.
![Schermata principale dell'app](https://developer.android.com/static/images/tv/app-browse.png?authuser=8&hl=it)
Figura 1. Il frammento di navigazione dell'app di esempio Leanback mostra i dati del catalogo video.
Creare un layout di navigazione multimediale
La classe BrowseSupportFragment
nella libreria Leanback
consente di creare un layout principale per sfogliare categorie e 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
completano la visualizzazione con i
dati del video e gli elementi UI e impostano parametri di layout come l'icona e il titolo e
se le intestazioni delle categorie sono abilitate.
La sottoclasse dell'applicazione che implementa i metodi BrowseSupportFragment
configura anche i listener di eventi per le azioni degli utenti 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
BrowseSupportFragment
metodi per definire lo stile del browser del catalogo multimediale:
setBadgeDrawable()
posiziona la risorsa disegnabile specificata nell'angolo in alto a destra del frammento di consultazione, come mostrato nelle figure 1 e 2. Questo metodo sostituisce la stringa del titolo con la risorsa disegnabile, se viene chiamato anchesetTitle()
. La risorsa disegnabile deve avere un'altezza di 52 dp.setTitle()
imposta la stringa del titolo nell'angolo in alto a destra del frammento di consultazione, a meno che non venga chiamatosetBadgeDrawable()
.setHeadersState()
esetHeadersTransitionOnBackEnabled()
nascondono o disattivano le intestazioni. Consulta la sezione Nascondere o disattivare le intestazioni per ulteriori informazioni.setBrandColor()
imposta il colore di sfondo per gli elementi UI nel frammento di consultazione, in particolare il colore di sfondo della sezione dell'intestazione, con il valore del colore specificato.setSearchAffordanceColor()
imposta il colore dell'icona di ricerca con il valore del colore specificato. L'icona di ricerca viene visualizzata nell'angolo in alto a sinistra del frammento di consultazione, come mostrato nelle figure 1 e 2.
Personalizza le visualizzazioni delle intestazioni
Il frammento di consultazione mostrato nella figura 1 mostra i nomi delle categorie di video, ovvero le intestazioni di riga del database video, nelle visualizzazioni di testo. Puoi anche personalizzare l'intestazione per includere visualizzazioni aggiuntive in un layout più complesso. Le seguenti sezioni mostrano come includere una visualizzazione immagine che mostri un'icona accanto al nome della categoria, come mostrato nella Figura 2.
![Schermata principale dell'app](https://developer.android.com/static/images/tv/custom-head.png?authuser=8&hl=it)
Figura 2. Le intestazioni di riga nel frammento di consultazione con un'icona e un'etichetta di testo.
Il layout dell'intestazione di 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 oggetto Presenter
e implementa i
metodi astratti per creare, associare e svincolare il titolare della visualizzazione. L'esempio
seguente mostra come associare il visualizzatore a due viste, 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 utilizzare il D-pad per scorrerle. Puoi gestire questa opzione in due modi:
- Imposta la visualizzazione in modo che sia attivabile in
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 per renderlo 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, utilizza il metodo setHeaderPresenterSelector()
per impostare il presentatore per l'intestazione di 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, consulta l'app di esempio Leanback .
Nascondi o disabilita le intestazioni
A volte potresti voler evitare la visualizzazione delle intestazioni di riga, ad esempio quando non ci sono abbastanza categorie da richiedere un elenco scorrevole. Richiama 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 consultazione, in base a una delle seguenti costanti come parametro:
HEADERS_ENABLED
: quando viene creata l'attività del frammento di esplorazione, le intestazioni vengono abilitate e mostrate per impostazione predefinita. Le intestazioni vengono visualizzate come mostrato nelle figure 1 e 2 di questa pagina.HEADERS_HIDDEN
: quando viene creata l'attività del frammento di esplorazione, le intestazioni sono abilitate e nascoste per impostazione predefinita. La sezione dell'intestazione dello schermo è compressa, come mostrato in una figura in Fornire una visualizzazione schede. L'utente può selezionare la sezione di intestazione compressa per espanderla.HEADERS_DISABLED
: quando viene creata l'attività di esplorazione dei frammenti, le intestazioni sono disattivate per impostazione predefinita e non vengono mai visualizzate.
Se hai impostato HEADERS_ENABLED
o HEADERS_HIDDEN
, puoi chiamare
setHeadersTransitionOnBackEnabled()
per supportare il ritorno all'intestazione di riga da un elemento di contenuti selezionato nella riga. Questo è abilitato per impostazione predefinita se non chiami il metodo. Per gestire personalmente il movimento dorso,
passa false
a setHeadersTransitionOnBackEnabled()
e implementa la tua gestione dello stack posteriore.
Visualizza elenchi di contenuti multimediali
La classe BrowseSupportFragment
consente di definire e visualizzare categorie di contenuti multimediali sfogliabili ed elementi multimediali da un catalogo di contenuti multimediali utilizzando adattatori e presentatori. Gli adattatori ti consentono di connetterti a origini dati locali o online contenenti le informazioni del tuo catalogo multimediale.
Gli adattatori utilizzano i presentatori per creare visualizzazioni e associare i dati a queste visualizzazioni per
mostrare un elemento sullo schermo.
Il seguente codice di esempio mostra un'implementazione di un Presenter
per la visualizzazione dei 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 i tuoi elementi multimediali, puoi creare un adattatore e collegarlo a BrowseSupportFragment
per visualizzare questi elementi sullo schermo e consentire all'utente di sfogliare l'elenco. Il seguente codice di esempio mostra come creare un adattatore per visualizzare categorie ed elementi in quelle 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 multimediale 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.
Aggiornare lo sfondo
Per aggiungere interesse visivo a 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.
La libreria di supporto Leanback offre una lezione 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à delle 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 multimediale aggiornano automaticamente lo sfondo mentre l'utente naviga tra le schede di contenuti multimediali. A tale scopo, puoi impostare 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 i vari elementi, aggiungi un tempo per ritardare l'aggiornamento dell'immagine di sfondo finché l'utente non si accontenta di un articolo. Questa tecnica evita eccessivi aggiornamenti delle immagini di sfondo.