Sviluppare l'interfaccia utente con Jetpack Compose per XR

Con Jetpack Compose per XR, puoi creare in modo dichiarativo la tua UI e il tuo layout spaziali utilizzando concetti di Compose familiari come righe e colonne. In questo modo puoi estendere la tua UI Android esistente nello spazio 3D o creare applicazioni 3D immersive completamente nuove.

Se stai spazializzando un'app esistente basata su Android Views, hai diverse opzioni di sviluppo. Puoi utilizzare le API di interoperabilità, utilizzare Compose e Views insieme o lavorare direttamente con la libreria SceneCore. Per maggiori dettagli, consulta la nostra guida all'utilizzo delle visualizzazioni.

Informazioni su sottospazi e componenti spazializzati

Quando scrivi la tua app per Android XR, è importante comprendere i concetti di sottospazio e componenti spazializzati.

Informazioni sullo spazio secondario

Quando sviluppi per Android XR, devi aggiungere un Subspace alla tua app o al tuo layout. Un sottospazio è una partizione dello spazio 3D all'interno dell'app in cui puoi inserire contenuti 3D, creare layout 3D e aggiungere profondità a contenuti altrimenti 2D. Uno spazio secondario viene visualizzato solo quando la spazializzazione è abilitata. In Home Space o su dispositivi non XR, qualsiasi codice all'interno di questo sottospazio viene ignorato.

Esistono due modi per creare uno spazio:

  • Subspace: questo componente componibile può essere posizionato ovunque nella gerarchia dell'interfaccia utente della tua app, consentendoti di mantenere i layout per l'interfaccia utente 2D e spaziale senza perdere il contesto tra i file. In questo modo è più facile condividere elementi come l'architettura delle app esistenti tra XR e altri fattori di forma senza dover sollevare lo stato attraverso l'intero albero dell'interfaccia utente o riprogettare l'app.
  • ApplicationSubspace: questa funzione crea solo uno spazio secondario a livello di app e deve essere posizionata nel livello più alto della gerarchia dell'interfaccia utente spaziale dell'applicazione. ApplicationSubspace esegue il rendering dei contenuti spaziali con VolumeConstraints facoltativo. A differenza di Subspace, ApplicationSubspace non può essere nidificato all'interno di un altro Subspace o ApplicationSubspace.

Per maggiori informazioni, vedi Aggiungere uno spazio alla tua app.

Informazioni sui componenti spazializzati

Componenti combinabili dello spazio secondario: questi componenti possono essere visualizzati solo in uno spazio secondario. Devono essere racchiusi tra Subspace o setSubspaceContent() prima di essere inseriti in un layout 2D. Un SubspaceModifier ti consente di aggiungere attributi come profondità, offset e posizionamento ai tuoi composable dello spazio secondario.

Gli altri componenti spazializzati non richiedono di essere chiamati all'interno di uno spazio secondario. Sono costituiti da elementi 2D convenzionali racchiusi in un contenitore spaziale. Questi elementi possono essere utilizzati all'interno di layout 2D o 3D, se definiti per entrambi. Quando la spazializzazione non è abilitata, le funzionalità spazializzate verranno ignorate e verranno ripristinate le controparti 2D.

Creare un pannello spaziale

Un SpatialPanel è un componente di sottospazio che ti consente di visualizzare i contenuti dell'app. Ad esempio, puoi visualizzare la riproduzione video, le immagini statiche o qualsiasi altro contenuto in un pannello spaziale.

Esempio di pannello dell'interfaccia utente spaziale

Puoi utilizzare SubspaceModifier per modificare le dimensioni, il comportamento e il posizionamento del riquadro spaziale, come mostrato nell'esempio seguente.

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

Punti chiave del codice

  • Poiché le API SpatialPanel sono composable di sottospazi, devi chiamarle all'interno di Subspace. La chiamata al di fuori di uno spazio secondario genera un'eccezione.
  • Le dimensioni di SpatialPanel sono state impostate utilizzando le specifiche height e width su SubspaceModifier. L'omissione di queste specifiche consente di determinare le dimensioni del riquadro in base alle misurazioni dei suoi contenuti.
  • Consenti all'utente di ridimensionare o spostare il pannello aggiungendo i modificatori movable o resizable.
  • Per informazioni dettagliate su dimensioni e posizionamento, consulta le nostre indicazioni per la progettazione dei riquadri spaziali. Per ulteriori dettagli sull'implementazione del codice, consulta la nostra documentazione di riferimento.

Come funziona un modificatore di sottospazio mobile

Quando un utente allontana un pannello, per impostazione predefinita un modificatore di sottospazio mobile scala il pannello in modo simile a come i pannelli vengono ridimensionati dal sistema nello spazio della casa. Tutti i contenuti per bambini ereditano questo comportamento. Per disattivare questa opzione, imposta il parametro scaleWithDistance su false.

Crea un orbiter

Un orbiter è un componente UI spaziale. È progettato per essere collegato a un pannello spaziale, un layout o un'altra entità corrispondente. Un orbiter in genere contiene elementi di navigazione e azioni contestuali correlate all'entità a cui è ancorato. Ad esempio, se hai creato un pannello spaziale per visualizzare contenuti video, puoi aggiungere i controlli di riproduzione video all'interno di un orbiter.

Esempio di orbiter

Come mostrato nell'esempio seguente, chiama un orbiter all'interno del layout 2D in un SpatialPanel per racchiudere i controlli utente come la navigazione. In questo modo, vengono estratti dal layout 2D e allegati al riquadro spaziale in base alla configurazione.

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

Punti chiave del codice

  • Poiché gli orbiter sono componenti dell'interfaccia utente spaziale, il codice può essere riutilizzato in layout 2D o 3D. In un layout 2D, la tua app esegue il rendering solo dei contenuti all'interno dell'orbiter e ignora l'orbiter stesso.
  • Per saperne di più su come utilizzare e progettare gli orbiter, consulta la nostra guida alla progettazione.

Aggiungere più riquadri spaziali a un layout spaziale

Puoi creare più pannelli spaziali e posizionarli all'interno di un layout spaziale utilizzando SpatialRow, SpatialColumn, SpatialBox e SpatialLayoutSpacer.

Esempio di più riquadri spaziali in un layout spaziale

Il seguente esempio di codice mostra come eseguire questa operazione.

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

Punti chiave del codice

Utilizzare un volume per posizionare un oggetto 3D nel layout

Per inserire un oggetto 3D nel layout, devi utilizzare un componente secondario dello spazio chiamato volume. Ecco un esempio di come farlo.

Esempio di un oggetto 3D in un layout

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

Informazioni aggiuntive

Aggiungere una piattaforma per i contenuti di immagini o video

Un SpatialExternalSurface è un sottospazio componibile che crea e gestisce il Surface in cui la tua app può disegnare contenuti, ad esempio un'immagine o un video. SpatialExternalSurface supporta contenuti stereoscopici o monoscopici.

Questo esempio mostra come caricare un video stereoscopico affiancato utilizzando Media3 Exoplayer e 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() }
        }
    }
}

Punti chiave del codice

  • Imposta StereoMode su Mono, SideBySide o TopBottom a seconda del tipo di contenuti che stai eseguendo il rendering:
    • Mono: L'immagine o il fotogramma video è costituito da un'unica immagine identica mostrata a entrambi gli occhi.
    • SideBySide: l'immagine o il frame video contiene una coppia di immagini o frame video disposti uno accanto all'altro, in cui l'immagine o il frame a sinistra rappresenta la visuale dell'occhio sinistro e l'immagine o il frame a destra rappresenta la visuale dell'occhio destro.
    • TopBottom: L'immagine o il fotogramma del video contiene una coppia di immagini o fotogrammi video impilati verticalmente, in cui l'immagine o il fotogramma in alto rappresenta la visuale dell'occhio sinistro e l'immagine o il fotogramma in basso rappresenta la visuale dell'occhio destro.
  • SpatialExternalSurface supporta solo superfici rettangolari.
  • Questo Surface non acquisisce gli eventi di input.
  • Non è possibile sincronizzare le modifiche di StereoMode con il rendering dell'applicazione o la decodifica video.
  • Questo elemento componibile non può essere visualizzato davanti ad altri pannelli, quindi non devi utilizzare modificatori mobili se nel layout sono presenti altri pannelli.

Aggiungere una piattaforma per contenuti video protetti da DRM

SpatialExternalSurface supporta anche la riproduzione di stream video protetti da DRM. Per abilitare questa funzionalità, devi creare una superficie sicura che esegua il rendering nei buffer grafici protetti. In questo modo, i contenuti non possono essere registrati o accessibili da componenti di sistema non sicuri.

Per creare una superficie sicura, imposta il parametro surfaceProtection su SurfaceProtection.Protected nel composable SpatialExternalSurface. Inoltre, devi configurare Media3 Exoplayer con le informazioni DRM appropriate per gestire l'acquisizione della licenza da un server delle licenze.

L'esempio seguente mostra come configurare SpatialExternalSurface e ExoPlayer per riprodurre un flusso video protetto da 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() }
        }
    }
}

Punti chiave del codice

  • Superficie protetta: l'impostazione surfaceProtection = SurfaceProtection.Protected su SpatialExternalSurface è essenziale per garantire che Surface sia supportato da buffer sicuri adatti ai contenuti DRM.
  • Configurazione DRM: devi configurare MediaItem con lo schema DRM (ad esempio C.WIDEVINE_UUID) e l'URI del server delle licenze. ExoPlayer utilizza queste informazioni per gestire la sessione DRM.
  • Contenuti protetti: durante il rendering su una superficie protetta, i contenuti video vengono decodificati e visualizzati su un percorso sicuro, il che contribuisce a soddisfare i requisiti di licenza dei contenuti. In questo modo, inoltre, i contenuti non vengono visualizzati nelle acquisizioni dello schermo.

Aggiungere altri componenti dell'interfaccia utente spaziale

I componenti dell'interfaccia utente spaziale possono essere posizionati ovunque nella gerarchia dell'interfaccia utente dell'applicazione. Questi elementi possono essere riutilizzati nell'interfaccia utente 2D e i loro attributi spaziali saranno visibili solo quando le funzionalità spaziali sono attive. In questo modo puoi aggiungere l'elevazione a menu, dialoghi e altri componenti senza dover scrivere il codice due volte. Consulta i seguenti esempi di UI spaziale per capire meglio come utilizzare questi elementi.

Componente UI

Quando la spazializzazione è abilitata

In ambiente 2D

SpatialDialog

Il riquadro si sposta leggermente indietro in profondità Z per visualizzare una finestra di dialogo in rilievo

Torna alla modalità 2D Dialog.

SpatialPopup

Il riquadro si sposta leggermente indietro in profondità Z per visualizzare un popup in rilievo

Viene visualizzato un Popup in 2D.

SpatialElevation

SpatialElevationLevel può essere impostato per aggiungere l'elevazione.

Spettacoli senza elevazione spaziale.

SpatialDialog

Questo è un esempio di finestra di dialogo che si apre dopo un breve ritardo. Quando viene utilizzato SpatialDialog, la finestra di dialogo viene visualizzata alla stessa profondità Z del pannello spaziale e il pannello viene spostato indietro di 125 dp quando la spazializzazione è attivata. SpatialDialog può essere utilizzato anche quando la spazializzazione non è attivata, nel qual caso SpatialDialog torna alla sua controparte 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")
                }
            }
        }
    }
}

Punti chiave del codice

Creare pannelli e layout personalizzati

Per creare pannelli personalizzati non supportati da Compose for XR, puoi lavorare direttamente con le istanze PanelEntity e il grafico della scena utilizzando le API SceneCore.

Ancorare gli orbiter a layout spaziali e altre entità

Puoi ancorare un orbiter a qualsiasi entità dichiarata in Compose. Ciò comporta la dichiarazione di un orbiter in un layout spaziale di elementi della UI come SpatialRow, SpatialColumn o SpatialBox. L'orbiter si ancora all'entità principale più vicina al punto in cui l'hai dichiarato.

Il comportamento dell'orbiter è determinato dalla posizione in cui lo dichiari:

  • In un layout 2D racchiuso in un SpatialPanel (come mostrato in uno snippet di codice precedente), l'orbiter si ancora a questo SpatialPanel.
  • In un Subspace, l'orbiter si ancora all'entità padre più vicina, ovvero il layout spaziale in cui è dichiarato l'orbiter.

Il seguente esempio mostra come ancorare un orbiter a una riga spaziale:

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

Punti chiave del codice

  • Quando dichiari un orbiter al di fuori di un layout 2D, l'orbiter viene ancorato all'entità principale più vicina. In questo caso, l'orbiter si ancora alla parte superiore del SpatialRow in cui è dichiarato.
  • I layout spaziali come SpatialRow, SpatialColumn e SpatialBox hanno tutti entità senza contenuti associati. Pertanto, un orbiter dichiarato in un layout spaziale viene ancorato a quel layout.

Vedi anche