Personalizzare il layout di navigazione

Animazione di esempio che mostra un layout di navigazione personalizzato su Android

Utilizza la personalizzazione del layout per controllare la gerarchia di oggetti View e il posizionamento sullo schermo sia per le visualizzazioni fornite da Google sia per le tue visualizzazioni aziendali personalizzate.

Anziché aggiungere visualizzazioni personalizzate in slot di intestazione o piè di pagina predefiniti, implementa un delegato di layout per gestire il layout dell'intero schermo. Durante le transizioni di stato di navigazione, il delegato di layout riceve i componenti forniti da Google, come la scheda di svolta, la scheda ETA e i pulsanti. Posiziona questi elementi insieme ai tuoi contenuti personalizzati utilizzando i sistemi di layout Android standard come ConstraintLayout, CoordinatorLayout o LinearLayout.

Utilizza questo framework per posizionare le informazioni aziendali personalizzate, come lo stato dell'ordine o le istruzioni di ritiro, con precisione sullo schermo, evitando che le visualizzazioni si sovrappongano.

Come funziona la personalizzazione del layout

La personalizzazione del layout utilizza un pattern di progettazione di delega. Anziché disegnare o posizionare automaticamente i componenti dell'interfaccia utente sullo schermo, l'SDK li passa direttamente a una classe personalizzata che scrivi: il delegato di layout.

Per personalizzare il layout, crea una classe che estenda la classe astratta NavigationLayoutDelegate e assegna un'istanza a NavigationView o SupportNavigationFragment. Ogni volta che si verifica una transizione di stato di navigazione, ad esempio il passaggio dalla mappa base alle indicazioni stradali attive, l'SDK esegue i metodi di callback sul delegato e fornisce i componenti dell'interfaccia utente disponibili per quello stato specifico.

Per fornire un'esperienza utente integrata e senza interruzioni, questo framework stabilisce una chiara separazione delle responsabilità tra l'app e l'SDK. Utilizza il delegato di layout per personalizzare quanto segue:

  • Crea la gerarchia delle visualizzazioni: scegli esattamente quali componenti Google e visualizzazioni aziendali personalizzate aggiungere allo schermo per ogni stato dell'interfaccia utente.
  • Posiziona ogni elemento: imposta ancore, margini e posizionamento del layout precisi sullo schermo. Evita di applicare vincoli di larghezza o altezza personalizzati ai componenti Google perché questi calcolano le proprie dimensioni interne.
  • Inquadra la mappa base: definisci i limiti visibili della fotocamera della mappa utilizzando le coordinate del componente dell'area visibile.
  • Disponi gli elementi sullo schermo: decidi se le visualizzazioni personalizzate devono essere visualizzate sopra, sotto o accanto ai controlli integrati di Google.

Nel frattempo, non puoi personalizzare i seguenti componenti Google con il delegato di layout:

  • Dimensioni dei componenti: le dimensioni e le dimensioni interne dei componenti forniti da Google, che l'SDK calcola automaticamente.
  • Condizioni di attivazione: quando vengono visualizzati avvisi o prompt dinamici in base ai dati del percorso in tempo reale.

Principi di implementazione

Quando scrivi un delegato di layout, tieni presente le seguenti regole per evitare bug di layout o arresti anomali di runtime:

  • L'impostazione predefinita è una schermata vuota: i componenti forniti da Google vengono visualizzati solo se il delegato li aggiunge esplicitamente alla gerarchia delle visualizzazioni e li posiziona.
  • Le API legacy non sono supportate: le proprietà e i metodi che controllano il modello di layout basato su slot legacy non saranno supportati e potrebbero non comportarsi come previsto quando è attivo un delegato personalizzato.
  • Non modificare le strutture di visualizzazione interne: Non utilizzare metodi come findViewById() per attraversare o modificare le gerarchie delle visualizzazioni dei componenti forniti da Google (ad esempio la scheda di svolta o la scheda ETA). Poiché queste gerarchie di visualizzazione interne sono dettagli di implementazione sottostanti, sono soggette a modifiche nelle versioni dell'SDK. La loro modifica può causare l'interruzione del layout durante i futuri aggiornamenti dell'SDK.

Compatibilità con le API legacy

Per un comportamento affidabile del layout quando utilizzi un delegato di layout personalizzato, evita di utilizzare le seguenti API legacy basate su slot, la cui rimozione è prevista. Per eseguire la migrazione di un'app esistente, sostituisci l'utilizzo di queste API con il codice nel delegato di layout personalizzato:

Mostra le API legacy basate su slot

API Legacy Sostituzione del delegato di layout
setCustomControl(View, CustomControlPosition) Aggiungi le visualizzazioni direttamente a ConstraintLayout o a un altro gruppo di oggetti View.
removeCustomControl(View) Rimuovi le visualizzazioni direttamente dalla gerarchia di oggetti View.
setEtaCardEnabled(boolean) Leggi la visualizzazione etaCard in onEnterActiveGuidance.
setHeaderEnabled(boolean) Leggi la visualizzazione turnCard in onEnterActiveGuidance.
setReportIncidentButtonEnabled(boolean) Individua il pulsante REPORTING in getActiveGuidanceButtons().
setTripProgressBarEnabled(boolean) Leggi la visualizzazione tripProgressBar in onEnterActiveGuidance.
addOnNavigationUiChangedListener(...) Affidati ai callback di transizione di stato di NavigationLayoutDelegate.
removeOnNavigationUiChangedListener(...) Gestisci le transizioni di stato del layout direttamente utilizzando il delegato.
addPromptVisibilityChangedListener(...) Affidati ai callback dei prompt di NavigationLayoutDelegate (ad esempio onShowPrompt()).
removePromptVisibilityChangedListener(...) Gestisci la logica di visualizzazione dei prompt direttamente utilizzando il delegato.
setCompassEnabled(boolean) Individua la bussola in getNavigationReadyButtons() o getActiveGuidanceButtons().

Elenco di controllo degli elementi essenziali

Segui questi passaggi e requisiti essenziali per implementare correttamente il delegato di layout:

  • Inizializza il delegato prima di creare l'interfaccia utente: Chiama setLayoutDelegate() prima che l'SDK inizializzi l'interfaccia utente di navigazione. Consulta gli esempi di implementazione del codice riportati di seguito per verificare la tempistica di configurazione esatta per la struttura dell'applicazione. L'inizializzazione del delegato dopo la creazione dell'interfaccia utente attiva un 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);
            
  • Crea il tuo contenitore di layout: crea un gruppo di visualizzazioni personalizzato, ad esempio un ConstraintLayout, per contenere gli elementi dell'interfaccia utente personalizzati e le visualizzazioni fornite da Google.

  • Collega le visualizzazioni obbligatorie: devi aggiungere il logo Google (getGoogleLogo()) e l'area visibile (getViewport()) al contenitore di layout in tutti gli stati di navigazione. Se non includi entrambe queste visualizzazioni viene attivato un ApiIllegalStateException di runtime e l'app si arresta in modo anomalo.

  • Rispetta le dimensioni fisse: l'SDK determina le dimensioni di diversi componenti forniti da Google. Non applicare vincoli di larghezza o altezza personalizzati ai seguenti elementi:

    • Scheda di svolta
    • scheda ETA
    • Prompt in arrivo
    • Logo Google
    • Widget della velocità
  • Collega il layout alla visualizzazione: chiama navigationView.setNavigationLayout() all'interno dei callback onEnterNavigationReady() e onEnterActiveGuidance() per collegare il contenitore a la gerarchia di oggetti View della mappa.

  • Esegui la pulizia all'uscita dallo stato: chiama navigationView.removeNavigationLayout() e rimuovi le visualizzazioni personalizzate all'interno dei callback onLeave per evitare perdite di memoria e stati dell'interfaccia utente sovrapposti.

  • Evita le API di layout legacy: non chiamare le API basate su slot ritirate come setCustomControl() o setHeaderEnabled() mentre è attivo un delegato personalizzato. L'SDK potrebbe ignorare queste chiamate legacy quando è collegato un delegato personalizzato.

Stati dell'interfaccia utente e componenti Google

Quando si verifica una transizione di stato di navigazione, l'SDK passa un oggetto UiState di sola lettura al callback del delegato. Questo oggetto raggruppa i flag di configurazione del layout corrente con i componenti Google (ad esempio, le istanze View) necessari per eseguire il rendering dello schermo.

Il delegato di layout gestisce la gerarchia di oggetti View in quattro stati operativi. Il seguente diagramma di stato illustra il modo in cui l'SDK esegue la transizione tra gli stati di navigazione e i metodi di callback che esegue sul delegato:

Diagramma di transizione dello stato del ciclo di vita di NavigationLayoutDelegate che mostra le transizioni tra gli stati Non-Navigation, Navigation Ready e Active Guidance.

I componenti Google specifici forniti al delegato dipendono dalla fase corrente del ciclo di vita della navigazione.

Componenti obbligatori (tutti gli stati di navigazione)

Indipendentemente dalla fase di navigazione corrente, devi includere, posizionare e mantenere visibili i seguenti componenti Google nella gerarchia di oggetti View:

  • Logo Google (getGoogleLogo()): questo componente mostra il logo obbligatorio di Google Maps. Se il pulsante Ricentra è attivato, il logo si trasforma automaticamente in questo pulsante quando il conducente scorre la mappa allontanandosi dal veicolo. Di conseguenza, il posizionamento del logo determina la posizione in cui viene visualizzato il pulsante Ricentra. Ti consigliamo di posizionare il logo nell'angolo in basso a sinistra del layout per soddisfare le aspettative standard dell'interfaccia utente della mappa.

  • Area visibile (getViewport()): una View invisibile che definisce i limiti di inquadratura della fotocamera. Il posizionamento dell'area visibile determina esattamente dove l'SDK centra il chevron del veicolo e disegna la linea del percorso attivo. Posiziona l'area visibile in modo che copra l'area aperta e non oscurata dello schermo, evitando in modo sicuro overlay opachi come i fogli inferiori personalizzati.

Durante lo stato di navigazione, l'interfaccia utente rimane minima per mantenere l'attenzione sulla mappa base. L'oggetto NavigationReadyUiState fornisce l'accesso a quanto segue:

  • getNavigationReadyButtons(): un elenco di visualizzazioni di azioni mobili configurate per la mappa base (in genere solo il pulsante Bussola). Poiché Google potrebbe aggiungere o riordinare i pulsanti nelle versioni future dell'SDK, evita di fare affidamento su indici di elenco fissi. Scorri invece l'elenco, leggi il tipo di ogni pulsante confrontando getType() con ButtonKnownType.COMPASS ed estrai la visualizzazione fisica chiamando getView(). Tieni presente che i pulsanti esatti in questo elenco differiscono da quelli disponibili durante le indicazioni attive.

Componenti delle indicazioni attive

Quando iniziano le indicazioni stradali, l'SDK sblocca la suite completa di controlli di navigazione. L'oggetto ActiveGuidanceUiState fornisce l'accesso ai seguenti componenti Google:

  • getTurnCard(): il banner dell'intestazione principale che mostra le indicazioni della manovra imminente, le misurazioni della distanza e le indicazioni delle corsie in cui posizionarsi. Posizionalo nella parte superiore del layout per stabilire una gerarchia di navigazione familiare e ancorare le visualizzazioni personalizzate intorno ad esso.

  • getEtaCard(): il banner del piè di pagina che mostra l'orario di arrivo stimato, il tempo di viaggio rimanente e la distanza rimanente dalla destinazione. Posizionalo lungo il bordo inferiore dello schermo o integra le sue coordinate con i fogli di gestione delle attività personalizzati.

  • getTripProgressBar(): una barra di avanzamento verticale che mostra al conducente la distanza percorsa lungo il percorso corrente. A differenza dei layout legacy che lo fissavano rigorosamente al bordo della mappa, hai la massima libertà di ancorarlo ovunque, ad esempio lungo un bordo laterale del contenitore personalizzato.

  • getSpeedWidget(): un controllo mobile per il tachimetro e i limiti di velocità pubblicati. A seconda delle impostazioni dell'API e della disponibilità dei dati, questa visualizzazione viene ridimensionata dinamicamente tra quattro stati visivi in fase di runtime (non visualizzando nulla, solo la velocità attuale, solo il limite di velocità o entrambe le letture). Poiché il widget può passare da una dimensione all'altra senza preavviso, ancora sempre le visualizzazioni circostanti utilizzando i vincoli in modo che il layout si adatti automaticamente per evitare sovrapposizioni spaziali.

  • getActiveGuidanceButtons(): un elenco espanso di visualizzazioni di azioni mobili per lo stato di indicazioni attive (in genere include sia il pulsante Bussola sia il pulsante Segnala incidente). Come nello stato di navigazione, puoi individuare ed estrarre le singole visualizzazioni dei pulsanti filtrando l'elenco in base a ButtonKnownType (COMPASS o REPORTING) ed estraendo la visualizzazione utilizzando getView(). Puoi quindi posizionarli in modo indipendente o utilizzare un layout come AutoHidingLinearLayout per impilare l'array in modo sicuro senza conflitti spaziali.

Componenti dei prompt dinamici

I prompt, come gli avvisi di incidenti o gli avvisi di autovelox, vengono attivati in modo indipendente durante le indicazioni attive.

Quando un prompt è pronto per essere visualizzato, l'SDK chiama il callback onShowPrompt() del delegato e passa una visualizzazione newPrompt. Il delegato è responsabile del posizionamento uniforme di questo prompt sul layout (in genere ancorato al bordo inferiore del contenitore della mappa).

Poiché il prompt in arrivo copre la parte inferiore dello schermo, devi aggiornare il layout per evitare che il prompt si sovrapponga all'area visibile, al logo Google o a qualsiasi pulsante allineato in basso.

Gestire le dimensioni dello schermo e la modalità larga

Per gestire le modifiche alle dimensioni della mappa e all'orientamento dello schermo, il delegato di layout utilizza le seguenti funzionalità:

  • Ridimensionamento della visualizzazione: regola il layout ogni volta che cambiano le dimensioni fisiche della mappa.

  • Modalità larga: passa a una variante di layout in formato largo quando la mappa è sufficientemente larga.

Rispondere al ridimensionamento della visualizzazione

Ogni volta che cambiano le dimensioni fisiche del contenitore della mappa, l'SDK esegue il callback onSizeChanged(). I layout a schermo diviso, i cursori di layout e la rotazione del dispositivo attivano in genere il callback onSizeChanged(). Utilizza questo callback per apportare modifiche adattabili generali all'interfaccia utente personalizzata. Implementa onSizeChanged() per riposizionare gli elementi personalizzati per un nuovo formato dello schermo, applicare i tuoi punti di interruzione di larghezza o altezza personalizzati e rilevare quando un evento di ridimensionamento attiva la modalità larga per scambiare in modo sicuro le varianti di layout.

Comprendere la modalità larga

La modalità larga si attiva quando il contenitore della mappa è sufficientemente largo da visualizzare i componenti dell'interfaccia utente affiancati.

Leggi il valore booleano isWideMode() dall'oggetto di stato per riposizionare gli elementi dell'interfaccia utente personalizzati e mantenere il centro della mappa libero per il conducente. Poiché i componenti forniti da Google, come la scheda di svolta e la scheda ETA, si riducono e si rimodellano automaticamente in modalità larga, la lettura di questo valore booleano garantisce che il layout si adatti esattamente nello stesso momento in cui i componenti Google vengono aggiornati.

Considera i seguenti esempi per il posizionamento dei componenti del layout in modalità standard e larga:

  • Modalità Ritratto standard: posiziona la scheda di svolta nella parte superiore dello schermo e la scheda dell'orario di arrivo stimato nella parte inferiore.

  • Modalità larga: sposta la scheda di svolta sul lato iniziale dello schermo e la scheda dell'orario di arrivo stimato sul lato finale.

Controllare la modalità larga durante le transizioni di stato

Se il layout supporta una variante in modalità larga, valuta il valore booleano isWideMode() all'interno di ogni callback di transizione di stato, anziché fare affidamento solo su onSizeChanged().

Controlla l'oggetto di stato durante i callback come onEnterNavigationReady() e onEnterActiveGuidance() per gestire i lanci iniziali dell'app in modalità Orizzontale. Questo approccio protegge il layout dagli eventi del ciclo di vita del sistema Android, come la ricreazione dell'attività predefinita, che ignorano completamente il callback di ridimensionamento, garantendo che la variante di layout standard o larga corretta venga attivata nel momento in cui inizia un nuovo stato di navigazione.

Allineamento allo stile di Google

Per fare in modo che l'interfaccia utente personalizzata corrisponda al ritmo visivo di Google, l'SDK fornisce la classe di utilità StyleValues. Puoi leggere questi valori in pixel indipendenti dalla densità (dp) per allineare perfettamente le visualizzazioni ai componenti Google.

Ad esempio, se vuoi posizionare un pulsante personalizzato nell'angolo in alto dello schermo opposto alla scheda di svolta, puoi chiamare StyleValues.headerTopPaddingDp() e assegnare il valore restituito come margine superiore del pulsante. In questo modo, il pulsante personalizzato si allinea visivamente al bordo superiore della scheda di svolta, mantenendo la simmetria dello schermo.

I padding e le misurazioni di stile disponibili includono:

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

Esempio: implementazione del layout basato su vincoli

L'esempio seguente mostra un delegato di layout di base che gestisce le transizioni di stato utilizzando le definizioni programmatiche di ConstraintLayout e ConstraintSet.

Sebbene questo esempio crei vincoli di visualizzazione nel codice, i delegati di layout possono anche espandere i layout XML Android standard.

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());
  }
}