Bật tính năng chỉ đường cho CarPlay

Phần này mô tả cách bạn có thể sử dụng SDK Điều hướng với thư viện Apple CarPlay để hiển thị trải nghiệm chỉ đường của ứng dụng trên đầu phát trung tâm tích hợp. Nếu hệ thống bảng điều khiển điện tử của người lái hỗ trợ CarPlay, thì người lái xe có thể dùng ứng dụng của bạn ngay trên màn hình ô tô bằng cách kết nối điện thoại với thiết bị. Tính năng hướng dẫn bằng giọng nói cũng hoạt động trên loa của ô tô.

Bạn tạo ứng dụng CarPlay từ một bộ mẫu giao diện người dùng do Apple cung cấp. Ứng dụng của bạn chịu trách nhiệm chọn mẫu để hiển thị và cung cấp dữ liệu bên trong mẫu đó.

Hệ thống trang tổng quan hiển thị các thành phần tương tác đã được phê duyệt an toàn để người lái xe có thể di chuyển an toàn đến điểm đến mà không bị phân tâm quá mức. Bạn cũng có thể lập trình ứng dụng để người lái xe có thể tương tác với các tính năng dành riêng cho ứng dụng, chẳng hạn như chấp nhận hoặc từ chối đơn đặt hàng hoặc xem vị trí của khách hàng trên bản đồ. Thông tin cập nhật về trạng thái đơn đặt hàng cũng có thể được lập trình để xuất hiện trên đơn vị trong trang tổng quan.

Màn hình chỉ đường trên CarPlay và điện thoại
Hình ảnh bên trái cho thấy một ví dụ về màn hình chỉ đường CarPlay. Hình ảnh bên phải cho thấy cách di chuyển tương tự như trên điện thoại.

Thiết lập

Khởi động bằng CarPlay

Trước tiên, hãy làm quen với tài liệu của Apple:

  • https://developer.apple.com/carplay/
  • https://developer.apple.com/carplay/documentation/CarPlay-App-Programming-Guide.pdf

Thiết lập SDK điều hướng

  1. Sau khi đọc tài liệu của Apple, bạn đã sẵn sàng làm việc với SDK điều hướng.
  2. Thiết lập dự án nếu bạn chưa tích hợp SDK Điều hướng vào ứng dụng.
  3. Bật nguồn cấp dữ liệu hướng dẫn TurnByTurn cho ứng dụng của bạn.
  4. Không bắt buộc. Sử dụng các biểu tượng được tạo từ SDK điều hướng.
  5. Vẽ bản đồ bằng cách sử dụng lớp GMSMapView được cung cấp trong lớp UIView. Hãy xem phần Điều hướng một tuyến đường để biết thêm thông tin. Điền dữ liệu từ thư việnTurnByTurn vào CPNavigationSession.

Vẽ bản đồ và giao diện người dùng chỉ đường

Lớp GMSMapView sẽ kết xuất một bản đồ và CPMapTemplate sẽ kết xuất giao diện người dùng trên màn hình CarPlay. Lớp này cung cấp nhiều chức năng tương tự như GMSMapView dành cho điện thoại, nhưng có khả năng tương tác hạn chế.

Swift

init(window: CPWindow) {
    super.init(nibName: nil, bundle: nil)
    self.window = window

    // More CPMapTemplate initialization

}

override func viewDidLoad() {
    super.viewDidLoad()

    let mapViewOptions = GMSMapViewOptions()
    mapViewOptions.screen = window.screen
    mapViewOptions.frame = self.view.bounds

    mapView = GMSMapView(options: mapViewOptions)
    mapView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
    mapView.settings.isNavigationHeaderEnabled = false
    mapView.settings.isNavigationFooterEnabled = false

    // Disable buttons: in CarPlay, no part of the map is clickable.
    // The app should instead place these buttons in the appropriate slots of the CarPlay template.
    mapView.settings.compassButton = false
    mapView.settings.isRecenterButtonEnabled = false
    mapView.shouldDisplaySpeedometer = false
    mapView.isMyLocationEnabled = true

    self.view.addSubview(mapView)
}

Objective-C

- (instancetype)initWithWindow:(CPWindow *)window {
  self = [super initWithNibName:nil bundle:nil];
  if (self) {
    _window = window;

  // More CPMapTemplate initialization
  }
}

- (void)viewDidLoad {
  [super viewDidLoad];
  GMSMapViewOptions *options = [[GMSMapViewOptions alloc] init];
  options.screen = _window.screen;
  options.frame = self.view.bounds;
  _mapView = [[GMSMapView alloc] initWithOptions:options];
  _mapView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
  _mapView.settings.navigationHeaderEnabled = NO;
  _mapView.settings.navigationFooterEnabled = NO;

  // Disable buttons: in CarPlay, no part of the map is clickable.
  // The app should instead place these buttons in the appropriate slots of the CarPlay template.
  _mapView.settings.compassButton = NO;
  _mapView.settings.recenterButtonEnabled = NO;

  _mapView.shouldDisplaySpeedometer = NO;
  _mapView.myLocationEnabled = YES;

  [self.view addSubview:_mapView];
}

Bật tương tác bản đồ

Để đảm bảo an toàn cho người lái xe, CarPlay giới hạn hoạt động tương tác với bề mặt màn hình ở một loạt phương thức CPMapTemplateDelegate. Hãy sử dụng các lệnh gọi lại này để hỗ trợ hạn chế tương tác của người lái với bản đồ trên màn hình tích hợp.

Để hỗ trợ các thao tác khác của người dùng, hãy tạo một mảng CPMapButton và gán mảng đó cho CPMapTemplate.mapButtons.

Mã sau đây tạo ra các tương tác kéo và các nút để kéo, phóng to và thu nhỏ, cũng như để cung cấp vị trí của người dùng.

Lia máy tương tác

Swift

// MARK: CPMapTemplateDelegate
func mapTemplate(_ mapTemplate: CPMapTemplate, panBeganWith direction: CPMapTemplate.PanDirection) {

}

func mapTemplate(_ mapTemplate: CPMapTemplate, panWith direction: CPMapTemplate.PanDirection) {
    let scrollAmount = scrollAmount(for: direction)
    let scroll = GMSCameraUpdate.scrollBy(x: scrollAmount.x, y: scrollAmount.y)
    mapView.animate(with: scroll)
}

func mapTemplate(_ mapTemplate: CPMapTemplate, panEndedWith direction: CPMapTemplate.PanDirection) {
}

func scrollAmount(for panDirection: CPMapTemplate.PanDirection) -> CGPoint {
    let scrollDistance = 80.0
    var scrollAmount = CGPoint(x: 0, y: 0)
    switch panDirection {
        case .left:
            scrollAmount.x -= scrollDistance
            break;
        case .right:
            scrollAmount.x += scrollDistance
            break;
        case .up:
            scrollAmount.y += scrollDistance
            break;
        case .down:
            scrollAmount.y -= scrollDistance
            break;
        default:
            break;
    }
    if scrollAmount.x != 0 && scrollAmount.y != 0 {
        // Adjust length if scrolling diagonally.
        scrollAmount = CGPointMake(scrollAmount.x * sqrt(1.0/2.0), scrollAmount.y * sqrt(1.0/2.0))
    }
    return scrollAmount
}

Objective-C

#pragma mark - CPMapTemplateDelegate

- (void)mapTemplate:(CPMapTemplate *)mapTemplate panBeganWithDirection:(CPPanDirection)direction {
}

- (void)mapTemplate:(CPMapTemplate *)mapTemplate panWithDirection:(CPPanDirection)direction {
CGPoint scrollAmount = [self scrollAmountForPanDirection:direction];
GMSCameraUpdate *scroll = [GMSCameraUpdate scrollByX:scrollAmount.x Y:scrollAmount.y];
[_mapView animateWithCameraUpdate:scroll];
}

- (void)mapTemplate:(CPMapTemplate *)mapTemplate panEndedWithDirection:(CPPanDirection)direction {
}
- (CGPoint)scrollAmountForPanDirection:(CPPanDirection)direction {
  static const CGFloat scrollDistance = 80.;
  CGPoint scrollAmount = {0., 0.};
  if (direction & CPPanDirectionLeft) {
    scrollAmount.x = -scrollDistance;
  }
  if (direction & CPPanDirectionRight) {
    scrollAmount.x = scrollDistance;
  }
  if (direction & CPPanDirectionUp) {
    scrollAmount.y = -scrollDistance;
  }
  if (direction & CPPanDirectionDown) {
    scrollAmount.y = scrollDistance;
  }
  if (scrollAmount.x != 0 && scrollAmount.y != 0) {
  // Adjust length if scrolling diagonally.
  scrollAmount =
    CGPointMake(scrollAmount.x * (CGFloat)M_SQRT1_2, scrollAmount.y * (CGFloat)M_SQRT1_2);
  }
  return scrollAmount;
}

Cách sử dụng nút phổ biến

Swift

// MARK: Create Buttons

func createMapButtons() -> [CPMapButton] {
    let panButton = mapButton(systemImageName: "dpad.fill") { [weak self] in
        self?.didTapPanButton()
    }

    let zoomOutButton = mapButton(systemImageName: "minus.magnifyingglass") { [weak self] in
        self?.didTapZoomOutButton()
    }

    let zoomInButton = mapButton(systemImageName: "plus.magnifyingglass") { [weak self] in
        self?.didTapZoomInButton()
    }

    let myLocationButton = mapButton(systemImageName: "location") { [weak self] in
        self?.didTapMyLocationButton()
    }

    let mapButtons = [panButton, zoomOutButton, zoomInButton, myLocationButton]
    return mapButtons
}

func mapButton(systemImageName: String, handler: @escaping () -> Void) -> CPMapButton {

}


// MARK: Button callbacks

@objc func didTapPanButton() {
    mapTemplate?.showPanningInterface(animated: true)
}

@objc func didTapZoomOutButton() {
    mapView.animate(with: GMSCameraUpdate.zoomOut())
}

@objc func didTapZoomInButton() {
    mapView.animate(with: GMSCameraUpdate.zoomIn())
}

@objc func didTapMyLocationButton() {
    if let lastLocation = lastLocation {
        let cameraPosition = GMSCameraPosition(target: lastLocation.coordinate, zoom: 15)
        mapView.animate(to: cameraPosition)
    }
}

Objective-C

#pragma mark - Create Buttons

- (NSArray<CPMapButton *>*)createMapButtons {
    NSMutableArray<CPMapButton *> *mapButtons = [NSMutableArray<CPMapButton *> array];

    __weak __typeof__(self) weakSelf = self;
    CPMapButton *panButton = [self mapButtonWithSystemImageNamed:@"dpad.fill"
                                                        handler:^(CPMapButton *_) {
                                                        [weakSelf didTapPanButton];
                                                        }];
    [mapButtons addObject:panButton];

    CPMapButton *zoomOutButton =
        [self mapButtonWithSystemImageNamed:@"minus.magnifyingglass"
                                    handler:^(CPMapButton *_Nonnull mapButon) {
                                    [weakSelf didTapZoomOutButton];
                                    }];
    [mapButtons addObject:zoomOutButton];

    CPMapButton *zoomInButton =
        [self mapButtonWithSystemImageNamed:@"plus.magnifyingglass"
                                    handler:^(CPMapButton *_Nonnull mapButon) {
                                    [weakSelf didTapZoomInButton];
                                    }];
    [mapButtons addObject:zoomInButton];

    CPMapButton *myLocationButton =
        [self mapButtonWithSystemImageNamed:@"location"
                                    handler:^(CPMapButton *_Nonnull mapButton) {
                                    [weakSelf didTapMyLocationButton];
                                    }];
    [mapButtons addObject:myLocationButton];
    return mapButtons;
}

#pragma mark - Button Callbacks

- (void)didTapZoomOutButton {
[_mapView animateWithCameraUpdate:[GMSCameraUpdate zoomOut]];
}

- (void)didTapZoomInButton {
[_mapView animateWithCameraUpdate:[GMSCameraUpdate zoomIn]];
}

- (void)didTapMyLocationButton {
CLLocation *location = self.lastLocation;
if (location) {
    GMSCameraPosition *position =
        [[GMSCameraPosition alloc] initWithTarget:self.lastLocation.coordinate zoom:15.];
    [_mapView animateToCameraPosition:position];
}
}

- (void)didTapPanButton {
[_mapTemplate showPanningInterfaceAnimated:YES];
_isPanningInterfaceEnabled = YES;
}

- (void)didTapStopPanningButton {
[_mapTemplate dismissPanningInterfaceAnimated:YES];
_isPanningInterfaceEnabled = NO;
}

Lưu ý: Bạn không thể chọn các tuyến đường thay thế trên màn hình CarPlay. Bạn phải chọn các ứng dụng này trên điện thoại trước khi CarPlay bắt đầu chạy.

Hiện thông tin đường đi

Phần này trình bày cách thiết lập trình nghe cho nguồn cấp dữ liệu và cách điền sẵn thông tin đường đi trong bảng hướng dẫn và bảng ước tính chuyến đi. Xem phần "Tạo ứng dụng chỉ đường CarPlay" trong Hướng dẫn lập trình ứng dụng CarPlay để biết thêm thông tin.

Bảng điều khiển hướng dẫn và ước tính chuyến đi cung cấp một thẻ điều hướng hiển thị thông tin về lộ trình di chuyển liên quan đến chuyến đi hiện tại. Thư viện rẽByTurn trong SDK điều hướng có thể giúp cung cấp một số thông tin này, chẳng hạn như biểu tượng, văn bản và thời gian còn lại.

Thiết lập trình nghe

Làm theo các hướng dẫn để thiết lập trình nghe sự kiện trong bài viết Bật nguồn cấp dữ liệu từng chặng.

Điền thông tin về lộ trình di chuyển

Phần đầu tiên của mã mẫu sau đây cho biết cách tạo số liệu ước tính về hành trình trên CarPlay bằng cách dịch GMSNavigationNavInfo.timeToCurrentStepSeconds sang CPTravelEstimate. Bạn có thể đọc thêm về các thành phần này và các thành phần hiển thị khác trong bài viết Bật nguồn cấp dữ liệu từng chặng.

Phần thứ hai của mẫu cho biết cách tạo một đối tượng và lưu trữ đối tượng đó trong trường userInfo của CPManuevers. Thao tác này sẽ xác định CPManeuverDisplayStyle, cũng dùng để cung cấp thông tin hướng dẫn làn đường. Xem Hướng dẫn lập trình ứng dụng CarPlay của Apple để biết thêm thông tin.

Swift

// Get a CPTravelEstimate from GMSNavigationNavInfo
func getTravelEstimates(from navInfo:GMSNavigationNavInfo) -> CPTravelEstimates {
    let distanceRemaining = navInfo.roundedDistance(navInfo.distanceToCurrentStepMeters)
    let timeRemaining = navInfo.roundedTime(navInfo.timeToCurrentStepSeconds)
    let travelEstimates = CPTravelEstimates(distanceRemaining: distanceRemaining, timeRemaining: timeRemaining)
    return travelEstimates
}

//  Create an object to be stored in the userInfo field of CPManeuver to determine the CPManeuverDisplayStyle. 

/** An object to be stored in the userInfo field of a CPManeuver. */

struct ManeuverUserInfo {
    var stepInfo: GMSNavigationStepInfo
    var isLaneGuidance: Bool
}

func mapTemplate(_ mapTemplate: CPMapTemplate, displayStyleFor maneuver: CPManeuver) -> CPManeuverDisplayStyle {
    let userInfo = maneuver.userInfo
    if let maneuverUserInfo = userInfo as? ManeuverUserInfo {
        return maneuverUserInfo.isLaneGuidance ? .symbolOnly : .leadingSymbol
    }
    return .leadingSymbol
}

// Get a CPManeuver with instructionVariants and symbolImage from GMSNavigationStepInfo
func getManeuver(for stepInfo: GMSNavigationStepInfo) -> CPManeuver {
    let maneuver = CPManeuver()
    maneuver.userInfo = ManeuverUserInfo(stepInfo: stepInfo, isLaneGuidance: false)
    switch stepInfo.maneuver {
        case .destination:
            maneuver.instructionVariants = ["Your destination is ahead."]
            break
        case .destinationLeft:
            maneuver.instructionVariants = ["Your destination is ahead on your left."]
            break
        case .destinationRight:
            maneuver.instructionVariants = ["Your destination is ahead on your right."]
            break
        default:
            maneuver.attributedInstructionVariants = currentNavInfo?.instructions(forStep: stepInfo, options: instructionOptions)
            break
    }
    maneuver.symbolImage = stepInfo.maneuverImage(with: instructionOptions.imageOptions)
    return maneuver
}

// Get the lane image for a CPManeuver from GMSNavigationStepInfo
func laneGuidanceManeuver(for stepInfo: GMSNavigationStepInfo) -> CPManeuver? {
    let maneuver = CPManeuver()
    maneuver.userInfo = ManeuverUserInfo(stepInfo: stepInfo, isLaneGuidance: true)
    let lanesImage = stepInfo.lanesImage(with: imageOptions)
    guard let lanesImage = lanesImage else { return nil }
    maneuver.symbolImage = lanesImage
    return maneuver
}

Objective-C

// Get a CPTravelEstimate from GMSNavigationNavInfo
- (nonull CPTravelEstimates *)travelEstimates:(GMSNavigationNavInfo *_Nonnull navInfo) {
NSMeasurement<NSUnitLength *> *distanceRemaining = [navInfo roundedDistance:navInfo.distanceToCurrentStepMeters];
NSTimeInterval timeRemaining = [navInfo roundedTime:navInfo.timeToCurrentStepSeconds];
CPTravelEstimate* travelEstimate = [[CPTravelEstimates alloc] initWithDistanceRemaining:distanceRemaining
                                                timeRemaining:timeRemaining];
}
//  Create an object to be stored in the userInfo field of CPManeuver to determine the CPManeuverDisplayStyle. 

/** An object to be stored in the userInfo field of a CPManeuver. */
@interface ManeuverUserInfo : NSObject

@property(nonatomic, readonly, nonnull) GMSNavigationStepInfo *stepInfo;
@property(nonatomic, readonly, getter=isLaneGuidance) BOOL laneGuidance;

- (nonnull instancetype)initWithStepInfo:(GMSNavigationStepInfo *)stepInfo
                        isLaneGuidance:(BOOL)isLaneGuidance NS_DESIGNATED_INITIALIZER;

- (instancetype)init NS_UNAVAILABLE;

@end

- (CPManeuverDisplayStyle)mapTemplate:(CPMapTemplate *)mapTemplate
            displayStyleForManeuver:(nonnull CPManeuver *)maneuver {
ManeuverUserInfo *userInfo = maneuver.userInfo;
return userInfo.laneGuidance ? CPManeuverDisplayStyleSymbolOnly : CPManeuverDisplayStyleDefault;
}
// Get a CPManeuver with instructionVariants and symbolImage from GMSNavigationStepInfo
- (nonnull CPManeuver *)maneuverForStep:(nonnull GMSNavigationStepInfo *)stepInfo {
CPManeuver *maneuver = [[CPManeuver alloc] init];
maneuver.userInfo = [[ManeuverUserInfo alloc] initWithStepInfo:stepInfo isLaneGuidance:NO];
switch (stepInfo.maneuver) {
    case GMSNavigationManeuverDestination:
    maneuver.instructionVariants = @[ @"Your destination is ahead." ];
    break;
    case GMSNavigationManeuverDestinationLeft:
    maneuver.instructionVariants = @[ @"Your destination is ahead on your left." ];
    break;
    case GMSNavigationManeuverDestinationRight:
    maneuver.instructionVariants = @[ @"Your destination is ahead on your right." ];
    break;
    default: {
    maneuver.attributedInstructionVariants =
        [_currentNavInfo instructionsForStep:stepInfo options:_instructionOptions];
    break;
    }
}
maneuver.symbolImage = [stepInfo maneuverImageWithOptions:_instructionOptions.imageOptions];
return maneuver;
}
// Get the lane image for a CPManeuver from GMSNavigationStepInfo
- (nullable CPManeuver *)laneGuidanceManeuverForStep:(nonnull GMSNavigationStepInfo *)stepInfo {
CPManeuver *maneuver = [[CPManeuver alloc] init];
maneuver.userInfo = [[ManeuverUserInfo alloc] initWithStepInfo:stepInfo isLaneGuidance:YES];
UIImage *lanesImage = [stepInfo lanesImageWithOptions:_imageOptions];
if (!lanesImage) {
    return nil;
}
maneuver.symbolImage = lanesImage;
return maneuver;
}

Thao tác

CarPlay sử dụng lớp CPManeuver để cung cấp hướng dẫn từng chặng. Hãy xem bài viết Bật nguồn cấp dữ liệu từng chặng để biết thêm thông tin về việc điều khiển và hướng dẫn làn đường.