Con Jetpack Compose para XR, puedes crear de forma declarativa tu IU y diseño espaciales con conceptos conocidos de Compose, como filas y columnas. Esto te permite extender tu IU de Android existente al espacio 3D o crear aplicaciones inmersivas en 3D completamente nuevas.
Si vas a espacializar una app existente basada en Views de Android, tienes varias opciones de desarrollo. Puedes usar APIs de interoperabilidad, usar Compose y Views juntos, o trabajar directamente con la biblioteca de SceneCore. Para obtener más detalles, consulta nuestra guía para trabajar con vistas.
Acerca de los subespacios y los componentes espacializados
Cuando escribas tu app para Android XR, es importante que comprendas los conceptos de subespacio y componentes espacializados.
Acerca del subespacio
Cuando desarrolles para Android XR, deberás agregar un subespacio a tu app o diseño. Un subespacio es una partición de espacio 3D dentro de tu app en la que puedes colocar contenido 3D, crear diseños 3D y agregar profundidad a contenido que, de otra forma, es 2D. Un subespacio solo se renderiza cuando la espacialización está habilitada. En el Espacio principal o en dispositivos que no son de XR, se ignora cualquier código dentro de ese subespacio.
Existen varias formas de crear un subespacio:
Subspace: Este elemento componible crea una jerarquía de IU espacial nueva e independiente. No hereda la posición, la orientación ni la escala espaciales de ningúnSubspacesuperior en el que esté anidado.Subspaceestá vinculado automáticamente por el cuadro de contenido recomendado del sistema.PlanarEmbeddedSubspace: Este elemento componible se puede colocar dentro de la jerarquía de la IU de tu app, lo que te permite mantener diseños para la IU espacial y en 2D.PlanarEmbeddedSubspacerespeta las restricciones y el posicionamiento de su elemento superior. El contenido 3D que se coloca dentro de él se posiciona en relación con esta área definida en 2D.
Para obtener más información, consulta Agrega un subespacio a tu app.
Acerca de los componentes espacializados
Elementos componibles de subespacio: Estos componentes solo se pueden renderizar en un subespacio.
Deben estar encerrados en Subspace antes de colocarse en un diseño 2D.
Un SubspaceModifier te permite agregar atributos como profundidad, desplazamiento y posicionamiento a tus elementos componibles de subespacio.
No es necesario llamar a otros componentes espacializados dentro de un subespacio. Consisten en elementos 2D convencionales unidos en un contenedor espacial. Estos elementos se pueden usar en diseños 2D o 3D si se definen para ambos. Cuando la espacialización no está habilitada, se ignorarán sus funciones espacializadas y se volverán a usar sus equivalentes en 2D.
Crea un panel espacial
Un SpatialPanel es un elemento componible de subespacio que te permite mostrar contenido de la app
. Por ejemplo, puedes mostrar reproducción de video, imágenes fijas o cualquier
otro contenido en un panel espacial.

Puedes usar SubspaceModifier para cambiar el tamaño, el comportamiento y el posicionamiento del panel espacial, como se muestra en el siguiente ejemplo.
Subspace { SpatialPanel( SubspaceModifier .height(824.dp) .width(1400.dp), dragPolicy = MovePolicy(), resizePolicy = ResizePolicy(), ) { SpatialPanelContent() } }
@Composable fun SpatialPanelContent() { Box( Modifier .background(color = Color.Black) .height(500.dp) .width(500.dp), contentAlignment = Alignment.Center ) { Text( text = "Spatial Panel", color = Color.White, fontSize = 25.sp ) } }
Puntos clave sobre el código
- Dado que las APIs de
SpatialPanelson elementos componibles de subespacio, debes llamar los dentro deSubspace. Si los llamas fuera de un subespacio, se arroja una excepción. - El tamaño del
SpatialPanelse estableció con las especificacionesheightywidthen elSubspaceModifier. Si omites estas especificaciones, el tamaño del panel se determinará según las medidas de su contenido. - Permite que el usuario mueva un panel agregando un
movablemodificador de subespacio. - Permite que el usuario cambie el tamaño de un panel agregando un
resizablemodificador de subespacio. - Para obtener detalles sobre el tamaño y la posición, consulta nuestra guía de diseño de paneles espaciales. Para obtener más detalles sobre la implementación de códigos, consulta nuestra documentación de referencia.
Cómo funciona el modificador movable
Cuando un usuario aleja un panel, de forma predeterminada, el modificador movable
escala el panel de manera similar a cómo el sistema cambia el tamaño de los paneles en el
espacio principal. Todo el contenido secundario hereda este comportamiento. Para inhabilitarlo, establece el parámetro shouldScaleWithDistance en false.
Crea un orbitador
Un orbitador es un componente de IU espacial. Está diseñado para adjuntarse a un
panel espacial o componente de diseño espacial correspondiente, como SpatialColumn,
SpatialRow, o SpatialBox. Por lo general, un orbitador contiene elementos de navegación y de acción contextual relacionados con la entidad a la que está anclado. Por ejemplo, si creaste un panel espacial para mostrar contenido de video, puedes agregar controles de reproducción de video dentro de un orbitador.

Como se muestra en el siguiente ejemplo, llama a un orbitador dentro del diseño 2D en un SpatialPanel para unir los controles del usuario, como la navegación. Si lo haces, se extraerán del diseño 2D y se adjuntarán al panel espacial según tu configuración.
Subspace { SpatialPanel( SubspaceModifier .height(824.dp) .width(1400.dp), dragPolicy = MovePolicy(), resizePolicy = ResizePolicy(), ) { SpatialPanelContent() OrbiterExample() } }
@Composable fun OrbiterExample() { Orbiter( position = ContentEdge.Bottom, offset = 96.dp, alignment = Alignment.CenterHorizontally ) { Surface(Modifier.clip(CircleShape)) { Row( Modifier .background(color = Color.Black) .height(100.dp) .width(600.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { Text( text = "Orbiter", color = Color.White, fontSize = 50.sp ) } } } }
Puntos clave sobre el código
- Dado que los orbitadores son componentes de IU espaciales, el código se puede reutilizar en diseños 2D o 3D. En un diseño 2D, tu app solo renderiza el contenido dentro del orbitador e ignora el orbitador mismo.
- Consulta nuestra guía de diseño para obtener más información sobre cómo usar y diseñar orbitadores.
Agrega varios paneles espaciales a un diseño espacial
Puedes crear varios paneles espaciales y colocarlos dentro de un diseño espacial con SpatialRow, SpatialColumn, SpatialBox y SpatialSpacer.

En el siguiente ejemplo de código, se muestra cómo hacerlo.
Subspace { SpatialRow { SpatialColumn { SpatialPanel(SubspaceModifier.height(250.dp).width(400.dp)) { SpatialPanelContent("Top Left") } SpatialPanel(SubspaceModifier.height(200.dp).width(400.dp)) { SpatialPanelContent("Middle Left") } SpatialPanel(SubspaceModifier.height(250.dp).width(400.dp)) { SpatialPanelContent("Bottom Left") } } SpatialColumn { SpatialPanel(SubspaceModifier.height(250.dp).width(400.dp)) { SpatialPanelContent("Top Right") } SpatialPanel(SubspaceModifier.height(200.dp).width(400.dp)) { SpatialPanelContent("Middle Right") } SpatialPanel(SubspaceModifier.height(250.dp).width(400.dp)) { SpatialPanelContent("Bottom Right") } } } }
@Composable fun SpatialPanelContent(text: String) { Column( Modifier .background(color = Color.Black) .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text( text = "Panel", color = Color.White, fontSize = 15.sp ) Text( text = text, color = Color.White, fontSize = 25.sp, fontWeight = FontWeight.Bold ) } }
Puntos clave sobre el código
SpatialRow,SpatialColumn,SpatialBoxySpatialSpacerson elementos componibles de subespacio y deben colocarse dentro de un subespacio.- Usa
SubspaceModifierpara personalizar tu diseño. - Para los diseños con varios paneles en fila, te recomendamos que establezcas un radio de curva de 825 dp con un
SubspaceModifierpara que los paneles rodeen al usuario. Para obtener más detalles, consulta nuestra guía de diseño.
Agrega un objeto 3D a tu diseño con SpatialGltfModel
Android XR admite el formato glTF para modelos 3D, que suele guardarse como
.glb archivos. Para agregar estos objetos a tu diseño, debes usar el
SpatialGltfModel elemento componible. Esta API simplifica el proceso de carga de recursos y la administración de su estado.
Para mostrar un modelo, primero define su fuente y estado con
rememberSpatialGltfModelState. Puedes cargar
modelos desde la carpeta assets de tu app, un URI o
raw data.
val modelState = rememberSpatialGltfModelState( source = SpatialGltfModelSource.fromPath( Paths.get("models/model_name.glb") ) )
Una vez que se define el estado, usa el elemento componible SpatialGltfModel para renderizarlo dentro de un subespacio.
SpatialGltfModel(state = modelState, modifier = SubspaceModifier)
Puntos clave sobre el código
- Carga asíncrona: El modelo se carga de forma asíncrona. Durante la composición inicial, su tamaño intrínseco puede ser cero. El diseño se vuelve a medir una vez que el modelo está listo.
- Control del estado: Usa
SpatialGltfModelState.statuspara consultar el estado de carga o controlar las animaciones. - Tamaño y escalamiento: De forma predeterminada, el tamaño del diseño coincide con el cuadro delimitador del recurso. Puedes anular esto con un
SubspaceModifier.sizepara escalar el modelo de manera uniforme para que quepa dentro de los límites especificados.
Usa un SceneCoreEntity para colocar entidades en tu diseño
El elemento componible SceneCoreEntity une las bibliotecas de Jetpack
SceneCore y Compose para XR para que puedas usar
entidades creadas con SceneCore en diseños de Compose. Esto te permite crear entidades de nivel inferior y componentes personalizados, al mismo tiempo que permite que Compose cambie el tamaño, la posición, el cambio de elementos superiores, la adición de elementos secundarios y la aplicación de modificadores a esas entidades.
Subspace { SceneCoreEntity( modifier = SubspaceModifier.offset(x = 50.dp), factory = { SurfaceEntity.create( session = session, pose = Pose.Identity, stereoMode = SurfaceEntity.StereoMode.MONO ) }, update = { entity -> // compose state changes may be applied to the // SceneCore entity here. entity.stereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE }, sizeAdapter = SceneCoreEntitySizeAdapter({ IntSize2d(it.width, it.height) }), ) { // Content here will be children of the SceneCoreEntity // in the scene graph. } }
Puntos clave sobre el código
- Bloque de fábrica: El bloque de fábrica es donde inicializas la entidad
SceneCoresubyacente. - Bloque de actualización: Usa el bloque de actualización para modificar las propiedades de la entidad en respuesta a los cambios en tu estado de Compose.
- Adaptación de tamaño: El
sizeAdaptercomunica las dimensiones de la entidad al sistema de diseño de Compose.
Información adicional
- Consulta Agrega modelos 3D a tu app para comprender mejor cómo cargar contenido 3D
dentro de un
SceneCoreEntity.
Agrega una superficie para contenido de imagen o video
Un SpatialExternalSurface es un elemento componible de subespacio que crea y
administra el Surface en el que tu app puede dibujar contenido, como una
imagen o video. SpatialExternalSurface admite contenido estereoscópico o monoscópico.
En este ejemplo, se muestra cómo cargar video estereoscópico uno al lado del otro con
Media3 Exoplayer y SpatialExternalSurface:
@OptIn(ExperimentalComposeApi::class) @Composable fun SpatialExternalSurfaceContent() { val context = LocalContext.current Subspace { SpatialExternalSurface( modifier = SubspaceModifier .width(1200.dp) // Default width is 400.dp if no width modifier is specified .height(676.dp), // Default height is 400.dp if no height modifier is specified // Use StereoMode.Mono, StereoMode.SideBySide, or StereoMode.TopBottom, depending // upon which type of content you are rendering: monoscopic content, side-by-side stereo // content, or top-bottom stereo content stereoMode = StereoMode.SideBySide, ) { val exoPlayer = remember { ExoPlayer.Builder(context).build() } val videoUri = Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) // Represents a side-by-side stereo video, where each frame contains a pair of // video frames arranged side-by-side. The frame on the left represents the left // eye view, and the frame on the right represents the right eye view. .path("sbs_video.mp4") .build() val mediaItem = MediaItem.fromUri(videoUri) // onSurfaceCreated is invoked only one time, when the Surface is created onSurfaceCreated { surface -> exoPlayer.setVideoSurface(surface) exoPlayer.setMediaItem(mediaItem) exoPlayer.prepare() exoPlayer.play() } // onSurfaceDestroyed is invoked when the SpatialExternalSurface composable and its // associated Surface are destroyed onSurfaceDestroyed { exoPlayer.release() } } } }
Puntos clave sobre el código
- Establece
StereoModeenMono,SideBySideoTopBottomsegún el tipo de contenido que renderices:Mono: La imagen o el fotograma consta de una sola imagen idéntica que se muestra en ambos ojos.SideBySide: La imagen o el fotograma contiene un par de imágenes o fotogramas dispuestos uno al lado del otro, en los que la imagen o el fotograma de la izquierda representa la vista del ojo izquierdo, y la imagen o el fotograma de la derecha representa la vista del ojo derecho.TopBottom: La imagen o el fotograma contiene un par de imágenes o fotogramas de video apilados verticalmente, en los que la imagen o el fotograma de la parte superior representan la vista del ojo izquierdo, y la imagen o el fotograma de la parte inferior representan la vista del ojo derecho.
SpatialExternalSurfacesolo admite superficies rectangulares.- Este
Surfaceno captura eventos de entrada. - No es posible sincronizar los cambios de
StereoModecon la renderización de la aplicación o la decodificación de video. - Este elemento componible no se puede renderizar delante de otros paneles, por lo que no debes usar un
MovePolicysi hay otros paneles en el diseño.
Agrega una superficie para contenido de video protegido por DRM
SpatialExternalSurface también admite la reproducción de transmisiones de video por Internet protegidas por DRM. Para habilitar esto, debes crear una superficie segura que se renderice en búferes gráficos protegidos. Esto evita que el contenido se grabe en pantalla o que los componentes del sistema no seguros accedan a él.
Para crear una superficie segura, establece el parámetro surfaceProtection en SurfaceProtection.Protected en el elemento componible SpatialExternalSurface.
Además, debes configurar Media3 ExoPlayer con la información de DRM adecuada para controlar la adquisición de licencias desde un servidor de licencias.
En el siguiente ejemplo, se muestra cómo configurar SpatialExternalSurface y ExoPlayer para reproducir una transmisión de video por Internet protegida por DRM:
@OptIn(ExperimentalComposeApi::class) @Composable fun DrmSpatialVideoPlayer() { val context = LocalContext.current Subspace { SpatialExternalSurface( modifier = SubspaceModifier .width(1200.dp) .height(676.dp), stereoMode = StereoMode.SideBySide, surfaceProtection = SurfaceProtection.Protected ) { val exoPlayer = remember { ExoPlayer.Builder(context).build() } // Define the URI for your DRM-protected content and license server. val videoUri = "https://your-content-provider.com/video.mpd" val drmLicenseUrl = "https://your-license-server.com/license" // Build a MediaItem with the necessary DRM configuration. val mediaItem = MediaItem.Builder() .setUri(videoUri) .setDrmConfiguration( MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID) .setLicenseUri(drmLicenseUrl) .build() ) .build() onSurfaceCreated { surface -> // The created surface is secure and can be used by the player. exoPlayer.setVideoSurface(surface) exoPlayer.setMediaItem(mediaItem) exoPlayer.prepare() exoPlayer.play() } onSurfaceDestroyed { exoPlayer.release() } } } }
Puntos clave sobre el código
- Superficie protegida: Es fundamental establecer
surfaceProtection = SurfaceProtection.ProtectedenSpatialExternalSurfacepara que elSurfacesubyacente esté respaldado por búferes seguros adecuados para el contenido de DRM. - Configuración de DRM: Debes configurar el
MediaItemcon el esquema de DRM (por ejemplo,C.WIDEVINE_UUID) y el URI de tu servidor de licencias. ExoPlayer usa esta información para administrar la sesión de DRM. - Contenido seguro: Cuando se renderiza en una superficie protegida, el contenido de video se decodifica y se muestra en una ruta segura, lo que ayuda a satisfacer los requisitos de licencias de contenido. Esto también evita que el contenido aparezca en capturas de pantalla.
Agrega otros componentes de IU espaciales
Los componentes de IU espaciales se pueden colocar en cualquier lugar de la jerarquía de la IU de tu aplicación. Estos elementos se pueden reutilizar en tu IU 2D, y sus atributos espaciales solo serán visibles cuando se habiliten las capacidades espaciales. Esto te permite agregar elevación a menús, diálogos y otros componentes sin necesidad de escribir el código dos veces. Consulta los siguientes ejemplos de IU espacial para comprender mejor cómo usar estos elementos.
Componente de la IU |
Cuando la espacialización está habilitada |
En el entorno 2D |
|---|---|---|
|
El panel se desplazará ligeramente hacia atrás en la profundidad Z para mostrar un diálogo elevado. |
Vuelve a |
|
El panel se desplazará ligeramente hacia atrás en la profundidad Z para mostrar una ventana emergente elevada. |
Vuelve a |
|
Se puede establecer |
Se muestra sin elevación espacial. |
SpatialDialog
Este es un ejemplo de un diálogo que se abre después de una breve demora. Cuando
SpatialDialog se usa, el diálogo aparece en la misma profundidad Z que el
panel espacial, y el panel se desplaza hacia atrás en 125 dp cuando se habilita la espacialización. SpatialDialog también se puede usar cuando la espacialización no está habilitada, en
cuyo caso SpatialDialog vuelve a su equivalente en 2D, Dialog.
@Composable fun DelayedDialog() { var showDialog by remember { mutableStateOf(false) } LaunchedEffect(Unit) { delay(3000) showDialog = true } if (showDialog) { SpatialDialog( onDismissRequest = { showDialog = false }, SpatialDialogProperties( dismissOnBackPress = true ) ) { Box( Modifier .height(150.dp) .width(150.dp) ) { Button(onClick = { showDialog = false }) { Text("OK") } } } } }
Puntos clave sobre el código
- Este es un ejemplo de
SpatialDialog. El uso deSpatialPopupySpatialElevationes muy similar. Para obtener más detalles, consulta nuestra referencia de la API.
Crea paneles y diseños personalizados
Para crear paneles personalizados que no sean compatibles con Compose para XR, puedes trabajar
directamente con PanelEntity instancias y el gráfico de escena con las
SceneCore APIs.
Ancla orbitadores a paneles y diseños espaciales
Puedes anclar un orbitador a SpatialPanels y componentes de diseño espacial declarados en Compose. Esto implica declarar un orbitador en un diseño espacial de elementos de la IU, como SpatialRow, SpatialColumn o SpatialBox. El orbitador se ancla al elemento superior más cercano a donde lo declaraste.
El comportamiento del orbitador está determinado por el lugar donde lo declares:
- En un diseño 2D incluido en un
SpatialPanel(como se muestra en un fragmento de código anterior), el orbitador se ancla a eseSpatialPanel. - En un
Subspace, el orbitador se ancla a la entidad principal más cercana, que es el diseño espacial en el que se declara el orbitador.
En el siguiente ejemplo, se muestra cómo anclar un orbitador a una fila espacial:
Subspace { SpatialRow { Orbiter( position = ContentEdge.Top, offset = 8.dp, offsetType = OrbiterOffsetType.InnerEdge, shape = SpatialRoundedCornerShape(size = CornerSize(50)) ) { Text( "Hello World!", style = MaterialTheme.typography.titleMedium, modifier = Modifier .background(Color.White) .padding(16.dp) ) } SpatialPanel( SubspaceModifier .height(824.dp) .width(1400.dp) ) { Box( modifier = Modifier .background(Color.Red) ) } SpatialPanel( SubspaceModifier .height(824.dp) .width(1400.dp) ) { Box( modifier = Modifier .background(Color.Blue) ) } } }
Puntos clave sobre el código
- Cuando declaras un orbitador fuera de un diseño 2D, el orbitador se ancla a su entidad principal más cercana. En este caso, el orbitador se ancla a la parte superior del
SpatialRowen el que se declara. - Los diseños espaciales, como
SpatialRow,SpatialColumnySpatialBox, tienen entidades sin contenido asociadas. Por lo tanto, un orbitador declarado en un diseño espacial se ancla a ese diseño.
Consulta también
- Agrega modelos 3D a tu app
- Cómo desarrollar una IU de apps para Android basadas en vistas
- Implementa Material Design para XR