该 Android for Cars 应用库 允许您将导航、兴趣点 (POI) 和物联网 (IOT) 应用带到汽车中。它通过提供一套旨在满足驾驶员分心标准的模板并处理各种汽车屏幕因素和输入方式等细节来实现此目的。
本指南概述了库的关键功能和概念,并指导您完成设置基本应用的过程。
开始之前
- 查看涵盖汽车应用库的 驾驶设计 页面
- 查看以下部分中的 关键术语和概念。
- 熟悉 Android Auto 系统 UI 和 Android Automotive OS 设计。
- 查看 发行说明。
- 查看 示例。
关键术语和概念
- 模型和模板
- 用户界面由模型对象的图形表示,这些对象可以以不同的方式排列在一起,这取决于它们所属的模板。模板是模型的子集,可以在这些图形中充当根。模型包含以文本和图像形式显示给用户的信息,以及配置此类信息视觉外观方面的属性,例如文本颜色或图像大小。主机将模型转换为旨在满足驾驶员分心标准的视图,并处理各种汽车屏幕因素和输入方式等细节。
- 主机
- 主机是后端组件,它实现了库 API 提供的功能,以便您的应用可以在汽车中运行。主机的职责范围从发现您的应用和管理其生命周期到将您的模型转换为视图并通知您的应用用户交互。在移动设备上,此主机由 Android Auto 实现。在 Android Automotive OS 上,此主机作为系统应用安装。
- 模板限制
- 不同的模板会对模型的内容实施限制。例如,列表模板限制了可以显示给用户的项目数量。模板在形成任务流程的方式上也有限制。例如,应用只能将最多五个模板推送到屏幕堆栈。有关更多详细信息,请参阅 模板限制。
屏幕
Screen
是库提供的类,应用实现该类以管理显示给用户的用户界面。一个Screen
具有 生命周期,并为应用提供了一种机制,以便在屏幕可见时发送要显示的模板。Screen
实例也可以推送到和弹出Screen
堆栈,这确保它们符合 模板流程限制。CarAppService
CarAppService
是一个抽象的Service
类,您的应用必须实现并导出该类才能被主机发现和管理。您的应用的CarAppService
负责使用createHostValidator
验证主机连接是否可信,然后使用onCreateSession
为每个连接提供Session
实例。会话
Session
是一个抽象类,您的应用必须实现并使用CarAppService.onCreateSession
返回。它是用于在汽车屏幕上显示信息的入口点。它具有一个 生命周期,用于告知您的应用在汽车屏幕上的当前状态,例如您的应用何时可见或隐藏。当
Session
启动时(例如,当应用首次启动时),主机会请求使用onCreateScreen
方法显示初始Screen
。
安装汽车应用库
请参阅 Jetpack 库 发行页面,了解如何将库添加到您的应用的说明。
配置应用的清单文件
在创建汽车应用之前,请按如下所示配置应用的清单文件。
声明您的 CarAppService
主机通过您的CarAppService
实现连接到您的应用。您在清单中声明此服务,以便主机发现并连接到您的应用。
您还需要在应用的意图过滤器中的<category>
元素中声明应用的类别。有关此元素允许的值,请参阅支持的应用类别列表。
以下代码片段显示了如何在清单中为兴趣点应用声明汽车应用服务
<application>
...
<service
...
android:name=".MyCarAppService"
android:exported="true">
<intent-filter>
<action android:name="androidx.car.app.CarAppService"/>
<category android:name="androidx.car.app.category.POI"/>
</intent-filter>
</service>
...
<application>
支持的应用类别
在声明CarAppService
(如上一节所述)时,通过在意图过滤器中添加以下一个或多个类别值来声明应用的类别
androidx.car.app.category.NAVIGATION
:提供转向导航方向的应用。有关此类别的更多文档,请查看为汽车构建导航应用。androidx.car.app.category.POI
:提供与查找兴趣点(例如停车位、充电站和加油站)相关的功能的应用。有关此类别的更多文档,请查看为汽车构建兴趣点应用。androidx.car.app.category.IOT
:允许用户在车内对连接的设备执行相关操作的应用。有关此类别的更多文档,请查看为汽车构建物联网应用。
有关每个类别的详细说明以及应用属于这些类别的标准,请参阅Android 汽车应用质量。
指定应用名称和图标
您需要指定主机可用于在系统 UI 中表示应用的应用名称和图标。
您可以使用label
和icon
属性来指定用于表示应用的应用名称和图标,这些属性位于您的CarAppService
中。
...
<service
android:name=".MyCarAppService"
android:exported="true"
android:label="@string/my_app_name"
android:icon="@drawable/my_app_icon">
...
</service>
...
如果未在<service>
元素中声明标签或图标,则主机将回退到为<application>
元素指定的 value。
设置自定义主题
要为汽车应用设置自定义主题,请在清单文件中添加<meta-data>
元素,如下所示
<meta-data android:name="androidx.car.app.theme" android:resource="@style/MyCarAppTheme />
然后,声明您的样式资源,以设置自定义汽车应用主题的以下属性
<resources> <style name="MyCarAppTheme"> <item name="carColorPrimary">@layout/my_primary_car_color</item> <item name="carColorPrimaryDark">@layout/my_primary_dark_car_color</item> <item name="carColorSecondary">@layout/my_secondary_car_color</item> <item name="carColorSecondaryDark">@layout/my_secondary_dark_car_color</item> <item name="carPermissionActivityLayout">@layout/my_custom_background</item> </style> </resources>
汽车应用 API 级别
汽车应用库定义了自己的 API 级别,以便您可以知道车辆上的模板主机支持哪些库功能。要检索主机支持的最高汽车应用 API 级别,请使用getCarAppApiLevel()
方法。
在您的AndroidManifest.xml
文件中声明您的应用支持的最低汽车应用 API 级别
<manifest ...>
<application ...>
<meta-data
android:name="androidx.car.app.minCarApiLevel"
android:value="1"/>
</application>
</manifest>
有关如何维护向后兼容性和声明使用功能所需的最低 API 级别的详细信息,请参阅RequiresCarApi
注释的文档。有关使用汽车应用库的某个特定功能需要哪个 API 级别的定义,请查看CarAppApiLevels
的参考文档。
创建您的 CarAppService 和 Session
您的应用需要扩展CarAppService
类并实现其onCreateSession
方法,该方法返回与当前主机连接对应的Session
实例
Kotlin
class HelloWorldService : CarAppService() { ... override fun onCreateSession(): Session { return HelloWorldSession() } ... }
Java
public final class HelloWorldService extends CarAppService { ... @Override @NonNull public Session onCreateSession() { return new HelloWorldSession(); } ... }
Session
实例负责在应用首次启动时返回要使用的Screen
实例
Kotlin
class HelloWorldSession : Session() { ... override fun onCreateScreen(intent: Intent): Screen { return HelloWorldScreen(carContext) } ... }
Java
public final class HelloWorldSession extends Session { ... @Override @NonNull public Screen onCreateScreen(@NonNull Intent intent) { return new HelloWorldScreen(getCarContext()); } ... }
为了处理汽车应用需要从非应用主页或登录屏幕的屏幕启动的情况(例如处理深层链接),您可以在从onCreateScreen
返回之前,使用ScreenManager.push
预先填充屏幕的后退堆栈。预先填充允许用户从应用显示的第一个屏幕导航回以前的屏幕。
创建您的起始屏幕
您可以通过定义扩展Screen
类并实现其onGetTemplate
方法的类来创建应用显示的屏幕,该方法返回表示要显示在汽车屏幕上的 UI 状态的Template
实例。
以下代码片段显示了如何声明一个使用PaneTemplate
模板显示简单的“Hello world!”字符串的Screen
Kotlin
class HelloWorldScreen(carContext: CarContext) : Screen(carContext) { override fun onGetTemplate(): Template { val row = Row.Builder().setTitle("Hello world!").build() val pane = Pane.Builder().addRow(row).build() return PaneTemplate.Builder(pane) .setHeaderAction(Action.APP_ICON) .build() } }
Java
public class HelloWorldScreen extends Screen { @NonNull @Override public Template onGetTemplate() { Row row = new Row.Builder().setTitle("Hello world!").build(); Pane pane = new Pane.Builder().addRow(row).build(); return new PaneTemplate.Builder(pane) .setHeaderAction(Action.APP_ICON) .build(); } }
CarContext 类
CarContext
类是ContextWrapper
的子类,您的Session
和Screen
实例可以访问它。它提供对汽车服务的访问,例如用于管理屏幕堆栈的ScreenManager
;用于一般应用相关功能(例如访问用于绘制地图的Surface
对象)的AppManager
;以及转向导航应用用于与主机通信导航元数据和其他导航相关事件的NavigationManager
。
有关导航应用可用的库功能的完整列表,请参阅访问导航模板。
CarContext
还提供其他功能,例如允许您使用汽车屏幕的配置加载可绘制资源,使用意图在汽车中启动应用,以及指示您的应用是否应以深色主题显示其地图。
实现屏幕导航
应用通常会呈现许多不同的屏幕,每个屏幕可能使用不同的模板,用户可以在与屏幕中显示的界面交互时浏览这些模板。
ScreenManager
类提供一个屏幕堆栈,您可以使用它来推送屏幕,当用户在汽车屏幕中选择后退按钮或使用某些汽车中提供的硬件后退按钮时,这些屏幕可以自动弹出。
以下代码片段显示了如何向消息模板添加后退操作以及在用户选择时推送新屏幕的操作
Kotlin
val template = MessageTemplate.Builder("Hello world!") .setHeaderAction(Action.BACK) .addAction( Action.Builder() .setTitle("Next screen") .setOnClickListener { screenManager.push(NextScreen(carContext)) } .build()) .build()
Java
MessageTemplate template = new MessageTemplate.Builder("Hello world!") .setHeaderAction(Action.BACK) .addAction( new Action.Builder() .setTitle("Next screen") .setOnClickListener( () -> getScreenManager().push(new NextScreen(getCarContext()))) .build()) .build();
Action.BACK
对象是一个标准Action
,它会自动调用ScreenManager.pop
。可以通过使用CarContext
中提供的OnBackPressedDispatcher
实例来覆盖此行为。
为了帮助确保应用在驾驶时安全使用,屏幕堆栈的最大深度可以为五个屏幕。有关更多详细信息,请参阅模板限制部分。
刷新模板的内容
您的应用可以通过调用Screen.invalidate
方法请求使Screen
的内容失效。主机随后会回调到应用的Screen.onGetTemplate
方法以检索包含新内容的模板。
刷新Screen
时,务必了解模板中可以更新的具体内容,以便主机不会将新模板计入模板配额。有关更多详细信息,请参阅模板限制部分。
我们建议您构建屏幕,以便在Screen
及其onGetTemplate
实现返回的模板类型之间建立一对一的映射关系。
绘制地图
使用以下模板的导航和兴趣点 (POI) 应用可以通过访问Surface
来绘制地图
模板 | 模板权限 | 类别指南 |
---|---|---|
NavigationTemplate |
androidx.car.app.NAVIGATION_TEMPLATES |
导航 |
MapWithContentTemplate |
androidx.car.app.NAVIGATION_TEMPLATES 或androidx.car.app.MAP_TEMPLATES |
导航、POI |
MapTemplate (已弃用) |
androidx.car.app.NAVIGATION_TEMPLATES |
导航 |
PlaceListNavigationTemplate (已弃用) |
androidx.car.app.NAVIGATION_TEMPLATES |
导航 |
RoutePreviewNavigationTemplate (已弃用) |
androidx.car.app.NAVIGATION_TEMPLATES |
导航 |
声明界面权限
除了应用使用的模板所需的权限外,您的应用还必须在其AndroidManifest.xml
文件中声明androidx.car.app.ACCESS_SURFACE
权限才能访问界面。
<manifest ...>
...
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
...
</manifest>
访问界面
要访问主机提供的Surface
,您必须实现一个SurfaceCallback
并将其提供给AppManager
汽车服务。当前的Surface
将通过onSurfaceAvailable()
和onSurfaceDestroyed()
回调的SurfaceContainer
参数传递给您的SurfaceCallback
。
Kotlin
carContext.getCarService(AppManager::class.java).setSurfaceCallback(surfaceCallback)
Java
carContext.getCarService(AppManager.class).setSurfaceCallback(surfaceCallback);
了解界面的可见区域
主机可以在地图顶部为模板绘制用户界面元素。主机通过调用SurfaceCallback.onVisibleAreaChanged
方法来传达保证无遮挡且对用户完全可见的界面区域。此外,为了最大程度地减少更改次数,主机将使用最小矩形调用SurfaceCallback.onStableAreaChanged
方法,该矩形始终基于当前模板可见。
例如,当导航应用使用带有顶部操作栏的NavigationTemplate
时,当用户一段时间内未与屏幕交互时,操作栏可以隐藏自身以腾出更多空间用于地图。在这种情况下,会向onStableAreaChanged
和onVisibleAreaChanged
回调相同的矩形。当操作栏隐藏时,只有onVisibleAreaChanged
会使用更大的区域被调用。如果用户与屏幕交互,则再次仅调用onVisibleAreaChanged
,并使用第一个矩形。
支持深色主题
当主机确定条件需要时,应用必须使用正确的深色在Surface
实例上重新绘制地图,如汽车 Android 应用质量中所述。
要确定是否绘制深色地图,您可以使用CarContext.isDarkMode
方法。每当深色主题状态发生变化时,您都会收到对Session.onCarConfigurationChanged
的调用。
允许用户与您的地图交互
使用以下模板时,您可以添加对用户与绘制的地图交互的支持,例如通过缩放和平移让他们查看地图的不同部分。
模板 | 自 Car 应用 API 级别起支持交互性 |
---|---|
NavigationTemplate |
2 |
PlaceListNavigationTemplate (已弃用) |
4 |
RoutePreviewNavigationTemplate (已弃用) |
4 |
MapTemplate (已弃用) |
5(模板的引入) |
MapWithContentTemplate |
7(模板的引入) |
实现交互性回调
SurfaceCallback
接口有一些回调方法,您可以实现这些方法以向使用上一节中的模板构建的地图添加交互性。
交互 | SurfaceCallback 方法 |
自 Car 应用 API 级别起支持 |
---|---|---|
点击 | onClick |
5 |
捏合缩放 | onScale |
2 |
单指拖动 | onScroll |
2 |
单指轻扫 | onFling |
2 |
双击 | onScale (缩放因子由模板主机确定) |
2 |
平移模式下的旋转微调 | onScroll (距离因子由模板主机确定) |
2 |
添加地图操作栏
这些模板可以具有用于地图相关操作(例如放大和缩小、重新居中、显示指南针以及您选择显示的其他操作)的地图操作栏。地图操作栏最多可以有四个仅图标按钮,这些按钮可以刷新而不会影响任务深度。它在空闲状态下隐藏,并在活动状态下重新显示。
要接收地图交互性回调,您**必须**在地图操作栏中添加Action.PAN
按钮。当用户按下平移按钮时,主机将进入平移模式,如下节所述。
如果您的应用省略了地图操作栏中的Action.PAN
按钮,则它不会接收来自SurfaceCallback
方法的用户输入,并且主机将退出任何先前激活的平移模式。
在触摸屏上,不会显示平移按钮。
了解平移模式
在平移模式下,模板主机将来自非触摸输入设备(例如旋转控制器和触摸板)的用户输入转换为相应的SurfaceCallback
方法。使用NavigationTemplate.Builder
中的setPanModeListener
方法响应用户操作以进入或退出平移模式。主机可以在用户处于平移模式时隐藏模板中的其他 UI 组件。
与用户互动
您的应用可以使用类似于移动应用的模式与用户互动。
处理用户输入
您的应用可以通过将相应的侦听器传递给支持它们的模型来响应用户输入。以下代码段显示了如何创建一个Action
模型,该模型设置一个OnClickListener
,该侦听器会回调到您的应用代码定义的方法。
Kotlin
val action = Action.Builder() .setTitle("Navigate") .setOnClickListener(::onClickNavigate) .build()
Java
Action action = new Action.Builder() .setTitle("Navigate") .setOnClickListener(this::onClickNavigate) .build();
然后,onClickNavigate
方法可以通过使用CarContext.startCarApp
方法启动默认导航汽车应用。
Kotlin
private fun onClickNavigate() { val intent = Intent(CarContext.ACTION_NAVIGATE, Uri.parse("geo:0,0?q=" + address)) carContext.startCarApp(intent) }
Java
private void onClickNavigate() { Intent intent = new Intent(CarContext.ACTION_NAVIGATE, Uri.parse("geo:0,0?q=" + address)); getCarContext().startCarApp(intent); }
有关如何启动应用(包括ACTION_NAVIGATE
意图的格式)的更多详细信息,请参阅使用意图启动汽车应用部分。
某些操作(例如需要指示用户在他们的移动设备上继续交互的操作)仅在汽车停放时才允许。您可以使用ParkedOnlyOnClickListener
来实现这些操作。如果汽车未停放,主机将向用户显示指示,表明在这种情况下不允许此操作。如果汽车停放,则代码将正常执行。以下代码段显示了如何使用ParkedOnlyOnClickListener
在移动设备上打开设置屏幕。
Kotlin
val row = Row.Builder() .setTitle("Open Settings") .setOnClickListener(ParkedOnlyOnClickListener.create(::openSettingsOnPhone)) .build()
Java
Row row = new Row.Builder() .setTitle("Open Settings") .setOnClickListener(ParkedOnlyOnClickListener.create(this::openSettingsOnPhone)) .build();
显示通知
仅当使用CarAppExtender
扩展发送到移动设备的通知时,这些通知才会显示在汽车屏幕上。某些通知属性(例如内容标题、文本、图标和操作)可以在CarAppExtender
中设置,在这些通知显示在汽车屏幕上时覆盖这些通知的属性。
以下代码段显示了如何向汽车屏幕发送一个通知,该通知显示的标题与移动设备上显示的标题不同。
Kotlin
val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) .setContentTitle(titleOnThePhone) .extend( CarAppExtender.Builder() .setContentTitle(titleOnTheCar) ... .build()) .build()
Java
Notification notification = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) .setContentTitle(titleOnThePhone) .extend( new CarAppExtender.Builder() .setContentTitle(titleOnTheCar) ... .build()) .build();
通知可能会影响用户界面的以下部分。
- 可能会向用户显示抬头通知 (HUN)。
- 可能会在通知中心添加一个条目,该条目可以选择在轨道上显示徽章。
- 对于导航应用,通知可能会在轨道小部件中显示,如逐步导航通知中所述。
您可以选择如何配置应用的通知以影响每个用户界面元素,方法是使用通知的优先级,如CarAppExtender
文档中所述。
如果使用true
的值调用NotificationCompat.Builder.setOnlyAlertOnce
,则高优先级通知将仅显示一次作为 HUN。
有关如何设计汽车应用通知的更多信息,请参阅 Google 设计指南,了解有关通知的内容。
显示吐司
您的应用可以使用CarToast
显示吐司,如以下代码段所示。
Kotlin
CarToast.makeText(carContext, "Hello!", CarToast.LENGTH_SHORT).show()
Java
CarToast.makeText(getCarContext(), "Hello!", CarToast.LENGTH_SHORT).show();
请求权限
如果您的应用需要访问受限数据或操作(例如位置),则适用Android 权限的标准规则。要请求权限,您可以使用CarContext.requestPermissions()
方法。
与使用标准 Android API相比,使用CarContext.requestPermissions()
的好处是,您无需启动自己的Activity
来创建权限对话框。此外,您可以在 Android Auto 和 Android Automotive OS 上使用相同的代码,而不必创建依赖于平台的流程。
在 Android Auto 上设置权限对话框的样式
在 Android Auto 上,用户的权限对话框将显示在手机上。默认情况下,对话框后面没有背景。要设置自定义背景,请在AndroidManifest.xml
文件中声明一个汽车应用主题,并为您的汽车应用主题设置carPermissionActivityLayout
属性。
<meta-data android:name="androidx.car.app.theme" android:resource="@style/MyCarAppTheme />
然后,为您的汽车应用主题设置carPermissionActivityLayout
属性。
<resources> <style name="MyCarAppTheme"> <item name="carPermissionActivityLayout">@layout/my_custom_background</item> </style> </resources>
使用 Intent 启动汽车应用
您可以调用 CarContext.startCarApp
方法来执行以下操作之一
- 打开拨号器进行电话呼叫。
- 使用 默认导航汽车应用 开始到某个位置的导航。
- 使用 Intent 启动您自己的应用。
以下示例演示了如何创建一个带有操作的通知,该操作使用显示停车预订详细信息的屏幕打开您的应用。您使用包含 PendingIntent
的内容 Intent 扩展通知实例,该 PendingIntent
包装了指向应用操作的显式 Intent
Kotlin
val notification = notificationBuilder ... .extend( CarAppExtender.Builder() .setContentIntent( PendingIntent.getBroadcast( context, ACTION_VIEW_PARKING_RESERVATION.hashCode(), Intent(ACTION_VIEW_PARKING_RESERVATION) .setComponent(ComponentName(context, MyNotificationReceiver::class.java)), 0)) .build())
Java
Notification notification = notificationBuilder ... .extend( new CarAppExtender.Builder() .setContentIntent( PendingIntent.getBroadcast( context, ACTION_VIEW_PARKING_RESERVATION.hashCode(), new Intent(ACTION_VIEW_PARKING_RESERVATION) .setComponent(new ComponentName(context, MyNotificationReceiver.class)), 0)) .build());
您的应用还必须声明一个 BroadcastReceiver
,当用户在通知界面中选择操作并调用 CarContext.startCarApp
且 Intent 包含数据 URI 时,将调用该接收器来处理 Intent
Kotlin
class MyNotificationReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val intentAction = intent.action if (ACTION_VIEW_PARKING_RESERVATION == intentAction) { CarContext.startCarApp( intent, Intent(Intent.ACTION_VIEW) .setComponent(ComponentName(context, MyCarAppService::class.java)) .setData(Uri.fromParts(MY_URI_SCHEME, MY_URI_HOST, intentAction))) } } }
Java
public class MyNotificationReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String intentAction = intent.getAction(); if (ACTION_VIEW_PARKING_RESERVATION.equals(intentAction)) { CarContext.startCarApp( intent, new Intent(Intent.ACTION_VIEW) .setComponent(new ComponentName(context, MyCarAppService.class)) .setData(Uri.fromParts(MY_URI_SCHEME, MY_URI_HOST, intentAction))); } } }
最后,应用中的 Session.onNewIntent
方法通过将停车预订屏幕推入堆栈来处理此 Intent(如果它尚未位于顶部)
Kotlin
override fun onNewIntent(intent: Intent) { val screenManager = carContext.getCarService(ScreenManager::class.java) val uri = intent.data if (uri != null && MY_URI_SCHEME == uri.scheme && MY_URI_HOST == uri.schemeSpecificPart && ACTION_VIEW_PARKING_RESERVATION == uri.fragment ) { val top = screenManager.top if (top !is ParkingReservationScreen) { screenManager.push(ParkingReservationScreen(carContext)) } } }
Java
@Override public void onNewIntent(@NonNull Intent intent) { ScreenManager screenManager = getCarContext().getCarService(ScreenManager.class); Uri uri = intent.getData(); if (uri != null && MY_URI_SCHEME.equals(uri.getScheme()) && MY_URI_HOST.equals(uri.getSchemeSpecificPart()) && ACTION_VIEW_PARKING_RESERVATION.equals(uri.getFragment()) ) { Screen top = screenManager.getTop(); if (!(top instanceof ParkingReservationScreen)) { screenManager.push(new ParkingReservationScreen(getCarContext())); } } }
有关如何处理汽车应用的通知的更多信息,请参阅 显示通知 部分。
模板限制
主机将给定任务显示的模板数量限制为最多五个,其中最后一个模板必须是以下类型之一
请注意,此限制适用于模板的数量,而不是堆栈中 Screen
实例的数量。例如,如果应用在屏幕 A 中发送两个模板,然后推入屏幕 B,则现在可以再发送三个模板。或者,如果每个屏幕都设置为发送单个模板,则应用可以在 ScreenManager
堆栈上推入五个屏幕实例。
这些限制有一些特殊情况:模板刷新以及后退和重置操作。
模板刷新
某些内容更新不计入模板限制。通常,如果应用推送的新模板与上一个模板类型相同,并且包含相同的主要内容,则新模板不会计入配额。例如,更新 ListTemplate
中行的切换状态不会计入配额。请参阅各个模板的文档,以了解哪些类型的内容更新可以被视为刷新。
后退操作
为了在任务中启用子流程,主机检测到应用何时从 ScreenManager
堆栈中弹出 Screen
,并根据应用后退的模板数量更新剩余配额。
例如,如果应用在屏幕 A 中发送两个模板,然后推入屏幕 B 并发送两个更多模板,则应用有一个配额剩余。如果应用然后返回到屏幕 A,则主机将配额重置为三个,因为应用已后退两个模板。
请注意,当返回到屏幕时,应用必须发送一个与该屏幕最后发送的模板类型相同的模板。发送任何其他模板类型会导致错误。但是,只要类型在后退操作期间保持不变,应用就可以自由修改模板的内容,而不会影响配额。
重置操作
某些模板具有表示任务结束的特殊语义。例如,NavigationTemplate
是一个预计会停留在屏幕上并使用新的转向指示刷新以供用户使用的视图。当它到达其中一个模板时,主机将重置模板配额,将该模板视为新任务的第一步。这允许应用开始一个新任务。请参阅各个模板的文档,以查看哪些模板会触发主机上的重置。
如果主机收到从通知操作或启动器启动应用的 Intent,则配额也会重置。此机制允许应用从通知开始新的任务流程,即使应用已绑定并在前台也是如此。
有关如何在汽车屏幕上显示应用通知的更多详细信息,请参阅 显示通知 部分。有关如何从通知操作启动应用的信息,请参阅 使用 Intent 启动汽车应用 部分。
连接 API
您可以使用 CarConnection
API 在运行时检索连接信息,以确定您的应用是在 Android Auto 还是 Android Automotive OS 上运行。
例如,在汽车应用的 Session
中,初始化一个 CarConnection
并订阅 LiveData
更新
Kotlin
CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated)
Java
new CarConnection(getCarContext()).getType().observe(this, this::onConnectionStateUpdated);
然后,您可以在观察者中对连接状态的变化做出反应
Kotlin
fun onConnectionStateUpdated(connectionState: Int) { val message = when(connectionState) { CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "Not connected to a head unit" CarConnection.CONNECTION_TYPE_NATIVE -> "Connected to Android Automotive OS" CarConnection.CONNECTION_TYPE_PROJECTION -> "Connected to Android Auto" else -> "Unknown car connection type" } CarToast.makeText(carContext, message, CarToast.LENGTH_SHORT).show() }
Java
private void onConnectionStateUpdated(int connectionState) { String message; switch(connectionState) { case CarConnection.CONNECTION_TYPE_NOT_CONNECTED: message = "Not connected to a head unit"; break; case CarConnection.CONNECTION_TYPE_NATIVE: message = "Connected to Android Automotive OS"; break; case CarConnection.CONNECTION_TYPE_PROJECTION: message = "Connected to Android Auto"; break; default: message = "Unknown car connection type"; break; } CarToast.makeText(getCarContext(), message, CarToast.LENGTH_SHORT).show(); }
约束 API
不同的汽车可能允许向用户显示不同数量的 Item
实例。使用 ConstraintManager
在运行时检查内容限制,并在模板中设置适当数量的项目。
首先从 CarContext
获取 ConstraintManager
Kotlin
val manager = carContext.getCarService(ConstraintManager::class.java)
Java
ConstraintManager manager = getCarContext().getCarService(ConstraintManager.class);
然后,您可以查询检索到的 ConstraintManager
对象以获取相关内容限制。例如,要获取网格中可以显示的项目数,请使用 CONTENT_LIMIT_TYPE_GRID
调用 getContentLimit
Kotlin
val gridItemLimit = manager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_GRID)
Java
int gridItemLimit = manager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_GRID);
添加登录流程
如果您的应用为用户提供登录体验,则可以使用诸如 SignInTemplate
和 LongMessageTemplate
等模板(使用 Car 应用 API 级别 2 及更高版本)来处理在汽车的车载主机上登录您的应用。
要创建 SignInTemplate
,请定义一个 SignInMethod
。Car 应用库当前支持以下登录方法
InputSignInMethod
用于用户名/密码登录。PinSignInMethod
用于 PIN 登录,用户使用在车载主机上显示的 PIN 从手机关联其帐户。ProviderSignInMethod
用于提供商登录,例如 Google 登录 和 一键登录。QRCodeSignInMethod
用于 QR 码登录,用户扫描 QR 码以在手机上完成登录。这在 Car API 级别 4 及更高版本中可用。
例如,要实现一个收集用户密码的模板,首先创建一个 InputCallback
来处理和验证用户输入
Kotlin
val callback = object : InputCallback { override fun onInputSubmitted(text: String) { // You will receive this callback when the user presses Enter on the keyboard. } override fun onInputTextChanged(text: String) { // You will receive this callback as the user is typing. The update // frequency is determined by the host. } }
Java
InputCallback callback = new InputCallback() { @Override public void onInputSubmitted(@NonNull String text) { // You will receive this callback when the user presses Enter on the keyboard. } @Override public void onInputTextChanged(@NonNull String text) { // You will receive this callback as the user is typing. The update // frequency is determined by the host. } };
InputSignInMethod
Builder
需要 InputCallback
。
Kotlin
val passwordInput = InputSignInMethod.Builder(callback) .setHint("Password") .setInputType(InputSignInMethod.INPUT_TYPE_PASSWORD) ... .build()
Java
InputSignInMethod passwordInput = new InputSignInMethod.Builder(callback) .setHint("Password") .setInputType(InputSignInMethod.INPUT_TYPE_PASSWORD) ... .build();
最后,使用新的 InputSignInMethod
创建 SignInTemplate
。
Kotlin
SignInTemplate.Builder(passwordInput) .setTitle("Sign in with username and password") .setInstructions("Enter your password") .setHeaderAction(Action.BACK) ... .build()
Java
new SignInTemplate.Builder(passwordInput) .setTitle("Sign in with username and password") .setInstructions("Enter your password") .setHeaderAction(Action.BACK) ... .build();
使用 AccountManager
具有身份验证功能的 Android Automotive OS 应用必须使用 AccountManager,原因如下
- 更好的用户体验和更轻松的帐户管理:用户可以轻松地从系统设置中的帐户菜单管理所有帐户,包括登录和注销。
- “访客”体验:由于汽车是共享设备,因此 OEM 可以启用车辆中的访客体验,在该体验中,无法添加帐户。
添加文本字符串变体
不同的汽车屏幕尺寸可能显示不同数量的文本。使用 Car 应用 API 级别 2 及更高版本,您可以指定文本字符串的多个变体以最佳地适应屏幕。要查看文本变体在哪里被接受,请查找接受 CarText
的模板和组件。
您可以使用 CarText.Builder.addVariant()
方法将文本字符串变体添加到 CarText
中
Kotlin
val itemTitle = CarText.Builder("This is a very long string") .addVariant("Shorter string") ... .build()
Java
CarText itemTitle = new CarText.Builder("This is a very long string") .addVariant("Shorter string") ... .build();
然后,您可以使用此 CarText
(例如,作为 GridItem
的主要文本)。
Kotlin
GridItem.Builder() .addTitle(itemTitle) ... .build()
Java
new GridItem.Builder() .addTitle(itemTitle) ... build();
按从最优到最不优的顺序添加字符串(例如,从最长到最短)。主机根据汽车屏幕上的可用空间选择适当长度的字符串。
为行添加内联 CarIcon
您可以使用 CarIconSpan
将图标与文本内联添加,以丰富应用的视觉吸引力。有关创建这些跨度的更多信息,请参阅 CarIconSpan.create
的文档。有关使用跨度进行文本样式设置的工作原理概述,请参阅 使用跨度进行精彩的文本样式设置。
Kotlin
val rating = SpannableString("Rating: 4.5 stars") rating.setSpan( CarIconSpan.create( // Create a CarIcon with an image of four and a half stars CarIcon.Builder(...).build(), // Align the CarIcon to the baseline of the text CarIconSpan.ALIGN_BASELINE ), // The start index of the span (index of the character '4') 8, // The end index of the span (index of the last 's' in "stars") 16, Spanned.SPAN_INCLUSIVE_INCLUSIVE ) val row = Row.Builder() ... .addText(rating) .build()
Java
SpannableString rating = new SpannableString("Rating: 4.5 stars"); rating.setSpan( CarIconSpan.create( // Create a CarIcon with an image of four and a half stars new CarIcon.Builder(...).build(), // Align the CarIcon to the baseline of the text CarIconSpan.ALIGN_BASELINE ), // The start index of the span (index of the character '4') 8, // The end index of the span (index of the last 's' in "stars") 16, Spanned.SPAN_INCLUSIVE_INCLUSIVE ); Row row = new Row.Builder() ... .addText(rating) .build();
汽车硬件 API
从 Car 应用 API 级别 3 开始,Car 应用库提供了可用于访问车辆属性和传感器的 API。
要求
要在 Android Auto 中使用这些 API,请首先将对 androidx.car.app:app-projected
的依赖项添加到 Android Auto 模块的 build.gradle
文件中。对于 Android Automotive OS,请将对 androidx.car.app:app-automotive
的依赖项添加到 Android Automotive OS 模块的 build.gradle
文件中。
此外,在您的 AndroidManifest.xml
文件中,您需要 声明请求所需汽车数据的相关权限。请注意,用户也必须 授予 您这些权限。您可以对 Android Auto 和 Android Automotive OS 使用 相同的代码,而无需创建平台相关的流程。但是,所需的权限不同。
CarInfo
此表描述了由 CarInfo
API 提供的属性以及使用它们需要请求的权限
方法 | 属性 | Android Auto 权限 | Android Automotive OS 权限 | 自 Car 应用 API 级别起支持 |
---|---|---|---|---|
fetchModel |
制造商、型号、年份 | android.car.permission.CAR_INFO |
3 | |
fetchEnergyProfile |
电动汽车连接器类型、燃料类型 | com.google.android.gms.permission.CAR_FUEL |
android.car.permission.CAR_INFO |
3 |
fetchExteriorDimensions
此数据仅在某些运行 API 30 或更高版本的 Android Automotive OS 车辆上可用 |
外部尺寸 | N/A | android.car.permission.CAR_INFO |
7 |
addTollListener
removeTollListener |
通行费卡状态、通行费卡类型 | 3 | ||
addEnergyLevelListener
removeEnergyLevelListener |
电量、油量、油量低、剩余续航里程 | com.google.android.gms.permission.CAR_FUEL |
android.car.permission.CAR_ENERGY ,android.car.permission.CAR_ENERGY_PORTS ,android.car.permission.READ_CAR_DISPLAY_UNITS
|
3 |
addSpeedListener
removeSpeedListener |
原始速度、显示速度(显示在汽车的仪表盘上) | com.google.android.gms.permission.CAR_SPEED |
android.car.permission.CAR_SPEED ,android.car.permission.READ_CAR_DISPLAY_UNITS |
3 |
addMileageListener
removeMileageListener |
里程表距离 | com.google.android.gms.permission.CAR_MILEAGE |
此数据在 Android Automotive OS 上不适用于从 Play 商店安装的应用。 | 3 |
例如,要获取剩余续航里程,请实例化一个 CarInfo
对象,然后创建并注册一个 OnCarDataAvailableListener
Kotlin
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo val listener = OnCarDataAvailableListener<EnergyLevel> { data -> if (data.rangeRemainingMeters.status == CarValue.STATUS_SUCCESS) { val rangeRemaining = data.rangeRemainingMeters.value } else { // Handle error } } carInfo.addEnergyLevelListener(carContext.mainExecutor, listener) … // Unregister the listener when you no longer need updates carInfo.removeEnergyLevelListener(listener)
Java
CarInfo carInfo = getCarContext().getCarService(CarHardwareManager.class).getCarInfo(); OnCarDataAvailableListener<EnergyLevel> listener = (data) -> { if(data.getRangeRemainingMeters().getStatus() == CarValue.STATUS_SUCCESS) { float rangeRemaining = data.getRangeRemainingMeters().getValue(); } else { // Handle error } }; carInfo.addEnergyLevelListener(getCarContext().getMainExecutor(), listener); … // Unregister the listener when you no longer need updates carInfo.removeEnergyLevelListener(listener);
不要假设汽车数据始终可用。如果遇到错误,请检查请求的值的 状态,以更好地了解为什么无法检索请求的数据。有关完整的 CarInfo
类定义,请参阅 参考文档。
CarSensors
CarSensors
类允许您访问车辆的加速度计、陀螺仪、指南针和位置数据。这些值的可用性可能取决于 OEM。加速度计、陀螺仪和指南针数据的格式与您从 SensorManager
API 获取的格式相同。例如,要检查车辆的方向
Kotlin
val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors val listener = OnCarDataAvailableListener<Compass> { data -> if (data.orientations.status == CarValue.STATUS_SUCCESS) { val orientation = data.orientations.value } else { // Data not available, handle error } } carSensors.addCompassListener(CarSensors.UPDATE_RATE_NORMAL, carContext.mainExecutor, listener) … // Unregister the listener when you no longer need updates carSensors.removeCompassListener(listener)
Java
CarSensors carSensors = getCarContext().getCarService(CarHardwareManager.class).getCarSensors(); OnCarDataAvailableListener<Compass> listener = (data) -> { if (data.getOrientations().getStatus() == CarValue.STATUS_SUCCESS) { List<Float> orientations = data.getOrientations().getValue(); } else { // Data not available, handle error } }; carSensors.addCompassListener(CarSensors.UPDATE_RATE_NORMAL, getCarContext().getMainExecutor(), listener); … // Unregister the listener when you no longer need updates carSensors.removeCompassListener(listener);
要访问汽车的位置数据,您还需要声明并请求 android.permission.ACCESS_FINE_LOCATION
权限。
测试
要在 Android Auto 上进行测试时模拟传感器数据,请参阅桌面主机单元指南的 传感器 和 传感器配置 部分。要在 Android Automotive OS 上进行测试时模拟传感器数据,请参阅 Android Automotive OS 模拟器指南的 模拟硬件状态 部分。
CarAppService、Session 和 Screen 生命周期
Session
和 Screen
类实现了 LifecycleOwner
接口。当用户与应用交互时,会调用您的 Session
和 Screen
对象的生命周期回调,如下面的图表所示。
CarAppService 和 Session 的生命周期
有关完整详细信息,请参阅 Session.getLifecycle
方法的文档。
Screen 的生命周期
有关完整详细信息,请参阅 Screen.getLifecycle
方法的文档。
从汽车麦克风录制
使用您的应用的 CarAppService
和 CarAudioRecord
API,您可以让您的应用访问用户的汽车麦克风。用户需要授予您的应用访问汽车麦克风的权限。您的应用可以在您的应用中录制和处理用户的输入。
录制权限
在录制任何音频之前,您必须首先在 AndroidManifest.xml
中声明录制权限,并请求用户授予该权限。
<manifest ...>
...
<uses-permission android:name="android.permission.RECORD_AUDIO" />
...
</manifest>
您需要在运行时请求录制权限。有关如何在汽车应用中请求权限的详细信息,请参阅 请求权限 部分。
录制音频
用户授予录制权限后,您可以录制音频并处理录制内容。
Kotlin
val carAudioRecord = CarAudioRecord.create(carContext) carAudioRecord.startRecording() val data = ByteArray(CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE) while(carAudioRecord.read(data, 0, CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE) >= 0) { // Use data array // Potentially call carAudioRecord.stopRecording() if your processing finds end of speech } carAudioRecord.stopRecording()
Java
CarAudioRecord carAudioRecord = CarAudioRecord.create(getCarContext()); carAudioRecord.startRecording(); byte[] data = new byte[CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE]; while (carAudioRecord.read(data, 0, CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE) >= 0) { // Use data array // Potentially call carAudioRecord.stopRecording() if your processing finds end of speech } carAudioRecord.stopRecording();
音频焦点
从汽车麦克风录制时,首先获取 音频焦点 以确保停止任何正在进行的媒体。如果您失去音频焦点,请停止录制。
以下是获取音频焦点的示例
Kotlin
val carAudioRecord = CarAudioRecord.create(carContext) // Take audio focus so that user's media is not recorded val audioAttributes = AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) // Use the most appropriate usage type for your use case .setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE) .build() val audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) .setAudioAttributes(audioAttributes) .setOnAudioFocusChangeListener { state: Int -> if (state == AudioManager.AUDIOFOCUS_LOSS) { // Stop recording if audio focus is lost carAudioRecord.stopRecording() } } .build() if (carContext.getSystemService(AudioManager::class.java) .requestAudioFocus(audioFocusRequest) != AudioManager.AUDIOFOCUS_REQUEST_GRANTED ) { // Don't record if the focus isn't granted return } carAudioRecord.startRecording() // Process the audio and abandon the AudioFocusRequest when done
Java
CarAudioRecord carAudioRecord = CarAudioRecord.create(getCarContext()); // Take audio focus so that user's media is not recorded AudioAttributes audioAttributes = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) // Use the most appropriate usage type for your use case .setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE) .build(); AudioFocusRequest audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) .setAudioAttributes(audioAttributes) .setOnAudioFocusChangeListener(state -> { if (state == AudioManager.AUDIOFOCUS_LOSS) { // Stop recording if audio focus is lost carAudioRecord.stopRecording(); } }) .build(); if (getCarContext().getSystemService(AudioManager.class).requestAudioFocus(audioFocusRequest) != AUDIOFOCUS_REQUEST_GRANTED) { // Don't record if the focus isn't granted return; } carAudioRecord.startRecording(); // Process the audio and abandon the AudioFocusRequest when done
测试库
面向汽车的 Android 测试库 提供了辅助类,您可以使用这些类在测试环境中验证应用的行为。例如,SessionController
允许您模拟与主机的连接,并验证是否创建并返回了正确的 Screen
和 Template
。
有关用法示例,请参阅 示例。
报告面向汽车的 Android 应用库问题
如果您发现库存在问题,请使用 Google Issue Tracker 报告。请务必在问题模板中填写所有必需的信息。
在提交新问题之前,请检查库的发行说明或问题列表中是否已列出该问题。您可以通过点击跟踪器中问题旁边的星号来订阅和投票。有关更多信息,请参阅 订阅问题。