طرح ناوبری را سفارشی کنید

انیمیشن نمونه‌ای که طرح‌بندی ناوبری سفارشی‌شده را در اندروید نشان می‌دهد

از سفارشی‌سازی طرح‌بندی برای کنترل سلسله مراتب نماها و قرارگیری صفحه نمایش، هم برای نماهای ارائه شده توسط گوگل و هم برای نماهای تجاری سفارشی خودتان، استفاده کنید.

به جای اضافه کردن نماهای سفارشی به بخش‌های از پیش تعریف شده هدر یا پاورقی، یک نماینده طرح‌بندی (layout delegate) پیاده‌سازی کنید تا طرح‌بندی را برای کل صفحه مدیریت کند. در طول انتقال حالت ناوبری، نماینده طرح‌بندی شما اجزای ارائه شده توسط گوگل، مانند کارت نوبت، کارت ETA و دکمه‌ها را دریافت می‌کند. این عناصر را با استفاده از سیستم‌های طرح‌بندی استاندارد اندروید مانند ConstraintLayout ، CoordinatorLayout یا LinearLayout در کنار محتوای سفارشی خود قرار دهید.

از این چارچوب برای قرار دادن اطلاعات تجاری سفارشی - مانند وضعیت سفارش یا دستورالعمل‌های دریافت - دقیقاً روی صفحه استفاده کنید و در عین حال از همپوشانی نماهای خود جلوگیری کنید.

نحوه سفارشی‌سازی طرح‌بندی

سفارشی‌سازی طرح‌بندی از یک الگوی طراحی وکالتی استفاده می‌کند. SDK به جای ترسیم یا قرارگیری خودکار اجزای رابط کاربری روی صفحه، آنها را مستقیماً به یک کلاس سفارشی که شما می‌نویسید، یعنی نماینده طرح‌بندی، ارسال می‌کند.

برای سفارشی‌سازی طرح‌بندی خود، کلاسی ایجاد کنید که کلاس انتزاعی NavigationLayoutDelegate بسط دهد و یک نمونه به NavigationView یا SupportNavigationFragment خود اختصاص دهد. هر زمان که حالت ناوبری تغییر می‌کند - مانند انتقال از نقشه پایه به راهنمای فعال گام به گام - SDK متدهای فراخوانی را روی نماینده شما اجرا می‌کند و اجزای رابط کاربری موجود برای آن حالت خاص را فراهم می‌کند.

برای ارائه یک تجربه کاربری یکپارچه و بی‌نقص، این چارچوب تفکیک واضحی از مسئولیت بین برنامه شما و SDK ایجاد می‌کند. از نماینده طرح‌بندی برای سفارشی‌سازی موارد زیر استفاده کنید:

  • سلسله مراتب نماها را بسازید: دقیقاً انتخاب کنید که کدام اجزای گوگل و نماهای تجاری سفارشی برای هر حالت رابط کاربری به صفحه اضافه شوند.
  • موقعیت هر عنصر: لنگرهای صفحه، حاشیه‌ها و محل قرارگیری طرح‌بندی را به طور دقیق تنظیم کنید. از اعمال محدودیت‌های عرض یا ارتفاع سفارشی برای اجزای گوگل خودداری کنید زیرا اجزای گوگل ابعاد داخلی خود را محاسبه می‌کنند.
  • نقشه پایه را قاب کنید: مرزهای قابل مشاهده دوربین نقشه را با استفاده از مختصات مؤلفه منظره تعریف کنید.
  • صفحه را لایه‌بندی کنید: تصمیم بگیرید که آیا نماهای سفارشی در بالا، پایین یا در کنار کنترل‌های داخلی گوگل شناور باشند.

در همین حال، شما نمی‌توانید اجزای گوگل زیر را با layout delegate سفارشی‌سازی کنید:

  • ابعاد کامپوننت: اندازه و ابعاد داخلی کامپوننت‌های ارائه شده توسط گوگل که SDK به طور خودکار محاسبه می‌کند.
  • شرایط فعال شدن: زمانی که هشدارها یا اعلان‌های پویا بر اساس داده‌های مسیر در لحظه ظاهر می‌شوند.

اصول اجرای

هنگام نوشتن یک layout delegate، قوانین زیر را برای جلوگیری از اشکالات طرح‌بندی یا خرابی‌های زمان اجرا در نظر داشته باشید:

  • پیش‌فرض یک صفحه خالی است: اجزای ارائه شده توسط گوگل فقط در صورتی ظاهر می‌شوند که نماینده شما صریحاً آنها را به سلسله مراتب نما اضافه کرده و موقعیت آنها را تعیین کند.
  • APIهای قدیمی پشتیبانی نمی‌شوند: ویژگی‌ها و متدهایی که مدل طرح‌بندی مبتنی بر اسلات قدیمی را کنترل می‌کنند، پشتیبانی نمی‌شوند و ممکن است هنگام فعال بودن یک نماینده سفارشی، آنطور که انتظار می‌رود رفتار نکنند.
  • ساختارهای نمای داخلی را تغییر ندهید: از متدهایی مانند findViewById() برای پیمایش یا تغییر سلسله مراتب نمای اجزای ارائه شده توسط گوگل (مانند کارت نوبت یا کارت ETA) استفاده نکنید. از آنجا که این سلسله مراتب نمای داخلی، جزئیات پیاده‌سازی زیربنایی هستند، در نسخه‌های مختلف SDK ممکن است تغییر کنند. تغییر آنها می‌تواند باعث شود طرح‌بندی شما در به‌روزرسانی‌های بعدی SDK از کار بیفتد.

سازگاری با API قدیمی

برای رفتار قابل اعتماد طرح‌بندی هنگام استفاده از نماینده طرح‌بندی سفارشی، از استفاده از APIهای مبتنی بر اسلات قدیمی زیر که برای منسوخ شدن برنامه‌ریزی شده‌اند، خودداری کنید. برای مهاجرت یک برنامه موجود، استفاده خود از این APIها را با کدی در نماینده طرح‌بندی سفارشی خود جایگزین کنید:

نمایش APIهای مبتنی بر اسلات‌های قدیمی

API قدیمی جایگزینی نماینده طرح‌بندی
setCustomControl(View, CustomControlPosition) نماهای خود را مستقیماً به ConstraintLayout یا گروه نمای دیگر خود اضافه کنید.
removeCustomControl(View) نماها را مستقیماً از سلسله مراتب نمای خود حذف کنید.
setEtaCardEnabled(boolean) نمای etaCard را در onEnterActiveGuidance بخوانید.
setHeaderEnabled(boolean) نمای turnCard را در onEnterActiveGuidance بخوانید.
setReportIncidentButtonEnabled(boolean) دکمه‌ی REPORTING را در getActiveGuidanceButtons() پیدا کنید.
setTripProgressBarEnabled(boolean) نمای tripProgressBar را در onEnterActiveGuidance بخوانید.
addOnNavigationUiChangedListener(...) به فراخوانی‌های انتقال حالت NavigationLayoutDelegate خود تکیه کنید.
removeOnNavigationUiChangedListener(...) انتقال حالت‌های طرح‌بندی را مستقیماً با استفاده از نماینده مدیریت کنید.
addPromptVisibilityChangedListener(...) به فراخوانی‌های سریع NavigationLayoutDelegate خود (مانند onShowPrompt() ) تکیه کنید.
removePromptVisibilityChangedListener(...) منطق نمایش اعلان را مستقیماً با استفاده از نماینده مدیریت کنید.
setCompassEnabled(boolean) قطب‌نما را در getNavigationReadyButtons() یا getActiveGuidanceButtons() پیدا کنید.

چک لیست ضروریات

برای پیاده‌سازی موفقیت‌آمیز layout delegate خود، این مراحل و الزامات ضروری را دنبال کنید:

  • مقداردهی اولیه نماینده قبل از ایجاد رابط کاربری: قبل از اینکه SDK رابط کاربری ناوبری را مقداردهی اولیه کند، تابع setLayoutDelegate() را فراخوانی کنید. برای تأیید زمان دقیق راه‌اندازی ساختار برنامه خود، به مثال‌های پیاده‌سازی کد زیر مراجعه کنید. مقداردهی اولیه نماینده پس از ایجاد رابط کاربری، باعث ایجاد خطای ApiIllegalStateException می‌شود.

    کاتلین

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

    جاوا

    // For SupportNavigationFragment
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        SupportNavigationFragment navFragment = (SupportNavigationFragment) getSupportFragmentManager().findFragmentById(R.id.nav_fragment);
        navFragment.setLayoutDelegate(new MyLayoutDelegate());
    }
    
    // For a programmatic NavigationView
    NavigationView navigationView = new NavigationView(context);
    navigationView.setLayoutDelegate(new MyLayoutDelegate());
    navigationView.onCreate(savedInstanceState);
            
  • ساخت کانتینر طرح‌بندی خودتان: یک گروه نمای سفارشی - مانند ConstraintLayout - ایجاد کنید تا عناصر رابط کاربری سفارشی شما و نماهای ارائه شده توسط گوگل را در خود جای دهد.

  • اضافه کردن نماهای اجباری: شما باید لوگوی گوگل ( getGoogleLogo() ) و Viewport ( getViewport() ) را در تمام حالت‌های ناوبری به کانتینر طرح‌بندی خود اضافه کنید. عدم اضافه کردن هر دوی این نماها باعث ایجاد خطای ApiIllegalStateException در زمان اجرا شده و برنامه را از کار می‌اندازد.

  • به ابعاد ثابت احترام بگذارید: SDK ابعاد چندین کامپوننت ارائه شده توسط گوگل را تعیین می‌کند. محدودیت‌های عرض یا ارتفاع سفارشی را برای عناصر زیر اعمال نکنید:

    • کارت را روشن کنید
    • کارت ایتا
    • درخواست‌های دریافتی
    • لوگوی گوگل
    • ویجت سرعت
  • طرح‌بندی خود را به نما (view) الصاق کنید: برای الصاق کانتینر خود به سلسله مراتب نمای نقشه، تابع navigationView.setNavigationLayout() را درون فراخوانی‌های onEnterNavigationReady() و onEnterActiveGuidance() فراخوانی کنید.

  • پاکسازی هنگام خروج از حالت: تابع navigationView.removeNavigationLayout() را فراخوانی کنید و نماهای سفارشی خود را درون فراخوانی‌های onLeave حذف کنید تا از نشت حافظه و همپوشانی حالت‌های رابط کاربری جلوگیری شود.

  • از APIهای طرح‌بندی قدیمی اجتناب کنید: APIهای مبتنی بر اسلات منسوخ‌شده مانند setCustomControl() یا setHeaderEnabled() را در حالی که یک نماینده سفارشی فعال است، فراخوانی نکنید. SDK ممکن است هنگام اتصال یک نماینده سفارشی، این فراخوانی‌های قدیمی را نادیده بگیرد.

حالت‌های رابط کاربری و کامپوننت‌های گوگل

وقتی یک تغییر حالت ناوبری رخ می‌دهد، SDK یک شیء UiState فقط خواندنی را به تابع فراخوانی نماینده شما ارسال می‌کند. این شیء، پرچم‌های پیکربندی طرح‌بندی فعلی را با اجزای گوگل (مثلاً نمونه‌های View ) که برای رندر صفحه نیاز دارید، بسته‌بندی می‌کند.

نماینده طرح‌بندی، سلسله مراتب نما را در چهار حالت عملیاتی مدیریت می‌کند. نمودار حالت زیر نحوه انتقال SDK بین حالت‌های ناوبری و متدهای فراخوانی که روی نماینده شما اجرا می‌کند را نشان می‌دهد:

نمودار انتقال وضعیت چرخه حیات NavigationLayoutDelegate که انتقال بین وضعیت‌های غیر ناوبری، آماده ناوبری و هدایت فعال را نشان می‌دهد.

اجزای خاص گوگل که به نماینده شما ارائه می‌شوند، به مرحله فعلی چرخه حیات ناوبری بستگی دارند.

اجزای مورد نیاز (تمام حالت‌های ناوبری)

صرف نظر از مرحله ناوبری فعلی، شما باید اجزای گوگل زیر را در سلسله مراتب نمای خود لحاظ کنید، آنها را در موقعیت مناسب قرار دهید و قابل مشاهده نگه دارید:

  • لوگوی گوگل ( getGoogleLogo() ): این کامپوننت لوگوی اجباری گوگل مپس را نمایش می‌دهد. اگر دکمه‌ی Re-center فعال باشد، وقتی راننده نقشه را از وسیله‌ی نقلیه‌ی خود دور می‌کند، لوگو به طور خودکار به این دکمه تبدیل می‌شود. در نتیجه، محل قرارگیری لوگو، محل نمایش دکمه‌ی Re-center را تعیین می‌کند. توصیه می‌شود لوگو را در گوشه‌ی پایین-چپ (پایین-چپ) طرح‌بندی خود قرار دهید تا با انتظارات استاندارد رابط کاربری نقشه مطابقت داشته باشد.

  • Viewport ( getViewport() ): یک View نامرئی که مرزهای قاب‌بندی دوربین را تعریف می‌کند. محل قرارگیری Viewport توسط شما، محل دقیق قرارگیری SDK در مرکز طرح شورون وسیله نقلیه و ترسیم خط مسیر فعال را تعیین می‌کند. Viewport را طوری قرار دهید که ناحیه باز و بدون پوشش صفحه نمایش شما را بپوشاند و از ایجاد پوشش‌های مات مانند صفحات پایینی سفارشی جلوگیری کند.

در طول حالت آماده برای ناوبری، رابط کاربری (UI) مینیمال باقی می‌ماند تا تمرکز روی نقشه پایه حفظ شود. شیء NavigationReadyUiState شما به موارد زیر دسترسی می‌دهد:

  • getNavigationReadyButtons(): فهرستی از نماهای عملیاتی شناور که برای نقشه پایه پیکربندی شده‌اند (معمولاً فقط دکمه Compass). از آنجا که گوگل ممکن است در نسخه‌های آینده SDK دکمه‌هایی را اضافه یا مرتب کند، از تکیه بر شاخص‌های لیست ثابت خودداری کنید. در عوض، در لیست حرکت کنید، نوع هر دکمه را با مقایسه getType() با ButtonKnownType.COMPASS بخوانید و با فراخوانی getView() نمای فیزیکی را استخراج کنید. توجه داشته باشید که دکمه‌های دقیق در این لیست با دکمه‌های موجود در طول هدایت فعال متفاوت هستند.

اجزای هدایت فعال

وقتی راهنمایی گام به گام شروع می‌شود، SDK مجموعه کامل کنترل‌های ناوبری را باز می‌کند. شیء ActiveGuidanceUiState شما به اجزای گوگل زیر دسترسی می‌دهد:

  • getTurnCard() : بنر اصلی هدر که مسیرهای مانورهای آینده، اندازه‌گیری‌های مسافت و راهنمای خطوط را نمایش می‌دهد. آن را در بالای طرح خود قرار دهید تا یک سلسله مراتب ناوبری آشنا ایجاد کنید و نماهای سفارشی خود را در اطراف آن قرار دهید.

  • getEtaCard() : بنر پایین صفحه که زمان تخمینی رسیدن، زمان باقیمانده سفر و فاصله باقیمانده تا مقصد را نمایش می‌دهد. آن را در امتداد لبه پایین صفحه خود قرار دهید، یا مختصات آن را با برگه‌های مدیریت وظیفه سفارشی خود ادغام کنید.

  • getTripProgressBar() : یک نوار پیشرفت عمودی که به راننده نشان می‌دهد در مسیر فعلی چقدر سفر کرده است. برخلاف طرح‌بندی‌های قدیمی که این نوار پیشرفت را صرفاً به لبه نقشه محدود می‌کردند، شما آزادی کامل دارید که آن را در هر جایی - مثلاً در امتداد لبه کناری کانتینر سفارشی خود - قرار دهید.

  • getSpeedWidget() : یک کنترل شناور برای سرعت‌سنج و محدودیت‌های سرعت ارسال شده. بسته به تنظیمات API و داده‌های موجود، این نما به صورت پویا بین چهار حالت بصری در زمان اجرا تغییر اندازه می‌دهد (هیچ چیزی نمایش نمی‌دهد، فقط سرعت فعلی، فقط محدودیت سرعت یا هر دو خوانش). از آنجا که ویجت می‌تواند بدون اطلاع قبلی بین این اندازه‌ها تغییر کند، همیشه نماهای اطراف را با استفاده از محدودیت‌ها لنگر بیندازید تا طرح‌بندی شما به طور خودکار سازگار شود تا از همپوشانی مکانی جلوگیری شود.

  • getActiveGuidanceButtons() : فهرستی گسترده از نماهای عملیاتی شناور برای حالت هدایت فعال (معمولاً شامل دکمه‌های Compass و Incident Reporting). درست مانند حالت آماده به کار در ناوبری، می‌توانید نماهای دکمه‌های منفرد را با فیلتر کردن لیست بر اساس ButtonKnownType ( COMPASS یا REPORTING ) و استخراج نما با استفاده از getView() پیدا کرده و استخراج کنید. سپس می‌توانید آنها را به طور مستقل قرار دهید یا از یک طرح‌بندی مانند AutoHidingLinearLayout برای انباشت ایمن آرایه بدون تداخل مکانی استفاده کنید.

اجزای اعلان پویای

اعلان‌هایی مانند هشدارهای حادثه یا هشدارهای دوربین‌های ایمنی، در طول هدایت فعال به‌طور مستقل فعال می‌شوند.

وقتی یک اعلان آماده نمایش است، SDK تابع onShowPrompt() مربوط به نماینده شما را فراخوانی می‌کند و یک view newPrompt ارسال می‌کند. نماینده شما مسئول قرار دادن روان این اعلان روی طرح‌بندی شما است (که معمولاً به لبه پایینی ظرف نقشه شما متصل می‌شود).

از آنجا که اعلان ورودی قسمت پایین صفحه را پوشش می‌دهد، باید طرح‌بندی خود را به‌روزرسانی کنید تا از همپوشانی اعلان با نمای دید، لوگوی گوگل یا هر دکمه‌ای که در پایین صفحه قرار دارد، جلوگیری شود.

مدیریت اندازه صفحه نمایش و حالت عریض

برای مدیریت تغییرات در ابعاد نقشه و جهت صفحه نمایش، نماینده طرح‌بندی از ویژگی‌های زیر استفاده می‌کند:

  • تغییر اندازه نمایش: طرح‌بندی شما را هر زمان که اندازه فیزیکی نقشه تغییر کند، تنظیم می‌کند.

  • حالت عریض: وقتی نقشه به اندازه کافی عریض باشد، به یک نوع طرح‌بندی با فرمت عریض تغییر می‌کند.

در حال پاسخ به تغییر اندازه نما

هر زمان که ابعاد فیزیکی ظرف نقشه تغییر کند، SDK فراخوانی onSizeChanged() را اجرا می‌کند. طرح‌بندی‌های تقسیم صفحه، اسلایدرهای طرح‌بندی و چرخش دستگاه معمولاً فراخوانی onSizeChanged() را فعال می‌کنند. از این فراخوانی برای ایجاد تنظیمات واکنش‌گرای کلی در رابط کاربری سفارشی خود استفاده کنید. onSizeChanged() را برای تغییر موقعیت عناصر سفارشی برای نسبت ابعاد صفحه نمایش جدید، اعمال نقاط شکست عرض یا ارتفاع سفارشی خود و تشخیص زمانی که یک رویداد تغییر اندازه، حالت عریض را تغییر می‌دهد تا انواع طرح‌بندی با خیال راحت تعویض شوند، پیاده‌سازی کنید.

درک عریض

حالت عریض زمانی فعال می‌شود که کانتینر نقشه به اندازه کافی عریض باشد تا اجزای رابط کاربری را در کنار هم نمایش دهد.

مقدار بولی isWideMode() را از شیء state بخوانید تا عناصر رابط کاربری سفارشی خود را تغییر موقعیت دهید و مرکز نقشه را برای راننده خالی نگه دارید. از آنجا که اجزای ارائه شده توسط گوگل مانند کارت چرخش و کارت ETA به طور خودکار در حالت عریض کوچک و تغییر شکل می‌دهند، خواندن این مقدار بولی تضمین می‌کند که طرح شما دقیقاً در همان لحظه‌ای که اجزای گوگل به‌روزرسانی می‌شوند، سازگار می‌شود.

مثال‌های زیر را برای موقعیت‌یابی اجزای طرح‌بندی خود در حالت‌های استاندارد و عریض در نظر بگیرید:

  • حالت عمودی استاندارد: کارت نوبت را در بالای صفحه و کارت زمان رسیدن به مقصد را در پایین صفحه قرار دهید.

  • حالت عریض: کارت نوبت را به سمت شروع صفحه و کارت زمان رسیدن به مقصد را به سمت انتهای صفحه منتقل کنید.

بررسی حالت عریض در حین انتقال وضعیت

اگر طرح‌بندی شما از تغییر حالت عریض پشتیبانی می‌کند، به جای تکیه بر onSizeChanged() ، مقدار بولی isWideMode() را درون هر فراخوانی انتقال حالت ارزیابی کنید.

شیء وضعیت را در طول فراخوانی‌های برگشتی مانند onEnterNavigationReady() و onEnterActiveGuidance() بررسی کنید تا راه‌اندازی اولیه برنامه در حالت افقی را مدیریت کنید. این رویکرد از طرح‌بندی شما در برابر رویدادهای چرخه حیات سیستم اندروید - مانند ایجاد مجدد Activity پیش‌فرض - که به طور کامل فراخوانی تغییر اندازه را دور می‌زنند، محافظت می‌کند و تضمین می‌کند که نوع صحیح طرح‌بندی استاندارد یا عریض در لحظه شروع یک حالت ناوبری جدید فعال می‌شود.

هم‌ترازی با استایل گوگل

برای کمک به هماهنگی رابط کاربری سفارشی شما با ریتم بصری گوگل، SDK کلاس کاربردی StyleValues ​​را ارائه می‌دهد. می‌توانید این مقادیر را در پیکسل‌های مستقل از چگالی (dp) بخوانید تا نماهای خود را کاملاً با اجزای گوگل هماهنگ کنید.

برای مثال، اگر می‌خواهید یک دکمه سفارشی را در گوشه بالای صفحه، روبروی کارت نوبت قرار دهید، می‌توانید StyleValues.headerTopPaddingDp() را فراخوانی کنید و مقدار برگشتی را به عنوان حاشیه بالای دکمه خود تعیین کنید. این کار تضمین می‌کند که دکمه سفارشی شما از نظر بصری با لبه بالای کارت نوبت هم‌تراز می‌شود و تقارن صفحه را حفظ می‌کند.

بالشتک‌ها و اندازه‌های موجود عبارتند از:

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

مثال: پیاده‌سازی طرح‌بندی مبتنی بر محدودیت

مثال زیر یک نماینده طرح‌بندی اولیه را نشان می‌دهد که انتقال حالت‌ها را با استفاده از تعاریف برنامه‌ریزی‌شده ConstraintLayout و ConstraintSet مدیریت می‌کند.

اگرچه این مثال محدودیت‌های نما را در کد ایجاد می‌کند، نمایندگان طرح‌بندی می‌توانند طرح‌بندی‌های استاندارد XML اندروید را نیز متورم کنند.

کاتلین

/*
 * 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()
  }
}

    

جاوا

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