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
也有自己的 生命周期,您可以接入该生命周期。
您将在本 Codelab 的 编写您的 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
文件中,添加汽车应用程序库版本的变量声明,如下所示。这使您可以轻松地在应用程序中的每个模块中使用相同的版本。
build.gradle(项目:位置)
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
。这是汽车应用程序库的主要工件,包含构建应用程序时使用的所有核心类。该库还有三个其他工件,androidx.car.app:app-projected
用于 Android Auto 特定功能,androidx.car.app:app-automotive
用于 Android Automotive OS 功能代码,以及androidx.car.app:app-testing
用于一些对单元测试有用的帮助程序。您将在本 Codelab 的后面部分使用app-projected
和app-automotive
。:common:data
。这是现有移动应用程序使用的相同数据模块,它允许对应用程序体验的每个版本使用相同的数据源。
build.gradle(模块: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
即可。
您从 createHostValidator
返回的 HostValidator
在绑定您的 CarAppService
时被引用,以确保主机受信任,如果主机不匹配您定义的参数,则绑定将失败。出于本 Codelab(以及一般测试)的目的,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(模块:app)
dependencies {
...
implementation project(path: ':common:car-app-service')
...
}
有了这个更改,应用程序自身模块的依赖项图如下所示
这将捆绑您刚刚在 :common:car-app-service
模块中编写的代码,以及汽车应用程序库中包含的其他组件,例如 提供的权限授予活动。
声明 com.google.android.gms.car.application 元数据
- 右键单击
:common:car-app-service
模块,然后选择 **新建 > Android 资源文件** 选项,然后覆盖以下值
- 文件名:
automotive_app_desc.xml
- 资源类型:
XML
- 根元素:
automotiveApp
- 在该文件中,添加以下
<uses>
元素,以声明您的应用程序使用汽车应用程序库提供的模板。
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 读取,并让它知道您的应用程序支持哪些功能——在本例中,它使用汽车应用程序库的模板系统。然后,此信息将用于处理行为,例如将应用程序添加到 Android Auto 启动器以及 从通知中打开应用程序。
可选:监听投影更改
有时,您希望了解用户的设备是否连接到汽车。您可以通过使用 CarConnection
API 来实现这一点,该 API 提供 LiveData
,使您可以观察连接状态。
- 要使用
CarConnection
API,首先在:app
模块中添加对androidx.car.app:app
工件的依赖项。
build.gradle(模块:app)
dependencies {
...
implementation "androidx.car.app:app:$car_app_library_version"
...
}
- 出于演示目的,您接下来可以创建一个简单的 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
工件的依赖项。与 Android Auto 中可选的 androidx.car.app:app-projected
工件不同,此依赖项在 Android Automotive OS 上是必需的,因为它包含用于运行应用程序的 CarAppActivity
。
- 要添加依赖项,请打开
build.gradle
文件,然后插入以下代码。
build.gradle(模块: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 徽标图标。
- 如果您愿意,可以将
:app
模块中的mipmap
资源目录复制并粘贴到: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 landscape)** 设备定义,并单击 **下一步**。
- 在下一页上,从上一步中选择一个系统映像(如果您选择的是 Android 11/API 30 映像,它可能位于 **x86 映像** 选项卡下,而不是默认的 **推荐** 选项卡下)。单击 **下一步**,选择您想要的任何高级选项,最后通过单击 **完成** 创建 AVD。
运行应用程序
- 使用
automotive
运行配置在您刚刚创建的模拟器上运行应用程序。
首次运行应用程序时,您可能会看到以下屏幕。
如果是这样,请单击 **检查更新** 按钮,它会将您带到 Google Automotive App Host 应用程序的 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
目录,然后单击 **新建** > **矢量资产**,然后使用以下配置创建导航图标
- 资产类型:
剪贴画
- 剪贴画:
导航
- 名称:
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 上其他应用程序交互的关键。
在屏幕之间导航
现在有两种类型的屏幕,是时候添加它们之间的导航了!Car App Library 中的导航使用堆栈模型进行推送和弹出,这对于在驾驶时完成的简单任务流非常理想。
- 要从
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
- 剪贴画:
收藏
- 尺寸:
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 并没有更新。
这是因为 Car App Library 有一个 刷新 概念。为了限制驾驶员分心,屏幕上刷新内容有一些限制(根据显示的模板而有所不同),并且每次刷新都必须通过您的代码显式请求,方法是调用 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. 恭喜
您成功构建了第一个 Car App Library 应用程序。现在是时候学习您学到的知识并将其应用到您自己的应用程序中!
如前所述,目前只有使用 Car App Library 应用程序构建的某些类别可以提交到 Play 商店。如果您的应用程序是导航应用程序、兴趣点 (POI) 应用程序(如您在此代码实验室中使用的应用程序)或物联网 (IOT) 应用程序,您现在就可以开始构建并发布您的应用程序,使其在两个平台上都能投入生产。
每年都会添加新的应用程序类别,因此,即使您无法立即应用您学到的知识,也可以稍后回来查看,可能时机已经成熟,可以将您的应用程序扩展到汽车中!
值得尝试的事情
- 安装 OEM 的模拟器(例如 Polestar 2 模拟器)并查看 OEM 自定义如何改变 Android Automotive OS 上 Car App Library 应用程序的外观和感觉。请注意,并非所有 OEM 模拟器都支持 Car App Library 应用程序。
- 查看 Showcase 示例应用程序,它展示了 Car App Library 的全部功能。
进一步阅读
- 使用 Android for Cars 应用库 涵盖了此代码实验室中的内容以及更多内容!
- Android for Cars App Library 设计指南 提供了对所有不同模板的详细说明,以及构建应用程序时应遵循的最佳实践。
- 适用于汽车的 Android 应用程序质量 页面描述了您的应用程序必须满足的标准,才能创造出色的用户体验并通过 Play 商店审核。