
레이아웃 맞춤설정을 사용하여 Google에서 제공하는 뷰와 자체 맞춤 비즈니스 뷰의 뷰 계층 구조와 화면 배치를 제어합니다.
미리 정의된 헤더 또는 바닥글 슬롯에 맞춤 뷰를 추가하는 대신 레이아웃 위임을 구현하여 전체 화면의 레이아웃을 관리합니다. 탐색 상태 전환 중에 레이아웃 위임은 회전 카드, 도착예정시간 카드, 버튼과 같은 Google에서 제공하는 구성요소를 수신합니다. ConstraintLayout, CoordinatorLayout 또는 LinearLayout와 같은 표준 Android 레이아웃 시스템을 사용하여 자체 맞춤 콘텐츠와 함께 이러한 요소를 배치합니다.
이 프레임워크를 사용하여 뷰가 겹치지 않도록 하면서 화면에 주문 상태 또는 픽업 안내와 같은 맞춤 비즈니스 정보를 정확하게 배치합니다.
레이아웃 맞춤설정의 작동 방식
레이아웃 맞춤설정은 위임 디자인 패턴을 사용합니다. SDK는 화면에 UI 구성요소를 자동으로 그리거나 배치하는 대신 개발자가 작성하는 맞춤 클래스인 레이아웃 위임에 직접 전달합니다.
레이아웃을 맞춤설정하려면 추상NavigationLayoutDelegate 클래스를 확장하는 클래스를 만들고 인스턴스를 NavigationView 또는 SupportNavigationFragment에 할당합니다. 기본 지도에서 활성 세부 경로 안내로 이동하는 것과 같이 탐색 상태가 전환될 때마다 SDK는 위임에서 콜백 메서드를 실행하고 해당 특정 상태에 사용할 수 있는 UI 구성요소를 제공합니다.
원활한 통합 사용자 환경을 제공하기 위해 이 프레임워크는 앱과 SDK 간의 책임 분리를 명확하게 설정합니다. 레이아웃 위임을 사용하여 다음을 맞춤설정합니다.
- 뷰 계층 구조 빌드: 각 UI 상태에 대해 화면에 추가할 Google 구성요소와 맞춤 비즈니스 뷰를 정확하게 선택합니다.
- 모든 요소 배치: 정확한 화면 앵커, 여백, 레이아웃 배치를 설정합니다. Google 구성요소는 자체 내부 크기를 계산하므로 Google 구성요소에 맞춤 너비 또는 높이 제약 조건을 적용하지 마세요.
- 기본 지도 프레임: 표시 영역 구성요소의 좌표를 사용하여 지도 카메라의 표시 경계를 정의합니다.
- 화면 레이어: 맞춤 뷰를 Google의 기본 제공 컨트롤 위, 아래 또는 옆에 표시할지 결정합니다.
한편 레이아웃 위임을 사용하여 다음 Google 구성요소를 맞춤설정할 수는 없습니다.
- 구성요소 크기: SDK에서 자동으로 계산하는 Google 제공 구성요소의 크기 및 내부 크기입니다.
- 트리거 조건: 실시간 경로 데이터를 기반으로 동적 알림 또는 프롬프트가 표시되는 경우입니다.
구현 원칙
레이아웃 위임을 작성할 때는 레이아웃 버그 또는 런타임 비정상 종료를 방지하기 위해 다음 규칙에 유의하세요.
- 기본값은 빈 화면입니다. Google에서 제공하는 구성요소는 위임이 뷰 계층 구조에 명시적으로 추가하고 배치하는 경우에만 표시됩니다.
- 레거시 API는 지원되지 않습니다. 레거시 슬롯 기반 레이아웃 모델을 제어하는 속성과 메서드는 지원되지 않으며 맞춤 위임이 활성 상태일 때 예상대로 작동하지 않을 수 있습니다.
- 내부 뷰 구조를 수정하지 마세요.
findViewById()와 같은 메서드를 사용하여 회전 카드 또는 도착예정시간 카드와 같은 Google에서 제공하는 구성요소의 뷰 계층 구조를 탐색하거나 수정하지 마세요. 이러한 내부 뷰 계층 구조는 기본 구현 세부정보이므로 SDK 출시 전반에 걸쳐 변경될 수 있습니다. 수정하면 향후 SDK 업데이트 중에 레이아웃이 중단될 수 있습니다.
레거시 API 호환성
맞춤 레이아웃 위임을 사용할 때 안정적인 레이아웃 동작을 위해 지원 중단될 예정인 다음 레거시 슬롯 기반 API를 사용하지 마세요. 기존 앱을 이전하려면 이러한 API의 사용을 맞춤 레이아웃 위임의 코드로 바꾸세요.
레거시 슬롯 기반 API 표시
| 레거시 API | 레이아웃 위임 대체 |
|---|---|
setCustomControl(View, CustomControlPosition) |
ConstraintLayout 또는 다른 뷰 그룹에 뷰를 직접 추가합니다. |
removeCustomControl(View) |
뷰 계층 구조에서 뷰를 직접 삭제합니다. |
setEtaCardEnabled(boolean) |
onEnterActiveGuidance에서 etaCard 뷰를 읽습니다. |
setHeaderEnabled(boolean) |
onEnterActiveGuidance에서 turnCard 뷰를 읽습니다. |
setReportIncidentButtonEnabled(boolean) |
getActiveGuidanceButtons()에서 REPORTING 버튼을 찾습니다. |
setTripProgressBarEnabled(boolean) |
onEnterActiveGuidance에서 tripProgressBar 뷰를 읽습니다. |
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 제공 구성요소의 크기를 결정합니다. 다음 요소에는 맞춤 너비 또는 높이 제약 조건을 적용하지 마세요.
- 회전 카드
- 도착예정시간 카드
- 수신 프롬프트
- Google 로고
- 속도 위젯
-
뷰에 레이아웃 연결:
navigationView.setNavigationLayout()내에서onEnterNavigationReady()및onEnterActiveGuidance()콜백을 호출하여 컨테이너를 지도 뷰 계층 구조에 연결합니다. -
상태 종료 시 정리:
navigationView.removeNavigationLayout()을 호출하고 맞춤 뷰를onLeave콜백 내에서 삭제하여 메모리 누수 및 겹치는 UI 상태를 방지합니다. -
레거시 레이아웃 API 피하기: 맞춤 위임이 활성 상태일 때 지원 중단된 슬롯 기반 API(예:
setCustomControl()또는setHeaderEnabled())를 호출하지 마세요. 맞춤 위임이 연결되면 SDK 가 이러한 레거시 호출을 무시할 수 있습니다.
UI 상태 및 Google 구성요소
탐색 상태 전환이 발생하면 SDK는 읽기 전용 UiState 객체를 위임 콜백에 전달합니다. 이 객체는 현재 레이아웃 구성 플래그를 화면을 렌더링하는 데 필요한 Google 구성요소 (View 인스턴스)와 번들로 묶습니다.
레이아웃 위임은 4가지 작동 상태에서 뷰 계층 구조를 관리합니다. 다음 상태 다이어그램은 SDK가 탐색 상태 간에 전환되는 방식과 위임에서 실행하는 콜백 메서드를 보여줍니다.
위임에 제공되는 특정 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(): 활성 안내 상태의 확장된 부동 작업 뷰 목록입니다 (일반적으로 나침반과 사고 신고 버튼이 모두 포함됨). 탐색 준비 상태와 마찬가지로ButtonKnownType(COMPASS또는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()); } }