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](https://developer.android.google.cn/static/images/tv/app-browse.png?authuser=0000&hl=it)
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.
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 anchesetTitle()
. 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 chesetBadgeDrawable()
ha chiamato.setHeadersState()
esetHeadersTransitionOnBackEnabled()
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'app](https://developer.android.google.cn/static/images/tv/custom-head.png?authuser=0000&hl=it)
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.