
ใช้การปรับแต่งเลย์เอาต์เพื่อควบคุมลำดับชั้นการแสดงผลและการจัดวางบนหน้าจอสำหรับทั้งมุมมองที่ Google จัดหาให้และมุมมองธุรกิจที่กำหนดเอง
แทนที่จะเพิ่มมุมมองที่กำหนดเองลงในช่องส่วนหัวหรือส่วนท้ายที่กำหนดไว้ล่วงหน้า ให้ใช้ผู้รับมอบสิทธิ์เลย์เอาต์เพื่อจัดการเลย์เอาต์สำหรับทั้งหน้าจอ ในระหว่างการเปลี่ยนสถานะการนำทาง ผู้รับมอบสิทธิ์เลย์เอาต์จะได้รับคอมโพเนนต์ที่ Google จัดหาให้ เช่น การ์ดเลี้ยว การ์ด ETA และปุ่มต่างๆ จัดวางองค์ประกอบเหล่านี้ไว้ข้างเนื้อหาที่กำหนดเองโดยใช้ระบบเลย์เอาต์ Android มาตรฐาน เช่น ConstraintLayout, CoordinatorLayout หรือ LinearLayout
ใช้เฟรมเวิร์กนี้เพื่อจัดวางข้อมูลทางธุรกิจที่กำหนดเอง เช่น สถานะการสั่งซื้อ หรือวิธีการรับสินค้า บนหน้าจออย่างแม่นยำ พร้อมทั้งป้องกันไม่ให้มุมมองซ้อนทับกัน
วิธีการทำงานของการปรับแต่งเลย์เอาต์
การปรับแต่งเลย์เอาต์ใช้รูปแบบการออกแบบการมอบหมาย SDK จะส่งคอมโพเนนต์ UI ไปยังคลาสที่กำหนดเองที่คุณเขียนขึ้นโดยตรง ซึ่งก็คือผู้รับมอบสิทธิ์เลย์เอาต์ แทนที่จะวาดหรือจัดวางคอมโพเนนต์ UI บนหน้าจอโดยอัตโนมัติ
หากต้องการปรับแต่งเลย์เอาต์ ให้สร้างคลาสที่ขยายคลาสNavigationLayoutDelegate แบบแอบสแตรกต์ และกำหนดอินสแตนซ์ให้กับ NavigationView หรือ SupportNavigationFragment เมื่อใดก็ตามที่สถานะการนำทางเปลี่ยนไป เช่น จากแผนที่ฐานไปเป็นการนำทางแบบเลี้ยวต่อเลี้ยวที่ใช้งานอยู่ SDK จะเรียกใช้เมธอดเรียกกลับในผู้รับมอบสิทธิ์และแสดงคอมโพเนนต์ UI ที่พร้อมใช้งานสำหรับสถานะนั้นๆ
เฟรมเวิร์กนี้กำหนดการแยกความรับผิดชอบที่ชัดเจนระหว่างแอปกับ SDK เพื่อมอบประสบการณ์การใช้งานที่ผสานรวมและราบรื่น ใช้ผู้รับมอบสิทธิ์เลย์เอาต์เพื่อปรับแต่งสิ่งต่อไปนี้
- สร้างลำดับชั้นของมุมมอง: เลือกคอมโพเนนต์ของ Google และมุมมองธุรกิจที่กำหนดเองที่จะเพิ่มลงในหน้าจอสำหรับสถานะ UI แต่ละสถานะ
- จัดวางทุกองค์ประกอบ: ตั้งค่าจุดยึด ขอบ และการจัดวางเลย์เอาต์บนหน้าจออย่างแม่นยำ หลีกเลี่ยงการใช้ข้อจำกัดความกว้างหรือความสูงที่กำหนดเองกับคอมโพเนนต์ของ Google เนื่องจากคอมโพเนนต์ของ Google จะคำนวณขนาดภายในของตัวเอง
- จัดเฟรมแผนที่ฐาน: กำหนดขอบเขตที่มองเห็นได้ของกล้องแผนที่โดยใช้พิกัดของคอมโพเนนต์วิวพอร์ต
- จัดเลเยอร์หน้าจอ: เลือกว่ามุมมองที่กำหนดเองจะลอยอยู่เหนือ ใต้ หรือข้างตัวควบคุมในตัวของ Google
ในขณะเดียวกัน คุณไม่สามารถปรับแต่งคอมโพเนนต์ของ Google ต่อไปนี้ด้วยผู้รับมอบสิทธิ์เลย์เอาต์
- ขนาดคอมโพเนนต์: ขนาดและขนาดภายในของคอมโพเนนต์ที่ Google จัดหาให้ ซึ่ง SDK จะคำนวณโดยอัตโนมัติ
- เงื่อนไขการทริกเกอร์: เมื่อการแจ้งเตือนหรือพรอมต์แบบไดนามิกปรากฏขึ้นโดยอิงตามข้อมูลเส้นทางแบบเรียลไทม์
หลักการใช้งาน
เมื่อเขียนผู้รับมอบสิทธิ์เลย์เอาต์ โปรดคำนึงถึงกฎต่อไปนี้เพื่อป้องกันไม่ให้เกิดข้อบกพร่องของเลย์เอาต์หรือแอปหยุดทำงานขณะรันไทม์
- ค่าเริ่มต้นคือหน้าจอว่าง: คอมโพเนนต์ที่ Google จัดหาให้จะปรากฏขึ้นก็ต่อเมื่อผู้รับมอบสิทธิ์เพิ่มคอมโพเนนต์เหล่านั้นลงในลำดับชั้นการแสดงผลและจัดวางคอมโพเนนต์อย่างชัดเจน
- ระบบไม่รองรับ API เดิม: ระบบจะไม่รองรับพร็อพเพอร์ตีและเมธอดที่ควบคุมโมเดลเลย์เอาต์เดิมแบบอิงตามช่อง และอาจทำงานไม่เป็นไปตามที่คาดไว้เมื่อผู้รับมอบสิทธิ์ที่กำหนดเองทำงานอยู่
- อย่าแก้ไขโครงสร้างมุมมองภายใน:
อย่าใช้เมธอด เช่น
findViewById()เพื่อข้ามหรือแก้ไขลำดับชั้นของมุมมองของคอมโพเนนต์ที่ Google จัดหาให้ (เช่น การ์ดเลี้ยวหรือการ์ด 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() |
รายการตรวจสอบสิ่งที่ต้องมี
ทำตามขั้นตอนและข้อกำหนดที่จำเป็นต่อไปนี้เพื่อใช้งานผู้รับมอบสิทธิ์เลย์เอาต์ให้สำเร็จ
-
เริ่มต้นผู้รับมอบสิทธิ์ก่อนสร้าง UI: เรียก
setLayoutDelegate()ก่อนที่ SDK จะเริ่มต้น UI การนำทาง ดูตัวอย่างการใช้งานโค้ดต่อไปนี้เพื่อยืนยันเวลาการตั้งค่าที่แน่นอน สำหรับโครงสร้างแอปพลิเคชัน การเริ่มต้นผู้รับมอบสิทธิ์ หลังจาก การสร้าง UI จะทริกเกอร์ApiIllegalStateExceptionKotlin
// For SupportNavigationFragment override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val navFragment = supportFragmentManager.findFragmentById(R.id.nav_fragment) as SupportNavigationFragment navFragment.setLayoutDelegate(MyLayoutDelegate()) } // For a programmatic NavigationView val navigationView = NavigationView(context) navigationView.setLayoutDelegate(MyLayoutDelegate()) navigationView.onCreate(savedInstanceState)
Java
// For SupportNavigationFragment @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); SupportNavigationFragment navFragment = (SupportNavigationFragment) getSupportFragmentManager().findFragmentById(R.id.nav_fragment); navFragment.setLayoutDelegate(new MyLayoutDelegate()); } // For a programmatic NavigationView NavigationView navigationView = new NavigationView(context); navigationView.setLayoutDelegate(new MyLayoutDelegate()); navigationView.onCreate(savedInstanceState);
-
สร้างคอนเทนเนอร์เลย์เอาต์ของคุณเอง: สร้างกลุ่มมุมมองที่กำหนดเอง เช่น
ConstraintLayoutเพื่อเก็บองค์ประกอบ UI ที่กำหนดเองและมุมมองที่ Google จัดหาให้ -
แนบมุมมองที่จำเป็น: คุณต้องเพิ่มโลโก้ Google (
getGoogleLogo()) และวิวพอร์ต (getViewport()) ลงในคอนเทนเนอร์เลย์เอาต์ ในสถานะการนำทางทั้งหมด หากไม่รวมมุมมองทั้ง 2 รายการนี้ ระบบจะทริกเกอร์ApiIllegalStateExceptionขณะรันไทม์และแอปจะหยุดทำงาน -
ใช้ขนาดคงที่: SDK จะกำหนด ขนาดสำหรับคอมโพเนนต์ที่ Google จัดหาให้หลายรายการ อย่าใช้ข้อจำกัดความกว้าง หรือความสูงที่กำหนดเองกับองค์ประกอบต่อไปนี้
- การ์ดเลี้ยว
- การ์ด ETA
- พรอมต์ที่เข้ามา
- โลโก้ Google
- วิดเจ็ตความเร็ว
-
แนบเลย์เอาต์กับมุมมอง: เรียก
navigationView.setNavigationLayout()ภายใน CallbackonEnterNavigationReady()และonEnterActiveGuidance()เพื่อแนบคอนเทนเนอร์กับ ลำดับชั้นการแสดงผลของมุมมองแผนที่ -
ล้างข้อมูลเมื่อออกจากสถานะ: เรียก
navigationView.removeNavigationLayout()และนำมุมมองที่กำหนดเอง ออกภายในการเรียกกลับonLeaveเพื่อป้องกันไม่ให้เกิดการรั่วไหลของหน่วยความจำและ สถานะ UI ซ้อนทับกัน -
หลีกเลี่ยง API เลย์เอาต์เดิม: อย่าเรียก API ที่เลิกใช้งานแล้ว แบบอิงตามช่อง เช่น
setCustomControl()หรือsetHeaderEnabled()ขณะที่ผู้รับมอบสิทธิ์ที่กำหนดเองทำงานอยู่ SDK อาจไม่สนใจการเรียกเดิมเหล่านี้เมื่อแนบผู้รับมอบสิทธิ์ที่กำหนดเอง
สถานะ UI และคอมโพเนนต์ของ Google
เมื่อมีการเปลี่ยนสถานะการนำทาง SDK จะส่งออบเจ็กต์ UiState แบบอ่านอย่างเดียวไปยังการเรียกกลับของผู้รับมอบสิทธิ์ ออบเจ็กต์นี้จะรวมแฟล็กการกำหนดค่าเลย์เอาต์ปัจจุบันกับคอมโพเนนต์ของ Google (เช่น อินสแตนซ์ View) ที่คุณต้องใช้ในการแสดงผลหน้าจอ
ผู้รับมอบสิทธิ์เลย์เอาต์จะจัดการลำดับชั้นการแสดงผลใน 4 สถานะการทำงาน แผนภาพสถานะต่อไปนี้แสดงวิธีที่ SDK เปลี่ยนสถานะการนำทางและเมธอดเรียกกลับที่ SDK เรียกใช้ในผู้รับมอบสิทธิ์
คอมโพเนนต์ของ Google ที่เฉพาะเจาะจงซึ่งส่งไปยังผู้รับมอบสิทธิ์จะขึ้นอยู่กับระยะปัจจุบันของวงจรการนำทาง
คอมโพเนนต์ที่จำเป็น (สถานะการนำทางทั้งหมด)
ไม่ว่าระยะการนำทางปัจจุบันจะเป็นระยะใด คุณต้องรวม จัดวาง และแสดงคอมโพเนนต์ของ Google ต่อไปนี้ในลำดับชั้นการแสดงผล
โลโก้ Google (
getGoogleLogo()): คอมโพเนนต์นี้จะแสดงโลโก้ Google Maps ที่จำเป็น หากเปิดใช้ปุ่มจัดกึ่งกลางใหม่ โลโก้จะเปลี่ยนเป็นปุ่มนี้โดยอัตโนมัติเมื่อผู้ขับเลื่อนแผนที่ออกจากรถ ดังนั้น การจัดวางโลโก้จะเป็นตัวกำหนดตำแหน่งที่ปุ่มจัดกึ่งกลางใหม่ จะปรากฏขึ้น เราขอแนะนำให้วางโลโก้ไว้ที่มุมซ้ายล่างของเลย์เอาต์เพื่อให้ตรงกับความคาดหวังของ UI แผนที่มาตรฐานวิวพอร์ต (
getViewport()):Viewที่ กำหนดขอบเขตการจัดเฟรมของกล้อง การจัดวางวิวพอร์ตจะเป็นตัวกำหนดตำแหน่งที่แน่นอนที่ SDK จะจัดกึ่งกลางเชฟรอนของยานพาหนะและวาดเส้นทางที่ใช้งานอยู่ จัดวางวิวพอร์ตให้ครอบคลุมพื้นที่เปิดที่มองเห็นได้ของหน้าจอ โดยหลีกเลี่ยงการซ้อนทับแบบทึบแสง เช่น ชีตด้านล่างที่กำหนดเอง
คอมโพเนนต์พร้อมนำทาง
ในสถานะพร้อมนำทาง UI จะยังคงเรียบง่ายเพื่อเน้นที่แผนที่ฐาน ออบเจ็กต์ NavigationReadyUiState ช่วยให้เข้าถึงสิ่งต่อไปนี้ได้
- getNavigationReadyButtons(): รายการมุมมองการทำงานลอยที่กำหนดค่าไว้สำหรับแผนที่ฐาน (โดยปกติจะเป็นปุ่มเข็มทิศเท่านั้น) เนื่องจาก Google อาจเพิ่มหรือจัดลำดับปุ่มใหม่ใน SDK รุ่นต่อๆ ไป จึงควรหลีกเลี่ยงการใช้ดัชนีรายการคงที่ ให้วนซ้ำในรายการ อ่านประเภทของแต่ละปุ่มโดยเปรียบเทียบ
getType()กับButtonKnownType.COMPASSและแยกมุมมองจริงโดยเรียกgetView()โปรดทราบว่าปุ่มที่แน่นอนในรายการนี้จะแตกต่างจากปุ่มที่พร้อมใช้งานระหว่างการนำทางแบบเลี้ยวต่อเลี้ยวที่ใช้งานอยู่
คอมโพเนนต์การนำทางแบบเลี้ยวต่อเลี้ยวที่ใช้งานอยู่
เมื่อการนำทางแบบเลี้ยวต่อเลี้ยวเริ่มต้นขึ้น SDK จะปลดล็อกชุดควบคุมการนำทางทั้งหมด ออบเจ็กต์ ActiveGuidanceUiState ช่วยให้เข้าถึงคอมโพเนนต์ของ Google ต่อไปนี้ได้
getTurnCard(): แบนเนอร์ส่วนหัวหลักที่แสดงทิศทางการเลี้ยวที่จะเกิดขึ้น การวัดระยะทาง และคำแนะนำช่องทาง วางไว้ที่ด้านบนของเลย์เอาต์เพื่อสร้างลำดับชั้นการนำทางที่คุ้นเคยและยึดมุมมองที่กำหนดเองไว้รอบๆgetEtaCard(): แบนเนอร์ส่วนท้ายที่แสดงเวลาถึงโดยประมาณ เวลาเดินทางที่เหลือ และระยะทางที่เหลือไปยังจุดหมาย วางไว้ตามขอบด้านล่างของหน้าจอ หรือผสานรวมพิกัดกับการ์ดการจัดการงานที่กำหนดเองgetTripProgressBar(): แถบความคืบหน้าแนวตั้งที่แสดงให้ผู้ขับเห็นว่าเดินทางไปไกลแค่ไหนแล้วตามเส้นทางปัจจุบัน คุณมีอิสระอย่างเต็มที่ในการยึดแถบความคืบหน้านี้ไว้ที่ใดก็ได้ เช่น ตามขอบด้านข้างของคอนเทนเนอร์ที่กำหนดเอง ซึ่งแตกต่างจากเลย์เอาต์เดิมที่ยึดแถบความคืบหน้านี้ไว้ที่ขอบแผนที่อย่างเคร่งครัดgetSpeedWidget(): ตัวควบคุมลอยสำหรับมาตรวัดความเร็วและขีดจำกัดความเร็วที่กำหนด มุมมองนี้จะปรับขนาดแบบไดนามิกระหว่างสถานะภาพ 4 สถานะขณะรันไทม์ (ไม่แสดงอะไรเลย แสดงเฉพาะความเร็วปัจจุบัน แสดงเฉพาะขีดจำกัดความเร็ว หรือแสดงทั้ง 2 ค่า) ทั้งนี้ขึ้นอยู่กับการตั้งค่า API และความพร้อมใช้งานของข้อมูล เนื่องจากวิดเจ็ตอาจเปลี่ยนขนาดเหล่านี้โดยไม่มีการแจ้งให้ทราบ ให้ยึดมุมมองโดยรอบไว้เสมอโดยใช้ข้อจำกัดเพื่อให้เลย์เอาต์ปรับเปลี่ยนโดยอัตโนมัติเพื่อป้องกันการซ้อนทับเชิงพื้นที่
getActiveGuidanceButtons(): รายการมุมมองการทำงานลอยแบบขยายสำหรับสถานะการนำทางแบบเลี้ยวต่อเลี้ยวที่ใช้งานอยู่ (โดยปกติจะรวมทั้งปุ่มเข็มทิศและปุ่มรายงานเหตุการณ์) เช่นเดียวกับในสถานะพร้อมนำทาง คุณสามารถค้นหาและแยกมุมมองปุ่มแต่ละรายการได้โดยกรองรายการตามButtonKnownType(COMPASSหรือREPORTING) และแยกมุมมองโดยใช้getView()จากนั้นคุณจะจัดวางปุ่มเหล่านั้นแยกกันหรือใช้เลย์เอาต์ เช่นAutoHidingLinearLayoutเพื่อซ้อนอาร์เรย์อย่างปลอดภัยโดยไม่มีความขัดแย้งเชิงพื้นที่
คอมโพเนนต์พรอมต์แบบไดนามิก
พรอมต์ เช่น การแจ้งเตือนเหตุการณ์หรือคำเตือนกล้องตรวจจับความเร็ว จะทริกเกอร์แยกกันระหว่างการนำทางแบบเลี้ยวต่อเลี้ยวที่ใช้งานอยู่
เมื่อพรอมต์พร้อมที่จะแสดง SDK จะเรียกใช้การเรียกกลับ onShowPrompt() ของผู้รับมอบสิทธิ์และส่งมุมมอง newPrompt ผู้รับมอบสิทธิ์มีหน้าที่รับผิดชอบในการจัดวางพรอมต์นี้เหนือเลย์เอาต์อย่างราบรื่น (โดยปกติจะยึดไว้ที่ขอบด้านล่างของคอนเทนเนอร์แผนที่)
เนื่องจากพรอมต์ที่เข้ามาจะครอบคลุมส่วนล่างของหน้าจอ คุณจึงต้องอัปเดตเลย์เอาต์เพื่อป้องกันไม่ให้พรอมต์ซ้อนทับวิวพอร์ต โลโก้ Google หรือปุ่มที่จัดแนวไว้ด้านล่าง
การจัดการขนาดหน้าจอและโหมดกว้าง
ผู้รับมอบสิทธิ์เลย์เอาต์ใช้ฟีเจอร์ต่อไปนี้เพื่อจัดการการเปลี่ยนแปลงขนาดแผนที่และการวางแนวหน้าจอ
การปรับขนาดมุมมอง: ปรับเลย์เอาต์ทุกครั้งที่ขนาดจริงของแผนที่เปลี่ยนแปลง
โหมดกว้าง: สลับไปใช้เลย์เอาต์รูปแบบกว้างเมื่อแผนที่กว้างพอ
การตอบสนองต่อการปรับขนาดมุมมอง
เมื่อใดก็ตามที่ขนาดจริงของคอนเทนเนอร์แผนที่เปลี่ยนแปลง SDK จะเรียกใช้การเรียกกลับonSizeChanged() เลย์เอาต์แบบแยกหน้าจอ แถบเลื่อนเลย์เอาต์ และการหมุนอุปกรณ์มักจะทริกเกอร์การเรียกกลับ onSizeChanged() ใช้การเรียกกลับนี้เพื่อทำการปรับเปลี่ยนทั่วไปที่ปรับเปลี่ยนตามอุปกรณ์กับ UI ที่กำหนดเอง ใช้ onSizeChanged() เพื่อจัดตำแหน่งองค์ประกอบที่กำหนดเองใหม่สำหรับสัดส่วนภาพหน้าจอใหม่ ใช้จุดพักความกว้างหรือความสูงที่กำหนดเอง และตรวจหาเมื่อเหตุการณ์การปรับขนาดสลับโหมดกว้างเพื่อสลับรูปแบบเลย์เอาต์อย่างปลอดภัย
ทำความเข้าใจโหมดกว้าง
โหมดกว้างจะเปิดใช้งานเมื่อคอนเทนเนอร์แผนที่กว้างพอที่จะแสดงคอมโพเนนต์ UI เคียงข้างกัน
อ่านบูลีน isWideMode() จากออบเจ็กต์สถานะเพื่อจัดตำแหน่งองค์ประกอบ UI ที่กำหนดเองใหม่และทำให้ตรงกลางแผนที่ว่างไว้สำหรับผู้ขับ เนื่องจากคอมโพเนนต์ที่ Google จัดหาให้ เช่น การ์ดเลี้ยวและการ์ด ETA จะหดและปรับรูปร่างโดยอัตโนมัติในโหมดกว้าง การอ่านบูลีนนี้จะช่วยให้เลย์เอาต์ปรับเปลี่ยนในเวลาเดียวกับที่คอมโพเนนต์ของ Google อัปเดต
ดูตัวอย่างต่อไปนี้สำหรับการจัดตำแหน่งคอมโพเนนต์เลย์เอาต์ในโหมดมาตรฐานและโหมดกว้าง
โหมดแนวตั้งมาตรฐาน: วางการ์ดเลี้ยวไว้ที่ด้านบนของหน้าจอและการ์ด ETA ไว้ที่ด้านล่าง
โหมดกว้าง: ย้ายการ์ดเลี้ยวไปที่ด้านเริ่มต้นของหน้าจอและการ์ด ETA ไปที่ด้านสิ้นสุด
การตรวจสอบโหมดกว้างระหว่างการเปลี่ยนสถานะ
หากเลย์เอาต์รองรับรูปแบบโหมดกว้าง ให้ประเมินบูลีน isWideMode() ภายในทุกการเรียกกลับการเปลี่ยนสถานะ แทนที่จะใช้ onSizeChanged() เพียงอย่างเดียว
onEnterNavigationReady() และ onEnterActiveGuidance() เพื่อจัดการการเปิดแอปครั้งแรกในโหมดแนวนอน แนวทางนี้จะปกป้องเลย์เอาต์จากเหตุการณ์วงจรการทำงานของระบบ Android เช่น การสร้าง Activity ใหม่เริ่มต้น ซึ่งข้ามการเรียกกลับการปรับขนาดไปทั้งหมด เพื่อให้มั่นใจว่ารูปแบบเลย์เอาต์มาตรฐานหรือกว้างที่ถูกต้องจะเปิดใช้งานทันทีที่สถานะการนำทางใหม่เริ่มต้นขึ้น
การจัดแนวให้ตรงกับการจัดรูปแบบของ Google
SDK มีคลาสยูทิลิตี StyleValues เพื่อช่วยให้ UI ที่กำหนดเองของคุณตรงกับจังหวะภาพของ Google คุณสามารถอ่านค่าเหล่านี้ในหน่วยความหนาแน่นของพิกเซลอิสระ (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. */ @file:Suppress("PackageName") package com.example.navigationapidemo.layoutdelegate import android.content.Context import android.util.TypedValue import android.view.View import android.view.ViewGroup import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import com.google.android.libraries.navigation.layoutcustomization.ActiveGuidanceUiState import com.google.android.libraries.navigation.layoutcustomization.AutoHidingVerticalLayout import com.google.android.libraries.navigation.layoutcustomization.NavigationLayoutDelegate import com.google.android.libraries.navigation.layoutcustomization.NavigationReadyUiState import com.google.android.libraries.navigation.layoutcustomization.NavigationUiButton.ButtonKnownType.COMPASS import com.google.android.libraries.navigation.layoutcustomization.NavigationUiParent import com.google.android.libraries.navigation.layoutcustomization.StyleValues.headerNominalHeightDp /** Kotlin equivalent of StandardUiElementsLayoutDelegate. */ class StandardUiElementsLayoutDelegateKt : NavigationLayoutDelegate() { private val layoutId = View.generateViewId() private val buttonsContainerId = View.generateViewId() private var layout: ConstraintLayout? = null private var buttonsContainer: AutoHidingVerticalLayout? = null private var navigationReadyConstraintSet: ConstraintSet? = null private var activeGuidanceConstraintSet: ConstraintSet? = null private var activeGuidanceWithPromptConstraintSet: ConstraintSet? = null private var activeGuidanceUiState: ActiveGuidanceUiState? = null override fun onEnterNavigationReady( navigationUiParent: NavigationUiParent, newState: NavigationReadyUiState, ) { val context = navigationUiParent.viewContext var currentLayout = layout if (currentLayout == null) { currentLayout = ConstraintLayout(context).apply { layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, ) id = layoutId } layout = currentLayout } removeFromParentView(newState.viewport) currentLayout.addView( newState.viewport, ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, ), ) removeFromParentView(newState.googleLogo) currentLayout.addView( newState.googleLogo, ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, ), ) var currentButtonsContainer = buttonsContainer if (currentButtonsContainer == null) { currentButtonsContainer = AutoHidingVerticalLayout(context).apply { id = buttonsContainerId } buttonsContainer = currentButtonsContainer } removeFromParentView(currentButtonsContainer) currentLayout.addView( currentButtonsContainer, ViewGroup.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ConstraintLayout.LayoutParams.MATCH_CONSTRAINT, ), ) for (button in newState.navigationReadyButtons) { removeFromParentView(button.view) currentButtonsContainer.addView(button.view) } if (navigationReadyConstraintSet == null) { navigationReadyConstraintSet = buildNavigationReadyConstraintSet(newState) } navigationReadyConstraintSet?.applyTo(currentLayout) navigationUiParent.removeNavigationLayout(currentLayout) navigationUiParent.setNavigationLayout(currentLayout) } private fun buildNavigationReadyConstraintSet(uiState: NavigationReadyUiState): ConstraintSet { return ConstraintSet().apply { clone(layout) connect( uiState.viewport.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, ) connect(uiState.viewport.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP) connect(uiState.viewport.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) connect( uiState.viewport.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM, ) connect( uiState.googleLogo.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM, ) connect( uiState.googleLogo.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, ) constrainButtonsToBottomEnd() } } override fun onLeaveNavigationReady( navigationUiParent: NavigationUiParent, oldState: NavigationReadyUiState, ) { buttonsContainer?.removeAllViews() layout?.removeAllViews() layout?.let { navigationUiParent.removeNavigationLayout(it) } } override fun onEnterActiveGuidance( navigationUiParent: NavigationUiParent, oldState: NavigationReadyUiState, newState: ActiveGuidanceUiState, ) { activeGuidanceUiState = newState val context = navigationUiParent.viewContext val currentLayout = checkNotNull(layout) { "layout must be initialized" } val currentButtonsContainer = checkNotNull(buttonsContainer) { "buttonsContainer must be initialized" } removeFromParentView(newState.turnCard) currentLayout.addView( newState.turnCard, ViewGroup.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, ), ) removeFromParentView(newState.etaCard) currentLayout.addView( newState.etaCard, ViewGroup.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, ), ) for (button in oldState.navigationReadyButtons) { removeFromParentView(button.view) } for (button in newState.activeGuidanceButtons) { val buttonLayoutParams = AutoHidingVerticalLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, ) if (button.type == COMPASS) { buttonLayoutParams.isHighPriority = true } removeFromParentView(button.view) currentButtonsContainer.addView(button.view, buttonLayoutParams) } if (activeGuidanceConstraintSet == null) { activeGuidanceConstraintSet = buildActiveGuidanceConstraintSet(context, newState) } activeGuidanceConstraintSet?.applyTo(currentLayout) } override fun onLeaveActiveGuidance( navigationUiParent: NavigationUiParent, oldState: ActiveGuidanceUiState, newState: NavigationReadyUiState, ) { removeFromParentView(oldState.etaCard) removeFromParentView(oldState.turnCard) buttonsContainer?.removeAllViews() val currentButtonsContainer = checkNotNull(buttonsContainer) { "buttonsContainer must be initialized" } for (button in newState.navigationReadyButtons) { currentButtonsContainer.addView( button.view, ViewGroup.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, ), ) } navigationReadyConstraintSet?.applyTo(layout) } private fun buildActiveGuidanceConstraintSet( context: Context, uiState: ActiveGuidanceUiState, ): ConstraintSet { return ConstraintSet().apply { clone(layout) connect(uiState.turnCard.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP) connect( uiState.turnCard.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, ) clear(uiState.viewport.id) connect( uiState.viewport.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, ) connect(uiState.viewport.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP) setMargin(uiState.viewport.id, ConstraintSet.TOP, dpToPx(headerNominalHeightDp(), context)) connect(uiState.viewport.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) connect(uiState.viewport.id, ConstraintSet.BOTTOM, uiState.etaCard.id, ConstraintSet.TOP) clear(uiState.googleLogo.id, ConstraintSet.BOTTOM) constrainLogoToTopOfEtaCard(uiState) constrainEtaCardToBottomStart(uiState) constrainButtonsToTopOfEtaCard(context, uiState) } } private fun ConstraintSet.constrainEtaCardToBottomStart(uiState: ActiveGuidanceUiState) { connect(uiState.etaCard.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START) connect(uiState.etaCard.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM) } private fun ConstraintSet.constrainLogoToTopOfEtaCard(uiState: ActiveGuidanceUiState) { connect(uiState.googleLogo.id, ConstraintSet.BOTTOM, uiState.etaCard.id, ConstraintSet.TOP) } private fun ConstraintSet.constrainButtonsToBottomEnd() { clear(buttonsContainerId, ConstraintSet.BOTTOM) clear(buttonsContainerId, ConstraintSet.TOP) connect(buttonsContainerId, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP) connect(buttonsContainerId, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM) connect(buttonsContainerId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) } private fun ConstraintSet.constrainButtonsToTopOfEtaCard( context: Context, uiState: ActiveGuidanceUiState, ) { clear(buttonsContainerId, ConstraintSet.BOTTOM) clear(buttonsContainerId, ConstraintSet.TOP) connect(buttonsContainerId, ConstraintSet.BOTTOM, uiState.etaCard.id, ConstraintSet.TOP) connect(buttonsContainerId, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP) connect(buttonsContainerId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) setMargin(buttonsContainerId, ConstraintSet.TOP, dpToPx(headerNominalHeightDp(), context)) } override fun onShowPrompt(navigationUiParent: NavigationUiParent, newPrompt: View) { val context = navigationUiParent.viewContext layout?.addView(newPrompt) if (activeGuidanceUiState != null) { activeGuidanceWithPromptConstraintSet = buildActiveGuidanceWithPromptConstraintSet(context, newPrompt) activeGuidanceWithPromptConstraintSet?.applyTo(layout) } } override fun onChangePrompt( navigationUiParent: NavigationUiParent, oldPrompt: View, newPrompt: View, ) { val context = navigationUiParent.viewContext activeGuidanceWithPromptConstraintSet?.clear(oldPrompt.id) val currentLayout = checkNotNull(layout) { "layout must be initialized" } currentLayout.removeView(oldPrompt) currentLayout.addView(newPrompt) if (activeGuidanceUiState != null) { activeGuidanceWithPromptConstraintSet = buildActiveGuidanceWithPromptConstraintSet(context, newPrompt) activeGuidanceWithPromptConstraintSet?.applyTo(currentLayout) } } override fun onHidePrompt(navigationUiParent: NavigationUiParent, oldPrompt: View) { activeGuidanceWithPromptConstraintSet?.clear(oldPrompt.id) layout?.removeView(oldPrompt) activeGuidanceConstraintSet?.applyTo(layout) } private fun buildActiveGuidanceWithPromptConstraintSet( context: Context, prompt: View, ): ConstraintSet { return ConstraintSet().apply { clone(layout) val state = checkNotNull(activeGuidanceUiState) { "activeGuidanceUiState must be initialized" } clear(state.viewport.id) connect(state.viewport.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START) connect(state.viewport.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP) setMargin(state.viewport.id, ConstraintSet.TOP, dpToPx(headerNominalHeightDp(), context)) connect(state.viewport.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) connect(state.viewport.id, ConstraintSet.BOTTOM, prompt.id, ConstraintSet.TOP) clear(state.googleLogo.id, ConstraintSet.BOTTOM) connect(state.googleLogo.id, ConstraintSet.BOTTOM, prompt.id, ConstraintSet.TOP) connect(prompt.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START) connect(prompt.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM) } } private fun removeFromParentView(view: View?) { if (view?.parent != null) { (view.parent as ViewGroup).removeView(view) } } private fun dpToPx(dp: Int, context: Context): Int { return TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), context.resources.displayMetrics, ) .toInt() } }
Java
/* * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.navigationapidemo.layoutdelegate; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.MATCH_CONSTRAINT; import static com.google.android.libraries.navigation.layoutcustomization.NavigationUiButton.ButtonKnownType.COMPASS; import android.content.Context; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintSet; import com.google.android.libraries.navigation.layoutcustomization.ActiveGuidanceUiState; import com.google.android.libraries.navigation.layoutcustomization.AutoHidingVerticalLayout; import com.google.android.libraries.navigation.layoutcustomization.NavigationLayoutDelegate; import com.google.android.libraries.navigation.layoutcustomization.NavigationReadyUiState; import com.google.android.libraries.navigation.layoutcustomization.NavigationUiButton; import com.google.android.libraries.navigation.layoutcustomization.NavigationUiParent; import com.google.android.libraries.navigation.layoutcustomization.StyleValues; /** * A sample implementation of {@link NavigationLayoutDelegate} demonstrating a basic, * portrait-optimized layout using {@link ConstraintLayout}. * * <p><b>Understanding the Layout Delegate State Machine:</b> Navigation SDK transitions through * distinct states, each calling corresponding lifecycle methods on this delegate: * * <ul> * <li><b>Navigation Ready:</b> Initiated by {@link #onEnterNavigationReady}. We initialize the * layout here and add non-guidance views, then pass it to {@link NavigationUiParent} as the * navigation layout. * <li><b>Active Guidance (Turn-by-Turn Mode):</b> Initiated by {@link #onEnterActiveGuidance}. We * set up the layout for Active Guidance, adding elements such as the turn card and ETA card. * <li><b>Prompts:</b> Prompts (e.g., incident alerts) may be triggered during Active Guidance * mode and can be added to the layout via {@link #onShowPrompt}. * </ul> * * This class caches its {@link ConstraintSet}s to ensure smooth transitions without needing to * recreate or inflate layouts continuously. */ public class StandardUiElementsLayoutDelegate extends NavigationLayoutDelegate { private final int layoutId; private final int buttonsContainerId; private ConstraintLayout layout; private AutoHidingVerticalLayout buttonsContainer; // We cache our ConstraintSet definitions to avoid cloning or rebuilding // constraint configurations programmatically on every transition. This optimization // keeps UI state switches (such as entering active guidance or popping up prompts) highly // performant. private ConstraintSet navigationReadyConstraintSet; private ConstraintSet activeGuidanceConstraintSet; private ConstraintSet activeGuidanceWithPromptConstraintSet; private ActiveGuidanceUiState activeGuidanceUiState; public StandardUiElementsLayoutDelegate() { layoutId = View.generateViewId(); buttonsContainerId = View.generateViewId(); } @Override public void onEnterNavigationReady( NavigationUiParent navigationUiParent, NavigationReadyUiState newState) { Context context = navigationUiParent.getViewContext(); // Implementation Tip: For simplicity, this sample instantiates views and constraints // programmatically. In a production application, you can safely inflate standard XML // layout templates to build your layout hierarchies and define base UI constraints. // Create the root layout if (layout == null) { layout = new ConstraintLayout(context); LayoutParams layoutParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT); layout.setLayoutParams(layoutParams); layout.setId(layoutId); } // Add the Viewport (REQUIRED): // The viewport is an invisible bounding box used by Nav SDK to frame the vehicle // chevron and the upcoming route line. We want to position this view such that it avoids // being obscured by fully-opaque UI elements (like the turn card or the ETA card). removeFromParentView(newState.getViewport()); LayoutParams viewportLayoutParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT); layout.addView(newState.getViewport(), viewportLayoutParams); // Add the Google Logo / Re-center Button (REQUIRED): // This view displays the Google logo during guidance and may transition into a // "Re-center" button if the user scrolls away from the vehicle chevron. It must // be added to the view hierarchy in all states. removeFromParentView(newState.getGoogleLogo()); LayoutParams googleLogoLayoutParams = new LayoutParams(MATCH_PARENT, WRAP_CONTENT); layout.addView(newState.getGoogleLogo(), googleLogoLayoutParams); // Add the container for UI buttons if (buttonsContainer == null) { // We use AutoHidingVerticalLayout to create an adaptive vertical button container that // automatically hides or shows child views based on available screen height. buttonsContainer = new AutoHidingVerticalLayout(context); buttonsContainer.setId(buttonsContainerId); } removeFromParentView(buttonsContainer); LayoutParams buttonsContainerLayoutParams = new LayoutParams(WRAP_CONTENT, MATCH_CONSTRAINT); layout.addView(buttonsContainer, buttonsContainerLayoutParams); // Add UI buttons to the container for (NavigationUiButton button : newState.getNavigationReadyButtons()) { removeFromParentView(button.getView()); buttonsContainer.addView(button.getView()); } // Build constraint set for Navigation Ready state if (navigationReadyConstraintSet == null) { navigationReadyConstraintSet = buildNavigationReadyConstraintSet(newState); } // Apply the constraints navigationReadyConstraintSet.applyTo(layout); // Set the layout in NavigationUiParent navigationUiParent.removeNavigationLayout(layout); navigationUiParent.setNavigationLayout(layout); } private ConstraintSet buildNavigationReadyConstraintSet(NavigationReadyUiState uiState) { ConstraintSet constraintSet = new ConstraintSet(); constraintSet.clone(layout); // Constrain viewport to the edges of its parent constraintSet.connect( uiState.getViewport().getId(), ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START); constraintSet.connect( uiState.getViewport().getId(), ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP); constraintSet.connect( uiState.getViewport().getId(), ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END); constraintSet.connect( uiState.getViewport().getId(), ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM); // Constrain the logo to the bottom start corner constraintSet.connect( uiState.getGoogleLogo().getId(), ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM); constraintSet.connect( uiState.getGoogleLogo().getId(), ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START); constrainButtonsToBottomEnd(constraintSet); return constraintSet; } @Override public void onLeaveNavigationReady( NavigationUiParent navigationUiParent, NavigationReadyUiState oldState) { buttonsContainer.removeAllViews(); layout.removeAllViews(); navigationUiParent.removeNavigationLayout(layout); } @Override public void onEnterActiveGuidance( NavigationUiParent navigationUiParent, NavigationReadyUiState oldState, ActiveGuidanceUiState newState) { activeGuidanceUiState = newState; Context context = navigationUiParent.getViewContext(); // Sizing Guideline: The turn card and ETA card are internally configured to adapt and size // themselves dynamically based on the layout width (non-wideMode vs. wideMode). Forcing fixed // widths or heights on these elements via layouts is unsupported. Always use WRAP_CONTENT to // let the elements determine their optimal proportions. // Add the turn card removeFromParentView(newState.getTurnCard()); LayoutParams turnCardLayoutParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT); layout.addView(newState.getTurnCard(), turnCardLayoutParams); // Add the ETA card removeFromParentView(newState.getEtaCard()); LayoutParams etaCardLayoutParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT); layout.addView(newState.getEtaCard(), etaCardLayoutParams); // Remove the Navigation Ready UI buttons for (NavigationUiButton button : oldState.getNavigationReadyButtons()) { removeFromParentView(button.getView()); } // By adding all buttons to the AutoHidingVerticalLayout, we can easily incorporate the latest // set of buttons when upgrading without any code changes required for (NavigationUiButton button : newState.getActiveGuidanceButtons()) { AutoHidingVerticalLayout.LayoutParams buttonLayoutParams = new AutoHidingVerticalLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); // Mark critical buttons (such as the compass) as high priority so they are the last to be // hidden by AutoHidingVerticalLayout when layout space is limited. if (button.getType() == COMPASS) { buttonLayoutParams.isHighPriority = true; } removeFromParentView(button.getView()); buttonsContainer.addView(button.getView(), buttonLayoutParams); } // Build constraint set for Active Guidance state if (activeGuidanceConstraintSet == null) { activeGuidanceConstraintSet = buildActiveGuidanceConstraintSet(context, newState); } // Apply the constraints activeGuidanceConstraintSet.applyTo(layout); } @Override public void onLeaveActiveGuidance( NavigationUiParent navigationUiParent, ActiveGuidanceUiState oldState, NavigationReadyUiState newState) { // Remove Active Guidance UI elements removeFromParentView(oldState.getEtaCard()); removeFromParentView(oldState.getTurnCard()); buttonsContainer.removeAllViews(); // Add Navigation Ready UI buttons for (NavigationUiButton button : newState.getNavigationReadyButtons()) { LayoutParams buttonLayoutParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT); buttonsContainer.addView(button.getView(), buttonLayoutParams); } navigationReadyConstraintSet.applyTo(layout); } private ConstraintSet buildActiveGuidanceConstraintSet( Context context, ActiveGuidanceUiState uiState) { ConstraintSet constraintSet = new ConstraintSet(); constraintSet.clone(layout); // Constrain turn card to top start corner constraintSet.connect( uiState.getTurnCard().getId(), ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP); constraintSet.connect( uiState.getTurnCard().getId(), ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START); // Constrain viewport to top of ETA card constraintSet.clear(uiState.getViewport().getId()); constraintSet.connect( uiState.getViewport().getId(), ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START); constraintSet.connect( uiState.getViewport().getId(), ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP); // Instead of constraining the viewport's top directly to the bottom of the turn card // (which varies in height and would trigger jumpy camera framing updates), we use a fixed // nominal height to estimate the height of the turncard. constraintSet.setMargin( uiState.getViewport().getId(), ConstraintSet.TOP, dpToPx(StyleValues.headerNominalHeightDp(), context)); constraintSet.connect( uiState.getViewport().getId(), ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END); constraintSet.connect( uiState.getViewport().getId(), ConstraintSet.BOTTOM, uiState.getEtaCard().getId(), ConstraintSet.TOP); constraintSet.clear(uiState.getGoogleLogo().getId(), ConstraintSet.BOTTOM); constrainLogoToTopOfEtaCard(uiState, constraintSet); constrainEtaCardToBottomStart(uiState, constraintSet); constrainButtonsToTopOfEtaCard(context, uiState, constraintSet); return constraintSet; } private static void constrainEtaCardToBottomStart( ActiveGuidanceUiState uiState, ConstraintSet constraintSet) { constraintSet.connect( uiState.getEtaCard().getId(), ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START); constraintSet.connect( uiState.getEtaCard().getId(), ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM); } private static void constrainLogoToTopOfEtaCard( ActiveGuidanceUiState uiState, ConstraintSet constraintSet) { constraintSet.connect( uiState.getGoogleLogo().getId(), ConstraintSet.BOTTOM, uiState.getEtaCard().getId(), ConstraintSet.TOP); } private void constrainButtonsToBottomEnd(ConstraintSet constraintSet) { constraintSet.clear(buttonsContainerId, ConstraintSet.BOTTOM); constraintSet.clear(buttonsContainerId, ConstraintSet.TOP); constraintSet.connect( buttonsContainerId, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP); constraintSet.connect( buttonsContainerId, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM); constraintSet.connect( buttonsContainerId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END); } private void constrainButtonsToTopOfEtaCard( Context context, ActiveGuidanceUiState uiState, ConstraintSet constraintSet) { constraintSet.clear(buttonsContainerId, ConstraintSet.BOTTOM); constraintSet.clear(buttonsContainerId, ConstraintSet.TOP); constraintSet.connect( buttonsContainerId, ConstraintSet.BOTTOM, uiState.getEtaCard().getId(), ConstraintSet.TOP); constraintSet.connect( buttonsContainerId, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP); constraintSet.connect( buttonsContainerId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END); constraintSet.setMargin( buttonsContainerId, ConstraintSet.TOP, dpToPx(StyleValues.headerNominalHeightDp(), context)); } @Override public void onShowPrompt(NavigationUiParent navigationUiParent, View newPrompt) { Context context = navigationUiParent.getViewContext(); layout.addView(newPrompt); // When a prompt is displayed at the bottom of the screen, we update our active constraints so // that the invisible Viewport sits entirely above the prompt. This automatically forces the // Nav SDK camera to adjust its zoom and framing so that the route chevron is always visible to // the driver. activeGuidanceWithPromptConstraintSet = buildActiveGuidanceWithPromptConstraintSet(context, newPrompt); activeGuidanceWithPromptConstraintSet.applyTo(layout); } @Override public void onChangePrompt( NavigationUiParent navigationUiParent, View oldPrompt, View newPrompt) { Context context = navigationUiParent.getViewContext(); activeGuidanceWithPromptConstraintSet.clear(oldPrompt.getId()); layout.removeView(oldPrompt); layout.addView(newPrompt); // When a prompt is displayed at the bottom of the screen, we update our active constraints so // that the invisible Viewport sits entirely above the prompt. This automatically forces the // Nav SDK camera to adjust its zoom and framing so that the route chevron is always visible to // the driver. activeGuidanceWithPromptConstraintSet = buildActiveGuidanceWithPromptConstraintSet(context, newPrompt); activeGuidanceWithPromptConstraintSet.applyTo(layout); } @Override public void onHidePrompt(NavigationUiParent navigationUiParent, View oldPrompt) { activeGuidanceWithPromptConstraintSet.clear(oldPrompt.getId()); layout.removeView(oldPrompt); activeGuidanceConstraintSet.applyTo(layout); } private ConstraintSet buildActiveGuidanceWithPromptConstraintSet(Context context, View prompt) { ConstraintSet constraintSet = new ConstraintSet(); constraintSet.clone(layout); // Constrain viewport to top of prompt constraintSet.clear(activeGuidanceUiState.getViewport().getId()); constraintSet.connect( activeGuidanceUiState.getViewport().getId(), ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START); constraintSet.connect( activeGuidanceUiState.getViewport().getId(), ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP); constraintSet.setMargin( activeGuidanceUiState.getViewport().getId(), ConstraintSet.TOP, dpToPx(StyleValues.headerNominalHeightDp(), context)); constraintSet.connect( activeGuidanceUiState.getViewport().getId(), ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END); constraintSet.connect( activeGuidanceUiState.getViewport().getId(), ConstraintSet.BOTTOM, prompt.getId(), ConstraintSet.TOP); // Constrain prompt to bottom start corner constraintSet.connect( prompt.getId(), ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START); constraintSet.connect( prompt.getId(), ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM); return constraintSet; } private void removeFromParentView(View view) { if (view != null && view.getParent() != null) { ((ViewGroup) view.getParent()).removeView(view); } } private static int dpToPx(int dp, Context context) { return (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()); } }