Mengembangkan UI spasial dengan Jetpack Compose untuk XR

Perangkat XR yang kompatibel
Panduan ini membantu Anda membangun pengalaman untuk jenis perangkat XR ini.
Headset XR
Kacamata XR Berkabel

Dengan Jetpack Compose untuk XR, Anda dapat membangun UI dan tata letak spasial secara deklaratif menggunakan konsep Compose yang sudah dikenal seperti baris dan kolom. Hal ini memungkinkan Anda memperluas UI Android yang ada ke ruang 3D atau membangun aplikasi 3D imersif yang benar-benar baru.

Jika Anda melakukan spasialisasi aplikasi berbasis View Android yang ada, Anda memiliki beberapa opsi pengembangan. Anda dapat menggunakan API interoperabilitas, menggunakan Compose dan View bersama-sama, atau bekerja langsung dengan library SceneCore. Lihat panduan kami untuk menggunakan tampilan guna mengetahui detail selengkapnya.

Tentang subruang dan komponen spasial

Saat menulis aplikasi untuk Android XR, Anda harus memahami konsep subruang dan komponen spasial.

Tentang subruang

Saat mengembangkan untuk Android XR, Anda harus menambahkan subruang ke aplikasi atau tata letak. Subruang adalah partisi ruang 3D dalam aplikasi Anda tempat Anda dapat menempatkan konten 3D, membangun tata letak 3D, dan menambahkan kedalaman pada konten 2D. Subruang hanya dirender saat spasialisasi diaktifkan. Di Ruang Utama atau di perangkat non-XR, kode apa pun dalam subruang tersebut akan diabaikan.

Ada beberapa cara untuk membuat subruang:

  • Subspace: Composable ini membuat hierarki UI Spasial baru yang independen. Hierarki ini tidak mewarisi posisi spasial, orientasi, atau skala Subspace induk yang disarangkan di dalamnya. Subspace secara otomatis terikat oleh kotak konten yang direkomendasikan sistem.
  • PlanarEmbeddedSubspace: Composable ini dapat ditempatkan dalam hierarki UI aplikasi, sehingga Anda dapat mempertahankan tata letak untuk UI 2D dan spasial. PlanarEmbeddedSubspace menghormati batasan dan posisi induknya. Konten 3D yang ditempatkan di dalamnya kemudian diposisikan relatif terhadap area yang ditentukan 2D ini.

Untuk mengetahui informasi selengkapnya, lihat Menambahkan subruang ke aplikasi.

Tentang komponen spasial

Composable subruang: Komponen ini hanya dapat dirender di subruang. Komponen ini harus dilampirkan dalam Subspace sebelum ditempatkan dalam tata letak 2D. A SubspaceModifier memungkinkan Anda menambahkan atribut seperti kedalaman, offset, dan posisi ke composable subruang.

Komponen spasial lainnya tidak perlu dipanggil di dalam subruang. Komponen ini terdiri dari elemen 2D konvensional yang digabungkan dalam penampung spasial. Elemen ini dapat digunakan dalam tata letak 2D atau 3D jika ditentukan untuk keduanya. Jika spasialisasi tidak diaktifkan, fitur spasialnya akan diabaikan dan akan kembali ke fitur 2D.

Membuat panel spasial

A SpatialPanel adalah composable subruang yang memungkinkan Anda menampilkan konten aplikasi. Misalnya, Anda dapat menampilkan pemutaran video, gambar statis, atau konten lainnya di panel spasial.

Contoh panel UI spasial

Anda dapat menggunakan SubspaceModifier untuk mengubah ukuran, perilaku, dan posisi panel spasial, seperti yang ditunjukkan dalam contoh berikut.

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

Poin utama tentang kode

  • Karena SpatialPanel API adalah composable subruang, Anda harus memanggil nya di dalam Subspace. Memanggilnya di luar subruang akan menampilkan pengecualian.
  • Ukuran SpatialPanel telah ditetapkan menggunakan spesifikasi height dan width pada SubspaceModifier. Jika spesifikasi ini dihilangkan, ukuran panel akan ditentukan oleh pengukuran kontennya.
  • Izinkan pengguna memindahkan panel dengan menambahkan movable pengubah subruang.
  • Izinkan pengguna mengubah ukuran panel dengan menambahkan resizable subruang pengubah.
  • Lihat panduan desain panel spasial kami untuk mengetahui detail tentang ukuran dan posisi. Lihat dokumentasi referensi kami untuk mengetahui detail selengkapnya tentang penerapan kode.

Cara kerja pengubah movable

Saat pengguna memindahkan panel dari mereka, secara default, pengubah movable akan menskalakan panel dengan cara yang mirip dengan cara panel diubah ukurannya oleh sistem di ruang utama. Semua konten turunan mewarisi perilaku ini. Untuk menonaktifkannya, tetapkan parameter shouldScaleWithDistance ke false.

Membuat orbiter

Orbiter adalah komponen UI spasial. Komponen ini didesain untuk dilampirkan ke panel spasial atau komponen tata letak spasial yang sesuai seperti SpatialColumn, SpatialRow, atau SpatialBox. Orbiter biasanya berisi item tindakan kontekstual dan navigasi yang terkait dengan entity yang ditautkan. Misalnya, jika Anda telah membuat panel spasial untuk menampilkan konten video, Anda dapat menambahkan kontrol pemutaran video di dalam orbiter.

Contoh orbiter

Seperti yang ditunjukkan dalam contoh berikut, panggil orbiter di dalam tata letak 2D di SpatialPanel untuk menggabungkan kontrol pengguna seperti navigasi. Dengan melakukannya, kontrol tersebut akan diekstrak dari tata letak 2D dan dilampirkan ke panel spasial sesuai dengan konfigurasi Anda.

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

Poin utama tentang kode

  • Karena orbiter adalah komponen UI spasial, kode dapat digunakan kembali dalam tata letak 2D atau 3D. Dalam tata letak 2D, aplikasi Anda hanya merender konten di dalam orbiter dan mengabaikan orbiter itu sendiri.
  • Lihat panduan desain kami untuk mengetahui informasi selengkapnya tentang cara menggunakan dan mendesain orbiter.

Menambahkan beberapa panel spasial ke tata letak spasial

Anda dapat membuat beberapa panel spasial dan menempatkannya dalam tata letak spasial menggunakan SpatialRow, SpatialColumn, SpatialBox, dan SpatialSpacer.

Contoh beberapa panel spasial dalam tata letak spasial

Contoh kode berikut menunjukkan cara melakukannya.

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

Poin utama tentang kode

  • SpatialRow, SpatialColumn, SpatialBox, dan SpatialSpacer adalah composable subruang dan harus ditempatkan di dalam subruang.
  • Gunakan SubspaceModifier untuk menyesuaikan tata letak.
  • Untuk tata letak dengan beberapa panel dalam satu baris, sebaiknya tetapkan radius kurva 825 dp menggunakan SubspaceModifier agar panel mengelilingi pengguna. Lihat panduan desain kami untuk mengetahui detailnya.

Menambahkan objek 3D ke tata letak menggunakan SpatialGltfModel

Android XR mendukung format glTF untuk model 3D, yang biasanya disimpan sebagai .glb file. Untuk menambahkan objek ini ke tata letak, Anda harus menggunakan composable SpatialGltfModel. API ini menyederhanakan proses pemuatan aset dan pengelolaan statusnya.

Untuk menampilkan model, tentukan terlebih dahulu sumber dan statusnya menggunakan rememberSpatialGltfModelState. Anda dapat memuat model dari folder assets aplikasi, URI, atau raw data.

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

Setelah status ditentukan, gunakan composable SpatialGltfModel untuk merendernya dalam Subruang.

SpatialGltfModel(state = modelState, modifier = SubspaceModifier)

Poin utama tentang kode

  • Pemuatan asinkron: Model dimuat secara asinkron. Selama komposisi awal, ukuran intrinsiknya mungkin nol; tata letak akan diukur ulang setelah model siap.
  • Mengontrol status: Gunakan SpatialGltfModelState.status untuk meminta status pemuatan atau mengontrol animasi.
  • Ukuran dan penskalaan: Secara default, ukuran tata letak cocok dengan kotak pembatas aset. Anda dapat menggantinya dengan SubspaceModifier.size untuk menskalakan model secara seragam agar sesuai dengan batas yang ditentukan.

Menggunakan SceneCoreEntity untuk menempatkan entity dalam tata letak

Composable SceneCoreEntity menjembatani library Jetpack SceneCore dan Compose untuk XR sehingga Anda dapat menggunakan entity yang dibuat dengan SceneCore dalam tata letak Compose. Hal ini memungkinkan Anda membangun entity dan komponen kustom tingkat bawah sekaligus memungkinkan Compose mengubah ukuran, memosisikan, mengubah induk, menambahkan turunan, dan menerapkan pengubah ke entity tersebut.

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

Poin utama tentang kode

  • Blok factory: Blok factory adalah tempat Anda menginisialisasi entity SceneCore yang mendasarinya.
  • Blok update: Gunakan blok update untuk mengubah properti entity sebagai respons terhadap perubahan dalam status Compose Anda.
  • Adaptasi Ukuran: sizeAdapter mengomunikasikan dimensi entity kembali ke sistem tata letak Compose.

Informasi tambahan

Menambahkan platform untuk konten gambar atau video

A SpatialExternalSurface adalah composable subruang yang membuat dan mengelola Surface tempat aplikasi Anda dapat menggambar konten, seperti gambar atau video. SpatialExternalSurface mendukung konten stereoskopik atau monoskopik.

Contoh ini menunjukkan cara memuat video stereoskopik berdampingan menggunakan Media3 Exoplayer dan 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() }
        }
    }
}

Poin utama tentang kode

  • Tetapkan StereoMode ke Mono, SideBySide, atau TopBottom, bergantung pada jenis konten yang Anda render:
    • Mono: Frame gambar atau video terdiri dari satu gambar identik yang ditampilkan ke kedua mata.
    • SideBySide: Frame gambar atau video berisi sepasang gambar atau frame video yang disusun berdampingan, dengan gambar atau frame di sebelah kiri mewakili tampilan mata kiri, dan gambar atau frame di sebelah kanan mewakili tampilan mata kanan.
    • TopBottom: Frame gambar atau video berisi sepasang gambar atau frame video yang ditumpuk secara vertikal, dengan gambar atau frame di bagian atas mewakili tampilan mata kiri, dan gambar atau frame di bagian bawah mewakili tampilan mata kanan.
  • SpatialExternalSurface hanya mendukung platform persegi panjang.
  • Ini Surface tidak mengambil peristiwa input.
  • Perubahan StereoMode tidak dapat disinkronkan dengan rendering aplikasi atau decoding video.
  • Composable ini tidak dapat dirender di depan panel lain, jadi Anda tidak boleh menggunakan MovePolicy jika ada panel lain dalam tata letak.

Menambahkan platform untuk konten video yang dilindungi DRM

SpatialExternalSurface juga mendukung pemutaran streaming video yang dilindungi DRM. Untuk mengaktifkannya, Anda harus membuat platform aman yang dirender ke buffer grafis yang dilindungi. Hal ini mencegah konten direkam layar atau diakses oleh komponen sistem yang tidak aman.

Untuk membuat platform yang aman, tetapkan parameter surfaceProtection ke SurfaceProtection.Protected pada SpatialExternalSurface composable. Selain itu, Anda harus mengonfigurasi Media3 Exoplayer dengan informasi DRM yang sesuai untuk menangani perolehan lisensi dari server lisensi.

Contoh berikut menunjukkan cara mengonfigurasi SpatialExternalSurface dan ExoPlayer untuk memutar streaming video yang dilindungi 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() }
        }
    }
}

Poin utama tentang kode

  • Platform yang Dilindungi: Menetapkan surfaceProtection = SurfaceProtection.Protected pada SpatialExternalSurface sangat penting agar Surface yang mendasarinya didukung oleh buffer aman yang sesuai untuk konten DRM.
  • Konfigurasi DRM: Anda harus mengonfigurasi MediaItem dengan skema DRM (misalnya, C.WIDEVINE_UUID) dan URI server lisensi Anda. ExoPlayer menggunakan informasi ini untuk mengelola sesi DRM.
  • Konten Aman: Saat merender ke platform yang dilindungi, konten video akan didekode dan ditampilkan di jalur yang aman, yang membantu memenuhi persyaratan lisensi konten. Hal ini juga mencegah konten muncul dalam screenshot.

Menambahkan komponen UI spasial lainnya

Komponen UI spasial dapat ditempatkan di mana saja dalam hierarki UI aplikasi. Elemen ini dapat digunakan kembali di UI 2D, dan atribut spasialnya hanya akan terlihat saat kemampuan spasial diaktifkan. Hal ini memungkinkan Anda menambahkan elevasi ke menu, dialog, dan komponen lainnya tanpa perlu menulis kode dua kali. Lihat contoh UI spasial berikut untuk lebih memahami cara menggunakan elemen ini.

Komponen UI

Saat spasialisasi diaktifkan

Di lingkungan 2D

SpatialDialog

Panel akan sedikit mundur dalam kedalaman z untuk menampilkan dialog yang ditinggikan

Kembali ke 2D Dialog.

SpatialPopup

Panel akan sedikit mundur dalam kedalaman z untuk menampilkan pop-up yang ditinggikan

Kembali ke 2D Popup.

SpatialElevation

SpatialElevationLevel dapat ditetapkan untuk menambahkan elevasi.

Ditampilkan tanpa elevasi spasial.

SpatialDialog

Ini adalah contoh dialog yang terbuka setelah penundaan singkat. Saat SpatialDialog digunakan, dialog akan muncul pada kedalaman z yang sama dengan panel spasial, dan panel akan didorong kembali sebesar 125 dp saat spasialisasi diaktifkan. SpatialDialog juga dapat digunakan saat spasialisasi tidak diaktifkan, yang dalam hal ini SpatialDialog akan kembali ke Dialog, yang merupakan padanan 2D-nya.

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

Poin utama tentang kode

Membuat panel dan tata letak kustom

Untuk membuat panel kustom yang tidak didukung oleh Compose untuk XR, Anda dapat bekerja langsung dengan PanelEntity instance dan grafik scene menggunakan SceneCore API.

Menautkan orbiter ke panel dan tata letak spasial

Anda dapat menautkan orbiter ke SpatialPanels dan komponen tata letak spasial yang dideklarasikan di Compose. Hal ini melibatkan deklarasi orbiter dalam tata letak spasial elemen UI seperti SpatialRow, SpatialColumn, atau SpatialBox. Orbiter ditautkan ke induk yang paling dekat dengan tempat Anda mendeklarasikannya.

Perilaku orbiter ditentukan oleh tempat Anda mendeklarasikannya:

  • Dalam tata letak 2D yang digabungkan dalam SpatialPanel (seperti yang ditunjukkan dalam cuplikan kode sebelumnya), orbiter ditautkan ke SpatialPanel tersebut.
  • Dalam Subspace, orbiter ditautkan ke entity induk terdekat, yang merupakan tata letak spasial tempat orbiter dideklarasikan.

Contoh berikut menunjukkan cara menautkan orbiter ke baris spasial:

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

Poin utama tentang kode

  • Saat Anda mendeklarasikan orbiter di luar tata letak 2D, orbiter akan ditautkan ke entity induk terdekat. Dalam hal ini, orbiter ditautkan ke bagian atas SpatialRow tempat orbiter dideklarasikan.
  • Tata letak spasial seperti SpatialRow, SpatialColumn, SpatialBox semuanya memiliki entity tanpa konten yang terkait dengannya. Oleh karena itu, orbiter yang dideklarasikan dalam tata letak spasial ditautkan ke tata letak tersebut.

Lihat juga