构建导航应用

此页面详细介绍了 Car 应用库的不同功能,您可以使用这些功能来实现循迹导航应用的功能。

在清单中声明导航支持

您的导航应用需要在 CarAppService 的 intent 过滤器中声明 androidx.car.app.category.NAVIGATION 汽车应用类别

<application>
    ...
   <service
       ...
        android:name=".MyNavigationCarAppService"
        android:exported="true">
      <intent-filter>
        <action android:name="androidx.car.app.CarAppService" />
        <category android:name="androidx.car.app.category.NAVIGATION"/>
      </intent-filter>
    </service>
    ...
</application>

支持导航 Intent

要支持到您的应用的导航 Intent(包括来自 Google 助理使用语音查询的 Intent),您的应用需要在其 Session.onCreateScreenSession.onNewIntent 中处理 CarContext.ACTION_NAVIGATE Intent。

有关 Intent 格式的详细信息,请参阅有关 CarContext.startCarApp 的文档。

访问导航模板

导航应用可以访问以下模板,这些模板在背景中显示一个带有地图的界面,并在主动导航期间显示循迹方向。

  • NavigationTemplate:在主动导航期间还显示可选的信息消息和行程估算。
  • MapWithContentTemplate:一个允许应用渲染地图瓦片以及某种内容(例如列表)的模板。内容通常呈现为地图瓦片顶部的叠加层,地图可见,稳定区域会根据内容进行调整。

有关如何使用这些模板设计导航应用用户界面的更多详细信息,请参阅 导航应用

要访问导航模板,您的应用需要在其 AndroidManifest.xml 文件中声明 androidx.car.app.NAVIGATION_TEMPLATES 权限。

<manifest ...>
  ...
  <uses-permission android:name="androidx.car.app.NAVIGATION_TEMPLATES"/>
  ...
</manifest>

需要其他权限才能 绘制地图

迁移到 MapWithContentTemplate

从汽车应用 API 级别 7 开始,MapTemplatePlaceListNavigationTemplateRoutePreviewNavigationTemplate 已弃用。已弃用的模板将继续得到支持,但强烈建议迁移到 MapWithContentTemplate

可以使用 MapWithContentTemplate 实现这些模板提供的功能。请参阅以下代码段以获取示例

MapTemplate

Kotlin

// MapTemplate (deprecated)
val template = MapTemplate.Builder()
    .setPane(paneBuilder.build())
    .setActionStrip(actionStrip)
    .setHeader(header)
    .setMapController(mapController)
    .build()

// MapWithContentTemplate
val template = MapWithContentTemplate.Builder()
    .setContentTemplate(
        PaneTemplate.Builder(paneBuilder.build())
            .setHeader(header)
            .build())
    .setActionStrip(actionStrip)
    .setMapController(mapController)
    .build()

Java

// MapTemplate (deprecated)
MapTemplate template = new MapTemplate.Builder()
    .setPane(paneBuilder.build())
    .setActionStrip(actionStrip)
    .setHeader(header)
    .setMapController(mapController)
    .build();

// MapWithContentTemplate
MapWithContentTemplate template = new MapWithContentTemplate.Builder()
    .setContentTemplate(new PaneTemplate.Builder(paneBuilder.build())
        .setHeader(header)
        build())
    .setActionStrip(actionStrip)
    .setMapController(mapController)
    .build();

PlaceListNavigationTemplate

Kotlin

// PlaceListNavigationTemplate (deprecated)
val template = PlaceListNavigationTemplate.Builder()
    .setItemList(itemListBuilder.build())
    .setHeader(header)
    .setActionStrip(actionStrip)
    .setMapActionStrip(mapActionStrip)
    .build()

// MapWithContentTemplate
val template = MapWithContentTemplate.Builder()
    .setContentTemplate(
        ListTemplate.Builder()
            .setSingleList(itemListBuilder.build())
            .setHeader(header)
            .build())
    .setActionStrip(actionStrip)
    .setMapController(
        MapController.Builder()
            .setMapActionStrip(mapActionStrip)
            .build())
    .build()

Java

// PlaceListNavigationTemplate (deprecated)
PlaceListNavigationTemplate template = new PlaceListNavigationTemplate.Builder()
    .setItemList(itemListBuilder.build())
    .setHeader(header)
    .setActionStrip(actionStrip)
    .setMapActionStrip(mapActionStrip)
    .build();

// MapWithContentTemplate
MapWithContentTemplate template = new MapWithContentTemplate.Builder()
    .setContentTemplate(new ListTemplate.Builder()
        .setSingleList(itemListBuilder.build())
        .setHeader(header)
        .build())
    .setActionStrip(actionStrip)
    .setMapController(new MapController.Builder()
        .setMapActionStrip(mapActionStrip)
        .build())
    .build();

RoutePreviewNavigationTemplate

Kotlin

// RoutePreviewNavigationTemplate (deprecated)
val template = RoutePreviewNavigationTemplate.Builder()
    .setItemList(
        ItemList.Builder()
            .addItem(
                Row.Builder()
                    .setTitle(title)
                    .build())
            .build())
    .setHeader(header)
    .setNavigateAction(
        Action.Builder()
            .setTitle(actionTitle)
            .setOnClickListener { ... }
            .build())
    .setActionStrip(actionStrip)
    .setMapActionStrip(mapActionStrip)
    .build()

// MapWithContentTemplate
val template = MapWithContentTemplate.Builder()
    .setContentTemplate(
        ListTemplate.Builder()
            .setSingleList(
                ItemList.Builder()
                    .addItem(
                        Row.Builder()
                            .setTitle(title)
                            .addAction(
                                Action.Builder()
                                    .setTitle(actionTitle)
                                    .setOnClickListener { ... }
                                    .build())
                            .build())
                    .build())
            .setHeader(header)
            .build())
    .setActionStrip(actionStrip)
    .setMapController(
        MapController.Builder()
            .setMapActionStrip(mapActionStrip)
            .build())
    .build()

Java

// RoutePreviewNavigationTemplate (deprecated)
RoutePreviewNavigationTemplate template = new RoutePreviewNavigationTemplate.Builder()
    .setItemList(new ItemList.Builder()
        .addItem(new Row.Builder()
            .setTitle(title))
            .build())
        .build())
    .setHeader(header)
    .setNavigateAction(new Action.Builder()
        .setTitle(actionTitle)
        .setOnClickListener(() -> { ... })
        .build())
    .setActionStrip(actionStrip)
    .setMapActionStrip(mapActionStrip)
    .build();

// MapWithContentTemplate
MapWithContentTemplate template = new MapWithContentTemplate.Builder()
    .setContentTemplate(new ListTemplate.Builder()
        .setSingleList(new ItemList.Builder()
            .addItem(new Row.Builder()
                  .setTitle(title))
                  .addAction(new Action.Builder()
                      .setTitle(actionTitle)
                      .setOnClickListener(() -> { ... })
                      .build())
                  .build())
            .build()))
        .setHeader(header)
        .build())
    .setActionStrip(actionStrip)
    .setMapController(new MapController.Builder()
        .setMapActionStrip(mapActionStrip)
        .build())
    .build();

导航应用必须与主机传达其他导航元数据。主机使用这些信息向车辆主机单元提供信息,并防止导航应用争夺共享资源。

导航元数据通过 NavigationManager 汽车服务提供,该服务可从 CarContext 访问。

Kotlin

val navigationManager = carContext.getCarService(NavigationManager::class.java)

Java

NavigationManager navigationManager = carContext.getCarService(NavigationManager.class);

开始、结束和停止导航

为了让主机管理多个导航应用、路线通知和车辆集群数据,它需要了解导航的当前状态。当用户开始导航时,请调用 NavigationManager.navigationStarted。类似地,当导航结束时(例如,当用户到达目的地或用户取消导航时),请调用 NavigationManager.navigationEnded

仅当用户完成导航时才调用 NavigationManager.navigationEnded。例如,如果您需要在行程中途重新计算路线,请改用 Trip.Builder.setLoading(true)

有时,主机需要应用停止导航,并在应用通过 NavigationManager.setNavigationManagerCallback 提供的 NavigationManagerCallback 对象中调用 onStopNavigation。然后,应用必须停止在集群显示屏、导航通知和语音指导中发出下一个转弯信息。

更新行程信息

在主动导航期间,请调用 NavigationManager.updateTrip。此调用中提供的信息可由车辆的集群和抬头显示器使用。根据驾驶的特定车辆,并非所有信息都会显示给用户。例如,桌面主机单元 (DHU) 显示添加到 TripStep,但不显示 Destination 信息。

绘制到集群显示屏

为了提供最具沉浸感的用户体验,您可能希望超越 在车辆的集群显示屏上显示基本元数据。从汽车应用 API 级别 6 开始,导航应用可以选择直接在集群显示屏上呈现自己的内容(在受支持的车辆中),但存在以下限制

  • 集群显示屏 API 不支持输入控件
  • 集群显示屏应仅显示地图瓦片。可以选择在这些瓦片上显示活动路线导航。
  • 集群显示屏 API 仅支持使用 NavigationTemplate
    • 与主显示屏不同,集群显示屏可能不会始终显示所有 NavigationTemplate UI 元素,例如转向指示、预计到达时间卡和操作。地图瓦片是唯一始终显示的 UI 元素。

声明集群支持

要让主机应用知道您的应用支持在集群显示屏上呈现内容,您必须在 CarAppService<intent-filter> 中添加一个 androidx.car.app.category.FEATURE_CLUSTER <category> 元素,如下面的代码段所示

<application>
    ...
   <service
       ...
        android:name=".MyNavigationCarAppService"
        android:exported="true">
      <intent-filter>
        <action android:name="androidx.car.app.CarAppService" />
        <category android:name="androidx.car.app.category.NAVIGATION"/>
        <category android:name="androidx.car.app.category.FEATURE_CLUSTER"/>
      </intent-filter>
    </service>
    ...
</application>

生命周期和状态管理

从 API 级别 6 开始,汽车应用 生命周期流程 保持不变,但现在 CarAppService::onCreateSession 采用类型为 SessionInfo 的参数,该参数提供有关正在创建的 Session 的其他信息(即,显示类型和受支持的模板集)。

应用可以选择使用相同的 Session 类来处理集群和主显示屏,或者创建特定于显示屏的 Sessions 来自定义每个显示屏上的行为(如下面的代码段所示)。

Kotlin

override fun onCreateSession(sessionInfo: SessionInfo): Session {
  return if (sessionInfo.displayType == SessionInfo.DISPLAY_TYPE_CLUSTER) {
    ClusterSession()
  } else {
    MainDisplaySession()
  }
}

Java

@Override
@NonNull
public Session onCreateSession(@NonNull SessionInfo sessionInfo) {
  if (sessionInfo.getDisplayType() == SessionInfo.DISPLAY_TYPE_CLUSTER) {
    return new ClusterSession();
  } else {
    return new MainDisplaySession();
  }
}

无法保证何时或是否提供集群显示屏,并且集群 Session 也可能成为唯一的 Session(例如,当您的应用正在主动导航时,用户将主显示屏切换到另一个应用)。“标准”协议是,只有在调用 NavigationManager::navigationStarted 后,应用才能控制集群显示屏。但是,应用可能会在没有活动导航的情况下获得集群显示屏,或者可能永远不会获得集群显示屏。您的应用需要通过呈现应用的地图瓦片空闲状态来处理这些情况。

主机为每个 Session 创建单独的绑定程序和 CarContext 实例。这意味着,当使用 ScreenManager::pushScreen::invalidate 等方法时,只会影响调用它们的 Session。如果需要跨 Session 通信(例如,通过使用 广播、共享单例或其他内容),应用应在这些实例之间创建自己的通信通道。

测试集群支持

您可以在 Android Auto 和 Android Automotive OS 上测试您的实现。对于 Android Auto,这是通过 配置桌面主机单元以模拟辅助集群显示屏 来完成的。对于 Android Automotive OS,API 级别 30 及更高版本的 通用系统映像 模拟集群显示屏。

使用文本或图标自定义 TravelEstimate

要使用文本、图标或两者自定义行程估算,请使用 TravelEstimate.Builder 类的 setTripIconsetTripText 方法。NavigationTemplate 使用 TravelEstimate 可选地在预计到达时间、剩余时间和剩余距离旁边或代替预计到达时间、剩余时间和剩余距离设置文本和图标。

图 1. 使用自定义图标和文本的行程估算。

以下代码段使用 setTripIconsetTripText 来自定义行程估算

Kotlin

TravelEstimate.Builder(Distance.create(...), DateTimeWithZone.create(...))
      ...
      .setTripIcon(CarIcon.Builder(...).build())
      .setTripText(CarText.create(...))
      .build()

Java

new TravelEstimate.Builder(Distance.create(...), DateTimeWithZone.create(...))
      ...
      .setTripIcon(CarIcon.Builder(...).build())
      .setTripText(CarText.create(...))
      .build();

提供转向指示通知

使用频繁更新的导航通知提供转向指示 (TBT) 导航说明。要在汽车屏幕上被视为导航通知,通知的构建器必须执行以下操作

  1. 使用 NotificationCompat.Builder.setOngoing 方法将通知标记为正在进行。
  2. 将通知的类别设置为 Notification.CATEGORY_NAVIGATION
  3. 使用 CarAppExtender 扩展通知。

导航通知显示在汽车屏幕底部的轨道小部件中。如果通知的重要性级别设置为 IMPORTANCE_HIGH,它也会显示为抬头显示通知 (HUN)。如果未使用 CarAppExtender.Builder.setImportance 方法设置重要性,则使用 通知频道的 重要性

应用可以在 CarAppExtender 中设置 PendingIntent,当用户点击 HUN 或轨道小部件时,此 PendingIntent 会发送到应用。

如果使用值为 trueNotificationCompat.Builder.setOnlyAlertOnce 调用,则高重要性通知在 HUN 中仅提醒一次。

以下代码段显示了如何构建导航通知

Kotlin

NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
    ...
    .setOnlyAlertOnce(true)
    .setOngoing(true)
    .setCategory(NotificationCompat.CATEGORY_NAVIGATION)
    .extend(
        CarAppExtender.Builder()
            .setContentTitle(carScreenTitle)
            ...
            .setContentIntent(
                PendingIntent.getBroadcast(
                    context,
                    ACTION_OPEN_APP.hashCode(),
                    Intent(ACTION_OPEN_APP).setComponent(
                        ComponentName(context, MyNotificationReceiver::class.java)),
                        0))
            .setImportance(NotificationManagerCompat.IMPORTANCE_HIGH)
            .build())
    .build()

Java

new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
    ...
    .setOnlyAlertOnce(true)
    .setOngoing(true)
    .setCategory(NotificationCompat.CATEGORY_NAVIGATION)
    .extend(
        new CarAppExtender.Builder()
            .setContentTitle(carScreenTitle)
            ...
            .setContentIntent(
                PendingIntent.getBroadcast(
                    context,
                    ACTION_OPEN_APP.hashCode(),
                    new Intent(ACTION_OPEN_APP).setComponent(
                        new ComponentName(context, MyNotificationReceiver.class)),
                        0))
            .setImportance(NotificationManagerCompat.IMPORTANCE_HIGH)
            .build())
    .build();

定期更新 TBT 通知以进行距离更改,这会更新轨道小部件,并且仅将通知显示为 HUN。您可以通过使用 CarAppExtender.Builder.setImportance 设置通知的重要性来控制 HUN 行为。将重要性设置为 IMPORTANCE_HIGH 将显示 HUN。将其设置为任何其他值只会更新轨道小部件。

刷新 PlaceListNavigationTemplate 内容

在浏览使用 PlaceListNavigationTemplate 构建的地点列表时,您可以让驾驶员点击按钮刷新内容。要启用列表刷新,请实现 OnContentRefreshListener 接口的 onContentRefreshRequested 方法,并使用 PlaceListNavigationTemplate.Builder.setOnContentRefreshListener 在模板上设置侦听器。

以下代码段显示了如何在模板上设置侦听器

Kotlin

PlaceListNavigationTemplate.Builder()
    ...
    .setOnContentRefreshListener {
        // Execute any desired logic
        ...
        // Then call invalidate() so onGetTemplate() is called again
        invalidate()
    }
    .build()

Java

new PlaceListNavigationTemplate.Builder()
        ...
        .setOnContentRefreshListener(() -> {
            // Execute any desired logic
            ...
            // Then call invalidate() so onGetTemplate() is called again
            invalidate();
        })
        .build();

只有当侦听器具有值时,才会在 PlaceListNavigationTemplate 的标题中显示刷新按钮。

当用户点击刷新按钮时,将调用您OnContentRefreshListener实现的onContentRefreshRequested方法。在onContentRefreshRequested中,调用Screen.invalidate方法。然后,主机回叫到您的应用的Screen.onGetTemplate方法,以检索包含已刷新内容的模板。有关刷新模板的更多信息,请参阅刷新模板的内容。只要onGetTemplate返回的下一个模板类型相同,它就计为刷新,并且不计入模板配额。

提供语音引导

要在汽车扬声器上播放导航引导,您的应用必须请求音频焦点。作为AudioFocusRequest的一部分,请将用途设置为AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE。此外,将焦点增益设置为AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK

模拟导航

要验证您的应用在提交到 Google Play 商店时的导航功能,您的应用必须实现NavigationManagerCallback.onAutoDriveEnabled回调。当调用此回调时,当用户开始导航时,您的应用必须模拟导航到所选目的地。当当前Session的生命周期达到Lifecycle.Event.ON_DESTROY状态时,您的应用可以退出此模式。

您可以通过从命令行执行以下操作来测试您对onAutoDriveEnabled的实现是否被调用

adb shell dumpsys activity service CAR_APP_SERVICE_NAME AUTO_DRIVE

以下示例显示了这一点

adb shell dumpsys activity service androidx.car.app.samples.navigation.car.NavigationCarAppService AUTO_DRIVE

默认导航汽车应用

在 Android Auto 中,默认导航汽车应用对应于用户最后启动的导航应用。当用户通过助手调用导航命令或其他应用发送启动导航的意图时,默认应用接收导航意图

显示上下文导航警报

Alert通过可选操作向驾驶员显示重要信息,而无需离开导航屏幕的上下文。为了为驾驶员提供最佳体验,AlertNavigationTemplate内工作,以避免阻塞导航路线并最大程度地减少驾驶员分心。

Alert仅在NavigationTemplate中可用。要通知NavigationTemplate之外的用户,请考虑使用抬头通知 (HUN),如显示通知中所述。

例如,使用Alert

  • 告知驾驶员与当前导航相关的更新,例如交通状况的变化。
  • 询问驾驶员与当前导航相关的更新,例如是否存在测速陷阱。
  • 提出即将执行的任务并询问驾驶员是否接受,例如驾驶员是否愿意在途中接人。

在基本形式中,Alert包含标题和Alert持续时间。持续时间由进度条表示。可选地,您可以添加副标题、图标和最多两个Action对象。

图 2. 上下文导航警报。

显示Alert后,如果驾驶员交互导致离开NavigationTemplate,它不会延续到另一个模板。它保留在原始NavigationTemplate中,直到Alert超时、用户执行操作或应用关闭Alert

创建警报

使用Alert.Builder创建Alert实例

Kotlin

Alert.Builder(
        /*alertId*/ 1,
        /*title*/ CarText.create("Hello"),
        /*durationMillis*/ 5000
    )
    // The fields below are optional
    .addAction(firstAction)
    .addAction(secondAction)
    .setSubtitle(CarText.create(...))
    .setIcon(CarIcon.APP_ICON)
    .setCallback(...)
    .build()

Java

new Alert.Builder(
        /*alertId*/ 1,
        /*title*/ CarText.create("Hello"),
        /*durationMillis*/ 5000
    )
    // The fields below are optional
    .addAction(firstAction)
    .addAction(secondAction)
    .setSubtitle(CarText.create(...))
    .setIcon(CarIcon.APP_ICON)
    .setCallback(...)
    .build();

如果要侦听Alert取消或关闭,请创建AlertCallback接口的实现。 AlertCallback调用路径为

配置警报持续时间

选择与您的应用需求匹配的Alert持续时间。导航Alert的推荐持续时间为 10 秒。有关更多信息,请参阅导航警报

显示警报

要显示Alert,请调用AppManager.showAlert方法,该方法可通过您的应用的CarContext使用。

// Show an alert
carContext.getCarService(AppManager.class).showAlert(alert)
  • 使用具有与当前显示的Alert的 ID 相同的alertIdAlert调用showAlert将不执行任何操作。Alert不会更新。要更新Alert,必须使用新的alertId重新创建它。
  • 使用具有与当前显示的Alert不同的alertIdAlert调用showAlert将关闭当前显示的Alert

关闭警报

虽然Alert会由于超时或驾驶员交互而自动关闭,但您也可以手动关闭Alert,例如,如果其信息已过时。要关闭Alert,请使用AlertalertId调用dismissAlert方法。

// Dismiss the same alert
carContext.getCarService(AppManager.class).dismissAlert(alert.getId())

使用与当前显示的Alert不匹配的alertId调用dismissAlert将不执行任何操作。它不会引发异常。