Développer une UI avec Jetpack Compose pour XR

Avec Jetpack Compose pour XR, vous pouvez créer votre UI et votre mise en page spatiales de manière déclarative à l'aide de concepts Compose connus tels que les lignes et les colonnes. Cela vous permet d'étendre votre UI Android existante dans l'espace 3D ou de créer des applications 3D immersives entièrement nouvelles.

Si vous spatialisez une application existante basée sur Android Views, plusieurs options de développement s'offrent à vous. Vous pouvez utiliser des API d'interopérabilité, utiliser Compose et Views ensemble, ou travailler directement avec la bibliothèque SceneCore. Pour en savoir plus, consultez notre guide sur l'utilisation des vues.

À propos des sous-espaces et des composants spatialisés

Lorsque vous écrivez votre application pour Android XR, il est important de comprendre les concepts de sous-espace et de composants spatialisés.

À propos des sous-espaces

Lorsque vous développez pour Android XR, vous devez ajouter un Subspace à votre application ou à votre mise en page. Un sous-espace est une partition d'un espace 3D au sein de votre application. Vous pouvez y placer du contenu 3D, y créer des mises en page 3D et ajouter de la profondeur à des contenus qui seraient en 2D autrement. Un sous-espace n'est rendu que lorsque la spatialisation est activée. Dans l'espace personnel ou sur les appareils non XR, tout code se trouvant dans ce sous-espace est ignoré.

Il existe deux façons de créer un sous-espace :

  • Subspace : ce composable peut être placé n'importe où dans la hiérarchie de l'UI de votre application, ce qui vous permet de conserver les mises en page pour l'UI 2D et spatiale sans perdre le contexte entre les fichiers. Cela facilite le partage d'éléments tels que l'architecture d'application existante entre la XR et d'autres facteurs de forme, sans avoir à hisser l'état dans l'ensemble de l'arborescence de l'UI ni à repenser l'architecture de votre application.
  • ApplicationSubspace : cette fonction ne crée qu'un sous-espace au niveau de l'application et doit être placée au niveau le plus élevé de la hiérarchie de l'UI spatiale de votre application. ApplicationSubspace affiche le contenu spatial avec VolumeConstraints en option. Contrairement à Subspace, ApplicationSubspace ne peut pas être imbriqué dans un autre Subspace ni dans un autre ApplicationSubspace.

Pour en savoir plus, consultez Ajouter un sous-espace à votre application.

À propos des composants spatialisés

Composables Subspace : ces composants ne peuvent être affichés que dans un sous-espace. Ils doivent être placés entre Subspace ou setSubspaceContent() avant d'être placés dans une mise en page 2D. Un SubspaceModifier vous permet d'ajouter des attributs tels que la profondeur, le décalage et le positionnement à vos composables de sous-espace.

Il n'est pas nécessaire d'appeler les autres composants spatialisés dans un sous-espace. Ils se composent d'éléments 2D classiques enveloppés dans un conteneur spatial. Ces éléments peuvent être utilisés dans des mises en page 2D ou 3D s'ils sont définis pour les deux. Lorsque la spatialisation n'est pas activée, leurs fonctionnalités spatialisées sont ignorées et elles reviennent à leurs équivalents 2D.

Créer un panneau spatial

Un SpatialPanel est un composable de sous-espace qui vous permet d'afficher du contenu d'application. Par exemple, vous pouvez afficher la lecture vidéo, des images fixes ou tout autre contenu dans un panneau spatial.

Exemple de panneau d'interface utilisateur spatiale

Vous pouvez utiliser SubspaceModifier pour modifier la taille, le comportement et le positionnement du panneau spatial, comme illustré dans l'exemple suivant.

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
        )
    }
}

Points clés concernant le code

  • Étant donné que les API SpatialPanel sont des composables de sous-espace, vous devez les appeler dans Subspace. Les appeler en dehors d'un sous-espace génère une exception.
  • La taille de SpatialPanel a été définie à l'aide des spécifications height et width sur SubspaceModifier. Si vous omettez ces spécifications, la taille du panneau sera déterminée par les dimensions de son contenu.
  • Autorisez l'utilisateur à redimensionner ou à déplacer le panneau en ajoutant les modificateurs movable ou resizable.
  • Pour en savoir plus sur la taille et le positionnement, consultez nos conseils de conception des panneaux spatiaux. Pour en savoir plus sur l'implémentation du code, consultez notre documentation de référence.

Fonctionnement d'un modificateur de sous-espace mobile

Lorsqu'un utilisateur éloigne un panneau de lui, un modificateur de sous-espace déplaçable met par défaut le panneau à l'échelle de la même manière que le système redimensionne les panneaux dans l'espace personnel. Tous les contenus enfants héritent de ce comportement. Pour désactiver cette fonctionnalité, définissez le paramètre scaleWithDistance sur false.

Créer un orbiteur

Un orbiteur est un composant d'UI spatiale. Il est conçu pour être associé à un panneau spatial, une mise en page ou une autre entité correspondants. Un orbiteur contient généralement des éléments de navigation et d'action contextuels liés à l'entité à laquelle il est ancré. Par exemple, si vous avez créé un panneau spatial pour afficher du contenu vidéo, vous pouvez ajouter des commandes de lecture vidéo dans un orbiteur.

Exemple d'orbiteur

Comme le montre l'exemple suivant, appelez un orbiteur dans la mise en page 2D dans un SpatialPanel pour encapsuler les commandes utilisateur telles que la navigation. Cela les extrait de votre mise en page 2D et les associe au panneau spatial en fonction de votre configuration.

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
                )
            }
        }
    }
}

Points clés concernant le code

  • Étant donné que les orbiteurs sont des composants d'UI spatiaux, le code peut être réutilisé dans des mises en page 2D ou 3D. Dans une mise en page 2D, votre application n'affiche que le contenu à l'intérieur de l'orbiteur et ignore l'orbiteur lui-même.
  • Pour en savoir plus sur l'utilisation et la conception des orbiters, consultez nos conseils de conception.

Ajouter plusieurs panneaux spatiaux à une mise en page spatiale

Vous pouvez créer plusieurs panneaux spatiaux et les placer dans une mise en page spatiale à l'aide de SpatialRow, SpatialColumn, SpatialBox et SpatialLayoutSpacer.

Exemple de plusieurs panneaux spatiaux dans une mise en page spatiale

L'exemple de code suivant montre comment procéder.

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
        )
    }
}

Points clés concernant le code

  • SpatialRow, SpatialColumn, SpatialBox et SpatialLayoutSpacer sont tous des composables de sous-espace et doivent être placés dans un sous-espace.
  • Utilisez SubspaceModifier pour personnaliser votre mise en page.
  • Pour les mises en page avec plusieurs panneaux sur une même ligne, nous vous recommandons de définir un rayon de courbure de 825 dp à l'aide d'un SubspaceModifier afin que les panneaux entourent l'utilisateur. Pour en savoir plus, consultez nos conseils de conception.

Utiliser un volume pour placer un objet 3D dans votre mise en page

Pour placer un objet 3D dans votre mise en page, vous devez utiliser un composable de sous-espace appelé volume. Voici un exemple :

Exemple d'objet 3D dans une mise en page

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) {

Informations supplémentaires

Ajouter une surface pour le contenu d'image ou vidéo

Un SpatialExternalSurface est un composable de sous-espace qui crée et gère le Surface dans lequel votre application peut dessiner du contenu, comme une image ou une vidéo. SpatialExternalSurface est compatible avec les contenus stéréoscopiques et monoscopiques.

Cet exemple montre comment charger une vidéo stéréoscopique côte à côte à l'aide de Media3 Exoplayer et 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() }
        }
    }
}

Points clés concernant le code

  • Définissez StereoMode sur Mono, SideBySide ou TopBottom selon le type de contenu que vous affichez :
    • Mono : l'image ou la vidéo est constituée d'une seule et même image affichée pour les deux yeux.
    • SideBySide : l'image ou la vidéo contient une paire d'images ou de vidéos côte à côte, où l'image ou la vidéo de gauche représente la vue de l'œil gauche, et l'image ou la vidéo de droite représente la vue de l'œil droit.
    • TopBottom : le frame d'image ou de vidéo contient une paire d'images ou de frames vidéo empilés verticalement, où l'image ou le frame en haut représente la vue de l'œil gauche, et l'image ou le frame en bas représente la vue de l'œil droit.
  • SpatialExternalSurface n'est compatible qu'avec les surfaces rectangulaires.
  • Ce Surface ne capture pas les événements d'entrée.
  • Il n'est pas possible de synchroniser les modifications StereoMode avec le rendu de l'application ou le décodage vidéo.
  • Ce composable ne peut pas s'afficher devant d'autres panneaux. Vous ne devez donc pas utiliser de modificateurs mobiles s'il existe d'autres panneaux dans la mise en page.

Ajouter une surface pour le contenu vidéo protégé par DRM

SpatialExternalSurface est également compatible avec la lecture de flux vidéo protégés par DRM. Pour ce faire, vous devez créer une surface sécurisée qui s'affiche dans des tampons graphiques protégés. Cela empêche l'enregistrement d'écran du contenu ou l'accès à celui-ci par des composants système non sécurisés.

Pour créer une surface sécurisée, définissez le paramètre surfaceProtection sur SurfaceProtection.Protected dans le composable SpatialExternalSurface. De plus, vous devez configurer Media3 ExoPlayer avec les informations DRM appropriées pour gérer l'acquisition de licence à partir d'un serveur de licences.

L'exemple suivant montre comment configurer SpatialExternalSurface et ExoPlayer pour lire un flux vidéo protégé par 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() }
        }
    }
}

Points clés concernant le code

  • Il est essentiel de définir surfaceProtection = SurfaceProtection.Protected sur SpatialExternalSurface pour que le Surface sous-jacent soit soutenu par des tampons sécurisés adaptés au contenu DRM.
  • Configuration DRM : vous devez configurer MediaItem avec le schéma DRM (par exemple, C.WIDEVINE_UUID) et l'URI de votre serveur de licence. ExoPlayer utilise ces informations pour gérer la session DRM.
  • Contenu sécurisé : lorsque le contenu vidéo est rendu sur une surface protégée, il est décodé et affiché sur un chemin sécurisé, ce qui permet de répondre aux exigences de licence de contenu. Cela empêche également le contenu d'apparaître dans les captures d'écran.

Ajouter d'autres composants d'UI spatiale

Les composants d'UI spatiaux peuvent être placés n'importe où dans la hiérarchie de l'UI de votre application. Ces éléments peuvent être réutilisés dans votre UI 2D, et leurs attributs spatiaux ne seront visibles que lorsque les fonctionnalités spatiales seront activées. Cela vous permet d'ajouter de l'élévation aux menus, aux boîtes de dialogue et à d'autres composants sans avoir à écrire votre code deux fois. Consultez les exemples d'UI spatiale suivants pour mieux comprendre comment utiliser ces éléments.

Composant d'UI

Lorsque la spatialisation est activée

Dans un environnement 2D

SpatialDialog

Le panneau recule légèrement en profondeur Z pour afficher une boîte de dialogue surélevée.

Repasse en mode 2D Dialog.

SpatialPopup

Le panneau recule légèrement en profondeur Z pour afficher un pop-up surélevé.

Utilise un Popup en 2D en cas de besoin.

SpatialElevation

SpatialElevationLevel peut être défini pour ajouter de l'élévation.

Afficher sans élévation spatiale.

SpatialDialog

Voici un exemple de boîte de dialogue qui s'ouvre après un court délai. Lorsque SpatialDialog est utilisé, la boîte de dialogue s'affiche à la même profondeur Z que le panneau spatial, et le panneau est repoussé de 125 dp lorsque la spatialisation est activée. SpatialDialog peut également être utilisé lorsque la spatialisation n'est pas activée. Dans ce cas, SpatialDialog revient à son équivalent 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")
                }
            }
        }
    }
}

Points clés concernant le code

Créer des panneaux et des mises en page personnalisés

Pour créer des panneaux personnalisés non compatibles avec Compose pour XR, vous pouvez travailler directement avec les instances PanelEntity et le graphique de scène à l'aide des API SceneCore.

Ancrer des orbiteurs à des mises en page spatiales et à d'autres entités

Vous pouvez ancrer un orbiteur à n'importe quelle entité déclarée dans Compose. Cela implique de déclarer un orbiteur dans une mise en page spatiale d'éléments d'UI tels que SpatialRow, SpatialColumn ou SpatialBox. L'orbiteur s'ancre à l'entité parente la plus proche de l'endroit où vous l'avez déclaré.

Le comportement de l'orbiteur est déterminé par l'endroit où vous le déclarez :

  • Dans une mise en page 2D enveloppée dans un SpatialPanel (comme indiqué dans un extrait de code précédent), l'orbiteur s'ancre à ce SpatialPanel.
  • Dans un Subspace, l'orbiteur s'ancre à l'entité parente la plus proche, qui correspond à la mise en page spatiale dans laquelle l'orbiteur est déclaré.

L'exemple suivant montre comment ancrer un orbiteur à une ligne spatiale :

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)
            )
        }
    }
}

Points clés concernant le code

  • Lorsque vous déclarez un orbiteur en dehors d'une mise en page 2D, il s'ancre à son entité parente la plus proche. Dans ce cas, l'orbiteur s'ancre en haut du SpatialRow dans lequel il est déclaré.
  • Les mises en page spatiales telles que SpatialRow, SpatialColumn et SpatialBox sont toutes associées à des entités sans contenu. Par conséquent, un orbiteur déclaré dans une mise en page spatiale s'ancre à cette mise en page.

Voir aussi