ナビゲーション レイアウトをカスタマイズする

Android でカスタマイズされたナビゲーション レイアウトを示すアニメーションの例

レイアウトのカスタマイズを使用すると、Google 提供のビューと独自のカスタム ビジネスビューの両方で、ビュー階層と画面配置を制御できます。

カスタムビューを事前定義されたヘッダー スロットまたはフッター スロットに追加するのではなく、レイアウト デリゲートを実装して画面全体のレイアウトを管理します。ナビゲーション状態の遷移中、レイアウト デリゲートは、ターンカード、到着予定時刻カード、ボタンなど、Google 提供のコンポーネントを受け取ります。これらの要素は、ConstraintLayoutCoordinatorLayoutLinearLayout などの標準の Android レイアウト システムを使用して、独自のカスタム コンテンツとともに配置します。

このフレームワークを使用すると、注文ステータスや受け取り手順などのカスタム ビジネス情報を画面に正確に配置しながら、ビューの重複を防ぐことができます。

レイアウトのカスタマイズの仕組み

レイアウトのカスタマイズでは、委譲デザイン パターンを使用します。SDK は、UI コンポーネントを画面に自動的に描画または配置するのではなく、ユーザーが作成したカスタムクラス(レイアウト デリゲート)に直接渡します。

レイアウトをカスタマイズするには、抽象 NavigationLayoutDelegate クラスを拡張するクラスを作成し、そのインスタンスを NavigationView または SupportNavigationFragment に割り当てます。基本地図からターンバイターン方式の経路案内に移行するなど、ナビゲーション状態が遷移するたびに、SDK はデリゲートでコールバック メソッドを実行し、その特定の状態で使用できる UI コンポーネントを提供します。

シームレスで統合されたユーザー エクスペリエンスを提供するため、このフレームワークでは、アプリと SDK の責任を明確に分離します。レイアウト デリゲートを使用して、次のものをカスタマイズします。

  • ビュー階層の構築: 各 UI 状態の画面に追加する 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) onEnterActiveGuidanceetaCard ビューを読み取ります。
setHeaderEnabled(boolean) onEnterActiveGuidanceturnCard ビューを読み取ります。
setReportIncidentButtonEnabled(boolean) getActiveGuidanceButtons()REPORTING ボタンを見つけます。
setTripProgressBarEnabled(boolean) onEnterActiveGuidancetripProgressBar ビューを読み取ります。
addOnNavigationUiChangedListener(...) NavigationLayoutDelegate の状態遷移コールバックに依存します。
removeOnNavigationUiChangedListener(...) デリゲートを使用してレイアウトの状態遷移を直接管理します。
addPromptVisibilityChangedListener(...) NavigationLayoutDelegate のプロンプト コールバック(onShowPrompt() など)に依存します。
removePromptVisibilityChangedListener(...) デリゲートを使用してプロンプト表示ロジックを直接管理します。
setCompassEnabled(boolean) getNavigationReadyButtons() または getActiveGuidanceButtons() でコンパスを見つけます。

基本チェックリスト

レイアウト デリゲートを正常に実装するには、次の基本的な手順と要件に従ってください。

  • UI を作成する前にデリゲートを初期化する: SDK がナビゲーション UI を初期化する前に setLayoutDelegate() を呼び出します。アプリケーション構造の正確な 設定タイミングを確認するには、次のコード実装例をご覧ください。UI の作成 後にデリゲートを初期化すると、 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);
            
  • 独自のレイアウト コンテナを作成する: カスタム UI 要素と Google 提供のビューを保持するカスタム ビューグループ(ConstraintLayout など)を作成します。

  • 必須のビューをアタッチする: すべてのナビゲーション状態で、Google ロゴ (getGoogleLogo())とビューポート(getViewport())をレイアウト コンテナに追加する必要があります。これらのビューの両方を含めないと ランタイム ApiIllegalStateExceptionがトリガーされ、アプリがクラッシュします。

  • 固定寸法を尊重する: SDK は、Google 提供のいくつかのコンポーネントの 寸法を決定します。次の要素にはカスタムの幅 または高さの制約を適用しないでください。

    • ターンカード
    • ETA カード
    • 受信プロンプト
    • Google ロゴ
    • 速度ウィジェット
  • レイアウトをビューにアタッチする: navigationView.setNavigationLayout()onEnterNavigationReady() コールバックと onEnterActiveGuidance() コールバック内で呼び出して、コンテナを マップビュー階層にアタッチします。

  • 状態の終了時にクリーンアップする: メモリリークや UI 状態の重複を防ぐため、onLeave コールバック内で navigationView.removeNavigationLayout() を呼び出して、カスタム ビューを削除します。

  • 以前のレイアウト API を使用しない: カスタム デリゲートがアクティブな場合は、非推奨の スロットベースの API(setCustomControl()setHeaderEnabled() など)を呼び出さないでください。カスタム デリゲートがアタッチされている場合、SDK はこれらの以前の呼び出しを無視する可能性があります。

UI の状態と Google コンポーネント

ナビゲーション状態の遷移が発生すると、SDK は読み取り専用の UiState オブジェクトをデリゲート コールバックに渡します。このオブジェクトは、現在のレイアウト構成フラグと、画面のレンダリングに必要な Google コンポーネント(View インスタンスなど)をバンドルします。

レイアウト デリゲートは、4 つの動作状態にわたってビュー階層を管理します。 次の状態図は、SDK がナビゲーション状態を遷移する方法と、デリゲートで実行するコールバック メソッドを示しています。

NavigationLayoutDelegate のライフサイクル状態遷移図。Non-Navigation、Navigation Ready、Active Guidance の各状態間の遷移を示しています。

デリゲートに提供される特定の Google コンポーネントは、ナビゲーション ライフサイクルの現在のフェーズによって異なります。

必須コンポーネント(すべてのナビゲーション状態)

現在のナビゲーション フェーズに関係なく、次の Google コンポーネントをビュー階層に含めて配置し、表示したままにする必要があります。

  • Google ロゴ(getGoogleLogo()): このコンポーネントには、必須の Google マップのロゴが表示されます。[再センタリング] ボタンが有効になっている場合、ドライバーが車両から離れた位置に地図をスクロールすると、ロゴが自動的にこのボタンに変わります。そのため、ロゴの配置によって [再センタリング] ボタンが表示される場所が決まります。標準の地図 UI の期待値に合わせて、ロゴをレイアウトの左下隅に配置することをおすすめします。

  • ビューポート(getViewport()): カメラのフレーミング境界を定義する非表示のView。ビューポートの配置によって、SDK が車両シェブロンを中央に配置し、アクティブなルート線を引く場所が正確に決まります。ビューポートは、カスタム ボトムシートなどの不透明なオーバーレイを安全に回避して、画面の開いた、隠されていない領域をカバーするように配置します。

ナビゲーション準備完了状態では、基本地図にフォーカスを維持するため、UI は最小限に抑えられます。NavigationReadyUiState オブジェクトを使用すると、次のものにアクセスできます。

  • getNavigationReadyButtons(): ベースマップ用に構成されたフローティング アクション ビューのリスト(通常はコンパスボタンのみ)。Google は今後の SDK リリースでボタンを追加または並べ替える可能性があるため、固定リスト インデックスに依存しないでください。代わりに、リストを反復処理し、getType()ButtonKnownType.COMPASS と比較して各ボタンのタイプを読み取り、getView() を呼び出して物理ビューを抽出します。このリストのボタンは、アクティブなガイダンス中に使用できるボタンとは異なります。

アクティブなガイダンス コンポーネント

ターンバイターン方式の経路案内が開始されると、SDK はナビゲーション コントロールのフルパッケージのロックを解除します。ActiveGuidanceUiState オブジェクトを使用すると、次の Google コンポーネントにアクセスできます。

  • getTurnCard(): 次の操作の方向、距離の測定値、車線案内を表示するメインヘッダー バナー。レイアウトの上部に配置して、使い慣れたナビゲーション階層を確立し、カスタムビューをその周囲に固定します。

  • getEtaCard(): 到着予定時刻、残りの移動時間、目的地までの残りの距離を表示するフッター バナー。 画面の下端に沿って配置するか、その座標をカスタム タスク管理シートに統合します。

  • getTripProgressBar(): 現在のルートに沿ってドライバーが移動した距離を示す縦型のプログレスバー。マップの端に厳密に固定されていた以前のレイアウトとは異なり、カスタム コンテナの端に沿って配置するなど、任意の場所に固定できます。

  • getSpeedWidget(): 速度計と制限速度のフローティング コントロール。API 設定とデータの可用性に応じて、このビューは実行時に 4 つの視覚状態(何も表示しない、現在の速度のみを表示する、制限速度のみを表示する、両方の値を表示する)の間で動的にサイズ変更されます。ウィジェットは予告なくこれらのサイズの間で切り替わる可能性があるため、常に制約を使用して周囲のビューを固定し、レイアウトが自動的に適応して空間の重複を防ぐようにします。

  • getActiveGuidanceButtons(): アクティブなガイダンス状態のフローティング アクション ビューの拡張リスト(通常はコンパスボタンとインシデント報告ボタンの両方を含む)。ナビゲーション準備完了状態と同様に、ButtonKnownTypeCOMPASS または REPORTING)でリストをフィルタして個々のボタンビューを見つけて抽出し、getView() を使用してビューを抽出できます。その後、個別に配置することも、AutoHidingLinearLayout などのレイアウトを使用して、空間の競合なしに配列を安全に積み重ねることもできます。

動的プロンプト コンポーネント

インシデント アラートや安全カメラの警告などのプロンプトは、アクティブなガイダンス中に個別にトリガーされます。

プロンプトが表示される準備が整うと、SDK はデリゲートの onShowPrompt() コールバックを呼び出し、newPrompt ビューを渡します。デリゲートは、このプロンプトをレイアウト上にスムーズに配置する役割を担います(通常はマップ コンテナの下端に固定されます)。

受信プロンプトは画面の下部を覆うため、プロンプトがビューポート、Google ロゴ、または下揃えのボタンと重複しないようにレイアウトを更新する必要があります。

画面サイズとワイドモードの処理

地図の寸法と画面の向きの変化を処理するため、レイアウト デリゲートは次の機能を使用します。

  • ビューのサイズ変更: 地図の物理サイズが変更されるたびにレイアウトを調整します。

  • ワイドモード: 地図が十分に広い場合は、ワイドフォーマットのレイアウト バリアントに切り替えます。

ビューのサイズ変更への対応

マップ コンテナの物理寸法が変更されるたびに、SDK は onSizeChanged() コールバックを実行します。分割画面レイアウト、レイアウト スライダー、デバイスの回転は、通常 onSizeChanged() コールバックをトリガーします。このコールバックを使用して、カスタム UI に一般的なレスポンシブ調整を行います。onSizeChanged() を実装して、新しい画面のアスペクト比に合わせてカスタム要素を再配置し、独自のカスタムの幅または高さのブレークポイントを適用します。また、サイズ変更イベントでワイドモードが切り替わったときに、レイアウト バリアントを安全に切り替えます。

ワイドモードについて

ワイドモードは、UI コンポーネントを並べて表示するのに十分な幅がマップ コンテナにある場合に有効になります。

状態オブジェクトから isWideMode() ブール値を読み取って、カスタム UI 要素を再配置し、地図の中央をドライバーのためにクリアします。ターンカードや到着予定(時刻)カードなどの Google 提供のコンポーネントは、ワイドモードで自動的に縮小して形状を変更するため、このブール値を読み取ることで、Google コンポーネントの更新と同時にレイアウトが適応されます。

標準モードとワイドモードでレイアウト コンポーネントを配置する例を次に示します。

  • 標準のポートレート モード: ターンカードを画面の上部に、推定所要時間カードを画面の下部に配置します。

  • ワイドモード: ターンカードを画面の左端に、推定所要時間カードを右端に移動します。

状態遷移中のワイドモードの確認

レイアウトがワイドモード バリアントをサポートしている場合は、onSizeChanged() のみに依存するのではなく、すべての状態遷移コールバック内で isWideMode() ブール値を評価します。

onEnterNavigationReady()onEnterActiveGuidance() などのコールバック中に状態オブジェクトを確認して、横表示モードでアプリを最初に起動します。このアプローチにより、サイズ変更コールバックを完全にバイパスする Android システムのライフサイクル イベント(デフォルトのアクティビティの再作成など)からレイアウトを保護し、新しいナビゲーション状態が開始されたときに正しい標準レイアウト バリアントまたはワイドレイアウト バリアントが有効になるようにします。

Google スタイリングに合わせる

カスタム UI を Google の視覚的なリズムに合わせるため、SDK は StyleValues ユーティリティ クラスを提供します。これらの値を密度非依存ピクセル(dp)で読み取って、ビューを Google コンポーネントに完全に合わせることができます。

たとえば、ターンカードの反対側の画面の上隅にカスタムボタンを配置する場合は、StyleValues.headerTopPaddingDp() を呼び出して、返された値をボタンの上マージンとして割り当てます。これにより、カスタムボタンがターンカードの上端と視覚的に揃い、画面の対称性が維持されます。

使用可能なスタイルのパディングと測定値は次のとおりです。

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

例: 制約ベースのレイアウト実装

次の例は、プログラムによる ConstraintLayout 定義と ConstraintSet 定義を使用して状態遷移を管理する基本的なレイアウト デリゲートを示しています。

この例ではコードでビューの制約を構築していますが、レイアウト デリゲートは標準の Android XML レイアウトを拡張することもできます。

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.
 */

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