
Mit der Layoutanpassung können Sie die Ansichtshierarchie und die Bildschirmplatzierung sowohl für von Google bereitgestellte Ansichten als auch für Ihre eigenen benutzerdefinierten Geschäftsansichten steuern.
Anstatt benutzerdefinierte Ansichten in vordefinierte Header- oder Footer-Slots einzufügen, implementieren Sie einen Layout-Delegaten, um das Layout für den gesamten Bildschirm zu verwalten. Bei Zustandsübergängen während der Navigation erhält Ihr Layout-Delegate die von Google bereitgestellten Komponenten, z. B. die Abbiegekarte, die Karte mit der voraussichtlichen Ankunftszeit und die Schaltflächen. Positionieren Sie diese Elemente neben Ihren eigenen benutzerdefinierten Inhalten mit Standard-Android-Layoutsystemen wie ConstraintLayout, CoordinatorLayout oder LinearLayout.
Mit diesem Framework können Sie benutzerdefinierte Geschäftsinformationen wie den Bestellstatus oder Abholanweisungen genau auf dem Bildschirm positionieren und gleichzeitig verhindern, dass sich Ihre Ansichten überschneiden.
Funktionsweise der Layoutanpassung
Für die Layoutanpassung wird ein Delegationsentwurfsmuster verwendet. Anstatt UI-Komponenten automatisch auf dem Bildschirm zu zeichnen oder zu positionieren, übergibt das SDK sie direkt an eine von Ihnen geschriebene benutzerdefinierte Klasse: den Layout-Delegaten.
Wenn Sie das Layout anpassen möchten, erstellen Sie eine Klasse, die die abstrakte KlasseNavigationLayoutDelegate erweitert, und weisen Sie Ihrem NavigationView oder SupportNavigationFragment eine Instanz zu. Wenn sich der Navigationsstatus ändert, z. B. von der Basiskarte zur aktiven detaillierten Routenführung, führt das SDK die Callback-Methoden für Ihren Delegaten aus und stellt die für diesen Status verfügbaren UI-Komponenten bereit.
Um eine nahtlose, integrierte Nutzererfahrung zu bieten, wird mit diesem Framework eine klare Trennung der Verantwortlichkeiten zwischen Ihrer App und dem SDK festgelegt. Mit dem Layout-Delegat können Sie Folgendes anpassen:
- Ansichtshierarchie erstellen:Wählen Sie genau aus, welche Google-Komponenten und benutzerdefinierten Geschäftsansichten für jeden UI-Status auf dem Bildschirm angezeigt werden sollen.
- Jedes Element positionieren:Legen Sie genaue Bildschirmanker, Ränder und Layoutplatzierungen fest. Vermeiden Sie es, benutzerdefinierte Breiten- oder Höhenbeschränkungen auf Google-Komponenten anzuwenden, da Google-Komponenten ihre eigenen internen Abmessungen berechnen.
- Basiskarte einrahmen:Definieren Sie die sichtbaren Grenzen der Kartenkamera mithilfe der Koordinaten der Darstellungsbereichskomponente.
- Ebenen auf dem Bildschirm:Legen Sie fest, ob benutzerdefinierte Ansichten über, unter oder neben den integrierten Steuerelementen von Google angezeigt werden sollen.
Die folgenden Google-Komponenten können nicht mit dem Layout-Delegat angepasst werden:
- Komponentenabmessungen:Die Größe und die internen Abmessungen der von Google bereitgestellten Komponenten, die vom SDK automatisch berechnet werden.
- Auslösebedingungen:Wann dynamische Benachrichtigungen oder Aufforderungen basierend auf Echtzeit-Routendaten angezeigt werden.
Implementierungsprinzipien
Beachten Sie beim Schreiben eines Layout-Delegaten die folgenden Regeln, um Layoutfehler oder Laufzeitabstürze zu vermeiden:
- Standardmäßig wird ein leerer Bildschirm angezeigt:Von Google bereitgestellte Komponenten werden nur angezeigt, wenn sie von Ihrem Bevollmächtigten explizit der Ansichtshierarchie hinzugefügt und positioniert werden.
- Legacy-APIs werden nicht unterstützt:Properties und Methoden, die das Legacy-Slot-basierte Layoutmodell steuern, werden nicht unterstützt und verhalten sich möglicherweise nicht wie erwartet, wenn ein benutzerdefinierter Delegat aktiv ist.
- Interne Ansichtsstrukturen nicht ändern:Verwenden Sie keine Methoden wie
findViewById(), um die Ansichtshierarchien von Google-Komponenten (z. B. die Abbiegekarte oder die Karte mit der voraussichtlichen Ankunftszeit) zu durchlaufen oder zu ändern. Da diese internen Ansichtshierarchien zugrunde liegende Implementierungsdetails sind, können sie sich bei SDK-Releases ändern. Wenn Sie sie ändern, kann es bei zukünftigen SDK-Updates zu Fehlern im Layout kommen.
-Kompatibilität mit Legacy-API
Wenn Sie einen benutzerdefinierten Layout-Delegaten verwenden, sollten Sie die folgenden alten slotbasierten APIs nicht verwenden, da sie eingestellt werden sollen. Wenn Sie eine vorhandene App migrieren möchten, ersetzen Sie die Verwendung dieser APIs durch Code in Ihrem benutzerdefinierten Layout-Delegat:
Legacy-Slot-basierte APIs anzeigen
| Legacy API | Layout-Delegate-Ersatz |
|---|---|
setCustomControl(View, CustomControlPosition) |
Fügen Sie Ihre Ansichten direkt der Gruppe ConstraintLayout oder einer anderen Ansichtsgruppe hinzu. |
removeCustomControl(View) |
Ansichten direkt aus der Ansichtshierarchie entfernen |
setEtaCardEnabled(boolean) |
Lesen Sie die Ansicht etaCard in onEnterActiveGuidance. |
setHeaderEnabled(boolean) |
Lesen Sie die Ansicht turnCard in onEnterActiveGuidance. |
setReportIncidentButtonEnabled(boolean) |
Suche in getActiveGuidanceButtons() nach der Taste REPORTING. |
setTripProgressBarEnabled(boolean) |
Lesen Sie die Ansicht tripProgressBar in onEnterActiveGuidance. |
addOnNavigationUiChangedListener(...) |
Verlassen Sie sich auf die Callbacks für Zustandsübergänge von NavigationLayoutDelegate. |
removeOnNavigationUiChangedListener(...) |
Layoutstatusübergänge direkt über den Delegaten verwalten. |
addPromptVisibilityChangedListener(...) |
Verlassen Sie sich auf Ihre NavigationLayoutDelegate-Prompt-Callbacks (z. B. onShowPrompt()). |
removePromptVisibilityChangedListener(...) |
Anzeigelogik für Aufforderungen direkt über den Delegaten verwalten |
setCompassEnabled(boolean) |
Suche den Kompass in getNavigationReadyButtons() oder getActiveGuidanceButtons(). |
Essentials-Checkliste
Folgen Sie diesen wichtigen Schritten und Anforderungen, um Ihren Layout-Delegaten erfolgreich zu implementieren:
-
Delegat vor dem Erstellen der Benutzeroberfläche initialisieren:Rufen Sie
setLayoutDelegate()auf, bevor das SDK die Navigations-Benutzeroberfläche initialisiert. In den folgenden Codebeispielen sehen Sie, wie Sie den genauen Zeitpunkt für die Einrichtung Ihrer Anwendungsstruktur prüfen. Wenn der Bevollmächtigte nach der Erstellung der Benutzeroberfläche initialisiert wird, wird einApiIllegalStateExceptionausgelöst.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);
-
Eigenen Layoutcontainer erstellen:Erstellen Sie eine benutzerdefinierte Ansichtsgruppe, z. B. ein
ConstraintLayout, um Ihre benutzerdefinierten UI-Elemente und die von Google bereitgestellten Ansichten aufzunehmen. -
Obligatorische Ansichten anhängen:Sie müssen das Google-Logo (
getGoogleLogo()) und den Viewport (getViewport()) in allen Navigationsstatus zu Ihrem Layoutcontainer hinzufügen. Wenn Sie nicht beide Ansichten einfügen, wird zur Laufzeit einApiIllegalStateExceptionausgelöst und die App stürzt ab. -
Feste Abmessungen berücksichtigen:Das SDK bestimmt die Abmessungen für mehrere von Google bereitgestellte Komponenten. Wenden Sie keine benutzerdefinierten Breiten- oder Höhenbeschränkungen auf die folgenden Elemente an:
- Karte umdrehen
- voraussichtliche-Ankunft-Karte
- Eingehende Prompts
- Google-Logo
- Widget für die Geschwindigkeit
-
Layout an die Ansicht anhängen:Rufen Sie
navigationView.setNavigationLayout()in denonEnterNavigationReady()- undonEnterActiveGuidance()-Callbacks auf, um den Container an die Hierarchie der Kartenansicht anzuhängen. -
Bereinigung beim Verlassen des Status:Rufen Sie
navigationView.removeNavigationLayout()auf und entfernen Sie Ihre benutzerdefinierten Ansichten in IhrenonLeave-Callbacks, um Speicherlecks und überlappende UI-Status zu vermeiden. -
Vermeiden Sie Legacy-Layout-APIs: Rufen Sie keine veralteten slotbasierten APIs wie
setCustomControl()odersetHeaderEnabled()auf, während ein benutzerdefiniertes Delegat aktiv ist. Das SDK ignoriert diese Legacy-Aufrufe möglicherweise, wenn ein benutzerdefinierter Delegat angehängt ist.
UI-Zustände und Google-Komponenten
Wenn ein Navigationsstatusübergang stattfindet, übergibt das SDK ein schreibgeschütztes UiState-Objekt an den Callback des Delegaten. Dieses Objekt bündelt die aktuellen Layoutkonfigurations-Flags mit den Google-Komponenten (z.B. View-Instanzen), die Sie zum Rendern des Bildschirms benötigen.
Der Layout-Delegate verwaltet die Ansichtshierarchie in vier Betriebszuständen. Das folgende Zustandsdiagramm veranschaulicht, wie das SDK zwischen Navigationszuständen wechselt und welche Callback-Methoden es für Ihren Delegaten ausführt:
Welche Google-Komponenten Ihrem Delegaten zur Verfügung gestellt werden, hängt von der aktuellen Phase des Navigationslebenszyklus ab.
Erforderliche Komponenten (alle Navigationsstatus)
Unabhängig von der aktuellen Navigationsphase müssen Sie die folgenden Google-Komponenten in Ihre Ansichtshierarchie einfügen, positionieren und sichtbar halten:
Google-Logo (
getGoogleLogo()): Diese Komponente zeigt das obligatorische Google Maps-Logo an. Wenn die Schaltfläche Neu zentrieren aktiviert ist, wird das Logo automatisch in diese Schaltfläche umgewandelt, wenn der Fahrer die Karte von seinem Fahrzeug wegscrollt. Die Platzierung des Logos bestimmt also, wo die Schaltfläche Neu zentrieren angezeigt wird. Es wird empfohlen, das Logo in der unteren linken Ecke des Layouts zu platzieren, um den Erwartungen an die Standard-Kartenoberfläche zu entsprechen.Darstellungsbereich (
getViewport()): Ein unsichtbaresView, das die Grenzen des Kamera-Framings definiert. Die Platzierung des Darstellungsbereichs bestimmt genau, wo das SDK das Fahrzeugsymbol zentriert und die aktive Routenlinie zeichnet. Positionieren Sie den Darstellungsbereich so, dass er den offenen, nicht verdeckten Bereich des Bildschirms abdeckt. Vermeiden Sie dabei undurchsichtige Overlays wie benutzerdefinierte Ansichten am unteren Rand.
navigationsbereite Komponenten
Im navigationsbereiten Zustand bleibt die Benutzeroberfläche minimal, damit der Fokus auf der Basiskarte bleibt. Das NavigationReadyUiState-Objekt bietet Zugriff auf Folgendes:
- getNavigationReadyButtons(): Eine Liste der unverankerten Aktionsansichten, die für die Basiskarte konfiguriert sind (normalerweise nur die Kompass-Schaltfläche). Da Google in zukünftigen SDK-Releases möglicherweise Schaltflächen hinzufügt oder neu anordnet, sollten Sie sich nicht auf feste Listenindexe verlassen. Iteriere stattdessen durch die Liste, lies den Typ jeder Taste, indem du
getType()mitButtonKnownType.COMPASSvergleichst, und extrahiere die physische Ansicht, indem dugetView()aufrufst. Die genauen Schaltflächen in dieser Liste unterscheiden sich von denen, die während der aktiven Zielführung verfügbar sind.
aktive Anleitungskomponenten
Sobald die detaillierte Routenführung beginnt, werden im SDK alle Navigationssteuerelemente freigeschaltet. Das ActiveGuidanceUiState-Objekt bietet Zugriff auf die folgenden Google-Komponenten:
getTurnCard(): Das primäre Headerbanner, das die nächsten Fahranweisungen, Entfernungsangaben und die Spurführung anzeigt. Platzieren Sie sie oben im Layout, um eine vertraute Navigationshierarchie zu schaffen und Ihre benutzerdefinierten Ansichten darum herum zu verankern.getEtaCard():Das Banner in der Fußzeile, das die voraussichtliche Ankunftszeit, die verbleibende Fahrzeit und die verbleibende Entfernung zum Ziel anzeigt. Platzieren Sie sie am unteren Rand des Bildschirms oder integrieren Sie die Koordinaten in Ihre benutzerdefinierten Tabellenblätter für die Aufgabenverwaltung.getTripProgressBar(): Eine vertikale Fortschrittsanzeige, die dem Fahrer zeigt, wie weit er auf der aktuellen Route bereits gefahren ist. Im Gegensatz zu älteren Layouts, bei denen das Logo fest am Kartenrand verankert war, können Sie es jetzt an einer beliebigen Stelle verankern, z. B. am Seitenrand Ihres benutzerdefinierten Containers.getSpeedWidget(): Ein schwebendes Steuerelement für den Tachometer und die angezeigten Geschwindigkeitsbegrenzungen. Je nach API-Einstellungen und Datenverfügbarkeit wird die Größe dieser Ansicht zur Laufzeit dynamisch zwischen vier visuellen Zuständen angepasst (keine Anzeige, nur die aktuelle Geschwindigkeit, nur die Geschwindigkeitsbegrenzung oder beide Messwerte). Da das Widget ohne Vorankündigung zwischen diesen Größen wechseln kann, sollten Sie die umliegenden Ansichten immer mit Einschränkungen verankern, damit sich Ihr Layout automatisch anpasst und räumliche Überschneidungen vermieden werden.
getActiveGuidanceButtons():Eine erweiterte Liste von unverankerten Aktionsansichten für den aktiven Navigationsstatus (normalerweise einschließlich des Kompass- und des Verkehrsmeldungs-Buttons). Wie im Status „Bereit für die Navigation“ können Sie einzelne Schaltflächenansichten finden und extrahieren, indem Sie die Liste nachButtonKnownType(COMPASSoderREPORTING) filtern und die Ansicht mitgetView()extrahieren. Sie können sie dann unabhängig voneinander positionieren oder ein Layout wieAutoHidingLinearLayoutverwenden, um das Array ohne räumliche Konflikte sicher zu stapeln.
Komponenten für dynamische Prompts
Aufforderungen wie Unfallmeldungen oder Warnungen vor Blitzern werden während der aktiven Zielführung unabhängig voneinander ausgelöst.
Wenn eine Aufforderung angezeigt werden kann, ruft das SDK den Callback onShowPrompt() des Bevollmächtigten auf und übergibt eine newPrompt-Ansicht. Der Delegate ist dafür verantwortlich, diese Aufforderung reibungslos über dem Layout zu positionieren (normalerweise am unteren Rand des Kartencontainers verankert).
Da die eingehende Aufforderung den unteren Teil des Bildschirms abdeckt, müssen Sie Ihr Layout aktualisieren, damit die Aufforderung nicht den Darstellungsbereich, das Google-Logo oder unten ausgerichtete Schaltflächen überlappt.
Bildschirmgröße und Breitbildmodus
Um Änderungen an den Kartenabmessungen und der Bildschirmausrichtung zu verarbeiten, verwendet der Layout-Delegate die folgenden Funktionen:
Größenanpassung der Ansicht:Das Layout wird angepasst, wenn sich die physische Größe der Karte ändert.
Breitbildmodus:Es wird zu einer Layoutvariante im Breitbildformat gewechselt, wenn die Karte breit genug ist.
Auf Größenänderungen der Ansicht reagieren
Wenn sich die physischen Abmessungen des Kartencontainers ändern, führt das SDK den CallbackonSizeChanged() aus. Splitscreen-Layouts, Layout-Schieberegler und Gerätedrehungen lösen häufig den Callback onSizeChanged() aus. Verwenden Sie diesen Callback, um allgemeine responsive Anpassungen an Ihrer benutzerdefinierten UI vorzunehmen. Implementiere onSizeChanged(), um benutzerdefinierte Elemente für ein neues Seitenverhältnis des Bildschirms neu zu positionieren, eigene benutzerdefinierte Breiten- oder Höhen-Breakpoints anzuwenden und zu erkennen, wenn ein Ereignis zur Größenänderung den Breitbildmodus umschaltet, um Layoutvarianten sicher zu wechseln.
Informationen zum Breitbildmodus
Der breite Modus wird aktiviert, wenn der Kartencontainer breit genug ist, um UI-Komponenten nebeneinander anzuzeigen.
Lesen Sie den booleschen Wert isWideMode() aus dem Zustandsobjekt, um Ihre benutzerdefinierten UI-Elemente neu zu positionieren und die Mitte der Karte für den Fahrer freizuhalten. Da von Google bereitgestellte Komponenten wie die Abbiege- und die voraussichtliche-Ankunft-Karte im breiten Modus automatisch verkleinert und umgeformt werden, sorgt das Lesen dieses booleschen Werts dafür, dass sich Ihr Layout genau im selben Moment anpasst, in dem die Google-Komponenten aktualisiert werden.
Sehen Sie sich die folgenden Beispiele für die Positionierung Ihrer Layoutkomponenten im Standard- und Breitbildmodus an:
Standard-Porträtmodus:Die Abbiegekarte wird oben auf dem Bildschirm und die voraussichtliche-Ankunft-Karte unten angezeigt.
Breitbildmodus:Verschieben Sie die Abbiegekarte auf die Startseite des Bildschirms und die voraussichtliche-Ankunft-Karte auf die Endseite.
Breitbildmodus bei Statusübergängen prüfen
Wenn dein Layout eine Variante für den breiten Modus unterstützt, solltest du den booleschen Wert isWideMode() in jedem Zustandsübergangs-Callback auswerten, anstatt dich nur auf onSizeChanged() zu verlassen.
onEnterNavigationReady() und onEnterActiveGuidance(), um den ersten App-Start im Querformat zu verarbeiten. Dieser Ansatz schützt Ihr Layout vor Android-Systemlebenszyklusereignissen wie der standardmäßigen Neuerstellung von Aktivitäten, die den Callback zur Größenänderung vollständig umgehen. So wird sichergestellt, dass die richtige Standard- oder Breitbild-Layoutvariante aktiviert wird, sobald ein neuer Navigationsstatus beginnt.
An Google-Stil anpassen
Damit Ihre benutzerdefinierte Benutzeroberfläche dem visuellen Rhythmus von Google entspricht, stellt das SDK die Dienstprogrammklasse StyleValues bereit. Sie können diese Werte in dichteunabhängigen Pixeln (dp) ablesen, um Ihre Ansichten perfekt an Google-Komponenten anzupassen.
Wenn Sie beispielsweise eine benutzerdefinierte Schaltfläche in der oberen Ecke des Bildschirms gegenüber der Abbiegekarte platzieren möchten, können Sie StyleValues.headerTopPaddingDp() aufrufen und den zurückgegebenen Wert als oberen Rand der Schaltfläche zuweisen. So wird die benutzerdefinierte Schaltfläche visuell am oberen Rand der Abbiegekarte ausgerichtet und die Symmetrie des Bildschirms bleibt erhalten.
Verfügbare Abstände und Maße:
StyleValues.headerNominalHeightDp()StyleValues.headerTopPaddingDp()StyleValues.headerFooterSidePaddingDp()StyleValues.mapControlSidePaddingDp()StyleValues.buttonMapControlSidePaddingDp()
Beispiel: Implementierung eines einschränkungsbasierten Layouts
Das folgende Beispiel zeigt einen einfachen Layout-Delegaten, der Zustandsübergänge mithilfe von programmatischen ConstraintLayout- und ConstraintSet-Definitionen verwaltet.
In diesem Beispiel werden Ansichtseinschränkungen im Code erstellt. Layout-Delegierte können aber auch Standard-Android-XML-Layouts aufblähen.
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()); } }
Den vollständigen Beispielcode und weitere Implementierungen von Layout-Delegaten finden Sie in den Beispielen für Layout-Delegaten auf GitHub. Sie können sich die einzelnen Quelldateien für dieses Beispiel auch auf GitHub ansehen: