Android for Cars 应用库可以帮助您实现汽车导航应用、地图注点 (POI) 应用和物联网 (IOT) 应用。为此,它提供了一系列模板,这些模板符合防止驾驶员分心的标准,并且它还解决了一些细节问题,例如存在各种车载显示屏类型和输入模式的问题。
本指南简要介绍了该库的关键功能和概念,并引导您逐步完成设置基本应用的过程。
准备工作
- 查看 Design for Driving 中涉及汽车应用库的页面
- 查看以下部分列出的关键术语和概念。
- 熟悉 Android Auto 系统界面和 Android Automotive OS 设计。
- 查看版本说明。
- 查看示例。
关键术语和概念
- 模型和模板
- 界面由模型对象的图来表示,这些模型对象可以按照它们所属的模板允许的不同方式排列在一起。模板是模型的子集,它们可以在这些图中充当根。模型包含要以文字和图片的形式显示给用户的信息,以及用于配置此类信息的视觉外观各个方面(例如,文字颜色或图片大小)的属性。主机会将模型转换为符合防止驾驶员分心标准的视图,还解决了一些细节问题,例如存在各种车载显示屏类型和输入模式。
- 主机
- 主机是一个后端组件,它会实现库的 API 提供的功能,以便您的应用在汽车中运行。从发现应用并管理其生命周期,到将模型转换为视图,再到将用户交互操作通知给应用,这些都属于主机的职责范围。在移动设备上,此主机由 Android Auto 实现。在 Android Automotive OS 上,此主机作为系统应用进行安装。
- 模板限制
- 不同的模板会对其模型的内容施加限制。例如,列表模板对可以呈现给用户的项数有限制。模板对可以采用什么方式连接它们以形成任务流也有限制。例如,应用最多只能将 5 个模板推送到屏幕堆栈。如需了解详情,请参阅模板限制。
Screen
Screen
是一个由库提供的类,应用实现该类来管理呈现给用户的界面。Screen
具有生命周期,并提供了一种机制,可让应用发送要在屏幕可见时显示的模板。此外,也可以将Screen
实例推送到Screen
堆栈以及将从该堆栈中弹出这些实例,这样可以确保它们遵循模板流限制。CarAppService
CarAppService
是一个抽象Service
类,应用必须实现并导出该类,才能被主机发现并由主机进行管理。应用的CarAppService
负责使用createHostValidator
验证主机连接是否可以信任,随后使用onCreateSession
为每个连接提供Session
实例。Session
Session
是一个抽象类,应用必须使用CarAppService.onCreateSession
实现并返回该类。它充当在车载显示屏上显示信息的入口点,并且具有生命周期,可告知车载显示屏上应用的当前状态,例如当应用可见或隐藏时。当
Session
开始时(例如当应用首次启动时),主机会使用onCreateScreen
方法请求要显示的初始Screen
。
安装汽车应用库
有关如何将库添加到应用的说明,请参阅 Jetpack 库发布页面。
配置应用的清单文件
您需要先按照以下说明配置应用的清单文件,然后才能创建汽车应用。
声明 CarAppService
主机通过 CarAppService
实现连接到您的应用。您应在清单中声明此服务,以允许主机发现并连接到您的应用。
您还需要在应用的 intent 过滤器的 <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
时,您可以通过在 intent 过滤器中添加以下一个或多个类别值来声明应用的类别,如上一部分中所述:
androidx.car.app.category.NAVIGATION
:此类应用提供精细导航方向。请参阅构建车载导航应用。androidx.car.app.category.POI
:此类应用提供与查找停车位、充电站和加油站等地图注点相关的功能。请参阅构建车载地图注点应用。androidx.car.app.category.IOT
:此类应用可让用户在车内对已连接的设备执行相关操作。请参阅构建车载物联网应用。androidx.car.app.category.WEATHER
:此类应用可让用户查看与其当前位置或沿途相关的相关天气信息。请参阅构建车载天气应用。
如需获取各个类别的详细说明以及有关属于各个类别的应用的标准,请参阅 Android 汽车应用质量。
指定应用名称和图标
您需要指定应用名称和图标,主机可以使用它们在系统界面中表示您的应用。
您可以使用 CarAppService
的 label
和 icon
属性来指定用于表示应用的应用名称和图标:
...
<service
android:name=".MyCarAppService"
android:exported="true"
android:label="@string/my_app_name"
android:icon="@drawable/my_app_icon">
...
</service>
...
如果未在 <service>
元素中声明标签或图标,主机将回退到使用为 <application>
元素指定的值。
设置自定义主题
若要为您的汽车应用设置自定义主题,请在清单文件中添加 <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
方法来创建由应用显示的屏幕,该方法会返回 Template
实例,它表示要在车载显示屏上显示的界面状态。
以下代码段展示了如何声明 Screen
,它使用 PaneTemplate
模板显示简单的“Hello World!”字符串:
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
还提供了一些其他功能,例如允许您使用车载显示屏上的配置加载可绘制资源、使用 intent 在汽车中启动应用,以及指示您的应用是否应以深色主题显示其地图。
实现屏幕导航
应用通常会呈现许多不同的屏幕,每个屏幕可能会利用不同的模板,用户可以在与屏幕中显示的界面交互时浏览这些屏幕。
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
对象是自动调用 ScreenManager.pop
的标准 Action
。可通过使用 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 |
导航、地图注点、天气 |
MapTemplate (已废弃) |
androidx.car.app.NAVIGATION_TEMPLATES |
导航 |
PlaceListNavigationTemplate (已废弃) |
androidx.car.app.NAVIGATION_TEMPLATES |
导航 |
RoutePreviewNavigationTemplate (已废弃) |
androidx.car.app.NAVIGATION_TEMPLATES |
导航 |
声明 Surface 权限
除了应用所使用的模板所需的权限之外,应用还必须在其 AndroidManifest.xml
文件中声明 androidx.car.app.ACCESS_SURFACE
权限,才能访问 Surface:
<manifest ...>
...
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
...
</manifest>
访问 Surface
如需访问主机提供的 Surface
,您必须实现 SurfaceCallback
并将该实现提供给 AppManager
汽车服务。当前 Surface
会在 onSurfaceAvailable()
和 onSurfaceDestroyed()
回调的 SurfaceContainer
参数中传递给 SurfaceCallback
。
Kotlin
carContext.getCarService(AppManager::class.java).setSurfaceCallback(surfaceCallback)
Java
carContext.getCarService(AppManager.class).setSurfaceCallback(surfaceCallback);
了解 Surface 设备的可见区域
主机可以在地图上绘制模板的界面元素。主机将通过调用 SurfaceCallback.onVisibleAreaChanged
来告知保证不被遮挡且完全对用户可见的 Surface 区域。此外,为了最大限度地减少更改次数,主机还会使用根据当前模板将会始终可见的最小矩形来调用 SurfaceCallback.onStableAreaChanged
方法。
例如,如果导航应用使用的是顶部带有操作条的 NavigationTemplate
,当用户有一段时间没有与屏幕交互时,该操作条可能会自动隐藏,以便为地图腾出更多空间。在这种情况下,将使用相同的矩形对 onStableAreaChanged
和 onVisibleAreaChanged
进行回调。当操作栏处于隐藏状态时,仅使用较大的区域调用 onVisibleAreaChanged
。如果用户与屏幕交互,则同样仅使用第一个矩形调用 onVisibleAreaChanged
。
支持深色主题
当主机确定条件允许时,应用必须使用适当的深色将地图重新绘制到 Surface
实例上,如 Android 汽车应用质量中所述。
为了决定是否应绘制深色地图,您可以使用 CarContext.isDarkMode
方法。每当深色主题状态发生变化时,您都会收到对 Session.onCarConfigurationChanged
的调用。
允许用户与您的地图互动
使用以下模板时,您可以添加相应支持来允许用户与您绘制的地图互动,例如让用户通过缩放和平移查看地图的不同部分。
模板 | 从哪个 Car App API 级别起支持相应互动功能 |
---|---|
NavigationTemplate |
2 |
PlaceListNavigationTemplate (已废弃) |
4 |
RoutePreviewNavigationTemplate (已废弃) |
4 |
MapTemplate (已废弃) |
5(模板简介) |
MapWithContentTemplate |
7(模板简介) |
实现互动回调
SurfaceCallback
接口提供了多个回调方法,您可以通过实现这些方法,为使用上一部分中的模板构建的地图添加互动性:
互动 | SurfaceCallback 方法 |
从哪个 Car App API 级别开始支持 |
---|---|---|
点按 | onClick |
5 |
双指张合即可缩放 | onScale |
2 |
单点触控拖动 | onScroll |
2 |
单点触控快速滑动 | onFling |
2 |
点按两次 | onScale (缩放比例由模板主机决定) |
2 |
平移模式下的旋转轻推 | onScroll (距离系数由模板主机决定) |
2 |
添加地图操作栏
这些模板可以为地图相关操作(例如放大和缩小、重新居中、显示罗盘或您选择显示的其他操作)提供地图操作栏。地图操作栏最多可包含四个仅显示图标的按钮,这些按钮可在不影响任务深度的情况下刷新。它在空闲状态下会隐藏,在活跃状态下则重新出现。
如需接收地图互动回调,您必须在地图操作栏中添加一个 Action.PAN
按钮。当用户按平移按钮时,主机会进入平移模式,具体如下一部分所述。
如果应用的地图操作栏中没有 Action.PAN
按钮,您将无法从 SurfaceCallback
方法接收用户输入,并且主机会退出先前启用的任何平移模式。
触摸屏上不会显示平移按钮。
了解平移模式
在平移模式下,模板主机会将来自非触控输入设备(例如旋控器和触控板)的用户输入在转换后传递给相应的 SurfaceCallback
方法。系统会使用 NavigationTemplate.Builder
中的 setPanModeListener
方法响应用户操作来进入或退出平移模式。当用户处于平移模式时,主机可以隐藏模板中的其他界面组件。
与用户互动
您的应用可以使用与移动应用类似的模式与用户互动。
处理用户输入
应用可通过将适当的监听器传递给支持它们的模型来响应用户输入。以下代码段展示了如何创建一个 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
intent 的格式),请参阅使用 intent 启动汽车应用部分。
某些操作(例如那些需要引导用户在其移动设备上继续交互的操作)只有在汽车停好后才允许执行。您可以使用 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
文档中所述。
如果调用 NotificationCompat.Builder.setOnlyAlertOnce
且将值设置为 true
,则高优先级通知将只以 HUN 的形式显示一次。
如需详细了解如何设计汽车应用的通知,请参阅 Google Design for Driving 中有关通知的指南。
显示消息框
应用可以使用 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 启动您自己的应用。
以下示例展示了如何创建一条通知,该通知包含一项操作,即打开应用中显示停车预订详情的屏幕。您可以使用内容 intent 扩展通知实例,该 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
,当用户在通知界面中选择相应的操作并使用包含数据 URI 的 intent 调用 CarContext.startCarApp
时,会调用该类来处理 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())); } } }
如需详细了解如何处理汽车应用的通知,请参阅显示通知部分。
模板限制
主机将针对给定任务显示的模板数限制为最多 5 个,在这 5 个模板中,最后一个模板必须是以下某种类型:
请注意,此限制适用于模板数,而不是堆栈中的 Screen
实例数。例如,如果在屏幕 A 中,应用发送了 2 个模板,然后推送屏幕 B,那么它现在可以再发送 3 个模板。或者,如果将每个屏幕的结构都设置为发送单个模板,那么应用可以将 5 个屏幕实例推送到 ScreenManager
堆栈上。
这些限制有一些特殊情况:模板刷新、返回和重置操作。
模板刷新
某些内容更新不计入模板限制。一般来说,如果应用推送的新模板所属的类型及其包含的主要内容与之前的模板相同,新模板就不会被计入配额。例如,更新 ListTemplate
中某一行的切换状态不会计入配额。如需详细了解可将哪些类型的内容更新视为刷新,请参阅各个模板的文档。
返回操作
为了在任务中启用子流,主机会检测应用何时从 ScreenManager
堆栈中弹出 Screen
,并根据应用倒退的模板数更新剩余配额。
例如,如果在屏幕 A 中,应用发送了 2 个模板,然后推送屏幕 B 并且又发送了 2 个模板,那么应用的剩余配额就为 1。如果应用随后弹回到屏幕 A,主机会将配额重置为 3,因为应用倒退了 2 个模板。
请注意,当弹回到某个屏幕时,应用发送的模板所属的类型必须与该屏幕上次发送的模板的类型相同。发送任何其他类型的模板会导致出现错误。不过,只要类型在返回操作期间保持不变,应用就可以随意修改模板的内容,而不会影响配额。
重置操作
某些模板具有表示任务结束的特殊语义。例如,NavigationTemplate
是一个视图,它应该会持续显示在屏幕上,并使用新的精细导航指示进行刷新,以供用户使用。到达其中一个模板时,主机会重置模板配额,将该模板当作新任务的第一步来对待,从而使应用能够开始新任务。如需了解哪些模板会在主机上触发重置操作,请参阅各个模板的文档。
如果主机收到通过通知操作或从启动器启动应用的 intent,也会重置配额。此机制使应用能够从通知开始新任务流,即使应用已绑定且在前台运行,也是如此。
如需详细了解如何在车载显示屏上显示应用的通知,请参阅显示通知部分。如需了解如何通过通知操作启动应用,请参阅使用 intent 启动汽车应用部分。
Connection 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(); }
Constraints 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
等模板以及汽车应用 API 2 及更高级别,以处理在汽车的车机上登录您应用的操作。
若要创建 SignInTemplate
,请定义 SignInMethod
。汽车应用库目前支持以下登录方法:
InputSignInMethod
:用于用户名/密码登录。PinSignInMethod
:用于 PIN 码登录,对于这种方法,用户可以使用车机上显示的 PIN 码从手机上关联其账号。ProviderSignInMethod
:用于提供商登录,例如 Google 登录和一键快捷功能。QRCodeSignInMethod
:用于二维码登录,用户在手机上扫描二维码即可完成登录。此方法适用于汽车 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 可以在车辆中启用“访客”体验,在这种体验模式下,无法添加账号。
添加文本字符串变体
不同尺寸的车载显示屏可以显示不同的文本量。利用汽车应用 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
添加内嵌文本的图标,从而提升应用的视觉吸引力。如需详细了解如何创建这些 span,请参阅有关 CarIconSpan.create
的文档。如需简要了解使用 span 设置文本样式的原理,请参阅使用 span 设置文本样式。
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();
Car Hardware API
从 Car App API 级别 3 开始,汽车应用库提供可用于访问车辆属性和传感器的 API。
要求
若要将 API 与 Android Auto 搭配使用,首先要将 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 App API 级别开始支持 |
---|---|---|---|---|
fetchModel |
品牌、型号、年份 | android.car.permission.CAR_INFO |
3 | |
fetchEnergyProfile |
EV 连接器类型、燃料类型 | com.google.android.gms.permission.CAR_FUEL |
android.car.permission.CAR_INFO |
3 |
fetchExteriorDimensions
此数据仅适用于搭载 API 30 或更高版本的部分 Android Automotive OS 车辆 |
外部尺寸 | 不适用 | 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 for Cars 测试库提供了一些辅助类,可用于在测试环境中验证应用的行为。例如,借助 SessionController
,您可以模拟与主机的连接,验证是否创建并返回正确的 Screen
和 Template
。
请参阅示例,查看使用示例。
报告 Android for Cars 应用库问题
如果您发现该库存在问题,请使用 Google 问题跟踪器报告该问题。请务必在问题模板中填写所有必填信息。
在提交新问题之前,请先查看该问题是否已在库的版本说明中列出或在问题列表中报告。您可以在跟踪器中点击问题的星标来对问题进行订阅和投票。如需了解详情,请参阅订阅问题。