构建导航应用

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

在清单中声明导航支持

导航应用需要在其 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<activity> 清单元素中,用于在用户不使用 Android Auto 时处理 intent。
  • Android Automotive OS:在 CarAppActivity<activity> 清单元素中。

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

必需的意图格式

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

可选 intent 格式

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

访问导航模板

导航应用可以访问以下模板,这些模板会在地图背景上显示一个 surface,在有效导航期间还会显示精细导航路线。

  • 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 信息。

绘制到仪表板显示屏

为了提供最沉浸式的用户体验,您可能不仅仅需要在车辆仪表板显示屏上显示基本元数据。从 Car App API 级别 6 开始,导航应用可以选择直接在仪表板显示屏(在支持的车辆中)呈现自己的内容,但存在以下限制:

  • Cluster Display API 不支持输入控件
  • 汽车应用质量指南 NF-9:仪表板显示屏应仅显示地图图块。可以选择在这些图块上显示有效的导航路线。
  • Cluster Display API 仅支持使用 NavigationTemplate
    • 与主显示屏不同,仪表板显示屏可能不会一直显示所有 NavigationTemplate 界面元素,例如精细导航说明、预计到达时间卡片和操作。地图图块是唯一始终显示的界面元素。

声明支持仪表板

如需让托管应用知道您的应用支持在仪表板显示屏上呈现,您必须向 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 创建单独的 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 扩展通知。

导航通知将显示在车载显示屏底部的侧边栏 widget 中。如果通知的重要性级别设置为 IMPORTANCE_HIGH,它也会显示为浮动通知 (HUN)。如果未使用 CarAppExtender.Builder.setImportance 方法设置重要性,将采用通知渠道的重要性

应用可以在 CarAppExtender 中设置 PendingIntent,以便在用户点按 HUN 或侧边栏 widget 时将其发送到应用。

如果调用 NotificationCompat.Builder.setOnlyAlertOnce 且将值设置为 true,则高重要性通知将只以 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 通知,这样会更新侧边栏 widget,并且只将通知显示为 HUN。您可以使用 CarAppExtender.Builder.setImportance 设置通知的重要性,由此控制 HUN 行为。如果将重要性设置为 IMPORTANCE_HIGH,便会显示 HUN。如果将重要性设置为任何其他值,则只会更新侧边栏 widget。

刷新 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 中,默认汽车导航应用为用户最近启动的导航应用。当用户通过助理调用导航命令或其他应用发送 intent 以开始导航时,默认应用会接收导航 intent

显示上下文导航提醒

Alert 可向驾驶员显示重要信息和可选操作,而无需离开导航屏幕的上下文。为了给驾驶员提供最佳体验,Alert 应在 NavigationTemplate 内运行,以免阻挡导航路线信息并尽量避免让驾驶员分心。

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) 
  • 调用 showAlert 时,如果其 AlertalertId 与当前所显示 Alert 的 ID 相同,则系统不会执行任何操作。Alert 不会更新。如需更新 Alert,您必须使用新的 alertId 重新创建它。
  • 调用 showAlert 时,如果其 AlertalertId 与当前显示的 Alert 不同,则系统会关闭当前显示的 Alert

关闭提醒

虽然 Alert 会因超时或驾驶员互动而自动关闭,但您也可以手动关闭 Alert,例如当其信息已过时的时候。如需关闭 Alert,请使用 AlertalertId 调用 dismissAlert 方法。

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

调用 dismissAlert 时,如果其 alertId 与当前显示的 Alert 不匹配,则系统不会执行任何操作。系统不会抛出异常。