Как настроить макет навигации

Анимация, показывающая пример настроенного макета навигации на Android

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

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

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

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

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

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

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

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

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

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

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

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

  • По умолчанию экран пуст. Компоненты Google появляются, только если ваш делегат явно добавляет их в иерархию View и размещает их.
  • Устаревшие API не поддерживаются. Свойства и методы, управляющие устаревшей моделью макета на основе рекламных мест, не будут поддерживаться и могут работать некорректно, если активен собственный делегат.
  • Не изменяйте внутренние структуры представлений. Не используйте такие методы, как findViewById(), для обхода или изменения иерархий View компонентов, предоставляемых Google (например, карточки поворота или карточки расчетного времени прибытия). Поскольку эти внутренние иерархии View являются деталями реализации, они могут меняться в разных выпусках 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().

Контрольный список Essentials

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

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

    Kotlin

    // For SupportNavigationFragment
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val navFragment = supportFragmentManager.findFragmentById(R.id.nav_fragment) as SupportNavigationFragment
        navFragment.setLayoutDelegate(MyLayoutDelegate())
    }
    
    // For a programmatic NavigationView
    val navigationView = NavigationView(context)
    navigationView.setLayoutDelegate(MyLayoutDelegate())
    navigationView.onCreate(savedInstanceState)
            

    Java

    // For SupportNavigationFragment
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        SupportNavigationFragment navFragment = (SupportNavigationFragment) getSupportFragmentManager().findFragmentById(R.id.nav_fragment);
        navFragment.setLayoutDelegate(new MyLayoutDelegate());
    }
    
    // For a programmatic NavigationView
    NavigationView navigationView = new NavigationView(context);
    navigationView.setLayoutDelegate(new MyLayoutDelegate());
    navigationView.onCreate(savedInstanceState);
            
  • Создайте собственный контейнер макета. Создайте собственную группу представлений, например ConstraintLayout, которая будет содержать ваши элементы интерфейса и представления Google.

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

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

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

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

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

Состояния интерфейса и компоненты Google

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

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

Диаграмма переходов между состояниями жизненного цикла NavigationLayoutDelegate, показывающая переходы между состояниями Non-Navigation, Navigation Ready и Active Guidance.

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

Обязательные компоненты (все состояния навигации)

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

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

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

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

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

активных компонента рекомендаций

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

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

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

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

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

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

компонента динамических подсказок

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

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

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

Как обрабатывать размер экрана и широкоэкранный режим

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

  • Изменение размера области просмотра. Макет корректируется при изменении физического размера карты.

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

Как реагировать на изменение размера области просмотра

При изменении физических размеров контейнера карты SDK выполняет обратный вызов 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.

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

    

Чтобы посмотреть полный образец кода или изучить дополнительные реализации делегата макета, ознакомьтесь с образцами делегата макета на GitHub. Вы также можете посмотреть отдельные исходные файлы для этого примера на GitHub: