Настройте расположение элементов навигации.

Пример анимации, демонстрирующей настраиваемый макет навигации на Android.

Используйте настройку макета, чтобы управлять иерархией представлений и размещением элементов на экране как для представлений, предоставляемых Google, так и для ваших собственных пользовательских бизнес-представлений.

Вместо добавления пользовательских элементов в предопределенные слоты заголовка или нижнего колонтитула, реализуйте делегат компоновки для управления компоновкой всего экрана. Во время переходов между состояниями навигации ваш делегат компоновки получает предоставленные Google компоненты, такие как карточка поворота, карточка ETA и кнопки. Разместите эти элементы рядом с собственным пользовательским контентом, используя стандартные системы компоновки Android, такие как ConstraintLayout , CoordinatorLayout или LinearLayout .

Используйте эту структуру для точного размещения на экране пользовательской информации о бизнесе, такой как статус заказа или инструкции по получению, предотвращая при этом наложение элементов интерфейса.

Как работает настройка макета

Настройка макета осуществляется с помощью шаблона проектирования «делегирование». Вместо автоматической отрисовки или позиционирования компонентов пользовательского интерфейса на экране, SDK передает их непосредственно созданному вами пользовательскому классу: делегату макета.

Для настройки макета создайте класс, расширяющий абстрактный класс NavigationLayoutDelegate , и назначьте его экземпляр вашему NavigationView или SupportNavigationFragment . При каждом изменении состояния навигации — например, при переходе от базовой карты к активной пошаговой навигации — SDK выполняет методы обратного вызова вашего делегата и предоставляет компоненты пользовательского интерфейса, доступные для этого конкретного состояния.

Для обеспечения бесшовного и интегрированного пользовательского опыта эта платформа устанавливает четкое разделение ответственности между вашим приложением и SDK. Используйте делегат макета для настройки следующих параметров:

  • Создайте иерархию представлений: выберите, какие именно компоненты Google и пользовательские бизнес-представления следует добавить на экран для каждого состояния пользовательского интерфейса.
  • Расположите каждый элемент: задайте точные привязки к экрану, поля и размещение элементов в макете. Избегайте применения пользовательских ограничений по ширине или высоте к компонентам Google, поскольку компоненты Google рассчитывают свои собственные внутренние размеры.
  • Настройка базовой карты: Определите видимые границы камеры карты, используя координаты компонента области просмотра.
  • Настройка отображения: решите, будут ли пользовательские элементы управления располагаться поверх, под или рядом со встроенными элементами управления Google.

Между тем, вы не можете настроить следующие компоненты Google с помощью делегата макета:

  • Размеры компонентов: Размер и внутренние габариты компонентов, предоставляемых Google, которые SDK рассчитывает автоматически.
  • Условия срабатывания: появление динамических оповещений или подсказок на основе данных о маршруте в реальном времени.

Принципы реализации

При написании делегата компоновки следует помнить о следующих правилах, чтобы предотвратить ошибки компоновки или сбои во время выполнения:

  • По умолчанию отображается пустой экран: компоненты, предоставляемые Google, появляются только в том случае, если ваш делегат явно добавляет их в иерархию представлений и позиционирует их.
  • Устаревшие API не поддерживаются: свойства и методы, управляющие устаревшей моделью компоновки на основе слотов, не будут поддерживаться и могут работать не так, как ожидается, при активном пользовательском делегате.
  • Не изменяйте внутренние структуры представлений: не используйте такие методы, как findViewById() для обхода или изменения иерархии представлений компонентов, предоставляемых Google (например, карточки с поворотом или карточки с расчетным временем прибытия). Поскольку эти внутренние иерархии представлений являются базовыми деталями реализации, они могут изменяться в разных версиях SDK. Их изменение может привести к нарушению работы макета при будущих обновлениях SDK.

Совместимость с устаревшим API

Для обеспечения надежной работы компоновки при использовании пользовательского делегата компоновки избегайте использования следующих устаревших API на основе слотов, которые планируется вывести из эксплуатации. Для миграции существующего приложения замените использование этих API кодом в вашем пользовательском делегате компоновки:

Показать устаревшие API на основе слотов

Устаревший API Замена делегата по компоновке
setCustomControl(View, CustomControlPosition) Добавляйте ваши представления непосредственно в ConstraintLayout или другую группу представлений.
removeCustomControl(View) Удаляйте представления непосредственно из иерархии представлений.
setEtaCardEnabled(boolean) Прочитайте описание представления etaCard в onEnterActiveGuidance .
setHeaderEnabled(boolean) Прочитайте описание turnCard в onEnterActiveGuidance .
setReportIncidentButtonEnabled(boolean) Найдите кнопку REPORTING в getActiveGuidanceButtons() .
setTripProgressBarEnabled(boolean) Прочитайте представление tripProgressBar в onEnterActiveGuidance .
addOnNavigationUiChangedListener(...) Полагайтесь на коллбэки перехода состояний вашего NavigationLayoutDelegate .
removeOnNavigationUiChangedListener(...) Управляйте переходами состояний макета непосредственно с помощью делегата.
addPromptVisibilityChangedListener(...) Полагайтесь на функции обратного вызова вашего NavigationLayoutDelegate (например, onShowPrompt() ).
removePromptVisibilityChangedListener(...) Управляйте логикой отображения подсказок непосредственно с помощью делегата.
setCompassEnabled(boolean) Найдите компас в getNavigationReadyButtons() или getActiveGuidanceButtons() .

Контрольный список необходимых вещей

Для успешной реализации делегата компоновки выполните следующие необходимые шаги и выполните следующие требования:

  • Инициализируйте делегат перед созданием пользовательского интерфейса: вызовите метод setLayoutDelegate() до того, как SDK инициализирует навигационный интерфейс. См. следующие примеры реализации кода, чтобы проверить точное время настройки для вашей структуры приложения. Инициализация делегата после создания пользовательского интерфейса вызывает исключение ApiIllegalStateException .

    Котлин

    // 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);
            
  • Создайте собственный контейнер макета: создайте пользовательскую группу представлений — например, ConstraintLayout — для размещения ваших пользовательских элементов интерфейса и представлений, предоставленных Google.

  • Добавьте обязательные элементы: необходимо добавить логотип Google ( getGoogleLogo() ) и область просмотра ( getViewport() ) в контейнер макета во всех состояниях навигации. Отсутствие обоих этих элементов вызовет исключение ApiIllegalStateException во время выполнения и приведет к сбою приложения.

  • Соблюдайте фиксированные размеры: SDK определяет размеры для ряда компонентов, предоставляемых Google. Не применяйте пользовательские ограничения по ширине или высоте к следующим элементам:

    • Поворот карты
    • карта ETA
    • Входящие запросы
    • логотип Google
    • Виджет скорости
  • Прикрепите свой макет к представлению: вызовите navigationView.setNavigationLayout() внутри ваших коллбэков onEnterNavigationReady() и onEnterActiveGuidance() , чтобы прикрепить ваш контейнер к иерархии представления карты.

  • Очистка памяти при выходе из состояния: вызовите navigationView.removeNavigationLayout() и удалите свои пользовательские представления внутри коллбэков onLeave , чтобы предотвратить утечки памяти и наложение состояний пользовательского интерфейса.

  • Избегайте устаревших API компоновки: не вызывайте устаревшие API на основе слотов, такие как setCustomControl() или setHeaderEnabled() пока активен пользовательский делегат. SDK может игнорировать эти устаревшие вызовы, если пользовательский делегат подключен.

Состояния пользовательского интерфейса и компоненты Google

При переходе между состояниями навигации SDK передает в функцию обратного вызова делегата объект UiState , доступный только для чтения. Этот объект объединяет текущие флаги конфигурации макета с компонентами Google (например, экземплярами View ), необходимыми для отображения экрана.

Делегат компоновки управляет иерархией представлений в четырех операционных состояниях. Следующая диаграмма состояний иллюстрирует, как SDK переходит между состояниями навигации и какие методы обратного вызова он выполняет в вашем делегате:

Диаграмма переходов состояний жизненного цикла NavigationLayoutDelegate, показывающая переходы между состояниями «Без навигации», «Готов к навигации» и «Активное руководство».

Конкретные компоненты Google, предоставляемые вашему представителю, зависят от текущего этапа жизненного цикла навигации.

Необходимые компоненты (все состояния навигации)

Вне зависимости от текущей фазы навигации, необходимо включить, расположить и обеспечить видимость следующих компонентов Google в иерархии представлений:

  • Логотип Google ( getGoogleLogo() ): Этот компонент отображает обязательный логотип Google Maps. Если кнопка «Перецентрировать» включена, логотип автоматически преобразуется в эту кнопку, когда водитель прокручивает карту, отводя взгляд от своего автомобиля. В результате, расположение логотипа определяет местоположение кнопки «Перецентрировать» . Рекомендуется размещать логотип в нижнем левом углу макета, чтобы соответствовать стандартным ожиданиям пользовательского интерфейса карты.

  • Viewport ( getViewport() ): Невидимый View , определяющий границы кадрирования камеры. Ваше расположение Viewport точно определяет, где SDK центрирует стрелку транспортного средства и рисует активную линию маршрута. Расположите Viewport так, чтобы он покрывал открытую, незатененную область экрана, избегая непрозрачных наложений, таких как пользовательские нижние панели.

В состоянии готовности к навигации пользовательский интерфейс остается минимальным, чтобы фокус оставался на базовой карте. Объект NavigationReadyUiState предоставляет доступ к следующим функциям:

  • getNavigationReadyButtons(): Список плавающих элементов управления, настроенных для базовой карты (обычно только кнопка «Компас»). Поскольку Google может добавлять или изменять порядок кнопок в будущих версиях SDK, избегайте использования фиксированных индексов списка. Вместо этого пройдитесь по списку, определите тип каждой кнопки, сравнив getType() с ButtonKnownType.COMPASS , и извлеките физическое представление, вызвав getView() . Обратите внимание, что точные кнопки в этом списке отличаются от тех, которые доступны во время активного навигации.

Активные компоненты навигации

Когда запускается пошаговая навигация, SDK открывает полный набор элементов управления навигацией. Ваш объект ActiveGuidanceUiState предоставляет доступ к следующим компонентам Google:

  • getTurnCard() : Основной заголовок, отображающий предстоящие указания по маневрированию, измерения расстояния и подсказки по полосам движения. Разместите его в верхней части макета, чтобы создать привычную иерархию навигации и закрепить вокруг него ваши пользовательские представления.

  • getEtaCard() : Нижний баннер, отображающий расчетное время прибытия, оставшееся время в пути и оставшееся расстояние до пункта назначения. Разместите его вдоль нижнего края экрана или интегрируйте его координаты с вашими пользовательскими таблицами управления задачами.

  • getTripProgressBar() : Вертикальный индикатор выполнения, показывающий водителю, какое расстояние он проехал по текущему маршруту. В отличие от устаревших макетов, которые фиксировали его строго по краю карты, здесь вы можете прикрепить его где угодно — например, вдоль бокового края вашего пользовательского контейнера.

  • getSpeedWidget() : Плавающий элемент управления для отображения спидометра и установленных ограничений скорости. В зависимости от настроек API и доступности данных, этот элемент динамически изменяет свой размер, переключаясь между четырьмя визуальными состояниями во время выполнения (отображение пустого пространства, только текущей скорости, только ограничения скорости или обоих показаний). Поскольку виджет может изменять размер без предупреждения, всегда привязывайте окружающие элементы с помощью ограничений, чтобы ваш макет автоматически адаптировался и предотвращал пространственное наложение.

  • getActiveGuidanceButtons() : Расширенный список плавающих элементов управления для активного состояния навигации (обычно включающий как кнопку «Компас», так и кнопку «Сообщить об инциденте»). Как и в состоянии готовности к навигации, вы можете найти и извлечь отдельные элементы управления кнопками, отфильтровав список по ButtonKnownType ( COMPASS или REPORTING ) и извлекая элемент управления с помощью getView() . Затем вы можете расположить их независимо или использовать компоновку, например AutoHidingLinearLayout , чтобы безопасно разместить массив друг над другом без пространственных конфликтов.

компоненты Dynamic Prompt

В процессе активного управления сигнальные сообщения, такие как оповещения об инцидентах или предупреждения о камерах контроля скорости, срабатывают независимо друг от друга.

Когда подсказка готова к отображению, SDK вызывает функцию обратного вызова onShowPrompt() вашего делегата и передает ей представление newPrompt . Ваш делегат отвечает за плавное размещение этой подсказки поверх вашего макета (обычно она привязана к нижнему краю контейнера карты).

Поскольку входящее сообщение занимает нижнюю часть экрана, необходимо изменить макет, чтобы сообщение не перекрывало область просмотра, логотип Google или любые кнопки, выровненные по нижнему краю.

Управление размером экрана и широкоэкранным режимом

Для обработки изменений размеров карты и ориентации экрана делегат компоновки использует следующие возможности:

  • Изменение размера отображения: Подстраивает макет при изменении физического размера карты.

  • Широкий режим: переключается на вариант с широким форматом, когда карта становится достаточно широкой.

Реагирует на изменение размера изображения

При каждом изменении физических размеров контейнера карты SDK вызывает функцию обратного вызова onSizeChanged() . Функция onSizeChanged onSizeChanged() часто срабатывает при использовании разделенных экранов, ползунков макета и поворота устройства. Используйте эту функцию для внесения общих корректировок в пользовательский интерфейс. Реализуйте onSizeChanged() для изменения положения пользовательских элементов в соответствии с новым соотношением сторон экрана, применения собственных контрольных точек ширины или высоты, а также для определения момента переключения широкоформатного режима при изменении размера окна, чтобы безопасно менять варианты макета.

Понимание широкого режима

Широкий режим активируется, когда контейнер карты достаточно широк, чтобы отображать компоненты пользовательского интерфейса рядом друг с другом.

Считывайте логическое значение isWideMode() из объекта состояния, чтобы изменить положение пользовательских элементов интерфейса и оставить центр карты свободным для водителя. Поскольку компоненты, предоставляемые Google, такие как карточка поворота и карточка времени прибытия, автоматически уменьшаются и изменяют форму в широком режиме, считывание этого логического значения гарантирует, что ваш макет адаптируется в тот же момент, когда обновляются компоненты Google.

Рассмотрим следующие примеры размещения элементов макета в стандартном и расширенном режимах:

  • Стандартный портретный режим: расположите карту поворота в верхней части экрана, а карту предполагаемого времени прибытия — в нижней.

  • Широкий режим: переместите карточку с указанием поворота в начальную часть экрана, а карточку с указанием предполагаемого времени прибытия — в конечную.

Проверка широкого режима во время переходов состояний

Если ваша разметка поддерживает широкий режим, оценивайте логическое значение isWideMode() внутри каждого обработчика событий перехода состояния, а не полагайтесь только на onSizeChanged() .

Проверяйте объект состояния во время вызовов функций обратного вызова, таких как onEnterNavigationReady() и onEnterActiveGuidance() , чтобы обрабатывать первоначальный запуск приложения в альбомном режиме. Такой подход защищает ваш макет от событий жизненного цикла системы Android, таких как повторное создание Activity по умолчанию, которые полностью обходят функцию обратного вызова изменения размера, гарантируя, что правильный вариант стандартного или широкого макета активируется в момент начала нового состояния навигации.

Выравнивание по стилям Google

Чтобы ваш пользовательский интерфейс соответствовал визуальному стилю Google, SDK предоставляет вспомогательный класс StyleValues . Вы можете считывать эти значения в пикселях, не зависящих от плотности (dp), чтобы идеально выравнивать ваши представления с компонентами Google.

Например, если вы хотите разместить пользовательскую кнопку в верхнем углу экрана напротив карточки с ходом, вы можете вызвать StyleValues.headerTopPaddingDp() и присвоить возвращаемое значение в качестве верхнего отступа вашей кнопки. Это гарантирует, что ваша пользовательская кнопка будет визуально выравниваться с верхним краем карточки с ходом, сохраняя симметрию экрана.

Доступные варианты наполнителей и размеров:

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

Пример: Реализация компоновки на основе ограничений

В следующем примере показан базовый делегат компоновки, который управляет переходами состояний с помощью программных определений ConstraintLayout и ConstraintSet .

Хотя в этом примере ограничения представления создаются в коде, делегаты компоновки также могут создавать стандартные XML-макеты Android.

Котлин

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

@file:Suppress("PackageName")

package com.example.navigationapidemo.layoutdelegate

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

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

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

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

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

  private var activeGuidanceUiState: ActiveGuidanceUiState? = null

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

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

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

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

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

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

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

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

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

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

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

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

      constrainButtonsToBottomEnd()
    }
  }

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

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

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

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

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

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

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

    activeGuidanceConstraintSet?.applyTo(currentLayout)
  }

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

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

    navigationReadyConstraintSet?.applyTo(layout)
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    activeGuidanceConstraintSet?.applyTo(layout)
  }

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

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

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

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

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

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

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

    

Java

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

package com.example.navigationapidemo.layoutdelegate;

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

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

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

  private ConstraintLayout layout;
  private AutoHidingVerticalLayout buttonsContainer;

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

  private ActiveGuidanceUiState activeGuidanceUiState;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    constrainButtonsToBottomEnd(constraintSet);

    return constraintSet;
  }

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

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

    Context context = navigationUiParent.getViewContext();

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

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

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

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

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

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

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

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

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

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

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

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

    navigationReadyConstraintSet.applyTo(layout);
  }

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

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

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

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

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

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

    constrainEtaCardToBottomStart(uiState, constraintSet);

    constrainButtonsToTopOfEtaCard(context, uiState, constraintSet);

    return constraintSet;
  }

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

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

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

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

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

    layout.addView(newPrompt);

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

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

    activeGuidanceWithPromptConstraintSet.clear(oldPrompt.getId());

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

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

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

    layout.removeView(oldPrompt);

    activeGuidanceConstraintSet.applyTo(layout);
  }

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

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

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

    return constraintSet;
  }

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

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