Tuỳ chỉnh bố cục điều hướng

Ảnh động minh hoạ bố cục điều hướng tuỳ chỉnh trên iOS

Lưu ý: Tài liệu này sử dụng quy ước đặt tên Swift cho các lớp, giao thức và API.

Sử dụng tính năng tuỳ chỉnh bố cục để kiểm soát hệ phân cấp view và vị trí màn hình cho cả khung hiển thị do Google cung cấp và khung hiển thị tuỳ chỉnh của riêng bạn.

Thay vì thêm các khung hiển thị tuỳ chỉnh vào các vùng bố cục được xác định trước, hãy triển khai một trình uỷ quyền bố cục để quản lý bố cục cho toàn bộ màn hình. Trong quá trình chuyển đổi trạng thái điều hướng, trình uỷ quyền bố cục sẽ nhận được các thành phần do Google cung cấp, chẳng hạn như thẻ rẽ, thẻ ETA và các nút. Hãy đặt các phần tử này cùng với nội dung tuỳ chỉnh của riêng bạn bằng cách sử dụng các quy tắc ràng buộc về Bố cục tự động chuẩn của iOS.

Hãy sử dụng khung này để định vị thông tin doanh nghiệp tuỳ chỉnh (chẳng hạn như trạng thái đơn đặt hàng hoặc hướng dẫn đến lấy hàng) một cách chính xác trên màn hình, đồng thời ngăn các khung hiển thị chồng chéo lên nhau.

Cách hoạt động của tính năng tuỳ chỉnh bố cục

Tính năng tuỳ chỉnh bố cục sử dụng mẫu thiết kế uỷ quyền. Thay vì tự động vẽ hoặc định vị các thành phần giao diện người dùng trên màn hình, SDK sẽ truyền trực tiếp các thành phần đó đến một lớp tuỳ chỉnh mà bạn viết: trình uỷ quyền bố cục.

Để tuỳ chỉnh bố cục, hãy tạo một lớp phù hợp với giao thức NavigationLayoutDelegate và truyền một thực thể đến phương thức setLayoutDelegate(_:) của GMSMapView. Bất cứ khi nào trạng thái điều hướng chuyển đổi (chẳng hạn như chuyển từ bản đồ cơ sở sang hướng dẫn từng chặng đang hoạt động), SDK sẽ thực thi các phương thức gọi lại trên trình uỷ nhiệm và cung cấp các thành phần giao diện người dùng có sẵn cho trạng thái cụ thể đó.

Để mang lại trải nghiệm người dùng liền mạch và tích hợp, khung này thiết lập sự phân tách rõ ràng về trách nhiệm giữa ứng dụng của bạn và SDK. Sử dụng đại diện bố cục để tuỳ chỉnh những nội dung sau:

  • Xây dựng hệ phân cấp view: Chọn chính xác thành phần Google và thành phần hiển thị doanh nghiệp tuỳ chỉnh nào cần thêm vào màn hình cho từng trạng thái giao diện người dùng.
  • Đặt vị trí cho mọi phần tử: Đặt neo màn hình, lề và vị trí bố cục chính xác. Tránh áp dụng các quy tắc ràng buộc về chiều rộng hoặc chiều cao tuỳ chỉnh cho các thành phần của Google vì các thành phần của Google sẽ tự tính toán kích thước nội bộ của chúng.
  • Định khung bản đồ cơ sở: Xác định ranh giới hiển thị của camera bản đồ bằng cách sử dụng toạ độ của thành phần khung nhìn.
  • Xếp lớp màn hình: Quyết định xem chế độ xem tuỳ chỉnh sẽ nổi lên trên, xuống dưới hay bên cạnh các chế độ điều khiển tích hợp của Google.

Trong khi đó, bạn không thể tuỳ chỉnh các thành phần sau đây của Google bằng trình uỷ quyền bố cục:

  • Kích thước thành phần: Kích thước và kích thước nội bộ của các thành phần do Google cung cấp, được SDK tự động tính toán.
  • Điều kiện kích hoạt: Thời điểm cảnh báo hoặc lời nhắc linh hoạt xuất hiện dựa trên dữ liệu tuyến đường theo thời gian thực.

Nguyên tắc triển khai

Khi viết một trình uỷ quyền bố cục, hãy lưu ý các quy tắc sau để tránh lỗi bố cục hoặc sự cố trong thời gian chạy:

  • Màn hình mặc định là màn hình trống: Các thành phần do Google cung cấp chỉ xuất hiện nếu đối tượng uỷ quyền của bạn thêm chúng vào hệ phân cấp view và định vị chúng một cách rõ ràng.
  • Không hỗ trợ các API cũ: Các thuộc tính và phương thức kiểm soát mô hình bố cục dựa trên vùng quảng cáo cũ sẽ không được hỗ trợ và có thể không hoạt động như mong đợi khi một trình uỷ quyền tuỳ chỉnh đang hoạt động.
  • Không sửa đổi cấu trúc khung hiển thị nội bộ: Không duyệt qua hoặc sửa đổi hệ phân cấp view của các thành phần do Google cung cấp (chẳng hạn như thẻ rẽ hoặc thẻ ETA). Vì các hệ phân cấp view nội bộ này là thông tin triển khai cơ bản, nên chúng có thể thay đổi tuỳ theo bản phát hành SDK. Việc sửa đổi các tệp này có thể khiến bố cục của bạn bị hỏng trong các bản cập nhật SDK sau này.

Khả năng tương thích với API cũ

Để đảm bảo hành vi bố cục đáng tin cậy khi sử dụng một trình uỷ nhiệm bố cục tuỳ chỉnh, hãy tránh sử dụng các API dựa trên vùng quảng cáo cũ sau đây, vì các API này dự kiến sẽ ngừng hoạt động. Để di chuyển một ứng dụng hiện có, hãy thay thế cách sử dụng các API này bằng mã trong trình uỷ quyền bố cục tuỳ chỉnh:

Hiện các API dựa trên vùng quảng cáo cũ

API cũ Thay thế Layout Delegate
isNavigationHeaderEnabled Đọc chế độ xem turnCard trong startGuidance(navMap:newState:oldState:).
isNavigationFooterEnabled Đọc chế độ xem etaCard trong startGuidance(navMap:newState:oldState:).
isNavigationTripProgressBarEnabled Đọc chế độ xem tripProgressBar trong startGuidance(navMap:newState:oldState:).
isNavigationReportIncidentButtonEnabled Tìm nút .reporting trong mảng buttons.
setHeaderAccessoryView(_:) và invalidateLayout(forAccessoryView:) Thêm thành phần hiển thị của riêng bạn vào bố cục và liên kết thành phần hiển thị đó với turnCard.
padding và paddingAdjustmentBehavior Đặt chế độ xem viewport để lấy khung hình cho camera.
navigationHeaderLayoutGuide Neo các thành phần hiển thị tuỳ chỉnh ngay vào turnCard.
navigationFooterLayoutGuide Neo các thành phần hiển thị tuỳ chỉnh ngay vào etaCard.
bottomTrailingButtonsLayoutGuide Neo các nút trực tiếp vào etaCard hoặc đường căn của vùng chứa.
bottomTrailingButtonsAlternateLayoutGuide Neo trực tiếp các nút vào nút .reporting hoặc các quy tắc ràng buộc wideMode.
navigatorWillPresentPrompt(_:) và navigatorDidDismissPrompt(_:) Dựa vào lệnh gọi lại lời nhắc NavigationLayoutDelegate, chẳng hạn như showPrompt(navMap:newPrompt:).

Khi bạn đặt một trình uỷ quyền bố cục tuỳ chỉnh, các hướng dẫn bố cục cũ (chẳng hạn như navigationHeaderLayoutGuide hoặc bottomTrailingButtonsLayoutGuide) và các phương thức chế độ xem phụ kiện tiêu đề sẽ không có hiệu lực. Thay vào đó, hãy liên kết các thành phần hiển thị của bạn với các thành phần hiển thị do Google cung cấp.

Thuộc tính compassButton của GMSUISettings vẫn điều khiển la bàn trong khi giao diện người dùng điều hướng bị tắt. Khi bật tính năng chỉ đường, trình uỷ quyền sẽ quyết định xem có hiển thị la bàn từ mảng buttons hay không.

Danh sách kiểm tra về các yếu tố thiết yếu

Hãy làm theo các bước và yêu cầu thiết yếu sau để triển khai thành công trình uỷ quyền bố cục:

  • Đặt đối tượng uỷ quyền trước khi bật tính năng chỉ đường: Gọi setLayoutDelegate(_:) trên GMSMapView trước khi bạn bật tính năng chỉ đường bằng isNavigationEnabled hoặc enableNavigation(with:). Phương thức này sẽ gửi một lỗi nếu tính năng đi theo chỉ dẫn đã được bật hoặc nếu bạn đã tạo chế độ xem bản đồ cho màn hình CarPlay. Chế độ xem bản đồ giữ một tệp tham chiếu rõ ràng đến đối tượng uỷ quyền của bạn. Để khôi phục bố cục tích hợp, hãy truyền nil trong khi tính năng điều hướng bị tắt.

    // 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
        
  • Đính kèm các thành phần hiển thị bắt buộc: Bạn phải thêm biểu trưng Google (googleLogo) và Khung nhìn (viewport) vào vùng chứa bố cục ở mọi trạng thái điều hướng.

  • Tuân thủ kích thước cố định: SDK xác định kích thước cho một số thành phần do Google cung cấp. Đừng áp dụng các quy tắc ràng buộc về chiều rộng hoặc chiều cao tuỳ chỉnh cho các phần tử sau:

    • Thẻ lật
    • Thẻ ETA
    • Lời nhắc đến
    • Biểu trưng Google
    • Tiện ích tốc độ
  • Thêm các phần tử dưới dạng khung hiển thị phụ: Thêm các khung hiển thị do Google cung cấp trực tiếp dưới dạng khung hiển thị phụ vào khung hiển thị vùng chứa tuỳ chỉnh hoặc vào chính GMSMapView.

  • Căn chỉnh bằng hướng dẫn bố cục: Các thành phần hiển thị do Google cung cấp bao gồm lề trực quan tích hợp sẵn. Để căn chỉnh các thành phần tuỳ chỉnh sao cho khớp hoàn toàn với các thành phần do Google cung cấp, hãy ràng buộc các thành phần hiển thị của bạn với thuộc tính layoutMarginsGuide của thành phần hiển thị của Google.

  • Dọn dẹp khi thoát trạng thái: Trong stopGuidance(navMap:newState:oldState:), hãy xoá các thành phần hiển thị hướng dẫn đang hoạt động và huỷ kích hoạt các quy tắc ràng buộc của chúng trước khi bạn khôi phục bố cục sẵn sàng điều hướng. Trong disableNavigationUI(navMap:oldState:), hãy xoá tất cả thành phần hiển thị và hướng dẫn bố cục mà bạn đã thêm, bao gồm cả mọi lời nhắc trên màn hình. Điều này giúp ngăn các khung hiển thị từ trạng thái này chồng chéo lên trạng thái tiếp theo.

  • Hỗ trợ tiếp cận và hỗ trợ từ phải sang trái: Các khung hiển thị do Google cung cấp đáp ứng các tiêu chuẩn hỗ trợ tiếp cận, nhưng người được uỷ quyền của bạn chịu trách nhiệm về khả năng hỗ trợ tiếp cận của bố cục tổng thể. Để hỗ trợ các ngôn ngữ đọc từ phải sang trái, hãy sử dụng neo đầu và neo cuối thay vì neo trái và neo phải.

  • Tránh các API bố cục cũ: Đừng sử dụng các thuộc tính sửa đổi bố cục cũ trên GMSUISettings hoặc các hướng dẫn bố cục cũ trên GMSMapView khi một trình uỷ quyền tuỳ chỉnh đang hoạt động. SDK có thể bỏ qua các cuộc gọi dùng tính năng cũ này khi một trình uỷ quyền tuỳ chỉnh được đính kèm.

Trạng thái giao diện người dùng và thành phần của Google

Khi quá trình chuyển đổi trạng thái điều hướng xảy ra, SDK sẽ truyền một đối tượng NavigationUIState chỉ có thể đọc đến lệnh gọi lại của thực thể đại diện. Đối tượng này gói các cờ cấu hình bố cục hiện tại với các thành phần của Google (ví dụ: các thực thể UIView) mà bạn cần để kết xuất màn hình.

Trình uỷ quyền bố cục quản lý hệ phân cấp view trên 4 trạng thái hoạt động. Sơ đồ trạng thái sau đây minh hoạ cách SDK chuyển đổi giữa các trạng thái điều hướng và phương thức gọi lại mà SDK thực thi trên trình uỷ quyền của bạn:

Sơ đồ chuyển đổi trạng thái vòng đời của NavigationLayoutDelegate cho thấy các chuyển đổi giữa các trạng thái Không điều hướng, Sẵn sàng điều hướng và Chỉ dẫn đang hoạt động.

Các thành phần cụ thể của Google được cung cấp cho đối tượng uỷ quyền của bạn phụ thuộc vào giai đoạn hiện tại của vòng đời điều hướng.

Thành phần bắt buộc (tất cả trạng thái điều hướng)

Bất kể giai đoạn điều hướng hiện tại là gì, bạn phải đưa vào, định vị và giữ cho các thành phần sau đây của Google hiển thị trong hệ phân cấp view:

  • Biểu trưng Google (googleLogo): Thành phần này hiển thị biểu trưng bắt buộc của Google Maps. Nếu nút Căn giữa lại được bật, thì biểu trưng sẽ tự động chuyển thành nút này khi người lái xe di chuyển bản đồ ra xa khỏi xe của họ. Do đó, vị trí đặt biểu trưng sẽ quyết định vị trí xuất hiện của nút Căn giữa lại. Bạn nên đặt biểu trưng ở góc dưới cùng bên trái của bố cục để phù hợp với giao diện người dùng bản đồ chuẩn.

  • Khung nhìn (viewport): Một UIView vô hình xác định ranh giới khung hình của camera. Vị trí của khung nhìn sẽ quyết định chính xác vị trí SDK đặt chữ V của xe ở giữa và vẽ đường đi đang hoạt động. Đặt khung nhìn sao cho khung nhìn bao phủ khu vực mở, không bị che khuất trên màn hình, tránh các lớp phủ mờ như bảng dưới cùng tuỳ chỉnh một cách an toàn.

Trong trạng thái sẵn sàng chỉ đường, giao diện người dùng vẫn ở mức tối giản để tập trung vào bản đồ cơ sở. Đối tượng NavigationReadyUIState của bạn cung cấp quyền truy cập vào những thông tin sau:

  • buttons: Một mảng các thành phần hiển thị hành động nổi được định cấu hình cho bản đồ cơ sở (thường chỉ là nút La bàn). Vì Google có thể thêm hoặc sắp xếp lại các nút trong các bản phát hành SDK sau này, nên hãy tránh dựa vào các chỉ mục mảng cố định. Thay vào đó, hãy lặp lại mảng, đọc loại của từng nút bằng cách so sánh thuộc tính type của nút đó với NavigationButtonKnownType.compass và trích xuất khung hiển thị thực tế bằng cách gọi thuộc tính view của nút đó. Xin lưu ý rằng các nút chính xác trong mảng này khác với các nút có sẵn trong quá trình hướng dẫn chủ động.

Thành phần hướng dẫn đang hoạt động

Khi bắt đầu hướng dẫn từng chặng, SDK sẽ mở khoá toàn bộ bộ điều khiển điều hướng. Đối tượng ActiveGuidanceUIState của bạn cung cấp quyền truy cập vào các thành phần sau đây của Google:

  • turnCard: Biểu ngữ tiêu đề chính hiển thị thông tin chỉ đường, số đo khoảng cách và hướng dẫn làn đường sắp tới. Đặt thanh này ở đầu bố cục để thiết lập hệ thống phân cấp điều hướng quen thuộc và cố định các chế độ xem tuỳ chỉnh xung quanh thanh này.

  • etaCard: Biểu ngữ chân trang hiển thị thời gian đến dự kiến, thời gian còn lại của hành trình và khoảng cách còn lại đến đích. Đặt thanh này dọc theo cạnh dưới của màn hình hoặc tích hợp toạ độ của thanh này vào trang tính quản lý công việc tuỳ chỉnh của bạn.

  • tripProgressBar: Thanh tiến trình dọc cho người lái xe biết quãng đường họ đã đi dọc theo tuyến đường hiện tại. Không giống như bố cục cũ cố định chặt chẽ vào cạnh bản đồ, bạn có toàn quyền neo bố cục này ở bất cứ đâu, chẳng hạn như dọc theo cạnh bên của vùng chứa tuỳ chỉnh.

  • speedWidget: Một thành phần điều khiển nổi cho đồng hồ tốc độ và giới hạn tốc độ được đăng. Tuỳ thuộc vào chế độ cài đặt API (các thuộc tính shouldDisplaySpeedometer và shouldDisplaySpeedLimit của GMSMapView) và khả năng cung cấp dữ liệu, chế độ xem này sẽ tự động đổi kích thước giữa 4 trạng thái hình ảnh trong thời gian chạy (không hiển thị gì, chỉ hiển thị tốc độ hiện tại, chỉ hiển thị giới hạn tốc độ hoặc hiển thị cả hai thông số). Vì tiện ích có thể chuyển đổi giữa các kích thước này mà không cần thông báo, nên hãy luôn cố định các khung hiển thị xung quanh bằng các quy tắc ràng buộc để bố cục của bạn tự động điều chỉnh nhằm tránh chồng chéo không gian.

  • buttons: Một mảng mở rộng gồm các chế độ xem hành động nổi cho trạng thái hướng dẫn đang hoạt động (thường bao gồm cả nút La bàn và nút Báo cáo sự cố). Tương tự như trong trạng thái sẵn sàng điều hướng, bạn có thể tìm và trích xuất từng thành phần hiển thị nút bằng cách lọc mảng theo type (.compass hoặc .reporting) và trích xuất thành phần hiển thị thực tế bằng thuộc tính view. Sau đó, bạn có thể đặt vị trí cho các thành phần này một cách độc lập hoặc thêm chúng vào một GMSNavigationHidingStackView. Chế độ xem ngăn xếp này sắp xếp các nút theo chiều dọc và khi hết không gian, chế độ xem này sẽ ẩn các nút bắt đầu từ cuối mảng arrangedSubviews. Đừng đặt isHidden trên các thành phần hiển thị mà nó quản lý.
Mẹo: Phương pháp hay nhất để lấy khung hình camera. Chế độ xem của thẻ rẽ có chiều cao cố định dựa trên NavigationStyleValues.headerNominalHeight, đủ cao cho mọi nội dung của thẻ rẽ. Để giữ cho khung hình của camera ổn định, hãy ràng buộc phần trên cùng của viewport trực tiếp với cạnh dưới của turnCard, như trong ví dụ về cách triển khai.

thành phần câu lệnh linh hoạt

Lời nhắc, chẳng hạn như cảnh báo sự cố hoặc cảnh báo camera an toàn, sẽ kích hoạt độc lập trong quá trình hướng dẫn đang hoạt động.

Khi lời nhắc đã sẵn sàng hiển thị, SDK sẽ gọi lệnh gọi lại showPrompt(navMap:newPrompt:) của đối tượng uỷ quyền và truyền trực tiếp một khung hiển thị newPrompt. Đại diện của bạn chịu trách nhiệm định vị lời nhắc này một cách mượt mà trên bố cục (thường được neo vào cạnh dưới của vùng chứa bản đồ).

Vì lời nhắc sắp tới sẽ che phần dưới của màn hình, nên bạn phải cập nhật bố cục để ngăn lời nhắc chồng lên khung nhìn, biểu trưng Google hoặc bất kỳ nút nào được căn chỉnh ở dưới cùng.

Đại diện của bạn cũng kiểm soát cách lời nhắc xuất hiện và biến mất khỏi màn hình. Để biết thông tin chi tiết, vui lòng xem phần Tuỳ chỉnh câu lệnh.

Xử lý kích thước màn hình và chế độ rộng

Để xử lý các thay đổi về kích thước bản đồ và hướng màn hình, trình uỷ quyền bố cục sử dụng các tính năng sau:

  • Đổi kích thước thành phần hiển thị: Điều chỉnh bố cục bất cứ khi nào kích thước thực của bản đồ thay đổi.

  • Chế độ rộng: Chuyển sang biến thể bố cục định dạng rộng khi bản đồ đủ rộng.

Phản hồi khi đổi kích thước khung hiển thị

Bố cục chia đôi màn hình, thanh trượt vùng chứa và chế độ xoay thiết bị thường kích hoạt lệnh gọi lại sizeChanged(navMap:newState:). Sử dụng lệnh gọi lại này để thực hiện các điều chỉnh thích ứng chung cho giao diện người dùng tuỳ chỉnh của bạn. Triển khai sizeChanged(navMap:newState:) để định vị lại các phần tử tuỳ chỉnh cho tỷ lệ khung hình mới của màn hình, áp dụng các điểm ngắt chiều rộng hoặc chiều cao tuỳ chỉnh của riêng bạn và phát hiện thời điểm sự kiện đổi kích thước bật/tắt chế độ rộng để hoán đổi các biến thể bố cục một cách an toàn.

Tìm hiểu về chế độ rộng

Chế độ rộng sẽ kích hoạt khi vùng chứa bản đồ đủ rộng để hiển thị các thành phần giao diện người dùng cạnh nhau.

Đọc boolean wideMode từ đối tượng trạng thái để đặt lại vị trí các phần tử trên giao diện người dùng tuỳ chỉnh và giữ cho tâm bản đồ luôn rõ ràng cho tài xế. Vì các thành phần do Google cung cấp như thẻ rẽ và thẻ ETA sẽ tự động thu nhỏ và định hình lại ở chế độ rộng, nên việc đọc boolean này đảm bảo bố cục của bạn điều chỉnh chính xác tại thời điểm các thành phần của Google cập nhật.

Hãy xem các ví dụ sau đây về cách định vị các thành phần bố cục ở chế độ tiêu chuẩn và chế độ rộng:

  • Chế độ dọc tiêu chuẩn: Đặt thẻ rẽ ở đầu màn hình và thẻ ETA ở dưới cùng.

  • Chế độ rộng: Giữ thẻ rẽ và thẻ ETA dọc theo cạnh trên của màn hình, với thẻ rẽ ở trên cùng và thẻ ETA ở dưới cùng. Việc đặt một trong hai thẻ cách xa cạnh đầu sẽ dẫn đến hành vi không xác định. Bạn có thể đặt khung nhìn và nội dung của riêng mình ở bất cứ đâu; lựa chọn phổ biến là phía sau của thẻ.

Kiểm tra chế độ rộng trong quá trình chuyển đổi trạng thái

Nếu bố cục của bạn hỗ trợ biến thể chế độ rộng, hãy đánh giá boolean wideMode bên trong mỗi lệnh gọi lại chuyển đổi trạng thái, thay vì chỉ dựa vào sizeChanged(navMap:newState:).

Kiểm tra đối tượng trạng thái trong các lệnh gọi lại như enableNavigationUI(navMap:newState:) và startGuidance(navMap:newState:oldState:) để xử lý các lần khởi chạy ứng dụng ban đầu theo hướng ngang. Phương pháp này đảm bảo rằng bố cục của bạn đánh giá trạng thái chế độ rộng và kích hoạt biến thể bố cục rộng hoặc tiêu chuẩn chính xác ngay khi trạng thái điều hướng mới bắt đầu.

Căn chỉnh theo kiểu của Google

Để giúp các thành phần hiển thị tuỳ chỉnh của bạn khớp với khoảng cách của các thành phần do Google cung cấp, SDK cung cấp lớp NavigationStyleValues. Các thuộc tính lớp của nó trả về các phép đo theo điểm.

Ví dụ: để đặt một nút tuỳ chỉnh ở góc trên cùng của màn hình đối diện với thẻ rẽ, hãy sử dụng NavigationStyleValues.headerTopPadding làm lề trên của nút. Sau đó, nút này sẽ thẳng hàng với cạnh trên của thẻ rẽ.

Bạn có thể sử dụng các chỉ số đo lường sau:

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

Chế độ xem do Google cung cấp cũng bao gồm lề tích hợp. Khi bạn xếp chồng hai khung hiển thị do Google cung cấp, hãy ràng buộc khung hiển thị này với layoutMarginsGuide của khung hiển thị kia để lề của chúng không cộng dồn. Ví dụ: để đặt tiện ích tốc độ phía trên thẻ ETA, hãy ràng buộc neo dưới của speedWidget với etaCard.layoutMarginsGuide.topAnchor.

Tuỳ chỉnh câu lệnh

Đại diện bố cục kiểm soát cách lời nhắc xuất hiện và rời khỏi màn hình, cũng như góc nào của lời nhắc được bo tròn.

Câu lệnh tạo ảnh động

SDK không tạo ảnh động cho lời nhắc. Đại diện của bạn quyết định cách mỗi lời nhắc xuất hiện và biến mất:

  • Trong showPrompt(navMap:newPrompt:), hãy thêm lời nhắc vào chế độ xem bản đồ ở vị trí bắt đầu, sau đó tạo ảnh động cho lời nhắc đó vào đúng vị trí. Ví dụ: bắt đầu lời nhắc ngay bên dưới khu vực hiển thị hoặc phía sau một bảng dưới cùng có cạnh trên cùng được căn chỉnh với đầu bảng.
  • Trong hidePrompt(navMap:oldPrompt:), hãy tạo ảnh động cho lời nhắc ra khỏi khung hiển thị, sau đó xoá lời nhắc khỏi khung hiển thị cấp cao khi ảnh động kết thúc.
  • Trong changePrompt(navMap:newPrompt:oldPrompt:), hãy chạy ảnh động ẩn và hiện cùng lúc để thay thế lời nhắc này bằng lời nhắc khác.

Ví dụ về cách triển khai sử dụng phương pháp này.

Để tắt lời nhắc về tình hình giao thông, tuyến đường tốt hơn và sự cố, hãy đặt thuộc tính shouldDisplayPrompts của GMSNavigator thành false.

Bo tròn các góc của câu lệnh

Lời nhắc tuân thủ giao thức NavigationRoundedCornerView, vì vậy, bạn có thể chọn góc nào được bo tròn. Theo mặc định, lời nhắc chỉ bo tròn các góc trên cùng, phù hợp với lời nhắc được neo vào cạnh dưới của màn hình. Nếu bạn đặt lời nhắc phía trên nội dung khác, chẳng hạn như bảng dưới cùng, hãy bo tròn cả bốn góc:

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

Ví dụ: Triển khai bố cục dựa trên quy tắc ràng buộc

Ví dụ sau đây cho thấy một trình uỷ quyền bố cục cơ bản sử dụng các quy tắc ràng buộc của Bố cục tự động để định vị thẻ rẽ, thẻ ETA, các nút, biểu trưng Google và khung nhìn. Thành phần này cũng trượt lời nhắc vào và ra khỏi màn hình. Để ví dụ ngắn gọn, ví dụ này không xử lý chế độ rộng, tiện ích tốc độ hoặc thanh tiến trình của chuyến đi, vì vậy, ví dụ này phù hợp với những ứng dụng chỉ hỗ trợ hướng dọc.

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)
  }
}

Để sử dụng trình uỷ quyền, hãy đặt trình uỷ quyền đó trên chế độ xem bản đồ trước khi bật tính năng chỉ đường, như mô tả trong Danh sách kiểm tra các thông tin thiết yếu.

Các giới hạn và vấn đề đã biết

Tính năng tuỳ chỉnh bố cục không áp dụng cho CarPlay, vì CarPlay sử dụng các mẫu bố cục cố định. Nếu bạn gọi setLayoutDelegate(_:) trên một chế độ xem bản đồ mà bạn tạo cho màn hình CarPlay, thì phương thức này sẽ gửi một lỗi.

Đối với các vấn đề ảnh hưởng đến một số phiên bản SDK, hãy xem phần Vấn đề đã biết trong ghi chú phát hành.