Cómo desarrollar una IU con Jetpack Compose para XR

Con Jetpack Compose para XR, puedes compilar 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 compilar aplicaciones envolventes en 3D completamente nuevas.

Si vas a espacializar una app existente basada en Android Views, tienes varias opciones de desarrollo. Puedes usar APIs de interoperabilidad, usar Compose y Views juntos, o trabajar directamente con la biblioteca de SceneCore. Consulta nuestra guía para trabajar con vistas para obtener más detalles.

Acerca de los subespacios y los componentes espacializados

Cuando escribes tu app para Android XR, es importante comprender los conceptos de subespacio y componentes espacializados.

Acerca del subespacio

Cuando desarrolles para Android XR, deberás agregar un Subspace 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, compilar 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 dos formas de crear un subespacio:

  • Subspace: Este elemento componible se puede colocar en cualquier lugar 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 sin perder el contexto entre los archivos. Esto facilita el uso compartido de elementos como la arquitectura de la app existente entre la XR y otros factores de forma sin necesidad de elevar el estado a través de todo el árbol de la IU ni rediseñar la app.
  • ApplicationSubspace: Esta función crea un subespacio a nivel de la app únicamente y debe colocarse en el nivel superior de la jerarquía de la IU espacial de tu aplicación. ApplicationSubspace renderiza contenido espacial con VolumeConstraints opcional. A diferencia de Subspace, ApplicationSubspace no se puede anidar dentro de otro Subspace o ApplicationSubspace.

Para obtener más información, consulta Cómo agregar 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 dentro de Subspace o setSubspaceContent() antes de colocarse dentro de 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 envueltos 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.

Cómo crear un panel espacial

Un SpatialPanel es un elemento componible de subespacio que te permite mostrar contenido de la app. Por ejemplo, puedes mostrar la reproducción de videos, imágenes fijas o cualquier otro contenido en un panel espacial.

Ejemplo de un panel de IU espacial

Puedes usar SubspaceModifier para cambiar el tamaño, el comportamiento y la posición del panel espacial, como se muestra en el siguiente ejemplo.

Subspace {
    SpatialPanel(
        SubspaceModifier
            .height(824.dp)
            .width(1400.dp)
            .movable()
            .resizable()
    ) {
        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

  • Debido a que las APIs de SpatialPanel son elementos componibles de subespacio, debes llamarlas dentro de Subspace. Si se llama a estos métodos fuera de un subespacio, se arroja una excepción.
  • El tamaño de SpatialPanel se estableció con las especificaciones height y width en SubspaceModifier. Si se omiten estas especificaciones, las medidas de su contenido determinarán el tamaño del panel.
  • Permite que el usuario cambie el tamaño o mueva el panel agregando los modificadores movable o resizable.
  • Consulta nuestra guía de diseño de paneles espaciales para obtener detalles sobre el tamaño y la posición. Consulta nuestra documentación de referencia para obtener más detalles sobre la implementación de códigos.

Cómo funciona un modificador de subespacio móvil

Cuando un usuario aleja un panel, de forma predeterminada, un modificador de subespacio movible lo ajusta de manera similar a como el sistema ajusta los paneles en el espacio principal. Todo el contenido infantil hereda este comportamiento. Para inhabilitar esta opción, establece el parámetro scaleWithDistance en false.

Crea un orbitador

Un orbitador es un componente de la IU espacial. Está diseñado para adjuntarse a un panel espacial, un diseño o alguna otra entidad correspondiente. 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, podrías agregar controles de reproducción de video dentro de un orbitador.

Ejemplo 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 de tu diseño 2D y se adjuntarán al panel espacial según tu configuración.

Subspace {
    SpatialPanel(
        SubspaceModifier
            .height(824.dp)
            .width(1400.dp)
            .movable()
            .resizable()
    ) {
        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 espaciales de la IU, 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 y lo ignora.
  • Consulta nuestra guía de diseño para obtener más información sobre cómo usar y diseñar orbitadores.

Cómo agregar 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 SpatialLayoutSpacer.

Ejemplo de varios paneles espaciales en un diseño espacial

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, SpatialBox y SpatialLayoutSpacer son todos elementos componibles de subespacio y deben colocarse dentro de un subespacio.
  • Usa SubspaceModifier para 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 SubspaceModifier para que los paneles rodeen al usuario. Consulta nuestra guía de diseño para obtener más detalles.

Usa un volumen para colocar un objeto 3D en tu diseño

Para colocar un objeto 3D en tu diseño, deberás usar un elemento componible de subespacio llamado volumen. Este es un ejemplo de cómo hacerlo.

Ejemplo de un objeto 3D en un diseño

Subspace {
    SpatialPanel(
        SubspaceModifier.height(1500.dp).width(1500.dp)
            .resizable().movable()
    ) {
        ObjectInAVolume(true)
        Box(
            Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Text(
                text = "Welcome",
                fontSize = 50.sp,
            )
        }
    }
}

@OptIn(ExperimentalSubspaceVolumeApi::class)
@Composable
fun ObjectInAVolume(show3DObject: Boolean) {

Información adicional

Agrega una superficie para el contenido de imágenes o videos

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 un video. SpatialExternalSurface admite contenido estereoscópico o monoscópico.

En este ejemplo, se muestra cómo cargar un video estereoscópico lado a lado 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 StereoMode en Mono, SideBySide o TopBottom según el tipo de contenido que renderices:
    • Mono: El fotograma de la imagen o el video consta de una sola imagen idéntica que se muestra en ambos ojos.
    • SideBySide: El fotograma de imagen o video contiene un par de imágenes o fotogramas de video dispuestos uno al lado del otro, en el 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: El fotograma de imagen o video 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.
  • SpatialExternalSurface solo admite superficies rectangulares.
  • Este Surface no captura eventos de entrada.
  • No es posible sincronizar los cambios de StereoMode con 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 modificadores movibles si hay otros paneles en el diseño.

Agrega una superficie para el contenido de video protegido por DRM

SpatialExternalSurface también admite la reproducción de transmisiones de video protegidas por DRM. Para habilitar esta opción, 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 SpatialExternalSurface componible. 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 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.Protected en SpatialExternalSurface para que el Surface subyacente esté respaldado por búferes seguros adecuados para el contenido protegido por DRM.
  • Configuración de DRM: Debes configurar MediaItem con 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 las capturas de pantalla.

Agrega otros componentes de la IU espacial

Los componentes de IU espaciales se pueden colocar en cualquier lugar de la jerarquía de 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 tu código dos veces. Consulta los siguientes ejemplos de IU espacial para comprender mejor cómo usar estos elementos.

Componente de IU

Cuando la espacialización está habilitada

En un entorno 2D

SpatialDialog

El panel se desplazará ligeramente hacia atrás en la profundidad Z para mostrar un diálogo elevado.

Recurre a Dialog en 2D.

SpatialPopup

El panel se desplazará ligeramente hacia atrás en la profundidad Z para mostrar una ventana emergente elevada.

Se recurre a un Popup en 2D.

SpatialElevation

Se puede configurar SpatialElevationLevel para agregar elevación.

Muestra sin elevación espacial.

SpatialDialog

Este es un ejemplo de un diálogo que se abre después de una breve demora. Cuando se usa SpatialDialog, 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 recurre a su contraparte 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

Crea paneles y diseños personalizados

Para crear paneles personalizados que no son compatibles con Compose para XR, puedes trabajar directamente con instancias de PanelEntity y el gráfico de escena con las APIs de SceneCore.

Ancla los orbitadores a diseños espaciales y otras entidades

Puedes anclar un orbitador a cualquier entidad declarada 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 a la entidad principal más cercana al lugar donde lo declaraste.

El comportamiento del orbitador se determina según dónde lo declares:

  • En un diseño 2D incluido en un SpatialPanel (como se muestra en un fragmento de código anterior), el orbitador se fija a ese SpatialPanel.
  • 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 SpatialRow en el que se declara.
  • Los diseños espaciales, como SpatialRow, SpatialColumn y SpatialBox, 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