سفارشی‌سازی چیدمان پیمایش

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

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

به‌جای افزودن نماهای سفارشی به جایگاه‌های ازپیش تعریف‌شده سرصفحه یا پاصفحه، یک نماینده چیدمان را برای مدیریت چیدمان کل صفحه پیاده‌سازی کنید. درطول انتقال‌های حالت ناوبری، نماینده چیدمان شما مؤلفه‌های ارائه‌شده توسط Google را دریافت می‌کند، مانند کارت پیچ، کارت زمان تقریبی رسیدن، و دکمه‌ها. این عناصر را بااستفاده از سیستم‌های چیدمان استاندارد Android مثل ConstraintLayout، CoordinatorLayout، یا LinearLayout در کنار محتوای سفارشی خودتان قرار دهید.

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

نحوه عملکرد سفارشی‌سازی چیدمان

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

برای سفارشی‌سازی چیدمان، کلاسی ایجاد کنید که کلاس انتزاعی NavigationLayoutDelegate را گسترش دهد و نمونه‌ای را به NavigationView یا SupportNavigationFragment شما اختصاص دهد. هرگاه وضعیت ناوبری تغییر کند—مثلاً از نقشه مبنا به راهنمایی گام‌به‌گام فعال منتقل شود—کیت توسعه نرم‌افزار روش‌های فراخوانی را در نماینده شما اجرا می‌کند و مؤلفه‌های واسط کاربر دردسترس برای آن وضعیت خاص را ارائه می‌دهد.

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

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

درعین‌حال، نمی‌توانید مؤلفه‌های Google زیر را با نماینده چیدمان سفارشی‌سازی کنید:

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

اصول پیاده‌سازی

هنگام نوشتن نماینده چیدمان، قوانین زیر را در ذهن داشته باشید تا از اشکالات چیدمان یا ازکارافتادن زمان اجرا جلوگیری کنید:

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

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

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

نمایش میاناهای برنامه‌سازی کاربردی قدیمی مبتنی بر جایگاه

Legacy API جایگزینی Layout Delegate
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() پیدا کنید.

بازبینه ضروریات

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

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

    Kotlin

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

    جاوا

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

  • پیوست کردن نماهای اجباری: باید نشان‌واره Google (getGoogleLogo()) و «نمای دید» (getViewport()) را در همه حالت‌های ناوبری به محفظه چیدمانتان اضافه کنید. اگر هر دو این نماها گنجانده نشود، ApiIllegalStateException زمان اجرا راه‌اندازی می‌شود و برنامه ازکار می‌افتد.

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

    • کارت نوبت
    • کارت زمان تقریبی ورود
    • پیام‌واره‌های ورودی
    • نشان‌واره Google
    • ابزاره سرعت
  • چیدمانتان را به نما پیوست کنید: navigationView.setNavigationLayout() را در onEnterNavigationReady() و onEnterActiveGuidance() فراخوانی‌های برگشتی فراخوانی کنید تا محفظه‌تان را به سلسله‌مراتب نمای نقشه پیوست کنید.

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

  • از میاناهای برنامه‌سازی کاربردی چیدمان قدیمی اجتناب کنید: وقتی نماینده سفارشی فعال است، میاناهای برنامه‌سازی کاربردی مبتنی بر جایگاه منسوخ‌شده مثل setCustomControl() یا setHeaderEnabled() را فراخوانی نکنید. وقتی نماینده سفارشی پیوست شده باشد، کیت توسعه نرم‌افزار ممکن است این تماس‌های قدیمی را نادیده بگیرد.

وضعیت‌های واسط کاربر و مؤلفه‌های Google

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

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

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

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

اجزای الزامی (همه حالت‌های پیمایش)

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

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

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

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

  • getNavigationReadyButtons(): فهرستی از نماهای کنش شناور پیکربندی‌شده برای نقشه پایه (معمولاً فقط دکمه «قطب‌نما»). چون ممکن است Google در نسخه‌های آتی کیت توسعه نرم‌افزار دکمه‌هایی را اضافه کند یا ترتیب آن‌ها را تغییر دهد، از تکیه بر نمایه‌های فهرست ثابت خودداری کنید. به‌جای آن، فهرست را تکرار کنید، نوع هر دکمه را با مقایسه کردن getType() با ButtonKnownType.COMPASS بخوانید، و نمای فیزیکی را با فراخوانی getView() استخراج کنید. توجه داشته باشید که دکمه‌های دقیق در این فهرست با دکمه‌های دردسترس درطول راهنمایی فعال متفاوت است.

مؤلفه راهنمایی فعال

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

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

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

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

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

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

‫ مؤلفه‌های پیام‌واره پویا

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

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

چون پیام‌واره ورودی بخش پایینی صفحه‌نمایش را می‌پوشاند، باید چیدمانتان را به‌روز کنید تا پیام‌واره با نمای دید، نشان‌واره Google، یا دکمه‌های تراز پایین هم‌پوشانی نداشته باشد.

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

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

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

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

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

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

آشنایی با حالت عریض

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

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

نمونه‌های زیر را برای جای‌گذاری مؤلفه‌های چیدمان در حالت‌های استاندارد و عریض درنظر بگیرید:

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

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

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

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

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

تراز کردن با سبک‌بندی Google

برای کمک به اینکه واسط کاربر سفارشی شما با ریتم بصری Google مطابقت داشته باشد، کیت توسعه نرم‌افزار کلاس کاربردی StyleValues را ارائه می‌دهد. می‌توانید این مقادیر را در پیکسل‌های مستقل از تراکم (dp) بخوانید تا نماهایتان را کاملاً با مؤلفه‌های Google تراز کنید.

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

حاشیه‌های داخلی و اندازه‌های سبک دردسترس عبارت‌اند از:

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

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

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

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

Kotlin

/*
 * Copyright 2026 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.navigationapidemo.layoutdelegate

import android.content.Context
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import com.google.android.libraries.navigation.layoutcustomization.ActiveGuidanceUiState
import com.google.android.libraries.navigation.layoutcustomization.AutoHidingVerticalLayout
import com.google.android.libraries.navigation.layoutcustomization.NavigationLayoutDelegate
import com.google.android.libraries.navigation.layoutcustomization.NavigationReadyUiState
import com.google.android.libraries.navigation.layoutcustomization.NavigationUiButton.ButtonKnownType.COMPASS
import com.google.android.libraries.navigation.layoutcustomization.NavigationUiParent
import com.google.android.libraries.navigation.layoutcustomization.StyleValues.headerNominalHeightDp

/** Kotlin equivalent of StandardUiElementsLayoutDelegate. */
class StandardUiElementsLayoutDelegateKt : NavigationLayoutDelegate() {

  private val layoutId = View.generateViewId()
  private val buttonsContainerId = View.generateViewId()

  private var layout: ConstraintLayout? = null
  private var buttonsContainer: AutoHidingVerticalLayout? = null

  private var navigationReadyConstraintSet: ConstraintSet? = null
  private var activeGuidanceConstraintSet: ConstraintSet? = null
  private var activeGuidanceWithPromptConstraintSet: ConstraintSet? = null

  private var activeGuidanceUiState: ActiveGuidanceUiState? = null

  override fun onEnterNavigationReady(
    navigationUiParent: NavigationUiParent,
    newState: NavigationReadyUiState,
  ) {
    val context = navigationUiParent.viewContext

    var currentLayout = layout
    if (currentLayout == null) {
      currentLayout =
        ConstraintLayout(context).apply {
          layoutParams =
            ViewGroup.LayoutParams(
              ViewGroup.LayoutParams.MATCH_PARENT,
              ViewGroup.LayoutParams.MATCH_PARENT,
            )
          id = layoutId
        }
      layout = currentLayout
    }

    removeFromParentView(newState.viewport)
    currentLayout.addView(
      newState.viewport,
      ViewGroup.LayoutParams(
        ViewGroup.LayoutParams.MATCH_PARENT,
        ViewGroup.LayoutParams.MATCH_PARENT,
      ),
    )

    removeFromParentView(newState.googleLogo)
    currentLayout.addView(
      newState.googleLogo,
      ViewGroup.LayoutParams(
        ViewGroup.LayoutParams.MATCH_PARENT,
        ViewGroup.LayoutParams.WRAP_CONTENT,
      ),
    )

    var currentButtonsContainer = buttonsContainer
    if (currentButtonsContainer == null) {
      currentButtonsContainer = AutoHidingVerticalLayout(context).apply { id = buttonsContainerId }
      buttonsContainer = currentButtonsContainer
    }

    removeFromParentView(currentButtonsContainer)
    currentLayout.addView(
      currentButtonsContainer,
      ViewGroup.LayoutParams(
        ViewGroup.LayoutParams.WRAP_CONTENT,
        ConstraintLayout.LayoutParams.MATCH_CONSTRAINT,
      ),
    )

    for (button in newState.navigationReadyButtons) {
      removeFromParentView(button.view)
      currentButtonsContainer.addView(button.view)
    }

    if (navigationReadyConstraintSet == null) {
      navigationReadyConstraintSet = buildNavigationReadyConstraintSet(newState)
    }
    navigationReadyConstraintSet?.applyTo(currentLayout)

    navigationUiParent.removeNavigationLayout(currentLayout)
    navigationUiParent.setNavigationLayout(currentLayout)
  }

  private fun buildNavigationReadyConstraintSet(uiState: NavigationReadyUiState): ConstraintSet {
    return ConstraintSet().apply {
      clone(layout)

      connect(
        uiState.viewport.id,
        ConstraintSet.START,
        ConstraintSet.PARENT_ID,
        ConstraintSet.START,
      )
      connect(uiState.viewport.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
      connect(uiState.viewport.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
      connect(
        uiState.viewport.id,
        ConstraintSet.BOTTOM,
        ConstraintSet.PARENT_ID,
        ConstraintSet.BOTTOM,
      )

      connect(
        uiState.googleLogo.id,
        ConstraintSet.BOTTOM,
        ConstraintSet.PARENT_ID,
        ConstraintSet.BOTTOM,
      )
      connect(
        uiState.googleLogo.id,
        ConstraintSet.START,
        ConstraintSet.PARENT_ID,
        ConstraintSet.START,
      )

      constrainButtonsToBottomEnd()
    }
  }

  override fun onLeaveNavigationReady(
    navigationUiParent: NavigationUiParent,
    oldState: NavigationReadyUiState,
  ) {
    buttonsContainer?.removeAllViews()
    layout?.removeAllViews()
    layout?.let { navigationUiParent.removeNavigationLayout(it) }
  }

  override fun onEnterActiveGuidance(
    navigationUiParent: NavigationUiParent,
    oldState: NavigationReadyUiState,
    newState: ActiveGuidanceUiState,
  ) {
    activeGuidanceUiState = newState
    val context = navigationUiParent.viewContext
    val currentLayout = checkNotNull(layout) { "layout must be initialized" }
    val currentButtonsContainer =
      checkNotNull(buttonsContainer) { "buttonsContainer must be initialized" }

    removeFromParentView(newState.turnCard)
    currentLayout.addView(
      newState.turnCard,
      ViewGroup.LayoutParams(
        ViewGroup.LayoutParams.WRAP_CONTENT,
        ViewGroup.LayoutParams.WRAP_CONTENT,
      ),
    )

    removeFromParentView(newState.etaCard)
    currentLayout.addView(
      newState.etaCard,
      ViewGroup.LayoutParams(
        ViewGroup.LayoutParams.WRAP_CONTENT,
        ViewGroup.LayoutParams.WRAP_CONTENT,
      ),
    )

    for (button in oldState.navigationReadyButtons) {
      removeFromParentView(button.view)
    }

    for (button in newState.activeGuidanceButtons) {
      val buttonLayoutParams =
        AutoHidingVerticalLayout.LayoutParams(
          ViewGroup.LayoutParams.WRAP_CONTENT,
          ViewGroup.LayoutParams.WRAP_CONTENT,
        )
      if (button.type == COMPASS) {
        buttonLayoutParams.isHighPriority = true
      }
      removeFromParentView(button.view)
      currentButtonsContainer.addView(button.view, buttonLayoutParams)
    }

    if (activeGuidanceConstraintSet == null) {
      activeGuidanceConstraintSet = buildActiveGuidanceConstraintSet(context, newState)
    }

    activeGuidanceConstraintSet?.applyTo(currentLayout)
  }

  override fun onLeaveActiveGuidance(
    navigationUiParent: NavigationUiParent,
    oldState: ActiveGuidanceUiState,
    newState: NavigationReadyUiState,
  ) {
    removeFromParentView(oldState.etaCard)
    removeFromParentView(oldState.turnCard)
    buttonsContainer?.removeAllViews()

    val currentButtonsContainer =
      checkNotNull(buttonsContainer) { "buttonsContainer must be initialized" }
    for (button in newState.navigationReadyButtons) {
      currentButtonsContainer.addView(
        button.view,
        ViewGroup.LayoutParams(
          ViewGroup.LayoutParams.WRAP_CONTENT,
          ViewGroup.LayoutParams.WRAP_CONTENT,
        ),
      )
    }

    navigationReadyConstraintSet?.applyTo(layout)
  }

  private fun buildActiveGuidanceConstraintSet(
    context: Context,
    uiState: ActiveGuidanceUiState,
  ): ConstraintSet {
    return ConstraintSet().apply {
      clone(layout)

      connect(uiState.turnCard.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
      connect(
        uiState.turnCard.id,
        ConstraintSet.START,
        ConstraintSet.PARENT_ID,
        ConstraintSet.START,
      )

      clear(uiState.viewport.id)
      connect(
        uiState.viewport.id,
        ConstraintSet.START,
        ConstraintSet.PARENT_ID,
        ConstraintSet.START,
      )
      connect(uiState.viewport.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
      setMargin(uiState.viewport.id, ConstraintSet.TOP, dpToPx(headerNominalHeightDp(), context))
      connect(uiState.viewport.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
      connect(uiState.viewport.id, ConstraintSet.BOTTOM, uiState.etaCard.id, ConstraintSet.TOP)

      clear(uiState.googleLogo.id, ConstraintSet.BOTTOM)
      constrainLogoToTopOfEtaCard(uiState)
      constrainEtaCardToBottomStart(uiState)
      constrainButtonsToTopOfEtaCard(context, uiState)
    }
  }

  private fun ConstraintSet.constrainEtaCardToBottomStart(uiState: ActiveGuidanceUiState) {
    connect(uiState.etaCard.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
    connect(uiState.etaCard.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
  }

  private fun ConstraintSet.constrainLogoToTopOfEtaCard(uiState: ActiveGuidanceUiState) {
    connect(uiState.googleLogo.id, ConstraintSet.BOTTOM, uiState.etaCard.id, ConstraintSet.TOP)
  }

  private fun ConstraintSet.constrainButtonsToBottomEnd() {
    clear(buttonsContainerId, ConstraintSet.BOTTOM)
    clear(buttonsContainerId, ConstraintSet.TOP)
    connect(buttonsContainerId, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
    connect(buttonsContainerId, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
    connect(buttonsContainerId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
  }

  private fun ConstraintSet.constrainButtonsToTopOfEtaCard(
    context: Context,
    uiState: ActiveGuidanceUiState,
  ) {
    clear(buttonsContainerId, ConstraintSet.BOTTOM)
    clear(buttonsContainerId, ConstraintSet.TOP)
    connect(buttonsContainerId, ConstraintSet.BOTTOM, uiState.etaCard.id, ConstraintSet.TOP)
    connect(buttonsContainerId, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
    connect(buttonsContainerId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
    setMargin(buttonsContainerId, ConstraintSet.TOP, dpToPx(headerNominalHeightDp(), context))
  }

  override fun onShowPrompt(navigationUiParent: NavigationUiParent, newPrompt: View) {
    val context = navigationUiParent.viewContext
    layout?.addView(newPrompt)

    if (activeGuidanceUiState != null) {
      activeGuidanceWithPromptConstraintSet =
        buildActiveGuidanceWithPromptConstraintSet(context, newPrompt)
      activeGuidanceWithPromptConstraintSet?.applyTo(layout)
    }
  }

  override fun onChangePrompt(
    navigationUiParent: NavigationUiParent,
    oldPrompt: View,
    newPrompt: View,
  ) {
    val context = navigationUiParent.viewContext
    activeGuidanceWithPromptConstraintSet?.clear(oldPrompt.id)

    val currentLayout = checkNotNull(layout) { "layout must be initialized" }
    currentLayout.removeView(oldPrompt)
    currentLayout.addView(newPrompt)

    if (activeGuidanceUiState != null) {
      activeGuidanceWithPromptConstraintSet =
        buildActiveGuidanceWithPromptConstraintSet(context, newPrompt)
      activeGuidanceWithPromptConstraintSet?.applyTo(currentLayout)
    }
  }

  override fun onHidePrompt(navigationUiParent: NavigationUiParent, oldPrompt: View) {
    activeGuidanceWithPromptConstraintSet?.clear(oldPrompt.id)
    layout?.removeView(oldPrompt)

    activeGuidanceConstraintSet?.applyTo(layout)
  }

  private fun buildActiveGuidanceWithPromptConstraintSet(
    context: Context,
    prompt: View,
  ): ConstraintSet {
    return ConstraintSet().apply {
      clone(layout)

      val state =
        checkNotNull(activeGuidanceUiState) { "activeGuidanceUiState must be initialized" }

      clear(state.viewport.id)
      connect(state.viewport.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
      connect(state.viewport.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
      setMargin(state.viewport.id, ConstraintSet.TOP, dpToPx(headerNominalHeightDp(), context))
      connect(state.viewport.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
      connect(state.viewport.id, ConstraintSet.BOTTOM, prompt.id, ConstraintSet.TOP)

      clear(state.googleLogo.id, ConstraintSet.BOTTOM)
      connect(state.googleLogo.id, ConstraintSet.BOTTOM, prompt.id, ConstraintSet.TOP)

      connect(prompt.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
      connect(prompt.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
    }
  }

  private fun removeFromParentView(view: View?) {
    if (view?.parent != null) {
      (view.parent as ViewGroup).removeView(view)
    }
  }

  private fun dpToPx(dp: Int, context: Context): Int {
    return TypedValue.applyDimension(
        TypedValue.COMPLEX_UNIT_DIP,
        dp.toFloat(),
        context.resources.displayMetrics,
      )
      .toInt()
  }
}

    

جاوا

/*
 * Copyright 2026 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.navigationapidemo.layoutdelegate;

import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.MATCH_CONSTRAINT;
import static com.google.android.libraries.navigation.layoutcustomization.NavigationUiButton.ButtonKnownType.COMPASS;

import android.content.Context;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import com.google.android.libraries.navigation.layoutcustomization.ActiveGuidanceUiState;
import com.google.android.libraries.navigation.layoutcustomization.AutoHidingVerticalLayout;
import com.google.android.libraries.navigation.layoutcustomization.NavigationLayoutDelegate;
import com.google.android.libraries.navigation.layoutcustomization.NavigationReadyUiState;
import com.google.android.libraries.navigation.layoutcustomization.NavigationUiButton;
import com.google.android.libraries.navigation.layoutcustomization.NavigationUiParent;
import com.google.android.libraries.navigation.layoutcustomization.StyleValues;

/**
 * A sample implementation of {@link NavigationLayoutDelegate} demonstrating a basic,
 * portrait-optimized layout using {@link ConstraintLayout}.
 *
 * <p><b>Understanding the Layout Delegate State Machine:</b> Navigation SDK transitions through
 * distinct states, each calling corresponding lifecycle methods on this delegate:
 *
 * <ul>
 *   <li><b>Navigation Ready:</b> Initiated by {@link #onEnterNavigationReady}. We initialize the
 *       layout here and add non-guidance views, then pass it to {@link NavigationUiParent} as the
 *       navigation layout.
 *   <li><b>Active Guidance (Turn-by-Turn Mode):</b> Initiated by {@link #onEnterActiveGuidance}. We
 *       set up the layout for Active Guidance, adding elements such as the turn card and ETA card.
 *   <li><b>Prompts:</b> Prompts (e.g., incident alerts) may be triggered during Active Guidance
 *       mode and can be added to the layout via {@link #onShowPrompt}.
 * </ul>
 *
 * This class caches its {@link ConstraintSet}s to ensure smooth transitions without needing to
 * recreate or inflate layouts continuously.
 */
public class StandardUiElementsLayoutDelegate extends NavigationLayoutDelegate {
  private final int layoutId;
  private final int buttonsContainerId;

  private ConstraintLayout layout;
  private AutoHidingVerticalLayout buttonsContainer;

  // We cache our ConstraintSet definitions to avoid cloning or rebuilding
  // constraint configurations programmatically on every transition. This optimization
  // keeps UI state switches (such as entering active guidance or popping up prompts) highly
  // performant.
  private ConstraintSet navigationReadyConstraintSet;
  private ConstraintSet activeGuidanceConstraintSet;
  private ConstraintSet activeGuidanceWithPromptConstraintSet;

  private ActiveGuidanceUiState activeGuidanceUiState;

  public StandardUiElementsLayoutDelegate() {
    layoutId = View.generateViewId();
    buttonsContainerId = View.generateViewId();
  }

  @Override
  public void onEnterNavigationReady(
      NavigationUiParent navigationUiParent, NavigationReadyUiState newState) {
    Context context = navigationUiParent.getViewContext();

    // Implementation Tip: For simplicity, this sample instantiates views and constraints
    // programmatically. In a production application, you can safely inflate standard XML
    // layout templates to build your layout hierarchies and define base UI constraints.

    // Create the root layout
    if (layout == null) {
      layout = new ConstraintLayout(context);
      LayoutParams layoutParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT);
      layout.setLayoutParams(layoutParams);
      layout.setId(layoutId);
    }

    // Add the Viewport (REQUIRED):
    // The viewport is an invisible bounding box used by Nav SDK to frame the vehicle
    // chevron and the upcoming route line. We want to position this view such that it avoids
    // being obscured by fully-opaque UI elements (like the turn card or the ETA card).
    removeFromParentView(newState.getViewport());
    LayoutParams viewportLayoutParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT);
    layout.addView(newState.getViewport(), viewportLayoutParams);

    // Add the Google Logo / Re-center Button (REQUIRED):
    // This view displays the Google logo during guidance and may transition into a
    // "Re-center" button if the user scrolls away from the vehicle chevron. It must
    // be added to the view hierarchy in all states.
    removeFromParentView(newState.getGoogleLogo());
    LayoutParams googleLogoLayoutParams = new LayoutParams(MATCH_PARENT, WRAP_CONTENT);
    layout.addView(newState.getGoogleLogo(), googleLogoLayoutParams);

    // Add the container for UI buttons
    if (buttonsContainer == null) {
      // We use AutoHidingVerticalLayout to create an adaptive vertical button container that
      // automatically hides or shows child views based on available screen height.
      buttonsContainer = new AutoHidingVerticalLayout(context);
      buttonsContainer.setId(buttonsContainerId);
    }
    removeFromParentView(buttonsContainer);
    LayoutParams buttonsContainerLayoutParams = new LayoutParams(WRAP_CONTENT, MATCH_CONSTRAINT);
    layout.addView(buttonsContainer, buttonsContainerLayoutParams);

    // Add UI buttons to the container
    for (NavigationUiButton button : newState.getNavigationReadyButtons()) {
      removeFromParentView(button.getView());
      buttonsContainer.addView(button.getView());
    }

    // Build constraint set for Navigation Ready state
    if (navigationReadyConstraintSet == null) {
      navigationReadyConstraintSet = buildNavigationReadyConstraintSet(newState);
    }

    // Apply the constraints
    navigationReadyConstraintSet.applyTo(layout);

    // Set the layout in NavigationUiParent
    navigationUiParent.removeNavigationLayout(layout);
    navigationUiParent.setNavigationLayout(layout);
  }

  private ConstraintSet buildNavigationReadyConstraintSet(NavigationReadyUiState uiState) {
    ConstraintSet constraintSet = new ConstraintSet();
    constraintSet.clone(layout);

    // Constrain viewport to the edges of its parent
    constraintSet.connect(
        uiState.getViewport().getId(),
        ConstraintSet.START,
        ConstraintSet.PARENT_ID,
        ConstraintSet.START);
    constraintSet.connect(
        uiState.getViewport().getId(),
        ConstraintSet.TOP,
        ConstraintSet.PARENT_ID,
        ConstraintSet.TOP);
    constraintSet.connect(
        uiState.getViewport().getId(),
        ConstraintSet.END,
        ConstraintSet.PARENT_ID,
        ConstraintSet.END);
    constraintSet.connect(
        uiState.getViewport().getId(),
        ConstraintSet.BOTTOM,
        ConstraintSet.PARENT_ID,
        ConstraintSet.BOTTOM);

    // Constrain the logo to the bottom start corner
    constraintSet.connect(
        uiState.getGoogleLogo().getId(),
        ConstraintSet.BOTTOM,
        ConstraintSet.PARENT_ID,
        ConstraintSet.BOTTOM);
    constraintSet.connect(
        uiState.getGoogleLogo().getId(),
        ConstraintSet.START,
        ConstraintSet.PARENT_ID,
        ConstraintSet.START);

    constrainButtonsToBottomEnd(constraintSet);

    return constraintSet;
  }

  @Override
  public void onLeaveNavigationReady(
      NavigationUiParent navigationUiParent, NavigationReadyUiState oldState) {
    buttonsContainer.removeAllViews();
    layout.removeAllViews();
    navigationUiParent.removeNavigationLayout(layout);
  }

  @Override
  public void onEnterActiveGuidance(
      NavigationUiParent navigationUiParent,
      NavigationReadyUiState oldState,
      ActiveGuidanceUiState newState) {
    activeGuidanceUiState = newState;

    Context context = navigationUiParent.getViewContext();

    // Sizing Guideline: The turn card and ETA card are internally configured to adapt and size
    // themselves dynamically based on the layout width (non-wideMode vs. wideMode). Forcing fixed
    // widths or heights on these elements via layouts is unsupported. Always use WRAP_CONTENT to
    // let the elements determine their optimal proportions.

    // Add the turn card
    removeFromParentView(newState.getTurnCard());
    LayoutParams turnCardLayoutParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
    layout.addView(newState.getTurnCard(), turnCardLayoutParams);

    // Add the ETA card
    removeFromParentView(newState.getEtaCard());
    LayoutParams etaCardLayoutParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
    layout.addView(newState.getEtaCard(), etaCardLayoutParams);

    // Remove the Navigation Ready UI buttons
    for (NavigationUiButton button : oldState.getNavigationReadyButtons()) {
      removeFromParentView(button.getView());
    }

    // By adding all buttons to the AutoHidingVerticalLayout, we can easily incorporate the latest
    // set of buttons when upgrading without any code changes required
    for (NavigationUiButton button : newState.getActiveGuidanceButtons()) {
      AutoHidingVerticalLayout.LayoutParams buttonLayoutParams =
          new AutoHidingVerticalLayout.LayoutParams(
              ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);

      // Mark critical buttons (such as the compass) as high priority so they are the last to be
      // hidden by AutoHidingVerticalLayout when layout space is limited.
      if (button.getType() == COMPASS) {
        buttonLayoutParams.isHighPriority = true;
      }

      removeFromParentView(button.getView());
      buttonsContainer.addView(button.getView(), buttonLayoutParams);
    }

    // Build constraint set for Active Guidance state
    if (activeGuidanceConstraintSet == null) {
      activeGuidanceConstraintSet = buildActiveGuidanceConstraintSet(context, newState);
    }

    // Apply the constraints
    activeGuidanceConstraintSet.applyTo(layout);
  }

  @Override
  public void onLeaveActiveGuidance(
      NavigationUiParent navigationUiParent,
      ActiveGuidanceUiState oldState,
      NavigationReadyUiState newState) {

    // Remove Active Guidance UI elements
    removeFromParentView(oldState.getEtaCard());
    removeFromParentView(oldState.getTurnCard());
    buttonsContainer.removeAllViews();

    // Add Navigation Ready UI buttons
    for (NavigationUiButton button : newState.getNavigationReadyButtons()) {
      LayoutParams buttonLayoutParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
      buttonsContainer.addView(button.getView(), buttonLayoutParams);
    }

    navigationReadyConstraintSet.applyTo(layout);
  }

  private ConstraintSet buildActiveGuidanceConstraintSet(
      Context context, ActiveGuidanceUiState uiState) {
    ConstraintSet constraintSet = new ConstraintSet();
    constraintSet.clone(layout);

    // Constrain turn card to top start corner
    constraintSet.connect(
        uiState.getTurnCard().getId(),
        ConstraintSet.TOP,
        ConstraintSet.PARENT_ID,
        ConstraintSet.TOP);
    constraintSet.connect(
        uiState.getTurnCard().getId(),
        ConstraintSet.START,
        ConstraintSet.PARENT_ID,
        ConstraintSet.START);

    // Constrain viewport to top of ETA card
    constraintSet.clear(uiState.getViewport().getId());
    constraintSet.connect(
        uiState.getViewport().getId(),
        ConstraintSet.START,
        ConstraintSet.PARENT_ID,
        ConstraintSet.START);
    constraintSet.connect(
        uiState.getViewport().getId(),
        ConstraintSet.TOP,
        ConstraintSet.PARENT_ID,
        ConstraintSet.TOP);

    // Instead of constraining the viewport's top directly to the bottom of the turn card
    // (which varies in height and would trigger jumpy camera framing updates), we use a fixed
    // nominal height to estimate the height of the turncard.
    constraintSet.setMargin(
        uiState.getViewport().getId(),
        ConstraintSet.TOP,
        dpToPx(StyleValues.headerNominalHeightDp(), context));

    constraintSet.connect(
        uiState.getViewport().getId(),
        ConstraintSet.END,
        ConstraintSet.PARENT_ID,
        ConstraintSet.END);
    constraintSet.connect(
        uiState.getViewport().getId(),
        ConstraintSet.BOTTOM,
        uiState.getEtaCard().getId(),
        ConstraintSet.TOP);

    constraintSet.clear(uiState.getGoogleLogo().getId(), ConstraintSet.BOTTOM);
    constrainLogoToTopOfEtaCard(uiState, constraintSet);

    constrainEtaCardToBottomStart(uiState, constraintSet);

    constrainButtonsToTopOfEtaCard(context, uiState, constraintSet);

    return constraintSet;
  }

  private static void constrainEtaCardToBottomStart(
      ActiveGuidanceUiState uiState, ConstraintSet constraintSet) {
    constraintSet.connect(
        uiState.getEtaCard().getId(),
        ConstraintSet.START,
        ConstraintSet.PARENT_ID,
        ConstraintSet.START);
    constraintSet.connect(
        uiState.getEtaCard().getId(),
        ConstraintSet.BOTTOM,
        ConstraintSet.PARENT_ID,
        ConstraintSet.BOTTOM);
  }

  private static void constrainLogoToTopOfEtaCard(
      ActiveGuidanceUiState uiState, ConstraintSet constraintSet) {
    constraintSet.connect(
        uiState.getGoogleLogo().getId(),
        ConstraintSet.BOTTOM,
        uiState.getEtaCard().getId(),
        ConstraintSet.TOP);
  }

  private void constrainButtonsToBottomEnd(ConstraintSet constraintSet) {
    constraintSet.clear(buttonsContainerId, ConstraintSet.BOTTOM);
    constraintSet.clear(buttonsContainerId, ConstraintSet.TOP);
    constraintSet.connect(
        buttonsContainerId, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP);
    constraintSet.connect(
        buttonsContainerId, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM);
    constraintSet.connect(
        buttonsContainerId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END);
  }

  private void constrainButtonsToTopOfEtaCard(
      Context context, ActiveGuidanceUiState uiState, ConstraintSet constraintSet) {
    constraintSet.clear(buttonsContainerId, ConstraintSet.BOTTOM);
    constraintSet.clear(buttonsContainerId, ConstraintSet.TOP);
    constraintSet.connect(
        buttonsContainerId, ConstraintSet.BOTTOM, uiState.getEtaCard().getId(), ConstraintSet.TOP);
    constraintSet.connect(
        buttonsContainerId, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP);
    constraintSet.connect(
        buttonsContainerId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END);
    constraintSet.setMargin(
        buttonsContainerId,
        ConstraintSet.TOP,
        dpToPx(StyleValues.headerNominalHeightDp(), context));
  }

  @Override
  public void onShowPrompt(NavigationUiParent navigationUiParent, View newPrompt) {
    Context context = navigationUiParent.getViewContext();

    layout.addView(newPrompt);

    // When a prompt is displayed at the bottom of the screen, we update our active constraints so
    // that the invisible Viewport sits entirely above the prompt. This automatically forces the
    // Nav SDK camera to adjust its zoom and framing so that the route chevron is always visible to
    // the driver.
    activeGuidanceWithPromptConstraintSet =
        buildActiveGuidanceWithPromptConstraintSet(context, newPrompt);
    activeGuidanceWithPromptConstraintSet.applyTo(layout);
  }

  @Override
  public void onChangePrompt(
      NavigationUiParent navigationUiParent, View oldPrompt, View newPrompt) {
    Context context = navigationUiParent.getViewContext();

    activeGuidanceWithPromptConstraintSet.clear(oldPrompt.getId());

    layout.removeView(oldPrompt);
    layout.addView(newPrompt);

    // When a prompt is displayed at the bottom of the screen, we update our active constraints so
    // that the invisible Viewport sits entirely above the prompt. This automatically forces the
    // Nav SDK camera to adjust its zoom and framing so that the route chevron is always visible to
    // the driver.
    activeGuidanceWithPromptConstraintSet =
        buildActiveGuidanceWithPromptConstraintSet(context, newPrompt);
    activeGuidanceWithPromptConstraintSet.applyTo(layout);
  }

  @Override
  public void onHidePrompt(NavigationUiParent navigationUiParent, View oldPrompt) {
    activeGuidanceWithPromptConstraintSet.clear(oldPrompt.getId());

    layout.removeView(oldPrompt);

    activeGuidanceConstraintSet.applyTo(layout);
  }

  private ConstraintSet buildActiveGuidanceWithPromptConstraintSet(Context context, View prompt) {
    ConstraintSet constraintSet = new ConstraintSet();
    constraintSet.clone(layout);

    // Constrain viewport to top of prompt
    constraintSet.clear(activeGuidanceUiState.getViewport().getId());
    constraintSet.connect(
        activeGuidanceUiState.getViewport().getId(),
        ConstraintSet.START,
        ConstraintSet.PARENT_ID,
        ConstraintSet.START);
    constraintSet.connect(
        activeGuidanceUiState.getViewport().getId(),
        ConstraintSet.TOP,
        ConstraintSet.PARENT_ID,
        ConstraintSet.TOP);
    constraintSet.setMargin(
        activeGuidanceUiState.getViewport().getId(),
        ConstraintSet.TOP,
        dpToPx(StyleValues.headerNominalHeightDp(), context));
    constraintSet.connect(
        activeGuidanceUiState.getViewport().getId(),
        ConstraintSet.END,
        ConstraintSet.PARENT_ID,
        ConstraintSet.END);
    constraintSet.connect(
        activeGuidanceUiState.getViewport().getId(),
        ConstraintSet.BOTTOM,
        prompt.getId(),
        ConstraintSet.TOP);

    // Constrain prompt to bottom start corner
    constraintSet.connect(
        prompt.getId(), ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START);
    constraintSet.connect(
        prompt.getId(), ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM);

    return constraintSet;
  }

  private void removeFromParentView(View view) {
    if (view != null && view.getParent() != null) {
      ((ViewGroup) view.getParent()).removeView(view);
    }
  }

  private static int dpToPx(int dp, Context context) {
    return (int)
        TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
  }
}

    

برای مشاهده کد نمونه کامل یا کاوش پیاده‌سازی‌های بیشتر نماینده چیدمان، نمونه‌های نماینده چیدمان در GitHub را ببینید. همچنین می‌توانید فایل‌های منبع جداگانه را برای این مثال در GitHub مشاهده کنید: