Une application multimédia qui s'exécute sur un téléviseur doit permettre aux utilisateurs de parcourir son offre de contenus, de faire une sélection et de commencer à lire le contenu. L'expérience de navigation dans le contenu doit être simple et intuitive, ainsi que visuellement agréable et attrayante.
Ce guide explique comment utiliser les classes fournies par la bibliothèque androidx.leanback obsolète pour implémenter une interface utilisateur permettant de parcourir la musique ou les vidéos du catalogue multimédia de votre application.
Remarque : L'exemple d'implémentation présenté ici utilise BrowseSupportFragment
plutôt que la classe BrowseFragment
obsolète. BrowseSupportFragment
étend la classe Fragment
AndroidX, ce qui permet d'assurer un comportement cohérent sur les différents appareils et versions d'Android.

Figure 1 : Le fragment de navigation de l'application exemple Leanback affiche les données du catalogue vidéo.
Créer une mise en page de navigation multimédia
La classe BrowseSupportFragment
du kit d'UI Leanback vous permet de créer une mise en page principale pour parcourir les catégories et les lignes d'éléments multimédias avec un minimum de code. L'exemple suivant montre comment créer une mise en page contenant un objet 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'activité principale de l'application définit cette vue, comme illustré dans l'exemple suivant :
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); } ...
Les méthodes BrowseSupportFragment
remplissent la vue avec les données vidéo et les éléments d'interface utilisateur, et définissent les paramètres de mise en page tels que l'icône et le titre, et indiquent si les en-têtes de catégorie sont activés.
La sous-classe de l'application qui implémente les méthodes BrowseSupportFragment
configure également les écouteurs d'événements pour les actions de l'utilisateur sur les éléments d'interface utilisateur et prépare le gestionnaire d'arrière-plan, comme illustré dans l'exemple suivant :
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()); } ...
Définir des éléments d'UI
Dans l'exemple précédent, la méthode privée setupUIElements()
appelle plusieurs méthodes BrowseSupportFragment
pour styliser le navigateur du catalogue multimédia :
setBadgeDrawable()
place la ressource drawable spécifiée en haut à droite du fragment de navigation, comme illustré sur les figures 1 et 2. Cette méthode remplace la chaîne de titre par la ressource drawable, sisetTitle()
est également appelée. La ressource Drawable doit avoir une hauteur de 52 dp.setTitle()
définit la chaîne de titre en haut à droite du fragment de navigation, sauf sisetBadgeDrawable()
est appelé.setHeadersState()
etsetHeadersTransitionOnBackEnabled()
masquent ou désactivent les en-têtes. Pour en savoir plus, consultez la section Masquer ou désactiver les en-têtes.setBrandColor()
définit la couleur d'arrière-plan des éléments d'interface utilisateur dans le fragment de navigation, en particulier la couleur d'arrière-plan de la section d'en-tête, avec la valeur de couleur spécifiée.setSearchAffordanceColor()
définit la couleur de l'icône de recherche avec la valeur de couleur spécifiée. L'icône de recherche s'affiche en haut à gauche du fragment de navigation, comme illustré dans les figures 1 et 2.
Personnaliser les vues d'en-tête
Le fragment de navigation affiché sur la figure 1 présente les noms des catégories de vidéos, qui sont les en-têtes de ligne de la base de données vidéo, dans des vues de texte. Vous pouvez également personnaliser l'en-tête pour inclure des vues supplémentaires dans une mise en page plus complexe. Les sections suivantes montrent comment inclure une vue d'image qui affiche une icône à côté du nom de la catégorie, comme illustré sur la figure 2.

Figure 2. En-têtes de ligne dans le fragment de navigation avec une icône et un libellé textuel.
La mise en page de l'en-tête de ligne est définie comme suit :
<?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>
Utilisez un Presenter
et implémentez les méthodes abstraites pour créer, lier et dissocier le support de vue. L'exemple suivant montre comment associer le ViewHolder à deux vues, un ImageView
et un 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 } }
Vos en-têtes doivent être sélectionnables pour que le pavé directionnel puisse être utilisé pour les parcourir. Pour cela, deux méthodes sont possibles :
- Définissez votre vue comme pouvant être sélectionnée dans
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 // ... }
- Définissez votre mise en page comme pouvant être sélectionnée :
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" ... android:focusable="true">
Enfin, dans l'implémentation BrowseSupportFragment
qui affiche le navigateur de catalogue, utilisez la méthode setHeaderPresenterSelector()
pour définir le présentateur de l'en-tête de ligne, comme indiqué dans l'exemple suivant.
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(); } });
Pour obtenir un exemple complet, consultez l' application exemple Leanback.
Masquer ou désactiver les en-têtes
Il peut arriver que vous ne souhaitiez pas que les en-têtes de ligne s'affichent, par exemple lorsqu'il n'y a pas assez de catégories pour nécessiter une liste déroulante. Appelez la méthode BrowseSupportFragment.setHeadersState()
pendant la méthode onActivityCreated()
du fragment pour masquer ou désactiver les en-têtes de ligne. La méthode setHeadersState()
définit l'état initial des en-têtes dans le fragment de navigation, en fonction de l'une des constantes suivantes en tant que paramètre :
HEADERS_ENABLED
: lorsque l'activité du fragment de navigation est créée, les en-têtes sont activés et affichés par défaut. Les en-têtes s'affichent comme indiqué dans les figures 1 et 2 de cette page.HEADERS_HIDDEN
: lorsque l'activité du fragment de navigation est créée, les en-têtes sont activés et masqués par défaut. La section d'en-tête de l'écran est réduite, comme le montre une figure dans Fournir une vue de carte. L'utilisateur peut sélectionner la section d'en-tête réduite pour la développer.HEADERS_DISABLED
: lorsque l'activité du fragment de navigation est créée, les en-têtes sont désactivés par défaut et ne sont jamais affichés.
Si HEADERS_ENABLED
ou HEADERS_HIDDEN
sont définis, vous pouvez appeler setHeadersTransitionOnBackEnabled()
pour permettre de revenir à l'en-tête de ligne à partir d'un élément de contenu sélectionné dans la ligne. Cette option est activée par défaut si vous n'appelez pas la méthode. Pour gérer vous-même le mouvement "Retour", transmettez false
à setHeadersTransitionOnBackEnabled()
et implémentez votre propre gestion de la pile "Retour".
Afficher des listes de contenus multimédias
La classe BrowseSupportFragment
vous permet de définir et d'afficher des catégories de contenu multimédia consultables et des éléments multimédias d'un catalogue multimédia à l'aide d'adaptateurs et de présentateurs. Les adaptateurs vous permettent de vous connecter à des sources de données locales ou en ligne contenant les informations de votre catalogue multimédia.
Les adaptateurs utilisent des présentateurs pour créer des vues et associer des données à ces vues afin d'afficher un élément à l'écran.
L'exemple de code suivant montre une implémentation d'un Presenter
pour afficher des données de chaîne :
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 } }
Une fois que vous avez créé une classe de présentateur pour vos éléments multimédias, vous pouvez créer un adaptateur et l'associer à BrowseSupportFragment
pour afficher ces éléments à l'écran et permettre à l'utilisateur de les parcourir. L'exemple de code suivant montre comment créer un adaptateur pour afficher les catégories et les éléments de ces catégories à l'aide de la classe StringPresenter
présentée dans l'exemple de code précédent :
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); }
Cet exemple montre une implémentation statique des adaptateurs. Une application de navigation multimédia classique utilise les données d'une base de données en ligne ou d'un service Web. Pour obtenir un exemple d'application de navigation qui utilise des données récupérées sur le Web, consultez l' exemple d'application Leanback.
Modifier l'arrière-plan
Pour rendre une application de navigation multimédia sur téléviseur plus attrayante, vous pouvez modifier l'image de fond lorsque les utilisateurs parcourent le contenu. Cette technique peut rendre l'interaction avec votre application plus cinématographique et agréable.
La boîte à outils Leanback UI fournit une classe BackgroundManager
pour modifier l'arrière-plan de l'activité de votre application TV. L'exemple suivant montre comment créer une méthode simple pour mettre à jour l'arrière-plan dans l'activité de votre application TV :
Kotlin
protected fun updateBackground(drawable: Drawable) { BackgroundManager.getInstance(this).drawable = drawable }
Java
protected void updateBackground(Drawable drawable) { BackgroundManager.getInstance(this).setDrawable(drawable); }
De nombreuses applications de navigation multimédia mettent automatiquement à jour l'arrière-plan lorsque l'utilisateur parcourt les listes de contenus multimédias. Pour ce faire, vous pouvez configurer un écouteur de sélection afin de mettre à jour automatiquement l'arrière-plan en fonction de la sélection actuelle de l'utilisateur. L'exemple suivant montre comment configurer une classe OnItemViewSelectedListener
pour capturer les événements de sélection et mettre à jour l'arrière-plan :
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(); } } }; }
Remarque : L'implémentation précédente est un exemple simple à des fins d'illustration. Lorsque vous créez cette fonction dans votre propre application, exécutez l'action de mise à jour en arrière-plan dans un thread distinct pour de meilleures performances. De plus, si vous prévoyez de mettre à jour l'arrière-plan en réponse au défilement des éléments par les utilisateurs, ajoutez un délai avant la mise à jour de l'image d'arrière-plan jusqu'à ce que l'utilisateur s'arrête sur un élément. Cette technique évite les mises à jour excessives des images d'arrière-plan.