
Gunakan penyesuaian tata letak untuk mengontrol hierarki tampilan dan penempatan layar untuk tampilan yang disediakan Google dan tampilan bisnis kustom Anda sendiri.
Daripada menambahkan tampilan kustom ke dalam slot header atau footer yang telah ditentukan sebelumnya, terapkan
delegasi tata letak untuk mengelola tata letak seluruh layar. Selama transisi status
navigasi, delegasi tata letak Anda menerima komponen yang disediakan Google, seperti kartu belokan, kartu perkiraan waktu tiba, dan tombol. Posisikan elemen ini bersama konten kustom Anda menggunakan sistem tata letak Android standar seperti ConstraintLayout, CoordinatorLayout, atau LinearLayout.
Gunakan framework ini untuk memosisikan informasi bisnis kustom—seperti status pesanan atau petunjuk pengambilan—secara tepat di layar sekaligus mencegah tampilan Anda tumpang-tindih.
Cara kerja penyesuaian tata letak
Penyesuaian tata letak menggunakan pola desain delegasi. Alih-alih menggambar atau memosisikan komponen UI di layar secara otomatis, SDK meneruskannya langsung ke class kustom yang Anda tulis: delegasi tata letak.
Untuk menyesuaikan tata letak, buat class yang memperluas class abstrakNavigationLayoutDelegate dan tetapkan instance ke NavigationView atau SupportNavigationFragment. Setiap kali transisi status navigasi—seperti
berpindah dari peta dasar ke panduan belokan demi belokan aktif—SDK akan menjalankan
metode callback pada delegasi Anda dan menyediakan komponen UI yang tersedia
untuk status tertentu tersebut.
Untuk memberikan pengalaman pengguna yang lancar dan terintegrasi, framework ini menetapkan pemisahan tanggung jawab yang jelas antara aplikasi dan SDK Anda. Gunakan delegasi tata letak untuk menyesuaikan hal berikut:
- Membangun hierarki tampilan: Pilih komponen Google dan tampilan bisnis kustom yang akan ditambahkan ke layar untuk setiap status UI.
- Memosisikan setiap elemen: Tetapkan penempatan tata letak, margin, dan anchor layar yang presisi. Hindari penerapan batasan lebar atau tinggi kustom pada komponen Google karena komponen Google menghitung dimensi internalnya sendiri.
- Bingkai peta dasar: Tentukan batas yang terlihat dari kamera peta menggunakan koordinat komponen area pandang.
- Menempatkan layar: Tentukan apakah tampilan kustom mengambang di atas, di bawah, atau di samping kontrol bawaan Google.
Sementara itu, Anda tidak dapat menyesuaikan komponen Google berikut dengan delegasi tata letak:
- Dimensi komponen: Ukuran dan dimensi internal komponen yang disediakan Google, yang dihitung secara otomatis oleh SDK.
- Kondisi pemicu: Saat pemberitahuan atau perintah dinamis muncul berdasarkan data rute real-time.
Prinsip penerapan
Saat menulis delegasi tata letak, perhatikan aturan berikut untuk mencegah bug tata letak atau error runtime:
- Defaultnya adalah layar kosong: Komponen yang disediakan Google hanya muncul jika delegasi Anda menambahkannya secara eksplisit ke hierarki tampilan dan memosisikannya.
- API lama tidak didukung: Properti dan metode yang mengontrol model tata letak berbasis slot lama tidak akan didukung dan mungkin tidak berperilaku seperti yang diharapkan saat delegasi kustom aktif.
- Jangan mengubah struktur tampilan internal:
Jangan menggunakan metode seperti
findViewById()untuk melintasi atau mengubah hierarki tampilan komponen yang disediakan Google (seperti kartu belokan atau kartu perkiraan waktu tiba). Karena hierarki tampilan internal ini adalah detail implementasi pokok, hierarki tersebut dapat berubah di seluruh rilis SDK. Memodifikasinya dapat menyebabkan tata letak Anda rusak selama update SDK mendatang.
Kompatibilitas API lama
Untuk perilaku tata letak yang andal saat menggunakan delegasi tata letak kustom, hindari penggunaan API berbasis slot lama berikut, yang direncanakan untuk dihentikan. Untuk memigrasikan aplikasi yang ada, ganti penggunaan API ini dengan kode di delegasi tata letak kustom Anda:
Menampilkan API berbasis slot lama
| API Lama | Penggantian Layout Delegate |
|---|---|
setCustomControl(View, CustomControlPosition) |
Tambahkan tampilan Anda langsung ke ConstraintLayout atau kelompok tampilan lainnya. |
removeCustomControl(View) |
Menghapus tampilan langsung dari hierarki tampilan Anda. |
setEtaCardEnabled(boolean) |
Baca tampilan etaCard di onEnterActiveGuidance. |
setHeaderEnabled(boolean) |
Baca tampilan turnCard di onEnterActiveGuidance. |
setReportIncidentButtonEnabled(boolean) |
Temukan tombol REPORTING di getActiveGuidanceButtons(). |
setTripProgressBarEnabled(boolean) |
Baca tampilan tripProgressBar di onEnterActiveGuidance. |
addOnNavigationUiChangedListener(...) |
Mengandalkan callback transisi status NavigationLayoutDelegate Anda. |
removeOnNavigationUiChangedListener(...) |
Kelola transisi status tata letak secara langsung menggunakan delegasi. |
addPromptVisibilityChangedListener(...) |
Mengandalkan callback perintah NavigationLayoutDelegate (seperti onShowPrompt()). |
removePromptVisibilityChangedListener(...) |
Mengelola logika tampilan perintah secara langsung menggunakan pengurus. |
setCompassEnabled(boolean) |
Temukan kompas di getNavigationReadyButtons() atau getActiveGuidanceButtons(). |
Checklist Essentials
Ikuti langkah-langkah dan persyaratan penting berikut untuk menerapkan delegasi tata letak dengan berhasil:
-
Lakukan inisialisasi delegasi sebelum membuat UI: Panggil
setLayoutDelegate()sebelum SDK melakukan inisialisasi UI navigasi. Lihat contoh penerapan kode berikut untuk memverifikasi waktu penyiapan yang tepat untuk struktur aplikasi Anda. Menginisialisasi delegasi setelah pembuatan UI akan memicuApiIllegalStateException.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);
-
Membangun penampung tata letak Anda sendiri: Buat grup tampilan kustom—seperti
ConstraintLayout—untuk menampung elemen UI kustom dan tampilan yang disediakan Google. -
Lampirkan tampilan wajib: Anda harus menambahkan logo Google (
getGoogleLogo()) dan Viewport (getViewport()) ke penampung tata letak di semua status navigasi. Jika kedua tampilan ini tidak disertakan, akan memicuApiIllegalStateExceptionruntime dan menyebabkan aplikasi error. -
Menghormati dimensi tetap: SDK menentukan dimensi untuk beberapa komponen yang disediakan Google. Jangan menerapkan batasan lebar atau tinggi kustom ke elemen berikut:
- Memutar kartu
- Kartu PWT
- Perintah masuk
- Logo Google
- Widget kecepatan
-
Lampirkan tata letak ke tampilan: Panggil
navigationView.setNavigationLayout()di dalam callbackonEnterNavigationReady()danonEnterActiveGuidance()untuk melampirkan penampung ke hierarki tampilan peta. -
Membersihkan saat keluar dari status: Panggil
navigationView.removeNavigationLayout()dan hapus tampilan kustom di dalam callbackonLeaveuntuk mencegah kebocoran memori dan tumpang-tindih status UI. -
Hindari API tata letak lama: Jangan memanggil API berbasis slot yang tidak digunakan lagi seperti
setCustomControl()atausetHeaderEnabled()saat delegasi kustom aktif. SDK dapat mengabaikan panggilan lama ini saat delegasi kustom dilampirkan.
Status UI dan komponen Google
Saat transisi status navigasi terjadi, SDK akan meneruskan objek
UiState hanya baca ke callback delegasi Anda. Objek ini menggabungkan flag konfigurasi tata letak saat ini dengan komponen Google (misalnya, instance View) yang Anda perlukan untuk merender layar.
Delegasi tata letak mengelola hierarki tampilan di empat status operasional. Diagram status berikut menggambarkan cara SDK bertransisi antar-status navigasi dan metode callback mana yang dijalankannya pada delegasi Anda:
Komponen Google tertentu yang diberikan kepada penerima tugas Anda bergantung pada fase siklus proses navigasi saat ini.
Komponen wajib (semua status navigasi)
Terlepas dari fase navigasi saat ini, Anda harus menyertakan, memosisikan, dan menjaga komponen Google berikut tetap terlihat dalam hierarki tampilan Anda:
Logo Google (
getGoogleLogo()): Komponen ini menampilkan logo Google Maps wajib. Jika tombol Pusatkan kembali diaktifkan, logo akan otomatis berubah menjadi tombol ini saat pengemudi men-scroll peta menjauhi kendaraan mereka. Akibatnya, penempatan logo Anda menentukan tempat tombol Pusatkan ulang muncul. Sebaiknya tempatkan logo di sudut kiri bawah tata letak Anda agar sesuai dengan ekspektasi UI peta standar.Area pandang (
getViewport()):Viewyang tidak terlihat yang menentukan batas penyesuaian frame kamera. Penempatan area tampilan menentukan secara persis tempat SDK memusatkan chevron kendaraan dan menggambar garis rute aktif. Posisikan area tampilan sehingga menutupi area layar yang terbuka dan tidak terhalang, serta menghindari overlay buram seperti sheet bawah kustom.
Komponen siap navigasi
Selama status siap navigasi, UI tetap minimal untuk menjaga fokus pada
peta dasar. Objek NavigationReadyUiState Anda memberikan akses ke hal berikut:
- getNavigationReadyButtons(): Daftar tampilan tindakan mengambang yang dikonfigurasi untuk peta dasar (biasanya hanya tombol Kompas). Karena Google dapat menambahkan atau menyusun ulang tombol dalam rilis SDK mendatang, hindari mengandalkan indeks daftar tetap. Sebagai gantinya, lakukan iterasi pada
daftar, baca jenis setiap tombol dengan membandingkan
getType()denganButtonKnownType.COMPASS, dan ekstrak tampilan fisik dengan memanggilgetView(). Perhatikan bahwa tombol persis dalam daftar ini berbeda dengan tombol yang tersedia selama panduan aktif.
Komponen panduan aktif
Saat panduan belokan demi belokan dimulai, SDK akan membuka kunci rangkaian lengkap kontrol navigasi. Objek ActiveGuidanceUiState Anda menyediakan akses ke komponen Google berikut:
getTurnCard(): Banner header utama yang menampilkan petunjuk manuver berikutnya, pengukuran jarak, dan panduan jalur. Tempatkan di bagian atas tata letak untuk membuat hierarki navigasi yang sudah dikenal dan menyematkan tampilan kustom di sekitarnya.getEtaCard(): Banner footer yang menampilkan perkiraan waktu kedatangan, sisa waktu perjalanan, dan sisa jarak ke tujuan. Letakkan di sepanjang tepi bawah layar, atau integrasikan koordinatnya dengan spreadsheet pengelolaan tugas kustom Anda.getTripProgressBar(): Status progres vertikal yang menunjukkan seberapa jauh pengemudi telah melakukan perjalanan di sepanjang rute saat ini. Tidak seperti tata letak lama yang membatasi penempatannya di tepi peta, Anda memiliki kebebasan penuh untuk menempatkannya di mana saja—misalnya, di sepanjang tepi samping penampung kustom Anda.getSpeedWidget(): Kontrol mengambang untuk speedometer dan batas kecepatan yang diposting. Bergantung pada setelan API dan ketersediaan data Anda, tampilan ini mengubah ukuran secara dinamis antara empat status visual saat runtime (tidak menampilkan apa pun, hanya kecepatan saat ini, hanya batas kecepatan, atau kedua pembacaan). Karena widget dapat berubah ukuran tanpa pemberitahuan, selalu tetapkan tampilan di sekitarnya menggunakan batasan agar tata letak Anda otomatis menyesuaikan untuk mencegah tumpang-tindih spasial.
getActiveGuidanceButtons(): Daftar yang diperluas dari tampilan tindakan mengambang untuk status panduan aktif (biasanya mencakup tombol Kompas dan Pelaporan Insiden). Sama seperti pada status siap navigasi, Anda dapat menemukan dan mengekstrak tampilan tombol individual dengan memfilter daftar menurutButtonKnownType(COMPASSatauREPORTING) dan mengekstrak tampilan menggunakangetView(). Kemudian, Anda dapat memosisikannya secara independen atau menggunakan tata letak sepertiAutoHidingLinearLayoutuntuk menumpuk array dengan aman tanpa konflik spasial.
Komponen Perintah Dinamis
Perintah, seperti peringatan insiden atau peringatan kamera keamanan, dipicu secara independen selama panduan aktif.
Saat dialog siap ditampilkan, SDK akan memanggil callback
onShowPrompt() delegasi Anda dan meneruskan tampilan newPrompt. Delegasi Anda bertanggung jawab untuk memosisikan
prompt ini dengan lancar di atas tata letak Anda (biasanya ditambatkan ke tepi bawah
penampung peta).
Karena perintah yang masuk menutupi bagian bawah layar, Anda harus memperbarui tata letak untuk mencegah perintah tumpang-tindih dengan area tampilan, logo Google, atau tombol yang disejajarkan di bagian bawah.
Menangani ukuran layar dan mode lebar
Untuk menangani perubahan dimensi peta dan orientasi layar, delegasi tata letak menggunakan fitur berikut:
Mengubah ukuran tampilan: Menyesuaikan tata letak Anda setiap kali ukuran fisik peta berubah.
Mode lebar: Beralih ke varian tata letak format lebar saat peta cukup lebar.
Merespons pengubahan ukuran tampilan
Setiap kali dimensi fisik penampung peta berubah, SDK akan mengeksekusi callbackonSizeChanged(). Tata letak layar terpisah, penggeser tata letak, dan rotasi perangkat biasanya memicu callback onSizeChanged(). Gunakan callback ini untuk
melakukan penyesuaian responsif umum pada UI kustom Anda. Terapkan
onSizeChanged() untuk memosisikan ulang elemen kustom untuk rasio aspek layar baru,
terapkan titik henti sementara lebar atau tinggi kustom Anda sendiri, dan deteksi saat peristiwa
pengubahan ukuran mengalihkan mode lebar untuk menukar varian tata letak dengan aman.
Memahami mode lebar
Mode lebar diaktifkan saat penampung peta cukup lebar untuk menampilkan komponen UI secara berdampingan.
Baca boolean isWideMode() dari objek status untuk memosisikan ulang elemen UI kustom dan menjaga agar bagian tengah peta tetap kosong untuk pengemudi. Karena komponen yang disediakan Google seperti kartu belokan dan kartu perkiraan waktu tiba otomatis menyusut dan berubah bentuk dalam mode lebar, membaca boolean ini memastikan tata letak Anda beradaptasi pada saat yang sama persis dengan saat komponen Google diperbarui.
Pertimbangkan contoh berikut untuk memosisikan komponen tata letak Anda dalam mode standar dan lebar:
Mode potret standar: Posisikan kartu belokan di bagian atas layar dan kartu PWT di bagian bawah.
Mode lebar: Pindahkan kartu belokan ke sisi awal layar dan kartu perkiraan waktu tiba ke sisi akhir.
Memeriksa mode lebar selama transisi status
Jika tata letak Anda mendukung variasi mode lebar, evaluasi boolean isWideMode()
di dalam setiap callback transisi status, bukan hanya mengandalkan
onSizeChanged().
onEnterNavigationReady() dan
onEnterActiveGuidance() untuk menangani peluncuran aplikasi awal dalam mode lanskap. Pendekatan
ini melindungi tata letak Anda dari peristiwa siklus proses sistem Android—seperti
pembuatan ulang Aktivitas default—yang sepenuhnya melewati callback pengubahan ukuran,
sehingga memastikan bahwa varian tata letak standar atau lebar yang benar diaktifkan saat
status navigasi baru dimulai.
Menyesuaikan dengan gaya visual Google
Untuk membantu UI kustom Anda cocok dengan ritme visual Google, SDK menyediakan class utilitas StyleValues. Anda dapat membaca nilai ini dalam piksel kepadatan mandiri (dp) untuk menyelaraskan tampilan dengan sempurna dengan komponen Google.
Misalnya, jika Anda ingin menempatkan tombol kustom di sudut atas
layar yang berlawanan dengan kartu giliran, Anda dapat memanggil StyleValues.headerTopPaddingDp()
dan menetapkan nilai yang ditampilkan sebagai margin atas tombol. Hal ini memastikan tombol kustom Anda secara visual selaras dengan tepi atas kartu giliran, sehingga menjaga simetri layar.
Pengukuran dan padding gaya yang tersedia meliputi:
StyleValues.headerNominalHeightDp()StyleValues.headerTopPaddingDp()StyleValues.headerFooterSidePaddingDp()StyleValues.mapControlSidePaddingDp()StyleValues.buttonMapControlSidePaddingDp()
Contoh: Penerapan tata letak berbasis batasan
Contoh berikut menunjukkan delegasi tata letak dasar yang mengelola transisi status menggunakan definisi ConstraintLayout dan ConstraintSet terprogram.
Meskipun contoh ini membuat batasan tampilan dalam kode, delegasi tata letak juga dapat meng-inflate tata letak XML Android standar.
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()); } }