Créer un navigateur de catalogue

Créez mieux avec Compose
Créez des interfaces utilisateur attrayantes avec un minimum de code à l'aide de Jetpack Compose pour l'OS Android TV.
<ph type="x-smartling-placeholder"></ph> Compose pour la télévision →

Une application multimédia exécutée sur un téléviseur doit permettre aux utilisateurs de parcourir ses offres de contenu, d'effectuer une sélection et lancer la lecture du contenu. Expérience de navigation de contenu doivent être simples et intuitifs, et visuellement agréables et engageants.

Ce guide explique comment utiliser les classes fournies par la bibliothèque androidx.leanback pour implémenter une interface utilisateur permettant de parcourir de la musique ou des vidéos du catalogue multimédia de votre application.

Remarque:L'exemple d'implémentation présenté ici utilise BrowseSupportFragment plutôt que la version obsolète BrowseFragment . BrowseSupportFragment étend l'AndroidX. Fragment, ce qui contribue à garantir un comportement cohérent entre les appareils et les versions d'Android.

Écran principal de l&#39;application

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 pour la navigation multimédia

BrowseSupportFragment du kit d'interface utilisateur 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 une le moins de code possible. 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 renseignent la vue avec la valeur des données vidéo et des éléments d'interface utilisateur, et définissez des paramètres de mise en page tels que l'icône et le titre, si les en-têtes de catégorie sont activés.

Pour en savoir plus sur la configuration des éléments d'interface utilisateur, consultez la section Définir l'interface utilisateur éléments. Pour savoir comment masquer les en-têtes, consultez la Section Masquer ou désactiver les en-têtes.

Sous-classe de l'application qui implémente BrowseSupportFragment configure également des écouteurs d'événements pour les actions des utilisateurs sur les éléments de l'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 les éléments d'interface utilisateur

Dans l'exemple précédent, la méthode privée setupUIElements() appelle plusieurs BrowseSupportFragment pour appliquer un style au navigateur Media Catalog:

  • setBadgeDrawable() place la ressource drawable spécifiée dans l'angle supérieur droit du fragment de navigation, comme comme illustré dans les figures 1 et 2. Cette méthode remplace la chaîne de titre par le caractère ressource drawable, si setTitle() est également appelé. La ressource drawable doit faire 52 dp. grand.
  • setTitle() définit la chaîne de titre dans le coin supérieur droit du fragment de navigation, sauf si setBadgeDrawable() est appelé.
  • setHeadersState() et setHeadersTransitionOnBackEnabled() 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 de l'interface utilisateur dans le fragment de navigation, en particulier l'en-tête. couleur d'arrière-plan de la section, 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. Icône de recherche apparaît dans l'angle supérieur gauche du fragment de navigation, comme illustré dans les figures 1 et 2.

Personnaliser les vues d'en-tête

Le fragment de navigation illustré à la figure 1 affiche les noms des catégories de vidéos, qui sont les en-têtes de ligne dans la base de données vidéo, dans les vues de texte. Vous pouvez également personnaliser pour inclure des vues supplémentaires dans une mise en page plus complexe. Les sections suivantes expliquent comment inclure une vue d'image qui affiche une icône à côté du nom de la catégorie, comme illustré dans la figure 2.

Écran principal de l&#39;application

Figure 2. Les en-têtes de ligne du fragment de navigation accompagnés d'une icône et une étiquette de texte.

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 la des méthodes abstraites pour créer, lier et dissocier le conteneur de vues. Les éléments suivants : exemple montre comment lier le ViewHolder à deux vues, un ImageView et 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 et les faire défiler. Il existe deux façons de gérer cela:

  • Configurez la vue pour qu'elle soit sélectionnable 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 pour qu'elle soit sélectionnable:
    <?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 navigateur de catalogue, utilisez setHeaderPresenterSelector() pour définir le présentateur pour l'en-tête de ligne, comme illustré 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 la <ph type="x-smartling-placeholder"></ph> Exemple d'application Leanback pour en savoir plus.

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 lorsque les en-têtes sont insuffisants les catégories pour nécessiter une liste déroulante. Appelez la méthode BrowseSupportFragment.setHeadersState(). pendant l'exécution de la méthode onActivityCreated() du fragment pour masquer ou désactiver les en-têtes de ligne. setHeadersState() définit l'état initial des en-têtes dans le fragment de navigation, en fonction de l'un des éléments suivants : constantes 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 par défaut. Les en-têtes apparaissent comme illustré 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 illustré dans la section . une figure dans Fournir une vue Fiche. La 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 sont jamais affichées.

Si HEADERS_ENABLED ou HEADERS_HIDDEN est défini, vous pouvez appeler setHeadersTransitionOnBackEnabled() pour permettre de revenir à l'en-tête de ligne à partir d'un élément de contenu sélectionné sur la ligne. Ceci est activé par "default" si vous n'appelez pas cette méthode. Pour gérer vous-même le mouvement du dos, transmettre false à setHeadersTransitionOnBackEnabled() et implémenter votre propre gestion de la pile "Retour".

Listes de contenus multimédias display

BrowseSupportFragment vous permet définir et afficher des catégories de contenu et des éléments multimédias consultables depuis un catalogue multimédia à l'aide d'adaptateurs et de présentateurs. Des adaptateurs pour vous connecter aux 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 lier des données à ces vues pour affichant 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 le connecter au BrowseSupportFragment pour afficher ces éléments à l'écran pour permettre à l'utilisateur de le parcourir. L'exemple suivant Le code 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 indiquée dans le 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 présente une implémentation statique des adaptateurs. Application de navigation multimédia classique utilise les données d'une base de données en ligne ou d'un service web. Pour un exemple d'application de navigation utilise des données récupérées sur le Web, consultez la <ph type="x-smartling-placeholder"></ph> Exemple d'application Leanback pour en savoir plus.

Modifier l'arrière-plan

Pour ajouter un intérêt visuel à une appli de navigation multimédia sur un téléviseur, vous pouvez modifier l'arrière-plan image lorsque les utilisateurs parcourent le contenu. Cette technique peut faciliter l'interaction avec votre application cinématique et agréable.

Le kit d'interface utilisateur Leanback fournit un BackgroundManager permettant de modifier l'arrière-plan de votre activité dans les applications TV. L'exemple suivant montre comment créez une méthode simple pour mettre à jour l'arrière-plan dans l'activité de vos applications 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 à jour automatiquement l'arrière-plan à mesure que l'utilisateur parcourt la page. par le biais de fiches média. Pour ce faire, vous pouvez configurer un écouteur de sélection modifier l'arrière-plan en fonction de la sélection actuelle de l'utilisateur. L'exemple suivant montre comment pour configurer une classe OnItemViewSelectedListener interceptez les événements de sélection et mettez à 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 visant à illustration. Lorsque vous créez cette fonction dans votre propre application, exécutez la 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 aux utilisateurs qui font défiler les éléments, ajoutez un laps de temps pour retarder la mise à jour d'une image de fond jusqu'à ce que l'utilisateur s'accorde sur un élément. Cette technique permet d'éviter Nombre excessif de mises à jour des images de fond