ปรับแต่งเลย์เอาต์การนำทาง

ภาพเคลื่อนไหวตัวอย่างแสดงเลย์เอาต์การนำทางที่กำหนดเองใน iOS

หมายเหตุ: เอกสารนี้ใช้แบบแผนการตั้งชื่อ Swift สำหรับคลาส, โปรโตคอล และ API

ใช้การปรับแต่งเลย์เอาต์เพื่อควบคุมลำดับชั้นการแสดงผลและการจัดวางหน้าจอสำหรับทั้งมุมมองที่ Google มีให้และมุมมองธุรกิจที่กำหนดเองของคุณเอง

แทนที่จะเพิ่มมุมมองที่กำหนดเองลงในช่องเลย์เอาต์ที่กำหนดไว้ล่วงหน้า ให้ใช้ตัวแทนเลย์เอาต์เพื่อจัดการเลย์เอาต์สำหรับทั้งหน้าจอ ในระหว่างการเปลี่ยนสถานะการนำทาง ตัวแทนเลย์เอาต์จะได้รับคอมโพเนนต์ที่ Google มีให้ เช่น การ์ดเลี้ยว การ์ด ETA และปุ่มต่างๆ วางองค์ประกอบเหล่านี้ไว้ข้างเนื้อหาที่กำหนดเองโดยใช้ข้อจำกัดของ Auto Layout มาตรฐานของ iOS

ใช้เฟรมเวิร์กนี้เพื่อจัดตำแหน่งข้อมูลทางธุรกิจที่กำหนดเอง เช่น สถานะการสั่งซื้อหรือวิธีการรับสินค้า บนหน้าจออย่างแม่นยำพร้อมทั้งป้องกันไม่ให้มุมมองซ้อนทับกัน

วิธีการทำงานของการปรับแต่งเลย์เอาต์

การปรับแต่งเลย์เอาต์ใช้รูปแบบการออกแบบการมอบสิทธิ์ แทนที่จะวาดหรือวางตำแหน่งคอมโพเนนต์ UI บนหน้าจอโดยอัตโนมัติ SDK จะส่งคอมโพเนนต์เหล่านั้นไปยังคลาสที่กำหนดเองที่คุณเขียนโดยตรง ซึ่งก็คือผู้รับมอบสิทธิ์เลย์เอาต์

หากต้องการปรับแต่งเลย์เอาต์ ให้สร้างคลาสที่สอดคล้องกับโปรโตคอล NavigationLayoutDelegate และส่งอินสแตนซ์ไปยังเมธอด setLayoutDelegate(_:) ของ GMSMapView เมื่อใดก็ตามที่สถานะการนำทางเปลี่ยนไป เช่น การเปลี่ยนจากแผนที่ฐานไปเป็นคำแนะนำแบบเลี้ยวต่อเลี้ยวที่ใช้งานอยู่ SDK จะเรียกใช้เมธอด Callback ในผู้รับมอบสิทธิ์และจัดเตรียมคอมโพเนนต์ UI ที่พร้อมใช้งานสำหรับสถานะที่เฉพาะเจาะจงนั้น

เฟรมเวิร์กนี้จะแยกความรับผิดชอบระหว่างแอปกับ SDK อย่างชัดเจน เพื่อมอบประสบการณ์การใช้งานที่ราบรื่นและผสานรวมให้แก่ผู้ใช้ ใช้ตัวแทนเลย์เอาต์เพื่อปรับแต่งสิ่งต่อไปนี้

  • สร้างลำดับชั้นการแสดงผล: เลือกคอมโพเนนต์ของ Google และมุมมองธุรกิจที่กำหนดเองที่ต้องการเพิ่มลงในหน้าจอสำหรับสถานะ UI แต่ละสถานะ
  • จัดตำแหน่งทุกองค์ประกอบ: กำหนดจุดยึดหน้าจอ ระยะขอบ และตำแหน่งเลย์เอาต์ที่แม่นยำ หลีกเลี่ยงการใช้ข้อจำกัดความกว้างหรือความสูงที่กำหนดเองกับคอมโพเนนต์ของ Google เนื่องจากคอมโพเนนต์ของ Google จะคำนวณขนาดภายในของตนเอง
  • จัดเฟรมแผนที่ฐาน: กำหนดขอบเขตที่มองเห็นได้ของกล้องแผนที่โดยใช้พิกัดของคอมโพเนนต์วิวพอร์ต
  • จัดเลเยอร์หน้าจอ: เลือกว่าจะให้มุมมองที่กำหนดเองลอยอยู่ด้านบน ด้านล่าง หรืออยู่ข้างๆ ตัวควบคุมในตัวของ Google

ในระหว่างนี้ คุณจะปรับแต่งคอมโพเนนต์ Google ต่อไปนี้ด้วยตัวแทนเลย์เอาต์ไม่ได้

  • ขนาดคอมโพเนนต์: ขนาดและขนาดภายในของคอมโพเนนต์ที่ Google มีให้ ซึ่ง SDK จะคำนวณโดยอัตโนมัติ
  • เงื่อนไขทริกเกอร์: เมื่อการแจ้งเตือนหรือข้อความแจ้งแบบไดนามิกปรากฏขึ้นตามข้อมูลเส้นทางแบบเรียลไทม์

หลักการติดตั้งใช้งาน

เมื่อเขียน Layout Delegate โปรดคำนึงถึงกฎต่อไปนี้เพื่อป้องกันข้อบกพร่องของเลย์เอาต์หรือข้อขัดข้องของรันไทม์

  • ค่าเริ่มต้นคือหน้าจอว่างเปล่า: คอมโพเนนต์ที่ Google มีให้จะปรากฏก็ต่อเมื่อผู้รับมอบสิทธิ์เพิ่มคอมโพเนนต์เหล่านั้นลงในลำดับชั้นการแสดงผลและจัดตำแหน่งอย่างชัดเจน
  • ไม่รองรับ API เดิม: ระบบจะไม่รองรับพร็อพเพอร์ตี้และเมธอดที่ควบคุมโมเดลเลย์เอาต์ตามช่องแบบเดิม และอาจไม่ทำงานตามที่คาดไว้เมื่อผู้รับมอบสิทธิ์ที่กำหนดเองทำงานอยู่
  • อย่าแก้ไขโครงสร้างมุมมองภายใน: อย่าข้ามหรือแก้ไขลำดับชั้นการแสดงผลของคอมโพเนนต์ที่ Google มีให้ (เช่น การ์ดเลี้ยวหรือการ์ด ETA) เนื่องจากลำดับชั้นการแสดงผลภายในเหล่านี้เป็นรายละเอียดการใช้งานเบื้องหลัง จึงอาจมีการเปลี่ยนแปลงใน SDK รุ่นต่างๆ การแก้ไขอาจทำให้เลย์เอาต์เสียหายระหว่างการอัปเดต SDK ในอนาคต

ความเข้ากันได้กับ API เดิมของ

เพื่อลักษณะการทำงานของเลย์เอาต์ที่เชื่อถือได้เมื่อใช้ตัวแทนเลย์เอาต์ที่กำหนดเอง ให้หลีกเลี่ยงการใช้ API แบบสล็อตเดิมต่อไปนี้ ซึ่งมีแผนที่จะเลิกใช้งาน หากต้องการย้ายข้อมูลแอปที่มีอยู่ ให้แทนที่การใช้ API เหล่านี้ด้วยโค้ดในตัวแทนเลย์เอาต์ที่กำหนดเอง ดังนี้

แสดง API แบบอิงตามช่องโฆษณาเดิม

API เดิม การเปลี่ยน Layout Delegate
isNavigationHeaderEnabled อ่านมุมมอง turnCard ใน startGuidance(navMap:newState:oldState:)
isNavigationFooterEnabled อ่านมุมมอง etaCard ใน startGuidance(navMap:newState:oldState:)
isNavigationTripProgressBarEnabled อ่านมุมมอง tripProgressBar ใน startGuidance(navMap:newState:oldState:)
isNavigationReportIncidentButtonEnabled ค้นหาปุ่ม .reporting ในอาร์เรย์ buttons
setHeaderAccessoryView(_:) และ invalidateLayout(forAccessoryView:) เพิ่มมุมมองของคุณเองลงในเลย์เอาต์และยึดไว้กับ turnCard
padding และ paddingAdjustmentBehavior จัดตำแหน่งมุมมอง viewport เพื่อจัดเฟรมกล้อง
navigationHeaderLayoutGuide ปักหมุดมุมมองที่กำหนดเองไว้ที่ turnCard โดยตรง
navigationFooterLayoutGuide ปักหมุดมุมมองที่กำหนดเองไว้ที่ etaCard โดยตรง
bottomTrailingButtonsLayoutGuide ยึดปุ่มไว้กับ etaCard หรือเส้นบอกแนวคอนเทนเนอร์โดยตรง
bottomTrailingButtonsAlternateLayoutGuide ยึดปุ่มไว้กับปุ่ม .reporting หรือข้อจำกัด wideMode โดยตรง
navigatorWillPresentPrompt(_:) และ navigatorDidDismissPrompt(_:) ใช้การโทรกลับที่รวดเร็วของ NavigationLayoutDelegate เช่น showPrompt(navMap:newPrompt:)

ขณะที่มีการตั้งค่าผู้รับมอบสิทธิ์เลย์เอาต์ที่กำหนดเอง ไกด์เลย์เอาต์เดิม (เช่น navigationHeaderLayoutGuide หรือ bottomTrailingButtonsLayoutGuide) และเมธอดมุมมองอุปกรณ์เสริมส่วนหัวจะไม่มีผล ให้ยึดมุมมองของคุณตามมุมมองที่ Google มีให้แทน

พร็อพเพอร์ตี้ compassButton ของ GMSUISettings ยังคงควบคุมเข็มทิศขณะที่ UI การนำทางปิดใช้อยู่ ขณะที่เปิดใช้การนำทาง ผู้รับมอบสิทธิ์จะตัดสินใจว่าจะแสดงเข็มทิศจากอาร์เรย์ buttons หรือไม่

รายการตรวจสอบ Essentials

ทำตามขั้นตอนและข้อกำหนดที่สำคัญต่อไปนี้เพื่อติดตั้งใช้งาน Layout Delegate ให้สำเร็จ

  • ตั้งค่าผู้รับมอบสิทธิ์ก่อนเปิดใช้การนำทาง: เรียกใช้ setLayoutDelegate(_:) ใน GMSMapView ก่อนเปิดใช้การนำทางด้วย isNavigationEnabled หรือ enableNavigation(with:) เมธอดจะแสดงข้อผิดพลาดหากเปิดใช้การนําทางอยู่แล้ว หรือหากคุณสร้างมุมมองแผนที่สําหรับหน้าจอ CarPlay มุมมองแผนที่จะเก็บการอ้างอิงที่ชัดเจนไปยังผู้รับมอบสิทธิ์ของคุณ หากต้องการคืนค่าเลย์เอาต์ในตัว ให้ส่ง nil ขณะที่การนำทางปิดใช้อยู่

    // Set the layout delegate before you enable navigation.
    do {
      try mapView.setLayoutDelegate(MyLayoutDelegate())
    } catch {
      print("Couldn't set the layout delegate: \(error)")
    }
    mapView.isNavigationEnabled = true
        
  • แนบมุมมองที่จำเป็น: คุณต้องเพิ่มโลโก้ Google (googleLogo) และวิวพอร์ต (viewport) ลงในคอนเทนเนอร์เลย์เอาต์ในสถานะการนำทางทั้งหมด

  • ใช้ขนาดคงที่: SDK จะกำหนดขนาดสำหรับคอมโพเนนต์หลายรายการที่ Google มีให้ อย่าใช้ข้อจำกัดความกว้างหรือความสูงที่กำหนดเองกับองค์ประกอบต่อไปนี้

    • การ์ดบอกทาง
    • การ์ด ETA
    • พรอมต์ที่เข้ามาใหม่
    • โลโก้ Google
    • วิดเจ็ตความเร็ว
  • เพิ่มองค์ประกอบเป็นมุมมองย่อย: เพิ่มมุมมองที่ Google มีให้เป็นมุมมองย่อยโดยตรงลงในมุมมองคอนเทนเนอร์ที่กำหนดเองหรือลงใน GMSMapView เอง

  • จัดแนวโดยใช้เส้นบอกแนวเลย์เอาต์: มุมมองที่ Google มีให้จะมีระยะขอบภาพในตัว หากต้องการจัดองค์ประกอบที่กำหนดเองให้สอดคล้องกับคอมโพเนนต์ที่ Google มีให้อย่างสมบูรณ์แบบ ให้จำกัดมุมมองของคุณไว้ที่พร็อพเพอร์ตี้ layoutMarginsGuide ของมุมมอง Google

  • ล้างข้อมูลเมื่อออกจากสถานะ: ใน stopGuidance(navMap:newState:oldState:) ให้นำมุมมองคำแนะนำที่ใช้งานอยู่ออกและปิดใช้งานข้อจำกัดก่อนที่จะคืนค่าเลย์เอาต์ที่พร้อมสำหรับการนำทาง ใน disableNavigationUI(navMap:oldState:) ให้นำมุมมองและเส้นบอกแนวเลย์เอาต์ทั้งหมดที่คุณเพิ่มออก รวมถึงข้อความแจ้งบนหน้าจอด้วย ซึ่งจะป้องกันไม่ให้มุมมองจากสถานะหนึ่งทับซ้อนกับสถานะถัดไป

  • การช่วยเหลือพิเศษและการรองรับการอ่านจากขวาไปซ้าย: มุมมองที่ Google มีให้เป็นไปตามมาตรฐานการช่วยเหลือพิเศษ แต่ผู้รับมอบสิทธิ์มีหน้าที่รับผิดชอบต่อการช่วยเหลือพิเศษของเลย์เอาต์โดยรวม หากต้องการรองรับภาษาที่อ่านจากขวาไปซ้าย ให้ใช้ Anchor นำหน้าและต่อท้ายแทน Anchor ซ้ายและขวา

  • หลีกเลี่ยง API เลย์เอาต์เดิม: อย่าใช้พร็อพเพอร์ตี้การแก้ไขเลย์เอาต์เดิมใน GMSUISettings หรือไกด์เลย์เอาต์เดิมใน GMSMapView ขณะที่ตัวแทนที่กำหนดเองทำงานอยู่ SDK อาจละเว้นการโทรเวอร์ชันเดิมเหล่านี้เมื่อมีการแนบผู้รับมอบสิทธิ์ที่กำหนดเอง

สถานะ UI และคอมโพเนนต์ของ Google

เมื่อเกิดการเปลี่ยนสถานะการนำทาง SDK จะส่งออบเจ็กต์ NavigationUIState แบบอ่านอย่างเดียวไปยัง Callback ของผู้รับมอบสิทธิ์ ออบเจ็กต์นี้จะรวมแฟล็กการกำหนดค่าเลย์เอาต์ปัจจุบันเข้ากับคอมโพเนนต์ของ Google (เช่น อินสแตนซ์ UIView) ที่คุณต้องใช้ในการแสดงผลหน้าจอ

ผู้รับมอบสิทธิ์เลย์เอาต์จะจัดการลำดับชั้นการแสดงผลในสถานะการทำงาน 4 สถานะ แผนภาพสถานะต่อไปนี้แสดงวิธีที่ SDK เปลี่ยนสถานะการนำทางและเมธอด Callback ที่ SDK ดำเนินการกับผู้รับมอบสิทธิ์

แผนภาพการเปลี่ยนสถานะวงจรของ NavigationLayoutDelegate ที่แสดงการเปลี่ยนสถานะระหว่างสถานะไม่มีการนำทาง พร้อมนำทาง และการนำทางที่ใช้งานอยู่

คอมโพเนนต์ Google ที่เฉพาะเจาะจงซึ่งมอบให้กับผู้รับมอบสิทธิ์จะขึ้นอยู่กับระยะปัจจุบันของวงจรการนําทาง

องค์ประกอบที่จำเป็น (สถานะการนำทางทั้งหมด)

ไม่ว่าจะอยู่ในระยะการนำทางใด คุณต้องรวม จัดตำแหน่ง และแสดงคอมโพเนนต์ของ Google ต่อไปนี้ให้เห็นในลำดับชั้นการแสดงผล

  • โลโก้ Google (googleLogo): คอมโพเนนต์นี้จะแสดงโลโก้ Google Maps ที่จำเป็น หากเปิดใช้ปุ่มจัดให้อยู่ตรงกลาง โลโก้จะเปลี่ยนเป็นปุ่มนี้โดยอัตโนมัติเมื่อผู้ขับเลื่อนแผนที่ออกจากยานพาหนะของตน ด้วยเหตุนี้ ตำแหน่งของโลโก้จึงเป็นตัวกำหนดตำแหน่งที่ปุ่มจัดให้อยู่ตรงกลางอีกครั้งจะปรากฏขึ้น เราขอแนะนำให้วางโลโก้ไว้ที่มุมล่างซ้ายของเลย์เอาต์เพื่อให้ตรงกับความคาดหวังของ UI แผนที่มาตรฐาน

  • วิวพอร์ต (viewport): UIView ที่มองไม่เห็นซึ่งกำหนดขอบเขตการจัดเฟรมของกล้อง ตำแหน่งของวิวพอร์ตจะเป็นตัวกำหนดตำแหน่งที่ SDK จะจัดกึ่งกลางเครื่องหมายบั้งของยานพาหนะและวาดเส้นทางที่ใช้งานอยู่ จัดตำแหน่งวิวพอร์ตให้ครอบคลุมพื้นที่ที่เปิดอยู่และไม่มีสิ่งบดบังบนหน้าจอ โดยหลีกเลี่ยงการวางซ้อนทับองค์ประกอบที่ทึบแสง เช่น Bottom Sheet ที่กำหนดเอง เพื่อความปลอดภัย

ในระหว่างสถานะพร้อมนำทาง UI จะยังคงมีขนาดเล็กที่สุดเพื่อมุ่งเน้นไปที่แผนที่ฐาน ออบเจ็กต์ NavigationReadyUIState ให้สิทธิ์เข้าถึงสิ่งต่อไปนี้

  • buttons: อาร์เรย์ของมุมมองการดำเนินการแบบลอยที่กำหนดค่าไว้สำหรับแผนที่ฐาน (โดยทั่วไปจะเป็นเพียงปุ่มเข็มทิศ) เนื่องจาก Google อาจเพิ่มหรือจัดเรียงปุ่มใหม่ใน SDK รุ่นต่อๆ ไป โปรดหลีกเลี่ยงการใช้ดัชนีอาร์เรย์แบบคงที่ แต่ให้ทำซ้ำผ่านอาร์เรย์ อ่านประเภทของปุ่มแต่ละปุ่มโดยเปรียบเทียบพร็อพเพอร์ตี้ type กับ NavigationButtonKnownType.compass และดึงข้อมูลมุมมองจริงโดยเรียกใช้พร็อพเพอร์ตี้ view โปรดทราบว่าปุ่มที่แน่นอนในอาร์เรย์นี้แตกต่างจากปุ่มที่ใช้ได้ในระหว่างการนำทางที่ใช้งานอยู่

คอมโพเนนต์คำแนะนำที่ใช้งานอยู่ รายการ

เมื่อคำแนะนำแบบเลี้ยวต่อเลี้ยวเริ่มต้นขึ้น SDK จะปลดล็อกชุดการควบคุมการนำทางทั้งหมด ออบเจ็กต์ ActiveGuidanceUIState ให้สิทธิ์เข้าถึงคอมโพเนนต์ Google ต่อไปนี้

  • turnCard: แบนเนอร์ส่วนหัวหลักที่แสดงทิศทางการเคลื่อนที่ ระยะทาง และคำแนะนำช่องทางที่กำลังจะถึง วางไว้ที่ด้านบนของเลย์เอาต์เพื่อสร้างลำดับชั้นการนำทางที่คุ้นเคยและยึดมุมมองที่กำหนดเองไว้รอบๆ

  • etaCard: แบนเนอร์ส่วนท้ายที่แสดงเวลาถึงโดยประมาณ ระยะเวลาการเดินทางที่เหลือ และระยะทางที่เหลือไปยังจุดหมาย วางไว้ที่ขอบด้านล่างของหน้าจอ หรือผสานรวมพิกัดของวิดเจ็ตกับชีตการจัดการงานที่กำหนดเอง

  • tripProgressBar: แถบความคืบหน้าแนวตั้งที่แสดงให้คนขับเห็นว่าเดินทางมาไกลแค่ไหนแล้วตามเส้นทางปัจจุบัน ซึ่งแตกต่างจากเลย์เอาต์เดิมที่กำหนดให้องค์ประกอบนี้อยู่ติดกับขอบแผนที่อย่างเคร่งครัด คุณมีอิสระอย่างเต็มที่ในการยึดองค์ประกอบนี้ไว้ที่ใดก็ได้ เช่น ตามขอบด้านข้างของคอนเทนเนอร์ที่กำหนดเอง

  • speedWidget: การควบคุมแบบลอยสำหรับมาตรวัดความเร็วและขีดจำกัดความเร็วที่ระบุไว้ มุมมองนี้จะปรับขนาดแบบไดนามิกระหว่างสถานะภาพ 4 สถานะที่รันไทม์ (ไม่แสดงอะไรเลย แสดงเฉพาะความเร็วปัจจุบัน แสดงเฉพาะขีดจำกัดความเร็ว หรือแสดงทั้ง 2 อย่าง) โดยขึ้นอยู่กับการตั้งค่า API (พร็อพเพอร์ตี้ shouldDisplaySpeedometer และ shouldDisplaySpeedLimit ของ GMSMapView) และความพร้อมใช้งานของข้อมูล เนื่องจากวิดเจ็ตสามารถเปลี่ยนขนาดเหล่านี้ได้โดยไม่ต้องแจ้งให้ทราบล่วงหน้า ให้ยึดมุมมองโดยรอบโดยใช้ข้อจำกัดเสมอเพื่อให้เลย์เอาต์ปรับโดยอัตโนมัติเพื่อป้องกันการทับซ้อนกันของพื้นที่

  • buttons: อาร์เรย์ที่ขยายของมุมมองการดำเนินการแบบลอยสำหรับสถานะคำแนะนำที่ใช้งานอยู่ (โดยปกติจะรวมทั้งเข็มทิศและปุ่มรายงานเหตุการณ์) เช่นเดียวกับในสถานะพร้อมสำหรับการนำทาง คุณสามารถค้นหาและดึงข้อมูลมุมมองปุ่มแต่ละรายการได้โดยกรองอาร์เรย์ตาม type (.compass หรือ .reporting) และดึงข้อมูลมุมมองจริงโดยใช้พร็อพเพอร์ตี้ view จากนั้นคุณจะจัดตำแหน่งกล้องแต่ละตัวแยกกัน หรือเพิ่มกล้องลงในGMSNavigationHidingStackViewก็ได้ มุมมองกองซ้อนนี้จะจัดเรียงปุ่มในแนวตั้ง และเมื่อพื้นที่หมดลง จะซ่อนปุ่มโดยเริ่มจากส่วนท้ายของอาร์เรย์ arrangedSubviews อย่าตั้งค่า isHidden ในมุมมองที่จัดการ
เคล็ดลับ: แนวทางปฏิบัติแนะนำสำหรับการจัดเฟรมกล้อง มุมมองของการ์ดเลี้ยวมีความสูงคงที่ตาม NavigationStyleValues.headerNominalHeight ซึ่งสูงพอสำหรับเนื้อหาการ์ดเลี้ยวทั้งหมด หากต้องการให้การจัดเฟรมกล้องคงที่ ให้จำกัดด้านบนของ viewport ไว้ที่ขอบด้านล่างของ turnCard โดยตรง ดังที่แสดงในตัวอย่างการติดตั้งใช้งาน

องค์ประกอบพรอมต์แบบไดนามิก รายการ

ข้อความแจ้ง เช่น การแจ้งเตือนเหตุการณ์หรือคำเตือนเกี่ยวกับกล้องจับความปลอดภัย จะทริกเกอร์อย่างอิสระในระหว่างการนำทางที่ใช้งานอยู่

เมื่อข้อความแจ้งพร้อมแสดง SDK จะเรียกใช้ Callback showPrompt(navMap:newPrompt:) ของผู้รับมอบสิทธิ์และส่งมุมมอง newPrompt โดยตรง ผู้รับมอบสิทธิ์มีหน้าที่จัดตำแหน่งข้อความแจ้งนี้ให้อยู่เหนือเลย์เอาต์อย่างราบรื่น (โดยปกติจะยึดไว้ที่ขอบด้านล่างของคอนเทนเนอร์แผนที่)

เนื่องจากข้อความแจ้งที่เข้ามาจะครอบคลุมส่วนล่างของหน้าจอ คุณจึงต้องอัปเดตเลย์เอาต์เพื่อป้องกันไม่ให้ข้อความแจ้งซ้อนทับวิวพอร์ต โลโก้ Google หรือปุ่มที่จัดชิดด้านล่าง

นอกจากนี้ ผู้รับมอบสิทธิ์ยังควบคุมวิธีที่พรอมต์เข้าและออกจากหน้าจอด้วย โปรดดูรายละเอียดที่หัวข้อการปรับแต่งพรอมต์

การจัดการขนาดหน้าจอและโหมดหน้าจอกว้าง

ในการจัดการการเปลี่ยนแปลงขนาดแผนที่และการวางแนวหน้าจอ ตัวแทนเลย์เอาต์จะใช้ฟีเจอร์ต่อไปนี้

  • การปรับขนาดมุมมอง: ปรับเลย์เอาต์เมื่อใดก็ตามที่ขนาดจริงของแผนที่มีการเปลี่ยนแปลง

  • โหมดกว้าง: สลับไปใช้ตัวแปรเลย์เอาต์รูปแบบกว้างเมื่อแผนที่กว้างพอ

การตอบสนองต่อการปรับขนาดมุมมอง

เลย์เอาต์แบบแยกหน้าจอ แถบเลื่อนคอนเทนเนอร์ และการหมุนอุปกรณ์มักจะทริกเกอร์ Callback sizeChanged(navMap:newState:) ใช้ Callback นี้เพื่อทำการปรับเปลี่ยน UI ที่กำหนดเองให้ปรับเปลี่ยนตามอุปกรณ์โดยทั่วไป ใช้ sizeChanged(navMap:newState:) เพื่อเปลี่ยนตำแหน่งองค์ประกอบที่กำหนดเองสำหรับสัดส่วนภาพใหม่ของหน้าจอ ใช้เบรกพอยต์ความกว้างหรือความสูงที่กำหนดเอง และตรวจจับเมื่อเหตุการณ์การปรับขนาดสลับโหมดกว้างเพื่อสลับตัวแปรเลย์เอาต์อย่างปลอดภัย

ทำความเข้าใจโหมดกว้าง

โหมดกว้างจะเปิดใช้งานเมื่อคอนเทนเนอร์แผนที่กว้างพอที่จะแสดงคอมโพเนนต์ UI เคียงข้างกัน

อ่านบูลีน wideMode จากออบเจ็กต์สถานะเพื่อเปลี่ยนตำแหน่งองค์ประกอบ UI ที่กำหนดเองและทำให้ตรงกลางแผนที่ชัดเจนสำหรับคนขับ เนื่องจากคอมโพเนนต์ที่ Google มีให้ เช่น การ์ดเลี้ยวและการ์ด ETA จะหดและปรับรูปร่างใหม่โดยอัตโนมัติในโหมดกว้าง การอ่านบูลีนนี้จะช่วยให้มั่นใจได้ว่าเลย์เอาต์ของคุณจะปรับเปลี่ยนในขณะเดียวกับที่คอมโพเนนต์ของ Google อัปเดต

พิจารณาตัวอย่างต่อไปนี้สำหรับการจัดตำแหน่งคอมโพเนนต์เลย์เอาต์ในโหมดมาตรฐานและโหมดกว้าง

  • โหมดแนวตั้งมาตรฐาน: วางการ์ดเลี้ยวไว้ที่ด้านบนของหน้าจอและการ์ด ETA ไว้ที่ด้านล่าง

  • โหมดกว้าง: แสดงการ์ดเลี้ยวและการ์ด ETA ไว้ที่ขอบด้านหน้าของหน้าจอ โดยให้การ์ดเลี้ยวอยู่ด้านบนและการ์ด ETA อยู่ด้านล่าง การวางการ์ดใดการ์ดหนึ่งให้ห่างจากขอบด้านหน้าจะทำให้เกิดลักษณะการทำงานที่ไม่ได้กำหนดไว้ คุณสามารถวางวิวพอร์ตและเนื้อหาของคุณเองไว้ที่ใดก็ได้ โดยตัวเลือกที่นิยมคือด้านท้ายของการ์ด

กำลังตรวจสอบโหมดกว้างระหว่างการเปลี่ยนสถานะ

หากเลย์เอาต์รองรับรูปแบบโหมดกว้าง ให้ประเมินบูลีน wideMode ภายใน Callback การเปลี่ยนสถานะทุกครั้ง แทนที่จะอาศัยเพียง sizeChanged(navMap:newState:)

ตรวจสอบออบเจ็กต์สถานะระหว่างการเรียกกลับ เช่น enableNavigationUI(navMap:newState:) และ startGuidance(navMap:newState:oldState:) เพื่อจัดการการเปิดแอปครั้งแรกในแนวนอน วิธีนี้ช่วยให้มั่นใจว่าเลย์เอาต์จะประเมินสถานะโหมดกว้างและเปิดใช้งานตัวแปรเลย์เอาต์มาตรฐานหรือแบบกว้างที่ถูกต้องทันทีที่สถานะการนําทางใหม่เริ่มต้นขึ้น

การปรับให้สอดคล้องกับรูปแบบของ Google

SDK มีคลาส NavigationStyleValues เพื่อช่วยให้มุมมองที่กำหนดเองตรงกับระยะห่างของคอมโพเนนต์ที่ Google มีให้ พร็อพเพอร์ตี้คลาสจะแสดงผลการวัดเป็นจุด

ตัวอย่างเช่น หากต้องการวางปุ่มที่กำหนดเองไว้ที่มุมด้านบนของหน้าจอตรงข้ามกับการ์ดเลี้ยว ให้ใช้ NavigationStyleValues.headerTopPadding เป็นระยะขอบด้านบนของปุ่ม จากนั้นปุ่มจะจัดแนวให้ตรงกับขอบด้านบนของการ์ดเลี้ยว

การวัดที่ใช้ได้มีดังนี้

  • NavigationStyleValues.buttonHorizontalPadding
  • NavigationStyleValues.buttonVerticalPadding
  • NavigationStyleValues.defaultCornerRadius
  • NavigationStyleValues.elementHorizontalPadding
  • NavigationStyleValues.headerFooterHorizontalPadding
  • NavigationStyleValues.headerNominalHeight
  • NavigationStyleValues.headerTopPadding

มุมมองที่ Google มีให้ยังรวมถึงระยะขอบในตัวด้วย เมื่อวางซ้อนมุมมองที่ Google มีให้ 2 มุมมอง ให้จำกัดมุมมองหนึ่งไว้ที่ layoutMarginsGuide ของอีกมุมมองหนึ่งเพื่อไม่ให้ระยะขอบเพิ่มขึ้น เช่น หากต้องการวางวิดเจ็ตความเร็วไว้เหนือการ์ด ETA ให้จำกัด Anchor ด้านล่างของ speedWidget ไว้ที่ etaCard.layoutMarginsGuide.topAnchor

การปรับแต่งพรอมต์

ตัวแทนเลย์เอาต์จะควบคุมวิธีที่พรอมต์เข้าและออกจากหน้าจอ รวมถึงมุมที่โค้งมน

พรอมต์การสร้างภาพเคลื่อนไหว

SDK จะไม่แสดงภาพเคลื่อนไหวของข้อความแจ้งให้คุณ ผู้รับมอบสิทธิ์จะเป็นผู้กำหนดลักษณะการปรากฏและหายไปของข้อความแจ้งแต่ละรายการ ดังนี้

  • ใน showPrompt(navMap:newPrompt:) ให้เพิ่มพรอมต์ลงในมุมมองแผนที่ที่ตำแหน่งเริ่มต้น แล้วทำให้เคลื่อนไหวเข้าสู่ตำแหน่ง เช่น เริ่มแสดงข้อความแจ้งใต้พื้นที่ที่มองเห็นได้ หรือหลัง Bottom Sheet โดยให้ขอบด้านบนอยู่ในแนวเดียวกับด้านบนของชีต
  • ใน hidePrompt(navMap:oldPrompt:) ให้ทำให้ข้อความแจ้งเคลื่อนไหวออกจากมุมมอง แล้วนำออกจากมุมมองหลักเมื่อภาพเคลื่อนไหวเสร็จสิ้น
  • ใน changePrompt(navMap:newPrompt:oldPrompt:) ให้เรียกใช้ภาพเคลื่อนไหวที่ซ่อนและแสดงพร้อมกันเพื่อแทนที่พรอมต์หนึ่งด้วยอีกพรอมต์หนึ่ง

ตัวอย่างการใช้งานใช้วิธีนี้

หากต้องการปิดข้อความแจ้งเกี่ยวกับการจราจร เส้นทางที่ดีกว่า และเหตุการณ์ ให้ตั้งค่าพร็อพเพอร์ตี้ shouldDisplayPrompts ของ GMSNavigator เป็น false

การปัดเศษมุมพรอมต์

ข้อความแจ้งเป็นไปตามโปรโตคอล NavigationRoundedCornerView คุณจึงเลือกได้ว่าจะให้มุมใดโค้งมน โดยค่าเริ่มต้น พรอมต์จะปัดเฉพาะมุมด้านบนเท่านั้น ซึ่งเหมาะกับพรอมต์ที่ตรึงไว้ที่ขอบด้านล่างของหน้าจอ หากคุณวางข้อความแจ้งไว้เหนือเนื้อหาอื่นๆ เช่น Bottom Sheet ให้ปัดมุมทั้ง 4 มุมให้โค้งมน ดังนี้

newPrompt.roundedCorners = [
  .layerMinXMinYCorner, .layerMaxXMinYCorner,
  .layerMinXMaxYCorner, .layerMaxXMaxYCorner,
]

ตัวอย่าง: การติดตั้งใช้งานเลย์เอาต์ตามข้อจำกัด

ตัวอย่างต่อไปนี้แสดงตัวแทนเลย์เอาต์พื้นฐานที่ใช้ข้อจำกัดของ Auto Layout เพื่อจัดตำแหน่งการ์ดเลี้ยว การ์ด ETA ปุ่ม โลโก้ Google และวิวพอร์ต และยังเลื่อนข้อความแจ้งเข้าและออกจากหน้าจอด้วย เพื่อให้ตัวอย่างสั้นลง จึงไม่ได้จัดการโหมดกว้าง วิดเจ็ตความเร็ว หรือแถบความคืบหน้าของการเดินทาง จึงเหมาะสำหรับแอปที่รองรับการวางแนวตั้งเท่านั้น

import GoogleNavigation
import UIKit

/// A layout delegate that positions the essential navigation UI elements: the
/// turn card, ETA card, buttons, Google logo, and viewport. It also slides
/// prompts on and off the screen.
@MainActor
final class BasicLayoutDelegate: NSObject, NavigationLayoutDelegate {
  /// How long prompts take to slide on or off the screen, in seconds.
  private static let promptAnimationDuration: TimeInterval = 0.4

  /// The views and constraints for one navigation state.
  @MainActor
  private final class StateElements {
    let buttonStack = GMSNavigationHidingStackView(
      position: .bottom, alignment: .trailing)
    var constraints: [NSLayoutConstraint] = []
    private unowned let navMap: GMSMapView
    private let buttons: [UIView]
    private let topLevelViews: [UIView]

    init(
      navMap: GMSMapView, buttons: [NavigationUIButton],
      topLevelViews: [UIView]
    ) {
      self.navMap = navMap
      self.buttons = buttons.map { $0.view }
      self.topLevelViews = topLevelViews
    }

    /// Adds the views to the map view, and then activates the constraints.
    func activate() {
      topLevelViews.forEach { navMap.addSubview($0) }
      buttonStack.arrangedSubviews = buttons
      navMap.addSubview(buttonStack)
      NSLayoutConstraint.activate(constraints)
    }

    /// Deactivates the constraints, and then removes the views.
    func deactivate() {
      NSLayoutConstraint.deactivate(constraints)
      buttonStack.removeFromSuperview()
      buttonStack.arrangedSubviews = []
      topLevelViews.forEach { $0.removeFromSuperview() }
    }
  }

  private var navigationReadyElements: StateElements?
  private var activeGuidanceElements: StateElements?
  private var currentPrompt: NavigationPromptView?
  private var promptBottomConstraint: NSLayoutConstraint?

  /// Constraints that apply in both the navigation-ready and active guidance
  /// states.
  private var sharedConstraints: [NSLayoutConstraint] = []

  /// Marks the bottom of the map content area. The guide stays above the ETA
  /// card and above any prompt that's on the screen.
  private let mapContentsBottomGuide = UILayoutGuide()

  // MARK: - Navigation-ready state

  func enableNavigationUI(
    navMap: GMSMapView, newState: NavigationReadyUIState
  ) {
    let safeArea = navMap.safeAreaLayoutGuide
    let bottomGuide = mapContentsBottomGuide
    navMap.addLayoutGuide(bottomGuide)

    // Keep the guide at the bottom of the safe area unless a higher-priority
    // constraint pushes it up.
    let lowPriorityBottom = bottomGuide.bottomAnchor.constraint(
      equalTo: safeArea.bottomAnchor)
    lowPriorityBottom.priority = .defaultLow
    sharedConstraints = [
      bottomGuide.heightAnchor.constraint(equalToConstant: 0),
      lowPriorityBottom,
    ]

    let logo = newState.googleLogo
    let viewport = newState.viewport
    let elements = StateElements(
      navMap: navMap, buttons: newState.buttons,
      topLevelViews: [logo, viewport])
    let buttonStack = elements.buttonStack
    elements.constraints = sharedConstraints + [
      buttonStack.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor),
      buttonStack.topAnchor.constraint(equalTo: safeArea.topAnchor),
      buttonStack.bottomAnchor.constraint(equalTo: bottomGuide.topAnchor),

      logo.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
      logo.bottomAnchor.constraint(equalTo: bottomGuide.topAnchor),

      viewport.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
      viewport.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor),
      viewport.topAnchor.constraint(equalTo: safeArea.topAnchor),
      viewport.bottomAnchor.constraint(equalTo: bottomGuide.topAnchor),
    ]
    elements.activate()
    navigationReadyElements = elements
  }

  func disableNavigationUI(
    navMap: GMSMapView, oldState: NavigationReadyUIState
  ) {
    currentPrompt?.removeFromSuperview()
    currentPrompt = nil
    promptBottomConstraint = nil
    navigationReadyElements?.deactivate()
    navigationReadyElements = nil
    navMap.removeLayoutGuide(mapContentsBottomGuide)
  }

  // MARK: - Active guidance state

  func startGuidance(
    navMap: GMSMapView, newState: ActiveGuidanceUIState,
    oldState: NavigationReadyUIState
  ) {
    navigationReadyElements?.deactivate()

    let safeArea = navMap.safeAreaLayoutGuide
    let bottomGuide = mapContentsBottomGuide
    let turnCard = newState.turnCard
    let etaCard = newState.etaCard
    let logo = newState.googleLogo
    let viewport = newState.viewport
    let elements = StateElements(
      navMap: navMap, buttons: newState.buttons,
      topLevelViews: [turnCard, etaCard, viewport, logo])
    let buttonStack = elements.buttonStack
    elements.constraints = sharedConstraints + [
      turnCard.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
      turnCard.topAnchor.constraint(equalTo: navMap.topAnchor),

      etaCard.leadingAnchor.constraint(equalTo: navMap.leadingAnchor),
      etaCard.bottomAnchor.constraint(equalTo: navMap.bottomAnchor),
      bottomGuide.bottomAnchor.constraint(
        lessThanOrEqualTo: etaCard.topAnchor),

      buttonStack.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor),
      buttonStack.topAnchor.constraint(equalTo: turnCard.bottomAnchor),
      buttonStack.bottomAnchor.constraint(equalTo: bottomGuide.topAnchor),

      logo.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
      logo.bottomAnchor.constraint(equalTo: bottomGuide.topAnchor),

      // Frame the camera in the open area below the turn card.
      viewport.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
      viewport.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor),
      viewport.topAnchor.constraint(equalTo: turnCard.bottomAnchor),
      viewport.bottomAnchor.constraint(equalTo: bottomGuide.topAnchor),
    ]
    elements.activate()
    activeGuidanceElements = elements
  }

  func stopGuidance(
    navMap: GMSMapView, newState: NavigationReadyUIState,
    oldState: ActiveGuidanceUIState
  ) {
    activeGuidanceElements?.deactivate()
    activeGuidanceElements = nil
    navigationReadyElements?.activate()
  }

  // MARK: - Prompts

  func showPrompt(navMap: GMSMapView, newPrompt: NavigationPromptView) {
    navMap.addSubview(newPrompt)
    currentPrompt = newPrompt

    // Start with the prompt just below the bottom edge of the map view.
    let bottomConstraint = newPrompt.bottomAnchor.constraint(
      equalTo: navMap.bottomAnchor, constant: newPrompt.bounds.height)
    promptBottomConstraint = bottomConstraint
    NSLayoutConstraint.activate([
      bottomConstraint,
      newPrompt.leadingAnchor.constraint(
        equalTo: navMap.safeAreaLayoutGuide.leadingAnchor),
      // Keep the map content above the prompt.
      mapContentsBottomGuide.bottomAnchor.constraint(
        lessThanOrEqualTo: newPrompt.topAnchor),
    ])

    // Lay out the prompt in its starting position, and then slide it up.
    navMap.layoutIfNeeded()
    UIView.animate(withDuration: Self.promptAnimationDuration) {
      bottomConstraint.constant = 0
      navMap.layoutIfNeeded()
    }
  }

  func hidePrompt(navMap: GMSMapView, oldPrompt: NavigationPromptView) {
    guard let bottomConstraint = promptBottomConstraint else { return }
    promptBottomConstraint = nil
    currentPrompt = nil

    // Slide the prompt down, and then remove it.
    UIView.animate(withDuration: Self.promptAnimationDuration) {
      bottomConstraint.constant = oldPrompt.bounds.height
      navMap.layoutIfNeeded()
    } completion: { _ in
      oldPrompt.removeFromSuperview()
    }
  }

  func changePrompt(
    navMap: GMSMapView, newPrompt: NavigationPromptView,
    oldPrompt: NavigationPromptView
  ) {
    // Slide the old prompt out and the new prompt in at the same time.
    hidePrompt(navMap: navMap, oldPrompt: oldPrompt)
    showPrompt(navMap: navMap, newPrompt: newPrompt)
  }
}

หากต้องการใช้ผู้รับมอบสิทธิ์ ให้ตั้งค่าในมุมมองแผนที่ก่อนเปิดใช้การนำทางตามที่อธิบายไว้ในรายการตรวจสอบที่จำเป็น

ข้อจำกัดและปัญหาที่ทราบ

การปรับแต่งเลย์เอาต์ไม่มีผลกับ CarPlay ซึ่งใช้เทมเพลตเลย์เอาต์แบบคงที่ หากคุณเรียกใช้ setLayoutDelegate(_:) ในมุมมองแผนที่ที่สร้างขึ้นสำหรับหน้าจอ CarPlay เมธอดจะแสดงข้อผิดพลาด

สำหรับปัญหาที่ส่งผลต่อ SDK บางเวอร์ชัน โปรดดูปัญหาที่ทราบในบันทึกประจำรุ่น