
Usa la personalización del diseño para controlar la jerarquía de vistas y la ubicación en la pantalla de las vistas proporcionadas por Google y tus propias vistas empresariales personalizadas.
En lugar de agregar vistas personalizadas en espacios predefinidos de encabezado o pie de página, implementa un delegado de diseño para administrar el diseño de toda la pantalla. Durante las transiciones de estado de navegación, el delegado de diseño recibe los componentes proporcionados por Google, como la tarjeta de giro, la tarjeta de hora de llegada estimada y los botones. Ubica estos elementos junto con tu propio contenido personalizado usando sistemas de diseño estándar de Android, como ConstraintLayout, CoordinatorLayout o LinearLayout.
Usa este framework para posicionar información empresarial personalizada, como el estado del pedido o las instrucciones de retiro, de forma precisa en la pantalla y, al mismo tiempo, evitar que se superpongan tus vistas.
Cómo funciona la personalización del diseño
La personalización del diseño usa un patrón de diseño de delegación. En lugar de dibujar o posicionar automáticamente los componentes de la IU en la pantalla, el SDK los pasa directamente a una clase personalizada que escribes: el delegado de diseño.
Para personalizar tu diseño, crea una clase que extienda la clase abstractaNavigationLayoutDelegate y asigna una instancia a tu NavigationView o SupportNavigationFragment. Cada vez que cambia el estado de navegación, como cuando se pasa del mapa base a las instrucciones paso a paso activas, el SDK ejecuta los métodos de devolución de llamada en tu delegado y proporciona los componentes de la IU disponibles para ese estado específico.
Para proporcionar una experiencia del usuario integrada y fluida, este framework establece una separación clara de responsabilidades entre tu app y el SDK. Usa el delegado de diseño para personalizar lo siguiente:
- Compila la jerarquía de vistas: Elige exactamente qué componentes de Google y vistas empresariales personalizadas agregar a la pantalla para cada estado de la IU.
- Posiciona cada elemento: Establece anclajes, márgenes y ubicaciones de diseño precisos en la pantalla. Evita aplicar restricciones de ancho o altura personalizadas a los componentes de Google, ya que estos calculan sus propias dimensiones internas.
- Enmarcar el mapa base: Define los límites visibles de la cámara del mapa con las coordenadas del componente del viewport.
- Crea capas en la pantalla: Decide si las vistas personalizadas flotan por encima, por debajo o junto a los controles integrados de Google.
Mientras tanto, no puedes personalizar los siguientes componentes de Google con el delegado de diseño:
- Dimensiones de los componentes: El tamaño y las dimensiones internas de los componentes proporcionados por Google, que el SDK calcula automáticamente.
- Condiciones de activación: Cuándo aparecen las alertas o los mensajes dinámicos según los datos de la ruta en tiempo real.
Principios de implementación
Cuando escribas un delegado de diseño, ten en cuenta las siguientes reglas para evitar errores de diseño o fallas en el entorno de ejecución:
- La opción predeterminada es una pantalla vacía: Los componentes proporcionados por Google solo aparecen si tu delegado los agrega de forma explícita a la jerarquía de vistas y los posiciona.
- No se admiten las APIs heredadas: No se admitirán las propiedades ni los métodos que controlan el modelo de diseño basado en espacios heredado, y es posible que no se comporten como se espera cuando un delegado personalizado esté activo.
- No modifiques las estructuras de vistas internas:
No uses métodos como
findViewById()para recorrer o modificar las jerarquías de vistas de los componentes proporcionados por Google (como la tarjeta de giro o la tarjeta de hora de llegada estimada). Debido a que estas jerarquías de vistas internas son detalles de implementación subyacentes, están sujetas a cambios en las versiones del SDK. Si los modificas, es posible que el diseño se rompa durante futuras actualizaciones del SDK.
Compatibilidad con la API heredada de
Para obtener un comportamiento de diseño confiable cuando uses un delegado de diseño personalizado, evita usar las siguientes APIs heredadas basadas en ranuras, que se darán de baja. Para migrar una app existente, reemplaza el uso de estas APIs por código en tu delegado de diseño personalizado:
Mostrar las APIs heredadas basadas en ranuras
| API heredada | Reemplazo de Layout Delegate |
|---|---|
setCustomControl(View, CustomControlPosition) |
Agrega tus vistas directamente a tu ConstraintLayout o a otro grupo de vistas. |
removeCustomControl(View) |
Quitar vistas directamente de tu jerarquía de vistas |
setEtaCardEnabled(boolean) |
Lee la vista etaCard en onEnterActiveGuidance. |
setHeaderEnabled(boolean) |
Lee la vista turnCard en onEnterActiveGuidance. |
setReportIncidentButtonEnabled(boolean) |
Busca el botón REPORTING en getActiveGuidanceButtons(). |
setTripProgressBarEnabled(boolean) |
Lee la vista tripProgressBar en onEnterActiveGuidance. |
addOnNavigationUiChangedListener(...) |
Confía en tus devoluciones de llamada de transición de estado de NavigationLayoutDelegate. |
removeOnNavigationUiChangedListener(...) |
Administra las transiciones de estado del diseño directamente con el delegado. |
addPromptVisibilityChangedListener(...) |
Confía en tus devoluciones de llamada de instrucciones de NavigationLayoutDelegate (como onShowPrompt()). |
removePromptVisibilityChangedListener(...) |
Administra la lógica de visualización de mensajes directamente con el delegado. |
setCompassEnabled(boolean) |
Busca la brújula en getNavigationReadyButtons() o getActiveGuidanceButtons(). |
Lista de tareas de Essentials
Sigue estos pasos y requisitos esenciales para implementar correctamente tu delegado de diseño:
-
Inicializa el delegado antes de crear la IU: Llama a
setLayoutDelegate()antes de que el SDK inicialice la IU de navegación. Consulta los siguientes ejemplos de implementación de código para verificar el momento exacto de la configuración de la estructura de tu aplicación. Si inicializas el delegado después de crear la IU, se activará 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 tu propio contenedor de diseño: Crea un grupo de vistas personalizado, como un
ConstraintLayout, para contener tus elementos de la IU personalizados y las vistas proporcionadas por Google. -
Adjunta vistas obligatorias: Debes agregar el logotipo de Google (
getGoogleLogo()) y el viewport (getViewport()) a tu contenedor de diseño en todos los estados de navegación. Si no incluyes ambas vistas, se activará unApiIllegalStateExceptionde tiempo de ejecución y la app fallará. -
Respeta las dimensiones fijas: El SDK determina las dimensiones de varios componentes proporcionados por Google. No apliques restricciones de ancho o altura personalizados a los siguientes elementos:
- Tarjeta de turno
- tarjeta de hora estimada de llegada
- Instrucciones entrantes
- Logotipo de Google
- Widget de velocidad
-
Adjunta tu diseño a la vista: Llama a
navigationView.setNavigationLayout()dentro de tus devoluciones de llamadaonEnterNavigationReady()yonEnterActiveGuidance()para adjuntar tu contenedor a la jerarquía de vistas del mapa. -
Realiza una limpieza cuando se salga del estado: Llama a
navigationView.removeNavigationLayout()y quita tus vistas personalizadas dentro de tus devoluciones de llamadaonLeavepara evitar fugas de memoria y estados de IU superpuestos. -
Evita las APIs de diseño heredadas: No llames a las APIs obsoletas basadas en ranuras, como
setCustomControl()osetHeaderEnabled(), mientras un delegado personalizado esté activo. Es posible que el SDK ignore estas llamadas heredadas cuando se adjunte un delegado personalizado.
Estados de la IU y componentes de Google
Cuando se produce una transición de estado de navegación, el SDK pasa un objeto UiState de solo lectura a tu devolución de llamada de delegado. Este objeto agrupa las marcas de configuración de diseño actuales con los componentes de Google (p.ej., instancias de View) que necesitas para renderizar la pantalla.
El delegado de diseño administra la jerarquía de vistas en cuatro estados operativos. En el siguiente diagrama de estado, se ilustra cómo el SDK realiza la transición entre los estados de navegación y qué métodos de devolución de llamada ejecuta en tu delegado:
Los componentes específicos de Google que se proporcionan a tu delegado dependen de la fase actual del ciclo de vida de la navegación.
Componentes obligatorios (todos los estados de navegación)
Independientemente de la fase de navegación actual, debes incluir, posicionar y mantener visibles los siguientes componentes de Google en tu jerarquía de vistas:
Logotipo de Google (
getGoogleLogo()): Este componente muestra el logotipo obligatorio de Google Maps. Si el botón Volver a centrar está habilitado, el logotipo se transforma automáticamente en este botón cuando el conductor desplaza el mapa para alejarlo de su vehículo. Como resultado, la ubicación del logotipo determina dónde aparece el botón Volver a centrar. Se recomienda colocar el logotipo en la esquina inferior izquierda de tu diseño para que coincida con las expectativas estándar de la IU del mapa.Viewport (
getViewport()): Es unViewinvisible que define los límites de encuadre de la cámara. La ubicación del viewport determina exactamente dónde el SDK centra el ícono de contenido de formato de cheurón del vehículo y dibuja la línea de ruta activa. Posiciona el viewport de modo que cubra el área abierta y sin obstrucciones de la pantalla, evitando de forma segura las superposiciones opacas, como las hojas inferiores personalizadas.
Componentes listos para la navegación
Durante el estado de navegación lista, la IU se mantiene al mínimo para que el enfoque permanezca en el mapa base. Tu objeto NavigationReadyUiState proporciona acceso a lo siguiente:
- getNavigationReadyButtons(): Es una lista de vistas de acciones flotantes configuradas para el mapa base (por lo general, solo el botón de la brújula). Como Google puede agregar o reordenar botones en futuras versiones del SDK, evita depender de índices de listas fijos. En su lugar, itera la lista, lee el tipo de cada botón comparando
getType()conButtonKnownType.COMPASSy extrae la vista física llamando agetView(). Ten en cuenta que los botones exactos de esta lista difieren de los que están disponibles durante la guía activa.
Componentes de orientación activa
Cuando comienza la guía paso a paso, el SDK desbloquea el conjunto completo de controles de navegación. Tu objeto ActiveGuidanceUiState proporciona acceso a los siguientes componentes de Google:
getTurnCard(): Es el banner del encabezado principal que muestra las próximas instrucciones de maniobra, las mediciones de distancia y la guía de carriles. Colócala en la parte superior de tu diseño para establecer una jerarquía de navegación familiar y fijar tus vistas personalizadas alrededor de ella.getEtaCard(): Es el banner de pie de página que muestra la hora de llegada estimada, el tiempo restante del viaje y la distancia restante hasta el destino. Colócala en el borde inferior de la pantalla o integra sus coordenadas con tus hojas de administración de tareas personalizadas.getTripProgressBar(): Es una barra de progreso vertical que muestra al conductor la distancia que recorrió en la ruta actual. A diferencia de los diseños heredados que fijaban esto estrictamente al borde del mapa, tienes total libertad para fijarlo en cualquier lugar, por ejemplo, a lo largo de un borde lateral de tu contenedor personalizado.getSpeedWidget(): Es un control flotante para el velocímetro y los límites de velocidad publicados. Según la configuración de la API y la disponibilidad de los datos, esta vista cambia de tamaño de forma dinámica entre cuatro estados visuales durante el tiempo de ejecución (no se muestra nada, solo la velocidad actual, solo el límite de velocidad o ambas lecturas). Como el widget puede cambiar entre estos tamaños sin previo aviso, siempre fija las vistas circundantes con restricciones para que tu diseño se adapte automáticamente y evite las superposiciones espaciales.
getActiveGuidanceButtons(): Es una lista expandida de vistas de acciones flotantes para el estado de guía activo (por lo general, incluye la brújula y el botón de aviso de incidentes). Al igual que en el estado listo para la navegación, puedes ubicar y extraer vistas de botones individuales filtrando la lista porButtonKnownType(COMPASSoREPORTING) y extrayendo la vista congetView(). Luego, puedes posicionarlos de forma independiente o usar un diseño comoAutoHidingLinearLayoutpara apilar el array de forma segura sin conflictos espaciales.
Componentes de las instrucciones dinámicas
Los mensajes, como las alertas de incidentes o las advertencias de cámaras de seguridad, se activan de forma independiente durante la guía activa.
Cuando un mensaje está listo para mostrarse, el SDK llama a la devolución de llamada onShowPrompt() de tu delegado y pasa una vista newPrompt. Tu delegado es responsable de posicionar sin problemas este mensaje sobre tu diseño (por lo general, anclado al borde inferior del contenedor del mapa).
Debido a que el mensaje entrante cubre la parte inferior de la pantalla, debes actualizar tu diseño para evitar que el mensaje se superponga con el viewport, el logotipo de Google o cualquier botón alineado en la parte inferior.
Cómo controlar el tamaño de la pantalla y el modo ancho
Para controlar los cambios en las dimensiones del mapa y la orientación de la pantalla, el delegado de diseño usa las siguientes funciones:
Cambio de tamaño de la vista: Ajusta tu diseño cada vez que cambia el tamaño físico del mapa.
Modo ancho: Cambia a una variante de diseño de formato ancho cuando el mapa es lo suficientemente ancho.
Cómo responder al cambio de tamaño de la vista
Cada vez que cambian las dimensiones físicas del contenedor del mapa, el SDK ejecuta la devolución de llamadaonSizeChanged(). Los diseños de pantalla dividida, los controles deslizantes de diseño y la rotación del dispositivo suelen activar la devolución de llamada onSizeChanged(). Usa esta devolución de llamada para realizar ajustes responsivos generales en tu IU personalizada. Implementa onSizeChanged() para cambiar la posición de los elementos personalizados para una nueva relación de aspecto de la pantalla, aplica tus propios puntos de interrupción de ancho o alto personalizados, y detecta cuándo un evento de cambio de tamaño activa el modo ancho para intercambiar de forma segura las variantes de diseño.
Información sobre el modo ancho
El modo ancho se activa cuando el contenedor del mapa es lo suficientemente ancho como para mostrar los componentes de la IU en paralelo.
Lee el valor booleano isWideMode() del objeto de estado para cambiar la posición de tus elementos de la IU personalizados y mantener el centro del mapa despejado para el conductor. Debido a que los componentes proporcionados por Google, como la tarjeta de giro y la tarjeta de hora de llegada estimada, se reducen y cambian de forma automáticamente en el modo ancho, leer este valor booleano garantiza que tu diseño se adapte en el mismo momento en que se actualizan los componentes de Google.
Considera los siguientes ejemplos para posicionar los componentes de tu diseño en los modos estándar y ancho:
Modo Retrato estándar: Coloca la tarjeta de giro en la parte superior de la pantalla y la tarjeta de hora de llegada en la parte inferior.
Modo ancho: Mueve la tarjeta de giro al lado de inicio de la pantalla y la tarjeta de hora de llegada estimada al lado de finalización.
Cómo verificar el modo ancho durante las transiciones de estado
Si tu diseño admite una variación de modo amplio, evalúa el booleano isWideMode() dentro de cada devolución de llamada de transición de estado, en lugar de depender solo de onSizeChanged().
onEnterNavigationReady() y onEnterActiveGuidance(), para controlar los inicios iniciales de la app en modo horizontal. Este enfoque protege tu diseño contra los eventos del ciclo de vida del sistema Android, como la recreación predeterminada de la actividad, que omiten por completo la devolución de llamada de cambio de tamaño, lo que garantiza que se active la variante de diseño estándar o amplia correcta en el momento en que comienza un nuevo estado de navegación.
Alineación con el estilo de Google
Para ayudar a que tu IU personalizada coincida con el ritmo visual de Google, el SDK proporciona la clase de utilidad StyleValues. Puedes leer estos valores en píxeles independientes de la densidad (dp) para alinear perfectamente tus vistas con los componentes de Google.
Por ejemplo, si deseas colocar un botón personalizado en la esquina superior de la pantalla, frente a la tarjeta de giro, puedes llamar a StyleValues.headerTopPaddingDp() y asignar el valor que se muestra como el margen superior del botón. Esto garantiza que el botón personalizado se alinee visualmente con el borde superior de la tarjeta de giro, lo que mantiene la simetría de la pantalla.
Los rellenos y las medidas de estilo disponibles incluyen los siguientes:
StyleValues.headerNominalHeightDp()StyleValues.headerTopPaddingDp()StyleValues.headerFooterSidePaddingDp()StyleValues.mapControlSidePaddingDp()StyleValues.buttonMapControlSidePaddingDp()
Ejemplo: Implementación de un diseño basado en restricciones
En el siguiente ejemplo, se muestra un delegado de diseño básico que administra las transiciones de estado con definiciones programáticas de ConstraintLayout y ConstraintSet.
Aunque en este ejemplo se compilan restricciones de vista en el código, los delegados de diseño también pueden aumentar los diseños XML estándar de Android.
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()); } }
Para ver el código de muestra completo o explorar otras implementaciones de delegados de diseño, consulta las muestras de delegados de diseño en GitHub. También puedes ver los archivos de origen individuales de este ejemplo en GitHub: