Benutzeroberfläche mit Jetpack Compose for XR entwickeln

Mit Jetpack Compose für XR können Sie Ihre räumliche Benutzeroberfläche und Ihr Layout deklarativ mit bekannten Compose-Konzepten wie Zeilen und Spalten erstellen. So können Sie Ihre bestehende Android-Benutzeroberfläche in den 3D-Raum erweitern oder komplett neue immersive 3D-Anwendungen erstellen.

Wenn Sie eine vorhandene Android Views-basierte App räumlich gestalten möchten, haben Sie mehrere Entwicklungsoptionen. Sie können Interoperabilitäts-APIs verwenden, Compose und Views zusammen nutzen oder direkt mit der SceneCore-Bibliothek arbeiten. Weitere Informationen finden Sie in unserem Leitfaden zum Arbeiten mit Ansichten.

Unterbereiche und räumlich dargestellte Komponenten

Wenn Sie Ihre App für Android XR entwickeln, ist es wichtig, die Konzepte von Unterraum und räumlich gerenderten Komponenten zu verstehen.

Unterbereich

Wenn Sie für Android XR entwickeln, müssen Sie Ihrer App oder Ihrem Layout ein Subspace hinzufügen. Ein Unterbereich ist eine Partition des 3D-Raums in Ihrer App, in der Sie 3D-Inhalte platzieren, 3D-Layouts erstellen und 2D-Inhalten Tiefe verleihen können. Ein Unterraum wird nur gerendert, wenn die Räumlichkeit aktiviert ist. Im Home Space oder auf Nicht-XR-Geräten wird jeglicher Code in diesem Unterbereich ignoriert.

Es gibt zwei Möglichkeiten, einen Unterbereich zu erstellen:

  • Subspace: Diese Composable-Funktion kann an beliebiger Stelle in der UI-Hierarchie Ihrer App platziert werden. So können Sie Layouts für 2D- und räumliche UIs beibehalten, ohne den Kontext zwischen Dateien zu verlieren. So können Sie beispielsweise die vorhandene App-Architektur einfacher zwischen XR und anderen Formfaktoren teilen, ohne den Status durch den gesamten UI-Baum übertragen oder Ihre App neu gestalten zu müssen.
  • ApplicationSubspace: Mit dieser Funktion wird nur ein Unterbereich auf App-Ebene erstellt. Sie muss auf der obersten Ebene in der räumlichen UI-Hierarchie Ihrer Anwendung platziert werden. ApplicationSubspace rendert räumliche Inhalte mit optionalem VolumeConstraints. Im Gegensatz zu Subspace kann ApplicationSubspace nicht in einem anderen Subspace oder ApplicationSubspace verschachtelt werden.

Weitere Informationen finden Sie unter Unterbereich zu Ihrer App hinzufügen.

Räumliche Komponenten

Subspace-Composables: Diese Komponenten können nur in einem Subspace gerendert werden. Sie müssen in Subspace oder setSubspaceContent() eingeschlossen werden, bevor sie in einem 2D-Layout platziert werden. Mit einem SubspaceModifier können Sie Ihren Subspace-Composables Attribute wie Tiefe, Versatz und Positionierung hinzufügen.

Andere räumlich gerenderte Komponenten müssen nicht in einem Unterraum aufgerufen werden. Sie bestehen aus herkömmlichen 2D-Elementen, die in einem räumlichen Container enthalten sind. Diese Elemente können in 2D- oder 3D-Layouts verwendet werden, wenn sie für beide definiert sind. Wenn die Räumlichkeit nicht aktiviert ist, werden die räumlichen Funktionen ignoriert und es wird auf die 2D-Entsprechungen zurückgegriffen.

Räumlichen Bereich erstellen

Ein SpatialPanel ist ein untergeordneter Bereich, der zusammengesetzt werden kann und in dem App-Inhalte angezeigt werden können. Sie können beispielsweise die Videowiedergabe, Standbilder oder andere Inhalte in einem räumlichen Bereich anzeigen.

Beispiel für ein räumliches UI-Feld

Mit SubspaceModifier können Sie die Größe, das Verhalten und die Position des räumlichen Bereichs ändern, wie im folgenden Beispiel gezeigt.

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

Wichtige Punkte zum Code

  • Da SpatialPanel-APIs Subspace-kompatibel sind, müssen Sie sie in Subspace aufrufen. Wenn Sie sie außerhalb eines Unterraums aufrufen, wird eine Ausnahme ausgelöst.
  • Die Größe des SpatialPanel wurde mit den Spezifikationen height und width für das SubspaceModifier festgelegt. Wenn Sie diese Spezifikationen weglassen, wird die Größe des Bereichs durch die Abmessungen des Inhalts bestimmt.
  • Fügen Sie die Modifikatoren movable oder resizable hinzu, damit der Nutzer die Größe des Bereichs ändern oder ihn verschieben kann.
  • Weitere Informationen zu Größe und Positionierung finden Sie in unserem Leitfaden zum Design von 3D-Panels. Weitere Informationen zur Code-Implementierung finden Sie in unserer Referenzdokumentation.

Funktionsweise eines verschiebbaren Unterraum-Modifiers

Wenn ein Nutzer ein Bedienfeld von sich wegbewegt, wird es standardmäßig durch einen verschiebbaren Unterraum-Modifikator ähnlich skaliert wie Bedienfelder, die vom System im Home-Bereich in der Größe angepasst werden. Dieses Verhalten wird auf alle Inhalte für Kinder angewendet. Wenn Sie diese Funktion deaktivieren möchten, setzen Sie den Parameter scaleWithDistance auf false.

Orbiter erstellen

Ein Orbiter ist eine räumliche UI-Komponente. Es ist für die Verknüpfung mit einem entsprechenden räumlichen Bereich, Layout oder einer anderen Einheit konzipiert. Ein Orbiter enthält in der Regel Navigations- und Kontextaktionen, die sich auf die Einheit beziehen, an die er angehängt ist. Wenn Sie beispielsweise ein räumliches Panel zum Anzeigen von Videoinhalten erstellt haben, können Sie in einem Orbiter Steuerelemente für die Videowiedergabe hinzufügen.

Beispiel für einen Orbiter

Wie im folgenden Beispiel gezeigt, rufen Sie einen Orbiter im 2D-Layout in einem SpatialPanel auf, um Nutzersteuerelemente wie die Navigation zu umschließen. Dadurch werden sie aus Ihrem 2D-Layout extrahiert und entsprechend Ihrer Konfiguration an das räumliche Panel angehängt.

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

Wichtige Punkte zum Code

  • Da Orbiters räumliche UI-Komponenten sind, kann der Code in 2D- oder 3D-Layouts wiederverwendet werden. In einem 2D-Layout rendert Ihre App nur die Inhalte im Orbiter und ignoriert den Orbiter selbst.
  • Weitere Informationen zur Verwendung und Gestaltung von Orbitern finden Sie in unseren Designrichtlinien.

Einem räumlichen Layout mehrere räumliche Bereiche hinzufügen

Sie können mehrere räumliche Bereiche erstellen und sie mit SpatialRow, SpatialColumn, SpatialBox und SpatialLayoutSpacer in einem räumlichen Layout platzieren.

Beispiel für mehrere räumliche Bereiche in einem räumlichen Layout

Im folgenden Codebeispiel wird gezeigt, wie Sie dies umsetzen.

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

Wichtige Punkte zum Code

  • SpatialRow, SpatialColumn, SpatialBox und SpatialLayoutSpacer sind alle Subspace-Composables und müssen in einem Subspace platziert werden.
  • Verwenden Sie SubspaceModifier, um das Layout anzupassen.
  • Bei Layouts mit mehreren nebeneinander angeordneten Feldern empfehlen wir, mit einem SubspaceModifier einen Kurvenradius von 825 dp festzulegen, damit die Felder den Nutzer umgeben. Weitere Informationen finden Sie in unseren Designrichtlinien.

Mit einem Volume ein 3D-Objekt in Ihrem Layout platzieren

Wenn Sie ein 3D-Objekt in Ihrem Layout platzieren möchten, müssen Sie eine zusammensetzbare Unterraumfunktion namens „volume“ verwenden. Hier ist ein Beispiel dafür.

Beispiel für ein 3D-Objekt in einem 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) {

Weitere Informationen

Oberfläche für Bild- oder Videoinhalte hinzufügen

Ein SpatialExternalSurface ist eine zusammensetzbare Unterkomponente, die das Surface erstellt und verwaltet, in das Ihre App Inhalte wie ein Bild oder ein Video zeichnen kann. SpatialExternalSurface unterstützt stereoskopische oder monoskopische Inhalte.

In diesem Beispiel wird gezeigt, wie stereoskopische Side-by-Side-Videos mit Media3 ExoPlayer und SpatialExternalSurface geladen werden:

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

Wichtige Punkte zum Code

  • Legen Sie StereoMode je nach Art der gerenderten Inhalte auf Mono, SideBySide oder TopBottom fest:
    • Mono: Das Bild oder der Videoframes besteht aus einem einzelnen, identischen Bild, das für beide Augen angezeigt wird.
    • SideBySide: Das Bild oder der Videoframes enthält ein Paar von Bildern oder Videoframes, die nebeneinander angeordnet sind. Das Bild oder der Frame auf der linken Seite stellt die Ansicht des linken Auges dar und das Bild oder der Frame auf der rechten Seite die Ansicht des rechten Auges.
    • TopBottom: Das Bild oder der Videoframes enthält ein vertikal gestapeltes Bild- oder Videoframes-Paar, wobei das Bild oder der Frame oben die Ansicht des linken Auges und das Bild oder der Frame unten die Ansicht des rechten Auges darstellt.
  • SpatialExternalSurface unterstützt nur rechteckige Oberflächen.
  • Mit diesem Surface werden keine Eingabeereignisse erfasst.
  • Es ist nicht möglich, StereoMode-Änderungen mit dem Rendern von Anwendungen oder der Videodecodierung zu synchronisieren.
  • Diese Composable kann nicht vor anderen Bereichen gerendert werden. Sie sollten also keine verschiebbaren Modifikatoren verwenden, wenn sich andere Bereiche im Layout befinden.

Oberfläche für DRM-geschützte Videoinhalte hinzufügen

SpatialExternalSurface unterstützt auch die Wiedergabe von DRM-geschützten Videostreams. Dazu müssen Sie eine sichere Oberfläche erstellen, die in geschützte Grafikpuffer gerendert wird. Dadurch wird verhindert, dass die Inhalte per Bildschirmaufzeichnung aufgezeichnet oder von nicht sicheren Systemkomponenten aufgerufen werden.

Um eine sichere Oberfläche zu erstellen, legen Sie den Parameter surfaceProtection für die zusammensetzbare Funktion SpatialExternalSurface auf SurfaceProtection.Protected fest. Außerdem müssen Sie Media3 ExoPlayer mit den entsprechenden DRM-Informationen konfigurieren, damit der Lizenzerwerb von einem Lizenzserver abgewickelt werden kann.

Das folgende Beispiel zeigt, wie SpatialExternalSurface und ExoPlayer für die Wiedergabe eines DRM-geschützten Videostreams konfiguriert werden:

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

Wichtige Punkte zum Code

  • Geschützte Oberfläche: Das Festlegen von surfaceProtection = SurfaceProtection.Protected auf SpatialExternalSurface ist wichtig, damit der zugrunde liegende Surface durch sichere Puffer gesichert wird, die für DRM-Inhalte geeignet sind.
  • DRM-Konfiguration: Sie müssen MediaItem mit dem DRM-Schema (z. B. C.WIDEVINE_UUID) und dem URI Ihres Lizenzservers konfigurieren. ExoPlayer verwendet diese Informationen, um die DRM-Sitzung zu verwalten.
  • Sicherer Inhalt: Wenn auf einer geschützten Oberfläche gerendert wird, werden die Videoinhalte auf einem sicheren Pfad decodiert und angezeigt. So können die Anforderungen der Inhaltslizenzierung erfüllt werden. Dadurch wird auch verhindert, dass die Inhalte in Screenshots angezeigt werden.

Andere räumliche UI-Komponenten hinzufügen

Räumliche UI-Komponenten können an beliebiger Stelle in der UI-Hierarchie Ihrer Anwendung platziert werden. Diese Elemente können in Ihrer 2D-Benutzeroberfläche wiederverwendet werden. Ihre räumlichen Attribute sind nur sichtbar, wenn räumliche Funktionen aktiviert sind. So können Sie Menüs, Dialogfeldern und anderen Komponenten eine Erhebung hinzufügen, ohne Ihren Code zweimal schreiben zu müssen. In den folgenden Beispielen für räumliche Benutzeroberflächen sehen Sie, wie Sie diese Elemente verwenden können.

UI-Komponente

Wenn die Räumlichkeit aktiviert ist

In 2D-Umgebung

SpatialDialog

Das Feld wird leicht nach hinten verschoben, um ein Dialogfeld anzuzeigen.

Fällt zurück auf 2D Dialog.

SpatialPopup

Das Feld wird in der Z-Tiefe leicht nach hinten verschoben, um ein erhöhtes Pop-up anzuzeigen.

Es wird auf ein 2D-Popup zurückgegriffen.

SpatialElevation

SpatialElevationLevel kann so eingestellt werden, dass die Höhe hinzugefügt wird.

Shows ohne räumliche Erhebung.

SpatialDialog

Das ist ein Beispiel für ein Dialogfeld, das nach einer kurzen Verzögerung geöffnet wird. Wenn SpatialDialog verwendet wird, wird das Dialogfeld in derselben Z-Tiefe wie das räumliche Feld angezeigt. Das Feld wird um 125 dp nach hinten verschoben, wenn die Räumlichkeit aktiviert ist. SpatialDialog kann auch verwendet werden, wenn die Räumlichkeit nicht aktiviert ist. In diesem Fall wird SpatialDialog auf das 2D-Pendant Dialog zurückgesetzt.

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

Wichtige Punkte zum Code

Benutzerdefinierte Bereiche und Layouts erstellen

Wenn Sie benutzerdefinierte Panels erstellen möchten, die von Compose für XR nicht unterstützt werden, können Sie direkt mit PanelEntity-Instanzen und dem Szenengraphen arbeiten. Verwenden Sie dazu die SceneCore-APIs.

Orbitale an räumliche Layouts und andere Einheiten anpassen

Sie können einen Orbiter an jede in Compose deklarierte Entität anfügen. Dazu müssen Sie einen Orbiter in einem räumlichen Layout von UI-Elementen wie SpatialRow, SpatialColumn oder SpatialBox deklarieren. Der Orbiter wird an der übergeordneten Einheit verankert, die sich am nächsten an der Stelle befindet, an der Sie ihn deklariert haben.

Das Verhalten des Orbiters hängt davon ab, wo Sie ihn deklarieren:

  • In einem 2D-Layout, das in ein SpatialPanel eingebettet ist (wie in einem vorherigen Code-Snippet gezeigt), wird der Orbiter an diesem SpatialPanel verankert.
  • In einem Subspace wird der Orbiter an der nächstgelegenen übergeordneten Einheit verankert, also dem räumlichen Layout, in dem der Orbiter deklariert ist.

Im folgenden Beispiel sehen Sie, wie Sie einen Orbiter an einer räumlichen Zeile verankern:

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

Wichtige Punkte zum Code

  • Wenn Sie einen Orbiter außerhalb eines 2D-Layouts deklarieren, wird er an der nächstgelegenen übergeordneten Entität verankert. In diesem Fall wird der Orbiter oben im SpatialRow verankert, in dem er deklariert ist.
  • Räumliche Layouts wie SpatialRow, SpatialColumn und SpatialBox haben alle inhaltslose Einheiten, die mit ihnen verknüpft sind. Daher wird ein Orbiter, der in einem räumlichen Layout deklariert wird, an dieses Layout angehängt.

Siehe auch