构建导航应用

本页面详细介绍了车载应用库的不同功能,您可以使用这些功能来实现您的逐向导航应用。

在清单中声明导航支持

您的导航应用需要在其 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 格式使导航应用能够与其他应用(如兴趣点应用和语音助手)协同工作。

要支持这些 intent 格式,首先需要通过在应用的清单中添加 intent 过滤器来声明支持。这些 intent 过滤器的位置取决于平台:

  • Android Auto:在 <activity> 清单元素中,用于处理用户未使用 Android Auto 时 intent 的 Activity
  • Android Automotive OS:在 <activity> 清单元素中,用于 CarAppActivity

然后,在应用的 Session 实现中,读取并处理 onCreateScreen()onNewIntent() 回调中的 intent。

必需的 intent 格式

为了满足 NF-6 质量要求,您的应用必须处理导航 intent

可选的 intent 格式

您还可以支持以下 intent 格式,以进一步提高应用的互操作性:

访问导航模板

导航应用可以访问以下模板,这些模板在后台显示带有地图的界面,并在主动导航期间显示逐向导航方向。

  • 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();

导航应用必须与主机通信额外的导航元数据。主机使用这些信息向车辆主机提供信息,并防止导航应用争用共享资源。

导航元数据通过可从 CarContext 访问的 NavigationManager 车载服务提供。

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 不支持输入控件。
  • 车载应用质量准则 NF-9:仪表盘显示屏应仅显示地图图块。这些图块上可以选择性地显示活动导航路线。
  • 仪表盘显示屏 API 仅支持使用 NavigationTemplate
    • 与主显示屏不同,仪表盘显示屏可能无法一致地显示所有 NavigationTemplate UI 元素,例如逐向指令、预计到达时间卡片和操作。地图图块是唯一一致显示的 UI 元素。

声明仪表盘支持

为了让主机应用知道您的应用支持在仪表盘显示屏上进行渲染,您必须将 androidx.car.app.category.FEATURE_CLUSTER <category> 元素添加到您的 CarAppService<intent-filter> 中,如以下代码段所示:

<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 创建单独的 binder 和 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 或导轨微件时,该 Intent 会发送到应用。

如果使用 true 值调用 NotificationCompat.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 中,默认导航车载应用对应于用户上次启动的导航应用。当用户通过 Assistant 调用导航命令或当另一个应用发送 intent 以启动导航时,默认应用会接收导航 intent

显示情境导航警报

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,请调用通过应用的 CarContext 可用的 AppManager.showAlert 方法。

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

解除警报

虽然 Alert 会因超时或驾驶员交互而自动解除,您也可以手动解除 Alert,例如当其信息过时时。要解除 Alert,请使用 AlertalertId 调用 dismissAlert 方法。

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

使用与当前显示的 Alert 不匹配的 alertId 调用 dismissAlert 不会执行任何操作,也不会抛出异常。