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 skalaSubspaceinduk yang disarangkan di dalamnya.Subspacesecara 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.PlanarEmbeddedSubspacemenghormati 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.

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
SpatialPanelAPI adalah composable subruang, Anda harus memanggil nya di dalamSubspace. Memanggilnya di luar subruang akan menampilkan pengecualian. - Ukuran
SpatialPaneltelah ditetapkan menggunakan spesifikasiheightdanwidthpadaSubspaceModifier. Jika spesifikasi ini dihilangkan, ukuran panel akan ditentukan oleh pengukuran kontennya. - Izinkan pengguna memindahkan panel dengan menambahkan
movablepengubah subruang. - Izinkan pengguna mengubah ukuran panel dengan menambahkan
resizablesubruang 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.

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 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, danSpatialSpaceradalah composable subruang dan harus ditempatkan di dalam subruang.- Gunakan
SubspaceModifieruntuk menyesuaikan tata letak. - Untuk tata letak dengan beberapa panel dalam satu baris, sebaiknya tetapkan radius kurva 825 dp menggunakan
SubspaceModifieragar 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.statusuntuk meminta status pemuatan atau mengontrol animasi. - Ukuran dan penskalaan: Secara default, ukuran tata letak cocok dengan kotak pembatas aset. Anda dapat menggantinya dengan
SubspaceModifier.sizeuntuk 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
SceneCoreyang mendasarinya. - Blok update: Gunakan blok update untuk mengubah properti entity sebagai respons terhadap perubahan dalam status Compose Anda.
- Adaptasi Ukuran:
sizeAdaptermengomunikasikan dimensi entity kembali ke sistem tata letak Compose.
Informasi tambahan
- Lihat Menambahkan model 3D ke aplikasi untuk lebih memahami cara memuat konten 3D
dalam
SceneCoreEntity.
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
StereoModekeMono,SideBySide, atauTopBottom, 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.
SpatialExternalSurfacehanya mendukung platform persegi panjang.- Ini
Surfacetidak mengambil peristiwa input. - Perubahan
StereoModetidak dapat disinkronkan dengan rendering aplikasi atau decoding video. - Composable ini tidak dapat dirender di depan panel lain, jadi Anda tidak boleh menggunakan
MovePolicyjika 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.ProtectedpadaSpatialExternalSurfacesangat penting agarSurfaceyang mendasarinya didukung oleh buffer aman yang sesuai untuk konten DRM. - Konfigurasi DRM: Anda harus mengonfigurasi
MediaItemdengan 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 |
|---|---|---|
|
Panel akan sedikit mundur dalam kedalaman z untuk menampilkan dialog yang ditinggikan |
Kembali ke 2D |
|
Panel akan sedikit mundur dalam kedalaman z untuk menampilkan pop-up yang ditinggikan |
Kembali ke 2D |
|
|
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
- Ini adalah contoh
SpatialDialog. PenggunaanSpatialPopupdanSpatialElevationsangat mirip. Lihat referensi API kami untuk mengetahui detail selengkapnya.
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 keSpatialPaneltersebut. - 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
SpatialRowtempat orbiter dideklarasikan. - Tata letak spasial seperti
SpatialRow,SpatialColumn,SpatialBoxsemuanya memiliki entity tanpa konten yang terkait dengannya. Oleh karena itu, orbiter yang dideklarasikan dalam tata letak spasial ditautkan ke tata letak tersebut.
Lihat juga
- Menambahkan model 3D ke aplikasi
- Mengembangkan UI untuk Aplikasi berbasis View Android
- Mengimplementasikan Desain Material untuk XR