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
。
安装 Car App 库
有关如何将库添加到您的应用的说明,请参阅 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>
元素指定的那些值。
设置自定义主题
要为您的汽车应用设置自定义主题,请在清单文件中添加<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>
Car App API 级别
Car App 库定义了自己的 API 级别,以便您可以知道车辆上的模板主机支持哪些库功能。要检索主机支持的最高 Car App API 级别,请使用getCarAppApiLevel()
方法。
在您的AndroidManifest.xml
文件中声明您的应用支持的最低 Car App API 级别
<manifest ...>
<application ...>
<meta-data
android:name="androidx.car.app.minCarApiLevel"
android:value="1"/>
</application>
</manifest>
有关如何维护向后兼容性和声明使用功能所需的最低 API 级别,请参阅RequiresCarApi
注释的文档。有关使用 Car App 库的某个功能所需的 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
会使用第一个矩形进行调用。
支持深色主题
如汽车 Android 应用质量中所述,当主机确定条件需要时,应用必须使用正确的深色重新绘制其地图到 Surface
实例上。
要决定是否绘制深色地图,您可以使用 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()
方法。
使用 CarContext.requestPermissions()
相比使用 标准 Android API 的好处在于,您无需启动自己的 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
并包含数据 URI 的 Intent 时,将调用该接收器来处理 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();
按从最优到最不优的顺序添加字符串——例如,从最长到最短。主机根据汽车屏幕上的可用空间选择合适的长度字符串。
为行添加内联 CarIcons
您可以使用 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 App API 级别 3 开始,Car App 库提供了一些 API,您可以使用它们来访问车辆属性和传感器。
要求
要将这些 API 与 Android Auto 一起使用,请先在 Android Auto 模块的 build.gradle
文件中添加对 androidx.car.app:app-projected
的依赖项。对于 Android Automotive OS,请在 Android Automotive OS 模块的 build.gradle
文件中添加对 androidx.car.app:app-automotive
的依赖项。
此外,在您的 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.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 App 库问题
如果您发现库存在问题,请使用 Google Issue Tracker 报告。请务必在问题模板中填写所有请求的信息。
在提交新问题之前,请检查库的发行说明或问题列表中是否已列出该问题。您可以通过点击跟踪器中问题旁边的星号来订阅和投票支持问题。有关更多信息,请参阅 订阅问题。