1. Antes de comenzar
Qué no es este codelab
- Una guía para crear apps de música (audio, p. ej., música, radio, podcasts) para Android Auto y el SO Android Automotive (consulta Cómo compilar apps de música para automóviles si deseas obtener más información al respecto)
Requisitos
- Necesitas la Versión preliminar de Android Studio. Algunos de los emuladores del SO Android Automotive que se usan en este codelab solo están disponibles a través de la versión preliminar de Android Studio. Si aún no la instalaste, puedes iniciar el codelab con la versión estable mientras se descarga la versión preliminar.
- Debes tener experiencia básica con Kotlin.
- Debes tener experiencia en la creación de dispositivos virtuales de Android y su ejecución en Android Emulator.
- Debes tener conocimientos básicos de Jetpack Compose.
- Debes tener conocimientos de los efectos secundarios.
- Debes tener conocimientos de las inserciones de ventanas.
Qué crearás
En este codelab, aprenderás a migrar al SO Android Automotive Road Reels, una app existente de transmisión de video por Internet para dispositivos móviles
La versión de punto de partida de la app que se ejecuta en un teléfono | La versión completa de la app que se ejecuta en un emulador del SO Android Automotive con un recorte de pantalla |
Qué aprenderás
- Cómo usar el emulador del SO Android Automotive
- Cómo realizar los cambios necesarios para crear una compilación del SO Android Automotive
- Suposiciones que suelen hacerse cuando se desarrollan apps para dispositivos móviles que pueden fallar cuando una app se ejecuta en el SO Android Automotive
- Los diferentes niveles de calidad de las apps para automóviles
- Cómo usar la sesión multimedia para permitir que otras apps controlen la reproducción de tu app
- Cómo pueden diferir la IU del sistema y las inserciones de ventanas en los dispositivos con el SO Android Automotive en comparación con los dispositivos móviles
2. Prepárate
Obtén el código
- El código para este codelab se puede encontrar en el directorio
build-a-parked-app
dentro del repositoriocar-codelabs
de GitHub. Para clonarlo, ejecuta el siguiente comando:
git clone https://github.com/android/car-codelabs.git
- También tienes la opción de descargar el repositorio como archivo ZIP:
Abre el proyecto
- Después de iniciar Android Studio, importa el proyecto. Elige solamente el directorio
build-a-parked-app/start
. El directoriobuild-a-parked-app/end
contiene el código de la solución, que puedes consultar en cualquier momento si no logras avanzar o si deseas ver el proyecto completo.
Familiarízate con el código
- Después de abrir el proyecto en Android Studio, dedica un momento a analizar el código de partida.
3. Aprende sobre las apps para usar en el SO Android Automotive con el automóvil estacionado
Las apps destinadas a usarse con el automóvil estacionado conforman un subconjunto de las categorías de apps compatibles con el SO Android Automotive. Al momento de escribir, constan de apps de transmisión de video por Internet, navegadores web y juegos. Estas apps son ideales para automóviles debido al hardware de los automóviles con Google integrado y a la prevalencia en constante crecimiento de los automóviles eléctricos, en los que el tiempo de carga representa una gran oportunidad para que los conductores y los pasajeros interactúen con este tipo de apps.
En muchos sentidos, los automóviles son similares a otros dispositivos con pantalla grande, como las tablets y los dispositivos plegables. Tienen pantallas táctiles con tamaños, resoluciones y relaciones de aspecto similares y que pueden estar en orientación horizontal o vertical (aunque, a diferencia de las tablets, su orientación es fija). También son dispositivos conectados que pueden entrar en la conexión de red y salir de esta. Con todo esto en mente, no sorprende que las apps que ya están optimizadas para pantallas grandes no suelan requerir mucho trabajo para ofrecer una excelente experiencia del usuario en automóviles.
Al igual que para las pantallas grandes, también hay niveles de calidad de las apps para automóviles:
- Nivel 3 (lista para el automóvil): Tu app es compatible con pantallas grandes y se puede usar mientras el automóvil está estacionado. Si bien es posible que no tenga ninguna función optimizada para automóviles, los usuarios pueden experimentar la app como lo harían en cualquier otro dispositivo Android con pantalla grande. Las apps para dispositivos móviles que cumplen con estos requisitos son aptas para distribuirse en automóviles sin modificaciones a través del programa de apps para dispositivos móviles listas para usar en automóviles.
- Nivel 2 (optimizada para el automóvil): Tu app brinda una gran experiencia en la pantalla central del automóvil. Para ello, tendrá ingeniería específica del automóvil para incluir capacidades que se pueden usar en los modos de conducción o estacionamiento, según la categoría de tu app.
- Nivel 1 (diferenciada para el automóvil): Tu app se diseñó para funcionar con una amplia variedad de hardware en automóviles y puede adaptar su experiencia a los modos de conducción y estacionamiento. Proporciona la mejor experiencia del usuario diseñada para las diferentes pantallas de los automóviles, como la consola central, el clúster de instrumentos y las pantallas adicionales, como las panorámicas que se encuentran en muchos automóviles de alta gama.
4. Ejecuta la app en el emulador del SO Android Automotive
Instala Automotive con las imágenes del sistema de Play Store
- Primero, abre SDK Manager en Android Studio y selecciona la pestaña SDK Platforms si aún no está seleccionada. En la esquina inferior derecha de la ventana de SDK Manager, asegúrate de que esté marcada la casilla junto a Show package details.
- Instala una de las imágenes del emulador Automotive with Play Store que se indican en Agrega imágenes genéricas del sistema. Las imágenes solo se pueden ejecutar en máquinas que tienen su misma arquitectura (x86/ARM).
Crea un dispositivo virtual del SO Android Automotive
- Después de abrir el Administrador de dispositivos, selecciona Automotive debajo de la columna Category en la parte izquierda de la ventana. Luego, selecciona el perfil de hardware incluido Automotive (1024p landscape) de la lista y haz clic en Next.
- En la siguiente página, selecciona la imagen del sistema del paso anterior. Haz clic en Next y selecciona las opciones avanzadas que desees antes de crear el AVD haciendo clic en Finish. Nota: Si eliges la imagen del nivel de API 30, es posible que esté en una pestaña que no sea la de Recommended.
Ejecuta la app
Ejecuta la app en el emulador que acabas de crear usando la configuración de ejecución app
existente. Experimenta con la app para probar las diferentes pantallas y comparar su comportamiento si la ejecutas en un emulador de teléfono o tablet.
5. Crea una compilación del SO Android Automotive
Si bien la app "simplemente funciona", hay algunos cambios pequeños que se deben realizar para que funcione de forma correcta en el SO Android Automotive y cumpla con los requisitos para poder publicarse en Play Store. No todos estos cambios deberían incluirse en la versión para dispositivos móviles de la app, por lo que primero crearás una variante de compilación del SO Android Automotive.
Agrega una dimensión de variantes para el factor de forma
Para comenzar, agrega una dimensión de variantes para el factor de forma al que se orienta la compilación. Para ello, modifica flavorDimensions
en el archivo build.gradle.kts
. Luego, agrega un bloque productFlavors
y variantes para cada factor de forma (mobile
y automotive
).
Para obtener más información, consulta Configura variantes de productos.
build.gradle.kts (módulo :app)
android {
...
flavorDimensions += "formFactor"
productFlavors {
create("mobile") {
// Inform Android Studio to use this flavor as the default (e.g. in the Build Variants tool window)
isDefault = true
// Since there is only one flavor dimension, this is optional
dimension = "formFactor"
}
create("automotive") {
// Since there is only one flavor dimension, this is optional
dimension = "formFactor"
// Adding a suffix makes it easier to differentiate builds (e.g. in the Play Console)
versionNameSuffix = "-automotive"
}
}
...
}
Después de actualizar el archivo build.gradle.kts
, deberías ver un banner en la parte superior del archivo que te informa que los archivos de Gradle cambiaron desde la última sincronización del proyecto y que posiblemente debas sincronizar el proyecto para que el IDE funcione de forma correcta. Haz clic en el botón Sync Now en ese banner para que Android Studio pueda importar estos cambios en la configuración de compilación.
A continuación, abre la ventana de herramientas Build Variants desde el elemento de menú Build > Select Build Variant… y selecciona la variante automotiveDebug
. De esta manera, te asegurarás de ver los archivos del conjunto de orígenes automotive
en la ventana Project y de que se use esta variante de compilación cuando se ejecute la app a través de Android Studio.
Crea un manifiesto del SO Android Automotive
A continuación, crearás un archivo AndroidManifest.xml
para el conjunto de orígenes automotive
. Este archivo contiene los elementos necesarios para las apps del SO Android Automotive.
- En la ventana Project, haz clic con el botón derecho en el módulo
app
. En el menú desplegable que aparece, selecciona New > Other > Android Manifest File. - En la ventana New Android Component que se abre, selecciona
automotive
como Target Source Set para el archivo nuevo. Haz clic en Finish para crear el archivo.
- Dentro del archivo
AndroidManifest.xml
que se acaba de crear (en la ruta de accesoapp/src/automotive/AndroidManifest.xml
), agrega lo siguiente:
AndroidManifest.xml (automotive)
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- https://developer.android.com/training/cars/parked#required-features -->
<uses-feature
android:name="android.hardware.type.automotive"
android:required="true" />
<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" />
</manifest>
Se requiere la primera declaración para subir el artefacto de compilación al segmento del SO Android Automotive en Play Console. Google Play usa la presencia de esta función para distribuir la app solo a dispositivos con la función android.hardware.type.automotive
(p. ej., automóviles).
Las otras declaraciones son obligatorias para garantizar que la app se pueda instalar en las diversas configuraciones de hardware presentes en los automóviles. Para obtener más información detallada, consulta Funciones obligatorias del SO Android Automotive.
Marca tu app como app de video
La última parte de los metadatos que se debe agregar es el archivo automotive_app_desc.xml
. Se usa para declarar la categoría de tu app en el contexto de Android para vehículos y es independiente de la categoría que selecciones para tu app en Play Console.
- Haz clic con el botón derecho en el módulo
app
y selecciona la opción New > Android Resource File e ingresa los siguientes valores antes de hacer clic en OK:
- File name:
automotive_app_desc.xml
- Resource type:
XML
- Root element:
automotiveApp
- Source set:
automotive
- Directory name:
xml
- Dentro de ese archivo, agrega el siguiente elemento
<uses>
para declarar que tu app es una app de video.
automotive_app_desc.xml
<?xml version="1.0" encoding="utf-8"?>
<automotiveApp xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses
name="video"
tools:ignore="InvalidUsesTagAttribute" />
</automotiveApp>
- En el archivo
AndroidManifest.xml
del conjunto de orígenesautomotive
(en el que acabas de agregar los elementos<uses-feature>
), agrega un elemento<application>
vacío. Dentro de él, agrega el siguiente elemento<meta-data>
que hace referencia al archivoautomotive_app_desc.xml
que acabas de crear.
AndroidManifest.xml (automotive)
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
...
<application>
<meta-data
android:name="com.android.automotive"
android:resource="@xml/automotive_app_desc" />
</application>
</manifest>
Con eso, realizaste todos los cambios necesarios para crear una compilación del SO Android Automotive para la app.
6. Cumple con los requisitos de calidad del SO Android Automotive: navegabilidad
Si bien crear una variante de compilación del SO Android Automotive es una parte de adaptar tu app para automóviles, sigue siendo necesario asegurarte de que esta se pueda usar y sea segura.
Agrega opciones de navegación
Mientras ejecutabas la app en el emulador del SO Android Automotive, es posible que hayas notado que no era posible regresar de la pantalla de detalles a la pantalla principal ni de la del reproductor a la de detalles. A diferencia de otros factores de forma, que pueden requerir un botón Atrás o un gesto táctil para habilitar la navegación hacia atrás, no existe tal requisito para los dispositivos con el SO Android Automotive. Por lo tanto, las apps deben proporcionar opciones de navegación en su IU para garantizar que los usuarios puedan navegar sin quedarse atrapados en una pantalla dentro de la app. Este requisito está codificado como el lineamiento de calidad AN-1.
Para admitir la navegación hacia atrás desde la pantalla de detalles hasta la pantalla principal, agrega un parámetro navigationIcon
adicional para el elemento CenterAlignedTopAppBar
de la pantalla de detalles de la siguiente manera:
RoadReelsApp.kt
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
...
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null
)
}
}
Para admitir la navegación hacia atrás desde la pantalla del reproductor hasta la pantalla principal, haz lo siguiente:
- Actualiza el elemento componible
TopControls
para que tome un parámetro de devolución de llamada con el nombreonClose
y agrega unIconButton
que lo llame cuando se haga clic.
PlayerControls.kt
@Composable
fun TopControls(
title: String?,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Box(modifier) {
IconButton(
modifier = Modifier
.align(Alignment.TopStart),
onClick = onClose
) {
Icon(
Icons.TwoTone.Close,
contentDescription = "Close player",
tint = Color.White
)
}
if (title != null) { ... }
}
}
- Actualiza el elemento componible
PlayerControls
para que también tome un parámetro de devolución de llamadaonClose
y lo pase aTopControls
.
PlayerControls.kt
fun PlayerControls(
visible: Boolean,
playerState: PlayerState,
onClose: () -> Unit,
onPlayPause: () -> Unit,
onSeek: (seekToMillis: Long) -> Unit,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut()
) {
Box(modifier = modifier.background(Color.Black.copy(alpha = .5f))) {
TopControls(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.screen_edge_padding))
.align(Alignment.TopCenter),
title = playerState.mediaMetadata.title?.toString(),
onClose = onClose
)
...
}
}
}
- A continuación, actualiza el elemento componible
PlayerScreen
para que tome el mismo parámetro y lo pase a suPlayerControls
.
PlayerScreen.kt
@Composable
fun PlayerScreen(
onClose: () -> Unit,
modifier: Modifier = Modifier,
) {
...
PlayerControls(
modifier = Modifier
.fillMaxSize(),
visible = isShowingControls,
playerState = playerState,
onClose = onClose,
onPlayPause = { if (playerState.isPlaying) player.pause() else player.play() },
onSeek = { player.seekTo(it) }
)
}
- Por último, en
RoadReelsNavHost
, proporciona la implementación que se pasa aPlayerScreen
:
RoadReelsNavHost.kt
composable(route = Screen.Player.name) {
PlayerScreen(onClose = { navController.popBackStack() })
}
Excelente. Ahora el usuario puede moverse entre pantallas sin encontrarse con ningún punto muerto. Además, la experiencia del usuario también puede ser mejor en otros factores de forma. Por ejemplo, en un teléfono alto, cuando la mano del usuario ya está cerca de la parte superior de la pantalla, puede navegar más fácilmente por la app sin necesidad de mover el dispositivo en la mano.
Adapta la app para que sea compatible con la orientación de la pantalla
A diferencia de la gran mayoría de los dispositivos móviles, la mayoría de los automóviles tienen una orientación fija. Es decir, admiten orientación horizontal o vertical, pero no ambas, ya que sus pantallas no se pueden rotar. Por este motivo, las apps deben evitar suponer que se admiten ambas orientaciones.
En la sección Crea un manifiesto del SO Android Automotive, agregaste dos elementos <uses-feature>
para las funciones android.hardware.screen.portrait
y android.hardware.screen.landscape
con el atributo required
establecido en false
. De esta manera, se garantiza que ninguna dependencia de funciones implícita en cualquier orientación de pantalla pueda impedir que la app se distribuya en automóviles. Sin embargo, esos elementos del manifiesto no cambian el comportamiento de la app, sino la forma en que se distribuye.
En la actualidad, la app tiene una función útil con la que se configura automáticamente la orientación de la actividad en modo horizontal cuando se abre el reproductor de video, de modo que los usuarios del teléfono no tengan que tocar su dispositivo para cambiar su orientación si todavía no está en el modo horizontal.
Lamentablemente, ese mismo comportamiento puede generar un bucle parpadeante o un formato letterbox en dispositivos con orientación vertical fija, lo que incluye muchos automóviles que circulan en la ruta en la actualidad.
Para solucionar este problema, puedes agregar una verificación basada en las orientaciones de pantalla que admita el dispositivo actual.
- Para simplificar la implementación, primero, agrega lo siguiente en
Extensions.kt
:
Extensions.kt
import android.content.Context
import android.content.pm.PackageManager
...
enum class SupportedOrientation {
Landscape,
Portrait,
}
fun Context.supportedOrientations(): List<SupportedOrientation> {
return when (Pair(
packageManager.hasSystemFeature(PackageManager.FEATURE_SCREEN_LANDSCAPE),
packageManager.hasSystemFeature(PackageManager.FEATURE_SCREEN_PORTRAIT)
)) {
Pair(true, false) -> listOf(SupportedOrientation.Landscape)
Pair(false, true) -> listOf(SupportedOrientation.Portrait)
// For backwards compat, if neither feature is declared, both can be assumed to be supported
else -> listOf(SupportedOrientation.Landscape, SupportedOrientation.Portrait)
}
}
- Luego, protege la llamada para establecer la orientación solicitada. Como las apps pueden tener un problema similar en el modo multiventana en dispositivos móviles, también puedes incluir una verificación para no configurar la orientación de forma dinámica en ese caso.
PlayerScreen.kt
import com.example.android.cars.roadreels.SupportedOrientation
import com.example.android.cars.roadreels.supportedOrientations
...
LaunchedEffect(Unit) {
...
// Only automatically set the orientation to landscape if the device supports landscape.
// On devices that are portrait only, the activity may enter a compat mode and won't get to
// use the full window available if so. The same applies if the app's window is portrait
// in multi-window mode.
if (context.supportedOrientations().contains(SupportedOrientation.Landscape)
&& !context.isInMultiWindowMode
) {
context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
}
...
}
La pantalla del reproductor entra en un bucle parpadeante en el emulador de Polestar 2 antes de agregar la verificación (cuando la actividad no controla los cambios de configuración de | La pantalla del reproductor se muestra en formato letterbox en el emulador de Polestar 2 antes de agregar la verificación (cuando la actividad controla los cambios de configuración de | La pantalla del reproductor no se muestra en formato letterbox en el emulador de Polestar 2 después de agregar la marca de verificación. |
Como esta es la única ubicación de la app que establece la orientación de la pantalla, ahora la app evita el formato letterbox. En tu propia app, busca atributos screenOrientation
o llamadas setRequestedOrientation
que sean únicamente para orientaciones horizontales o verticales (incluidos el sensor, la marcha atrás y las variantes del usuario de cada una), y quítalos o protégelos según sea necesario para limitar el formato letterbox. Para obtener más información, consulta Modo de compatibilidad del dispositivo.
Adapta la app a la capacidad de control de la barra del sistema
Lamentablemente, si bien el cambio anterior garantiza que la app no entre en un bucle parpadeante ni se muestre con formato letterbox, también expone otra suposición que no se cumple, es decir, que las barras del sistema siempre se pueden ocultar. Como los usuarios tienen diferentes necesidades cuando utilizan sus automóviles (en comparación con cuando usan sus teléfonos o tablets), los OEM tienen la opción de evitar que las apps oculten las barras del sistema para garantizar que siempre se pueda acceder a los controles del automóvil, como los de climatización, en la pantalla.
Como resultado, es posible que las apps se rendericen detrás de las barras del sistema cuando lo hagan en modo envolvente y asuman que las barras se pueden ocultar. Puedes observarlo en el paso anterior, ya que los controles de la parte inferior y superior del reproductor ya no estarán visibles cuando la app no se muestre en formato letterbox. En este caso específico, la app ya no se puede navegar, dado que el botón para cerrar el reproductor está oculto, y su funcionalidad se ve impedida, puesto que no se puede usar la barra deslizante de búsqueda.
La solución más fácil sería aplicar el padding de las inserciones de ventana systemBars
al reproductor de la siguiente manera:
PlayerScreen.kt
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
...
Box(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.systemBars)
) {
PlayerView(...)
PlayerControls(...)
}
Sin embargo, esta solución no es la ideal, ya que causa que los elementos de la IU se muevan cuando desaparecen las animaciones de las barras del sistema.
Para mejorar la experiencia del usuario, puedes actualizar la app para hacer un seguimiento de las inserciones que se pueden controlar y para aplicar padding solo a las que no se pueden controlar.
- Como otras pantallas dentro de la app podrían estar interesadas en controlar las inserciones de ventana, tiene sentido pasar las inserciones controlables como un
CompositionLocal
. Crea un nuevo archivoLocalControllableInsets.kt
en el paquetecom.example.android.cars.roadreels
y agrega lo siguiente:
LocalControllableInsets.kt
import androidx.compose.runtime.compositionLocalOf
// Assume that no insets can be controlled by default
const val DEFAULT_CONTROLLABLE_INSETS = 0
val LocalControllableInsets = compositionLocalOf { DEFAULT_CONTROLLABLE_INSETS }
- Configura un
OnControllableInsetsChangedListener
para detectar cambios.
MainActivity.kt
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat.OnControllableInsetsChangedListener
...
class MainActivity : ComponentActivity() {
private lateinit var onControllableInsetsChangedListener: OnControllableInsetsChangedListener
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
var controllableInsetsTypeMask by remember { mutableIntStateOf(DEFAULT_CONTROLLABLE_INSETS) }
onControllableInsetsChangedListener =
OnControllableInsetsChangedListener { _, typeMask ->
if (controllableInsetsTypeMask != typeMask) {
controllableInsetsTypeMask = typeMask
}
}
WindowCompat.getInsetsController(window, window.decorView)
.addOnControllableInsetsChangedListener(onControllableInsetsChangedListener)
RoadReelsTheme {
RoadReelsApp(calculateWindowSizeClass(this))
}
}
}
override fun onDestroy() {
super.onDestroy()
WindowCompat.getInsetsController(window, window.decorView)
.removeOnControllableInsetsChangedListener(onControllableInsetsChangedListener)
}
}
- Agrega un
CompositionLocalProvider
de nivel superior que contenga los elementos componibles de la app y el tema, y que vincule valores aLocalControllableInsets
.
MainActivity.kt
import androidx.compose.runtime.CompositionLocalProvider
...
CompositionLocalProvider(LocalControllableInsets provides controllableInsetsTypeMask) {
RoadReelsTheme {
RoadReelsApp(calculateWindowSizeClass(this))
}
}
- En el reproductor, lee el valor actual y úsalo para determinar las inserciones que se usarán para el padding.
PlayerScreen.kt
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.union
import androidx.compose.ui.unit.dp
import com.example.android.cars.roadreels.LocalControllableInsets
...
val controllableInsetsTypeMask = LocalControllableInsets.current
// When the system bars can be hidden, ignore them when applying padding to the player and
// controls so they don't jump around as the system bars disappear. If they can't be hidden
// include them so nothing renders behind the system bars
var windowInsetsForPadding = WindowInsets(0.dp)
if (controllableInsetsTypeMask.and(WindowInsetsCompat.Type.statusBars()) == 0) {
windowInsetsForPadding = windowInsetsForPadding.union(WindowInsets.statusBars)
}
if (controllableInsetsTypeMask.and(WindowInsetsCompat.Type.navigationBars()) == 0) {
windowInsetsForPadding = windowInsetsForPadding.union(WindowInsets.navigationBars)
}
Box(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(windowInsetsForPadding)
) {
PlayerView(...)
PlayerControls(...)
}
El contenido no se mueve cuando se pueden ocultar las barras del sistema. | El contenido permanece visible cuando no se pueden ocultar las barras del sistema. |
Mucho mejor. El contenido no se mueve, a la vez que los controles son completamente visibles, incluso en los automóviles en los que no se pueden controlar las barras del sistema.
7. Cumple con los requisitos de calidad del SO Android Automotive: distracción del conductor
Por último, hay una diferencia importante entre los automóviles y otros factores de forma: se usan para conducir. Por lo tanto, es muy importante limitar las distracciones mientras se conduce. Todas las apps para usar en el SO Android Automotive con el automóvil estacionado deben pausar la reproducción cuando comienza la conducción. Cuando comienza la conducción, aparece una superposición del sistema, y, a su vez, se llama al evento de ciclo de vida onPause
para que la app se superponga. Durante esta llamada, las apps deben pausar la reproducción.
Simula la conducción
Navega a la vista del reproductor en el emulador y comienza a reproducir contenido. Luego, sigue los pasos para simular la conducción y observa que, si bien el sistema oculta la IU de la app, la reproducción no se pausa. Infringe el lineamiento de calidad DD-2 de apps para automóviles.
Pausa la reproducción cuando comienzas a conducir
- Agrega una dependencia en el artefacto
androidx.lifecycle:lifecycle-runtime-compose
, que contiene elLifecycleEventEffect
que ayuda a ejecutar el código en eventos de ciclo de vida.
libs.version.toml
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
Build.gradle.kts (módulo :app)
implementation(libs.androidx.lifecycle.runtime.compose)
- Después de sincronizar el proyecto para descargar la dependencia, agrega un
LifecycleEventEffect
que se ejecute en el eventoON_PAUSE
para pausar la reproducción.
PlayerScreen.kt
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
...
@Composable
fun PlayerScreen(...) {
...
LifecycleEventEffect(Lifecycle.Event.ON_PAUSE) {
player.pause()
}
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
player.play()
}
...
}
Una vez implementada la corrección, sigue los mismos pasos que hiciste antes para simular la conducción durante la reproducción activa y observa que la reproducción se detiene y se cumple con el requisito DD-2.
8. Prueba la app en el emulador de pantalla distante
Un nuevo parámetro de configuración que comenzará a aparecer en los automóviles es un ajuste de dos pantallas con una pantalla principal en la consola central y una pantalla secundaria en la parte superior del panel, cerca del parabrisas. Las apps se pueden mover de la pantalla central a la pantalla secundaria y viceversa para brindar más opciones a los conductores y pasajeros.
Instala la imagen Automotive Distant Display
- Primero, abre SDK Manager en Android Studio y selecciona la pestaña SDK Platforms si aún no está seleccionada. En la esquina inferior derecha de la ventana de SDK Manager, asegúrate de que esté marcada la casilla junto a Show package details.
- Instala la imagen del emulador Automotive Distant Display with Google APIs para la arquitectura de tu computadora (x86/ARM).
Crea un dispositivo virtual del SO Android Automotive
- Después de abrir el Administrador de dispositivos, selecciona Automotive debajo de la columna Category en la parte izquierda de la ventana. Luego, selecciona el perfil de hardware incluido Automotive Distant Display de la lista y haz clic en Next.
- En la siguiente página, selecciona la imagen del sistema del paso anterior. Haz clic en Next y selecciona las opciones avanzadas que desees antes de crear el AVD haciendo clic en Finish.
Ejecuta la app
Ejecuta la app en el emulador que acabas de crear usando la configuración de ejecución app
existente. Sigue las instrucciones que se indican en Usa el emulador de pantalla distante para mover la app hacia la pantalla distante y desde esta. Prueba mover la app cuando esté en la pantalla principal o de detalles y en la del reproductor, y prueba interactuar con la app en ambas pantallas.
9. Mejora la experiencia de la app en la pantalla distante
Cuando usaste la app en la pantalla distante, es posible que hayas notado dos cosas:
- La reproducción se reinicia cuando la app se mueve hacia la pantalla distante y desde esta
- No puedes interactuar con la app mientras esté en la pantalla distante, lo que incluye cambiar el estado de reproducción.
Mejora la continuidad de las apps
El problema por el que se reinicia la reproducción se debe a que se vuelve a crear la actividad por un cambio de configuración. Como la app se escribe con Compose y la configuración que cambia está relacionada con el tamaño, es sencillo permitir que Compose controle los cambios de configuración para ti restringiendo la recreación de actividades para los cambios de configuración basados en el tamaño. De esta manera, la transición entre pantallas se vuelve fluida sin interrupciones en la reproducción ni recargas debido a la recreación de actividades.
AndroidManifest.xml
<activity
android:name="com.example.android.cars.roadreels.MainActivity"
...
android:configChanges="screenSize|smallestScreenSize|orientation|screenLayout|density">
...
</activity>
Implementa los controles de reproducción
Para solucionar el problema por el que no se puede controlar la app mientras está en la pantalla distante, puedes implementar MediaSession
. Las sesiones multimedia proporcionan una forma universal de interactuar con un reproductor de audio o video. Para obtener más información, consulta Cómo controlar y anunciar la reproducción con una MediaSession.
- Agrega una dependencia en el artefacto
androidx.media3:media3-session
.
libs.version.toml
androidx-media3-mediasession = { group = "androidx.media3", name = "media3-session", version.ref = "media3" }
build.gradle.kts (módulo :app)
implementation(libs.androidx.media3.mediasession)
- Crea un
MediaSession
con su compilador.
PlayerScreen.kt
import androidx.media3.session.MediaSession
@Composable
fun PlayerScreen(...) {
...
val mediaSession = remember(context, player) {
MediaSession.Builder(context, player).build()
}
...
}
- Luego, agrega una línea adicional en el bloque
onDispose
deDisposableEffect
en el elemento componiblePlayer
para liberarMediaSession
cuandoPlayer
salga del árbol de composición.
PlayerScreen.kt
DisposableEffect(Unit) {
onDispose {
mediaSession.release()
player.release()
...
}
}
- Por último, cuando estés en la pantalla del reproductor, puedes probar los controles multimedia con el comando
adb shell cmd media_session dispatch
.
# To play content
adb shell cmd media_session dispatch play
# To pause content
adb shell cmd media_session dispatch pause
# To toggle the playing state
adb shell cmd media_session dispatch play-pause
Con eso, la app funciona mucho mejor en automóviles con pantallas distantes. Sin embargo, más que eso, también funciona mejor en otros factores de forma. En los dispositivos que pueden rotar la pantalla o permitir que los usuarios cambien el tamaño de una ventana de la app, la app ahora también se adapta sin problemas en esas situaciones.
Además, gracias a la integración de la sesión multimedia, la reproducción de la app se puede controlar no solo con controles de hardware y software en automóviles, sino también con otras fuentes, como una consulta de Asistente de Google o un botón para pausar en un par de auriculares, lo que les brinda a los usuarios más opciones para controlar la app en todos los factores de forma.
10. Prueba la app en diferentes configuraciones del sistema
Como la app ya funciona bien en la pantalla principal y en la pantalla distante, lo último que debes verificar es cómo maneja las diferentes configuraciones de la barra del sistema y los cortes de pantalla. Según describe en Cómo trabajar con inserciones de ventanas y cortes de pantalla, los dispositivos con el SO Android Automotive pueden tener configuraciones que no cumplan las suposiciones que suelen ser verdaderas en los factores de forma de dispositivos móviles.
En esta sección, descargarás un emulador que se pueda configurar en el tiempo de ejecución, configurarás el emulador para que tenga una barra del sistema izquierda y probarás la app con esa configuración.
Instala la imagen Android Automotive with Google APIs
- Primero, abre SDK Manager en Android Studio y selecciona la pestaña SDK Platforms si aún no está seleccionada. En la esquina inferior derecha de la ventana de SDK Manager, asegúrate de que esté marcada la casilla junto a Show package details.
- Instala la imagen del emulador Android Automotive with Google APIs del nivel de API 33 para la arquitectura de tu computadora (x86/ARM).
Crea un dispositivo virtual del SO Android Automotive
- Después de abrir el Administrador de dispositivos, selecciona Automotive debajo de la columna Category en la parte izquierda de la ventana. Luego, selecciona el perfil de hardware incluido Automotive (1080p landscape) de la lista y haz clic en Next.
- En la siguiente página, selecciona la imagen del sistema del paso anterior. Haz clic en Next y selecciona las opciones avanzadas que desees antes de crear el AVD haciendo clic en Finish.
Configura una barra del sistema lateral
Como se detalla en Cómo realizar pruebas con el emulador configurable, hay una variedad de opciones para emular diferentes configuraciones del sistema presentes en los automóviles.
Para los fines de este codelab, se puede usar com.android.systemui.rro.left
para probar una configuración diferente de la barra del sistema. Para habilitarlo, usa el siguiente comando:
adb shell cmd overlay enable --user 0 com.android.systemui.rro.left
Como la app usa el modificador systemBars
como contentWindowInsets
en Scaffold
, el contenido ya se dibuja en un área segura de las barras del sistema. Para ver qué sucedería si la app asumiera que las barras del sistema solo aparecían en la parte inferior y superior de la pantalla, cambia ese parámetro por lo siguiente:
RoadReelsApp.kt
contentWindowInsets = if (route?.equals(Screen.Player.name) == true) WindowInsets(0.dp) else WindowInsets.systemBars.only(WindowInsetsSides.Vertical)
¡Uy! La pantalla de lista y de detalles se renderiza detrás de la barra del sistema. Gracias al trabajo anterior, la pantalla del reproductor estaría bien, incluso si las barras del sistema no se pudieran controlar desde entonces.
Antes de pasar a la siguiente sección, asegúrate de revertir el cambio que acabas de realizar en el parámetro windowContentPadding
.
11. Trabaja con cortes de pantalla
Por último, algunos automóviles tienen pantallas con cortes de pantalla muy diferentes a los que se ven en los dispositivos móviles. En lugar de las muescas o los cortes de la cámara estenopeica, algunos automóviles con el SO Android Automotive tienen pantallas curvas que hacen que la pantalla no sea rectangular.
Para ver cómo se comporta la app cuando hay un corte de pantalla, primero habilita el corte de pantalla con el siguiente comando:
adb shell cmd overlay enable --user 0 com.android.internal.display.cutout.emulation.free_form
Para probar realmente el comportamiento de la app, habilita también la barra izquierda del sistema que se usó en la última sección, en caso de que aún no lo esté:
adb shell cmd overlay enable --user 0 com.android.systemui.rro.left
Sin modificaciones, la app no se renderiza en el corte de pantalla (la forma exacta del corte es difícil de determinar en este momento, pero se aclarará en el siguiente paso). Una app así está bien y ofrece una mejor experiencia que una app que se renderiza en cortes, pero no se adapta a ellos con cuidado.
Renderiza en el corte de pantalla
Para brindarles a los usuarios la experiencia más envolvente posible, puedes aprovechar mucho más espacio renderizando en el corte de pantalla.
- Para renderizar en el corte de pantalla, crea un archivo
integers.xml
que contenga la anulación específica para los automóviles. Para ello, usa el calificador modo de IU con el valor Car Dock (el nombre es un remanente de cuando solo existía Android Auto, pero también lo usa el SO Android Automotive). Además, comoLAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
se introdujo en Android R, también agrega el calificador Version de Android con el valor 30. Consulta Cómo usar recursos alternativos para obtener más detalles.
- En el archivo que acabas de crear (
res/values-car-v30/integers.xml
), agrega lo siguiente:
integers.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="windowLayoutInDisplayCutoutMode">3</integer>
</resources>
El valor entero 3
corresponde a LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
y anula el valor predeterminado de 0
de res/values/integers.xml
, que corresponde a LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
. Ya se hace referencia a este valor entero en MainActivity.kt
para anular el modo establecido por enableEdgeToEdge()
. Para obtener más información sobre este atributo, consulta la documentación de referencia.
Ahora, cuando ejecutes la app, observa que el contenido se extiende hasta el corte y se ve muy envolvente. Sin embargo, la barra superior de la aplicación y parte del contenido están parcialmente ocultos por el corte de pantalla, lo que provoca un problema similar a lo que sucedió cuando la app asumió que las barras del sistema solo aparecerían en la parte inferior y superior.
Corrige las barras superiores de la aplicación
Para corregir las barras superiores de la aplicación, puedes agregar el siguiente parámetro windowInsets
a los elementos componibles CenterAlignedTopAppBar
:
RoadReelsApp.kt
import androidx.compose.foundation.layout.safeDrawing
...
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)
Como safeDrawing
consta de las inserciones displayCutout
y systemBars
, mejora el parámetro windowInsets
predeterminado, que solo usa systemBars
cuando se posiciona la barra superior de la aplicación.
Además, como la barra superior de la aplicación se posiciona en la parte superior de la ventana, no debes incluir el componente inferior de las inserciones safeDrawing
. Si lo haces, es posible que se agregue padding innecesario.
Corrige la pantalla principal
Una opción para corregir el contenido en la pantalla principal y de detalles sería usar safeDrawing
en lugar de systemBars
para contentWindowInsets
de Scaffold
. Sin embargo, la app se ve mucho menos envolvente con esa opción, ya que el contenido se corta de forma abrupta donde comienza el corte de pantalla, lo que no es mucho mejor que si la app no se renderizara en el corte de pantalla.
Para obtener una interfaz de usuario más envolvente, puedes controlar las inserciones en cada componente dentro de la pantalla.
- Actualiza
contentWindowInsets
deScaffold
para que sea constantemente de 0 dp (en lugar de solo paraPlayerScreen
). De esta manera, cada pantalla o componente dentro de una pantalla puede determinar cómo se comporta con respecto a las inserciones.
RoadReelsApp.kt
Scaffold(
...,
contentWindowInsets = WindowInsets(0.dp)
) { ... }
- Establece
windowInsetsPadding
de los elementos componiblesText
del encabezado de la fila para usar los componentes horizontales de las insercionessafeDrawing
. El componente superior de estas inserciones es controlado por la barra superior de la aplicación, y el componente inferior se mencionará más adelante.
MainScreen.kt
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
...
LazyColumn(
contentPadding = PaddingValues(bottom = dimensionResource(R.dimen.screen_edge_padding))
) {
items(NUM_ROWS) { rowIndex: Int ->
Text(
"Row $rowIndex",
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier
.padding(
horizontal = dimensionResource(R.dimen.screen_edge_padding),
vertical = dimensionResource(R.dimen.row_header_vertical_padding)
)
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal))
)
...
}
- Quita el parámetro
contentPadding
deLazyRow
. Luego, al principio y al final de cadaLazyRow
, agrega unSpacer
del ancho del componentesafeDrawing
correspondiente para asegurarte de que todas las miniaturas se puedan ver por completo. Usa el modificadorwidthIn
para asegurarte de que estos separadores tengan mínimo el mismo ancho que el padding del contenido. Sin estos elementos, los elementos al principio y los finales de la fila podrían estar ocultos detrás de las barras del sistema o el corte de pantalla, incluso cuando se deslicen por completo al principio o al final de la fila.
MainScreen.kt
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsEndWidth
import androidx.compose.foundation.layout.windowInsetsStartWidth
...
LazyRow(
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.list_item_spacing)),
) {
item {
Spacer(
Modifier
.windowInsetsStartWidth(WindowInsets.safeDrawing)
.widthIn(min = dimensionResource(R.dimen.screen_edge_padding))
)
}
items(NUM_ITEMS_PER_ROW) { ... }
item {
Spacer(
Modifier
.windowInsetsEndWidth(WindowInsets.safeDrawing)
.widthIn(min = dimensionResource(R.dimen.screen_edge_padding))
)
}
}
- Por último, agrega
Spacer
al final deLazyColumn
para tener en cuenta las posibles barras del sistema o las inserciones de corte de pantalla en la parte inferior de la pantalla. No es necesario un separador equivalente en la parte superior deLazyColumn
, ya que la barra superior de la aplicación se encarga de eso. Si la app usara una barra inferior en lugar de una superior, deberías agregarSpacer
al comienzo de la lista con el modificadorwindowInsetsTopHeight
. Además, si la app usara una barra inferior y superior, no se necesitaría ningún separador.
MainScreen.kt
import androidx.compose.foundation.layout.windowInsetsBottomHeight
...
LazyColumn(...){
items(NUM_ROWS) { ... }
item {
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
}
}
Perfecto. Las barras superiores de la aplicación son completamente visibles, y, cuando te desplazas hasta el final de una fila, ahora puedes ver todas las miniaturas en su totalidad.
Corrige la pantalla de detalles
La pantalla de detalles no es tan mala, pero el contenido aún se corta.
Como la pantalla de detalles no tiene contenido desplazable, para solucionarlo, solo se necesita agregar un modificador windowInsetsPadding
en el nivel superior Box
.
DetailScreen.kt
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
...
Box(
modifier = modifier
.padding(dimensionResource(R.dimen.screen_edge_padding))
.windowInsetsPadding(WindowInsets.safeDrawing)
) { ... }
Corrige la pantalla del reproductor
Si bien PlayerScreen
ya aplica padding para algunas o todas las inserciones de ventana de la barra del sistema, como se comenta en la sección anterior Cumple con los requisitos de calidad del SO Android Automotive: navegabilidad, no es suficiente para asegurarte de que no se oculte ahora que la app se renderiza en cortes de pantalla. En los dispositivos móviles, los recortes de pantalla casi siempre están contenidos, por completo, dentro de las barras del sistema. Sin embargo, en los automóviles, los cortes de pantalla pueden extenderse mucho más allá de las barras del sistema, lo que no cumple las suposiciones.
Para solucionar este problema, cambia el valor inicial de la variable windowInsetsForPadding
de cero a displayCutout
:
PlayerScreen.kt
import androidx.compose.foundation.layout.displayCutout
...
var windowInsetsForPadding = WindowInsets(WindowInsets.displayCutout)
Genial. La app aprovecha al máximo la pantalla y sigue siendo funcional.
Además, si ejecutas la app en un dispositivo móvil, también será más envolvente. Los elementos de la lista se renderizan hasta los bordes de la pantalla, incluso detrás de la barra de navegación.
12. Felicitaciones
Migraste y optimizaste, de forma correcta, tu primera app para usarse en el automóvil estacionado. Ahora es el momento de aplicar lo que aprendiste a tu propia app.
Pruebas para hacer
- Anula algunos de los valores de los recursos de dimensión para aumentar el tamaño de los elementos cuando se ejecuten en un automóvil.
- Prueba aún más configuraciones del emulador configurable.
- Prueba la app con algunas de las imágenes del emulador de OEM disponibles.
Lecturas adicionales
- Cómo compilar apps para usar en el SO Android Automotive con el auto estacionado
- Compilación de apps de video para el SO Android Automotive
- Cómo crear juegos para el SO Android Automotive
- Cómo compilar navegadores para el SO Android Automotive
- En la página Calidad de las apps para Android para vehículos, se describen los criterios con los que debe cumplir tu app para generar una excelente experiencia del usuario y aprobar la revisión de Play Store. Asegúrate de filtrar la categoría de la app.