Tworzenie interfejsu użytkownika za pomocą Jetpack Compose na potrzeby XR

Jetpack Compose na potrzeby XR umożliwia deklaratywne tworzenie interfejsu przestrzennego i układu za pomocą znanych koncepcji Compose, takich jak wiersze i kolumny. Dzięki temu możesz rozszerzyć istniejący interfejs Androida na przestrzeń 3D lub tworzyć zupełnie nowe aplikacje 3D z elementami immersyjnymi.

Jeśli chcesz przestrzennie zlokalizować istniejącą aplikację opartą na widokach Androida, masz kilka opcji programowania. Możesz używać interfejsów API interoperacyjności, korzystać jednocześnie z Compose i widoków lub pracować bezpośrednio z biblioteką SceneCore. Więcej informacji znajdziesz w naszym przewodniku po pracy z widokami.

Informacje o podprzestrzeniach i komponentach przestrzennych

Podczas pisania aplikacji na Androida XR ważne jest, aby zrozumieć koncepcje podprzestrzeniskładników przestrzennych.

Informacje o podprzestrzeni

Podczas tworzenia aplikacji na Androida XR musisz dodać do niej lub do układu Subspace. 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 domowej lub na urządzeniach innych niż XR każdy kod w tej podprzestrzeni jest ignorowany.

Podprzestrzeń możesz utworzyć na 2 sposoby:

  • Subspace: ten komponent można umieścić w dowolnym miejscu w hierarchii interfejsu aplikacji, co pozwala zachować układy interfejsu 2D i przestrzennego bez utraty kontekstu między plikami. Ułatwia to udostępnianie elementów takich jak istniejąca architektura aplikacji między XR a innymi formatami bez konieczności przenoszenia stanu przez całe drzewo interfejsu lub przebudowywania aplikacji.
  • ApplicationSubspace: ta funkcja tworzy podprzestrzeń tylko na poziomie aplikacji i musi być umieszczona na najwyższym poziomie w hierarchii przestrzennego interfejsu użytkownika aplikacji. ApplicationSubspace renderuje treści przestrzenne z opcjonalnym VolumeConstraints. W przeciwieństwie do elementu Subspace element ApplicationSubspace nie może być zagnieżdżony w innym elemencie Subspace ani ApplicationSubspace.

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

Informacje o komponentach przestrzennych

Komponenty Subspace: te komponenty mogą być renderowane tylko w przestrzeni podrzędnej. Przed umieszczeniem w układzie 2D muszą być zamknięte w tagach Subspace lub setSubspaceContent(). SubspaceModifier umożliwia dodawanie atrybutów, takich jak głębokość, przesunięcie i pozycjonowanie, do funkcji kompozycyjnych podprzestrzeni.

Inne komponenty przestrzenne nie wymagają wywoływania w podprzestrzeni. Składają się one z tradycyjnych elementów 2D umieszczonych w kontenerze przestrzennym. Jeśli są zdefiniowane dla obu typów, można ich używać w układach 2D i 3D. Jeśli przestrzenność nie jest włączona, funkcje przestrzenne zostaną zignorowane i zastąpione odpowiednikami 2D.

Tworzenie panelu przestrzennego

SpatialPanel to komponent przestrzenny, który umożliwia wyświetlanie treści aplikacji, np. odtwarzanie filmów, wyświetlanie zdjęć lub innych treści w panelu przestrzennym.

Przykład panelu interfejsu przestrzennego

Za pomocą parametru SubspaceModifier możesz zmienić rozmiar, działanie i pozycję panelu przestrzennego, jak pokazano w tym przykładzie.

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

Najważniejsze informacje o kodzie

  • Ponieważ interfejsy API SpatialPanel są funkcjami kompozycyjnymi przestrzeni podrzędnej, musisz wywoływać je w funkcji Subspace. Wywołanie ich poza podprzestrzenią powoduje zgłoszenie wyjątku.
  • Rozmiar elementu SpatialPanel został określony za pomocą specyfikacji heightwidth w elemencie SubspaceModifier. Pominięcie tych specyfikacji pozwala określić rozmiar panelu na podstawie wymiarów jego zawartości.
  • Zezwalaj użytkownikom na zmianę rozmiaru lub przenoszenie panelu, dodając modyfikatory movable lub resizable.
  • Szczegółowe informacje o rozmiarach i położeniu znajdziesz w naszych wskazówkach dotyczących projektowania paneli przestrzennych. Więcej informacji o implementacji kodu znajdziesz w naszej dokumentacji referencyjnej.

Jak działa modyfikator ruchomej podprzestrzeni

Gdy użytkownik odsuwa panel od siebie, modyfikator przestrzeni ruchomej domyślnie skaluje go w podobny sposób, jak system zmienia rozmiar paneli w przestrzeni domowej. Wszystkie treści dla dzieci dziedziczą to zachowanie. Aby wyłączyć tę funkcję, ustaw parametr scaleWithDistance na false.

Tworzenie satelity

Orbiter to przestrzenny komponent interfejsu. Jest przeznaczony do dołączania do odpowiedniego panelu przestrzennego, układu lub innego elementu. Orbiter zwykle zawiera elementy nawigacyjne i kontekstowe związane z podmiotem, do którego jest przypięty. Jeśli na przykład utworzysz panel przestrzenny do wyświetlania treści wideo, możesz dodać elementy sterujące odtwarzaniem wideo w orbiterze.

Przykład orbitera

Jak pokazano w przykładzie poniżej, wywołaj orbiter w układzie 2D w 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 konfiguracją.

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

Najważniejsze informacje o kodzie

  • Orbiter to przestrzenny komponent interfejsu, więc kod można ponownie wykorzystać w układach 2D lub 3D. W układzie 2D aplikacja renderuje tylko treści w orbiterze i ignoruje sam orbiter.
  • Więcej informacji o korzystaniu z orbiterów i ich projektowaniu 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ą funkcji SpatialRow, SpatialColumn, SpatialBoxSpatialLayoutSpacer.

Przykład wielu paneli przestrzennych w układzie przestrzennym

Poniższy przykład kodu pokazuje, 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

Umieszczanie obiektu 3D w układzie za pomocą woluminu

Aby umieścić obiekt 3D w układzie, musisz użyć komponentu podprzestrzeni o nazwie „volume”. Oto przykład, jak to zrobić.

Przykład obiektu 3D w układzie

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

Dodatkowe informacje

Dodawanie powierzchni dla treści obrazu lub filmu

SpatialExternalSurface to komponent podrzędny, który tworzy i zarządza Surface, w 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 wczytać stereoskopowy film side-by-side za pomocą Media3 Exoplayera 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 wartość StereoMode na Mono, SideBySide lub TopBottom w zależności od typu renderowanych treści:
    • Mono: obraz lub klatka filmu składa się z jednego, identycznego obrazu wyświetlanego w obu oczach.
    • SideBySide: obraz lub klatka filmu zawiera parę obrazów lub klatek filmu ułożonych obok siebie, gdzie obraz lub klatka po lewej stronie przedstawia widok z lewego oka, a obraz lub klatka po prawej stronie przedstawia widok z prawego oka.
    • TopBottom: obraz lub klatka filmu zawiera parę obrazów lub klatek filmu ułożonych pionowo, gdzie obraz lub klatka u góry przedstawia widok z lewego oka, a obraz lub klatka u dołu przedstawia widok z prawego oka.
  • SpatialExternalSurface obsługuje tylko prostokątne powierzchnie.
  • Ten Surface nie rejestruje zdarzeń wprowadzania danych.
  • Nie można zsynchronizować zmian StereoMode z renderowaniem aplikacji ani dekodowaniem wideo.
  • Ten komponent nie może być renderowany przed innymi panelami, więc nie należy używać modyfikatorów, które można przesuwać, jeśli w układzie są inne panele.

Dodawanie platformy dla treści wideo chronionych DRM

SpatialExternalSurface obsługuje też odtwarzanie strumieni wideo chronionych przez DRM. Aby to zrobić, musisz utworzyć bezpieczną powierzchnię, która renderuje do chronionych buforów graficznych. Zapobiega to nagrywaniu ekranu z treściami lub uzyskiwaniu do nich dostępu przez niezabezpieczone komponenty systemu.

Aby utworzyć bezpieczną powierzchnię, ustaw parametr surfaceProtection na wartość SurfaceProtection.Protected w komponencie SpatialExternalSurface. Musisz też skonfigurować Media3 Exoplayer, podając odpowiednie informacje o DRM, aby umożliwić pobieranie licencji z serwera licencji.

Poniższy przykład pokazuje, jak skonfigurować SpatialExternalSurfaceExoPlayer, aby odtwarzać strumień wideo chroniony 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

  • Protected Surface: ustawienie surfaceProtection = SurfaceProtection.Protected wartości SpatialExternalSurface jest niezbędne, aby warstwa bazowa Surface była obsługiwana 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 na bezpiecznej ścieżce, co pomaga spełnić wymagania licencyjne dotyczące treści. Zapobiega to również wyświetlaniu treści na zrzutach ekranu.

Dodawanie innych komponentów interfejsu przestrzennego

Komponenty interfejsu przestrzennego można umieścić w dowolnym miejscu w 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. Dzięki temu możesz dodawać do menu, okien i innych komponentów efekt podniesienia bez konieczności dwukrotnego pisania kodu. Aby lepiej zrozumieć, jak korzystać z tych elementów, zapoznaj się z poniższymi przykładami interfejsu przestrzennego.

Komponent interfejsu

Gdy przestrzenność jest włączona

W środowisku 2D

SpatialDialog

Panel lekko się cofnie w głąb, aby wyświetlić okno dialogowe.

Wracasz do widoku 2D Dialog.

SpatialPopup

Panel lekko się cofnie w głąb, aby wyświetlić wyskakujące okienko.

Wracasz do 2D Popup.

SpatialElevation

SpatialElevationLevel można ustawić tak, aby dodawać wysokość.

Programy bez przestrzennego podniesienia.

SpatialDialog

To jest przykład okna, które otwiera się po krótkim opóźnieniu. Gdy używana jest wartość SpatialDialog, okno dialogowe pojawia się na tej samej głębokości z jak panel przestrzenny, a panel jest cofany 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 2-wymiarowego odpowiednika, 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ć niestandardowe panele, które nie są obsługiwane przez Compose for XR, możesz pracować bezpośrednio z instancjami PanelEntity i grafem sceny za pomocą interfejsów API SceneCore.

Przywiązywanie orbiterów do układów przestrzennych i innych elementów

Orbiter możesz przypiąć do dowolnego elementu zadeklarowanego w Compose. Obejmuje to zadeklarowanie orbitera w układzie przestrzennym elementów interfejsu, takich jak SpatialRow, SpatialColumn lub SpatialBox. Orbiter jest przytwierdzony do najbliższego elementu nadrzędnego, w którego pobliżu został zadeklarowany.

Zachowanie orbitera zależy od tego, gdzie go zadeklarujesz:

  • W układzie 2D zawartym w elemencie SpatialPanel (jak pokazano we fragmencie poprzedniego kodu) orbiter jest do niego przytwierdzony.SpatialPanel
  • Subspace orbiter jest przytwierdzony do najbliższego elementu nadrzędnego, czyli układu przestrzennego, w którym jest zadeklarowany.

Poniższy przykład pokazuje, jak przytwierdzić 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 element orbiter poza układem 2D, zostanie on przytwierdzony do najbliższego elementu nadrzędnego. W takim przypadku orbiter jest przytwierdzony do górnej części SpatialRow, w którym jest zadeklarowany.
  • Układy przestrzenne, takie jak SpatialRow, SpatialColumnSpatialBox, mają powiązane z nimi jednostki bez treści. Dlatego orbiter zadeklarowany w układzie przestrzennym jest do niego przypisany.

Zobacz również