Mengembangkan UI dengan Jetpack Compose untuk XR

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 Android View yang sudah ada, Anda memiliki beberapa opsi pengembangan. Anda dapat menggunakan API interoperabilitas, menggunakan Compose dan View secara bersamaan, atau bekerja langsung dengan library SceneCore. Lihat panduan kami untuk menggunakan tampilan untuk mengetahui detail selengkapnya.

Tentang subruang dan komponen yang dispatialisasi

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

Tentang subruang

Saat mengembangkan untuk Android XR, Anda harus menambahkan Subspace 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 jika spasialisasi diaktifkan. Di Ruang Rumah atau di perangkat non-XR, kode apa pun dalam subruang tersebut akan diabaikan.

Ada dua cara untuk membuat subruang:

  • Subspace: Composable ini dapat ditempatkan di mana saja dalam hierarki UI aplikasi Anda, sehingga Anda dapat mempertahankan tata letak untuk UI 2D dan spasial tanpa kehilangan konteks antar-file. Hal ini mempermudah berbagi hal-hal seperti arsitektur aplikasi yang ada antara XR dan faktor bentuk lainnya tanpa perlu mengangkat status melalui seluruh hierarki UI atau mendesain ulang aplikasi.
  • ApplicationSubspace: Fungsi ini hanya membuat subruang tingkat aplikasi dan harus ditempatkan di tingkat paling atas dalam hierarki UI spasial aplikasi Anda. ApplicationSubspace merender konten spasial dengan VolumeConstraints opsional. Tidak seperti Subspace, ApplicationSubspace tidak dapat disarangkan dalam Subspace atau ApplicationSubspace lainnya.

Untuk mengetahui informasi selengkapnya, lihat Menambahkan subruang ke aplikasi Anda.

Tentang komponen yang dispatialisasi

Composable subruang: Komponen ini hanya dapat dirender di subruang. Elemen ini harus disertakan dalam Subspace atau setSubspaceContent() sebelum ditempatkan dalam tata letak 2D. SubspaceModifier memungkinkan Anda menambahkan atribut seperti kedalaman, offset, dan pemosisian ke composable subruang Anda.

Komponen spasialisasi lainnya tidak perlu dipanggil di dalam subruang. Elemen 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-nya.

Membuat panel spasial

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

Contoh panel UI spasial

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

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

Poin penting tentang kode

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

Cara kerja pengubah subruang yang dapat dipindahkan

Saat pengguna memindahkan panel dari dirinya, secara default pengubah subruang yang dapat dipindahkan akan menskalakan panel dengan cara yang mirip dengan cara panel diubah ukurannya oleh sistem di ruang beranda. Semua konten turunan mewarisi perilaku ini. Untuk menonaktifkannya, tetapkan parameter scaleWithDistance ke false.

Membuat pengorbit

Orbiter adalah komponen UI spasial. Objek ini dirancang untuk dilampirkan ke panel tata ruang, tata letak, atau entitas lainnya yang sesuai. Orbiter biasanya berisi item tindakan kontekstual dan navigasi yang terkait dengan entitas yang ditambatkan. Misalnya, jika Anda telah membuat panel spasial untuk menampilkan konten video, Anda dapat menambahkan kontrol pemutaran video di dalam pengorbit.

Contoh pengorbit

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

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

Poin penting tentang kode

  • Karena pengorbit 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 pengorbit.

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

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 penting tentang kode

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

Menggunakan volume untuk menempatkan objek 3D dalam tata letak

Untuk menempatkan objek 3D dalam tata letak, Anda harus menggunakan composable subruang yang disebut volume. Berikut contoh cara melakukannya.

Contoh objek 3D dalam tata letak

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

Informasi tambahan

Menambahkan platform untuk konten gambar atau video

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 penting tentang kode

  • Tetapkan StereoMode ke Mono, SideBySide, atau TopBottom, bergantung pada jenis konten yang Anda render:
    • Mono: Bingkai 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 disusun 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.
  • Surface ini tidak merekam peristiwa input.
  • Anda tidak dapat menyinkronkan perubahan StereoMode dengan rendering aplikasi atau decoding video.
  • Composable ini tidak dapat dirender di depan panel lain, jadi Anda tidak boleh menggunakan pengubah yang dapat dipindahkan 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 permukaan aman yang dirender ke buffer grafis yang dilindungi. Tindakan ini mencegah konten direkam layar atau diakses oleh komponen sistem yang tidak aman.

Untuk membuat permukaan yang aman, tetapkan parameter surfaceProtection ke SurfaceProtection.Protected pada composable SpatialExternalSurface. 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 penting tentang kode

  • Surface 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 pemberian 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 Anda. Elemen ini dapat digunakan kembali di UI 2D Anda, dan atribut spasialnya hanya akan terlihat jika kemampuan spasial diaktifkan. Hal ini memungkinkan Anda menambahkan ketinggian pada 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 didorong kembali dalam kedalaman z untuk menampilkan dialog yang ditinggikan

Melakukan fallback ke 2D Dialog.

SpatialPopup

Panel akan sedikit didorong kembali dalam kedalaman z untuk menampilkan pop-up yang lebih tinggi

Melakukan fallback ke Popup 2D.

SpatialElevation

SpatialElevationLevel dapat disetel untuk menambahkan elevasi.

Tampilan 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 didorong kembali sejauh 125 dp saat spasialisasi diaktifkan. SpatialDialog juga dapat digunakan saat spasialisasi tidak diaktifkan, dalam hal ini SpatialDialog akan kembali ke padanannya dalam 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")
                }
            }
        }
    }
}

Poin penting tentang kode

Membuat panel dan tata letak kustom

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

Menyematkan pengorbit ke tata letak spasial dan entitas lainnya

Anda dapat menyematkan pengorbit ke entitas apa pun yang dideklarasikan di Compose. Hal ini melibatkan deklarasi orbiter dalam tata letak spasial elemen UI seperti SpatialRow, SpatialColumn, atau SpatialBox. Pengorbit di-anchor ke entitas induk terdekat dengan tempat Anda mendeklarasikannya.

Perilaku pengorbit ditentukan oleh tempat Anda mendeklarasikannya:

  • Dalam tata letak 2D yang di-wrap dalam SpatialPanel (seperti yang ditunjukkan dalam cuplikan kode sebelumnya), pengorbit di-anchor ke SpatialPanel tersebut.
  • Dalam Subspace, orbiter dikaitkan ke entity induk terdekat, yaitu tata letak spasial tempat orbiter dideklarasikan.

Contoh berikut menunjukkan cara menyematkan pengorbit 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 penting tentang kode

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

Lihat juga