Tworzenie przestrzennego interfejsu za pomocą Jetpack Compose na platformę XR

Obsługiwane urządzenia z XR
Te wskazówki pomogą Ci tworzyć aplikacje na te typy urządzeń z XR.
Zestawy słuchawkowe XR
Przewodowe okulary XR

Dzięki Jetpack Compose for XR możesz deklaratywnie tworzyć przestrzenny interfejs i układ za pomocą znanych koncepcji Compose, takich jak wiersze i kolumny. Pozwala to rozszerzyć istniejący interfejs Androida na przestrzeń 3D lub tworzyć zupełnie nowe, wciągające aplikacje 3D.

Jeśli tworzysz aplikację opartą na widokach Androida, możesz skorzystać z kilku opcji. Możesz użyć interfejsów API interoperacyjności, korzystać z Compose i widoków razem lub pracować bezpośrednio z biblioteką SceneCore. Więcej informacji znajdziesz w naszym przewodniku dotyczącym pracy z widokami.

Informacje o podprzestrzeniach i komponentach przestrzennych

Podczas pisania aplikacji na Androida XR ważne jest, aby zrozumieć koncepcje podprzestrzeni i komponentów przestrzennych.

Informacje o podprzestrzeni

Podczas tworzenia aplikacji na Androida XR musisz dodać do niej lub do układu podprzestrzeń. Podprzestrzeń to część przestrzeni 3D w aplikacji, w której możesz umieszczać treści 3D, tworzyć układy 3D i dodawać głębię do treści 2D. Podprzestrzeń jest renderowana tylko wtedy, gdy włączona jest przestrzenność. W przestrzeni głównej lub na urządzeniach innych niż XR kod w tej podprzestrzeni jest ignorowany.

Podprzestrzeń można utworzyć na kilka sposobów:

  • Subspace: ten komponent tworzy nową, niezależną hierarchię przestrzennego interfejsu. Nie dziedziczy pozycji przestrzennej, orientacji ani skali żadnej nadrzędnej podprzestrzeni Subspace, w której jest zagnieżdżony. Subspace jest automatycznie powiązana z zalecanym przez system polem treści.
  • PlanarEmbeddedSubspace: ten komponent można umieścić w hierarchii interfejsu aplikacji, co pozwala zachować układy interfejsu 2D i przestrzennego. PlanarEmbeddedSubspace uwzględnia ograniczenia i pozycjonowanie elementu nadrzędnego. Umieszczone w nim treści 3D są następnie pozycjonowane względem tego obszaru zdefiniowanego w 2D.

Więcej informacji znajdziesz w artykule Dodawanie podprzestrzeni do aplikacji.

Informacje o komponentach przestrzennych

Komponenty podprzestrzeni: te komponenty można renderować tylko w podprzestrzeni. Przed umieszczeniem w układzie 2D muszą być zamknięte w komponencie Subspace. A SubspaceModifier umożliwia dodawanie do komponentów podprzestrzeni atrybutów takich jak głębia, przesunięcie i pozycjonowanie.

Inne komponenty przestrzenne nie wymagają wywoływania w podprzestrzeni. Składają się z konwencjonalnych elementów 2D opakowanych w kontener przestrzenny. Jeśli te elementy są zdefiniowane zarówno dla układów 2D, jak i 3D, można ich używać w obu tych układach. Gdy przestrzenność nie jest włączona, ich funkcje przestrzenne są ignorowane i wracają do swoich odpowiedników 2D.

Tworzenie panelu przestrzennego

SpatialPanel to komponent podprzestrzeni, który umożliwia wyświetlanie treści aplikacji. Możesz na przykład wyświetlać w nim odtwarzanie wideo, obrazy statyczne lub inne treści.

Przykład panelu interfejsu przestrzennego

Za pomocą SubspaceModifier możesz zmieniać rozmiar, zachowanie i pozycjonowanie panelu przestrzennego, jak pokazano w tym przykładzie.

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

Najważniejsze informacje o kodzie

  • Ponieważ SpatialPanel interfejsy API są komponentami podprzestrzeni, musisz wywołać je w komponencie Subspace. Wywołanie ich poza podprzestrzenią powoduje zgłoszenie wyjątku.
  • Rozmiar komponentu SpatialPanel został ustawiony za pomocą specyfikacji height i width w komponencie SubspaceModifier. Pominięcie tych specyfikacji powoduje, że rozmiar panelu jest określany na podstawie wymiarów jego zawartości.
  • Aby umożliwić użytkownikowi przesuwanie panelu, dodaj modyfikator podprzestrzeni movable.
  • Aby umożliwić użytkownikowi zmianę rozmiaru panelu, dodaj resizable modyfikator podprzestrzeni.
  • Szczegółowe informacje o rozmiarach i pozycjonowaniu znajdziesz w naszych wskazówkach dotyczących projektowania paneli przestrzennych. Więcej informacji o implementacji kodu znajdziesz w naszej dokumentacji.

Jak działa modyfikator movable

Gdy użytkownik odsuwa panel od siebie, modyfikator movable domyślnie skaluje go w podobny sposób jak system zmienia rozmiar paneli w przestrzeni głównej. To zachowanie dziedziczą wszystkie treści podrzędne. Aby wyłączyć tę funkcję, ustaw parametr shouldScaleWithDistance na false.

Tworzenie orbitera

Orbiter to komponent przestrzennego interfejsu. Jest on przeznaczony do dołączania do odpowiedniego panelu przestrzennego lub komponentu układu przestrzennego, takiego jak SpatialColumn, SpatialRow czy SpatialBox. Orbiter zwykle zawiera elementy nawigacji i działania kontekstowe związane z encją, do której jest zakotwiczony. Jeśli na przykład utworzysz panel przestrzenny do wyświetlania treści wideo, możesz dodać do orbitera elementy sterujące odtwarzaniem wideo.

Przykład orbitera

Jak pokazano w tym przykładzie, wywołaj orbiter w układzie 2D w komponencie SpatialPanel, aby opakować elementy sterujące użytkownika, takie jak nawigacja. Spowoduje to wyodrębnienie ich z układu 2D i dołączenie do panelu przestrzennego zgodnie z Twoją konfiguracją.

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

Najważniejsze informacje o kodzie

  • Ponieważ orbitery są komponentami przestrzennego interfejsu, kod można ponownie wykorzystać w układach 2D i 3D. W układzie 2D aplikacja renderuje tylko treści w orbiterze i ignoruje sam orbiter.
  • Więcej informacji o tym, jak używać i projektować orbitery, znajdziesz w naszych wskazówkach dotyczących projektowania.

Dodawanie wielu paneli przestrzennych do układu przestrzennego

Możesz utworzyć wiele paneli przestrzennych i umieścić je w układzie przestrzennym za pomocą SpatialRow, SpatialColumn, SpatialBox i SpatialSpacer.

Przykład wielu paneli przestrzennych w układzie przestrzennym

W tym przykładzie kodu pokazujemy, jak to zrobić.

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

Najważniejsze informacje o kodzie

Dodawanie obiektu 3D do układu za pomocą komponentu SpatialGltfModel

Android XR obsługuje format glTF dla modeli 3D, zwykle zapisywanych jako pliki .glb. Aby dodać te obiekty do układu, użyj komponentu SpatialGltfModel. Ten interfejs API upraszcza proces wczytywania komponentów i zarządzania ich stanem.

Aby wyświetlić model, najpierw zdefiniuj jego źródło i stan za pomocą rememberSpatialGltfModelState. Modele możesz wczytywać z folderu assets aplikacji, z URI lub z raw data.

val modelState = rememberSpatialGltfModelState(
    source = SpatialGltfModelSource.fromPath(
        Paths.get("models/model_name.glb")
    )
)

Po zdefiniowaniu stanu użyj komponentu SpatialGltfModel, aby wyrenderować go w podprzestrzeni.

SpatialGltfModel(state = modelState, modifier = SubspaceModifier)

Najważniejsze informacje o kodzie

  • Wczytywanie asynchroniczne: model jest wczytywany asynchronicznie. Podczas początkowej kompozycji jego rozmiar wewnętrzny może wynosić zero. Układ jest ponownie mierzony, gdy model jest gotowy.
  • Kontrolowanie stanu: użyj SpatialGltfModelState.status, aby sprawdzić stan wczytywania lub sterować animacjami.
  • Rozmiar i skalowanie: domyślnie rozmiar układu jest zgodny z ramką ograniczającą komponentu. Możesz to zmienić za pomocą komponentu SubspaceModifier.size, aby równomiernie skalować model tak, aby mieścił się w określonych granicach.

Używanie komponentu SceneCoreEntity do umieszczania encji w układzie

Komponent SceneCoreEntity łączy biblioteki Jetpack SceneCore i Compose for XR, dzięki czemu możesz używać encji utworzonych za pomocą SceneCore w układach Compose. Pozwala to tworzyć encje niższego poziomu i komponenty niestandardowe, a jednocześnie umożliwia Compose zmianę rozmiaru, położenia, rodzica, dodawanie elementów podrzędnych i stosowanie modyfikatorów do tych encji.

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

Najważniejsze informacje o kodzie

  • Blok fabryczny: w bloku fabrycznym inicjujesz podstawową encję SceneCore.
  • Blok aktualizacji: użyj bloku aktualizacji, aby modyfikować właściwości encji w odpowiedzi na zmiany stanu Compose.
  • Dopasowanie rozmiaru: komponent sizeAdapter przekazuje wymiary encji do systemu układu Compose.

Dodatkowe informacje

Dodawanie powierzchni dla treści graficznych lub wideo

SpatialExternalSurface to komponent podprzestrzeni, który tworzy i zarządza komponentem Surface, na którym aplikacja może rysować treści, takie jak obraz lub film. SpatialExternalSurface obsługuje treści stereoskopowe i monoskopowe.

Ten przykład pokazuje, jak wczytywać stereoskopowe wideo obok siebie za pomocą Media3 Exoplayer i 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() }
        }
    }
}

Najważniejsze informacje o kodzie

  • Ustaw StereoMode na Mono, SideBySide, lub TopBottom w zależności od typu renderowanych treści:
    • Mono: klatka obrazu lub filmu składa się z pojedynczego, identycznego obrazu wyświetlanego na obu oczach.
    • SideBySide: klatka obrazu lub filmu zawiera parę obrazów lub klatek wideo ułożonych obok siebie, gdzie obraz lub klatka po lewej stronie reprezentuje widok lewego oka, a obraz lub klatka po prawej stronie – widok prawego oka.
    • TopBottom: klatka obrazu lub filmu zawiera parę obrazów lub klatek wideo ułożonych pionowo, gdzie obraz lub klatka u góry reprezentuje widok lewego oka, a obraz lub klatka u dołu – widok prawego oka.
  • SpatialExternalSurface obsługuje tylko powierzchnie prostokątne.
  • Ten Surface nie przechwytuje zdarzeń wejściowych.
  • Nie można zsynchronizować StereoMode zmian z renderowaniem aplikacji ani dekodowaniem wideo.
  • Ten komponent nie może być renderowany przed innymi panelami, więc nie należy używać komponentu MovePolicy, jeśli w układzie znajdują się inne panele.

Dodawanie powierzchni dla treści wideo chronionych przez DRM

SpatialExternalSurface obsługuje też odtwarzanie strumieni wideo chronionych przez DRM. Aby to włączyć, musisz utworzyć bezpieczną powierzchnię, która renderuje do chronionych buforów graficznych. Uniemożliwia to nagrywanie ekranu i dostęp do treści przez niezabezpieczone komponenty systemu.

Aby utworzyć bezpieczną powierzchnię, ustaw parametr surfaceProtection na SurfaceProtection.Protected w komponencie SpatialExternalSurface. Dodatkowo musisz skonfigurować Media3 Exoplayer z odpowiednimi informacjami o DRM, aby obsługiwać pobieranie licencji z serwera licencji.

Ten przykład pokazuje, jak skonfigurować SpatialExternalSurface i ExoPlayer do odtwarzania strumienia wideo chronionego przez 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() }
        }
    }
}

Najważniejsze informacje o kodzie

  • Chroniona powierzchnia: ustawienie surfaceProtection = SurfaceProtection.Protected w komponencie SpatialExternalSurface jest niezbędne, aby podstawowy komponent Surface był obsługiwany przez bezpieczne bufory odpowiednie dla treści DRM.
  • Konfiguracja DRM: musisz skonfigurować MediaItem ze schematem DRM (np. C.WIDEVINE_UUID) i adresem URI serwera licencji. ExoPlayer używa tych informacji do zarządzania sesją DRM.
  • Bezpieczne treści: podczas renderowania na chronionej powierzchni treści wideo są dekodowane i wyświetlane w bezpieczny sposób, co pomaga spełnić wymagania dotyczące licencjonowania treści. Uniemożliwia to też wyświetlanie treści na zrzutach ekranu.

Dodawanie innych komponentów przestrzennego interfejsu

Komponenty przestrzennego interfejsu można umieszczać w dowolnym miejscu hierarchii interfejsu aplikacji. Te elementy można ponownie wykorzystać w interfejsie 2D, a ich atrybuty przestrzenne będą widoczne tylko wtedy, gdy włączone są funkcje przestrzenne. Pozwala to dodawać wysokość do menu, okien i innych komponentów bez konieczności dwukrotnego pisania kodu. Aby lepiej zrozumieć, jak używać tych elementów, zapoznaj się z tymi przykładami przestrzennego interfejsu.

Komponent interfejsu

Gdy włączona jest przestrzenność

W środowisku 2D

SpatialDialog

Panel zostanie lekko przesunięty do tyłu w głąb osi Z, aby wyświetlić podniesione okno.

Wracamy do komponentu w 2D Dialog.

SpatialPopup

Panel zostanie lekko przesunięty do tyłu w głąb osi Z, aby wyświetlić podniesione okno.

Wracamy do komponentu w 2D Popup.

SpatialElevation

Aby dodać wysokość, możesz ustawić SpatialElevationLevel.

Wyświetla się bez wysokości przestrzennej.

SpatialDialog

To jest przykład okna, które otwiera się po krótkim opóźnieniu. Gdy SpatialDialog jest używany, okno pojawia się na tej samej głębokości osi Z co panel przestrzenny, a panel jest przesuwany do tyłu o 125 dp, gdy włączona jest przestrzenność. SpatialDialog można też używać, gdy przestrzenność nie jest włączona. W takim przypadku SpatialDialog wraca do swojego odpowiednika 2D, czyli 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")
                }
            }
        }
    }
}

Najważniejsze informacje o kodzie

Tworzenie paneli i układów niestandardowych

Aby tworzyć panele niestandardowe, które nie są obsługiwane przez Compose for XR, możesz pracować bezpośrednio z PanelEntity instancjami i wykresem sceny za pomocą SceneCore interfejsów API.

Kotwiczenie orbiterów do paneli i układów przestrzennych

Możesz zakotwiczyć orbiter do komponentów SpatialPanels i układu przestrzennego zadeklarowanych w Compose. W tym celu zadeklaruj orbiter w układzie przestrzennym elementów interfejsu, takich jak SpatialRow, SpatialColumn czy SpatialBox. Orbiter kotwiczy się do najbliższego elementu nadrzędnego w miejscu, w którym został zadeklarowany.

Zachowanie orbitera zależy od miejsca, w którym go zadeklarujesz:

  • W układzie 2D opakowanym w SpatialPanel (jak pokazano w poprzednim fragmencie kodu) orbiter kotwiczy się do tego SpatialPanel.
  • W komponencie Subspace orbiter kotwiczy się do najbliższej encji nadrzędnej, czyli do układu przestrzennego, w którym jest zadeklarowany.

Ten przykład pokazuje, jak zakotwiczyć orbiter do wiersza przestrzennego:

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

Najważniejsze informacje o kodzie

  • Gdy zadeklarujesz orbiter poza układem 2D, orbiter kotwiczy się do najbliższej encji nadrzędnej. W tym przypadku orbiter kotwiczy się u góry komponentu SpatialRow, w którym jest zadeklarowany.
  • Układy przestrzenne, takie jak SpatialRow, SpatialColumn i SpatialBox, mają powiązane z nimi encje bez treści. Dlatego orbiter zadeklarowany w układzie przestrzennym kotwiczy się do tego układu.

Zobacz też