
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 negli slot predefiniti dell'intestazione o del piè di pagina, implementa
un delegato del layout per gestire il layout dell'intero schermo. Durante le transizioni di stato della navigazione, il delegato del layout riceve i componenti forniti da Google, come la scheda di svolta, la scheda ETA e i pulsanti. Posiziona questi elementi accanto ai tuoi contenuti personalizzati utilizzando i sistemi di layout Android standard come ConstraintLayout, CoordinatorLayout o LinearLayout.
Utilizza questo framework per posizionare con precisione sullo schermo informazioni sull'attività personalizzate, come lo stato dell'ordine o le istruzioni per il ritiro, evitando al contempo la sovrapposizione delle visualizzazioni.
Come funziona la personalizzazione del layout
La personalizzazione del layout utilizza un pattern di progettazione della 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 del layout.
Per personalizzare il layout, crea una classe che estende la classe astrattaNavigationLayoutDelegate e assegna un'istanza a NavigationView o SupportNavigationFragment. Ogni volta che lo stato di navigazione cambia, ad esempio quando si passa dalla mappa base alle indicazioni stradali passo passo attive, l'SDK esegue i metodi di callback sul delegato e fornisce i componenti UI disponibili per quello stato specifico.
Per offrire un'esperienza utente fluida e integrata, questo framework stabilisce una chiara separazione delle responsabilità tra la tua app e l'SDK. Utilizza il delegato del layout per personalizzare quanto segue:
- Crea la gerarchia di oggetti View:scegli esattamente quali componenti Google e visualizzazioni aziendali personalizzate aggiungere alla schermata per ogni stato della UI.
- Posiziona ogni elemento:imposta ancoraggi, 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 confini visibili della fotocamera della mappa utilizzando le coordinate del componente dell'area visibile.
- Stratifica lo schermo:decidi se le visualizzazioni personalizzate devono essere posizionate sopra, sotto o accanto ai controlli integrati di Google.
Nel frattempo, non puoi personalizzare i seguenti componenti Google con il delegato del 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 sul 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 in fase di runtime:
- Per impostazione predefinita, viene visualizzata una schermata vuota: i componenti forniti da Google vengono visualizzati solo se il delegato li aggiunge esplicitamente alla gerarchia di oggetti View 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 di oggetti View dei componenti forniti da Google (come la scheda di svolta o la scheda ETA). Poiché queste gerarchie di oggetti View interne sono dettagli di implementazione sottostanti, sono soggette a modifiche tra le release 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 di layout affidabile quando utilizzi un delegato di layout personalizzato, evita di utilizzare le seguenti API basate su slot legacy, che verranno ritirate. Per eseguire la migrazione di un'app esistente, sostituisci l'utilizzo di queste API con il codice nel delegato del layout personalizzato:
Mostra le API legacy basate su slot
| API Legacy | Layout Delegate replacement |
|---|---|
setCustomControl(View, CustomControlPosition) |
Aggiungi le tue visualizzazioni direttamente a ConstraintLayout o a un altro gruppo di visualizzazioni. |
removeCustomControl(View) |
Rimuovi le visualizzazioni direttamente dalla gerarchia di oggetti View. |
setEtaCardEnabled(boolean) |
Leggi la vista etaCard in onEnterActiveGuidance. |
setHeaderEnabled(boolean) |
Leggi la vista turnCard in onEnterActiveGuidance. |
setReportIncidentButtonEnabled(boolean) |
Individua il pulsante REPORTING in getActiveGuidanceButtons(). |
setTripProgressBarEnabled(boolean) |
Leggi la vista 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 NavigationLayoutDelegate (come 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 di Essentials
Segui questi passaggi e requisiti essenziali per implementare correttamente il tuo delegato del layout:
-
Inizializza il delegato prima di creare l'interfaccia utente:chiama
setLayoutDelegate()prima che l'SDK inizializzi l'interfaccia utente di navigazione. Consulta i seguenti esempi di implementazione del codice per verificare la tempistica di configurazione esatta per la struttura della tua applicazione. L'inizializzazione del delegato dopo la creazione della UI attiva unApiIllegalStateException.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 container di layout:crea un gruppo di visualizzazioni personalizzate, ad esempio un
ConstraintLayout, per contenere gli elementi UI personalizzati e le visualizzazioni fornite da Google. -
Collega le visualizzazioni obbligatorie:devi aggiungere il logo Google (
getGoogleLogo()) e l'area visibile (getViewport()) al contenitore del layout in tutti gli stati di navigazione. Se non includi entrambe queste visualizzazioni, viene attivato unApiIllegalStateExceptiondi 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:
- Gira la carta
- scheda ETA
- Prompt in arrivo
- Logo Google
- Widget Velocità
-
Collega il layout alla visualizzazione:chiama
navigationView.setNavigationLayout()all'interno dei callbackonEnterNavigationReady()eonEnterActiveGuidance()per collegare il container alla gerarchia di oggetti View della mappa. -
Esegui la pulizia all'uscita dallo stato: chiama
navigationView.removeNavigationLayout()e rimuovi le tue visualizzazioni personalizzate all'interno dei callbackonLeaveper evitare perdite di memoria e stati della UI sovrapposti. -
Evita le API di layout legacy:non chiamare API basate su slot deprecate come
setCustomControl()osetHeaderEnabled()mentre è attivo un delegato personalizzato. L'SDK potrebbe ignorare queste chiamate legacy quando è collegato un delegato personalizzato.
Stati della UI e componenti Google
Quando si verifica una transizione dello 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 es. le istanze View) necessari per eseguire il rendering della schermata.
Il delegato del layout gestisce la gerarchia di oggetti View in quattro stati operativi. Il seguente diagramma di stato illustra le transizioni dell'SDK tra gli stati di navigazione e i metodi di callback che esegue sul tuo delegato:
I componenti Google specifici forniti al tuo 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 Google Maps obbligatorio. Se il pulsante Ricentra è attivo, il logo si trasforma automaticamente in questo pulsante quando il conducente scorre la mappa lontano dal veicolo. Di conseguenza, la posizione del logo determina dove 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()): unViewinvisibile che definisce i limiti dell'inquadratura della videocamera. La posizione dell'area visibile determina esattamente dove l'SDK centra il simbolo di direzione 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 riquadri inferiori personalizzati.
Componenti pronti per la navigazione
Durante lo stato di navigazione pronta, la UI 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 floating configurate per la mappa base (in genere solo il pulsante Bussola). Poiché Google potrebbe aggiungere o riordinare i pulsanti nelle future release dell'SDK, evita di fare affidamento su indici di elenchi fissi. Invece, esegui l'iterazione dell'elenco, leggi il tipo di ogni pulsante confrontando
getType()conButtonKnownType.COMPASSed estrai la visualizzazione fisica chiamandogetView(). Tieni presente che i pulsanti esatti in questo elenco differiscono da quelli disponibili durante le indicazioni stradali attive.
Componenti di guida attiva
Quando inizia la navigazione passo passo, 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 per la manovra imminente, le misurazioni della distanza e l'assistenza per il cambio di corsia. Posizionala nella parte superiore del layout per stabilire una gerarchia di navigazione familiare e ancorare le visualizzazioni personalizzate intorno.getEtaCard(): il banner del piè di pagina che mostra l'ora di arrivo stimata, il tempo di percorrenza rimanente e la distanza rimanente dalla destinazione. Posizionalo lungo il bordo inferiore dello schermo o integra le sue coordinate con i tuoi fogli di gestione delle attività personalizzati.getTripProgressBar(): una barra di avanzamento verticale che mostra al conducente la distanza percorsa lungo il percorso attuale. A differenza dei layout legacy che lo fissavano rigorosamente al bordo della mappa, hai la totale libertà di ancorarlo ovunque, ad esempio lungo un bordo laterale del tuo container personalizzato.getSpeedWidget(): un controllo mobile per il tachimetro e i limiti di velocità indicati. A seconda delle impostazioni dell'API e della disponibilità dei dati, questa visualizzazione si ridimensiona dinamicamente tra quattro stati visivi in fase di runtime (non viene visualizzato 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 floating per lo stato di indicazioni attive (in genere include sia la bussola sia il pulsante Segnalazione di un problema). Come nello stato Pronto per la navigazione, puoi individuare ed estrarre le singole visualizzazioni dei pulsanti filtrando l'elenco in base aButtonKnownType(COMPASSoREPORTING) ed estraendo la visualizzazione utilizzandogetView(). Puoi quindi posizionarli in modo indipendente o utilizzare un layout comeAutoHidingLinearLayoutper impilare l'array in modo sicuro senza conflitti spaziali.
Componenti dei prompt dinamici
I prompt, come gli avvisi di incidenti o di autovelox, vengono attivati indipendentemente durante la navigazione attiva.
Quando un prompt è pronto per essere visualizzato, l'SDK chiama il callback onShowPrompt() del tuo delegato e passa una visualizzazione newPrompt. Il tuo delegato è responsabile del posizionamento corretto di questo prompt sul layout (in genere ancorato al bordo inferiore del contenitore della mappa).
Poiché la richiesta in arrivo copre la parte inferiore dello schermo, devi aggiornare il layout per evitare che la richiesta si sovrapponga alla viewport, al logo Google o a eventuali pulsanti allineati in basso.
Gestione delle dimensioni dello schermo e della modalità estesa
Per gestire le modifiche alle dimensioni della mappa e all'orientamento dello schermo, il delegato del layout utilizza le seguenti funzionalità:
Ridimensionamento della visualizzazione:regola il layout ogni volta che cambiano le dimensioni fisiche della mappa.
Modalità ampia:passa a una variante di layout in formato ampio quando la mappa è abbastanza ampia.
Risposta al ridimensionamento della visualizzazione
Ogni volta che le dimensioni fisiche del container della mappa cambiano, l'SDK esegue il callbackonSizeChanged(). I layout con schermo diviso, i cursori di layout e la rotazione del dispositivo attivano comunemente il callback onSizeChanged(). Utilizza questo callback per apportare modifiche adattabili generali alla tua UI personalizzata. Implementa onSizeChanged() per riposizionare gli elementi personalizzati per un nuovo formato dello schermo, applica i tuoi breakpoint di larghezza o altezza personalizzati e rileva quando un evento di ridimensionamento attiva la modalità estesa per scambiare in sicurezza le varianti di layout.
Informazioni sulla modalità larga
La modalità estesa si attiva quando il contenitore della mappa è abbastanza largo da visualizzare i componenti dell'interfaccia utente affiancati.
Leggi il valore booleano isWideMode() dall'oggetto di stato per riposizionare gli elementi UI 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à estesa, 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 nelle modalità standard e ampia:
Modalità Ritratto standard:posiziona la scheda di svolta nella parte superiore dello schermo e la scheda ETA nella parte inferiore.
Modalità ampia: sposta la scheda di svolta sul lato iniziale dello schermo e la scheda ETA sul lato finale.
Controllo della modalità larga durante le transizioni di stato
Se il tuo layout supporta una variante in modalità estesa, valuta il valore booleano isWideMode() all'interno di ogni callback di transizione di stato, anziché fare affidamento solo su onSizeChanged().
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 bypassano completamente il callback di ridimensionamento, garantendo che la variante di layout standard o ampia corretta venga attivata nel momento in cui inizia un nuovo stato di navigazione.
Allineamento allo stile di Google
Per aiutare la tua UI personalizzata ad adattarsi 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 tue visualizzazioni ai componenti Google.
Ad esempio, se vuoi posizionare un pulsante personalizzato nell'angolo superiore 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.
Le spaziature interne e le misurazioni degli stili 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 eseguire l'inflate dei 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. */ 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()); } }
Per visualizzare il codice campione completo o esplorare altre implementazioni del delegato di layout, consulta gli esempi di delegato di layout su GitHub. Puoi anche visualizzare i singoli file di codice sorgente per questo esempio su GitHub: