Avec Jetpack Compose pour XR, vous pouvez créer de manière déclarative votre UI et votre mise en page spatiales à l'aide de concepts Compose familiers tels que les lignes et les colonnes. Vous pouvez ainsi étendre votre UI Android existante à l'espace 3D ou créer des applications 3D immersives entièrement nouvelles.
Si vous spatialisez une application Android basée sur des vues existante, 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 sous-espace à 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, 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'affichage restreint ou sur les appareils non XR, tout code contenu dans ce sous-espace est ignoré.
Il existe plusieurs façons de créer un sous-espace :
Subspace: ce composable crée une hiérarchie d'UI spatiale nouvelle et indépendante. Il n'hérite pas de la position spatiale, de l'orientation ni de l'échelle d'unSubspaceparent dans lequel il est imbriqué.Subspaceest automatiquement lié à la zone de contenu recommandée par le système.PlanarEmbeddedSubspace: ce composable peut être placé dans la hiérarchie d'UI de votre application, ce qui vous permet de conserver des mises en page pour l'UI 2D et spatiale.PlanarEmbeddedSubspacerespecte les contraintes et le positionnement de son parent. Le contenu 3D placé à l'intérieur est ensuite positionné par rapport à cette zone définie en 2D.
Pour en savoir plus, consultez la section Ajouter un sous-espace à votre application.
À propos des composants spatialisés
Composables de sous-espace : ces composants ne peuvent être rendus que dans un sous-espace.
Ils doivent être placés dans Subspace 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.
D'autres composants spatialisés n'ont pas besoin d'être appelés dans un sous-espace. Ils se composent d'éléments 2D classiques encapsulé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 le contenu de l'application. Par exemple, vous pouvez afficher la lecture vidéo, des images fixes ou tout autre contenu dans un panneau spatial.

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), 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 ) } }
Points clés concernant le code
- Étant donné que les API
SpatialPanelsont des composables de sous-espace, vous devez les appeler dansSubspace. Si vous les appelez en dehors d'un sous-espace, une exception est générée. - La taille du
SpatialPanela été définie à l'aide des spécificationsheightetwidthsur leSubspaceModifier. Si vous omettez ces spécifications, la taille du panneau est déterminée par les mesures de son contenu. - Autorisez l'utilisateur à déplacer un panneau en ajoutant un
movablemodificateur de sous-espace. - Autorisez l'utilisateur à redimensionner un panneau en ajoutant un
resizablemodificateur de sous-espace. - Pour en savoir plus sur le dimensionnement et le positionnement, consultez nos conseils de conception de panneaux spatiaux. Pour en savoir plus sur l'implémentation du code, consultez notre documentation de référence.
Fonctionnement du modificateur movable
Par défaut, lorsqu'un utilisateur éloigne un panneau, le modificateur movable
met le panneau à l'échelle de la même manière que le système redimensionne les panneaux dans
l'affichage restreint. Tout le contenu enfant hérite de ce comportement. Pour désactiver cette fonctionnalité, définissez le paramètre shouldScaleWithDistance sur false.
Créer un orbiteur
Un orbiteur est un composant d'UI spatiale. Il est conçu pour être associé à un
panneau spatial ou à un composant de mise en page spatiale correspondant, tel que SpatialColumn,
SpatialRow, ou SpatialBox. Un orbiteur contient généralement des éléments de navigation et d'action contextuelle 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.

Comme illustré dans l'exemple suivant, appelez un orbiteur dans la mise en page 2D d'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), 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 ) } } } }
Points clés concernant le code
- Étant donné que les orbiteurs sont des composants d'UI spatiale, le code peut être réutilisé dans des mises en page 2D ou 3D. Dans une mise en page 2D, votre application ne rend 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 orbiteurs, 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
SpatialSpacer.

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, etSpatialSpacersont tous des composables de sous-espace et doivent être placés dans un sous-espace.- Utilisez
SubspaceModifierpour personnaliser votre mise en page. - Pour les mises en page comportant plusieurs panneaux sur une même ligne, nous vous recommandons de définir un rayon de courbe de 825 dp à l'aide d'un
SubspaceModifierafin que les panneaux entourent votre utilisateur. Pour en savoir plus, consultez nos conseils de conception.
Ajouter un objet 3D à votre mise en page à l'aide de SpatialGltfModel
Android XR est compatible avec le format glTF pour les modèles 3D, généralement enregistrés sous forme de fichiers
.glb. Pour ajouter ces objets à votre mise en page, vous devez utiliser le
SpatialGltfModel composable. Cette API simplifie le processus de chargement des éléments et de gestion de leur état.
Pour afficher un modèle, définissez d'abord sa source et son état à l'aide de
rememberSpatialGltfModelState. Vous pouvez charger
des modèles à partir du dossier assets de votre application, d'un URI ou de
raw data.
val modelState = rememberSpatialGltfModelState( source = SpatialGltfModelSource.fromPath( Paths.get("models/model_name.glb") ) )
Une fois l'état défini, utilisez le composable SpatialGltfModel pour le rendre dans un sous-espace.
SpatialGltfModel(state = modelState, modifier = SubspaceModifier)
Points clés concernant le code
- Chargement asynchrone : le modèle est chargé de manière asynchrone. Lors de la composition initiale, sa taille intrinsèque peut être nulle. La mise en page est remesurée une fois le modèle prêt.
- Contrôle de l'état : utilisez
SpatialGltfModelState.statuspour interroger l'état de chargement ou contrôler les animations. - Dimensionnement et mise à l'échelle : par défaut, la taille de la mise en page correspond au cadre de délimitation de l'élément. Vous pouvez remplacer ce comportement par un
SubspaceModifier.sizepour mettre le modèle à l'échelle de manière uniforme afin qu'il s'adapte aux limites spécifiées.
Utiliser une SceneCoreEntity pour placer des entités dans votre mise en page
Le SceneCoreEntity composable relie les bibliothèques Jetpack
SceneCore et Compose pour XR afin que vous puissiez utiliser
des entités créées avec SceneCore dans des mises en page Compose. Cela vous permet de créer des entités de niveau inférieur et des composants personnalisés tout en permettant à Compose de dimensionner, de positionner, de reparenter, d'ajouter des enfants et d'appliquer des modificateurs à ces entités.
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. } }
Points clés concernant le code
- Bloc d'usine : le bloc d'usine est l'endroit où vous initialisez l'entité
SceneCoresous-jacente. - Bloc de mise à jour : utilisez le bloc de mise à jour pour modifier les propriétés de l'entité en réponse aux modifications apportées à votre état Compose.
- Adaptation de la taille : le
sizeAdaptercommunique les dimensions de l'entité au système de mise en page Compose.
Informations supplémentaires
- Pour mieux comprendre comment charger du contenu 3D
dans une
SceneCoreEntity, consultez la section Ajouter des modèles 3D à votre application.
Ajouter une surface pour le contenu image ou vidéo
Un SpatialExternalSurface est un composable de sous-espace qui crée et
gère la Surface dans laquelle votre application peut dessiner du contenu, tel qu'une
image ou vidéo. SpatialExternalSurface est compatible avec le contenu stéréoscopique ou monoscopique.
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
StereoModesurMono,SideBySide, ouTopBottomen fonction du type de contenu que vous affichez :Mono: l'image ou la trame vidéo se compose d'une seule image identique affichée aux deux yeux.SideBySide: l'image ou la trame vidéo contient une paire d'images ou de trames vidéo disposées côte à côte, où l'image ou la trame de gauche représente la vue de l'œil gauche, et l'image ou la trame de droite représente la vue de l'œil droit.TopBottom: l'image ou la trame vidéo contient une paire d'images ou de trames vidéo empilées verticalement, où l'image ou la trame du haut représente la vue de l'œil gauche, et l'image ou la trame du bas représente la vue de l'œil droit.
SpatialExternalSurfacen'est compatible qu'avec les surfaces rectangulaires.- Cette
Surfacene capture pas les événements d'entrée. - Il n'est pas possible de synchroniser les modifications
StereoModeavec le rendu de l'application ou le décodage vidéo. - Ce composable ne peut pas être rendu devant d'autres panneaux. Vous ne devez donc pas utiliser de
MovePolicysi d'autres panneaux sont présents 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 activer cette fonctionnalité, vous devez créer une surface sécurisée qui s'affiche dans des tampons graphiques protégés. Cela empêche l'enregistrement de l'écran ou l'accès au contenu par des composants système non sécurisés.
Pour créer une surface sécurisée, définissez le surfaceProtection paramètre sur
SurfaceProtection.Protected dans le SpatialExternalSurface composable.
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
- Surface protégée : il est essentiel de définir
surfaceProtection = SurfaceProtection.ProtectedsurSpatialExternalSurfaceafin que laSurfacesous-jacente soit soutenue par des tampons sécurisés adaptés au contenu DRM. - Configuration DRM : vous devez configurer le
MediaItemavec le schéma DRM (par exemple,C.WIDEVINE_UUID) et l'URI de votre serveur de licences. ExoPlayer utilise ces informations pour gérer la session DRM. - Contenu sécurisé : lors du rendu sur une surface protégée, le contenu vidéo 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 spatiale peuvent être placés n'importe où dans la hiérarchie d'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 suivants d'UI spatiale pour mieux comprendre comment utiliser ces éléments.
Composant d'UI |
Lorsque la spatialisation est activée |
Dans un environnement 2D |
|---|---|---|
|
Le panneau recule légèrement en profondeur Z pour afficher une boîte de dialogue surélevée. |
Revient à |
|
Le panneau recule légèrement en profondeur Z pour afficher une fenêtre pop-up surélevée. |
Revient à |
|
|
S'affiche 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 apparaît à 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
- Voici un exemple de
SpatialDialog. L'utilisation deSpatialPopupetSpatialElevationest très similaire. Pour en savoir plus, consultez notre documentation de référence de l'API pour plus de détails.
Créer des panneaux et des mises en page personnalisés
Pour créer des panneaux personnalisés qui ne sont pas compatibles avec Compose pour XR, vous pouvez travailler
directement avec PanelEntity instances et le graphe de scène à l'aide des
SceneCore API.
Ancrer des orbiteurs à des panneaux et des mises en page spatiaux
Vous pouvez ancrer un orbiteur à des SpatialPanels et à des composants de mise en page spatiale déclarés 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 au parent le 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 encapsulée dans un
SpatialPanel(comme illustré dans un extrait de code précédent), l'orbiteur s'ancre à ceSpatialPanel. - Dans un
Subspace, l'orbiteur s'ancre à l'entité parente la plus proche, qui est 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, l'orbiteur s'ancre à son entité parente la plus proche. Dans ce cas, l'orbiteur s'ancre en haut de la
SpatialRowdans laquelle il est déclaré. - Les mises en page spatiales telles que
SpatialRow,SpatialColumnetSpatialBoxsont 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
- Ajouter des modèles 3D à votre application
- Développer une UI pour des applications Android basées sur des vues
- Implémenter Material Design pour XR