1. 开始之前
在这个 codelab 中,您将学习如何使用 Android for Cars 应用库 为 Android Auto 和 Android Automotive OS 构建针对驾驶场景优化的应用。您首先添加对 Android Auto 的支持,然后只需少量额外工作即可创建可在 Android Automotive OS 上运行的应用变体。在使应用在这两个平台上都能运行之后,您将构建一个额外的屏幕和一些基本交互功能!
本 codelab 不包含以下内容
- 有关如何为 Android Auto 和 Android Automotive OS 创建媒体(音频)应用的指南。有关如何构建此类应用的详细信息,请参阅 为汽车构建媒体应用。
- 有关如何为 Android Auto 创建消息应用的指南。有关如何构建此类应用的详细信息,请参阅 为 Android Auto 构建消息应用。
您需要什么
- Android Studio 预览版。Android Automotive OS 模拟器仅通过 Android Studio 预览版提供。如果您尚未安装 Android Studio 预览版,则可以在预览版下载的同时使用稳定版开始 codelab。
- 基本的 Kotlin 经验。
- Android 服务 的基本知识。
- 创建 Android 虚拟设备 和在 Android 模拟器中运行它们 的经验。
- 关于 Android 应用模块化 的基本知识。
- 关于 构建器设计模式 的基本知识。
您将构建什么
Android Auto | Android Automotive OS |
您将学习什么
- 汽车应用库的客户端-主机架构的工作原理。
- 如何编写您自己的
CarAppService
、Session
和Screen
类。 - 如何在 Android Auto 和 Android Automotive OS 中共享您的实现。
- 如何使用桌面车载单元在您的开发机器上运行 Android Auto。
- 如何运行 Android Automotive OS 模拟器。
2. 设置
获取代码
- 此 codelab 的代码可在
car-codelabs
GitHub 存储库中的car-app-library-fundamentals
目录中找到。要克隆它,请运行以下命令
git clone https://github.com/android/car-codelabs.git
- 或者,您可以将存储库下载为 ZIP 文件
打开项目
- 启动 Android Studio 后,导入项目,只选择
car-app-library-fundamentals/start
目录。car-app-library-fundamentals/end
目录包含解决方案代码,如果您遇到困难或只想查看完整的项目,可以随时参考。
熟悉代码
- 在 Android Studio 中打开项目后,花一些时间浏览起始代码。
请注意,应用的起始代码被分解为两个模块::app
和 :common:data
。
:app
模块包含移动应用的 UI 和逻辑,:common:data
模块包含 Place
模型 数据类 和用于读取 Place
模型的 存储库。为简便起见,存储库从硬编码列表中读取,但在实际应用中可以轻松地从数据库或后端服务器读取。
:app
模块包含对 :common:data
模块的依赖项,以便它可以读取和显示 Place
模型列表。
3. 了解 Android for Cars 应用库
Android for Cars 应用库是 一组 Jetpack 库,使开发者能够构建可在车辆中使用的应用。它提供了一个模板框架,该框架提供针对驾驶场景优化的用户界面,同时还负责适应汽车中存在的各种硬件配置(例如,输入方法、屏幕尺寸和纵横比)。总而言之,这使您作为开发者可以轻松构建应用,并确信它将在运行 Android Auto 和 Android Automotive OS 的各种车辆上都能良好运行。
了解其工作原理
使用汽车应用库构建的应用不会直接在 Android Auto 或 Android Automotive OS 上运行。相反,它们依赖于与客户端应用通信并代表客户端呈现其用户界面的主机应用。Android Auto 本身就是一个主机,而 Google 汽车应用主机 是在内置 Google 的 Android Automotive OS 车辆上使用的主机。以下是构建应用时必须扩展的汽车应用库的关键类
CarAppService
CarAppService
是 Android 的 Service
类的子类,是主机应用与客户端应用(例如您在本 codelab 中构建的应用)通信的入口点。其主要目的是创建主机应用与其交互的 Session
实例。
Session
您可以将Session
视为在车辆显示屏上运行的客户端应用程序的一个实例。与其他 Android 组件一样,它也有自己的生命周期,可用于初始化和拆除在Session
实例存在期间使用的资源。CarAppService
和Session
之间存在一对多的关系。例如,一个CarAppService
可能有两个Session
实例,一个用于主显示屏,另一个用于导航应用程序的集群显示屏(对于支持集群屏幕的导航应用程序)。
屏幕
Screen
实例负责生成主机应用程序呈现的用户界面。这些用户界面由Template
类表示,每个类都模拟特定类型的布局,例如网格或列表。每个Session
管理一个Screen
实例堆栈,这些实例处理用户在应用程序不同部分的流程。与Session
一样,Screen
也有自己的生命周期,您可以将其挂钩。
您可以在本代码实验室的编写您的 CarAppService部分编写CarAppService
、Session
和Screen
,因此如果您还不完全明白,请不要担心。
4. 设置初始配置
首先,设置包含CarAppService
的模块并声明其依赖项。
创建 car-app-service 模块
- 在项目窗口中选择
:common
模块,右键单击并选择**新建 > 模块**选项。 - 在打开的模块向导中,选择左侧列表中的**Android 库**模板(以便此模块可以用作其他模块的依赖项),然后使用以下值
- 模块名称:
:common:car-app-service
- 包名称:
com.example.places.carappservice
- 最低 SDK:
API 23: Android 6.0 (棉花糖)
设置**依赖项**
- 在项目级
build.gradle
文件中,添加 Car 应用库版本的变量声明,如下所示。这使您可以轻松地在应用程序的每个模块中使用相同的版本。
build.gradle (Project: Places)
buildscript {
ext {
// All versions can be found at https://developer.android.com/jetpack/androidx/releases/car-app
car_app_library_version = '1.3.0-rc01'
...
}
}
- 接下来,向
:common:car-app-service
模块的build.gradle
文件添加两个依赖项。
androidx.car.app:app
。这是 Car 应用库的主要工件,其中包含构建应用时使用的所有核心类。还有其他三个工件构成了该库,androidx.car.app:app-projected
用于 Android Auto 特定的功能,androidx.car.app:app-automotive
用于 Android Automotive OS 功能代码,以及androidx.car.app:app-testing
用于单元测试的一些有用的帮助程序。您将在后面的代码实验室中使用app-projected
和app-automotive
。:common:data
。这是现有移动应用程序使用的相同数据模块,允许为应用程序体验的每个版本使用相同的数据源。
build.gradle (Module :common:car-app-service)
dependencies {
...
implementation "androidx.car.app:app:$car_app_library_version"
implementation project(":common:data")
...
}
进行此更改后,应用程序自身模块的依赖关系图如下所示
现在依赖项已设置完毕,是时候编写CarAppService
了!
5. 编写您的 CarAppService
- 首先在
:common:car-app-service
模块中的carappservice
包内创建一个名为PlacesCarAppService.kt
的文件。 - 在此文件中,创建一个名为
PlacesCarAppService
的类,该类扩展CarAppService
。
PlacesCarAppService.kt
class PlacesCarAppService : CarAppService() {
override fun createHostValidator(): HostValidator {
return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
}
override fun onCreateSession(): Session {
// PlacesSession will be an unresolved reference until the next step
return PlacesSession()
}
}
CarAppService
抽象类为您实现了Service
方法(例如onBind
和onUnbind
),并防止进一步覆盖这些方法以确保与主机应用程序的正确互操作性。您只需实现createHostValidator
和onCreateSession
即可。
当绑定您的CarAppService
时,将引用您从createHostValidator
返回的HostValidator
,以确保主机是受信任的,并且如果主机与您定义的参数不匹配,则绑定将失败。出于本代码实验室(以及一般测试)的目的,ALLOW_ALL_HOSTS_VALIDATOR
使确保您的应用连接变得容易,但不应在生产环境中使用。有关如何为生产应用配置此内容的更多信息,请参阅createHostValidator
的文档。
对于像这样的简单应用,onCreateSession
可以简单地返回Session
的实例。在一个更复杂的应用中,这将是初始化在您的应用在车辆上运行时使用的长期资源(例如指标和日志记录客户端)的好地方。
- 最后,您需要在
:common:car-app-service
模块的AndroidManifest.xml
文件中添加与PlacesCarAppService
对应的<service>
元素,以便让操作系统(以及其他应用程序,例如主机)知道它的存在。
AndroidManifest.xml (:common:car-app-service)
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!--
This AndroidManifest.xml will contain all of the elements that should be shared across the
Android Auto and Automotive OS versions of the app, such as the CarAppService <service> element
-->
<application>
<service
android:name="com.example.places.carappservice.PlacesCarAppService"
android:exported="true">
<intent-filter>
<action android:name="androidx.car.app.CarAppService" />
<category android:name="androidx.car.app.category.POI" />
</intent-filter>
</service>
</application>
</manifest>
这里需要注意两点
<action>
元素允许主机(和启动器)应用程序查找该应用程序。<category>
元素声明应用程序的类别,这决定了它必须满足哪些应用程序质量标准(稍后将详细介绍)。其他可能的值为androidx.car.app.category.NAVIGATION
和androidx.car.app.category.IOT
。
创建 PlacesSession 类
- 创建一个
PlacesCarAppService.kt
文件并添加以下代码
PlacesCarAppService.kt
class PlacesSession : Session() {
override fun onCreateScreen(intent: Intent): Screen {
// MainScreen will be an unresolved reference until the next step
return MainScreen(carContext)
}
}
对于像这样的简单应用程序,您可以在onCreateScreen
中只返回主屏幕。但是,由于此方法采用Intent
作为参数,因此功能更丰富的应用程序也可能会从中读取并填充屏幕的后退堆栈或使用其他一些条件逻辑。
创建 MainScreen 类
接下来,创建一个名为screen.
的新包。
- 右键单击
com.example.places.carappservice
包并选择**新建** > **包**(它的完整包名称将为com.example.places.carappservice.screen
)。您将在此处放置应用程序的所有Screen
子类。 - 在
screen
包中,创建一个名为MainScreen.kt
的文件以包含MainScreen
类,该类扩展Screen
。目前,它使用PaneTemplate
显示简单的“Hello, world!”消息。
MainScreen.kt
class MainScreen(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()
}
}
6. 添加对 Android Auto 的支持
尽管您现在已经实现了使应用程序启动并运行所需的所有逻辑,但在您可以在 Android Auto 上运行它之前,还需要设置另外两个配置部分。
添加对 car-app-service 模块的依赖项
在:app
模块的build.gradle
文件中,添加以下内容
build.gradle (Module :app)
dependencies {
...
implementation project(path: ':common:car-app-service')
...
}
进行此更改后,应用程序自身模块的依赖关系图如下所示
这将把您刚刚在:common:car-app-service
模块中编写的代码与 Car 应用库中包含的其他组件(例如提供的权限授予活动)捆绑在一起。
声明 com.google.android.gms.car.application 元数据
- 右键单击
:common:car-app-service
模块并选择**新建** > **Android 资源文件**选项,然后覆盖以下值
- 文件名:
automotive_app_desc.xml
- 资源类型:
XML
- 根元素:
automotiveApp
- 在该文件中,添加以下
<uses>
元素,以声明您的应用使用 Car 应用库提供的模板。
automotive_app_desc.xml
<?xml version="1.0" encoding="utf-8"?>
<automotiveApp>
<uses name="template"/>
</automotiveApp>
- 在
:app
模块的AndroidManifest.xml
文件中,添加以下<meta-data>
元素,该元素引用您刚刚创建的automotive_app_desc.xml
文件。
AndroidManifest.xml (:app)
<application ...>
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc" />
...
</application>
Android Auto 读取此文件,并让它知道您的应用支持哪些功能——在本例中,它使用 Car 应用库的模板系统。然后,此信息将用于处理添加应用到 Android Auto 启动器和从通知中打开它等行为。
可选:侦听投影更改
有时,您想知道用户的设备是否连接到汽车。您可以使用CarConnection
API 来实现此目的,该 API 提供一个LiveData
,让您可以观察连接状态。
- 要使用
CarConnection
API,首先在:app
模块中添加对androidx.car.app:app
工件的依赖项。
build.gradle (Module :app)
dependencies {
...
implementation "androidx.car.app:app:$car_app_library_version"
...
}
- 出于演示目的,您可以创建一个简单的 Composable,例如以下显示当前连接状态的 Composable。在一个真实的应用程序中,此状态可能会被捕获到某些日志中,用于在投影时禁用手机屏幕上的某些功能,或者其他一些操作。
MainActivity.kt
@Composable
fun ProjectionState(carConnectionType: Int, modifier: Modifier = Modifier) {
val text = when (carConnectionType) {
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "Not projecting"
CarConnection.CONNECTION_TYPE_NATIVE -> "Running on Android Automotive OS"
CarConnection.CONNECTION_TYPE_PROJECTION -> "Projecting"
else -> "Unknown connection type"
}
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
modifier = modifier
)
}
- 现在已经可以显示数据了,接下来读取数据并将其传递给 Composable,如下代码片段所示。
MainActivity.kt
setContent {
val carConnectionType by CarConnection(this).type.observeAsState(initial = -1)
PlacesTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column {
Text(
text = "Places",
style = MaterialTheme.typography.displayLarge,
modifier = Modifier.padding(8.dp)
)
ProjectionState(
carConnectionType = carConnectionType,
modifier = Modifier.padding(8.dp)
)
PlaceList(places = PlacesRepository().getPlaces())
}
}
}
}
- 运行应用程序后,它应该显示“未投影”。
7. 使用桌面车载单元 (DHU) 进行测试
实现CarAppService
并配置好 Android Auto 后,就可以运行应用程序并查看其外观了。
- 将应用程序安装到您的手机上,然后按照安装和运行 DHU 的说明进行操作。
DHU 运行后,您应该在启动器中看到应用程序图标(如果没有,请仔细检查您是否已按照上一节中的所有步骤操作,然后从终端退出并重新启动 DHU)。
糟糕——它崩溃了!
- 要查看应用程序崩溃的原因,您可以切换右上角的调试图标(仅在 DHU 上运行时可见),或检查 Android Studio 中的 Logcat。
Error: [type: null, cause: null, debug msg: java.lang.IllegalArgumentException: Min API level not declared in manifest (androidx.car.app.minCarApiLevel) at androidx.car.app.AppInfo.retrieveMinCarAppApiLevel(AppInfo.java:143) at androidx.car.app.AppInfo.create(AppInfo.java:91) at androidx.car.app.CarAppService.getAppInfo(CarAppService.java:380) at androidx.car.app.CarAppBinder.getAppInfo(CarAppBinder.java:255) at androidx.car.app.ICarApp$Stub.onTransact(ICarApp.java:182) at android.os.Binder.execTransactInternal(Binder.java:1285) at android.os.Binder.execTransact(Binder.java:1244) ]
从日志中,您可以看到清单中缺少应用程序支持的最低 API 级别声明。在添加该条目之前,最好先了解为什么需要它。
与 Android 本身一样,Car App Library 也具有 API 级别概念,因为主机应用程序和客户端应用程序之间需要一个契约才能进行通信。主机应用程序支持给定的 API 级别及其关联的功能(以及为了向后兼容性,也支持早期级别的功能)。例如,SignInTemplate
可用于运行 API 级别 2 或更高版本的主机。但是,如果您尝试将其用于仅支持 API 级别 1 的主机,则该主机将不知道模板类型,并且无法对其执行任何有意义的操作。
在主机与客户端绑定过程中,为了成功绑定,支持的 API 级别必须有一定程度的重叠。例如,如果主机仅支持 API 级别 1,但客户端应用程序如果没有 API 级别 2 的功能(如清单声明所示)就无法运行,则应用程序不应该连接,因为客户端无法在主机上成功运行。因此,客户端必须在其清单中声明最低所需的 API 级别,以确保只有能够支持它的主机才能与其绑定。
- 要设置最低支持的 API 级别,请在
:common:car-app-service
模块的AndroidManfiest.xml
文件中添加以下<meta-data>
元素
AndroidManifest.xml (:common:car-app-service)
<application>
<meta-data
android:name="androidx.car.app.minCarApiLevel"
android:value="1" />
<service android:name="com.example.places.carappservice.PlacesCarAppService" ...>
...
</service>
</application>
- 再次安装应用程序并在 DHU 上启动它,然后您应该看到以下内容
为了完整起见,您还可以尝试将minCarApiLevel
设置为较大的值(例如 100),以查看当主机和客户端不兼容时尝试启动应用程序时会发生什么情况(提示:它会崩溃,类似于未设置任何值的情况)。
同样重要的是要注意,与 Android 本身一样,如果您在运行时验证主机是否支持所需的级别,则可以使用高于声明的最小 API 级别提供的功能。
可选:侦听投影更改
- 如果您在上一步中添加了
CarConnection
侦听器,则当 DHU 运行时,您应该在手机上看到状态更新,如下所示
8. 添加对 Android Automotive OS 的支持
Android Auto 运行后,是时候更进一步,也支持 Android Automotive OS 了。
创建:automotive
**模块**
- 要创建一个包含特定于 Android Automotive OS 版本应用程序的代码的模块,请在 Android Studio 中打开**文件** > **新建** > **新建模块...**,从左侧模板类型列表中选择**Automotive**选项,然后使用以下值
- 应用程序/库名称:
Places
(与主应用程序相同,但如果需要,您也可以选择不同的名称) - 模块名称:
automotive
- 包名称:
com.example.places.automotive
- 语言:
Kotlin
- 最低 SDK:
API 29: Android 10.0 (Q)
——如前面创建:common:car-app-service
模块时所述,所有支持 Car App Library 应用程序的 Android Automotive OS 车辆至少运行 API 29。
- 单击**下一步**,然后在下一个屏幕上选择**无 Activity**,最后单击**完成**。
添加依赖项
与 Android Auto 一样,您需要声明对:common:car-app-service
模块的依赖项。通过这样做,您可以跨两个平台共享您的实现!
此外,您需要添加对androidx.car.app:app-automotive
构件的依赖项。androidx.car.app:app-projected
构件对于 Android Auto 是可选的,而此依赖项在 Android Automotive OS 上是必需的,因为它包含用于运行应用程序的CarAppActivity
。
- 要添加依赖项,请打开
build.gradle
文件,然后插入以下代码
build.gradle (Module :automotive)
dependencies {
...
implementation project(':common:car-app-service')
implementation "androidx.car.app:app-automotive:$car_app_library_version"
...
}
进行此更改后,应用程序自身模块的依赖关系图如下所示
设置清单
- 首先,您需要将两个功能
android.hardware.type.automotive
和android.software.car.templates_host
声明为必需。
android.hardware.type.automotive
是一个系统功能,表示设备本身是车辆(有关更多详细信息,请参阅FEATURE_AUTOMOTIVE
)。只有将此功能标记为必需的应用程序才能提交到 Play Console 上的 Automotive OS 轨道(提交到其他轨道的应用程序不能要求此功能)。android.software.car.templates_host
是仅存在于具有运行模板应用程序所需模板主机的车辆中的系统功能。
AndroidManifest.xml (:automotive)
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature
android:name="android.hardware.type.automotive"
android:required="true" />
<uses-feature
android:name="android.software.car.templates_host"
android:required="true" />
...
</manifest>
- 接下来,您需要声明某些功能不是必需的。
这样做是为了确保您的应用程序与内置 Google 的汽车中提供的各种硬件兼容。例如,如果您的应用程序需要android.hardware.screen.portrait
功能,则它与具有横向屏幕的车辆不兼容,因为大多数车辆中的方向是固定的。这就是为什么对于这些功能将android:required
属性设置为false
的原因。
AndroidManifest.xml (:automotive)
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
...
<uses-feature
android:name="android.hardware.wifi"
android:required="false" />
<uses-feature
android:name="android.hardware.screen.portrait"
android:required="false" />
<uses-feature
android:name="android.hardware.screen.landscape"
android:required="false" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
...
</manifest>
- 接下来,您需要像对 Android Auto 所做的那样添加对
automotive_app_desc.xml
文件的引用。
请注意,这次android:name
属性与之前不同——它不是com.google.android.gms.car.application
,而是com.android.automotive
。与之前一样,这引用了:common:car-app-service
模块中的automotive_app_desc.xml
文件,这意味着相同的资源同时用于 Android Auto 和 Android Automotive OS。请注意,<meta-data>
元素位于<application>
元素内(因此您必须更改application
标记,使其不再是自闭合的)!
AndroidManifest.xml (:automotive)
<application>
...
<meta-data android:name="com.android.automotive"
android:resource="@xml/automotive_app_desc"/>
...
</application>
- 最后,您需要为库中包含的
CarAppActivity
添加一个<activity>
元素。
AndroidManifest.xml (:automotive)
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
...
<application ...>
...
<activity
android:name="androidx.car.app.activity.CarAppActivity"
android:exported="true"
android:launchMode="singleTask"
android:theme="@android:style/Theme.DeviceDefault.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="distractionOptimized"
android:value="true" />
</activity>
</application>
</manifest>
所有这些代码的作用如下
android:name
列出了app-automotive
包中CarAppActivity
类的完全限定类名。android:exported
设置为true
,因为此Activity
必须可以由自身以外的应用程序(启动器)启动。android:launchMode
设置为singleTask
,因此一次只能存在一个CarAppActivity
实例。android:theme
设置为@android:style/Theme.DeviceDefault.NoActionBar
,以便应用程序占用可用的全部屏幕空间。- 意图过滤器指示这是应用程序的启动器
Activity
。 - 有一个
<meta-data>
元素指示系统应用程序可以在存在 UX 限制的情况下使用,例如车辆行驶时。
可选:从 :app 模块复制启动器图标
由于您刚刚创建了:automotive
模块,因此它具有默认的绿色 Android 徽标图标。
- 如果需要,可以将
mipmap
资源目录从:app
模块复制粘贴到:automotive
模块,以使用与移动应用程序相同的启动器图标!
9. 使用 Android Automotive OS 模拟器进行测试
安装带有 Play 商店的 Automotive 系统映像
- 首先,在 Android Studio 中打开 SDK 管理器,如果尚未选择,请选择**SDK 平台**选项卡。在 SDK 管理器窗口的右下角,确保选中**显示包详细信息**框。
- 安装以下一个或多个模拟器映像。映像只能在与自身架构相同的机器(x86/ARM)上运行。
- Android 12L > 带有 Play 商店的 Automotive Intel x86 Atom_64 系统映像
- Android 12L > 带有 Play 商店的 Automotive ARM 64 v8a 系统映像
- Android 11 > 带有 Play 商店的 Automotive Intel x86 Atom_64 系统映像
- Android 10 > 带有 Play 商店的 Automotive Intel x86 Atom_64 系统映像
创建 Android Automotive OS Android 虚拟设备
- 在打开设备管理器后**,**在窗口左侧的**类别**列下选择**Automotive**。然后,从列表中选择**Automotive (1024p 横向)**设备定义,然后单击**下一步**。
- 在下一页上,从上一步中选择一个系统映像(如果您选择了 Android 11/API 30 映像,它可能位于**x86 映像**选项卡下,而不是默认的**推荐**选项卡下)。单击**下一步**,并选择您想要的任何高级选项,最后通过单击**完成**来创建 AVD。
运行应用程序
- 使用
automotive
运行配置,在您刚刚创建的模拟器上运行应用。
首次运行应用时,您可能会看到如下屏幕
如果是这种情况,请点击检查更新按钮,这会将您带到 Google 汽车应用主机应用的 Play 商店页面,您应该点击安装按钮。如果您在点击检查更新按钮时未登录,则会引导您完成登录流程。登录后,您可以再次打开应用,以便点击该按钮并返回 Play 商店页面。
- 最后,安装主机后,再次从启动器(底部一行九点网格图标)打开应用,您应该会看到以下内容
下一步,您将在 :common:car-app-service
模块中进行更改,以显示地点列表并允许用户在另一个应用中启动导航到所选位置。
10. 添加地图和详情屏幕
向主屏幕添加地图
- 首先,将
MainScreen
类onGetTemplate
方法中的代码替换为以下代码
MainScreen.kt
override fun onGetTemplate(): Template {
val placesRepository = PlacesRepository()
val itemListBuilder = ItemList.Builder()
.setNoItemsMessage("No places to show")
placesRepository.getPlaces()
.forEach {
itemListBuilder.addItem(
Row.Builder()
.setTitle(it.name)
// Each item in the list *must* have a DistanceSpan applied to either the title
// or one of the its lines of text (to help drivers make decisions)
.addText(SpannableString(" ").apply {
setSpan(
DistanceSpan.create(
Distance.create(Math.random() * 100, Distance.UNIT_KILOMETERS)
), 0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE
)
})
.setOnClickListener { TODO() }
// Setting Metadata is optional, but is required to automatically show the
// item's location on the provided map
.setMetadata(
Metadata.Builder()
.setPlace(Place.Builder(CarLocation.create(it.latitude, it.longitude))
// Using the default PlaceMarker indicates that the host should
// decide how to style the pins it shows on the map/in the list
.setMarker(PlaceMarker.Builder().build())
.build())
.build()
).build()
)
}
return PlaceListMapTemplate.Builder()
.setTitle("Places")
.setItemList(itemListBuilder.build())
.build()
}
此代码从 PlacesRepository
读取 Place
实例列表,然后将每个实例转换为 Row
,添加到 PlaceListMapTemplate
显示的 ItemList
中。
- 再次运行应用(在任一或两个平台上),查看结果!
Android Auto | Android Automotive OS |
糟糕,另一个错误——看起来缺少权限。
java.lang.SecurityException: The car app does not have a required permission: androidx.car.app.MAP_TEMPLATES at android.os.Parcel.createExceptionOrNull(Parcel.java:2373) at android.os.Parcel.createException(Parcel.java:2357) at android.os.Parcel.readException(Parcel.java:2340) at android.os.Parcel.readException(Parcel.java:2282) ...
- 要修复此错误,请在
:common:car-app-service
模块的清单中添加以下<uses-permission>
元素。
使用 PlaceListMapTemplate
的任何应用都必须声明此权限,否则应用会像刚刚演示的那样崩溃。请注意,只有将类别声明为 androidx.car.app.category.POI
的应用才能使用此模板,进而使用此权限。
AndroidManifest.xml (:common:car-app-service)
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />
...
</manifest>
添加权限后运行应用,它应该在每个平台上看起来像这样
Android Auto | Android Automotive OS |
当您提供必要的 Metadata
时,地图的渲染由应用主机为您处理!
添加详情屏幕
接下来,是时候添加一个详情屏幕,让用户查看有关特定位置的更多信息,并可以选择使用他们首选的导航应用导航到该位置,或返回其他地点列表。这可以使用 PaneTemplate
完成,它允许您在可选操作按钮旁边显示最多四行信息。
- 首先,右键单击
:common:car-app-service
模块中的res
目录,单击新建>矢量资源,然后使用以下配置创建一个导航图标
- 资源类型:
剪贴画
- 剪贴画:
navigation
- 名称:
baseline_navigation_24
- 大小:
24
dp x24
dp - 颜色:
#000000
- 不透明度:
100%
- 然后,在
screen
包中,创建一个名为DetailScreen.kt
的文件(位于现有的MainScreen.kt
文件旁边),并添加以下代码
DetailScreen.kt
class DetailScreen(carContext: CarContext, private val placeId: Int) : Screen(carContext) {
override fun onGetTemplate(): Template {
val place = PlacesRepository().getPlace(placeId)
?: return MessageTemplate.Builder("Place not found")
.setHeaderAction(Action.BACK)
.build()
val navigateAction = Action.Builder()
.setTitle("Navigate")
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.baseline_navigation_24
)
).build()
)
// Only certain intent actions are supported by `startCarApp`. Check its documentation
// for all of the details. To open another app that can handle navigating to a location
// you must use the CarContext.ACTION_NAVIGATE action and not Intent.ACTION_VIEW like
// you might on a phone.
.setOnClickListener { carContext.startCarApp(place.toIntent(CarContext.ACTION_NAVIGATE)) }
.build()
return PaneTemplate.Builder(
Pane.Builder()
.addAction(navigateAction)
.addRow(
Row.Builder()
.setTitle("Coordinates")
.addText("${place.latitude}, ${place.longitude}")
.build()
).addRow(
Row.Builder()
.setTitle("Description")
.addText(place.description)
.build()
).build()
)
.setTitle(place.name)
.setHeaderAction(Action.BACK)
.build()
}
}
请特别注意 navigateAction
的构建方式——在其 OnClickListener
中调用 startCarApp
是与 Android Auto 和 Android Automotive OS 上其他应用交互的关键。
屏幕间导航
现在已经有两种类型的屏幕了,是时候在它们之间添加导航了!汽车应用库中的导航使用推入和弹出堆栈模型,这非常适合在驾驶时完成的简单任务流程。
- 要从
MainScreen
上的列表项之一导航到该项的DetailScreen
,请添加以下代码
MainScreen.kt
Row.Builder()
...
.setOnClickListener { screenManager.push(DetailScreen(carContext, it.id)) }
...
从 DetailScreen
返回到 MainScreen
已经被处理,因为在构建 DetailScreen
上显示的 PaneTemplate
时会调用 setHeaderAction(Action.BACK)
。当用户点击标题栏操作时,主机会为您处理将当前屏幕从堆栈中弹出,但这可以被您的应用覆盖(如果需要)。
- 现在运行应用,查看
DetailScreen
和应用内导航的实际效果!
11. 更新屏幕上的内容
通常,您希望让用户与屏幕互动并更改屏幕上元素的状态。为了演示如何做到这一点,您将构建一个功能,让用户在 DetailScreen
中在收藏和取消收藏地点之间切换。
- 首先,添加一个局部变量
isFavorite
来保存状态。在实际应用中,这应该作为数据层的一部分存储,但局部变量足以用于演示。
DetailScreen.kt
class DetailScreen(carContext: CarContext, private val placeId: Int) : Screen(carContext) {
private var isFavorite = false
...
}
- 接下来,右键单击
:common:car-app-service
模块中的res
目录,单击新建>矢量资源,然后使用以下配置创建一个收藏图标
- 资源类型:
剪贴画
- 名称:
baseline_favorite_24
- 剪贴画:
favorite
- 大小:
24
dp x24
dp - 颜色:
#000000
- 不透明度:
100%
- 然后,在
DetailsScreen.kt
中,为PaneTemplate
创建一个ActionStrip
。
ActionStrip
UI 组件位于标题行标题的对面,非常适合次要和三级操作。由于导航是在 DetailScreen
上要执行的主要操作,因此将收藏或取消收藏的 Action
放入 ActionStrip
是一个很好的屏幕结构方式。
DetailScreen.kt
val navigateAction = ...
val actionStrip = ActionStrip.Builder()
.addAction(
Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.baseline_favorite_24
)
).setTint(
if (isFavorite) CarColor.RED else CarColor.createCustom(
Color.LTGRAY,
Color.DKGRAY
)
).build()
)
.setOnClickListener {
isFavorite = !isFavorite
}.build()
)
.build()
...
这里有两个需要注意的地方
CarIcon
根据项目的状态进行着色。setOnClickListener
用于响应用户的输入并切换收藏状态。
- 不要忘记在
PaneTemplate.Builder
上调用setActionStrip
以便使用它!
DetailScreen.kt
return PaneTemplate.Builder(...)
...
.setActionStrip(actionStrip)
.build()
- 现在,运行应用并查看发生了什么
有趣……看起来点击正在发生,但 UI 没有更新。
这是因为汽车应用库有一个刷新概念。为了限制驾驶员分心,刷新屏幕上的内容有一定的限制(这因显示的模板而异),并且必须通过调用 Screen
类的 invalidate
方法,由您自己的代码明确请求每次刷新。仅更新 onGetTemplate
中引用的某些状态不足以更新 UI。
- 要解决此问题,请更新
OnClickListener
,如下所示
DetailScreen.kt
.setOnClickListener {
isFavorite = !isFavorite
// Request that `onGetTemplate` be called again so that updates to the
// screen's state can be picked up
invalidate()
}
- 再次运行应用,您会看到爱心图标的颜色应该在每次点击时都会更新!
就这样,您就拥有了一个与 Android Auto 和 Android Automotive OS 良好集成的基本应用!
12. 恭喜您
您已成功构建您的第一个汽车应用库应用。现在是时候运用您学到的知识并将其应用到您自己的应用中了!
如前所述,目前只有使用汽车应用库构建的某些类别的应用才能提交到 Play 商店。如果您的应用是导航应用、兴趣点 (POI) 应用(如您在此 codelab 中使用的应用)或物联网 (IOT) 应用,您可以立即开始构建并将其发布到两个平台上的生产环境。
每年都会添加新的应用类别,因此,即使您无法立即应用您学到的知识,也可以稍后再查看,到时候可能正是扩展您的应用到汽车的时候了!
一些尝试性操作
- 安装 OEM 的模拟器(例如 Polestar 2 模拟器),看看 OEM 自定义如何改变 Android Automotive OS 上汽车应用库应用的外观和感觉。请注意,并非所有 OEM 模拟器都支持汽车应用库应用。
- 查看 展示样本应用程序,它演示了汽车应用库的全部功能。
进一步阅读
- 使用 Android for Cars 应用库 涵盖了此 codelab 中的内容以及更多内容!
- Android for Cars 应用库设计指南 提供了对所有不同模板和构建应用时应遵循的最佳实践的详细描述。
- 汽车 Android 应用质量 页面描述了您的应用必须满足的标准,才能创造出色的用户体验并通过 Play 商店审核。