Personnaliser la mise en page de navigation

Exemple d'animation montrant une mise en page de navigation personnalisée sur Android

Utilisez la personnalisation de la mise en page pour contrôler la hiérarchie des vues et le positionnement à l'écran des vues fournies par Google et de vos propres vues d'entreprise personnalisées.

Au lieu d'ajouter des vues personnalisées dans des emplacements d'en-tête ou de pied de page prédéfinis, implémentez un délégué de mise en page pour gérer la mise en page de l'ensemble de l'écran. Lors des transitions d'état de navigation, votre délégué de mise en page reçoit les composants fournis par Google, tels que la fiche d'itinéraire, la carte ETA et les boutons. Positionnez ces éléments à côté de votre propre contenu personnalisé à l'aide de systèmes de mise en page Android standards tels que ConstraintLayout, CoordinatorLayout ou LinearLayout.

Utilisez ce framework pour positionner précisément les informations commerciales personnalisées (comme l'état d'une commande ou les instructions de retrait) à l'écran tout en empêchant vos vues de se chevaucher.

Fonctionnement de la personnalisation de la mise en page

La personnalisation de la mise en page utilise un modèle de conception de délégation. Au lieu de dessiner ou de positionner automatiquement les composants de l'interface utilisateur à l'écran, le SDK les transmet directement à une classe personnalisée que vous écrivez : le délégué de mise en page.

Pour personnaliser votre mise en page, créez une classe qui étend la classe abstraite NavigationLayoutDelegate et attribuez une instance à votre NavigationView ou SupportNavigationFragment. Chaque fois que l'état de navigation change (par exemple, lorsque vous passez de la carte de base à la navigation détaillée active), le SDK exécute les méthodes de rappel sur votre délégué et fournit les composants d'interface utilisateur disponibles pour cet état spécifique.

Pour offrir une expérience utilisateur fluide et intégrée, ce framework établit une séparation claire des responsabilités entre votre application et le SDK. Utilisez le délégué de mise en page pour personnaliser les éléments suivants :

  • Créer la hiérarchie des vues : choisissez exactement les composants Google et les vues d'entreprise personnalisées à ajouter à l'écran pour chaque état de l'interface utilisateur.
  • Positionner chaque élément : définissez des ancres d'écran, des marges et un positionnement précis de la mise en page. Évitez d'appliquer des contraintes de largeur ou de hauteur personnalisées aux composants Google, car ils calculent leurs propres dimensions internes.
  • Encadrer la carte de base : définissez les limites visibles de la caméra de la carte à l'aide des coordonnées du composant de la fenêtre d'affichage.
  • Superposer l'écran : déterminez si les vues personnalisées flottent au-dessus, en dessous ou à côté des commandes intégrées de Google.

En attendant, vous ne pouvez pas personnaliser les composants Google suivants avec le délégué de mise en page :

  • Dimensions des composants : taille et dimensions internes des composants fournis par Google, que le SDK calcule automatiquement.
  • Conditions de déclenchement : lorsque des alertes ou des invites dynamiques s'affichent en fonction des données d'itinéraire en temps réel.

Principes d'implémentation

Lorsque vous écrivez un délégué de mise en page, gardez à l'esprit les règles suivantes pour éviter les bugs de mise en page ou les plantages d'exécution :

  • L'écran par défaut est vide : les composants fournis par Google ne s'affichent que si votre délégué les ajoute explicitement à la hiérarchie des vues et les positionne.
  • Les API Legacy ne sont pas compatibles : les propriétés et les méthodes qui contrôlent l' ancien modèle de mise en page basé sur des emplacements ne sont pas compatibles et peuvent ne pas se comporter comme prévu lorsqu'un délégué personnalisé est actif.
  • Ne modifiez pas les structures de vue internes n'utilisez pas de méthodes telles que findViewById() pour parcourir ou modifier les hiérarchies de vues des composants fournis par Google (tels que la fiche d'itinéraire ou la carte ETA). Étant donné que ces hiérarchies de vues internes sont des détails d'implémentation sous-jacents, elles sont susceptibles d'être modifiées dans les différentes versions du SDK. Si vous les modifiez, votre mise en page risque de ne plus fonctionner lors des futures mises à jour du SDK.

Compatibilité avec l'API Legacy

Pour un comportement de mise en page fiable lorsque vous utilisez un délégué de mise en page personnalisé, évitez d'utiliser les API Legacy basées sur des emplacements suivantes, qui devraient être abandonnées. Pour migrer une application existante, remplacez votre utilisation de ces API par du code dans votre délégué de mise en page personnalisé :

Afficher les API Legacy basées sur des emplacements

API Legacy Remplacement du délégué de mise en page
setCustomControl(View, CustomControlPosition) Ajoutez vos vues directement à votre ConstraintLayout ou à un autre groupe de vues.
removeCustomControl(View) Supprimez les vues directement de votre hiérarchie de vues.
setEtaCardEnabled(boolean) Lisez la vue etaCard dans onEnterActiveGuidance.
setHeaderEnabled(boolean) Lisez la vue turnCard dans onEnterActiveGuidance.
setReportIncidentButtonEnabled(boolean) Recherchez le bouton REPORTING dans getActiveGuidanceButtons().
setTripProgressBarEnabled(boolean) Lisez la vue tripProgressBar dans onEnterActiveGuidance.
addOnNavigationUiChangedListener(...) Fiez-vous aux rappels de transition d'état de votre NavigationLayoutDelegate.
removeOnNavigationUiChangedListener(...) Gérez directement les transitions d'état de la mise en page à l'aide du délégué.
addPromptVisibilityChangedListener(...) Fiez-vous aux rappels d'invite de votre NavigationLayoutDelegate (comme onShowPrompt()).
removePromptVisibilityChangedListener(...) Gérez directement la logique d'affichage des invites à l'aide du délégué.
setCompassEnabled(boolean) Recherchez le compas dans getNavigationReadyButtons() ou getActiveGuidanceButtons().

Checklist des éléments essentiels

Suivez ces étapes et exigences essentielles pour implémenter correctement votre délégué de mise en page :

  • Initialisez le délégué avant de créer l'interface utilisateur : appelez setLayoutDelegate() avant que le SDK n'initialise l'interface utilisateur de navigation. Consultez les exemples d'implémentation de code suivants pour vérifier le timing exact de la configuration de la structure de votre application. L'initialisation du délégué après la création de l'interface utilisateur déclenche une ApiIllegalStateException.

    Kotlin

    // For SupportNavigationFragment
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val navFragment = supportFragmentManager.findFragmentById(R.id.nav_fragment) as SupportNavigationFragment
        navFragment.setLayoutDelegate(MyLayoutDelegate())
    }
    
    // For a programmatic NavigationView
    val navigationView = NavigationView(context)
    navigationView.setLayoutDelegate(MyLayoutDelegate())
    navigationView.onCreate(savedInstanceState)
            

    Java

    // For SupportNavigationFragment
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        SupportNavigationFragment navFragment = (SupportNavigationFragment) getSupportFragmentManager().findFragmentById(R.id.nav_fragment);
        navFragment.setLayoutDelegate(new MyLayoutDelegate());
    }
    
    // For a programmatic NavigationView
    NavigationView navigationView = new NavigationView(context);
    navigationView.setLayoutDelegate(new MyLayoutDelegate());
    navigationView.onCreate(savedInstanceState);
            
  • Créez votre propre conteneur de mise en page : créez un groupe de vues personnalisé, tel qu'un ConstraintLayout, pour contenir vos éléments d'interface utilisateur personnalisés et les vues fournies par Google.

  • Associez des vues obligatoires : vous devez ajouter le logo Google (getGoogleLogo()) et la fenêtre d'affichage (getViewport()) à votre conteneur de mise en page dans tous les états de navigation. Si vous n'incluez pas ces deux vues , une ApiIllegalStateException d'exécution est déclenchée et l'application plante.

  • Respectez les dimensions fixes : le SDK détermine les dimensions de plusieurs composants fournis par Google. N'appliquez pas de contraintes de largeur ou de hauteur personnalisées aux éléments suivants :

    • Fiche d'itinéraire
    • Carte ETA
    • Invites entrantes
    • Logo Google
    • Widget de vitesse
  • Associez votre mise en page à la vue : appelez navigationView.setNavigationLayout() dans vos rappels onEnterNavigationReady() et onEnterActiveGuidance() pour associer votre conteneur à la hiérarchie des vues de la carte.

  • Nettoyez à la sortie de l'état : appelez navigationView.removeNavigationLayout() et supprimez vos vues personnalisées dans vos rappels onLeave pour éviter les fuites de mémoire et les états d'interface utilisateur qui se chevauchent.

  • Évitez les API de mise en page Legacy : n'appelez pas d'API basées sur des emplacements obsolètes telles que setCustomControl() ou setHeaderEnabled() lorsqu'un délégué personnalisé est actif. Le SDK peut ignorer ces appels Legacy lorsqu'un délégué personnalisé est associé.

États de l'interface utilisateur et composants Google

Lorsqu'une transition d'état de navigation se produit, le SDK transmet un objet UiState en lecture seule à votre rappel de délégué. Cet objet regroupe les indicateurs de configuration de mise en page actuels avec les composants Google (par exemple, les instances View) dont vous avez besoin pour afficher l'écran.

Le délégué de mise en page gère la hiérarchie des vues dans quatre états opérationnels. Le diagramme d'état suivant illustre la façon dont le SDK passe d'un état de navigation à un autre et les méthodes de rappel qu'il exécute sur votre délégué :

Diagramme de transition de l'état du cycle de vie NavigationLayoutDelegate montrant les transitions entre les états "Non-Navigation", "Navigation Ready" et "Active Guidance".

Les composants Google spécifiques fournis à votre délégué dépendent de la phase actuelle du cycle de vie de la navigation.

Composants obligatoires (tous les états de navigation)

Quelle que soit la phase de navigation actuelle, vous devez inclure, positionner et maintenir les composants Google suivants visibles dans votre hiérarchie de vues :

  • Logo Google (getGoogleLogo()) : ce composant affiche le logo Google Maps obligatoire. Si le bouton Recentrer est activé, le logo se transforme automatiquement en ce bouton lorsque le conducteur fait défiler la carte loin de son véhicule. Par conséquent, l'emplacement du logo détermine l'endroit où le bouton Recentrer s'affiche. Il est recommandé de placer le logo dans l'angle inférieur gauche de votre mise en page pour correspondre aux attentes standards de l'interface utilisateur de la carte.

  • Fenêtre d'affichage (getViewport()): View invisible qui définit les limites de cadrage de la caméra. L'emplacement de la fenêtre d'affichage détermine précisément l'endroit où le SDK centre le chevron du véhicule et dessine la ligne d'itinéraire active. Positionnez la fenêtre d'affichage de sorte qu'elle couvre la zone ouverte et non masquée de votre écran, en évitant les superpositions opaques telles que les feuilles de calcul personnalisées en bas de l'écran.

Pendant l'état prêt pour la navigation, l'interface utilisateur reste minimale pour se concentrer sur la carte de base. Votre objet NavigationReadyUiState donne accès aux éléments suivants :

  • getNavigationReadyButtons() : liste des vues d'action flottantes configurées pour la carte de base (généralement, seul le bouton Compas). Étant donné que Google peut ajouter ou réorganiser des boutons dans les futures versions du SDK, évitez de vous fier à des index de liste fixes. Parcourez plutôt la liste, lisez le type de chaque bouton en comparant getType() à ButtonKnownType.COMPASS, puis extrayez la vue physique en appelant getView(). Notez que les boutons exacts de cette liste diffèrent de ceux disponibles lors des instructions actives.

Composants d'instructions actives

Lorsque la navigation détaillée commence, le SDK déverrouille l'ensemble complet des commandes de navigation. Votre objet ActiveGuidanceUiState donne accès aux composants Google suivants :

  • getTurnCard() : bannière d'en-tête principale affichant les prochaines directions de manœuvre, les mesures de distance et l'assistant de voies de circulation. Placez-la en haut de votre mise en page pour établir une hiérarchie de navigation familière et ancrer vos vues personnalisées autour d'elle.

  • getEtaCard() : bannière de pied de page affichant l'heure d'arrivée estimée, le temps de trajet restant et la distance restante jusqu'à la destination. Placez-la le long du bord inférieur de votre écran ou intégrez ses coordonnées à vos feuilles de calcul personnalisées de gestion des tâches.

  • getTripProgressBar() : barre de progression verticale qui indique au conducteur la distance parcourue sur l'itinéraire actuel. Contrairement aux mises en page Legacy qui fixaient cette barre strictement au bord de la carte, vous avez toute liberté de l'ancrer n'importe où, par exemple le long d'un bord latéral de votre conteneur personnalisé.

  • getSpeedWidget() : commande flottante pour le compteur de vitesse et les limites de vitesse indiquées. En fonction des paramètres de votre API et de la disponibilité des données, cette vue est redimensionnée dynamiquement entre quatre états visuels au moment de l'exécution (n'affiche rien, uniquement la vitesse actuelle, uniquement la limite de vitesse ou les deux lectures). Étant donné que le widget peut passer d'une taille à l'autre sans préavis, ancrez toujours les vues environnantes à l'aide de contraintes afin que votre mise en page s'adapte automatiquement pour éviter les chevauchements spatiaux.

  • getActiveGuidanceButtons() : liste étendue des vues d'action flottantes pour l'état d'instructions actives (incluant généralement le bouton Compas et le bouton Signaler un incident). Comme dans l'état prêt pour la navigation, vous pouvez localiser et extraire des vues de boutons individuelles en filtrant la liste par ButtonKnownType (COMPASS ou REPORTING) et en extrayant la vue à l'aide de getView(). Vous pouvez ensuite les positionner indépendamment ou utiliser une mise en page telle que AutoHidingLinearLayout pour empiler le tableau en toute sécurité sans conflit spatial.

Composants d'invite dynamique

Les invites, telles que les alertes d'incident ou les avertissements de caméra de sécurité, se déclenchent indépendamment pendant les instructions actives.

Lorsqu'une invite est prête à être affichée, le SDK appelle le rappel onShowPrompt() de votre délégué et transmet une vue newPrompt. Votre délégué est chargé de positionner cette invite en douceur sur votre mise en page (généralement ancrée au bord inférieur de votre conteneur de carte).

Étant donné que l'invite entrante couvre la partie inférieure de l'écran, vous devez mettre à jour votre mise en page pour éviter qu'elle ne chevauche la fenêtre d'affichage, le logo Google ou tout bouton aligné en bas.

Gérer la taille de l'écran et le mode élargi

Pour gérer les modifications des dimensions de la carte et de l'orientation de l'écran, le délégué de mise en page utilise les fonctionnalités suivantes :

  • Redimensionnement de la vue : ajuste votre mise en page chaque fois que la taille physique de la carte change.

  • Mode élargi : passe à une variante de mise en page au format large lorsque la carte est suffisamment large.

Répondre au redimensionnement de la vue

Chaque fois que les dimensions physiques du conteneur de carte changent, le SDK exécute le rappel onSizeChanged(). Les mises en page en écran partagé, les curseurs de mise en page et la rotation de l'appareil déclenchent généralement le rappel onSizeChanged(). Utilisez ce rappel pour apporter des ajustements réactifs généraux à votre interface utilisateur personnalisée. Implémentez onSizeChanged() pour repositionner les éléments personnalisés pour un nouveau format d'écran, appliquer vos propres points d'arrêt de largeur ou de hauteur personnalisés, et détecter quand un événement de redimensionnement active le mode élargi pour échanger en toute sécurité les variantes de mise en page.

Comprendre le mode élargi

Le mode élargi s'active lorsque le conteneur de carte est suffisamment large pour afficher les composants de l'interface utilisateur côte à côte.

Lisez le booléen isWideMode() à partir de l'objet d'état pour repositionner vos éléments d'interface utilisateur personnalisés et laisser le centre de la carte libre pour le conducteur. Étant donné que les composants fournis par Google, tels que la fiche d'itinéraire et la carte ETA, rétrécissent et se remodèlent automatiquement en mode élargi, la lecture de ce booléen garantit que votre mise en page s'adapte au même moment que la mise à jour des composants Google.

Voici quelques exemples de positionnement des composants de votre mise en page en mode standard et en mode élargi :

  • Mode Portrait standard : placez la fiche d'itinéraire en haut de l'écran et la carte ETA en bas.

  • Mode élargi : déplacez la fiche d'itinéraire vers le début de l'écran et la carte ETA vers la fin.

Vérifier le mode élargi lors des transitions d'état

Si votre mise en page est compatible avec une variante en mode élargi, évaluez le booléen isWideMode() dans chaque rappel de transition d'état, plutôt que de vous fier uniquement à onSizeChanged().

Vérifiez l'objet d'état lors des rappels tels que onEnterNavigationReady() et onEnterActiveGuidance() pour gérer les lancements initiaux de l'application en mode paysage. Cette approche protège votre mise en page contre les événements de cycle de vie du système Android, tels que la recréation d'activité par défaut, qui contournent complètement le rappel de redimensionnement, en veillant à ce que la variante de mise en page standard ou large correcte s'active au début d'un nouvel état de navigation.

Alignement sur le style Google

Pour que votre interface utilisateur personnalisée corresponde au rythme visuel de Google, le SDK fournit la classe utilitaire StyleValues. Vous pouvez lire ces valeurs en pixels indépendants de la densité (dp) pour aligner parfaitement vos vues sur les composants Google.

Par exemple, si vous souhaitez placer un bouton personnalisé dans l'angle supérieur de l'écran opposé à la fiche d'itinéraire, vous pouvez appeler StyleValues.headerTopPaddingDp() et attribuer la valeur renvoyée comme marge supérieure de votre bouton. Cela garantit que votre bouton personnalisé s'aligne visuellement sur le bord supérieur de la fiche d'itinéraire, en conservant la symétrie de l'écran.

Les marges et mesures de style disponibles incluent les éléments suivants :

  • StyleValues.headerNominalHeightDp()
  • StyleValues.headerTopPaddingDp()
  • StyleValues.headerFooterSidePaddingDp()
  • StyleValues.mapControlSidePaddingDp()
  • StyleValues.buttonMapControlSidePaddingDp()

Exemple : Implémentation de la mise en page basée sur des contraintes

L'exemple suivant illustre un délégué de mise en page de base qui gère les transitions d'état à l'aide de définitions ConstraintLayout et ConstraintSet programmatiques.

Bien que cet exemple crée des contraintes de vue dans le code, les délégués de mise en page peuvent également gonfler les mises en page XML Android standards.

Kotlin

/*
 * Copyright 2026 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

@file:Suppress("PackageName")

package com.example.navigationapidemo.layoutdelegate

import android.content.Context
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import com.google.android.libraries.navigation.layoutcustomization.ActiveGuidanceUiState
import com.google.android.libraries.navigation.layoutcustomization.AutoHidingVerticalLayout
import com.google.android.libraries.navigation.layoutcustomization.NavigationLayoutDelegate
import com.google.android.libraries.navigation.layoutcustomization.NavigationReadyUiState
import com.google.android.libraries.navigation.layoutcustomization.NavigationUiButton.ButtonKnownType.COMPASS
import com.google.android.libraries.navigation.layoutcustomization.NavigationUiParent
import com.google.android.libraries.navigation.layoutcustomization.StyleValues.headerNominalHeightDp

/** Kotlin equivalent of StandardUiElementsLayoutDelegate. */
class StandardUiElementsLayoutDelegateKt : NavigationLayoutDelegate() {

  private val layoutId = View.generateViewId()
  private val buttonsContainerId = View.generateViewId()

  private var layout: ConstraintLayout? = null
  private var buttonsContainer: AutoHidingVerticalLayout? = null

  private var navigationReadyConstraintSet: ConstraintSet? = null
  private var activeGuidanceConstraintSet: ConstraintSet? = null
  private var activeGuidanceWithPromptConstraintSet: ConstraintSet? = null

  private var activeGuidanceUiState: ActiveGuidanceUiState? = null

  override fun onEnterNavigationReady(
    navigationUiParent: NavigationUiParent,
    newState: NavigationReadyUiState,
  ) {
    val context = navigationUiParent.viewContext

    var currentLayout = layout
    if (currentLayout == null) {
      currentLayout =
        ConstraintLayout(context).apply {
          layoutParams =
            ViewGroup.LayoutParams(
              ViewGroup.LayoutParams.MATCH_PARENT,
              ViewGroup.LayoutParams.MATCH_PARENT,
            )
          id = layoutId
        }
      layout = currentLayout
    }

    removeFromParentView(newState.viewport)
    currentLayout.addView(
      newState.viewport,
      ViewGroup.LayoutParams(
        ViewGroup.LayoutParams.MATCH_PARENT,
        ViewGroup.LayoutParams.MATCH_PARENT,
      ),
    )

    removeFromParentView(newState.googleLogo)
    currentLayout.addView(
      newState.googleLogo,
      ViewGroup.LayoutParams(
        ViewGroup.LayoutParams.MATCH_PARENT,
        ViewGroup.LayoutParams.WRAP_CONTENT,
      ),
    )

    var currentButtonsContainer = buttonsContainer
    if (currentButtonsContainer == null) {
      currentButtonsContainer = AutoHidingVerticalLayout(context).apply { id = buttonsContainerId }
      buttonsContainer = currentButtonsContainer
    }

    removeFromParentView(currentButtonsContainer)
    currentLayout.addView(
      currentButtonsContainer,
      ViewGroup.LayoutParams(
        ViewGroup.LayoutParams.WRAP_CONTENT,
        ConstraintLayout.LayoutParams.MATCH_CONSTRAINT,
      ),
    )

    for (button in newState.navigationReadyButtons) {
      removeFromParentView(button.view)
      currentButtonsContainer.addView(button.view)
    }

    if (navigationReadyConstraintSet == null) {
      navigationReadyConstraintSet = buildNavigationReadyConstraintSet(newState)
    }
    navigationReadyConstraintSet?.applyTo(currentLayout)

    navigationUiParent.removeNavigationLayout(currentLayout)
    navigationUiParent.setNavigationLayout(currentLayout)
  }

  private fun buildNavigationReadyConstraintSet(uiState: NavigationReadyUiState): ConstraintSet {
    return ConstraintSet().apply {
      clone(layout)

      connect(
        uiState.viewport.id,
        ConstraintSet.START,
        ConstraintSet.PARENT_ID,
        ConstraintSet.START,
      )
      connect(uiState.viewport.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
      connect(uiState.viewport.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
      connect(
        uiState.viewport.id,
        ConstraintSet.BOTTOM,
        ConstraintSet.PARENT_ID,
        ConstraintSet.BOTTOM,
      )

      connect(
        uiState.googleLogo.id,
        ConstraintSet.BOTTOM,
        ConstraintSet.PARENT_ID,
        ConstraintSet.BOTTOM,
      )
      connect(
        uiState.googleLogo.id,
        ConstraintSet.START,
        ConstraintSet.PARENT_ID,
        ConstraintSet.START,
      )

      constrainButtonsToBottomEnd()
    }
  }

  override fun onLeaveNavigationReady(
    navigationUiParent: NavigationUiParent,
    oldState: NavigationReadyUiState,
  ) {
    buttonsContainer?.removeAllViews()
    layout?.removeAllViews()
    layout?.let { navigationUiParent.removeNavigationLayout(it) }
  }

  override fun onEnterActiveGuidance(
    navigationUiParent: NavigationUiParent,
    oldState: NavigationReadyUiState,
    newState: ActiveGuidanceUiState,
  ) {
    activeGuidanceUiState = newState
    val context = navigationUiParent.viewContext
    val currentLayout = checkNotNull(layout) { "layout must be initialized" }
    val currentButtonsContainer =
      checkNotNull(buttonsContainer) { "buttonsContainer must be initialized" }

    removeFromParentView(newState.turnCard)
    currentLayout.addView(
      newState.turnCard,
      ViewGroup.LayoutParams(
        ViewGroup.LayoutParams.WRAP_CONTENT,
        ViewGroup.LayoutParams.WRAP_CONTENT,
      ),
    )

    removeFromParentView(newState.etaCard)
    currentLayout.addView(
      newState.etaCard,
      ViewGroup.LayoutParams(
        ViewGroup.LayoutParams.WRAP_CONTENT,
        ViewGroup.LayoutParams.WRAP_CONTENT,
      ),
    )

    for (button in oldState.navigationReadyButtons) {
      removeFromParentView(button.view)
    }

    for (button in newState.activeGuidanceButtons) {
      val buttonLayoutParams =
        AutoHidingVerticalLayout.LayoutParams(
          ViewGroup.LayoutParams.WRAP_CONTENT,
          ViewGroup.LayoutParams.WRAP_CONTENT,
        )
      if (button.type == COMPASS) {
        buttonLayoutParams.isHighPriority = true
      }
      removeFromParentView(button.view)
      currentButtonsContainer.addView(button.view, buttonLayoutParams)
    }

    if (activeGuidanceConstraintSet == null) {
      activeGuidanceConstraintSet = buildActiveGuidanceConstraintSet(context, newState)
    }

    activeGuidanceConstraintSet?.applyTo(currentLayout)
  }

  override fun onLeaveActiveGuidance(
    navigationUiParent: NavigationUiParent,
    oldState: ActiveGuidanceUiState,
    newState: NavigationReadyUiState,
  ) {
    removeFromParentView(oldState.etaCard)
    removeFromParentView(oldState.turnCard)
    buttonsContainer?.removeAllViews()

    val currentButtonsContainer =
      checkNotNull(buttonsContainer) { "buttonsContainer must be initialized" }
    for (button in newState.navigationReadyButtons) {
      currentButtonsContainer.addView(
        button.view,
        ViewGroup.LayoutParams(
          ViewGroup.LayoutParams.WRAP_CONTENT,
          ViewGroup.LayoutParams.WRAP_CONTENT,
        ),
      )
    }

    navigationReadyConstraintSet?.applyTo(layout)
  }

  private fun buildActiveGuidanceConstraintSet(
    context: Context,
    uiState: ActiveGuidanceUiState,
  ): ConstraintSet {
    return ConstraintSet().apply {
      clone(layout)

      connect(uiState.turnCard.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
      connect(
        uiState.turnCard.id,
        ConstraintSet.START,
        ConstraintSet.PARENT_ID,
        ConstraintSet.START,
      )

      clear(uiState.viewport.id)
      connect(
        uiState.viewport.id,
        ConstraintSet.START,
        ConstraintSet.PARENT_ID,
        ConstraintSet.START,
      )
      connect(uiState.viewport.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
      setMargin(uiState.viewport.id, ConstraintSet.TOP, dpToPx(headerNominalHeightDp(), context))
      connect(uiState.viewport.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
      connect(uiState.viewport.id, ConstraintSet.BOTTOM, uiState.etaCard.id, ConstraintSet.TOP)

      clear(uiState.googleLogo.id, ConstraintSet.BOTTOM)
      constrainLogoToTopOfEtaCard(uiState)
      constrainEtaCardToBottomStart(uiState)
      constrainButtonsToTopOfEtaCard(context, uiState)
    }
  }

  private fun ConstraintSet.constrainEtaCardToBottomStart(uiState: ActiveGuidanceUiState) {
    connect(uiState.etaCard.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
    connect(uiState.etaCard.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
  }

  private fun ConstraintSet.constrainLogoToTopOfEtaCard(uiState: ActiveGuidanceUiState) {
    connect(uiState.googleLogo.id, ConstraintSet.BOTTOM, uiState.etaCard.id, ConstraintSet.TOP)
  }

  private fun ConstraintSet.constrainButtonsToBottomEnd() {
    clear(buttonsContainerId, ConstraintSet.BOTTOM)
    clear(buttonsContainerId, ConstraintSet.TOP)
    connect(buttonsContainerId, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
    connect(buttonsContainerId, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
    connect(buttonsContainerId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
  }

  private fun ConstraintSet.constrainButtonsToTopOfEtaCard(
    context: Context,
    uiState: ActiveGuidanceUiState,
  ) {
    clear(buttonsContainerId, ConstraintSet.BOTTOM)
    clear(buttonsContainerId, ConstraintSet.TOP)
    connect(buttonsContainerId, ConstraintSet.BOTTOM, uiState.etaCard.id, ConstraintSet.TOP)
    connect(buttonsContainerId, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
    connect(buttonsContainerId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
    setMargin(buttonsContainerId, ConstraintSet.TOP, dpToPx(headerNominalHeightDp(), context))
  }

  override fun onShowPrompt(navigationUiParent: NavigationUiParent, newPrompt: View) {
    val context = navigationUiParent.viewContext
    layout?.addView(newPrompt)

    if (activeGuidanceUiState != null) {
      activeGuidanceWithPromptConstraintSet =
        buildActiveGuidanceWithPromptConstraintSet(context, newPrompt)
      activeGuidanceWithPromptConstraintSet?.applyTo(layout)
    }
  }

  override fun onChangePrompt(
    navigationUiParent: NavigationUiParent,
    oldPrompt: View,
    newPrompt: View,
  ) {
    val context = navigationUiParent.viewContext
    activeGuidanceWithPromptConstraintSet?.clear(oldPrompt.id)

    val currentLayout = checkNotNull(layout) { "layout must be initialized" }
    currentLayout.removeView(oldPrompt)
    currentLayout.addView(newPrompt)

    if (activeGuidanceUiState != null) {
      activeGuidanceWithPromptConstraintSet =
        buildActiveGuidanceWithPromptConstraintSet(context, newPrompt)
      activeGuidanceWithPromptConstraintSet?.applyTo(currentLayout)
    }
  }

  override fun onHidePrompt(navigationUiParent: NavigationUiParent, oldPrompt: View) {
    activeGuidanceWithPromptConstraintSet?.clear(oldPrompt.id)
    layout?.removeView(oldPrompt)

    activeGuidanceConstraintSet?.applyTo(layout)
  }

  private fun buildActiveGuidanceWithPromptConstraintSet(
    context: Context,
    prompt: View,
  ): ConstraintSet {
    return ConstraintSet().apply {
      clone(layout)

      val state =
        checkNotNull(activeGuidanceUiState) { "activeGuidanceUiState must be initialized" }

      clear(state.viewport.id)
      connect(state.viewport.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
      connect(state.viewport.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
      setMargin(state.viewport.id, ConstraintSet.TOP, dpToPx(headerNominalHeightDp(), context))
      connect(state.viewport.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
      connect(state.viewport.id, ConstraintSet.BOTTOM, prompt.id, ConstraintSet.TOP)

      clear(state.googleLogo.id, ConstraintSet.BOTTOM)
      connect(state.googleLogo.id, ConstraintSet.BOTTOM, prompt.id, ConstraintSet.TOP)

      connect(prompt.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
      connect(prompt.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
    }
  }

  private fun removeFromParentView(view: View?) {
    if (view?.parent != null) {
      (view.parent as ViewGroup).removeView(view)
    }
  }

  private fun dpToPx(dp: Int, context: Context): Int {
    return TypedValue.applyDimension(
        TypedValue.COMPLEX_UNIT_DIP,
        dp.toFloat(),
        context.resources.displayMetrics,
      )
      .toInt()
  }
}

    

Java

/*
 * Copyright 2026 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.navigationapidemo.layoutdelegate;

import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.MATCH_CONSTRAINT;
import static com.google.android.libraries.navigation.layoutcustomization.NavigationUiButton.ButtonKnownType.COMPASS;

import android.content.Context;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import com.google.android.libraries.navigation.layoutcustomization.ActiveGuidanceUiState;
import com.google.android.libraries.navigation.layoutcustomization.AutoHidingVerticalLayout;
import com.google.android.libraries.navigation.layoutcustomization.NavigationLayoutDelegate;
import com.google.android.libraries.navigation.layoutcustomization.NavigationReadyUiState;
import com.google.android.libraries.navigation.layoutcustomization.NavigationUiButton;
import com.google.android.libraries.navigation.layoutcustomization.NavigationUiParent;
import com.google.android.libraries.navigation.layoutcustomization.StyleValues;

/**
 * A sample implementation of {@link NavigationLayoutDelegate} demonstrating a basic,
 * portrait-optimized layout using {@link ConstraintLayout}.
 *
 * <p><b>Understanding the Layout Delegate State Machine:</b> Navigation SDK transitions through
 * distinct states, each calling corresponding lifecycle methods on this delegate:
 *
 * <ul>
 *   <li><b>Navigation Ready:</b> Initiated by {@link #onEnterNavigationReady}. We initialize the
 *       layout here and add non-guidance views, then pass it to {@link NavigationUiParent} as the
 *       navigation layout.
 *   <li><b>Active Guidance (Turn-by-Turn Mode):</b> Initiated by {@link #onEnterActiveGuidance}. We
 *       set up the layout for Active Guidance, adding elements such as the turn card and ETA card.
 *   <li><b>Prompts:</b> Prompts (e.g., incident alerts) may be triggered during Active Guidance
 *       mode and can be added to the layout via {@link #onShowPrompt}.
 * </ul>
 *
 * This class caches its {@link ConstraintSet}s to ensure smooth transitions without needing to
 * recreate or inflate layouts continuously.
 */
public class StandardUiElementsLayoutDelegate extends NavigationLayoutDelegate {
  private final int layoutId;
  private final int buttonsContainerId;

  private ConstraintLayout layout;
  private AutoHidingVerticalLayout buttonsContainer;

  // We cache our ConstraintSet definitions to avoid cloning or rebuilding
  // constraint configurations programmatically on every transition. This optimization
  // keeps UI state switches (such as entering active guidance or popping up prompts) highly
  // performant.
  private ConstraintSet navigationReadyConstraintSet;
  private ConstraintSet activeGuidanceConstraintSet;
  private ConstraintSet activeGuidanceWithPromptConstraintSet;

  private ActiveGuidanceUiState activeGuidanceUiState;

  public StandardUiElementsLayoutDelegate() {
    layoutId = View.generateViewId();
    buttonsContainerId = View.generateViewId();
  }

  @Override
  public void onEnterNavigationReady(
      NavigationUiParent navigationUiParent, NavigationReadyUiState newState) {
    Context context = navigationUiParent.getViewContext();

    // Implementation Tip: For simplicity, this sample instantiates views and constraints
    // programmatically. In a production application, you can safely inflate standard XML
    // layout templates to build your layout hierarchies and define base UI constraints.

    // Create the root layout
    if (layout == null) {
      layout = new ConstraintLayout(context);
      LayoutParams layoutParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT);
      layout.setLayoutParams(layoutParams);
      layout.setId(layoutId);
    }

    // Add the Viewport (REQUIRED):
    // The viewport is an invisible bounding box used by Nav SDK to frame the vehicle
    // chevron and the upcoming route line. We want to position this view such that it avoids
    // being obscured by fully-opaque UI elements (like the turn card or the ETA card).
    removeFromParentView(newState.getViewport());
    LayoutParams viewportLayoutParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT);
    layout.addView(newState.getViewport(), viewportLayoutParams);

    // Add the Google Logo / Re-center Button (REQUIRED):
    // This view displays the Google logo during guidance and may transition into a
    // "Re-center" button if the user scrolls away from the vehicle chevron. It must
    // be added to the view hierarchy in all states.
    removeFromParentView(newState.getGoogleLogo());
    LayoutParams googleLogoLayoutParams = new LayoutParams(MATCH_PARENT, WRAP_CONTENT);
    layout.addView(newState.getGoogleLogo(), googleLogoLayoutParams);

    // Add the container for UI buttons
    if (buttonsContainer == null) {
      // We use AutoHidingVerticalLayout to create an adaptive vertical button container that
      // automatically hides or shows child views based on available screen height.
      buttonsContainer = new AutoHidingVerticalLayout(context);
      buttonsContainer.setId(buttonsContainerId);
    }
    removeFromParentView(buttonsContainer);
    LayoutParams buttonsContainerLayoutParams = new LayoutParams(WRAP_CONTENT, MATCH_CONSTRAINT);
    layout.addView(buttonsContainer, buttonsContainerLayoutParams);

    // Add UI buttons to the container
    for (NavigationUiButton button : newState.getNavigationReadyButtons()) {
      removeFromParentView(button.getView());
      buttonsContainer.addView(button.getView());
    }

    // Build constraint set for Navigation Ready state
    if (navigationReadyConstraintSet == null) {
      navigationReadyConstraintSet = buildNavigationReadyConstraintSet(newState);
    }

    // Apply the constraints
    navigationReadyConstraintSet.applyTo(layout);

    // Set the layout in NavigationUiParent
    navigationUiParent.removeNavigationLayout(layout);
    navigationUiParent.setNavigationLayout(layout);
  }

  private ConstraintSet buildNavigationReadyConstraintSet(NavigationReadyUiState uiState) {
    ConstraintSet constraintSet = new ConstraintSet();
    constraintSet.clone(layout);

    // Constrain viewport to the edges of its parent
    constraintSet.connect(
        uiState.getViewport().getId(),
        ConstraintSet.START,
        ConstraintSet.PARENT_ID,
        ConstraintSet.START);
    constraintSet.connect(
        uiState.getViewport().getId(),
        ConstraintSet.TOP,
        ConstraintSet.PARENT_ID,
        ConstraintSet.TOP);
    constraintSet.connect(
        uiState.getViewport().getId(),
        ConstraintSet.END,
        ConstraintSet.PARENT_ID,
        ConstraintSet.END);
    constraintSet.connect(
        uiState.getViewport().getId(),
        ConstraintSet.BOTTOM,
        ConstraintSet.PARENT_ID,
        ConstraintSet.BOTTOM);

    // Constrain the logo to the bottom start corner
    constraintSet.connect(
        uiState.getGoogleLogo().getId(),
        ConstraintSet.BOTTOM,
        ConstraintSet.PARENT_ID,
        ConstraintSet.BOTTOM);
    constraintSet.connect(
        uiState.getGoogleLogo().getId(),
        ConstraintSet.START,
        ConstraintSet.PARENT_ID,
        ConstraintSet.START);

    constrainButtonsToBottomEnd(constraintSet);

    return constraintSet;
  }

  @Override
  public void onLeaveNavigationReady(
      NavigationUiParent navigationUiParent, NavigationReadyUiState oldState) {
    buttonsContainer.removeAllViews();
    layout.removeAllViews();
    navigationUiParent.removeNavigationLayout(layout);
  }

  @Override
  public void onEnterActiveGuidance(
      NavigationUiParent navigationUiParent,
      NavigationReadyUiState oldState,
      ActiveGuidanceUiState newState) {
    activeGuidanceUiState = newState;

    Context context = navigationUiParent.getViewContext();

    // Sizing Guideline: The turn card and ETA card are internally configured to adapt and size
    // themselves dynamically based on the layout width (non-wideMode vs. wideMode). Forcing fixed
    // widths or heights on these elements via layouts is unsupported. Always use WRAP_CONTENT to
    // let the elements determine their optimal proportions.

    // Add the turn card
    removeFromParentView(newState.getTurnCard());
    LayoutParams turnCardLayoutParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
    layout.addView(newState.getTurnCard(), turnCardLayoutParams);

    // Add the ETA card
    removeFromParentView(newState.getEtaCard());
    LayoutParams etaCardLayoutParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
    layout.addView(newState.getEtaCard(), etaCardLayoutParams);

    // Remove the Navigation Ready UI buttons
    for (NavigationUiButton button : oldState.getNavigationReadyButtons()) {
      removeFromParentView(button.getView());
    }

    // By adding all buttons to the AutoHidingVerticalLayout, we can easily incorporate the latest
    // set of buttons when upgrading without any code changes required
    for (NavigationUiButton button : newState.getActiveGuidanceButtons()) {
      AutoHidingVerticalLayout.LayoutParams buttonLayoutParams =
          new AutoHidingVerticalLayout.LayoutParams(
              ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);

      // Mark critical buttons (such as the compass) as high priority so they are the last to be
      // hidden by AutoHidingVerticalLayout when layout space is limited.
      if (button.getType() == COMPASS) {
        buttonLayoutParams.isHighPriority = true;
      }

      removeFromParentView(button.getView());
      buttonsContainer.addView(button.getView(), buttonLayoutParams);
    }

    // Build constraint set for Active Guidance state
    if (activeGuidanceConstraintSet == null) {
      activeGuidanceConstraintSet = buildActiveGuidanceConstraintSet(context, newState);
    }

    // Apply the constraints
    activeGuidanceConstraintSet.applyTo(layout);
  }

  @Override
  public void onLeaveActiveGuidance(
      NavigationUiParent navigationUiParent,
      ActiveGuidanceUiState oldState,
      NavigationReadyUiState newState) {

    // Remove Active Guidance UI elements
    removeFromParentView(oldState.getEtaCard());
    removeFromParentView(oldState.getTurnCard());
    buttonsContainer.removeAllViews();

    // Add Navigation Ready UI buttons
    for (NavigationUiButton button : newState.getNavigationReadyButtons()) {
      LayoutParams buttonLayoutParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
      buttonsContainer.addView(button.getView(), buttonLayoutParams);
    }

    navigationReadyConstraintSet.applyTo(layout);
  }

  private ConstraintSet buildActiveGuidanceConstraintSet(
      Context context, ActiveGuidanceUiState uiState) {
    ConstraintSet constraintSet = new ConstraintSet();
    constraintSet.clone(layout);

    // Constrain turn card to top start corner
    constraintSet.connect(
        uiState.getTurnCard().getId(),
        ConstraintSet.TOP,
        ConstraintSet.PARENT_ID,
        ConstraintSet.TOP);
    constraintSet.connect(
        uiState.getTurnCard().getId(),
        ConstraintSet.START,
        ConstraintSet.PARENT_ID,
        ConstraintSet.START);

    // Constrain viewport to top of ETA card
    constraintSet.clear(uiState.getViewport().getId());
    constraintSet.connect(
        uiState.getViewport().getId(),
        ConstraintSet.START,
        ConstraintSet.PARENT_ID,
        ConstraintSet.START);
    constraintSet.connect(
        uiState.getViewport().getId(),
        ConstraintSet.TOP,
        ConstraintSet.PARENT_ID,
        ConstraintSet.TOP);

    // Instead of constraining the viewport's top directly to the bottom of the turn card
    // (which varies in height and would trigger jumpy camera framing updates), we use a fixed
    // nominal height to estimate the height of the turncard.
    constraintSet.setMargin(
        uiState.getViewport().getId(),
        ConstraintSet.TOP,
        dpToPx(StyleValues.headerNominalHeightDp(), context));

    constraintSet.connect(
        uiState.getViewport().getId(),
        ConstraintSet.END,
        ConstraintSet.PARENT_ID,
        ConstraintSet.END);
    constraintSet.connect(
        uiState.getViewport().getId(),
        ConstraintSet.BOTTOM,
        uiState.getEtaCard().getId(),
        ConstraintSet.TOP);

    constraintSet.clear(uiState.getGoogleLogo().getId(), ConstraintSet.BOTTOM);
    constrainLogoToTopOfEtaCard(uiState, constraintSet);

    constrainEtaCardToBottomStart(uiState, constraintSet);

    constrainButtonsToTopOfEtaCard(context, uiState, constraintSet);

    return constraintSet;
  }

  private static void constrainEtaCardToBottomStart(
      ActiveGuidanceUiState uiState, ConstraintSet constraintSet) {
    constraintSet.connect(
        uiState.getEtaCard().getId(),
        ConstraintSet.START,
        ConstraintSet.PARENT_ID,
        ConstraintSet.START);
    constraintSet.connect(
        uiState.getEtaCard().getId(),
        ConstraintSet.BOTTOM,
        ConstraintSet.PARENT_ID,
        ConstraintSet.BOTTOM);
  }

  private static void constrainLogoToTopOfEtaCard(
      ActiveGuidanceUiState uiState, ConstraintSet constraintSet) {
    constraintSet.connect(
        uiState.getGoogleLogo().getId(),
        ConstraintSet.BOTTOM,
        uiState.getEtaCard().getId(),
        ConstraintSet.TOP);
  }

  private void constrainButtonsToBottomEnd(ConstraintSet constraintSet) {
    constraintSet.clear(buttonsContainerId, ConstraintSet.BOTTOM);
    constraintSet.clear(buttonsContainerId, ConstraintSet.TOP);
    constraintSet.connect(
        buttonsContainerId, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP);
    constraintSet.connect(
        buttonsContainerId, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM);
    constraintSet.connect(
        buttonsContainerId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END);
  }

  private void constrainButtonsToTopOfEtaCard(
      Context context, ActiveGuidanceUiState uiState, ConstraintSet constraintSet) {
    constraintSet.clear(buttonsContainerId, ConstraintSet.BOTTOM);
    constraintSet.clear(buttonsContainerId, ConstraintSet.TOP);
    constraintSet.connect(
        buttonsContainerId, ConstraintSet.BOTTOM, uiState.getEtaCard().getId(), ConstraintSet.TOP);
    constraintSet.connect(
        buttonsContainerId, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP);
    constraintSet.connect(
        buttonsContainerId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END);
    constraintSet.setMargin(
        buttonsContainerId,
        ConstraintSet.TOP,
        dpToPx(StyleValues.headerNominalHeightDp(), context));
  }

  @Override
  public void onShowPrompt(NavigationUiParent navigationUiParent, View newPrompt) {
    Context context = navigationUiParent.getViewContext();

    layout.addView(newPrompt);

    // When a prompt is displayed at the bottom of the screen, we update our active constraints so
    // that the invisible Viewport sits entirely above the prompt. This automatically forces the
    // Nav SDK camera to adjust its zoom and framing so that the route chevron is always visible to
    // the driver.
    activeGuidanceWithPromptConstraintSet =
        buildActiveGuidanceWithPromptConstraintSet(context, newPrompt);
    activeGuidanceWithPromptConstraintSet.applyTo(layout);
  }

  @Override
  public void onChangePrompt(
      NavigationUiParent navigationUiParent, View oldPrompt, View newPrompt) {
    Context context = navigationUiParent.getViewContext();

    activeGuidanceWithPromptConstraintSet.clear(oldPrompt.getId());

    layout.removeView(oldPrompt);
    layout.addView(newPrompt);

    // When a prompt is displayed at the bottom of the screen, we update our active constraints so
    // that the invisible Viewport sits entirely above the prompt. This automatically forces the
    // Nav SDK camera to adjust its zoom and framing so that the route chevron is always visible to
    // the driver.
    activeGuidanceWithPromptConstraintSet =
        buildActiveGuidanceWithPromptConstraintSet(context, newPrompt);
    activeGuidanceWithPromptConstraintSet.applyTo(layout);
  }

  @Override
  public void onHidePrompt(NavigationUiParent navigationUiParent, View oldPrompt) {
    activeGuidanceWithPromptConstraintSet.clear(oldPrompt.getId());

    layout.removeView(oldPrompt);

    activeGuidanceConstraintSet.applyTo(layout);
  }

  private ConstraintSet buildActiveGuidanceWithPromptConstraintSet(Context context, View prompt) {
    ConstraintSet constraintSet = new ConstraintSet();
    constraintSet.clone(layout);

    // Constrain viewport to top of prompt
    constraintSet.clear(activeGuidanceUiState.getViewport().getId());
    constraintSet.connect(
        activeGuidanceUiState.getViewport().getId(),
        ConstraintSet.START,
        ConstraintSet.PARENT_ID,
        ConstraintSet.START);
    constraintSet.connect(
        activeGuidanceUiState.getViewport().getId(),
        ConstraintSet.TOP,
        ConstraintSet.PARENT_ID,
        ConstraintSet.TOP);
    constraintSet.setMargin(
        activeGuidanceUiState.getViewport().getId(),
        ConstraintSet.TOP,
        dpToPx(StyleValues.headerNominalHeightDp(), context));
    constraintSet.connect(
        activeGuidanceUiState.getViewport().getId(),
        ConstraintSet.END,
        ConstraintSet.PARENT_ID,
        ConstraintSet.END);
    constraintSet.connect(
        activeGuidanceUiState.getViewport().getId(),
        ConstraintSet.BOTTOM,
        prompt.getId(),
        ConstraintSet.TOP);

    // Constrain prompt to bottom start corner
    constraintSet.connect(
        prompt.getId(), ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START);
    constraintSet.connect(
        prompt.getId(), ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM);

    return constraintSet;
  }

  private void removeFromParentView(View view) {
    if (view != null && view.getParent() != null) {
      ((ViewGroup) view.getParent()).removeView(view);
    }
  }

  private static int dpToPx(int dp, Context context) {
    return (int)
        TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
  }
}